diff --git a/demo-shell/resources/i18n/en.json b/demo-shell/resources/i18n/en.json index 9c2ba8c9ce..3b33c436f0 100644 --- a/demo-shell/resources/i18n/en.json +++ b/demo-shell/resources/i18n/en.json @@ -270,14 +270,16 @@ }, "TOOLTIP_MESSAGE": { "START_INPUT": "Starting page" - } + }, + "TASK_CONTEXT_MENU": "Task List Context Menu" }, "PROCESS_LIST_DEMO": { "ERROR_MESSAGE": { "APP_ID_REQUIRED_ERROR": "Insert App ID", "APP_ID_TYPE_ERROR": "App ID must be a number", "NUMBER_GREATER_THAN": "Value must be greater than or equal to {{ value }}" - } + }, + "PROCESS_CONTEXT_MENU": "Process List Context Menu" }, "GROUP-TITLE1-TRANSLATION-KEY": "CUSTOM TITLE TRANSLATION ONE", "GROUP-TITLE2-TRANSLATION-KEY": "CUSTOM TITLE TRANSLATION TWO", diff --git a/demo-shell/src/app/components/process-service/process-service.component.html b/demo-shell/src/app/components/process-service/process-service.component.html index 680eac1af1..4b6263aa97 100644 --- a/demo-shell/src/app/components/process-service/process-service.component.html +++ b/demo-shell/src/app/components/process-service/process-service.component.html @@ -47,6 +47,8 @@ [state]="taskFilter?.filter?.state" [sort]="taskFilter?.filter?.sort" [landingTaskId]="currentTaskId" + [showContextMenu]="taskContextMenu" + (showRowContextMenu)="onShowTaskRowContextMenu($event)" (rowClick)="onTaskRowClick($event)" (success)="onSuccessTaskList()" (row-click)="onRowClick($event)" @@ -161,6 +163,8 @@ [page]="processPage" [size]="paginationPageSize" [sort]="processFilter?.filter?.sort" + [showContextMenu]="processContextMenu" + (showRowContextMenu)="onShowProcessRowContextMenu($event)" (rowClick)="onProcessRowClick($event)" (row-dblclick)="onProcessRowDblClick($event)" [multiselect]="multiSelectProcess" @@ -280,6 +284,12 @@ Multiselect Task List +
+ Show Task list Context menu +
+
+ Show Process list Context menu +

