[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
This commit is contained in:
Pablo Martinez Garcia
2021-09-16 15:48:43 +02:00
committed by GitHub
parent f89833461c
commit fb140531f4
17 changed files with 408 additions and 238 deletions

View File

@@ -47,14 +47,15 @@ describe('InfoDrawerComponent', () => {
}; };
const extensionServiceMock = { const extensionServiceMock = {
getSidebarTabs: () => {}, getSidebarTabs: () => {},
getAllowedSidebarActions: () => [ getAllowedSidebarActions: () =>
{ of([
id: 'app.sidebar.close', {
order: 100, id: 'app.sidebar.close',
title: 'close', order: 100,
icon: 'highlight_off' title: 'close',
} icon: 'highlight_off'
] }
])
}; };
beforeEach(() => { beforeEach(() => {

View File

@@ -27,7 +27,7 @@ import { Component, HostListener, Input, OnChanges, OnDestroy, OnInit } from '@a
import { MinimalNodeEntity, MinimalNodeEntryEntity, SiteEntry } from '@alfresco/js-api'; import { MinimalNodeEntity, MinimalNodeEntryEntity, SiteEntry } from '@alfresco/js-api';
import { ContentActionRef, SidebarTabRef } from '@alfresco/adf-extensions'; import { ContentActionRef, SidebarTabRef } from '@alfresco/adf-extensions';
import { Store } from '@ngrx/store'; 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 { AppExtensionService } from '../../services/app.extension.service';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@@ -60,11 +60,11 @@ export class InfoDrawerComponent implements OnChanges, OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.tabs = this.extensions.getSidebarTabs(); this.tabs = this.extensions.getSidebarTabs();
this.store this.extensions
.select(getAppSelection) .getAllowedSidebarActions()
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe(() => { .subscribe((actions) => {
this.actions = this.extensions.getAllowedSidebarActions(); this.actions = actions;
}); });
this.store this.store

View File

@@ -6,6 +6,7 @@
[color]="color" [color]="color"
[attr.aria-label]="actionRef.description || actionRef.title | translate" [attr.aria-label]="actionRef.description || actionRef.title | translate"
[attr.title]="actionRef.description || actionRef.title | translate" [attr.title]="actionRef.description || actionRef.title | translate"
[disabled]="actionRef.disabled"
(click)="runAction()" (click)="runAction()"
> >
<adf-icon [value]="actionRef.icon"></adf-icon> <adf-icon [value]="actionRef.icon"></adf-icon>

View File

@@ -5,6 +5,7 @@
[attr.aria-label]="actionRef.description || actionRef.title | translate" [attr.aria-label]="actionRef.description || actionRef.title | translate"
[attr.title]="actionRef.description || actionRef.title | translate" [attr.title]="actionRef.description || actionRef.title | translate"
[matMenuTriggerFor]="menu" [matMenuTriggerFor]="menu"
[disabled]="actionRef.disabled"
#matTrigger="matMenuTrigger" #matTrigger="matMenuTrigger"
> >
<adf-icon [value]="actionRef.icon"></adf-icon> <adf-icon [value]="actionRef.icon"></adf-icon>

View File

@@ -60,9 +60,17 @@ describe('AppExtensionService', () => {
extensions = TestBed.inject(ExtensionService); extensions = TestBed.inject(ExtensionService);
}); });
const applyConfig = (config: ExtensionConfig) => { const applyConfig = (config: ExtensionConfig, selection?: boolean) => {
extensions.setup(config); extensions.setup(config);
service.setup(config); service.setup(config);
if (selection) {
service.selection = {
isEmpty: false,
count: 1,
libraries: null,
nodes: null
};
}
}; };
describe('configs', () => { describe('configs', () => {
@@ -284,7 +292,7 @@ describe('AppExtensionService', () => {
}); });
describe('content actions', () => { describe('content actions', () => {
it('should load content actions from the config', () => { it('should load content actions from the config', (done) => {
applyConfig({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -297,23 +305,26 @@ describe('AppExtensionService', () => {
{ {
id: 'aca:toolbar/separator-1', id: 'aca:toolbar/separator-1',
order: 1, order: 1,
type: ContentActionType.separator, type: ContentActionType.default,
title: 'action1' title: 'action1'
}, },
{ {
id: 'aca:toolbar/separator-2', id: 'aca:toolbar/separator-2',
order: 2, order: 2,
type: ContentActionType.separator, type: ContentActionType.default,
title: 'action2' 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({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -326,27 +337,30 @@ describe('AppExtensionService', () => {
{ {
id: 'aca:toolbar/separator-2', id: 'aca:toolbar/separator-2',
order: 2, order: 2,
type: ContentActionType.separator, type: ContentActionType.default,
title: 'action2' title: 'action2'
}, },
{ {
id: 'aca:toolbar/separator-1', id: 'aca:toolbar/separator-1',
order: 1, order: 1,
type: ContentActionType.separator, type: ContentActionType.default,
title: 'action1' title: 'action1'
} }
] ]
} }
}); });
expect(service.toolbarActions.length).toBe(2); service.getAllowedToolbarActions().subscribe((actions) => {
expect(service.toolbarActions[0].id).toBe('aca:toolbar/separator-1'); expect(actions.length).toBe(2);
expect(service.toolbarActions[1].id).toBe('aca:toolbar/separator-2'); expect(actions[0].id).toBe('aca:toolbar/separator-1');
expect(actions[1].id).toBe('aca:toolbar/separator-2');
done();
});
}); });
}); });
describe('open with', () => { describe('open with', () => {
it('should load [open with] actions for the viewer', () => { it('should load [open with] actions for the viewer', (done) => {
applyConfig({ applyConfig({
$id: 'test', $id: 'test',
$name: '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({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -413,11 +430,14 @@ describe('AppExtensionService', () => {
} }
}); });
expect(service.openWithActions.length).toBe(1); service.getOpenWithActions().subscribe((actions) => {
expect(service.openWithActions[0].id).toBe('aca:viewer/action2'); 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({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -453,14 +473,17 @@ describe('AppExtensionService', () => {
} }
}); });
expect(service.openWithActions.length).toBe(2); service.getOpenWithActions().subscribe((actions) => {
expect(service.openWithActions[0].id).toBe('aca:viewer/action1'); expect(actions.length).toBe(2);
expect(service.openWithActions[1].id).toBe('aca:viewer/action2'); expect(actions[0].id).toBe('aca:viewer/action1');
expect(actions[1].id).toBe('aca:viewer/action2');
done();
});
}); });
}); });
describe('create', () => { describe('create', () => {
it('should load [create] actions from config', () => { it('should load [create] actions from config', (done) => {
applyConfig({ applyConfig({
$id: 'test', $id: 'test',
$name: '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({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -512,9 +538,12 @@ describe('AppExtensionService', () => {
} }
}); });
expect(service.createActions.length).toBe(2); service.getCreateActions().subscribe((actions) => {
expect(service.createActions[0].id).toBe('aca:create/folder-2'); expect(actions.length).toBe(2);
expect(service.createActions[1].id).toBe('aca:create/folder'); 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', () => { describe('getSharedLinkViewerToolbarActions', () => {
it('should get shared link viewer actions', () => { it('should get shared link viewer actions', (done) => {
const actions = [ const actions = [
{ {
id: 'id', id: 'id',
@@ -724,23 +753,29 @@ describe('AppExtensionService', () => {
} }
]; ];
applyConfig({ applyConfig(
$id: 'test', {
$name: 'test', $id: 'test',
$version: '1.0.0', $name: 'test',
$license: 'MIT', $version: '1.0.0',
$vendor: 'Good company', $license: 'MIT',
$runtime: '1.5.0', $vendor: 'Good company',
features: { $runtime: '1.5.0',
viewer: { features: {
shared: { viewer: {
toolbarActions: actions 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', () => { describe('getHeaderActions', () => {
it('should load user actions from the config', () => { it('should load user actions from the config', (done) => {
applyConfig({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -806,21 +841,24 @@ describe('AppExtensionService', () => {
{ {
id: 'header.action.separator.1', id: 'header.action.separator.1',
order: 1, order: 1,
type: ContentActionType.separator type: ContentActionType.default
}, },
{ {
id: 'header.action.separator.2', id: 'header.action.separator.2',
order: 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({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -844,12 +882,15 @@ describe('AppExtensionService', () => {
} }
}); });
const actions = service.getHeaderActions(); service.getHeaderActions().subscribe((headerActions) => {
expect(actions[0].id).toBe('header.action.2'); expect(headerActions.length).toBe(2);
expect(actions[1].id).toBe('header.action.1'); 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({ applyConfig({
$id: 'test', $id: 'test',
$name: 'test', $name: 'test',
@@ -880,9 +921,12 @@ describe('AppExtensionService', () => {
} }
}); });
const actions = service.getHeaderActions()[0]; service.getHeaderActions().subscribe((headerActions) => {
expect(actions.children[0].id).toBe('header.action.2'); expect(headerActions.length).toBe(1);
expect(actions.children[1].id).toBe('header.action.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({ applyConfig({
$id: 'test', $id: 'test',
$name: '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({ applyConfig({
$id: 'test', $id: 'test',
$name: '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({ applyConfig({
$id: 'test', $id: 'test',
$name: '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({ applyConfig({
$id: 'test', $id: 'test',
$name: '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', () => { it('should set the action disabled for shared link viewer toolbar actions', (done) => {
applyConfig({ applyConfig(
$id: 'test', {
$name: 'test', $id: 'test',
$version: '1.0.0', $name: 'test',
$license: 'MIT', $version: '1.0.0',
$vendor: 'Good company', $license: 'MIT',
$runtime: '1.5.0', $vendor: 'Good company',
features: { $runtime: '1.5.0',
viewer: { features: {
shared: { viewer: {
toolbarActions: actions 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({ applyConfig({
$id: 'test', $id: 'test',
$name: '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', () => { it('should set the action disabled for context menu actions', (done) => {
applyConfig({ applyConfig(
$id: 'test', {
$name: 'test', $id: 'test',
$version: '1.0.0', $name: 'test',
$license: 'MIT', $version: '1.0.0',
$vendor: 'Good company', $license: 'MIT',
$runtime: '1.5.0', $vendor: 'Good company',
features: { $runtime: '1.5.0',
contextMenu: actions features: {
} contextMenu: actions
}); }
},
true
);
expect(service.getAllowedContextMenuActions()).toEqual(expectedActionsWithoutChildren); service.getAllowedContextMenuActions().subscribe((serviceActions) => {
expect(serviceActions).toEqual(expectedActionsWithoutChildren);
done();
});
}); });
}); });
}); });

View File

@@ -56,6 +56,7 @@ import { RepositoryInfo, NodeEntry } from '@alfresco/js-api';
import { ViewerRules } from '../models/viewer.rules'; import { ViewerRules } from '../models/viewer.rules';
import { SettingsGroupRef } from '../models/types'; import { SettingsGroupRef } from '../models/types';
import { NodePermissionService } from '../services/node-permission.service'; import { NodePermissionService } from '../services/node-permission.service';
import { map } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -63,21 +64,22 @@ import { NodePermissionService } from '../services/node-permission.service';
export class AppExtensionService implements RuleContext { export class AppExtensionService implements RuleContext {
private _references = new BehaviorSubject<ExtensionRef[]>([]); private _references = new BehaviorSubject<ExtensionRef[]>([]);
headerActions: Array<ContentActionRef> = [];
toolbarActions: Array<ContentActionRef> = [];
viewerToolbarActions: Array<ContentActionRef> = [];
sharedLinkViewerToolbarActions: Array<ContentActionRef> = [];
contextMenuActions: Array<ContentActionRef> = [];
openWithActions: Array<ContentActionRef> = [];
createActions: Array<ContentActionRef> = [];
navbar: Array<NavBarGroupRef> = []; navbar: Array<NavBarGroupRef> = [];
sidebarTabs: Array<SidebarTabRef> = []; sidebarTabs: Array<SidebarTabRef> = [];
sidebarActions: Array<ContentActionRef> = [];
contentMetadata: any; contentMetadata: any;
search: any; search: any;
viewerRules: ViewerRules = {}; viewerRules: ViewerRules = {};
settingGroups: Array<SettingsGroupRef> = []; settingGroups: Array<SettingsGroupRef> = [];
private _headerActions = new BehaviorSubject<Array<ContentActionRef>>([]);
private _toolbarActions = new BehaviorSubject<Array<ContentActionRef>>([]);
private _viewerToolbarActions = new BehaviorSubject<Array<ContentActionRef>>([]);
private _sharedLinkViewerToolbarActions = new BehaviorSubject<Array<ContentActionRef>>([]);
private _contextMenuActions = new BehaviorSubject<Array<ContentActionRef>>([]);
private _openWithActions = new BehaviorSubject<Array<ContentActionRef>>([]);
private _createActions = new BehaviorSubject<Array<ContentActionRef>>([]);
private _sidebarActions = new BehaviorSubject<Array<ContentActionRef>>([]);
documentListPresets: { documentListPresets: {
files: Array<DocumentListPresetRef>; files: Array<DocumentListPresetRef>;
libraries: Array<DocumentListPresetRef>; libraries: Array<DocumentListPresetRef>;
@@ -106,6 +108,8 @@ export class AppExtensionService implements RuleContext {
references$: Observable<ExtensionRef[]>; references$: Observable<ExtensionRef[]>;
config: ExtensionConfig;
constructor( constructor(
public auth: AuthenticationService, public auth: AuthenticationService,
protected store: Store<AppStore>, protected store: Store<AppStore>,
@@ -124,12 +128,16 @@ export class AppExtensionService implements RuleContext {
this.navigation = result.navigation; this.navigation = result.navigation;
this.profile = result.profile; this.profile = result.profile;
this.repository = result.repository; this.repository = result.repository;
if (this.config) {
this.setup(this.config);
}
}); });
} }
async load() { async load() {
const config = await this.extensions.load(); this.config = await this.extensions.load();
this.setup(config); this.setup(this.config);
} }
setup(config: ExtensionConfig) { setup(config: ExtensionConfig) {
@@ -140,15 +148,15 @@ export class AppExtensionService implements RuleContext {
this.settingGroups = this.loader.getElements<SettingsGroupRef>(config, 'settings'); this.settingGroups = this.loader.getElements<SettingsGroupRef>(config, 'settings');
this.headerActions = this.loader.getContentActions(config, 'features.header'); this._headerActions.next(this.loader.getContentActions(config, 'features.header'));
this.sidebarActions = this.loader.getContentActions(config, 'features.sidebar.toolbar'); this._sidebarActions.next(this.loader.getContentActions(config, 'features.sidebar.toolbar'));
this.toolbarActions = this.loader.getContentActions(config, 'features.toolbar'); this._toolbarActions.next(this.loader.getContentActions(config, 'features.toolbar'));
this.viewerToolbarActions = this.loader.getContentActions(config, 'features.viewer.toolbarActions'); this._viewerToolbarActions.next(this.loader.getContentActions(config, 'features.viewer.toolbarActions'));
this.sharedLinkViewerToolbarActions = this.loader.getContentActions(config, 'features.viewer.shared.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<ContentActionRef>(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<ContentActionRef>(config, 'features.create');
this.navbar = this.loadNavBar(config); this.navbar = this.loadNavBar(config);
this.sidebarTabs = this.loader.getElements<SidebarTabRef>(config, 'features.sidebar.tabs'); this.sidebarTabs = this.loader.getElements<SidebarTabRef>(config, 'features.sidebar.tabs');
this.contentMetadata = this.loadContentMetadata(config); this.contentMetadata = this.loadContentMetadata(config);
@@ -341,12 +349,16 @@ export class AppExtensionService implements RuleContext {
}; };
} }
getCreateActions(): Array<ContentActionRef> { getCreateActions(): Observable<Array<ContentActionRef>> {
return this.createActions return this._createActions.pipe(
.filter((action) => this.filterVisible(action)) map((createActions) =>
.map((action) => this.copyAction(action)) createActions
.map((action) => this.buildMenu(action)) .filter((action) => this.filterVisible(action))
.map((action) => this.setActionDisabledFromRule(action)); .map((action) => this.copyAction(action))
.map((action) => this.buildMenu(action))
.map((action) => this.setActionDisabledFromRule(action))
)
);
} }
private buildMenu(actionRef: ContentActionRef): ContentActionRef { private buildMenu(actionRef: ContentActionRef): ContentActionRef {
@@ -385,48 +397,58 @@ export class AppExtensionService implements RuleContext {
.reduce(reduceSeparators, []); .reduce(reduceSeparators, []);
} }
getAllowedSidebarActions(): Array<ContentActionRef> { getAllowedSidebarActions(): Observable<Array<ContentActionRef>> {
return this.getAllowedActions(this.sidebarActions); return this._sidebarActions.pipe(map((sidebarActions) => this.getAllowedActions(sidebarActions)));
} }
getAllowedToolbarActions(): Array<ContentActionRef> { getAllowedToolbarActions(): Observable<Array<ContentActionRef>> {
return this.getAllowedActions(this.toolbarActions); return this._toolbarActions.pipe(map((toolbarActions) => this.getAllowedActions(toolbarActions)));
} }
getViewerToolbarActions(): Array<ContentActionRef> { getViewerToolbarActions(): Observable<Array<ContentActionRef>> {
return this.getAllowedActions(this.viewerToolbarActions); return this._viewerToolbarActions.pipe(map((viewerToolbarActions) => this.getAllowedActions(viewerToolbarActions)));
} }
getSharedLinkViewerToolbarActions(): Array<ContentActionRef> { getOpenWithActions(): Observable<Array<ContentActionRef>> {
return this.getAllowedActions(this.sharedLinkViewerToolbarActions); return this._openWithActions.pipe(map((openWithActions) => this.getAllowedActions(openWithActions)));
} }
getHeaderActions(): Array<ContentActionRef> { getSharedLinkViewerToolbarActions(): Observable<Array<ContentActionRef>> {
return this.headerActions return this._sharedLinkViewerToolbarActions.pipe(
.filter((action) => this.filterVisible(action)) map((sharedLinkViewerToolbarActions) => (!this.selection.isEmpty ? this.getAllowedActions(sharedLinkViewerToolbarActions) : []))
.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(): Array<ContentActionRef> { getHeaderActions(): Observable<Array<ContentActionRef>> {
return this.getAllowedActions(this.contextMenuActions); 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<Array<ContentActionRef>> {
return this._contextMenuActions.pipe(map((contextMenuActions) => (!this.selection.isEmpty ? this.getAllowedActions(contextMenuActions) : [])));
} }
getSettingsGroups(): Array<SettingsGroupRef> { getSettingsGroups(): Array<SettingsGroupRef> {

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
import { ContextMenuComponent } from './context-menu.component'; import { ContextMenuComponent } from './context-menu.component';
import { ContextMenuModule } from './context-menu.module'; import { ContextMenuModule } from './context-menu.module';
@@ -75,7 +75,7 @@ describe('ContextMenuComponent', () => {
contextMenuOverlayRef = TestBed.inject(ContextMenuOverlayRef); contextMenuOverlayRef = TestBed.inject(ContextMenuOverlayRef);
extensionsService = TestBed.inject(AppExtensionService); extensionsService = TestBed.inject(AppExtensionService);
spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue([contextItem]); spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of([contextItem]));
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -85,15 +85,16 @@ describe('ContextMenuComponent', () => {
expect(contextMenuOverlayRef.close).toHaveBeenCalled(); expect(contextMenuOverlayRef.close).toHaveBeenCalled();
}); });
it('should render defined context menu actions items', fakeAsync(() => { it('should render defined context menu actions items', async () => {
component.ngAfterViewInit(); component.ngAfterViewInit();
tick(500); fixture.detectChanges();
await fixture.whenStable();
const contextMenuElements = document.body.querySelector('.aca-context-menu').querySelectorAll('button'); const contextMenuElements = document.body.querySelector('.aca-context-menu').querySelectorAll('button');
expect(contextMenuElements.length).toBe(1); expect(contextMenuElements.length).toBe(1);
expect(contextMenuElements[0].querySelector('span').innerText).toBe(contextItem.title); 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', () => {
spyOn(extensionsService, 'runActionById'); spyOn(extensionsService, 'runActionById');

View File

@@ -25,8 +25,6 @@
import { Component, ViewEncapsulation, OnInit, OnDestroy, HostListener, ViewChild, AfterViewInit, Inject } from '@angular/core'; import { Component, ViewEncapsulation, OnInit, OnDestroy, HostListener, ViewChild, AfterViewInit, Inject } from '@angular/core';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { AppStore, getAppSelection } from '@alfresco/aca-shared/store';
import { Store } from '@ngrx/store';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { ContentActionRef } from '@alfresco/adf-extensions'; import { ContentActionRef } from '@alfresco/adf-extensions';
@@ -62,7 +60,6 @@ export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit {
constructor( constructor(
private contextMenuOverlayRef: ContextMenuOverlayRef, private contextMenuOverlayRef: ContextMenuOverlayRef,
private extensions: AppExtensionService, private extensions: AppExtensionService,
private store: Store<AppStore>,
@Inject(CONTEXT_MENU_DIRECTION) public direction: Direction @Inject(CONTEXT_MENU_DIRECTION) public direction: Direction
) {} ) {}
@@ -82,14 +79,10 @@ export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit {
} }
ngOnInit() { ngOnInit() {
this.store this.extensions
.select(getAppSelection) .getAllowedContextMenuActions()
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe((selection) => { .subscribe((actions) => (this.actions = actions));
if (selection.count) {
this.actions = this.extensions.getAllowedContextMenuActions();
}
});
} }
ngAfterViewInit() { ngAfterViewInit() {

View File

@@ -34,6 +34,7 @@ import { By } from '@angular/platform-browser';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { of } from 'rxjs';
describe('CreateMenuComponent', () => { describe('CreateMenuComponent', () => {
let fixture: ComponentFixture<CreateMenuComponent>; let fixture: ComponentFixture<CreateMenuComponent>;
@@ -47,13 +48,15 @@ describe('CreateMenuComponent', () => {
extensionService = TestBed.inject(AppExtensionService); extensionService = TestBed.inject(AppExtensionService);
getCreateActionsSpy = spyOn(extensionService, 'getCreateActions'); getCreateActionsSpy = spyOn(extensionService, 'getCreateActions');
getCreateActionsSpy.and.returnValue([ getCreateActionsSpy.and.returnValue(
{ of([
id: 'action1', {
type: ContentActionType.button, id: 'action1',
title: 'action one' type: ContentActionType.button,
} title: 'action one'
]); }
])
);
fixture = TestBed.createComponent(CreateMenuComponent); fixture = TestBed.createComponent(CreateMenuComponent);
}); });
@@ -102,20 +105,22 @@ describe('CreateMenuComponent', () => {
}); });
it('should render sub-menus', async () => { it('should render sub-menus', async () => {
getCreateActionsSpy.and.returnValue([ getCreateActionsSpy.and.returnValue(
{ of([
id: 'level1', {
type: ContentActionType.menu, id: 'level1',
title: 'level one', type: ContentActionType.menu,
children: [ title: 'level one',
{ children: [
id: 'level2', {
type: ContentActionType.button, id: 'level2',
title: 'level two' type: ContentActionType.button,
} title: 'level two'
] }
} ]
]); }
])
);
await clickMenu(); await clickMenu();

View File

@@ -25,8 +25,6 @@
import { Component, Input, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'; import { Component, Input, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { ContentActionRef } from '@alfresco/adf-extensions'; 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 { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { AppExtensionService } from '@alfresco/aca-shared'; import { AppExtensionService } from '@alfresco/aca-shared';
@@ -48,14 +46,14 @@ export class CreateMenuComponent implements OnInit, OnDestroy {
@Input() @Input()
expanded: boolean; expanded: boolean;
constructor(private store: Store<AppStore>, private extensions: AppExtensionService) {} constructor(private extensions: AppExtensionService) {}
ngOnInit() { ngOnInit() {
this.store this.extensions
.select(getRuleContext) .getCreateActions()
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe(() => { .subscribe((createActions) => {
this.createActions = this.extensions.getCreateActions(); this.createActions = createActions;
}); });
} }

View File

@@ -26,11 +26,17 @@
import { AppHeaderComponent } from './header.component'; import { AppHeaderComponent } from './header.component';
import { AppState } from '@alfresco/aca-shared/store'; import { AppState } from '@alfresco/aca-shared/store';
import { of } from 'rxjs'; 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 { 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', () => { describe('AppHeaderComponent', () => {
let component: AppHeaderComponent; let component: AppHeaderComponent;
let fixture: ComponentFixture<AppHeaderComponent>;
const actions = [ const actions = [
{ id: 'action-1', type: 'button' }, { id: 'action-1', type: 'button' },
@@ -38,11 +44,12 @@ describe('AppHeaderComponent', () => {
] as Array<ContentActionRef>; ] as Array<ContentActionRef>;
const store = { const store = {
select: jasmine.createSpy('select') select: jasmine.createSpy('select'),
dispatch: () => {}
} as any; } as any;
const appExtensionService = { const appExtensionService = {
getHeaderActions: () => actions getHeaderActions: () => of(actions)
} as any; } as any;
const app = { const app = {
@@ -52,11 +59,27 @@ describe('AppHeaderComponent', () => {
} as AppState; } as AppState;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, CoreModule.forChild(), AppSearchInputModule],
declarations: [AppHeaderComponent],
providers: [
{
provide: AppExtensionService,
useValue: appExtensionService
},
{
provide: Store,
useValue: store
}
]
});
store.select.and.callFake((memoizeFn) => { store.select.and.callFake((memoizeFn) => {
return of(memoizeFn({ app })); 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(() => { it('should set header color, name and logo', fakeAsync(() => {
@@ -65,8 +88,9 @@ describe('AppHeaderComponent', () => {
component.headerColor$.subscribe((val) => expect(val).toBe(app.headerColor)); component.headerColor$.subscribe((val) => expect(val).toBe(app.headerColor));
})); }));
it('should get header actions', () => { it('should get header actions', fakeAsync(() => {
component.ngOnInit(); component.ngOnInit();
tick();
expect(component.actions).toEqual(actions); expect(component.actions).toEqual(actions);
}); }));
}); });

View File

@@ -23,12 +23,13 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
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 { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { ContentActionRef } from '@alfresco/adf-extensions'; import { ContentActionRef } from '@alfresco/adf-extensions';
import { AppStore, getHeaderColor, getAppName, getLogoPath, getHeaderImagePath } from '@alfresco/aca-shared/store'; import { AppStore, getHeaderColor, getAppName, getLogoPath, getHeaderImagePath } from '@alfresco/aca-shared/store';
import { AppExtensionService } from '@alfresco/aca-shared'; import { AppExtensionService } from '@alfresco/aca-shared';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
@@ -37,7 +38,8 @@ import { AppExtensionService } from '@alfresco/aca-shared';
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'app-header' } host: { class: 'app-header' }
}) })
export class AppHeaderComponent implements OnInit { export class AppHeaderComponent implements OnInit, OnDestroy {
private onDestroy$: Subject<boolean> = new Subject<boolean>();
@Output() @Output()
toggleClicked = new EventEmitter(); toggleClicked = new EventEmitter();
@@ -60,7 +62,17 @@ export class AppHeaderComponent implements OnInit {
} }
ngOnInit() { 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) { trackByActionId(_: number, action: ContentActionRef) {

View File

@@ -87,11 +87,23 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges {
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe((selection) => { .subscribe((selection) => {
this.selection = 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.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 this.store
.select(getCurrentFolder) .select(getCurrentFolder)
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))

View File

@@ -153,7 +153,12 @@ export class PreviewComponent extends PageComponent implements OnInit, OnDestroy
.subscribe(() => {}) .subscribe(() => {})
]); ]);
this.openWith = this.extensions.openWithActions; this.extensions
.getOpenWithActions()
.pipe(takeUntil(this.onDestroy$))
.subscribe((actions) => {
this.openWith = actions;
});
} }
ngOnDestroy() { ngOnDestroy() {

View File

@@ -64,6 +64,12 @@ describe('SharedLinkViewComponent', () => {
fixture = TestBed.createComponent(SharedLinkViewComponent); fixture = TestBed.createComponent(SharedLinkViewComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
appExtensionService = TestBed.inject(AppExtensionService); appExtensionService = TestBed.inject(AppExtensionService);
appExtensionService.selection = {
isEmpty: true,
count: 0,
libraries: null,
nodes: null
};
spyGetSharedLink = spyOn(component['sharedLinksApi'], 'getSharedLink'); spyGetSharedLink = spyOn(component['sharedLinksApi'], 'getSharedLink');
@@ -93,8 +99,6 @@ describe('SharedLinkViewComponent', () => {
})); }));
it('should not update actions reference if selection is empty', fakeAsync(() => { 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' } })); spyGetSharedLink.and.returnValue(Promise.resolve({ entry: { id: 'shared-id' } }));
fixture.detectChanges(); fixture.detectChanges();
@@ -104,8 +108,13 @@ describe('SharedLinkViewComponent', () => {
})); }));
it('should update actions reference if selection is not empty', fakeAsync(() => { it('should update actions reference if selection is not empty', fakeAsync(() => {
spyOn(storeMock, 'select').and.returnValue(of({ isEmpty: false })); appExtensionService.selection = {
spyOn(appExtensionService, 'getSharedLinkViewerToolbarActions'); isEmpty: false,
count: 1,
libraries: null,
nodes: null
};
spyOn(appExtensionService, 'getSharedLinkViewerToolbarActions').and.callThrough();
spyGetSharedLink.and.returnValue(Promise.resolve({ entry: { id: 'shared-id' } })); spyGetSharedLink.and.returnValue(Promise.resolve({ entry: { id: 'shared-id' } }));
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -23,15 +23,15 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { AppStore, SetSelectedNodesAction, getAppSelection } from '@alfresco/aca-shared/store'; import { AppStore, SetSelectedNodesAction } from '@alfresco/aca-shared/store';
import { AlfrescoApiService } from '@alfresco/adf-core'; import { AlfrescoApiService } from '@alfresco/adf-core';
import { ContentActionRef } from '@alfresco/adf-extensions'; import { ContentActionRef } from '@alfresco/adf-extensions';
import { SharedLinkEntry, SharedlinksApi } from '@alfresco/js-api'; 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 { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { forkJoin, from, of } from 'rxjs'; import { forkJoin, from, of, Subject } from 'rxjs';
import { catchError, mergeMap } from 'rxjs/operators'; import { catchError, mergeMap, takeUntil } from 'rxjs/operators';
import { AppExtensionService } from '@alfresco/aca-shared'; import { AppExtensionService } from '@alfresco/aca-shared';
@Component({ @Component({
@@ -41,7 +41,8 @@ import { AppExtensionService } from '@alfresco/aca-shared';
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'app-shared-link-view' } host: { class: 'app-shared-link-view' }
}) })
export class SharedLinkViewComponent implements OnInit { export class SharedLinkViewComponent implements OnInit, OnDestroy {
private onDestroy$: Subject<boolean> = new Subject<boolean>();
private sharedLinksApi: SharedlinksApi; private sharedLinksApi: SharedlinksApi;
sharedLinkId: string = null; sharedLinkId: string = null;
viewerToolbarActions: Array<ContentActionRef> = []; viewerToolbarActions: Array<ContentActionRef> = [];
@@ -69,9 +70,17 @@ export class SharedLinkViewComponent implements OnInit {
this.sharedLinkId = sharedId; this.sharedLinkId = sharedId;
}); });
this.store.select(getAppSelection).subscribe((selection) => { this.extensions
if (!selection.isEmpty) this.viewerToolbarActions = this.extensions.getSharedLinkViewerToolbarActions(); .getSharedLinkViewerToolbarActions()
}); .pipe(takeUntil(this.onDestroy$))
.subscribe((actions) => {
this.viewerToolbarActions = actions;
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
} }
trackByActionId(_: number, action: ContentActionRef) { trackByActionId(_: number, action: ContentActionRef) {

View File

@@ -27,7 +27,7 @@ import { AppExtensionService, AppHookService, ContentApiService } from '@alfresc
import { import {
AppStore, AppStore,
ClosePreviewAction, ClosePreviewAction,
getRuleContext, getAppSelection,
isInfoDrawerOpened, isInfoDrawerOpened,
RefreshPreviewAction, RefreshPreviewAction,
ReloadDocumentListAction, ReloadDocumentListAction,
@@ -131,18 +131,24 @@ export class AppViewerComponent implements OnInit, OnDestroy {
}); });
this.store this.store
.select(getRuleContext) .select(getAppSelection)
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe((ruleContext) => { .subscribe((selection) => {
this.selection = ruleContext.selection; this.selection = selection;
});
if (this.toolbarActions.length === 0) { this.extensions
this.toolbarActions = this.extensions.getViewerToolbarActions(); .getViewerToolbarActions()
} .pipe(takeUntil(this.onDestroy$))
.subscribe((actions) => {
this.toolbarActions = actions;
});
if (this.openWith.length === 0) { this.extensions
this.openWith = this.extensions.openWithActions; .getOpenWithActions()
} .pipe(takeUntil(this.onDestroy$))
.subscribe((actions) => {
this.openWith = actions;
}); });
this.route.params.subscribe((params) => { this.route.params.subscribe((params) => {