[ADF-2503] conditional visibility for content actions (#3325)

* conditional visibility for content actions

* fix typo

* workaround for "target: all"
This commit is contained in:
Denys Vuika 2018-05-15 16:53:52 +01:00 committed by Eugenio Romano
parent 7154eb1e84
commit d67f160fdc
8 changed files with 216 additions and 13 deletions

View File

@ -273,6 +273,25 @@
</data-columns> </data-columns>
<content-actions> <content-actions>
<!-- Conditional actions demo -->
<content-action
icon="get_app"
title="Download this file now!"
handler="download"
[visible]="canDownloadNode">
</content-action>
<content-action
icon="get_app"
title="Never see this action again"
handler="download"
[visible]="false">
</content-action>
<content-action
icon="get_app"
title="This can be toggled"
handler="download"
[visible]="showCustomDownloadAction">
</content-action>
<!-- common actions --> <!-- common actions -->
<content-action <content-action
icon="get_app" icon="get_app"
@ -417,6 +436,12 @@
</mat-slide-toggle> </mat-slide-toggle>
</section> </section>
<section>
<mat-slide-toggle color="primary" [(ngModel)]="showCustomDownloadAction">
Toggle custom download action
</mat-slide-toggle>
</section>
<section> <section>
<mat-slide-toggle [color]="'primary'" [(ngModel)]="multiselect">{{'DOCUMENT_LIST.MULTISELECT_CHECKBOXES' | <mat-slide-toggle [color]="'primary'" [(ngModel)]="multiselect">{{'DOCUMENT_LIST.MULTISELECT_CHECKBOXES' |
translate}} translate}}

View File

@ -152,6 +152,8 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild(InfinitePaginationComponent) @ViewChild(InfinitePaginationComponent)
infinitePaginationComponent: InfinitePaginationComponent; infinitePaginationComponent: InfinitePaginationComponent;
@Input()
showCustomDownloadAction = false;
permissionsStyle: PermissionStyleModel[] = []; permissionsStyle: PermissionStyleModel[] = [];
infiniteScrolling: boolean; infiniteScrolling: boolean;
@ -491,4 +493,11 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
this.infinitePaginationComponent.reset(); this.infinitePaginationComponent.reset();
this.reloadForInfiniteScrolling(); this.reloadForInfiniteScrolling();
} }
canDownloadNode = (node: MinimalNodeEntity): boolean => {
if (node && node.entry && node.entry.name === 'For Sale.docx') {
return true;
}
return false;
}
} }

View File