multiple diff --git a/demo-shell/src/app/components/process-service/process-service.component.ts b/demo-shell/src/app/components/process-service/process-service.component.ts index b80519620a..ebc8959b0e 100644 --- a/demo-shell/src/app/components/process-service/process-service.component.ts +++ b/demo-shell/src/app/components/process-service/process-service.component.ts @@ -36,7 +36,7 @@ import { import { FORM_FIELD_VALIDATORS, FormRenderingService, FormService, DynamicTableRow, ValidateDynamicTableRowEvent, AppConfigService, PaginationComponent, UserPreferenceValues, - AlfrescoApiService, UserPreferencesService, LogService + AlfrescoApiService, UserPreferencesService, LogService, DataCellEvent } from '@alfresco/adf-core'; import { AnalyticsReportListComponent } from '@alfresco/adf-insights'; @@ -118,6 +118,8 @@ export class ProcessServiceComponent implements AfterViewInit, OnDestroy, OnInit multiSelectTask = false; multiSelectProcess = false; selectionMode = 'single'; + taskContextMenu = false; + processContextMenu = false; private tabs = { tasks: 0, processes: 1, reports: 2 }; @@ -526,4 +528,33 @@ export class ProcessServiceComponent implements AfterViewInit, OnDestroy, OnInit this.currentTaskId = null; } + onShowTaskRowContextMenu(event: DataCellEvent) { + event.value.actions = [ + { + data: event.value.row['obj'], + model: { + key: 'taskDetails', + icon: 'open', + title: 'TASK_LIST_DEMO.TASK_CONTEXT_MENU', + visible: true + }, + subject: new Subject() + } + ]; + } + + onShowProcessRowContextMenu(event: DataCellEvent) { + event.value.actions = [ + { + data: event.value.row['obj'], + model: { + key: 'processDetails', + icon: 'open', + title: 'PROCESS_LIST_DEMO.PROCESS_CONTEXT_MENU', + visible: true + }, + subject: new Subject() + } + ]; + } } diff --git a/docs/docassets/images/process-instance-list-context-menu.png b/docs/docassets/images/process-instance-list-context-menu.png new file mode 100644 index 0000000000..f64a927c5d Binary files /dev/null and b/docs/docassets/images/process-instance-list-context-menu.png differ diff --git a/docs/docassets/images/task-list-context-menu.png b/docs/docassets/images/task-list-context-menu.png new file mode 100644 index 0000000000..7a4990aee4 Binary files /dev/null and b/docs/docassets/images/task-list-context-menu.png differ diff --git a/docs/process-services/components/process-list.component.md b/docs/process-services/components/process-list.component.md index d53e36abcb..6fe5700df9 100644 --- a/docs/process-services/components/process-list.component.md +++ b/docs/process-services/components/process-list.component.md @@ -64,6 +64,7 @@ when the process list is empty: | size | `number` | | The number of processes to fetch in each page. | | sort | `string` | | Defines the sort ordering of the list. Possible values are `created-desc`, `created-asc`, `ended-desc`, `ended-asc`. | | state | `string` | | Defines the state of the processes. Possible values are `running`, `completed` and `all` | +| showContextMenu | `boolean` | false | Toggles custom context menu for the component. | ### Events @@ -72,6 +73,7 @@ when the process list is empty: | error | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when an error occurs while loading the list of process instances from the server. | | rowClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when a row in the process list is clicked. | | success | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`ProcessListModel`](../../../lib/process-services/src/lib/process-list/models/process-list.model.ts)`>` | Emitted when the list of process instances has been loaded successfully from the server. | +| showRowContextMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/datatable/components/datatable/data-cell.event.ts)`>` | Emitted before the context menu is displayed for a row. | ## Details @@ -186,6 +188,48 @@ The Process Instance List also supports pagination: ``` +#### showRowContextMenu event + +Emitted before the context menu is displayed for a row. + +Note that the ProcessInstanceListComponent itself does not populate the context menu with items. You can provide all necessary content via the handler. + +```html + + +``` + +Event properties: + +```ts +value: { + row: DataRow, + col: DataColumn, + actions: [] +} +``` + +Handler example: + +```ts +onShowRowContextMenu(event: DataCellEvent) { + event.value.actions = [ + { title: 'Process List Context Menu' }, + { ... } + ] +} +``` +![](../../docassets/images/process-instance-list-context-menu.png) + +This event is cancellable. You can use `event.preventDefault()` to prevent the default behavior. + +The ProcessInstanceList will automatically render the supplied menu items. + +See the [ContextMenu](https://www.npmjs.com/package/ng2-alfresco-core) +documentation for more details on the format and behavior of context actions. + ## See also - [Data column component](../../core/components/data-column.component.md) diff --git a/docs/process-services/components/task-list.component.md b/docs/process-services/components/task-list.component.md index adaa840e61..15ec26bf81 100644 --- a/docs/process-services/components/task-list.component.md +++ b/docs/process-services/components/task-list.component.md @@ -73,6 +73,7 @@ when the task list is empty: | start | `number` | | Starting point of the list within the full set of tasks. | | state | `string` | | Current state of the process. Possible values are: `completed`, `active`. | | taskId | `string` | | The id of a task | +| showContextMenu | `boolean` | false | Toggles custom context menu for the component. | ### Events @@ -82,6 +83,7 @@ when the task list is empty: | rowClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when a task in the list is clicked | | rowsSelected | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when rows are selected/unselected | | success | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the task list is loaded | +| showRowContextMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/datatable/components/datatable/data-cell.event.ts)`>` | Emitted before the context menu is displayed for a row. | ## Details @@ -228,6 +230,50 @@ typical tasklist: You can customize the styling of a column and also add features like tooltips and automatic translation of column titles. See the [Data Column component](../../core/components/data-column.component.md) page for more information about these features. +#### showRowContextMenu event + +Emitted before the context menu is displayed for a row. + +Note that the TaskListComponent itself does not populate the context menu with items. +You can provide all necessary content via the handler. + +```html + + +``` + +Event properties: + +```ts +value: { + row: DataRow, + col: DataColumn, + actions: [] +} +``` + +Handler example: + +```ts +onShowRowContextMenu(event: DataCellEvent) { + event.value.actions = [ + { title: 'Task List Context Menu' }, + { ... } + ] +} +``` + +![](../../docassets/images/task-list-context-menu.png) + +This event is cancellable. You can use `event.preventDefault()` to prevent the default behavior. + +The TaskListComponent will automatically render the supplied menu items. + +See the [ContextMenu](https://www.npmjs.com/package/ng2-alfresco-core) +documentation for more details on the format and behavior of context actions. + ## See also - [Data column component](../../core/components/data-column.component.md) diff --git a/lib/process-services/src/lib/process-list/components/process-list.component.html b/lib/process-services/src/lib/process-list/components/process-list.component.html index e67ec4916a..4469bec092 100644 --- a/lib/process-services/src/lib/process-list/components/process-list.component.html +++ b/lib/process-services/src/lib/process-list/components/process-list.component.html @@ -7,6 +7,8 @@ [selectionMode]="selectionMode" [multiselect]="multiselect" [resolverFn]="resolverFn" + [contextMenu]="showContextMenu" + (showRowContextMenu)="onShowRowContextMenu($event)" (rowClick)="onRowClick($event)" (row-keyup)="onRowKeyUp($event)"> diff --git a/lib/process-services/src/lib/process-list/components/process-list.component.spec.ts b/lib/process-services/src/lib/process-list/components/process-list.component.spec.ts index 16bac42410..f9ce269517 100644 --- a/lib/process-services/src/lib/process-list/components/process-list.component.spec.ts +++ b/lib/process-services/src/lib/process-list/components/process-list.component.spec.ts @@ -15,14 +15,14 @@ * limitations under the License. */ -import { Component, SimpleChange, ViewChild } from '@angular/core'; +import { Component, SimpleChange, ViewChild, OnInit, Output, EventEmitter } from '@angular/core'; import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; +import { of, throwError, Subject } from 'rxjs'; import { By } from '@angular/platform-browser'; import { ProcessInstanceListComponent } from './process-list.component'; import { AppConfigService, setupTestBed, CoreModule, DataTableModule, DataRow, DataColumn, - DataRowEvent, ObjectDataRow, ObjectDataTableAdapter + DataRowEvent, ObjectDataRow, ObjectDataTableAdapter, DataCellEvent } from '@alfresco/adf-core'; import { fakeProcessInstance, fakeProcessInstancesWithNoName, fakeProcessInstancesEmpty, fakeProcessCustomSchema } from '../../mock'; import { ProcessService } from '../services/process.service'; @@ -527,3 +527,115 @@ describe('Process List: Custom EmptyTemplateComponent', () => { }); }); }); + +@Component({ + template: ` + + + + + + +
{{entry.row.obj.startedBy | fullName}}
+
+
+
+
` +}) + +class ProcessListContextMenuComponent implements OnInit { + + appId: number; + @Output() + contextAction = new EventEmitter(); + private performAction$ = new Subject(); + + ngOnInit() { + this.performContextActions(); + } + + onShowRowContextMenu(event: DataCellEvent) { + event.value.actions = [ + { + data: event.value.row['obj'], + model: + { + key: 'processDetails', + icon: 'open', + title: 'View Process Details', + visible: true + }, + subject: this.performAction$ + }, + { + data: event.value.row['obj'], + model: + { + key: 'cancel', + icon: 'open', + title: 'Cancel Process', + visible: true + }, + subject: this.performAction$ + } + ]; + } + + performContextActions() { + this.performAction$ + .subscribe((action: any) => { + this.contextAction.emit(action.data); + }); + } +} + +describe('ProcessListContextMenuComponent', () => { + let fixture: ComponentFixture; + let customComponent: ProcessListContextMenuComponent; + let processService: ProcessService; + let element: HTMLElement; + + setupTestBed({ + imports: [CoreModule.forRoot()], + declarations: [ProcessInstanceListComponent, ProcessListContextMenuComponent], + providers: [ProcessService] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProcessListContextMenuComponent); + customComponent = fixture.componentInstance; + element = fixture.nativeElement; + processService = TestBed.get(ProcessService); + customComponent.appId = 12345; + spyOn(processService, 'getProcesses').and.returnValue(of(fakeProcessInstance)); + fixture.detectChanges(); + }); + + afterEach(() => { + const event = new KeyboardEvent('keydown', { + bubbles : true, cancelable : true, key : 'Escape' + }); + document.querySelector('.cdk-overlay-backdrop').dispatchEvent(event); + fixture.detectChanges(); + }); + + it('Should be able to show context menu on process list', async () => { + const contextMenu = element.querySelector(`[data-automation-id="text_${fakeProcessInstance.data[0].name}"]`); + const contextActionSpy = spyOn(customComponent.contextAction, 'emit').and.callThrough(); + contextMenu.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true })); + fixture.detectChanges(); + await fixture.whenStable(); + const contextActions = document.querySelectorAll('.mat-menu-item'); + + expect(contextActions.length).toBe(2); + expect(contextActions[0]['disabled']).toBe(false, 'View Process Details action not enabled'); + expect(contextActions[1]['disabled']).toBe(false, 'Cancel Process action not enabled'); + contextActions[0].dispatchEvent(new Event('click')); + fixture.detectChanges(); + expect(contextActionSpy).toHaveBeenCalled(); + }); +}); diff --git a/lib/process-services/src/lib/process-list/components/process-list.component.ts b/lib/process-services/src/lib/process-list/components/process-list.component.ts index 502292995a..5cd9a1a8ca 100644 --- a/lib/process-services/src/lib/process-list/components/process-list.component.ts +++ b/lib/process-services/src/lib/process-list/components/process-list.component.ts @@ -27,7 +27,8 @@ import { PaginatedComponent, PaginationComponent, PaginationModel, - UserPreferencesService + UserPreferencesService, + DataCellEvent } from '@alfresco/adf-core'; import { AfterContentInit, @@ -110,6 +111,14 @@ export class ProcessInstanceListComponent extends DataTableSchema implements OnC @Input() selectFirstRow: boolean = true; + /** Toggles custom context menu for the component. */ + @Input() + showContextMenu: boolean = false; + + /** Emitted before the context menu is displayed for a row. */ + @Output() + showRowContextMenu = new EventEmitter(); + /** * Resolver function is used to show dynamic complex column objects * see the docs to learn how to configure a resolverFn. @@ -284,6 +293,10 @@ export class ProcessInstanceListComponent extends DataTableSchema implements OnC } } + onShowRowContextMenu(event: DataCellEvent) { + this.showRowContextMenu.emit(event); + } + private createRequestNode(): ProcessFilterParamRepresentationModel { return new ProcessFilterParamRepresentationModel({ appDefinitionId: this.appId, diff --git a/lib/process-services/src/lib/task-list/components/task-list.component.html b/lib/process-services/src/lib/task-list/components/task-list.component.html index 33f805cf8f..4264ea60e3 100644 --- a/lib/process-services/src/lib/task-list/components/task-list.component.html +++ b/lib/process-services/src/lib/task-list/components/task-list.component.html @@ -8,6 +8,8 @@ [loading]="isLoading" [multiselect]="multiselect" [selectionMode]="selectionMode" + [contextMenu]="showContextMenu" + (showRowContextMenu)="onShowRowContextMenu($event)" (row-select)="onRowSelect($event)" (row-unselect)="onRowUnselect($event)" (rowClick)="onRowClick($event)" diff --git a/lib/process-services/src/lib/task-list/components/task-list.component.spec.ts b/lib/process-services/src/lib/task-list/components/task-list.component.spec.ts index a7c915514b..eee621373c 100644 --- a/lib/process-services/src/lib/task-list/components/task-list.component.spec.ts +++ b/lib/process-services/src/lib/task-list/components/task-list.component.spec.ts @@ -15,16 +15,16 @@ * limitations under the License. */ -import { Component, SimpleChange, ViewChild } from '@angular/core'; +import { Component, SimpleChange, ViewChild, OnInit, Output, EventEmitter } from '@angular/core'; import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { AppConfigService, setupTestBed, CoreModule, DataTableModule, DataRowEvent, ObjectDataRow } from '@alfresco/adf-core'; +import { AppConfigService, setupTestBed, CoreModule, DataTableModule, DataRowEvent, ObjectDataRow, DataCellEvent } from '@alfresco/adf-core'; import { TaskListService } from '../services/tasklist.service'; import { TaskListComponent } from './task-list.component'; import { ProcessTestingModule } from '../../testing/process.testing.module'; import { fakeGlobalTask, fakeCustomSchema, fakeEmptyTask, paginatedTask } from '../../mock'; import { TranslateService } from '@ngx-translate/core'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { TaskListModule } from '../task-list.module'; declare let jasmine: any; @@ -807,3 +807,112 @@ describe('Task List: Custom EmptyTemplateComponent', () => { }); }); }); + +@Component({ + template: ` + + + + + + +
{{entry.row?.obj?.startedBy | fullName}}
+
+
+
+
` +}) + +class TaskListContextMenuComponent implements OnInit { + + @Output() + contextAction = new EventEmitter(); + private performAction$ = new Subject(); + + ngOnInit() { + this.performContextActions(); + } + + onShowRowContextMenu(event: DataCellEvent) { + event.value.actions = [ + { + data: event.value.row['obj'], + model: + { + key: 'taskDetails', + icon: 'open', + title: 'View Task Details', + visible: true + }, + subject: this.performAction$ + }, + { + data: event.value.row['obj'], + model: + { + key: 'cancel', + icon: 'open', + title: 'Cancel Process', + visible: true + }, + subject: this.performAction$ + } + ]; + } + + performContextActions() { + this.performAction$ + .subscribe((action: any) => { + this.contextAction.emit(action.data); + }); + } +} + +describe('TaskListContextMenuComponent', () => { + let fixture: ComponentFixture; + let customComponent: TaskListContextMenuComponent; + let taskListService: TaskListService; + let element: HTMLElement; + + setupTestBed({ + imports: [CoreModule.forRoot()], + declarations: [TaskListComponent, TaskListContextMenuComponent], + providers: [TaskListService] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TaskListContextMenuComponent); + customComponent = fixture.componentInstance; + element = fixture.nativeElement; + taskListService = TestBed.get(TaskListService); + spyOn(taskListService, 'findTasksByState').and.returnValues(of(fakeGlobalTask)); + fixture.detectChanges(); + }); + + afterEach(() => { + const event = new KeyboardEvent('keydown', { + bubbles : true, cancelable : true, key : 'Escape' + }); + document.querySelector('.cdk-overlay-backdrop').dispatchEvent(event); + fixture.detectChanges(); + }); + + it('Should be able to show context menu on task list', async () => { + const contextMenu = element.querySelector(`[data-automation-id="text_${fakeGlobalTask.data[0].name}"]`); + const contextActionSpy = spyOn(customComponent.contextAction, 'emit').and.callThrough(); + contextMenu.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true })); + fixture.detectChanges(); + await fixture.whenStable(); + const contextActions = document.querySelectorAll('.mat-menu-item'); + + expect(contextActions.length).toBe(2); + expect(contextActions[0]['disabled']).toBe(false, 'View Task Details action not enabled'); + expect(contextActions[1]['disabled']).toBe(false, 'Cancel Task action not enabled'); + contextActions[0].dispatchEvent(new Event('click')); + fixture.detectChanges(); + expect(contextActionSpy).toHaveBeenCalled(); + }); +}); diff --git a/lib/process-services/src/lib/task-list/components/task-list.component.ts b/lib/process-services/src/lib/task-list/components/task-list.component.ts index 31f8411e00..ed851203d7 100644 --- a/lib/process-services/src/lib/task-list/components/task-list.component.ts +++ b/lib/process-services/src/lib/task-list/components/task-list.component.ts @@ -18,7 +18,7 @@ import { DataRowEvent, DataTableAdapter, DataTableSchema, CustomEmptyContentTemplateDirective, CustomLoadingContentTemplateDirective, AppConfigService, PaginationComponent, PaginatedComponent, - UserPreferencesService, UserPreferenceValues, PaginationModel } from '@alfresco/adf-core'; + UserPreferencesService, UserPreferenceValues, PaginationModel, DataCellEvent } from '@alfresco/adf-core'; import { AfterContentInit, Component, ContentChild, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnDestroy, OnInit } from '@angular/core'; @@ -123,6 +123,14 @@ export class TaskListComponent extends DataTableSchema implements OnChanges, Aft @Input() start: number; + /** Toggles custom context menu for the component. */ + @Input() + showContextMenu: boolean = false; + + /** Emitted before the context menu is displayed for a row. */ + @Output() + showRowContextMenu = new EventEmitter(); + /** Emitted when a task in the list is clicked */ @Output() rowClick = new EventEmitter(); @@ -356,6 +364,10 @@ export class TaskListComponent extends DataTableSchema implements OnChanges, Aft } } + onShowRowContextMenu(event: DataCellEvent) { + this.showRowContextMenu.emit(event); + } + /** * Optimize name field * @param instances