From fb140531f486111dcab91617a87992ae5dbe7b21 Mon Sep 17 00:00:00 2001 From: Pablo Martinez Garcia Date: Thu, 16 Sep 2021 15:48:43 +0200 Subject: [PATCH] [AAE-5874] Re-evaluate extension rules when context changes (#2296) * [AAE-5874] Re-evaluate extension rules when context changes * [AAE-5874] Add disabled attribute to toolbar actions * [AAE-5874] Fix unit tests --- .../info-drawer/info-drawer.component.spec.ts | 17 +- .../info-drawer/info-drawer.component.ts | 10 +- .../toolbar-button.component.html | 1 + .../toolbar-menu/toolbar-menu.component.html | 1 + .../services/app.extension.service.spec.ts | 253 +++++++++++------- .../src/lib/services/app.extension.service.ts | 134 ++++++---- .../context-menu.component.spec.ts | 11 +- .../context-menu/context-menu.component.ts | 13 +- .../create-menu/create-menu.component.spec.ts | 47 ++-- .../create-menu/create-menu.component.ts | 12 +- .../header/header.component.spec.ts | 36 ++- src/app/components/header/header.component.ts | 20 +- src/app/components/page.component.ts | 16 +- .../components/preview/preview.component.ts | 7 +- .../shared-link-view.component.spec.ts | 17 +- .../shared-link-view.component.ts | 25 +- src/app/components/viewer/viewer.component.ts | 26 +- 17 files changed, 408 insertions(+), 238 deletions(-) diff --git a/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.spec.ts b/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.spec.ts index 13d365c9d..c7f7bb36f 100644 --- a/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.spec.ts +++ b/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.spec.ts @@ -47,14 +47,15 @@ describe('InfoDrawerComponent', () => { }; const extensionServiceMock = { getSidebarTabs: () => {}, - getAllowedSidebarActions: () => [ - { - id: 'app.sidebar.close', - order: 100, - title: 'close', - icon: 'highlight_off' - } - ] + getAllowedSidebarActions: () => + of([ + { + id: 'app.sidebar.close', + order: 100, + title: 'close', + icon: 'highlight_off' + } + ]) }; beforeEach(() => { diff --git a/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.ts b/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.ts index dd81e81ea..6d3d13c69 100644 --- a/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.ts +++ b/projects/aca-shared/src/lib/components/info-drawer/info-drawer.component.ts @@ -27,7 +27,7 @@ import { Component, HostListener, Input, OnChanges, OnDestroy, OnInit } from '@a import { MinimalNodeEntity, MinimalNodeEntryEntity, SiteEntry } from '@alfresco/js-api'; import { ContentActionRef, SidebarTabRef } from '@alfresco/adf-extensions'; import { Store } from '@ngrx/store'; -import { getAppSelection, SetInfoDrawerStateAction, ToggleInfoDrawerAction, infoDrawerPreview } from '@alfresco/aca-shared/store'; +import { SetInfoDrawerStateAction, ToggleInfoDrawerAction, infoDrawerPreview } from '@alfresco/aca-shared/store'; import { AppExtensionService } from '../../services/app.extension.service'; import { ContentApiService } from '../../services/content-api.service'; import { takeUntil } from 'rxjs/operators'; @@ -60,11 +60,11 @@ export class InfoDrawerComponent implements OnChanges, OnInit, OnDestroy { ngOnInit() { this.tabs = this.extensions.getSidebarTabs(); - this.store - .select(getAppSelection) + this.extensions + .getAllowedSidebarActions() .pipe(takeUntil(this.onDestroy$)) - .subscribe(() => { - this.actions = this.extensions.getAllowedSidebarActions(); + .subscribe((actions) => { + this.actions = actions; }); this.store diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.html b/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.html index 8bb294ef9..a39c6ebd7 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.html +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.html @@ -6,6 +6,7 @@ [color]="color" [attr.aria-label]="actionRef.description || actionRef.title | translate" [attr.title]="actionRef.description || actionRef.title | translate" + [disabled]="actionRef.disabled" (click)="runAction()" > diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html index c498390cd..b496dae3c 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.html @@ -5,6 +5,7 @@ [attr.aria-label]="actionRef.description || actionRef.title | translate" [attr.title]="actionRef.description || actionRef.title | translate" [matMenuTriggerFor]="menu" + [disabled]="actionRef.disabled" #matTrigger="matMenuTrigger" > diff --git a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts index 84fb7e277..932e41351 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts @@ -60,9 +60,17 @@ describe('AppExtensionService', () => { extensions = TestBed.inject(ExtensionService); }); - const applyConfig = (config: ExtensionConfig) => { + const applyConfig = (config: ExtensionConfig, selection?: boolean) => { extensions.setup(config); service.setup(config); + if (selection) { + service.selection = { + isEmpty: false, + count: 1, + libraries: null, + nodes: null + }; + } }; describe('configs', () => { @@ -284,7 +292,7 @@ describe('AppExtensionService', () => { }); describe('content actions', () => { - it('should load content actions from the config', () => { + it('should load content actions from the config', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -297,23 +305,26 @@ describe('AppExtensionService', () => { { id: 'aca:toolbar/separator-1', order: 1, - type: ContentActionType.separator, + type: ContentActionType.default, title: 'action1' }, { id: 'aca:toolbar/separator-2', order: 2, - type: ContentActionType.separator, + type: ContentActionType.default, title: 'action2' } ] } }); - expect(service.toolbarActions.length).toBe(2); + service.getAllowedToolbarActions().subscribe((actions) => { + expect(actions.length).toBe(2); + done(); + }); }); - it('should sort content actions by order', () => { + it('should sort content actions by order', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -326,27 +337,30 @@ describe('AppExtensionService', () => { { id: 'aca:toolbar/separator-2', order: 2, - type: ContentActionType.separator, + type: ContentActionType.default, title: 'action2' }, { id: 'aca:toolbar/separator-1', order: 1, - type: ContentActionType.separator, + type: ContentActionType.default, title: 'action1' } ] } }); - expect(service.toolbarActions.length).toBe(2); - expect(service.toolbarActions[0].id).toBe('aca:toolbar/separator-1'); - expect(service.toolbarActions[1].id).toBe('aca:toolbar/separator-2'); + service.getAllowedToolbarActions().subscribe((actions) => { + expect(actions.length).toBe(2); + expect(actions[0].id).toBe('aca:toolbar/separator-1'); + expect(actions[1].id).toBe('aca:toolbar/separator-2'); + done(); + }); }); }); describe('open with', () => { - it('should load [open with] actions for the viewer', () => { + it('should load [open with] actions for the viewer', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -373,10 +387,13 @@ describe('AppExtensionService', () => { } }); - expect(service.openWithActions.length).toBe(1); + service.getOpenWithActions().subscribe((actions) => { + expect(actions.length).toBe(1); + done(); + }); }); - it('should load only enabled [open with] actions for the viewer', () => { + it('should load only enabled [open with] actions for the viewer', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -413,11 +430,14 @@ describe('AppExtensionService', () => { } }); - expect(service.openWithActions.length).toBe(1); - expect(service.openWithActions[0].id).toBe('aca:viewer/action2'); + service.getOpenWithActions().subscribe((actions) => { + expect(actions.length).toBe(1); + expect(actions[0].id).toBe('aca:viewer/action2'); + done(); + }); }); - it('should sort [open with] actions by order', () => { + it('should sort [open with] actions by order', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -453,14 +473,17 @@ describe('AppExtensionService', () => { } }); - expect(service.openWithActions.length).toBe(2); - expect(service.openWithActions[0].id).toBe('aca:viewer/action1'); - expect(service.openWithActions[1].id).toBe('aca:viewer/action2'); + service.getOpenWithActions().subscribe((actions) => { + expect(actions.length).toBe(2); + expect(actions[0].id).toBe('aca:viewer/action1'); + expect(actions[1].id).toBe('aca:viewer/action2'); + done(); + }); }); }); describe('create', () => { - it('should load [create] actions from config', () => { + it('should load [create] actions from config', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -481,10 +504,13 @@ describe('AppExtensionService', () => { } }); - expect(service.createActions.length).toBe(1); + service.getCreateActions().subscribe((actions) => { + expect(actions.length).toBe(1); + done(); + }); }); - it('should sort [create] actions by order', () => { + it('should sort [create] actions by order', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -512,9 +538,12 @@ describe('AppExtensionService', () => { } }); - expect(service.createActions.length).toBe(2); - expect(service.createActions[0].id).toBe('aca:create/folder-2'); - expect(service.createActions[1].id).toBe('aca:create/folder'); + service.getCreateActions().subscribe((actions) => { + expect(actions.length).toBe(2); + expect(actions[0].id).toBe('aca:create/folder-2'); + expect(actions[1].id).toBe('aca:create/folder'); + done(); + }); }); }); @@ -700,7 +729,7 @@ describe('AppExtensionService', () => { }); describe('getSharedLinkViewerToolbarActions', () => { - it('should get shared link viewer actions', () => { + it('should get shared link viewer actions', (done) => { const actions = [ { id: 'id', @@ -724,23 +753,29 @@ describe('AppExtensionService', () => { } ]; - applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', - features: { - viewer: { - shared: { - toolbarActions: actions + applyConfig( + { + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + viewer: { + shared: { + toolbarActions: actions + } } } - } - }); + }, + true + ); - expect(service.getSharedLinkViewerToolbarActions()).toEqual(expectedActions); + service.getSharedLinkViewerToolbarActions().subscribe((sharedLinkViewerToolbarActions) => { + expect(sharedLinkViewerToolbarActions).toEqual(expectedActions); + done(); + }); }); }); @@ -793,7 +828,7 @@ describe('AppExtensionService', () => { }); describe('getHeaderActions', () => { - it('should load user actions from the config', () => { + it('should load user actions from the config', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -806,21 +841,24 @@ describe('AppExtensionService', () => { { id: 'header.action.separator.1', order: 1, - type: ContentActionType.separator + type: ContentActionType.default }, { id: 'header.action.separator.2', order: 2, - type: ContentActionType.separator + type: ContentActionType.default } ] } }); - expect(service.headerActions.length).toBe(2); + service.getHeaderActions().subscribe((headerActions) => { + expect(headerActions.length).toBe(2); + done(); + }); }); - it('should sort header actions by order', () => { + it('should sort header actions by order', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -844,12 +882,15 @@ describe('AppExtensionService', () => { } }); - const actions = service.getHeaderActions(); - expect(actions[0].id).toBe('header.action.2'); - expect(actions[1].id).toBe('header.action.1'); + service.getHeaderActions().subscribe((headerActions) => { + expect(headerActions.length).toBe(2); + expect(headerActions[0].id).toBe('header.action.2'); + expect(headerActions[1].id).toBe('header.action.1'); + done(); + }); }); - it('should sort header menu children actions by order', () => { + it('should sort header menu children actions by order', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -880,9 +921,12 @@ describe('AppExtensionService', () => { } }); - const actions = service.getHeaderActions()[0]; - expect(actions.children[0].id).toBe('header.action.2'); - expect(actions.children[1].id).toBe('header.action.1'); + service.getHeaderActions().subscribe((headerActions) => { + expect(headerActions.length).toBe(1); + expect(headerActions[0].children[0].id).toBe('header.action.2'); + expect(headerActions[0].children[1].id).toBe('header.action.1'); + done(); + }); }); }); @@ -1030,7 +1074,7 @@ describe('AppExtensionService', () => { } ]; - it('should set the action disabled for create actions', () => { + it('should set the action disabled for create actions', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -1043,10 +1087,13 @@ describe('AppExtensionService', () => { } }); - expect(service.getCreateActions()).toEqual(expectedActionsWithChildren); + service.getCreateActions().subscribe((createActions) => { + expect(createActions).toEqual(expectedActionsWithChildren); + done(); + }); }); - it('should set the action disabled for sidebar actions', () => { + it('should set the action disabled for sidebar actions', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -1061,10 +1108,13 @@ describe('AppExtensionService', () => { } }); - expect(service.getAllowedSidebarActions()).toEqual(expectedActionsWithoutChildren); + service.getAllowedSidebarActions().subscribe((serviceActions) => { + expect(serviceActions).toEqual(expectedActionsWithoutChildren); + done(); + }); }); - it('should set the action disabled for toolbar actions', () => { + it('should set the action disabled for toolbar actions', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -1077,10 +1127,13 @@ describe('AppExtensionService', () => { } }); - expect(service.getAllowedToolbarActions()).toEqual(expectedActionsWithoutChildren); + service.getAllowedToolbarActions().subscribe((serviceActions) => { + expect(serviceActions).toEqual(expectedActionsWithoutChildren); + done(); + }); }); - it('should set the action disabled for viewer toolbar actions', () => { + it('should set the action disabled for viewer toolbar actions', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -1093,30 +1146,39 @@ describe('AppExtensionService', () => { } }); - expect(service.getViewerToolbarActions()).toEqual(expectedActionsWithoutChildren); + service.getViewerToolbarActions().subscribe((serviceActions) => { + expect(serviceActions).toEqual(expectedActionsWithoutChildren); + done(); + }); }); - it('should set the action disabled for shared link viewer toolbar actions', () => { - applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', - features: { - viewer: { - shared: { - toolbarActions: actions + it('should set the action disabled for shared link viewer toolbar actions', (done) => { + applyConfig( + { + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + viewer: { + shared: { + toolbarActions: actions + } } } - } - }); + }, + true + ); - expect(service.getSharedLinkViewerToolbarActions()).toEqual(expectedActionsWithoutChildren); + service.getSharedLinkViewerToolbarActions().subscribe((serviceActions) => { + expect(serviceActions).toEqual(expectedActionsWithoutChildren); + done(); + }); }); - it('should set the action disabled for header actions', () => { + it('should set the action disabled for header actions', (done) => { applyConfig({ $id: 'test', $name: 'test', @@ -1129,23 +1191,32 @@ describe('AppExtensionService', () => { } }); - expect(service.getHeaderActions()).toEqual(expectedActionsWithoutChildren); + service.getHeaderActions().subscribe((serviceActions) => { + expect(serviceActions).toEqual(expectedActionsWithoutChildren); + done(); + }); }); - it('should set the action disabled for context menu actions', () => { - applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', - features: { - contextMenu: actions - } - }); + it('should set the action disabled for context menu actions', (done) => { + applyConfig( + { + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + contextMenu: actions + } + }, + true + ); - expect(service.getAllowedContextMenuActions()).toEqual(expectedActionsWithoutChildren); + service.getAllowedContextMenuActions().subscribe((serviceActions) => { + expect(serviceActions).toEqual(expectedActionsWithoutChildren); + done(); + }); }); }); }); diff --git a/projects/aca-shared/src/lib/services/app.extension.service.ts b/projects/aca-shared/src/lib/services/app.extension.service.ts index 1332ffc67..6d2b7d5f0 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.ts @@ -56,6 +56,7 @@ import { RepositoryInfo, NodeEntry } from '@alfresco/js-api'; import { ViewerRules } from '../models/viewer.rules'; import { SettingsGroupRef } from '../models/types'; import { NodePermissionService } from '../services/node-permission.service'; +import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -63,21 +64,22 @@ import { NodePermissionService } from '../services/node-permission.service'; export class AppExtensionService implements RuleContext { private _references = new BehaviorSubject([]); - headerActions: Array = []; - toolbarActions: Array = []; - viewerToolbarActions: Array = []; - sharedLinkViewerToolbarActions: Array = []; - contextMenuActions: Array = []; - openWithActions: Array = []; - createActions: Array = []; navbar: Array = []; sidebarTabs: Array = []; - sidebarActions: Array = []; contentMetadata: any; search: any; viewerRules: ViewerRules = {}; settingGroups: Array = []; + private _headerActions = new BehaviorSubject>([]); + private _toolbarActions = new BehaviorSubject>([]); + private _viewerToolbarActions = new BehaviorSubject>([]); + private _sharedLinkViewerToolbarActions = new BehaviorSubject>([]); + private _contextMenuActions = new BehaviorSubject>([]); + private _openWithActions = new BehaviorSubject>([]); + private _createActions = new BehaviorSubject>([]); + private _sidebarActions = new BehaviorSubject>([]); + documentListPresets: { files: Array; libraries: Array; @@ -106,6 +108,8 @@ export class AppExtensionService implements RuleContext { references$: Observable; + config: ExtensionConfig; + constructor( public auth: AuthenticationService, protected store: Store, @@ -124,12 +128,16 @@ export class AppExtensionService implements RuleContext { this.navigation = result.navigation; this.profile = result.profile; this.repository = result.repository; + + if (this.config) { + this.setup(this.config); + } }); } async load() { - const config = await this.extensions.load(); - this.setup(config); + this.config = await this.extensions.load(); + this.setup(this.config); } setup(config: ExtensionConfig) { @@ -140,15 +148,15 @@ export class AppExtensionService implements RuleContext { this.settingGroups = this.loader.getElements(config, 'settings'); - this.headerActions = this.loader.getContentActions(config, 'features.header'); - this.sidebarActions = this.loader.getContentActions(config, 'features.sidebar.toolbar'); - this.toolbarActions = this.loader.getContentActions(config, 'features.toolbar'); - this.viewerToolbarActions = this.loader.getContentActions(config, 'features.viewer.toolbarActions'); - this.sharedLinkViewerToolbarActions = this.loader.getContentActions(config, 'features.viewer.shared.toolbarActions'); + this._headerActions.next(this.loader.getContentActions(config, 'features.header')); + this._sidebarActions.next(this.loader.getContentActions(config, 'features.sidebar.toolbar')); + this._toolbarActions.next(this.loader.getContentActions(config, 'features.toolbar')); + this._viewerToolbarActions.next(this.loader.getContentActions(config, 'features.viewer.toolbarActions')); + this._sharedLinkViewerToolbarActions.next(this.loader.getContentActions(config, 'features.viewer.shared.toolbarActions')); + this._contextMenuActions.next(this.loader.getContentActions(config, 'features.contextMenu')); + this._openWithActions.next(this.loader.getContentActions(config, 'features.viewer.openWith')); + this._createActions.next(this.loader.getElements(config, 'features.create')); - this.contextMenuActions = this.loader.getContentActions(config, 'features.contextMenu'); - this.openWithActions = this.loader.getContentActions(config, 'features.viewer.openWith'); - this.createActions = this.loader.getElements(config, 'features.create'); this.navbar = this.loadNavBar(config); this.sidebarTabs = this.loader.getElements(config, 'features.sidebar.tabs'); this.contentMetadata = this.loadContentMetadata(config); @@ -341,12 +349,16 @@ export class AppExtensionService implements RuleContext { }; } - getCreateActions(): Array { - return this.createActions - .filter((action) => this.filterVisible(action)) - .map((action) => this.copyAction(action)) - .map((action) => this.buildMenu(action)) - .map((action) => this.setActionDisabledFromRule(action)); + getCreateActions(): Observable> { + return this._createActions.pipe( + map((createActions) => + createActions + .filter((action) => this.filterVisible(action)) + .map((action) => this.copyAction(action)) + .map((action) => this.buildMenu(action)) + .map((action) => this.setActionDisabledFromRule(action)) + ) + ); } private buildMenu(actionRef: ContentActionRef): ContentActionRef { @@ -385,48 +397,58 @@ export class AppExtensionService implements RuleContext { .reduce(reduceSeparators, []); } - getAllowedSidebarActions(): Array { - return this.getAllowedActions(this.sidebarActions); + getAllowedSidebarActions(): Observable> { + return this._sidebarActions.pipe(map((sidebarActions) => this.getAllowedActions(sidebarActions))); } - getAllowedToolbarActions(): Array { - return this.getAllowedActions(this.toolbarActions); + getAllowedToolbarActions(): Observable> { + return this._toolbarActions.pipe(map((toolbarActions) => this.getAllowedActions(toolbarActions))); } - getViewerToolbarActions(): Array { - return this.getAllowedActions(this.viewerToolbarActions); + getViewerToolbarActions(): Observable> { + return this._viewerToolbarActions.pipe(map((viewerToolbarActions) => this.getAllowedActions(viewerToolbarActions))); } - getSharedLinkViewerToolbarActions(): Array { - return this.getAllowedActions(this.sharedLinkViewerToolbarActions); + getOpenWithActions(): Observable> { + return this._openWithActions.pipe(map((openWithActions) => this.getAllowedActions(openWithActions))); } - getHeaderActions(): Array { - return this.headerActions - .filter((action) => this.filterVisible(action)) - .map((action) => { - if (action.type === ContentActionType.menu) { - const copy = this.copyAction(action); - if (copy.children && copy.children.length > 0) { - copy.children = copy.children - .filter((childAction) => this.filterVisible(childAction)) - .sort(sortByOrder) - .reduce(reduceEmptyMenus, []) - .reduce(reduceSeparators, []); - } - return copy; - } - - return action; - }) - .map((action) => this.setActionDisabledFromRule(action)) - .sort(sortByOrder) - .reduce(reduceEmptyMenus, []) - .reduce(reduceSeparators, []); + getSharedLinkViewerToolbarActions(): Observable> { + return this._sharedLinkViewerToolbarActions.pipe( + map((sharedLinkViewerToolbarActions) => (!this.selection.isEmpty ? this.getAllowedActions(sharedLinkViewerToolbarActions) : [])) + ); } - getAllowedContextMenuActions(): Array { - return this.getAllowedActions(this.contextMenuActions); + getHeaderActions(): Observable> { + return this._headerActions.pipe( + map((headerActions) => + headerActions + .filter((action) => this.filterVisible(action)) + .map((action) => { + if (action.type === ContentActionType.menu) { + const copy = this.copyAction(action); + if (copy.children && copy.children.length > 0) { + copy.children = copy.children + .filter((childAction) => this.filterVisible(childAction)) + .sort(sortByOrder) + .reduce(reduceEmptyMenus, []) + .reduce(reduceSeparators, []); + } + return copy; + } + + return action; + }) + .map((action) => this.setActionDisabledFromRule(action)) + .sort(sortByOrder) + .reduce(reduceEmptyMenus, []) + .reduce(reduceSeparators, []) + ) + ); + } + + getAllowedContextMenuActions(): Observable> { + return this._contextMenuActions.pipe(map((contextMenuActions) => (!this.selection.isEmpty ? this.getAllowedActions(contextMenuActions) : []))); } getSettingsGroups(): Array { diff --git a/src/app/components/context-menu/context-menu.component.spec.ts b/src/app/components/context-menu/context-menu.component.spec.ts index 72f6181e8..1102f28ac 100644 --- a/src/app/components/context-menu/context-menu.component.spec.ts +++ b/src/app/components/context-menu/context-menu.component.spec.ts @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . */ -import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { AppTestingModule } from '../../testing/app-testing.module'; import { ContextMenuComponent } from './context-menu.component'; import { ContextMenuModule } from './context-menu.module'; @@ -75,7 +75,7 @@ describe('ContextMenuComponent', () => { contextMenuOverlayRef = TestBed.inject(ContextMenuOverlayRef); extensionsService = TestBed.inject(AppExtensionService); - spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue([contextItem]); + spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of([contextItem])); fixture.detectChanges(); }); @@ -85,15 +85,16 @@ describe('ContextMenuComponent', () => { expect(contextMenuOverlayRef.close).toHaveBeenCalled(); }); - it('should render defined context menu actions items', fakeAsync(() => { + it('should render defined context menu actions items', async () => { component.ngAfterViewInit(); - tick(500); + fixture.detectChanges(); + await fixture.whenStable(); const contextMenuElements = document.body.querySelector('.aca-context-menu').querySelectorAll('button'); expect(contextMenuElements.length).toBe(1); expect(contextMenuElements[0].querySelector('span').innerText).toBe(contextItem.title); - })); + }); it('should run action with provided action id', () => { spyOn(extensionsService, 'runActionById'); diff --git a/src/app/components/context-menu/context-menu.component.ts b/src/app/components/context-menu/context-menu.component.ts index d31402259..38a33a6b9 100644 --- a/src/app/components/context-menu/context-menu.component.ts +++ b/src/app/components/context-menu/context-menu.component.ts @@ -25,8 +25,6 @@ import { Component, ViewEncapsulation, OnInit, OnDestroy, HostListener, ViewChild, AfterViewInit, Inject } from '@angular/core'; import { MatMenuTrigger } from '@angular/material/menu'; -import { AppStore, getAppSelection } from '@alfresco/aca-shared/store'; -import { Store } from '@ngrx/store'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ContentActionRef } from '@alfresco/adf-extensions'; @@ -62,7 +60,6 @@ export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit { constructor( private contextMenuOverlayRef: ContextMenuOverlayRef, private extensions: AppExtensionService, - private store: Store, @Inject(CONTEXT_MENU_DIRECTION) public direction: Direction ) {} @@ -82,14 +79,10 @@ export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit { } ngOnInit() { - this.store - .select(getAppSelection) + this.extensions + .getAllowedContextMenuActions() .pipe(takeUntil(this.onDestroy$)) - .subscribe((selection) => { - if (selection.count) { - this.actions = this.extensions.getAllowedContextMenuActions(); - } - }); + .subscribe((actions) => (this.actions = actions)); } ngAfterViewInit() { diff --git a/src/app/components/create-menu/create-menu.component.spec.ts b/src/app/components/create-menu/create-menu.component.spec.ts index 88b6fc52d..f866b92f4 100644 --- a/src/app/components/create-menu/create-menu.component.spec.ts +++ b/src/app/components/create-menu/create-menu.component.spec.ts @@ -34,6 +34,7 @@ import { By } from '@angular/platform-browser'; import { AppTestingModule } from '../../testing/app-testing.module'; import { MatMenuModule } from '@angular/material/menu'; import { MatButtonModule } from '@angular/material/button'; +import { of } from 'rxjs'; describe('CreateMenuComponent', () => { let fixture: ComponentFixture; @@ -47,13 +48,15 @@ describe('CreateMenuComponent', () => { extensionService = TestBed.inject(AppExtensionService); getCreateActionsSpy = spyOn(extensionService, 'getCreateActions'); - getCreateActionsSpy.and.returnValue([ - { - id: 'action1', - type: ContentActionType.button, - title: 'action one' - } - ]); + getCreateActionsSpy.and.returnValue( + of([ + { + id: 'action1', + type: ContentActionType.button, + title: 'action one' + } + ]) + ); fixture = TestBed.createComponent(CreateMenuComponent); }); @@ -102,20 +105,22 @@ describe('CreateMenuComponent', () => { }); it('should render sub-menus', async () => { - getCreateActionsSpy.and.returnValue([ - { - id: 'level1', - type: ContentActionType.menu, - title: 'level one', - children: [ - { - id: 'level2', - type: ContentActionType.button, - title: 'level two' - } - ] - } - ]); + getCreateActionsSpy.and.returnValue( + of([ + { + id: 'level1', + type: ContentActionType.menu, + title: 'level one', + children: [ + { + id: 'level2', + type: ContentActionType.button, + title: 'level two' + } + ] + } + ]) + ); await clickMenu(); diff --git a/src/app/components/create-menu/create-menu.component.ts b/src/app/components/create-menu/create-menu.component.ts index 1e8e0c6ed..3ece99851 100644 --- a/src/app/components/create-menu/create-menu.component.ts +++ b/src/app/components/create-menu/create-menu.component.ts @@ -25,8 +25,6 @@ import { Component, Input, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'; import { ContentActionRef } from '@alfresco/adf-extensions'; -import { AppStore, getRuleContext } from '@alfresco/aca-shared/store'; -import { Store } from '@ngrx/store'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { AppExtensionService } from '@alfresco/aca-shared'; @@ -48,14 +46,14 @@ export class CreateMenuComponent implements OnInit, OnDestroy { @Input() expanded: boolean; - constructor(private store: Store, private extensions: AppExtensionService) {} + constructor(private extensions: AppExtensionService) {} ngOnInit() { - this.store - .select(getRuleContext) + this.extensions + .getCreateActions() .pipe(takeUntil(this.onDestroy$)) - .subscribe(() => { - this.createActions = this.extensions.getCreateActions(); + .subscribe((createActions) => { + this.createActions = createActions; }); } diff --git a/src/app/components/header/header.component.spec.ts b/src/app/components/header/header.component.spec.ts index c9723a84c..d16723ca2 100644 --- a/src/app/components/header/header.component.spec.ts +++ b/src/app/components/header/header.component.spec.ts @@ -26,11 +26,17 @@ import { AppHeaderComponent } from './header.component'; import { AppState } from '@alfresco/aca-shared/store'; import { of } from 'rxjs'; -import { fakeAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ContentActionRef } from '@alfresco/adf-extensions'; +import { Store } from '@ngrx/store'; +import { AppTestingModule } from '../../testing/app-testing.module'; +import { AppExtensionService } from '../../../../projects/aca-shared/src/lib/services/app.extension.service'; +import { CoreModule } from '@alfresco/adf-core'; +import { AppSearchInputModule } from '../search/search-input.module'; describe('AppHeaderComponent', () => { let component: AppHeaderComponent; + let fixture: ComponentFixture; const actions = [ { id: 'action-1', type: 'button' }, @@ -38,11 +44,12 @@ describe('AppHeaderComponent', () => { ] as Array; const store = { - select: jasmine.createSpy('select') + select: jasmine.createSpy('select'), + dispatch: () => {} } as any; const appExtensionService = { - getHeaderActions: () => actions + getHeaderActions: () => of(actions) } as any; const app = { @@ -52,11 +59,27 @@ describe('AppHeaderComponent', () => { } as AppState; beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AppTestingModule, CoreModule.forChild(), AppSearchInputModule], + declarations: [AppHeaderComponent], + providers: [ + { + provide: AppExtensionService, + useValue: appExtensionService + }, + { + provide: Store, + useValue: store + } + ] + }); + store.select.and.callFake((memoizeFn) => { return of(memoizeFn({ app })); }); - component = new AppHeaderComponent(store, appExtensionService); + fixture = TestBed.createComponent(AppHeaderComponent); + component = fixture.componentInstance; }); it('should set header color, name and logo', fakeAsync(() => { @@ -65,8 +88,9 @@ describe('AppHeaderComponent', () => { component.headerColor$.subscribe((val) => expect(val).toBe(app.headerColor)); })); - it('should get header actions', () => { + it('should get header actions', fakeAsync(() => { component.ngOnInit(); + tick(); expect(component.actions).toEqual(actions); - }); + })); }); diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts index 67bf15c9a..492a14843 100644 --- a/src/app/components/header/header.component.ts +++ b/src/app/components/header/header.component.ts @@ -23,12 +23,13 @@ * along with Alfresco. If not, see . */ -import { Component, ViewEncapsulation, Output, EventEmitter, OnInit, Input } from '@angular/core'; +import { Component, ViewEncapsulation, Output, EventEmitter, OnInit, Input, OnDestroy } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { ContentActionRef } from '@alfresco/adf-extensions'; import { AppStore, getHeaderColor, getAppName, getLogoPath, getHeaderImagePath } from '@alfresco/aca-shared/store'; import { AppExtensionService } from '@alfresco/aca-shared'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'app-header', @@ -37,7 +38,8 @@ import { AppExtensionService } from '@alfresco/aca-shared'; encapsulation: ViewEncapsulation.None, host: { class: 'app-header' } }) -export class AppHeaderComponent implements OnInit { +export class AppHeaderComponent implements OnInit, OnDestroy { + private onDestroy$: Subject = new Subject(); @Output() toggleClicked = new EventEmitter(); @@ -60,7 +62,17 @@ export class AppHeaderComponent implements OnInit { } ngOnInit() { - this.actions = this.appExtensions.getHeaderActions(); + this.appExtensions + .getHeaderActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.actions = actions; + }); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); } trackByActionId(_: number, action: ContentActionRef) { diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index 9efd2b754..0e6ff2f03 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -87,11 +87,23 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { .pipe(takeUntil(this.onDestroy$)) .subscribe((selection) => { this.selection = selection; - this.actions = this.extensions.getAllowedToolbarActions(); - this.viewerToolbarActions = this.extensions.getViewerToolbarActions(); this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first); }); + this.extensions + .getAllowedToolbarActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.actions = actions; + }); + + this.extensions + .getViewerToolbarActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.viewerToolbarActions = actions; + }); + this.store .select(getCurrentFolder) .pipe(takeUntil(this.onDestroy$)) diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts index a5e7c5fb3..62102860f 100644 --- a/src/app/components/preview/preview.component.ts +++ b/src/app/components/preview/preview.component.ts @@ -153,7 +153,12 @@ export class PreviewComponent extends PageComponent implements OnInit, OnDestroy .subscribe(() => {}) ]); - this.openWith = this.extensions.openWithActions; + this.extensions + .getOpenWithActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.openWith = actions; + }); } ngOnDestroy() { diff --git a/src/app/components/shared-link-view/shared-link-view.component.spec.ts b/src/app/components/shared-link-view/shared-link-view.component.spec.ts index 0bb18dcb4..a87c40f1e 100644 --- a/src/app/components/shared-link-view/shared-link-view.component.spec.ts +++ b/src/app/components/shared-link-view/shared-link-view.component.spec.ts @@ -64,6 +64,12 @@ describe('SharedLinkViewComponent', () => { fixture = TestBed.createComponent(SharedLinkViewComponent); component = fixture.componentInstance; appExtensionService = TestBed.inject(AppExtensionService); + appExtensionService.selection = { + isEmpty: true, + count: 0, + libraries: null, + nodes: null + }; spyGetSharedLink = spyOn(component['sharedLinksApi'], 'getSharedLink'); @@ -93,8 +99,6 @@ describe('SharedLinkViewComponent', () => { })); it('should not update actions reference if selection is empty', fakeAsync(() => { - spyOn(storeMock, 'select').and.returnValue(of({ isEmpty: true })); - spyGetSharedLink.and.returnValue(Promise.resolve({ entry: { id: 'shared-id' } })); fixture.detectChanges(); @@ -104,8 +108,13 @@ describe('SharedLinkViewComponent', () => { })); it('should update actions reference if selection is not empty', fakeAsync(() => { - spyOn(storeMock, 'select').and.returnValue(of({ isEmpty: false })); - spyOn(appExtensionService, 'getSharedLinkViewerToolbarActions'); + appExtensionService.selection = { + isEmpty: false, + count: 1, + libraries: null, + nodes: null + }; + spyOn(appExtensionService, 'getSharedLinkViewerToolbarActions').and.callThrough(); spyGetSharedLink.and.returnValue(Promise.resolve({ entry: { id: 'shared-id' } })); fixture.detectChanges(); diff --git a/src/app/components/shared-link-view/shared-link-view.component.ts b/src/app/components/shared-link-view/shared-link-view.component.ts index 86677a585..9dde3e92d 100644 --- a/src/app/components/shared-link-view/shared-link-view.component.ts +++ b/src/app/components/shared-link-view/shared-link-view.component.ts @@ -23,15 +23,15 @@ * along with Alfresco. If not, see . */ -import { AppStore, SetSelectedNodesAction, getAppSelection } from '@alfresco/aca-shared/store'; +import { AppStore, SetSelectedNodesAction } from '@alfresco/aca-shared/store'; import { AlfrescoApiService } from '@alfresco/adf-core'; import { ContentActionRef } from '@alfresco/adf-extensions'; import { SharedLinkEntry, SharedlinksApi } from '@alfresco/js-api'; -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; -import { forkJoin, from, of } from 'rxjs'; -import { catchError, mergeMap } from 'rxjs/operators'; +import { forkJoin, from, of, Subject } from 'rxjs'; +import { catchError, mergeMap, takeUntil } from 'rxjs/operators'; import { AppExtensionService } from '@alfresco/aca-shared'; @Component({ @@ -41,7 +41,8 @@ import { AppExtensionService } from '@alfresco/aca-shared'; encapsulation: ViewEncapsulation.None, host: { class: 'app-shared-link-view' } }) -export class SharedLinkViewComponent implements OnInit { +export class SharedLinkViewComponent implements OnInit, OnDestroy { + private onDestroy$: Subject = new Subject(); private sharedLinksApi: SharedlinksApi; sharedLinkId: string = null; viewerToolbarActions: Array = []; @@ -69,9 +70,17 @@ export class SharedLinkViewComponent implements OnInit { this.sharedLinkId = sharedId; }); - this.store.select(getAppSelection).subscribe((selection) => { - if (!selection.isEmpty) this.viewerToolbarActions = this.extensions.getSharedLinkViewerToolbarActions(); - }); + this.extensions + .getSharedLinkViewerToolbarActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.viewerToolbarActions = actions; + }); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); } trackByActionId(_: number, action: ContentActionRef) { diff --git a/src/app/components/viewer/viewer.component.ts b/src/app/components/viewer/viewer.component.ts index fb60700a9..2897e323e 100644 --- a/src/app/components/viewer/viewer.component.ts +++ b/src/app/components/viewer/viewer.component.ts @@ -27,7 +27,7 @@ import { AppExtensionService, AppHookService, ContentApiService } from '@alfresc import { AppStore, ClosePreviewAction, - getRuleContext, + getAppSelection, isInfoDrawerOpened, RefreshPreviewAction, ReloadDocumentListAction, @@ -131,18 +131,24 @@ export class AppViewerComponent implements OnInit, OnDestroy { }); this.store - .select(getRuleContext) + .select(getAppSelection) .pipe(takeUntil(this.onDestroy$)) - .subscribe((ruleContext) => { - this.selection = ruleContext.selection; + .subscribe((selection) => { + this.selection = selection; + }); - if (this.toolbarActions.length === 0) { - this.toolbarActions = this.extensions.getViewerToolbarActions(); - } + this.extensions + .getViewerToolbarActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.toolbarActions = actions; + }); - if (this.openWith.length === 0) { - this.openWith = this.extensions.openWithActions; - } + this.extensions + .getOpenWithActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.openWith = actions; }); this.route.params.subscribe((params) => {