[ACS-10323] a11y fix - Adding/removing favorites are not announced by SR (#5002)

* [ACS-10323] a11y fix - Adding/removing favorites are not announced by screen readers

* fix unit test

* [ACS-10323] cr fixes
This commit is contained in:
Mykyta Maliarchuk
2026-01-26 10:58:50 +01:00
committed by GitHub
parent 715f928b3e
commit 0bc5bcc137
13 changed files with 278 additions and 46 deletions
@@ -566,7 +566,9 @@
"comment": "workaround for Recent Files and Search API issue", "comment": "workaround for Recent Files and Search API issue",
"type": "custom", "type": "custom",
"order": 400, "order": 400,
"data": "['/favorites', '/favorite/libraries']", "data": {
"routes": ["/favorites", "/favorite/libraries"]
},
"component": "app.toolbar.toggleFavorite", "component": "app.toolbar.toggleFavorite",
"rules": { "rules": {
"visible": [ "visible": [
@@ -939,7 +941,10 @@
"comment": "workaround for Recent Files and Search API issue", "comment": "workaround for Recent Files and Search API issue",
"type": "custom", "type": "custom",
"order": 802, "order": 802,
"data": "['/favorites', '/favorite/libraries']", "data": {
"routes": ["/favorites", "/favorite/libraries"],
"focusAfterClosed": ".adf-context-menu-source"
},
"component": "app.toolbar.toggleFavorite", "component": "app.toolbar.toggleFavorite",
"rules": { "rules": {
"visible": [ "visible": [
@@ -953,6 +958,9 @@
"type": "custom", "type": "custom",
"order": 803, "order": 803,
"component": "app.toolbar.toggleFavoriteLibrary", "component": "app.toolbar.toggleFavoriteLibrary",
"data": {
"focusAfterClosed": ".adf-context-menu-source"
},
"rules": { "rules": {
"visible": [ "visible": [
"app.selection.library" "app.selection.library"
@@ -45,7 +45,7 @@
(validationError)="error = $event" (validationError)="error = $event"
[hasLibrariesConstraint]="hasLibrariesConstraint" [hasLibrariesConstraint]="hasLibrariesConstraint"
/> />
<mat-error *ngIf="error" class="app-search-error"> <mat-error *ngIf="error" class="app-search-error" role="alert" aria-live="assertive">
{{ error | translate }} {{ error | translate }}
</mat-error> </mat-error>
<div id="search-options" class="app-search-options"> <div id="search-options" class="app-search-options">
@@ -22,7 +22,7 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>. * from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library.component'; import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library.component';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -30,12 +30,14 @@ import { AppTestingModule } from '../../../testing/app-testing.module';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AppHookService, ContentApiService } from '@alfresco/aca-shared'; import { AppHookService, ContentApiService } from '@alfresco/aca-shared';
import { UnitTestingUtils } from '@alfresco/adf-core';
describe('ToggleFavoriteLibraryComponent', () => { describe('ToggleFavoriteLibraryComponent', () => {
let fixture: ComponentFixture<ToggleFavoriteLibraryComponent>; let fixture: ComponentFixture<ToggleFavoriteLibraryComponent>;
let component: ToggleFavoriteLibraryComponent; let component: ToggleFavoriteLibraryComponent;
let appHookService: AppHookService; let appHookService: AppHookService;
let contentApiService: any; let contentApiService: any;
let unitTestingUtils: UnitTestingUtils;
const selection = { library: { entry: { id: 'libraryId' } } }; const selection = { library: { entry: { id: 'libraryId' } } };
const mockRouter = { const mockRouter = {
@@ -66,6 +68,7 @@ describe('ToggleFavoriteLibraryComponent', () => {
fixture = TestBed.createComponent(ToggleFavoriteLibraryComponent); fixture = TestBed.createComponent(ToggleFavoriteLibraryComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
contentApiService = TestBed.inject(ContentApiService); contentApiService = TestBed.inject(ContentApiService);
unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
appHookService = TestBed.inject(AppHookService); appHookService = TestBed.inject(AppHookService);
spyOn(contentApiService['favoritesApi'], 'getFavoriteSite').and.returnValue(Promise.resolve(null)); spyOn(contentApiService['favoritesApi'], 'getFavoriteSite').and.returnValue(Promise.resolve(null));
@@ -86,14 +89,24 @@ describe('ToggleFavoriteLibraryComponent', () => {
expect(component.library.isFavorite).toBe(true); expect(component.library.isFavorite).toBe(true);
}); });
it('should emit onToggleEvent() event', async () => { it('should emit onToggleEvent() event', fakeAsync(() => {
spyOn(appHookService.favoriteLibraryToggle, 'next'); spyOn(appHookService.favoriteLibraryToggle, 'next');
fixture.detectChanges();
await fixture.whenStable();
component.onToggleEvent(); component.onToggleEvent();
tick(100);
expect(appHookService.favoriteLibraryToggle.next).toHaveBeenCalled(); expect(appHookService.favoriteLibraryToggle.next).toHaveBeenCalled();
}));
it('should focus element on toggle when focusAfterClosed is provided', () => {
const mockElement = jasmine.createSpyObj<HTMLElement>('HTMLElement', ['focus']);
spyOn(document, 'querySelector').and.returnValue(mockElement);
component.data = { focusAfterClosed: '.adf-context-menu-source' };
const button = unitTestingUtils.getByCSS('button');
button.triggerEventHandler('toggle', new CustomEvent('toggle'));
expect(document.querySelector).toHaveBeenCalledWith('.adf-context-menu-source');
expect(mockElement.focus).toHaveBeenCalled();
}); });
}); });
@@ -22,7 +22,7 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>. * from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, DestroyRef, inject, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppHookService } from '@alfresco/aca-shared'; import { AppHookService } from '@alfresco/aca-shared';
import { AppStore, getAppSelection } from '@alfresco/aca-shared/store'; import { AppStore, getAppSelection } from '@alfresco/aca-shared/store';
@@ -56,6 +56,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class ToggleFavoriteLibraryComponent implements OnInit { export class ToggleFavoriteLibraryComponent implements OnInit {
library; library;
@Input() data: { focusAfterClosed?: string };
@ViewChild(MatMenuItem) @ViewChild(MatMenuItem)
menuItem: MatMenuItem; menuItem: MatMenuItem;
@@ -84,6 +86,11 @@ export class ToggleFavoriteLibraryComponent implements OnInit {
} }
onToggleEvent() { onToggleEvent() {
if (this.data?.focusAfterClosed) {
document.querySelector<HTMLElement>('.adf-context-menu-source')?.focus();
}
setTimeout(() => {
this.appHookService.favoriteLibraryToggle.next(); this.appHookService.favoriteLibraryToggle.next();
}, 100);
} }
} }
@@ -29,11 +29,14 @@ import { ExtensionService } from '@alfresco/adf-extensions';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { AppTestingModule } from '../../../testing/app-testing.module'; import { AppTestingModule } from '../../../testing/app-testing.module';
import { UnitTestingUtils } from '@alfresco/adf-core';
describe('ToggleFavoriteComponent', () => { describe('ToggleFavoriteComponent', () => {
let component: ToggleFavoriteComponent; let component: ToggleFavoriteComponent;
let fixture; let fixture;
let router; let router;
let unitTestingUtils: UnitTestingUtils;
const mockRouter = { const mockRouter = {
url: 'some-url' url: 'some-url'
}; };
@@ -54,6 +57,7 @@ describe('ToggleFavoriteComponent', () => {
fixture = TestBed.createComponent(ToggleFavoriteComponent); fixture = TestBed.createComponent(ToggleFavoriteComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
router = TestBed.inject(Router); router = TestBed.inject(Router);
}); });
@@ -66,7 +70,7 @@ describe('ToggleFavoriteComponent', () => {
}); });
it('should not dispatch reload if route is not specified', () => { it('should not dispatch reload if route is not specified', () => {
component.data = '["/reload_on_this_route"]'; component.data = { routes: ['/reload_on_this_route'] };
router.url = '/somewhere_over_the_rainbow'; router.url = '/somewhere_over_the_rainbow';
fixture.detectChanges(); fixture.detectChanges();
@@ -76,7 +80,7 @@ describe('ToggleFavoriteComponent', () => {
}); });
it('should dispatch reload if route is specified', () => { it('should dispatch reload if route is specified', () => {
component.data = '["/reload_on_this_route"]'; component.data = { routes: ['/reload_on_this_route'] };
router.url = '/reload_on_this_route'; router.url = '/reload_on_this_route';
fixture.detectChanges(); fixture.detectChanges();
@@ -84,4 +88,16 @@ describe('ToggleFavoriteComponent', () => {
expect(mockStore.dispatch).toHaveBeenCalled(); expect(mockStore.dispatch).toHaveBeenCalled();
}); });
it('should focus element on toggle when focusAfterClosed is provided', () => {
const mockElement = jasmine.createSpyObj<HTMLElement>('HTMLElement', ['focus']);
spyOn(document, 'querySelector').and.returnValue(mockElement);
component.data = { routes: [], focusAfterClosed: '.adf-context-menu-source' };
const button = unitTestingUtils.getByCSS('button');
button.triggerEventHandler('toggle', new CustomEvent('toggle'));
expect(document.querySelector).toHaveBeenCalledWith('.adf-context-menu-source');
expect(mockElement.focus).toHaveBeenCalled();
});
}); });
@@ -64,12 +64,14 @@ export class ToggleFavoriteComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
if (this.data) { this.reloadOnRoutes = this.data?.routes ?? [];
this.reloadOnRoutes = JSON.parse(this.data.replace(/'/g, '"'));
}
} }
onToggleEvent() { onToggleEvent() {
const focusAfterClosed = this.data?.focusAfterClosed;
if (focusAfterClosed) {
document.querySelector<HTMLElement>(focusAfterClosed)?.focus();
}
if (this.reloadOnRoutes.includes(this.router.url)) { if (this.reloadOnRoutes.includes(this.router.url)) {
this.documentListService.reload(); this.documentListService.reload();
} }
@@ -902,6 +902,20 @@ describe('ContentManagementService', () => {
store.dispatch(new DeleteNodesAction(selection)); store.dispatch(new DeleteNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-warning-snackbar'); expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-warning-snackbar');
}); });
it('should focus element when deleting nodes with focusAfterClosed selector', fakeAsync(() => {
const mockElement = jasmine.createSpyObj<HTMLElement>('HTMLElement', ['focus']);
spyOn(document, 'querySelector').and.returnValue(mockElement);
spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
const nodes = [{ entry: { id: '1', name: 'name1' } } as NodeEntry];
contentManagementService.deleteNodes(nodes, false, '.some-button');
tick();
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
expect(mockElement.focus).toHaveBeenCalled();
}));
}); });
describe('Permanent Delete', () => { describe('Permanent Delete', () => {
@@ -927,6 +941,20 @@ describe('ContentManagementService', () => {
expect(contentApi.purgeDeletedNode).toHaveBeenCalled(); expect(contentApi.purgeDeletedNode).toHaveBeenCalled();
}); });
it('should focus element when purging nodes with focusAfterClosed selector', fakeAsync(() => {
const mockElement = jasmine.createSpyObj<HTMLElement>('HTMLElement', ['focus']);
spyOn(document, 'querySelector').and.returnValue(mockElement);
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(of({}));
const nodes = [{ entry: { id: '1', name: 'name1' } } as NodeEntry];
contentManagementService.purgeDeletedNodes(nodes, '.some-button');
tick();
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
expect(mockElement.focus).toHaveBeenCalled();
}));
describe('notification', () => { describe('notification', () => {
it('raises warning on multiple fail and one success', (done) => { it('raises warning on multiple fail and one success', (done) => {
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
@@ -1307,6 +1335,28 @@ describe('ContentManagementService', () => {
store.dispatch(new RestoreDeletedNodesAction(selection)); store.dispatch(new RestoreDeletedNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-info-snackbar'); expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-info-snackbar');
}); });
it('should focus element when restoring nodes with focusAfterClosed selector', fakeAsync(() => {
const mockElement = jasmine.createSpyObj<HTMLElement>('HTMLElement', ['focus']);
spyOn(document, 'querySelector').and.returnValue(mockElement);
spyOn(contentApi, 'restoreNode').and.returnValue(of({} as NodeEntry));
const path = {
elements: [
{
id: '1-1',
name: 'Company Home'
}
]
};
const nodes = [{ entry: { name: 'node1', id: '1', path } } as NodeEntry];
contentManagementService.restoreDeletedNodes(nodes, '.some-button');
tick(100);
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
expect(mockElement.focus).toHaveBeenCalled();
}));
}); });
}); });
@@ -1996,6 +2046,19 @@ describe('ContentManagementService', () => {
expect(showInfoSpy).toHaveBeenCalledWith('APP.MESSAGES.INFO.FAVORITE_NODES_ADDED', null, { number: 2 }); expect(showInfoSpy).toHaveBeenCalledWith('APP.MESSAGES.INFO.FAVORITE_NODES_ADDED', null, { number: 2 });
}); });
it('should focus element when adding favorite with focusAfterClosed selector', fakeAsync(() => {
const mockElement = jasmine.createSpyObj<HTMLElement>('HTMLElement', ['focus']);
spyOn(document, 'querySelector').and.returnValue(mockElement);
spyOn(contentApi, 'addFavorite').and.returnValue(of({ entry: { targetGuid: '', target: '' } }));
contentManagementService.addFavorite([fakeNode1, fakeNode2], '.some-button');
tick(100);
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
expect(mockElement.focus).toHaveBeenCalled();
}));
it('should call proper content api and display proper snackbar message if one node is provided for removeFavorite', () => { it('should call proper content api and display proper snackbar message if one node is provided for removeFavorite', () => {
spyOn(contentApi, 'removeFavorite').and.returnValue(of({})); spyOn(contentApi, 'removeFavorite').and.returnValue(of({}));
@@ -2024,5 +2087,18 @@ describe('ContentManagementService', () => {
expect(contentApi.removeFavorite).toHaveBeenCalledWith([fakeNode1, fakeNode2]); expect(contentApi.removeFavorite).toHaveBeenCalledWith([fakeNode1, fakeNode2]);
expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.FAVORITE_NODE_NOT_FOUND', null, { name: 'mock-folder2-name' }); expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.FAVORITE_NODE_NOT_FOUND', null, { name: 'mock-folder2-name' });
}); });
it('should focus element when removing favorite with focusAfterClosed selector', fakeAsync(() => {
const mockElement = jasmine.createSpyObj<HTMLElement>('HTMLElement', ['focus']);
spyOn(document, 'querySelector').and.returnValue(mockElement);
spyOn(contentApi, 'removeFavorite').and.returnValue(of({}));
contentManagementService.removeFavorite([fakeNode1, fakeNode2], '.some-button');
tick(100);
expect(document.querySelector).toHaveBeenCalledWith('.some-button');
expect(mockElement.focus).toHaveBeenCalled();
}));
}); });
}); });
@@ -93,7 +93,8 @@ export class ContentManagementService {
private readonly documentListService = inject(DocumentListService); private readonly documentListService = inject(DocumentListService);
private readonly createMenuButtonSelector = 'app-toolbar-menu button[id="app.toolbar.create"]'; private readonly createMenuButtonSelector = 'app-toolbar-menu button[id="app.toolbar.create"]';
addFavorite(nodes: Array<NodeEntry>) { addFavorite(nodes: Array<NodeEntry>, focusedElementOnCloseSelector?: string) {
this.focusAfterClose(focusedElementOnCloseSelector);
if (nodes && nodes.length > 0) { if (nodes && nodes.length > 0) {
this.contentApi.addFavorite(nodes).subscribe(() => { this.contentApi.addFavorite(nodes).subscribe(() => {
const favoriteNodes = nodes.map((node) => { const favoriteNodes = nodes.map((node) => {
@@ -112,7 +113,8 @@ export class ContentManagementService {
} }
} }
removeFavorite(nodes: Array<NodeEntry>) { removeFavorite(nodes: Array<NodeEntry>, focusedElementOnCloseSelector?: string) {
this.focusAfterClose(focusedElementOnCloseSelector);
if (nodes && nodes.length > 0) { if (nodes && nodes.length > 0) {
this.contentApi.removeFavorite(nodes).subscribe({ this.contentApi.removeFavorite(nodes).subscribe({
next: () => { next: () => {
@@ -373,7 +375,7 @@ export class ContentManagementService {
return this.permission.check(folderNode, ['create']); return this.permission.check(folderNode, ['create']);
} }
purgeDeletedNodes(nodes: NodeEntry[]) { purgeDeletedNodes(nodes: NodeEntry[], focusedElementOnCloseSelector?: string) {
if (!nodes || nodes.length === 0) { if (!nodes || nodes.length === 0) {
return; return;
} }
@@ -389,6 +391,7 @@ export class ContentManagementService {
}); });
dialogRef.afterClosed().subscribe((result) => { dialogRef.afterClosed().subscribe((result) => {
this.focusAfterClose(focusedElementOnCloseSelector);
if (result === true) { if (result === true) {
const nodesToDelete: NodeInfo[] = nodes.map((node) => { const nodesToDelete: NodeInfo[] = nodes.map((node) => {
const { name } = node.entry; const { name } = node.entry;
@@ -404,7 +407,7 @@ export class ContentManagementService {
}); });
} }
restoreDeletedNodes(selection: NodeEntry[] = []) { restoreDeletedNodes(selection: NodeEntry[] = [], focusedElementOnCloseSelector?: string) {
if (!selection.length) { if (!selection.length) {
return; return;
} }
@@ -433,8 +436,11 @@ export class ContentManagementService {
const remainingNodes = this.diff(selectedNodes, nodes.list.entries); const remainingNodes = this.diff(selectedNodes, nodes.list.entries);
if (!remainingNodes.length) { if (!remainingNodes.length) {
this.focusAfterClose(focusedElementOnCloseSelector);
this.showRestoreNotification(status); this.showRestoreNotification(status);
setTimeout(() => {
this.documentListService.reload(); this.documentListService.reload();
}, 50);
} else { } else {
this.restoreDeletedNodes(remainingNodes); this.restoreDeletedNodes(remainingNodes);
} }
@@ -689,7 +695,8 @@ export class ContentManagementService {
); );
} }
deleteNodes(items: NodeEntry[], allowUndo = true): void { deleteNodes(items: NodeEntry[], allowUndo = true, focusedElementOnCloseSelector?: string): void {
this.focusAfterClose(focusedElementOnCloseSelector);
const batch: Observable<DeletedNodeInfo>[] = []; const batch: Observable<DeletedNodeInfo>[] = [];
items.forEach((node) => { items.forEach((node) => {
@@ -0,0 +1,88 @@
/*!
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FavoriteEffects } from './favorite.effects';
import { ContentManagementService } from '../../services/content-management.service';
import { AddFavoriteAction, AppStore, RemoveFavoriteAction, SetSelectedNodesAction } from '@alfresco/aca-shared/store';
import { Store } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { AppTestingModule } from '../../testing/app-testing.module';
import { NodeEntry } from '@alfresco/js-api';
describe('FavoriteEffects', () => {
let contentService: ContentManagementService;
let store: Store<AppStore>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
providers: [provideEffects([FavoriteEffects])]
});
store = TestBed.inject(Store);
contentService = TestBed.inject(ContentManagementService);
});
it('should call addFavorite with payload when AddFavoriteAction is dispatched', () => {
spyOn(contentService, 'addFavorite').and.stub();
const payload = [{ entry: { id: 'node1' } }] as NodeEntry[];
store.dispatch(new AddFavoriteAction(payload, { focusedElementOnCloseSelector: 'test-selector' }));
expect(contentService.addFavorite).toHaveBeenCalledWith(payload, 'test-selector');
});
it('should call addFavorite with selection when AddFavoriteAction is dispatched without payload', fakeAsync(() => {
spyOn(contentService, 'addFavorite').and.stub();
const selection = [{ entry: { isFile: true } }] as NodeEntry[];
store.dispatch(new SetSelectedNodesAction(selection));
tick(100);
store.dispatch(new AddFavoriteAction(null, { focusedElementOnCloseSelector: 'test-selector' }));
expect(contentService.addFavorite).toHaveBeenCalledWith(selection, 'test-selector');
}));
it('should call removeFavorite with payload when RemoveFavoriteAction is dispatched', () => {
spyOn(contentService, 'removeFavorite').and.stub();
const payload = [{ entry: { id: 'node1' } }] as NodeEntry[];
store.dispatch(new RemoveFavoriteAction(payload, { focusedElementOnCloseSelector: 'test-selector' }));
expect(contentService.removeFavorite).toHaveBeenCalledWith(payload, 'test-selector');
});
it('should call removeFavorite with selection when RemoveFavoriteAction is dispatched without payload', fakeAsync(() => {
spyOn(contentService, 'removeFavorite').and.stub();
const selection: any = [{ entry: { isFile: true } }] as NodeEntry[];
store.dispatch(new SetSelectedNodesAction(selection));
tick(100);
store.dispatch(new RemoveFavoriteAction(null, { focusedElementOnCloseSelector: 'test-selector' }));
expect(contentService.removeFavorite).toHaveBeenCalledWith(selection, 'test-selector');
}));
});
@@ -41,14 +41,14 @@ export class FavoriteEffects {
ofType<AddFavoriteAction>(NodeActionTypes.AddFavorite), ofType<AddFavoriteAction>(NodeActionTypes.AddFavorite),
map((action) => { map((action) => {
if (action.payload && action.payload.length > 0) { if (action.payload && action.payload.length > 0) {
this.content.addFavorite(action.payload); this.content.addFavorite(action.payload, action.configuration.focusedElementOnCloseSelector);
} else { } else {
this.store this.store
.select(getAppSelection) .select(getAppSelection)
.pipe(take(1)) .pipe(take(1))
.subscribe((selection) => { .subscribe((selection) => {
if (selection && !selection.isEmpty) { if (selection && !selection.isEmpty) {
this.content.addFavorite(selection.nodes); this.content.addFavorite(selection.nodes, action.configuration.focusedElementOnCloseSelector);
} }
}); });
} }
@@ -63,14 +63,14 @@ export class FavoriteEffects {
ofType<RemoveFavoriteAction>(NodeActionTypes.RemoveFavorite), ofType<RemoveFavoriteAction>(NodeActionTypes.RemoveFavorite),
map((action) => { map((action) => {
if (action.payload && action.payload.length > 0) { if (action.payload && action.payload.length > 0) {
this.content.removeFavorite(action.payload); this.content.removeFavorite(action.payload, action.configuration.focusedElementOnCloseSelector);
} else { } else {
this.store this.store
.select(getAppSelection) .select(getAppSelection)
.pipe(take(1)) .pipe(take(1))
.subscribe((selection) => { .subscribe((selection) => {
if (selection && !selection.isEmpty) { if (selection && !selection.isEmpty) {
this.content.removeFavorite(selection.nodes); this.content.removeFavorite(selection.nodes, action.configuration.focusedElementOnCloseSelector);
} }
}); });
} }
@@ -156,7 +156,7 @@ describe('NodeEffects', () => {
const node: any = {}; const node: any = {};
store.dispatch(new PurgeDeletedNodesAction([node])); store.dispatch(new PurgeDeletedNodesAction([node]));
expect(contentService.purgeDeletedNodes).toHaveBeenCalledWith([node]); expect(contentService.purgeDeletedNodes).toHaveBeenCalledWith([node], undefined);
}); });
it('should purge nodes from the active selection', fakeAsync(() => { it('should purge nodes from the active selection', fakeAsync(() => {
@@ -167,8 +167,8 @@ describe('NodeEffects', () => {
tick(100); tick(100);
store.dispatch(new PurgeDeletedNodesAction(null)); store.dispatch(new PurgeDeletedNodesAction([node], { focusedElementOnCloseSelector: '.test-selector' }));
expect(contentService.purgeDeletedNodes).toHaveBeenCalledWith([node]); expect(contentService.purgeDeletedNodes).toHaveBeenCalledWith([node], '.test-selector');
})); }));
it('should do nothing if invoking purge with no data', () => { it('should do nothing if invoking purge with no data', () => {
@@ -187,7 +187,7 @@ describe('NodeEffects', () => {
const node: any = {}; const node: any = {};
store.dispatch(new RestoreDeletedNodesAction([node])); store.dispatch(new RestoreDeletedNodesAction([node]));
expect(contentService.restoreDeletedNodes).toHaveBeenCalledWith([node]); expect(contentService.restoreDeletedNodes).toHaveBeenCalledWith([node], undefined);
}); });
it('should restore deleted nodes from the active selection', fakeAsync(() => { it('should restore deleted nodes from the active selection', fakeAsync(() => {
@@ -198,8 +198,8 @@ describe('NodeEffects', () => {
tick(100); tick(100);
store.dispatch(new RestoreDeletedNodesAction(null)); store.dispatch(new RestoreDeletedNodesAction(null, { focusedElementOnCloseSelector: '.test-selector' }));
expect(contentService.restoreDeletedNodes).toHaveBeenCalledWith([node]); expect(contentService.restoreDeletedNodes).toHaveBeenCalledWith([node], '.test-selector');
})); }));
it('should do nothing if invoking restore with no data', () => { it('should do nothing if invoking restore with no data', () => {
@@ -220,7 +220,7 @@ describe('NodeEffects', () => {
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new DeleteNodesAction([node], true) })); expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new DeleteNodesAction([node], true) }));
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ShowLoaderAction(true) })); expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ShowLoaderAction(true) }));
expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true); expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true, undefined);
}); });
it('should delete nodes from the active selection', fakeAsync(() => { it('should delete nodes from the active selection', fakeAsync(() => {
@@ -231,11 +231,13 @@ describe('NodeEffects', () => {
tick(100); tick(100);
store.dispatch(new DeleteNodesAction(null)); store.dispatch(new DeleteNodesAction(null, true, { focusedElementOnCloseSelector: '.test-selector' }));
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new DeleteNodesAction(null, true) })); expect(store.dispatch).toHaveBeenCalledWith(
jasmine.objectContaining({ ...new DeleteNodesAction(null, true, { focusedElementOnCloseSelector: '.test-selector' }) })
);
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ShowLoaderAction(true) })); expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ShowLoaderAction(true) }));
expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true); expect(contentService.deleteNodes).toHaveBeenCalledWith([node], true, '.test-selector');
})); }));
it('should do nothing if invoking delete with no data', () => { it('should do nothing if invoking delete with no data', () => {
@@ -119,14 +119,14 @@ export class NodeEffects {
ofType<PurgeDeletedNodesAction>(NodeActionTypes.PurgeDeleted), ofType<PurgeDeletedNodesAction>(NodeActionTypes.PurgeDeleted),
map((action) => { map((action) => {
if (action?.payload?.length > 0) { if (action?.payload?.length > 0) {
this.contentService.purgeDeletedNodes(action.payload); this.contentService.purgeDeletedNodes(action.payload, action.configuration?.focusedElementOnCloseSelector);
} else { } else {
this.store this.store
.select(getAppSelection) .select(getAppSelection)
.pipe(take(1)) .pipe(take(1))
.subscribe((selection) => { .subscribe((selection) => {
if (selection && selection.count > 0) { if (selection && selection.count > 0) {
this.contentService.purgeDeletedNodes(selection.nodes); this.contentService.purgeDeletedNodes(selection.nodes, action.configuration?.focusedElementOnCloseSelector);
} }
}); });
} }
@@ -141,14 +141,14 @@ export class NodeEffects {
ofType<RestoreDeletedNodesAction>(NodeActionTypes.RestoreDeleted), ofType<RestoreDeletedNodesAction>(NodeActionTypes.RestoreDeleted),
map((action) => { map((action) => {
if (action?.payload?.length > 0) { if (action?.payload?.length > 0) {
this.contentService.restoreDeletedNodes(action.payload); this.contentService.restoreDeletedNodes(action.payload, action.configuration?.focusedElementOnCloseSelector);
} else { } else {
this.store this.store
.select(getAppSelection) .select(getAppSelection)
.pipe(take(1)) .pipe(take(1))
.subscribe((selection) => { .subscribe((selection) => {
if (selection && selection.count > 0) { if (selection && selection.count > 0) {
this.contentService.restoreDeletedNodes(selection.nodes); this.contentService.restoreDeletedNodes(selection.nodes, action.configuration?.focusedElementOnCloseSelector);
} }
}); });
} }
@@ -164,14 +164,14 @@ export class NodeEffects {
map((action) => { map((action) => {
this.store.dispatch(new ShowLoaderAction(true)); this.store.dispatch(new ShowLoaderAction(true));
if (action?.payload?.length > 0) { if (action?.payload?.length > 0) {
this.contentService.deleteNodes(action.payload, action.allowUndo); this.contentService.deleteNodes(action.payload, action.allowUndo, action.configuration?.focusedElementOnCloseSelector);
} else { } else {
this.store this.store
.select(getAppSelection) .select(getAppSelection)
.pipe(take(1)) .pipe(take(1))
.subscribe((selection) => { .subscribe((selection) => {
if (selection && selection.count > 0) { if (selection && selection.count > 0) {
this.contentService.deleteNodes(selection.nodes, action.allowUndo); this.contentService.deleteNodes(selection.nodes, action.allowUndo, action.configuration?.focusedElementOnCloseSelector);
} }
}); });
} }
@@ -63,7 +63,8 @@ export class DeleteNodesAction implements Action {
constructor( constructor(
public payload: NodeEntry[] = [], public payload: NodeEntry[] = [],
public allowUndo = true public allowUndo = true,
public configuration?: ModalConfiguration
) {} ) {}
} }
@@ -76,13 +77,19 @@ export class UndoDeleteNodesAction implements Action {
export class RestoreDeletedNodesAction implements Action { export class RestoreDeletedNodesAction implements Action {
readonly type = NodeActionTypes.RestoreDeleted; readonly type = NodeActionTypes.RestoreDeleted;
constructor(public payload: Array<NodeEntry>) {} constructor(
public payload: Array<NodeEntry>,
public configuration?: ModalConfiguration
) {}
} }
export class PurgeDeletedNodesAction implements Action { export class PurgeDeletedNodesAction implements Action {
readonly type = NodeActionTypes.PurgeDeleted; readonly type = NodeActionTypes.PurgeDeleted;
constructor(public payload: Array<NodeEntry>) {} constructor(
public payload: Array<NodeEntry>,
public configuration?: ModalConfiguration
) {}
} }
export class DownloadNodesAction implements Action { export class DownloadNodesAction implements Action {
@@ -183,13 +190,19 @@ export class UnlockWriteAction implements Action {
export class AddFavoriteAction implements Action { export class AddFavoriteAction implements Action {
readonly type = NodeActionTypes.AddFavorite; readonly type = NodeActionTypes.AddFavorite;
constructor(public payload: Array<NodeEntry>) {} constructor(
public payload: Array<NodeEntry>,
public configuration?: ModalConfiguration
) {}
} }
export class RemoveFavoriteAction implements Action { export class RemoveFavoriteAction implements Action {
readonly type = NodeActionTypes.RemoveFavorite; readonly type = NodeActionTypes.RemoveFavorite;
constructor(public payload: Array<NodeEntry>) {} constructor(
public payload: Array<NodeEntry>,
public configuration?: ModalConfiguration
) {}
} }
export class ManageAspectsAction implements Action { export class ManageAspectsAction implements Action {
readonly type = NodeActionTypes.ChangeAspects; readonly type = NodeActionTypes.ChangeAspects;