mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[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:
@@ -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"
|
||||||
|
|||||||
+1
-1
@@ -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">
|
||||||
|
|||||||
+18
-5
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-2
@@ -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() {
|
||||||
this.appHookService.favoriteLibraryToggle.next();
|
if (this.data?.focusAfterClosed) {
|
||||||
|
document.querySelector<HTMLElement>('.adf-context-menu-source')?.focus();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.appHookService.favoriteLibraryToggle.next();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-2
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-3
@@ -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);
|
||||||
this.documentListService.reload();
|
setTimeout(() => {
|
||||||
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user