@ -90,8 +90,9 @@ export class MyView {
| handler | `string` | | System actions. Can be "delete", "download", "copy" or "move". | | handler | `string` | | System actions. Can be "delete", "download", "copy" or "move". |
| icon | `string` | | The name of the icon to display next to the menu command (can be left blank). | | icon | `string` | | The name of the icon to display next to the menu command (can be left blank). |
| permission | `string` | | The permission type. | | permission | `string` | | The permission type. |
| target | `string` | ContentActionTarget.All | Type of item that the action appies to. Can be "document" or "folder" | | target | `string` | ContentActionTarget.All | Type of item that the action applies to. Can be "document" or "folder" |
| title | `string` | "Action" | The title of the action as shown in the menu. | | title | `string` | "Action" | The title of the action as shown in the menu. |
| visible | `boolean` or `Function` | Visibility state (see examples further in the document) |
### Events ### Events
@ -319,6 +320,84 @@ allow the item being copied/moved to be the destination if it is itself a folder
</adf-document-list> </adf-document-list>
``` ```
### Conditional visibility
The `<content-action>` component allows you to control visibility with the help of the `visible` property and supports three major scenarios:
* direct value of `boolean` type
* binding to a property of the `boolean` type
* binding to a property of the `Function` type that evaluates condition and returns `boolean` value
#### Using direct value of boolean type
```html
<content-action
icon="get_app"
title="Never see this action again"
handler="download"
[visible]="false">
</content-action>
```
#### Using a property of the boolean type
```html
<content-action
icon="get_app"
title="This can be toggled"
handler="download"
[visible]="showCustomDownloadAction">
</content-action>
```
The markup above relies on the `showCustomDownloadAction` property declared at your component class level:
```ts
export class MyComponent {
@Input()
showCustomDownloadAction = true;
}
```
#### Using a property of the Function type
```html
<content-action
icon="get_app"
title="Download this file now!"
handler="download"
[visible]="canDownloadNode">
</content-action>
```
The code above relies on the `canDownloadNode` property of a `Function` type declared at your component class level:
```ts
export class MyComponent {
canDownloadNode = (node: MinimalNodeEntity): boolean => {
if (node && node.entry && node.entry.name === 'For Sale.docx') {
return true;
}
return false;
}
}
```
Code above checks the node name, and evaluates to `true` only if corresponding node is called "For Sale.docx".
Please note that if you want to preserve `this` context within the evaluator function,
its property should be declared as a lambda one:
```ts
functionName = (parameters): boolean => {
// implementation
return true;
}
```
### Customizing built-in actions ### Customizing built-in actions
The built-in actions are defined in the [Document Actions service](document-actions.service.md) and The built-in actions are defined in the [Document Actions service](document-actions.service.md) and

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
import { EventEmitter } from '@angular/core'; import { EventEmitter } from '@angular/core';
import { async, TestBed } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { ContentService, setupTestBed } from '@alfresco/adf-core'; import { ContentService, setupTestBed } from '@alfresco/adf-core';
@ -81,6 +81,24 @@ describe('ContentAction', () => {
expect(model.icon).toBe(action.icon); expect(model.icon).toBe(action.icon);
}); });
it('should update visibility binding', () => {
let action = new ContentActionComponent(actionList, null, null);
action.target = 'document';
action.title = '<title>';
action.icon = '<icon>';
action.visible = true;
action.ngOnInit();
expect(action.documentActionModel.visible).toBeTruthy();
action.visible = false;
action.ngOnChanges({
'visible': new SimpleChange(true, false, false)
});
expect(action.documentActionModel.visible).toBeFalsy();
});
it('should get action handler from document actions service', () => { it('should get action handler from document actions service', () => {
let handler = function () { let handler = function () {

View File

@ -17,7 +17,7 @@
/* tslint:disable:component-selector */ /* tslint:disable:component-selector */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, OnChanges, SimpleChanges } from '@angular/core';
import { ContentActionHandler } from '../../models/content-action.model'; import { ContentActionHandler } from '../../models/content-action.model';
import { DocumentActionsService } from '../../services/document-actions.service'; import { DocumentActionsService } from '../../services/document-actions.service';
@ -33,7 +33,7 @@ import { ContentActionListComponent } from './content-action-list.component';
FolderActionsService FolderActionsService
] ]
}) })
export class ContentActionComponent implements OnInit { export class ContentActionComponent implements OnInit, OnChanges {
/** The title of the action as shown in the menu. */ /** The title of the action as shown in the menu. */
@Input() @Input()
@ -43,6 +43,9 @@ export class ContentActionComponent implements OnInit {
@Input() @Input()
icon: string; icon: string;
@Input()
visible: boolean | Function = true;
/** System actions. Can be "delete", "download", "copy" or "move". */ /** System actions. Can be "delete", "download", "copy" or "move". */
@Input() @Input()
handler: string; handler: string;
@ -83,6 +86,9 @@ export class ContentActionComponent implements OnInit {
@Output() @Output()
success = new EventEmitter(); success = new EventEmitter();
documentActionModel: ContentActionModel;
folderActionModel: ContentActionModel;
constructor( constructor(
private list: ContentActionListComponent, private list: ContentActionListComponent,
private documentActions: DocumentActionsService, private documentActions: DocumentActionsService,
@ -91,10 +97,21 @@ export class ContentActionComponent implements OnInit {
ngOnInit() { ngOnInit() {
if (this.target === ContentActionTarget.All) { if (this.target === ContentActionTarget.All) {
this.generateAction(ContentActionTarget.Folder); this.folderActionModel = this.generateAction(ContentActionTarget.Folder);
this.generateAction(ContentActionTarget.Document); this.documentActionModel = this.generateAction(ContentActionTarget.Document);
} else { } else {
this.generateAction(this.target); this.documentActionModel = this.generateAction(this.target);
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.visible && !changes.visible.firstChange) {
if (this.documentActionModel) {
this.documentActionModel.visible = changes.visible.currentValue;
}
if (this.folderActionModel) {
this.folderActionModel.visible = changes.visible.currentValue;
}
} }
} }
@ -105,14 +122,15 @@ export class ContentActionComponent implements OnInit {
return false; return false;
} }
private generateAction(target: string) { private generateAction(target: string): ContentActionModel {
let model = new ContentActionModel({ const model = new ContentActionModel({
title: this.title, title: this.title,
icon: this.icon, icon: this.icon,
permission: this.permission, permission: this.permission,
disableWithNoPermission: this.disableWithNoPermission, disableWithNoPermission: this.disableWithNoPermission,
target: target, target: target,
disabled: this.disabled disabled: this.disabled,
visible: this.visible
}); });
if (this.handler) { if (this.handler) {
model.handler = this.getSystemHandler(target, this.handler); model.handler = this.getSystemHandler(target, this.handler);
@ -125,6 +143,7 @@ export class ContentActionComponent implements OnInit {
} }
this.register(model); this.register(model);
return model;
} }
getSystemHandler(target: string, name: string): ContentActionHandler { getSystemHandler(target: string, name: string): ContentActionHandler {

View File

@ -312,6 +312,48 @@ describe('DocumentList', () => {
}); });
it('should not display hidden content actions', () => {
documentList.actions = [
new ContentActionModel({
target: 'document',
title: 'Action1',
visible: false
}),
new ContentActionModel({
target: 'document',
title: 'Action2',
visible: true
})
];
const nodeFile = { entry: { isFile: true, name: 'xyz' } };
const actions = documentList.getNodeActions(nodeFile);
expect(actions.length).toBe(1);
expect(actions[0].title).toBe('Action2');
});
it('should evaluate conditional visibility for content actions', () => {
documentList.actions = [
new ContentActionModel({
target: 'document',
title: 'Action1',
visible: (): boolean => true
}),
new ContentActionModel({
target: 'document',
title: 'Action2',
visible: (): boolean => false
})
];
const nodeFile = { entry: { isFile: true, name: 'xyz' } };
const actions = documentList.getNodeActions(nodeFile);
expect(actions.length).toBe(1);
expect(actions[0].title).toBe('Action1');
});
it('should not disable the action if there is copy permission', () => { it('should not disable the action if there is copy permission', () => {
let documentMenu = new ContentActionModel({ let documentMenu = new ContentActionModel({
disableWithNoPermission: true, disableWithNoPermission: true,

View File

@ -434,9 +434,15 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
} }
if (target) { if (target) {
let actionsByTarget = this.actions.filter(entry => { let actionsByTarget = this.actions
return entry.target.toLowerCase() === target; .filter(entry => {
}).map(action => new ContentActionModel(action)); const isVisible = (typeof entry.visible === 'function')
? entry.visible(node)
: entry.visible;
return isVisible && entry.target.toLowerCase() === target;
})
.map(action => new ContentActionModel(action));
actionsByTarget.forEach((action) => { actionsByTarget.forEach((action) => {
this.disableActionsWithNoPermissions(node, action); this.disableActionsWithNoPermissions(node, action);

View File

@ -24,6 +24,7 @@ export class ContentActionModel {
permission: string; permission: string;
disableWithNoPermission: boolean = false; disableWithNoPermission: boolean = false;
disabled: boolean = false; disabled: boolean = false;
visible: boolean | Function = true;
constructor(obj?: any) { constructor(obj?: any) {
if (obj) { if (obj) {
@ -35,6 +36,10 @@ export class ContentActionModel {
this.permission = obj.permission; this.permission = obj.permission;
this.disableWithNoPermission = obj.disableWithNoPermission; this.disableWithNoPermission = obj.disableWithNoPermission;
this.disabled = obj.disabled; this.disabled = obj.disabled;
if (obj.hasOwnProperty('visible')) {
this.visible = obj.visible;
}
} }
} }
} }