[ACS-3757] returning focus to element from which they were opened (#2837)

* ACS-3757 Return focus to More Actions button after closing modals opened from that button

* ACS-3757 Return focus to specific row from personal files after closing modal opened from context menu from row

* ACS-3757 Fixed issue that sometimes node was undefined

* ACS-3757 Return focus after closing upload new version modal

* ACS-3757 Added restore focus on list of libraries, restoring focus to correct row when multi rows are selected, little refactoring

* ACS-3757 Use runActionById function instead of runAction

* ACS-3757 Fixed unit tests

* ACS-3757 Updated description

* ACS-3757 Adrressing comments for static and for selectors in jsons

* ACS-3757 Remove boolean flag from jsons

* ACS-3757 Added some unit tests

* ACS-3757 Resolved conflicts

* ACS-3757 Created ModalConfiguration interface

* ACS-3757 Increase version of ADF

* ACS-3757 Fix for e2e

* ACS-3757 Fix some more e2e

* ACS-3757 Removed log
This commit is contained in:
AleksanderSklorz 2022-12-13 17:06:18 +01:00 committed by GitHub
parent e58a0c81ba
commit b609a9cd33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 498 additions and 228 deletions

View File

@ -1,6 +1,6 @@
<ng-container *ngIf="selection$ | async as selection">
<ng-container *ngIf="!data.iconButton">
<button mat-menu-item data-automation-id="share-action-button" (click)="editSharedNode(selection)">
<button mat-menu-item data-automation-id="share-action-button" (click)="editSharedNode(selection, '.adf-context-menu-source')">
<mat-icon>link</mat-icon>
<ng-container *ngIf="isShared(selection); else not_shared">
<span>{{ 'APP.ACTIONS.SHARE_EDIT' | translate }}</span>
@ -12,9 +12,10 @@
<button
mat-icon-button
data-automation-id="share-action-button"
(click)="editSharedNode(selection)"
(click)="editSharedNode(selection, '#share-action-button')"
[attr.aria-label]="getLabel(selection) | translate"
[attr.title]="getLabel(selection) | translate"
id="share-action-button"
>
<mat-icon>link</mat-icon>
</button>

View File

@ -34,7 +34,10 @@ import { AppStore, ShareNodeAction, getAppSelection } from '@alfresco/aca-shared
templateUrl: './toggle-shared.component.html'
})
export class ToggleSharedComponent implements OnInit {
@Input() data: { iconButton?: string };
@Input()
data: {
iconButton?: string;
};
selection$: Observable<SelectionState>;
@ -53,8 +56,13 @@ export class ToggleSharedComponent implements OnInit {
return selection.first && selection.first.entry && selection.first.entry.properties && !!selection.first.entry.properties['qshare:sharedId'];
}
editSharedNode(selection: SelectionState) {
this.store.dispatch(new ShareNodeAction(selection.first));
editSharedNode(selection: SelectionState, focusedElementOnCloseSelector: string) {
this.store.dispatch(
new ShareNodeAction({
...selection.first,
focusedElementOnCloseSelector
})
);
}
getLabel(selection: SelectionState): string {

View File

@ -4,7 +4,7 @@
<mat-menu #rootMenu="matMenu" class="aca-context-menu" hasBackdrop="false" acaContextMenuOutsideEvent (clickOutside)="onClickOutsideEvent()">
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId" [ngSwitch]="entry.type">
<ng-container *ngSwitchDefault>
<button mat-menu-item [id]="entry.id" (click)="runAction(entry.actions.click)">
<button mat-menu-item [id]="entry.id" (click)="runAction(entry)">
<adf-icon [value]="entry.icon"></adf-icon>
<span>{{ entry.title | translate }}</span>
</button>

View File

@ -96,11 +96,13 @@ describe('ContextMenuComponent', () => {
expect(contextMenuElements[0].querySelector('span').innerText).toBe(contextItem.title);
});
it('should run action with provided action id', () => {
it('should run action with provided action id and correct payload', () => {
spyOn(extensionsService, 'runActionById');
component.runAction(contextItem.actions.click);
component.runAction(contextItem);
expect(extensionsService.runActionById).toHaveBeenCalledWith(contextItem.actions.click);
expect(extensionsService.runActionById).toHaveBeenCalledWith(contextItem.actions.click, {
focusedElementOnCloseSelector: '.adf-context-menu-source'
});
});
});

View File

@ -69,8 +69,10 @@ export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit {
}
}
runAction(actionId: string) {
this.extensions.runActionById(actionId);
runAction(contentActionRef: ContentActionRef) {
this.extensions.runActionById(contentActionRef.actions.click, {
focusedElementOnCloseSelector: '.adf-context-menu-source'
});
}
ngOnDestroy() {

View File

@ -24,7 +24,7 @@
*/
import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing';
import { of, throwError, Subject } from 'rxjs';
import { of, throwError, Subject, BehaviorSubject, EMPTY } from 'rxjs';
import { Actions, ofType, EffectsModule } from '@ngrx/effects';
import {
AppStore,
@ -56,7 +56,7 @@ import { NodeActionsService } from './node-actions.service';
import { TranslationService, AlfrescoApiService, FileModel, NotificationService } from '@alfresco/adf-core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';
import { NodeEntry, Node, VersionPaging } from '@alfresco/js-api';
import { NodeEntry, Node, VersionPaging, MinimalNodeEntity } from '@alfresco/js-api';
import { NewVersionUploaderDataAction, NewVersionUploaderService, NodeAspectService, ViewVersion } from '@alfresco/adf-content-services';
describe('ContentManagementService', () => {
@ -1410,30 +1410,79 @@ describe('ContentManagementService', () => {
}));
it('should update node selection after dialog is closed', fakeAsync(() => {
spyOn(document, 'querySelector').and.returnValue(document.createElement('button'));
const node = { entry: { id: '1', name: 'name1' } } as NodeEntry;
spyOn(store, 'dispatch').and.callThrough();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(null)
} as MatDialogRef<MatDialog>);
store.dispatch(new ShareNodeAction(node));
expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(new SetSelectedNodesAction([node]));
const payload = {
...node,
...{
focusedElementOnCloseSelector: 'some-selector'
}
};
store.dispatch(new ShareNodeAction(payload));
expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(new SetSelectedNodesAction([payload]));
}));
it('should emit event when node is un-shared', fakeAsync(() => {
spyOn(document, 'querySelector').and.returnValue(document.createElement('button'));
const node = { entry: { id: '1', name: 'name1' } } as NodeEntry;
spyOn(appHookService.linksUnshared, 'next').and.callThrough();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(node)
} as MatDialogRef<MatDialog>);
store.dispatch(new ShareNodeAction(node));
store.dispatch(
new ShareNodeAction({
...node,
...{
focusedElementOnCloseSelector: 'some-selector'
}
})
);
tick();
flush();
expect(appHookService.linksUnshared.next).toHaveBeenCalled();
}));
it('should focus element indicated by passed selector after closing modal', () => {
const elementToFocusSelector = 'button';
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
spyOn(store, 'select').and.returnValue(new BehaviorSubject(''));
contentManagementService.shareNode(
{
entry: {}
},
elementToFocusSelector
);
afterClosed$.next();
expect(elementToFocus.focus).toHaveBeenCalled();
});
it('should not looking for element to focus if passed selector is empty string', () => {
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
spyOn(document, 'querySelector');
spyOn(store, 'select').and.returnValue(new BehaviorSubject(''));
contentManagementService.shareNode(
{
entry: {}
},
''
);
afterClosed$.next();
expect(document.querySelector).not.toHaveBeenCalled();
});
});
describe('Unlock Node', () => {
@ -1544,8 +1593,10 @@ describe('ContentManagementService', () => {
showVersionsOnly: true,
title: 'VERSION.DIALOG.TITLE'
};
contentManagementService.manageVersions(fakeNodeIsFile);
const elementToFocusSelector = 'some-selector';
contentManagementService.manageVersions(fakeNodeIsFile, elementToFocusSelector);
expect(spyOnOpenUploadNewVersionDialog['calls'].argsFor(0)[0]).toEqual(expectedArgument);
expect(spyOnOpenUploadNewVersionDialog['calls'].argsFor(0)[2]).toEqual(elementToFocusSelector);
});
it('should dispatch ReloadDocumentListAction if dialog emit refresh action', () => {
@ -1615,6 +1666,37 @@ describe('ContentManagementService', () => {
expect(alfrescoApiService.nodeUpdated.next).toHaveBeenCalledWith(newNode);
}));
it('should focus element indicated by passed selector after closing modal', () => {
const elementToFocusSelector = 'button';
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable(),
componentInstance: {
error: EMPTY
}
} as MatDialogRef<any>);
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
contentManagementService.editFolder({} as MinimalNodeEntity, elementToFocusSelector);
afterClosed$.next();
expect(elementToFocus.focus).toHaveBeenCalled();
});
it('should not looking for element to focus if passed selector is empty string', () => {
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable(),
componentInstance: {
error: EMPTY
}
} as MatDialogRef<any>);
spyOn(document, 'querySelector');
contentManagementService.editFolder({} as MinimalNodeEntity, '');
afterClosed$.next();
expect(document.querySelector).not.toHaveBeenCalled();
});
});
describe('aspect list dialog', () => {
@ -1633,21 +1715,50 @@ describe('ContentManagementService', () => {
createdAt: null,
createdByUser: null
};
const elementToFocusSelector = 'some-selector';
spyOn(contentApi, 'getNodeInfo').and.returnValue(of(responseNode));
contentManagementService.manageAspects(fakeNode);
contentManagementService.manageAspects(fakeNode, elementToFocusSelector);
expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('real-node-ghostbuster');
expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('real-node-ghostbuster', elementToFocusSelector);
});
it('should open dialog for managing the aspects', () => {
spyOn(nodeAspectService, 'updateNodeAspects').and.stub();
const fakeNode = { entry: { id: 'fake-node-id' } };
const elementToFocusSelector = 'some-selector';
contentManagementService.manageAspects(fakeNode);
contentManagementService.manageAspects(fakeNode, elementToFocusSelector);
expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('fake-node-id');
expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('fake-node-id', elementToFocusSelector);
});
});
describe('leaveLibrary', () => {
it('should focus element indicated by passed selector after closing modal', () => {
const elementToFocusSelector = 'button';
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
contentManagementService.leaveLibrary('', elementToFocusSelector);
afterClosed$.next();
expect(elementToFocus.focus).toHaveBeenCalled();
});
it('should not looking for element to focus if passed selector is empty string', () => {
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
spyOn(document, 'querySelector');
contentManagementService.leaveLibrary('', '');
afterClosed$.next();
expect(document.querySelector).not.toHaveBeenCalled();
});
});
});

