diff --git a/demo-shell/src/app/app.module.ts b/demo-shell/src/app/app.module.ts index 50899dc56c..9cc184f586 100644 --- a/demo-shell/src/app/app.module.ts +++ b/demo-shell/src/app/app.module.ts @@ -132,7 +132,7 @@ registerLocaleData(localeSv); ContentModule.forRoot(), InsightsModule.forRoot(), ProcessModule.forRoot(), - ProcessServicesCloudModule, + ProcessServicesCloudModule.forRoot(), ExtensionsModule.forRoot(), ThemePickerModule, ChartsModule, diff --git a/demo-shell/src/app/components/cloud/start-process-cloud-demo.component.ts b/demo-shell/src/app/components/cloud/start-process-cloud-demo.component.ts index dd680a36cb..84336c708a 100644 --- a/demo-shell/src/app/components/cloud/start-process-cloud-demo.component.ts +++ b/demo-shell/src/app/components/cloud/start-process-cloud-demo.component.ts @@ -17,13 +17,17 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { NotificationService, AppConfigService } from '@alfresco/adf-core'; +import { NotificationService, AppConfigService, FormRenderingService } from '@alfresco/adf-core'; import { CloudLayoutService } from './services/cloud-layout.service'; import { PreviewService } from '../../services/preview.service'; +import { CloudFormRenderingService } from '@alfresco/adf-process-services-cloud'; @Component({ templateUrl: './start-process-cloud-demo.component.html', - styleUrls: ['./start-process-cloud-demo.component.scss'] + styleUrls: ['./start-process-cloud-demo.component.scss'], + providers: [ + { provide: FormRenderingService, useClass: CloudFormRenderingService } + ] }) export class StartProcessCloudDemoComponent implements OnInit { diff --git a/docs/content-services/components/content-node-selector-panel.component.md b/docs/content-services/components/content-node-selector-panel.component.md index a0c0086a79..17c9422fe7 100644 --- a/docs/content-services/components/content-node-selector-panel.component.md +++ b/docs/content-services/components/content-node-selector-panel.component.md @@ -44,6 +44,7 @@ Opens a [Content Node Selector](content-node-selector.component.md) in its own | showDropdownSiteList | `boolean` | | Toggle sites list dropdown rendering | | showFilesInResult | `void` | | Shows the files and folders in the search result | | showSearch | `boolean` | | Toggle search input rendering | +| selectionMode | `string` | "single" | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode, you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. | ### Events diff --git a/docs/content-services/components/document-list.component.md b/docs/content-services/components/document-list.component.md index b20ef47027..3bd6f24b3d 100644 --- a/docs/content-services/components/document-list.component.md +++ b/docs/content-services/components/document-list.component.md @@ -94,6 +94,7 @@ Displays the documents from a repository. | nodeDblClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntityEvent`](../../../lib/content-services/src/lib/document-list/components/node.event.ts)`>` | Emitted when the user double-clicks a list node | | preview | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntityEvent`](../../../lib/content-services/src/lib/document-list/components/node.event.ts)`>` | Emitted when the user acts upon files with either single or double click (depends on `navigation-mode`). Useful for integration with the [Viewer component](../../core/components/viewer.component.md). | | ready | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodePaging`](https://github.com/Alfresco/alfresco-js-api/blob/development/src/api/content-rest-api/docs/NodePaging.md)`>` | Emitted when the Document List has loaded all items and is ready for use | +| nodeSelected | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`NodeEntryEvent`](../../../lib/content-services/src/lib/document-list/components/node.event.ts)`>` | Returns the current node selected | ## Details diff --git a/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts b/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts index abd4323abf..6e3e8fd796 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts @@ -140,6 +140,7 @@ export class ContentNodeDialogService { const data: ContentNodeSelectorComponentData = { title: this.getTitleTranslation(action, contentEntry.name), actionName: action, + selectionMode: 'single', currentFolderId: contentEntry.parentId, imageResolver: this.imageResolver.bind(this), where: '(isFolder=true)', @@ -182,6 +183,7 @@ export class ContentNodeDialogService { const data: ContentNodeSelectorComponentData = { title: this.getTitleTranslation(action, this.translation.instant('DROPDOWN.MY_FILES_OPTION')), actionName: action, + selectionMode: 'single', currentFolderId: contentEntry.id, imageResolver: this.imageResolver.bind(this), isSelectionValid: this.hasAllowableOperationsOnNodeFolder.bind(this), @@ -209,6 +211,7 @@ export class ContentNodeDialogService { const data: ContentNodeSelectorComponentData = { title: this.getTitleTranslation(action, this.translation.instant('DROPDOWN.MY_FILES_OPTION')), actionName: action, + selectionMode: 'single', currentFolderId: contentEntry.id, imageResolver: this.imageResolver.bind(this), isSelectionValid: (entry: Node) => entry.isFile, diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html index 91041822c8..3e90f3693c 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts index 417ad1b8dd..868441e18f 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts @@ -133,14 +133,14 @@ describe('ContentNodeSelectorComponent', () => { }); it('should trigger the select event when selection has been made', (done) => { - const expectedNode = {}; + const expectedNode = { id: 'fakeid'}; component.select.subscribe((nodes) => { expect(nodes.length).toBe(1); expect(nodes[0]).toBe(expectedNode); done(); }); - component.chosenNode = expectedNode; + component.chosenNode = [expectedNode]; }); it('should be able to filter out the exclude site content', () => { @@ -291,7 +291,7 @@ describe('ContentNodeSelectorComponent', () => { tick(debounceSearch); const chosenNode = new Node({ path: { elements: ['one'] } }); - component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); + component.onCurrentSelection([ { entry: chosenNode } ]); fixture.detectChanges(); tick(debounceSearch); @@ -313,7 +313,7 @@ describe('ContentNodeSelectorComponent', () => { fixture.detectChanges(); const chosenNode = { path: { elements: [] } }; - component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); + component.onCurrentSelection([ { entry: chosenNode } ]); fixture.detectChanges(); tick(debounceSearch); @@ -395,6 +395,7 @@ describe('ContentNodeSelectorComponent', () => { describe('Search functionality', () => { let getCorrespondingNodeIdsSpy; + const entry: Node = { id: 'fakeid'}; const defaultSearchOptions = (searchTerm, rootNodeId = undefined, skipCount = 0, showFiles = false) => { @@ -472,7 +473,7 @@ describe('ContentNodeSelectorComponent', () => { })); it('should reset the currently chosen node in case of starting a new search', fakeAsync(() => { - component.chosenNode = {}; + component.chosenNode = [entry]; typeToSearchBox('kakarot'); tick(debounceSearch); @@ -617,10 +618,11 @@ describe('ContentNodeSelectorComponent', () => { })); it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => { - component.chosenNode = {}; + component.chosenNode = [entry]; + component.nodePaging = { list: { - entries: [{ entry: component.chosenNode }] + entries: [{ entry }] } }; component.searchTerm = 'piccolo'; @@ -935,7 +937,7 @@ describe('ContentNodeSelectorComponent', () => { describe('Chosen node', () => { - const entry: Node = {}; + const entry: Node = { id: 'fakeid'}; const nodePage: NodePaging = { list: {}, pagination: {} }; let hasAllowableOperations; @@ -960,11 +962,10 @@ describe('ContentNodeSelectorComponent', () => { component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); expect(nodes).not.toBeNull(); - expect(component.chosenNode).toBe(entry); + expect(component.chosenNode[0]).toBe(entry); }); component.documentList.ready.emit(nodePage); - fixture.detectChanges(); })); it('should be null after selecting node without the necessary permissions', async(() => { @@ -978,7 +979,6 @@ describe('ContentNodeSelectorComponent', () => { }); component.documentList.ready.emit(nodePage); - fixture.detectChanges(); })); it('should NOT be null after clicking on a node (with the right permissions) in the list (onNodeSelect)', async(() => { @@ -987,54 +987,48 @@ describe('ContentNodeSelectorComponent', () => { component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); expect(nodes).not.toBeNull(); - expect(component.chosenNode).toBe(entry); + expect(component.chosenNode[0]).toBe(entry); }); - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); + component.onCurrentSelection([ { entry } ]); })); - it('should remain null when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', async(() => { + it('should remain empty when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', async(() => { hasAllowableOperations = false; component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); - expect(nodes).toBeNull(); - expect(component.chosenNode).toBeNull(); + expect(nodes).toEqual([]); + expect(component.chosenNode).toEqual([]); }); - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); + component.onCurrentSelection([ { entry } ]); })); - it('should become null when clicking on a node (with the WRONG permissions) after previously selecting a right node', async(() => { + it('should become empty when clicking on a node (with the WRONG permissions) after previously selecting a right node', async(() => { component.select.subscribe((nodes) => { - if (hasAllowableOperations) { expect(nodes).toBeDefined(); expect(nodes).not.toBeNull(); - expect(component.chosenNode).not.toBeNull(); - + expect(component.chosenNode[0]).not.toBeNull(); } else { expect(nodes).toBeDefined(); - expect(nodes).toBeNull(); - expect(component.chosenNode).toBeNull(); + expect(nodes).toEqual([]); + expect(component.chosenNode).toEqual([]); } }); hasAllowableOperations = true; - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); + component.onCurrentSelection([{ entry }]); hasAllowableOperations = false; - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); + component.onCurrentSelection([{ entry }]); + })); - it('should be null when the chosenNode is reset', async(() => { + it('should be empty when the chosenNode is reset', async(() => { hasAllowableOperations = true; - component.onNodeSelect({ detail: { node: { entry: {} } } }); - fixture.detectChanges(); + component.onCurrentSelection([{ entry: {} }]); component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); @@ -1043,7 +1037,6 @@ describe('ContentNodeSelectorComponent', () => { }); component.resetChosenNode(); - fixture.detectChanges(); })); }); @@ -1060,7 +1053,7 @@ describe('ContentNodeSelectorComponent', () => { component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); expect(nodes).not.toBeNull(); - expect(component.chosenNode).not.toBeNull(); + expect(component.chosenNode[0]).not.toBeNull(); expect(component.isSelectionValid).not.toBeNull(); }); @@ -1070,21 +1063,21 @@ describe('ContentNodeSelectorComponent', () => { it('should NOT be null after clicking on a node in the list (onNodeSelect)', async(() => { fixture.detectChanges(); + component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); expect(nodes).not.toBeNull(); - expect(component.chosenNode).not.toBeNull(); + expect(component.chosenNode[0]).not.toBeNull(); + expect(component.chosenNode[0].id).toBe('fakeid'); expect(component.isSelectionValid).not.toBeNull(); }); - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); + component.onCurrentSelection([{ entry }]); })); it('should be null when the chosenNode is reset', async(() => { fixture.detectChanges(); - component.onNodeSelect({ detail: { node: { entry: {} } } }); - fixture.detectChanges(); + component.onCurrentSelection([{ entry: {} }]); component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); @@ -1111,7 +1104,7 @@ describe('ContentNodeSelectorComponent', () => { component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); expect(nodes).not.toBeNull(); - expect(component.chosenNode).not.toBeNull(); + expect(component.chosenNode[0]).not.toBeNull(); expect(component.isSelectionValid).not.toBeNull(); }); @@ -1119,24 +1112,9 @@ describe('ContentNodeSelectorComponent', () => { fixture.detectChanges(); })); - it('should NOT be null after clicking on a node in the list (onNodeSelect)', async(() => { - component.select.subscribe((nodes) => { - expect(nodes).toBeDefined(); - expect(nodes).not.toBeNull(); - expect(component.chosenNode).not.toBeNull(); - expect(component.isSelectionValid).not.toBeNull(); - }); - fixture.detectChanges(); - - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - })); - it('should be null when the chosenNode is reset', async(() => { fixture.detectChanges(); - - component.onNodeSelect({ detail: { node: { entry: {} } } }); - fixture.detectChanges(); + component.onCurrentSelection([{ entry: {} }]); component.select.subscribe((nodes) => { expect(nodes).toBeDefined(); @@ -1149,6 +1127,7 @@ describe('ContentNodeSelectorComponent', () => { fixture.detectChanges(); })); }); + }); }); }); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts index 9f05b3bfd3..05e36068da 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts @@ -26,7 +26,7 @@ import { SitesService } from '@alfresco/adf-core'; import { FormControl } from '@angular/forms'; -import { Node, NodePaging, Pagination, SiteEntry, SitePaging } from '@alfresco/js-api'; +import { Node, NodePaging, Pagination, SiteEntry, SitePaging, NodeEntry } from '@alfresco/js-api'; import { DocumentListComponent } from '../document-list/components/document-list.component'; import { RowFilter } from '../document-list/data/row-filter.model'; import { ImageResolver } from '../document-list/data/image-resolver.model'; @@ -132,6 +132,10 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { @Input() pageSize: number = this.DEFAULT_PAGINATION.maxItems; + /** Define the selection mode for document list. The allowed values are single or multiple */ + @Input() + selectionMode: 'single' | 'multiple' = 'single'; + /** Function used to decide if the selected node has permission to be selected. * Default value is a function that always returns true. */ @@ -198,7 +202,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { showingSearchResults: boolean = false; loadingSearchResults: boolean = false; inDialog: boolean = false; - _chosenNode: Node = null; + _chosenNode: Node [] = null; folderIdToShow: string | null = null; breadcrumbFolderTitle: string | null = null; startSiteGuid: string | null = null; @@ -223,13 +227,9 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { private sitesService: SitesService) { } - set chosenNode(value: Node) { + set chosenNode(value: Node[]) { this._chosenNode = value; - let valuesArray = null; - if (value) { - valuesArray = [value]; - } - this.select.next(valuesArray); + this.select.next(value); } get chosenNode() { @@ -334,7 +334,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { let folderNode: Node; if (this.showingSearchResults && this.chosenNode) { - folderNode = this.chosenNode; + folderNode = this.chosenNode[0]; } else { folderNode = this.documentList.folderNode; } @@ -468,7 +468,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { */ private attemptNodeSelection(entry: Node): void { if (entry && this.isSelectionValid(entry)) { - this.chosenNode = entry; + this.chosenNode = [entry]; } else { this.resetChosenNode(); } @@ -482,12 +482,14 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { } /** - * Invoked when user selects a node + * It filters and emit the selection coming from the document list * - * @param event CustomEvent for node-select + * @param nodesEntries */ - onNodeSelect(event: any): void { - this.attemptNodeSelection(event.detail.node.entry); + onCurrentSelection(nodesEntries: NodeEntry[]): void { + const validNodesEntity = nodesEntries.filter((node) => this.isSelectionValid(node.entry)); + const nodes: Node[] = validNodesEntity.map((node) => node.entry ); + this.chosenNode = nodes; } setTitleIfCustomSite(site: SiteEntry) { diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.component-data.interface.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector.component-data.interface.ts index c5c9a488f1..bd15647b3a 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.component-data.interface.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector.component-data.interface.ts @@ -27,6 +27,7 @@ export interface ContentNodeSelectorComponentData { rowFilter?: any; where?: string; imageResolver?: any; + selectionMode: string; isSelectionValid?: (entry: Node) => boolean; breadcrumbTransform?: (node) => any; excludeSiteContent?: string[]; diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html b/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html index 5362b57cd7..302371d796 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html @@ -14,6 +14,7 @@ [isSelectionValid]="data?.isSelectionValid" [breadcrumbTransform]="data?.breadcrumbTransform" [excludeSiteContent]="data?.excludeSiteContent" + [selectionMode]="data?.selectionMode" [where]="data?.where" [showSearch]="data?.showSearch" [showDropdownSiteList]="data?.showDropdownSiteList" @@ -31,7 +32,7 @@