View File

@ -85,6 +85,8 @@ interface RestoredNode {
providedIn: 'root'
})
export class ContentManagementService {
private readonly createMenuButtonSelector = 'app-create-menu button';
constructor(
private alfrescoApiService: AlfrescoApiService,
private store: Store<AppStore>,
@ -128,32 +130,32 @@ export class ContentManagementService {
}
}
manageVersions(node: any) {
manageVersions(node: any, focusedElementOnCloseSelector?: string) {
if (node && node.entry) {
// shared and favorite
const id = node.entry.nodeId || (node as any).entry.guid;
if (id) {
this.contentApi.getNodeInfo(id).subscribe((entry) => {
this.openVersionManagerDialog(entry);
this.openVersionManagerDialog(entry, focusedElementOnCloseSelector);
});
} else {
this.openVersionManagerDialog(node.entry);
this.openVersionManagerDialog(node.entry, focusedElementOnCloseSelector);
}
}
}
manageAspects(node: any) {
manageAspects(node: any, focusedElementOnCloseSelector?: string) {
if (node && node.entry) {
// shared and favorite
const id = node.entry.nodeId || (node as any).entry.guid;
if (id) {
this.contentApi.getNodeInfo(id).subscribe((entry) => {
this.openAspectListDialog(entry);
this.openAspectListDialog(entry, focusedElementOnCloseSelector);
});
} else {
this.openAspectListDialog(node.entry);
this.openAspectListDialog(node.entry, focusedElementOnCloseSelector);
}
}
}
@ -180,22 +182,22 @@ export class ContentManagementService {
});
}
shareNode(node: any): void {
shareNode(node: any, focusedElementOnCloseSelector?: string): void {
if (node && node.entry) {
// shared and favorite
const id = node.entry.nodeId || (node as any).entry.guid;
if (id) {
this.contentApi.getNodeInfo(id).subscribe((entry) => {
this.openShareLinkDialog({ entry });
this.openShareLinkDialog({ entry }, focusedElementOnCloseSelector);
});
} else {
this.openShareLinkDialog(node);
this.openShareLinkDialog(node, focusedElementOnCloseSelector);
}
}
}
openShareLinkDialog(node) {
openShareLinkDialog(node, focusedElementOnCloseSelector?: string) {
this.store
.select(getSharedUrl)
.pipe(take(1))
@ -214,6 +216,7 @@ export class ContentManagementService {
.subscribe(() => {
this.store.dispatch(new SetSelectedNodesAction([node]));
this.appHookService.linksUnshared.next();
this.focusAfterClose(focusedElementOnCloseSelector);
});
});
}
@ -237,11 +240,11 @@ export class ContentManagementService {
if (node) {
this.store.dispatch(new ReloadDocumentListAction());
}
ContentManagementService.focusCreateMenuButton();
this.focusAfterClose(this.createMenuButtonSelector);
});
}
editFolder(folder: MinimalNodeEntity) {
editFolder(folder: MinimalNodeEntity, focusedElementOnCloseSelector?: string) {
if (!folder) {
return;
}
@ -261,6 +264,7 @@ export class ContentManagementService {
if (node) {
this.alfrescoApiService.nodeUpdated.next(node);
}
this.focusAfterClose(focusedElementOnCloseSelector);
});
}
@ -278,7 +282,7 @@ export class ContentManagementService {
if (node) {
this.appHookService.libraryCreated.next(node);
}
ContentManagementService.focusCreateMenuButton();
this.focusAfterClose(this.createMenuButtonSelector);
}),
map((node: SiteEntry) => {
if (node && node.entry && node.entry.guid) {
@ -301,7 +305,7 @@ export class ContentManagementService {
);
}
leaveLibrary(siteId: string): void {
leaveLibrary(siteId: string, focusedElementOnCloseSelector?: string): void {
const dialogRef = this.dialogRef.open(ConfirmDialogComponent, {
data: {
title: 'APP.DIALOGS.CONFIRM_LEAVE.TITLE',
@ -324,6 +328,7 @@ export class ContentManagementService {
}
);
}
this.focusAfterClose(focusedElementOnCloseSelector);
});
}
@ -421,8 +426,8 @@ export class ContentManagementService {
});
}
copyNodes(nodes: Array<MinimalNodeEntity>) {
zip(this.nodeActionsService.copyNodes(nodes), this.nodeActionsService.contentCopied).subscribe(
copyNodes(nodes: Array<MinimalNodeEntity>, focusedElementOnCloseSelector?: string) {
zip(this.nodeActionsService.copyNodes(nodes, undefined, focusedElementOnCloseSelector), this.nodeActionsService.contentCopied).subscribe(
(result) => {
const [operationResult, newItems] = result;
this.showCopyMessage(operationResult, nodes, newItems);
@ -433,10 +438,10 @@ export class ContentManagementService {
);
}
moveNodes(nodes: Array<MinimalNodeEntity>) {
moveNodes(nodes: Array<MinimalNodeEntity>, focusedElementOnCloseSelector?: string) {
const permissionForMove = '!';
zip(this.nodeActionsService.moveNodes(nodes, permissionForMove), this.nodeActionsService.contentMoved).subscribe(
zip(this.nodeActionsService.moveNodes(nodes, permissionForMove, focusedElementOnCloseSelector), this.nodeActionsService.contentMoved).subscribe(
(result) => {
const [operationResult, moveResponse] = result;
this.showMoveMessage(nodes, operationResult, moveResponse);
@ -570,7 +575,7 @@ export class ContentManagementService {
);
}
private openVersionManagerDialog(node: any) {
private openVersionManagerDialog(node: any, focusedElementOnCloseSelector?: string) {
// workaround Shared
if (node.isFile || node.nodeId) {
const newVersionUploaderDialogData: NewVersionUploaderDialogData = {
@ -579,8 +584,9 @@ export class ContentManagementService {
title: 'VERSION.DIALOG.TITLE'
};
this.newVersionUploaderService
.openUploadNewVersionDialog(newVersionUploaderDialogData, { width: '630px', role: 'dialog' })
.subscribe((newVersionUploaderData: NewVersionUploaderData) => {
.openUploadNewVersionDialog(newVersionUploaderDialogData, { width: '630px', role: 'dialog' }, focusedElementOnCloseSelector)
.subscribe({
next: (newVersionUploaderData: NewVersionUploaderData) => {
switch (newVersionUploaderData.action) {
case NewVersionUploaderDataAction.refresh:
this.store.dispatch(new ReloadDocumentListAction());
@ -595,16 +601,17 @@ export class ContentManagementService {
default:
break;
}
}
});
} else {
this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION'));
}
}
private openAspectListDialog(node: any) {
private openAspectListDialog(node: any, focusedElementOnCloseSelector?: string) {
// workaround Shared
if (node.isFile || node.id) {
this.nodeAspectService.updateNodeAspects(node.id);
this.nodeAspectService.updateNodeAspects(node.id, focusedElementOnCloseSelector);
} else {
this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION'));
}
@ -1080,7 +1087,9 @@ export class ContentManagementService {
.subscribe(() => this.undoMoveNodes(moveResponse, initialParentId));
}
private static focusCreateMenuButton(): void {
document.querySelector<HTMLElement>('app-create-menu button').focus();
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector).focus();
}
}
}

View File

@ -115,7 +115,9 @@ describe('NodeActionsService', () => {
describe('ContentNodeSelector configuration', () => {
it('should validate selection when allowableOperation has `create`', () => {
spyOn(dialog, 'open');
spyOn(dialog, 'open').and.returnValue({
afterClosed: of
} as MatDialogRef<any>);
const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }];
service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]);
@ -131,7 +133,9 @@ describe('NodeActionsService', () => {
});
it('should invalidate selection when allowableOperation does not have `create`', () => {
spyOn(dialog, 'open');
spyOn(dialog, 'open').and.returnValue({
afterClosed: of
} as MatDialogRef<any>);
const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }];
service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]);
@ -147,7 +151,9 @@ describe('NodeActionsService', () => {
});
it('should invalidate selection if isSite', () => {
spyOn(dialog, 'open');
spyOn(dialog, 'open').and.returnValue({
afterClosed: of
} as MatDialogRef<any>);
const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }];
service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]);
@ -164,7 +170,9 @@ describe('NodeActionsService', () => {
});
it('should validate selection if not a Site', () => {
spyOn(dialog, 'open');
spyOn(dialog, 'open').and.returnValue({
afterClosed: of
} as MatDialogRef<any>);
const contentEntities = [new TestNode(), { entry: { nodeId: '1234' } }];
service.getContentNodeSelection(NodeAction.CHOOSE, contentEntities as NodeEntry[]);
@ -280,7 +288,7 @@ describe('NodeActionsService', () => {
spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => {
dialogData = data;
return { componentInstance: {} } as MatDialogRef<any>;
return { componentInstance: {}, afterClosed: of } as MatDialogRef<any>;
});
service.copyNodes([fileToCopy, folderToCopy]);
@ -335,7 +343,7 @@ describe('NodeActionsService', () => {
subject.next([destinationFolder.entry]);
expect(spyOnBatchOperation.calls.count()).toEqual(1);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined, undefined);
});
it('should use the custom data object with custom rowFilter & imageResolver & title with destination picker', () => {
@ -346,12 +354,12 @@ describe('NodeActionsService', () => {
let dialogData = null;
const spyOnDialog = spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => {
dialogData = data;
return { componentInstance: {} } as MatDialogRef<any>;
return { componentInstance: {}, afterClosed: of } as MatDialogRef<any>;
});
service.copyNodes([fileToCopy, folderToCopy]);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.COPY, [fileToCopy, folderToCopy], undefined, undefined);
expect(spyOnDestinationPicker.calls.count()).toEqual(1);
expect(spyOnDialog.calls.count()).toEqual(1);
@ -388,7 +396,7 @@ describe('NodeActionsService', () => {
let dialogData: any;
spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => {
dialogData = data;
return { componentInstance: {} } as MatDialogRef<any>;
return { componentInstance: {}, afterClosed: of } as MatDialogRef<any>;
});
service.copyNodes([{ entry: { id: 'entry-id', name: 'entry-name' } }]);
@ -410,7 +418,7 @@ describe('NodeActionsService', () => {
let dialogData = null;
spyOn(dialog, 'open').and.callFake((_contentNodeSelectorComponent: any, data: any) => {
dialogData = data;
return { componentInstance: {} } as MatDialogRef<any>;
return { componentInstance: {}, afterClosed: of } as MatDialogRef<any>;
});
service.copyNodes([{ entry: { id: 'entry-id' } }]);
@ -736,7 +744,7 @@ describe('NodeActionsService', () => {
service.moveNodes([fileToMove, folderToMove], permissionToMove);
subject.next([destinationFolder.entry]);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove, undefined);
expect(spyOnDestinationPicker).toHaveBeenCalled();
});
@ -749,7 +757,7 @@ describe('NodeActionsService', () => {
service.moveNodes([fileToMove, folderToMove], permissionToMove);
subject.next([destinationFolder.entry]);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove);
expect(spyOnBatchOperation).toHaveBeenCalledWith(NodeAction.MOVE, [fileToMove, folderToMove], permissionToMove, undefined);
expect(spyOnDestinationPicker).not.toHaveBeenCalled();
});

View File

@ -78,9 +78,10 @@ export class NodeActionsService {
*
* @param contentEntities nodes to copy
* @param permission permission which is needed to apply the action
* @param focusedElementOnCloseSelector element's selector which should be autofocused after closing modal
*/
copyNodes(contentEntities: any[], permission?: string): Subject<string> {
return this.doBatchOperation(NodeAction.COPY, contentEntities, permission);
copyNodes(contentEntities: any[], permission?: string, focusedElementOnCloseSelector?: string): Subject<string> {
return this.doBatchOperation(NodeAction.COPY, contentEntities, permission, focusedElementOnCloseSelector);
}
/**
@ -88,9 +89,10 @@ export class NodeActionsService {
*
* @param contentEntities nodes to move
* @param permission permission which is needed to apply the action
* @param focusedElementOnCloseSelector element's selector which should be autofocused after closing modal
*/
moveNodes(contentEntities: any[], permission?: string): Subject<string> {
return this.doBatchOperation(NodeAction.MOVE, contentEntities, permission);
moveNodes(contentEntities: any[], permission?: string, focusedElementOnCloseSelector?: string): Subject<string> {
return this.doBatchOperation(NodeAction.MOVE, contentEntities, permission, focusedElementOnCloseSelector);
}
/**
@ -99,14 +101,15 @@ export class NodeActionsService {
* @param action the action to perform (copy|move)
* @param contentEntities the contentEntities which have to have the action performed on
* @param permission permission which is needed to apply the action
* @param focusedElementOnCloseSelector element's selector which should be autofocused after closing modal
*/
doBatchOperation(action: BatchOperationType, contentEntities: any[], permission?: string): Subject<string> {
doBatchOperation(action: BatchOperationType, contentEntities: any[], permission?: string, focusedElementOnCloseSelector?: string): Subject<string> {
const observable: Subject<string> = new Subject<string>();
if (!this.isEntryEntitiesArray(contentEntities)) {
observable.error(new Error(JSON.stringify({ error: { statusCode: 400 } })));
} else if (this.checkPermission(action, contentEntities, permission)) {
const destinationSelection = this.getContentNodeSelection(action, contentEntities);
const destinationSelection = this.getContentNodeSelection(action, contentEntities, focusedElementOnCloseSelector);
destinationSelection.subscribe((selections: MinimalNodeEntryEntity[]) => {
const contentEntry = contentEntities[0].entry;
// Check if there's nodeId for Shared Files
@ -171,7 +174,11 @@ export class NodeActionsService {
return entryParentId;
}
getContentNodeSelection(action: NodeAction, contentEntities: MinimalNodeEntity[]): Subject<MinimalNodeEntryEntity[]> {
getContentNodeSelection(
action: NodeAction,
contentEntities: MinimalNodeEntity[],
focusedElementOnCloseSelector?: string
): Subject<MinimalNodeEntryEntity[]> {
const currentParentFolderId = this.getEntryParentId(contentEntities[0].entry);
const customDropdown = new SitePaging({
@ -211,12 +218,15 @@ export class NodeActionsService {
excludeSiteContent: ContentNodeDialogService.nonDocumentSiteContent
};
this.dialog.open(ContentNodeSelectorComponent, {
this.dialog
.open(ContentNodeSelectorComponent, {
data,
panelClass: 'adf-content-node-selector-dialog',
width: '630px',
role: 'dialog'
});
})
.afterClosed()
.subscribe(() => this.focusAfterClose(focusedElementOnCloseSelector));
data.select.subscribe({
complete: this.close.bind(this)
@ -687,4 +697,10 @@ export class NodeActionsService {
return moveStatus;
}
}
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector).focus();
}
}
}

View File

@ -0,0 +1,115 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 Alfresco Software Limited
*
* 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
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { TestBed } from '@angular/core/testing';
import { AppTestingModule } from '../../testing/app-testing.module';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { BehaviorSubject, Subject } from 'rxjs';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { DownloadNodesAction } from '@alfresco/aca-shared/store';
import { SelectionState } from '@alfresco/adf-extensions';
import { VersionEntry } from '@alfresco/js-api';
import { DownloadEffects } from './download.effects';
describe('DownloadEffects', () => {
let store: Store;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, EffectsModule.forRoot([DownloadEffects])]
});
store = TestBed.inject(Store);
});
describe('downloadNode$', () => {
let dialog: MatDialog;
beforeEach(() => {
dialog = TestBed.inject(MatDialog);
});
it('should focus element indicated by passed selector after closing modal', () => {
const elementToFocusSelector = 'button';
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
spyOn(store, 'select').and.returnValues(
new BehaviorSubject({
isEmpty: false,
nodes: [
{
entry: {
id: 'someId',
isFolder: true
}
}
]
} as SelectionState),
new BehaviorSubject<VersionEntry>(null)
);
store.dispatch(
new DownloadNodesAction({
focusedElementOnCloseSelector: elementToFocusSelector
})
);
afterClosed$.next();
expect(elementToFocus.focus).toHaveBeenCalled();
});
it('should not looking for element to focus if passed selector is empty string', () => {
const afterClosed$ = new Subject<void>();
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<any>);
spyOn(document, 'querySelector');
spyOn(store, 'select').and.returnValues(
new BehaviorSubject({
isEmpty: false,
nodes: [
{
entry: {
id: 'someId',
isFolder: true
}
}
]
} as SelectionState),
new BehaviorSubject<VersionEntry>(null)
);
store.dispatch(
new DownloadNodesAction({
focusedElementOnCloseSelector: ''
})
);
afterClosed$.next();
expect(document.querySelector).not.toHaveBeenCalled();
});
});
});

View File

@ -31,7 +31,7 @@ import { MatDialog } from '@angular/material/dialog';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { map, take } from 'rxjs/operators';
import { ContentApiService } from '@alfresco/aca-shared';
import { ContentApiService, ModalConfiguration } from '@alfresco/aca-shared';
import { ContentUrlService } from '../../services/content-url.service';
@Injectable()
@ -49,7 +49,7 @@ export class DownloadEffects {
this.actions$.pipe(
ofType<DownloadNodesAction>(NodeActionTypes.Download),
map((action) => {
if (action.payload && action.payload.length > 0) {
if (Array.isArray(action.payload) && action.payload?.length > 0) {
this.downloadNodes(action.payload);
} else {
this.store
@ -64,7 +64,7 @@ export class DownloadEffects {
if (version) {
this.downloadFileVersion(selection.nodes[0].entry, version.entry);
} else {
this.downloadNodes(selection.nodes);
this.downloadNodes(selection.nodes, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector);
}
});
}
@ -75,7 +75,7 @@ export class DownloadEffects {
{ dispatch: false }
);
private downloadNodes(toDownload: Array<MinimalNodeEntity>) {
private downloadNodes(toDownload: Array<MinimalNodeEntity>, focusedElementSelector?: string) {
const nodes = toDownload.map((node) => {
const { id, nodeId, name, isFile, isFolder } = node.entry as any;
@ -92,16 +92,16 @@ export class DownloadEffects {
}
if (nodes.length === 1) {
this.downloadNode(nodes[0]);
this.downloadNode(nodes[0], focusedElementSelector);
} else {
this.downloadZip(nodes);
this.downloadZip(nodes, focusedElementSelector);
}
}
private downloadNode(node: NodeInfo) {
private downloadNode(node: NodeInfo, focusedElementSelector?: string) {
if (node) {
if (node.isFolder) {
this.downloadZip([node]);
this.downloadZip([node], focusedElementSelector);
} else {
this.downloadFile(node);
}
@ -128,17 +128,20 @@ export class DownloadEffects {
}
}
private downloadZip(nodes: Array<NodeInfo>) {
private downloadZip(nodes: Array<NodeInfo>, focusedElementSelector?: string) {
if (nodes && nodes.length > 0) {
const nodeIds = nodes.map((node) => node.id);
this.dialog.open(DownloadZipDialogComponent, {
this.dialog
.open(DownloadZipDialogComponent, {
width: '600px',
disableClose: true,
data: {
nodeIds
}
});
})
.afterClosed()
.subscribe(() => this.focusAfterClose(focusedElementSelector));
}
}
@ -159,4 +162,10 @@ export class DownloadEffects {
private get isSharedLinkPreview() {
return location.href.includes('/preview/s/');
}
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector).focus();
}
}
}

View File

@ -39,7 +39,7 @@ import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { map, mergeMap, take } from 'rxjs/operators';
import { ContentApiService } from '@alfresco/aca-shared';
import { ContentApiService, ModalConfiguration } from '@alfresco/aca-shared';
import { ContentManagementService } from '../../services/content-management.service';
@Injectable()
@ -56,7 +56,7 @@ export class LibraryEffects {
this.actions$.pipe(
ofType<DeleteLibraryAction>(LibraryActionTypes.Delete),
map((action) => {
if (action.payload) {
if (typeof action?.payload === 'string') {
this.content.deleteLibrary(action.payload);
} else {
this.store
@ -78,7 +78,7 @@ export class LibraryEffects {
this.actions$.pipe(
ofType<LeaveLibraryAction>(LibraryActionTypes.Leave),
map((action) => {
if (action.payload) {
if (typeof action.payload === 'string') {
this.content.leaveLibrary(action.payload);
} else {
this.store
@ -86,7 +86,7 @@ export class LibraryEffects {
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.library) {
this.content.leaveLibrary(selection.library.entry.id);
this.content.leaveLibrary(selection.library.entry.id, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector);
}
});
}

View File

@ -79,10 +79,12 @@ describe('NodeEffects', () => {
it('should share node from payload', () => {
spyOn(contentService, 'shareNode').and.stub();
const node: any = {};
const node: any = {
entry: {}
};
store.dispatch(new ShareNodeAction(node));
expect(contentService.shareNode).toHaveBeenCalledWith(node);
expect(contentService.shareNode).toHaveBeenCalledWith(node, undefined);
});
it('should share node from active selection', fakeAsync(() => {
@ -94,7 +96,7 @@ describe('NodeEffects', () => {
tick(100);
store.dispatch(new ShareNodeAction(null));
expect(contentService.shareNode).toHaveBeenCalledWith(node);
expect(contentService.shareNode).toHaveBeenCalledWith(node, undefined);
}));
it('should do nothing if invoking share with no data', () => {
@ -300,7 +302,7 @@ describe('NodeEffects', () => {
tick(100);
store.dispatch(new EditFolderAction(null));
expect(contentService.editFolder).toHaveBeenCalledWith(currentFolder);
expect(contentService.editFolder).toHaveBeenCalledWith(currentFolder, undefined);
}));
it('should do nothing if editing folder with no selection and payload', () => {
@ -332,7 +334,7 @@ describe('NodeEffects', () => {
store.dispatch(new CopyNodesAction(null));
expect(contentService.copyNodes).toHaveBeenCalledWith([node]);
expect(contentService.copyNodes).toHaveBeenCalledWith([node], undefined);
}));
it('should do nothing if invoking copy with no data', () => {
@ -364,7 +366,7 @@ describe('NodeEffects', () => {
store.dispatch(new MoveNodesAction(null));
expect(contentService.moveNodes).toHaveBeenCalledWith([node]);
expect(contentService.moveNodes).toHaveBeenCalledWith([node], undefined);
}));
it('should do nothing if invoking move with no data', () => {
@ -488,7 +490,7 @@ describe('NodeEffects', () => {
store.dispatch(new ManageAspectsAction(null));
expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: true, id: 'file-node-id' } });
expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: true, id: 'file-node-id' } }, undefined);
}));
it('should call aspect dialog from the active folder selection', fakeAsync(() => {
@ -501,7 +503,7 @@ describe('NodeEffects', () => {
store.dispatch(new ManageAspectsAction(null));
expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: false, id: 'folder-node-id' } });
expect(contentService.manageAspects).toHaveBeenCalledWith({ entry: { isFile: false, id: 'folder-node-id' } }, undefined);
}));
});
});

View File

@ -54,6 +54,7 @@ import {
} from '@alfresco/aca-shared/store';
import { ContentManagementService } from '../../services/content-management.service';
import { ViewUtilService } from '@alfresco/adf-core';
import { ModalConfiguration } from '@alfresco/aca-shared';
@Injectable()
export class NodeEffects {
@ -69,15 +70,15 @@ export class NodeEffects {
this.actions$.pipe(
ofType<ShareNodeAction>(NodeActionTypes.Share),
map((action) => {
if (action.payload) {
this.contentService.shareNode(action.payload);
if (action.payload?.entry) {
this.contentService.shareNode(action.payload, action.payload?.focusedElementOnCloseSelector);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.file) {
this.contentService.shareNode(selection.file);
this.contentService.shareNode(selection.file, action.payload?.focusedElementOnCloseSelector);
}
});
}
@ -215,7 +216,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<EditFolderAction>(NodeActionTypes.EditFolder),
map((action) => {
if (action.payload) {
if (action.payload?.entry) {
this.contentService.editFolder(action.payload);
} else {
this.store
@ -223,7 +224,7 @@ export class NodeEffects {
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.folder) {
this.contentService.editFolder(selection.folder);
this.contentService.editFolder(selection.folder, action.payload?.focusedElementOnCloseSelector);
}
});
}
@ -237,7 +238,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<CopyNodesAction>(NodeActionTypes.Copy),
map((action) => {
if (action.payload && action.payload.length > 0) {
if (Array.isArray(action.payload) && action.payload?.length > 0) {
this.contentService.copyNodes(action.payload);
} else {
this.store
@ -245,7 +246,7 @@ export class NodeEffects {
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.contentService.copyNodes(selection.nodes);
this.contentService.copyNodes(selection.nodes, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector);
}
});
}
@ -259,7 +260,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<MoveNodesAction>(NodeActionTypes.Move),
map((action) => {
if (action.payload && action.payload.length > 0) {
if (Array.isArray(action.payload) && action.payload?.length > 0) {
this.contentService.moveNodes(action.payload);
} else {
this.store
@ -267,7 +268,7 @@ export class NodeEffects {
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.contentService.moveNodes(selection.nodes);
this.contentService.moveNodes(selection.nodes, (action.payload as ModalConfiguration)?.focusedElementOnCloseSelector);
}
});
}
@ -281,7 +282,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<ManagePermissionsAction>(NodeActionTypes.ManagePermissions),
map((action) => {
if (action && action.payload) {
if (action?.payload?.entry) {
const route = 'personal-files/details';
this.store.dispatch(new NavigateRouteAction([route, action.payload.entry.id, 'permissions']));
} else {
@ -305,7 +306,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<ExpandInfoDrawerAction>(NodeActionTypes.ExpandInfoDrawer),
map((action) => {
if (action && action.payload) {
if (action?.payload?.entry) {
const route = 'personal-files/details';
this.store.dispatch(new NavigateRouteAction([route, action.payload.entry.id]));
} else {
@ -329,7 +330,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<ManageVersionsAction>(NodeActionTypes.ManageVersions),
map((action) => {
if (action && action.payload) {
if (action?.payload?.entry) {
this.contentService.manageVersions(action.payload);
} else {
this.store
@ -337,7 +338,7 @@ export class NodeEffects {
.pipe(take(1))
.subscribe((selection) => {
if (selection && selection.file) {
this.contentService.manageVersions(selection.file);
this.contentService.manageVersions(selection.file, action.payload?.focusedElementOnCloseSelector);
}
});
}
@ -395,7 +396,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<ManageAspectsAction>(NodeActionTypes.ChangeAspects),
map((action) => {
if (action && action.payload) {
if (action?.payload?.entry) {
this.contentService.manageAspects(action.payload);
} else {
this.store
@ -403,7 +404,7 @@ export class NodeEffects {
.pipe(take(1))
.subscribe((selection) => {
if (selection && !selection.isEmpty) {
this.contentService.manageAspects(selection.nodes[0]);
this.contentService.manageAspects(selection.nodes[0], action.payload?.focusedElementOnCloseSelector);
}
});
}
@ -429,7 +430,7 @@ export class NodeEffects {
this.actions$.pipe(
ofType<ManageRulesAction>(NodeActionTypes.ManageRules),
map((action) => {
if (action && action.payload) {
if (action?.payload?.entry) {
this.store.dispatch(new NavigateRouteAction(['nodes', action.payload.entry.id, 'rules']));
} else {
this.store

View File

@ -41,12 +41,14 @@ import { of } from 'rxjs';
import { catchError, map, take } from 'rxjs/operators';
import { ContentManagementService } from '../../services/content-management.service';
import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { ModalConfiguration } from '@alfresco/aca-shared';
@Injectable()
export class UploadEffects {
private fileInput: HTMLInputElement;
private folderInput: HTMLInputElement;
private fileVersionInput: HTMLInputElement;
private readonly createMenuButtonSelector = 'app-create-menu button';
constructor(
private store: Store<AppStore>,
@ -90,7 +92,7 @@ export class UploadEffects {
this.actions$.pipe(
ofType<UploadFilesAction>(UploadActionTypes.UploadFiles),
map(() => {
this.registerFocusingCreateMenuButton(this.fileInput);
this.registerFocusingElementAfterModalClose(this.fileInput, this.createMenuButtonSelector);
this.fileInput.click();
})
),
@ -102,7 +104,7 @@ export class UploadEffects {
this.actions$.pipe(
ofType<UploadFolderAction>(UploadActionTypes.UploadFolder),
map(() => {
this.registerFocusingCreateMenuButton(this.folderInput);
this.registerFocusingElementAfterModalClose(this.folderInput, this.createMenuButtonSelector);
this.folderInput.click();
})
),
@ -114,11 +116,15 @@ export class UploadEffects {
this.actions$.pipe(
ofType<UploadFileVersionAction>(UploadActionTypes.UploadFileVersion),
map((action) => {
if (action?.payload) {
if (action?.payload instanceof CustomEvent) {
const node = action?.payload?.detail?.data?.node?.entry;
const file: any = action?.payload?.detail?.files[0]?.file;
this.contentService.versionUpdateDialog(node, file);
} else if (!action?.payload) {
} else if (!action?.payload || !(action.payload instanceof CustomEvent)) {
this.registerFocusingElementAfterModalClose(
this.fileVersionInput,
(action?.payload as ModalConfiguration)?.focusedElementOnCloseSelector
);
this.fileVersionInput.click();
}
})
@ -199,18 +205,18 @@ export class UploadEffects {
});
}
private registerFocusingCreateMenuButton(input: HTMLInputElement): void {
private registerFocusingElementAfterModalClose(input: HTMLInputElement, focusedElementSelector: string): void {
input.addEventListener(
'click',
() => {
window.addEventListener(
'focus',
() => {
const createMenuButton = document.querySelector<HTMLElement>('app-create-menu button');
createMenuButton.addEventListener('focus', () => createMenuButton.classList.add('cdk-program-focused'), {
const elementToFocus = document.querySelector<HTMLElement>(focusedElementSelector);
elementToFocus.addEventListener('focus', () => elementToFocus.classList.add('cdk-program-focused'), {
once: true
});
createMenuButton.focus();
elementToFocus.focus();
},
{
once: true

88
package-lock.json generated
View File

@ -11,9 +11,9 @@
"dev": true
},
"@alfresco/adf-cli": {
"version": "6.0.0-A.1-37352",
"resolved": "https://registry.npmjs.org/@alfresco/adf-cli/-/adf-cli-6.0.0-A.1-37352.tgz",
"integrity": "sha512-lM//JUPyBmyO9PVDd628VFyumMJElBKNDvy6E0Yv/iKZmj2S/93pCFhkjnnbG/hOMNAIy5HePOvwOHQ9+vQAgg==",
"version": "6.0.0-A.1-37376",
"resolved": "https://registry.npmjs.org/@alfresco/adf-cli/-/adf-cli-6.0.0-A.1-37376.tgz",
"integrity": "sha512-sEKwZ9DS4CAzub6oKOmJGFoLzc4esMW4859GnbLVU9V8pBZ+KPIfRECY3AsCrRvbuK7dSM8EQgsrT8KDb9IZDg==",
"dev": true,
"requires": {
"@alfresco/js-api": "5.2.0",
@ -28,17 +28,17 @@
}
},
"@alfresco/adf-content-services": {
"version": "6.0.0-A.1-37352",
"resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-6.0.0-A.1-37352.tgz",
"integrity": "sha512-vKtVJDl7WE19WkbB9KHfeka5lnSBOUFAEwjBsSr/UGhNFkQn0zSS1I4CXhjY5h0o+X31JVuIzVLfPqAWiEOZkA==",
"version": "6.0.0-A.1-37376",
"resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-6.0.0-A.1-37376.tgz",
"integrity": "sha512-QrwadBgJFG4n7iaUJfirhYPEWCO+BD1YiPBlo3+gK9LJvnaHiAiiAFbnXqaa2FY1RlFm6Td6NH0yoHJtwQH+sQ==",
"requires": {
"tslib": "^2.3.0"
}
},
"@alfresco/adf-core": {
"version": "6.0.0-A.1-37352",
"resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-6.0.0-A.1-37352.tgz",
"integrity": "sha512-zGvZkpIYTaUgZLzyGWBK17MCA+HOQbEEefr3V3KisuLFgCXWwD2GyGn1xSba+kp6CDbabGjfTG/2pG0oDk8NeA==",
"version": "6.0.0-A.1-37376",
"resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-6.0.0-A.1-37376.tgz",
"integrity": "sha512-NryzWdDie9eyrp0a7lC1P+6R0nXp7KzoHfIVrWO1rNhPWBhYgJlRHPP44KBO0/Ydgbio3jvnwZlX15+Kq0/Awg==",
"requires": {
"@editorjs/code": "2.7.0",
"@editorjs/editorjs": "2.25.0",
@ -56,20 +56,20 @@
}
},
"@alfresco/adf-extensions": {
"version": "6.0.0-A.1-37352",
"resolved": "https://registry.npmjs.org/@alfresco/adf-extensions/-/adf-extensions-6.0.0-A.1-37352.tgz",
"integrity": "sha512-OcalA6Jl9tr1ynFj9SPbH+i+h4faMpqdFzDza6ZjT2Hquq2Thk5ylzMz+LIHW1FB2wsfKssy9a/409sRHyy1Uw==",
"version": "6.0.0-A.1-37376",
"resolved": "https://registry.npmjs.org/@alfresco/adf-extensions/-/adf-extensions-6.0.0-A.1-37376.tgz",
"integrity": "sha512-amPdYFRlctYGfKIf1IKt9+SSsVcXOhvOxMsccbN9jI6ShlXBvhBTK+jtItqoJp3GDfWWGU/7RJ0anbZbbQQxTw==",
"requires": {
"tslib": "^2.3.0"
}
},
"@alfresco/adf-testing": {
"version": "6.0.0-A.1-37352",
"resolved": "https://registry.npmjs.org/@alfresco/adf-testing/-/adf-testing-6.0.0-A.1-37352.tgz",
"integrity": "sha512-0T18iLgKq76YzCK+iJwpRie8xrJXmGBM/YZ/OqMwAmyHPt5nUY7Uzy+stBlh/TDC9bULCejf3aZByMP82lT7hg==",
"version": "6.0.0-A.1-37376",
"resolved": "https://registry.npmjs.org/@alfresco/adf-testing/-/adf-testing-6.0.0-A.1-37376.tgz",
"integrity": "sha512-WXbpfnKgRHCk7yb/OiWOkJuCvldxRa4vBlP4sRQ4ggoywS85V1yRtJqespoDV/MmYZQ0/ZNXZtVFW8GHzQPOcQ==",
"dev": true,
"requires": {
"@alfresco/js-api": "5.3.0-466",
"@alfresco/js-api": "5.3.0-475",
"@angular/compiler": "14.1.3",
"@angular/core": "14.1.3",
"rxjs": "6.6.6",
@ -78,9 +78,9 @@
},
"dependencies": {
"@alfresco/js-api": {
"version": "5.3.0-466",
"resolved": "https://registry.npmjs.org/@alfresco/js-api/-/js-api-5.3.0-466.tgz",
"integrity": "sha512-ArBqTqEDbzR/jD6YtJTrPQG3Wz69WXURjnzZE8Os+JWUacJS1n/xEqncJY+xDoILuT1EtaEjogOWNYZixUqXlg==",
"version": "5.3.0-475",
"resolved": "https://registry.npmjs.org/@alfresco/js-api/-/js-api-5.3.0-475.tgz",
"integrity": "sha512-oNx3f6c7UlEhAry4pOSoXfZV5E53EM10dBXO2O2PrNS1hNJyT/XiWzeFT3szF8pMQWKyHc3fR5JRVThnTnR++A==",
"dev": true,
"requires": {
"event-emitter": "^0.3.5",
@ -7548,14 +7548,6 @@
"color-convert": "^2.0.1"
}
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -11343,16 +11335,6 @@
"dev": true,
"requires": {
"minimatch": "^5.0.1"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
}
}
},
"image-size": {
@ -13770,14 +13752,6 @@
"picomatch": "^2.0.4"
}
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"cacache": {
"version": "16.1.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
@ -14277,14 +14251,6 @@
"humanize-ms": "^1.2.1"
}
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"cacache": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.2.tgz",
@ -14642,14 +14608,6 @@
"npm-normalize-package-bin": "^1.0.1"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"glob": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
@ -15162,14 +15120,6 @@
"humanize-ms": "^1.2.1"
}
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"builtins": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",

View File

@ -26,9 +26,9 @@
},
"private": true,
"dependencies": {
"@alfresco/adf-content-services": "6.0.0-A.1-37352",
"@alfresco/adf-core": "6.0.0-A.1-37352",
"@alfresco/adf-extensions": "6.0.0-A.1-37352",
"@alfresco/adf-content-services": "6.0.0-A.1-37376",
"@alfresco/adf-core": "6.0.0-A.1-37376",
"@alfresco/adf-extensions": "6.0.0-A.1-37376",
"@alfresco/js-api": "5.2.0",
"@angular/animations": "14.1.2",
"@angular/cdk": "14.1.2",
@ -58,8 +58,8 @@
"zone.js": "0.11.8"
},
"devDependencies": {
"@alfresco/adf-cli": "6.0.0-A.1-37352",
"@alfresco/adf-testing": "6.0.0-A.1-37352",
"@alfresco/adf-cli": "6.0.0-A.1-37376",
"@alfresco/adf-testing": "6.0.0-A.1-37376",
"@angular-custom-builders/lite-serve": "^0.2.3",
"@angular-devkit/build-angular": "14.1.2",
"@angular-eslint/builder": "^14.1.2",

View File

@ -53,7 +53,9 @@ export class ToolbarButtonComponent {
runAction() {
if (this.hasClickAction(this.actionRef)) {
this.extensions.runActionById(this.actionRef.actions.click);
this.extensions.runActionById(this.actionRef.actions.click, {
focusedElementOnCloseSelector: `#${this.actionRef.id.replace(/\./g, '\\.')}`
});
}
}

View File

@ -44,6 +44,8 @@ import { MatMenuItem } from '@angular/material/menu';
export class ToolbarMenuItemComponent {
@Input()
actionRef: ContentActionRef;
@Input()
menuId?: string;
@ViewChild(MatMenuItem)
menuItem: MatMenuItem;
@ -52,7 +54,14 @@ export class ToolbarMenuItemComponent {
runAction() {
if (this.hasClickAction(this.actionRef)) {
this.extensions.runActionById(this.actionRef.actions.click);
this.extensions.runActionById(
this.actionRef.actions.click,
this.menuId
? {
focusedElementOnCloseSelector: `#${this.menuId.replace(/\./g, '\\.')}`
}
: undefined
);
}
}

View File

@ -18,7 +18,7 @@
<adf-dynamic-component [id]="child.component" [data]="child.data"></adf-dynamic-component>
</ng-container>
<ng-container *ngSwitchDefault>
<app-toolbar-menu-item [actionRef]="child"></app-toolbar-menu-item>
<app-toolbar-menu-item [actionRef]="child" [menuId]="actionRef.id"></app-toolbar-menu-item>
</ng-container>
</ng-container>
</ng-container>

View File

@ -0,0 +1,3 @@
export interface ModalConfiguration {
focusedElementOnCloseSelector?: string;
}

View File

@ -498,7 +498,7 @@ export class AppExtensionService implements RuleContext {
return false;
}
runActionById(id: string) {
runActionById(id: string, additionalPayload?: { [key: string]: any }) {
const action = this.extensions.getActionById(id);
if (action) {
const { type, payload } = action;
@ -507,9 +507,21 @@ export class AppExtensionService implements RuleContext {
};
const expression = this.extensions.runExpression(payload, context);
this.store.dispatch({ type, payload: expression });
this.store.dispatch({
type,
payload:
typeof expression === 'object'
? {
...expression,
...additionalPayload
}
: expression
});
} else {
this.store.dispatch({ type: id });
this.store.dispatch({
type: id,
payload: additionalPayload
});
}
}

View File

@ -48,6 +48,7 @@ export * from './lib/directives/shared.directives.module';
export * from './lib/models/types';
export * from './lib/models/viewer.rules';
export * from './lib/models/modal-configuration';
export * from './lib/routing/shared.guard';

View File

@ -25,6 +25,7 @@
import { Action } from '@ngrx/store';
import { SiteBody } from '@alfresco/js-api';
import { ModalConfiguration } from '@alfresco/aca-shared';
export enum LibraryActionTypes {
Delete = 'DELETE_LIBRARY',
@ -59,5 +60,5 @@ export class UpdateLibraryAction implements Action {
export class LeaveLibraryAction implements Action {
readonly type = LibraryActionTypes.Leave;
constructor(public payload?: string) {}
constructor(public payload?: string | ModalConfiguration) {}
}

View File

@ -25,6 +25,7 @@
import { Action } from '@ngrx/store';
import { MinimalNodeEntity } from '@alfresco/js-api';
import { ModalConfiguration } from '@alfresco/aca-shared';
export enum NodeActionTypes {
SetSelection = 'SET_SELECTED_NODES',
@ -84,7 +85,7 @@ export class PurgeDeletedNodesAction implements Action {
export class DownloadNodesAction implements Action {
readonly type = NodeActionTypes.Download;
constructor(public payload: MinimalNodeEntity[] = []) {}
constructor(public payload: MinimalNodeEntity[] | ModalConfiguration = []) {}
}
export class CreateFolderAction implements Action {
@ -96,13 +97,13 @@ export class CreateFolderAction implements Action {
export class EditFolderAction implements Action {
readonly type = NodeActionTypes.EditFolder;
constructor(public payload: MinimalNodeEntity) {}
constructor(public payload: MinimalNodeEntity & ModalConfiguration) {}
}
export class ShareNodeAction implements Action {
readonly type = NodeActionTypes.Share;
constructor(public payload: MinimalNodeEntity) {}
constructor(public payload: MinimalNodeEntity & ModalConfiguration) {}
}
export class UnshareNodesAction implements Action {
@ -114,13 +115,13 @@ export class UnshareNodesAction implements Action {
export class CopyNodesAction implements Action {
readonly type = NodeActionTypes.Copy;
constructor(public payload: Array<MinimalNodeEntity>) {}
constructor(public payload: Array<MinimalNodeEntity> | ModalConfiguration) {}
}
export class MoveNodesAction implements Action {
readonly type = NodeActionTypes.Move;
constructor(public payload: Array<MinimalNodeEntity>) {}
constructor(public payload: Array<MinimalNodeEntity> | ModalConfiguration) {}
}
export class ManagePermissionsAction implements Action {
@ -143,7 +144,7 @@ export class PrintFileAction implements Action {
export class ManageVersionsAction implements Action {
readonly type = NodeActionTypes.ManageVersions;
constructor(public payload: MinimalNodeEntity) {}
constructor(public payload: MinimalNodeEntity & ModalConfiguration) {}
}
export class EditOfflineAction implements Action {
@ -172,7 +173,7 @@ export class RemoveFavoriteAction implements Action {
export class ManageAspectsAction implements Action {
readonly type = NodeActionTypes.ChangeAspects;
constructor(public payload: MinimalNodeEntity) {}
constructor(public payload: MinimalNodeEntity & ModalConfiguration) {}
}
export class ManageRulesAction implements Action {

View File

@ -24,6 +24,7 @@
*/
import { Action } from '@ngrx/store';
import { ModalConfiguration } from '@alfresco/aca-shared';
export enum UploadActionTypes {
UploadFiles = 'UPLOAD_FILES',
@ -46,5 +47,5 @@ export class UploadFolderAction implements Action {
export class UploadFileVersionAction implements Action {
readonly type = UploadActionTypes.UploadFileVersion;
constructor(public payload: CustomEvent) {}
constructor(public payload: CustomEvent | ModalConfiguration) {}
}