[AAE-3115] Content node selector - Ability to select multiple files (#5904)

* Ability to select multiple files

* Fix unit test

* Rename event into NodeSelected

* restrict the typo

Co-authored-by: Denys Vuika <denys.vuika@alfresco.com>

Co-authored-by: Denys Vuika <denys.vuika@alfresco.com>
This commit is contained in:
Maurizio Vitale 2020-07-24 10:12:51 +01:00 committed by GitHub
parent 5a820cbecd
commit 1dde6bb1c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 120 additions and 88 deletions

View File

@ -132,7 +132,7 @@ registerLocaleData(localeSv);
ContentModule.forRoot(), ContentModule.forRoot(),
InsightsModule.forRoot(), InsightsModule.forRoot(),
ProcessModule.forRoot(), ProcessModule.forRoot(),
ProcessServicesCloudModule, ProcessServicesCloudModule.forRoot(),
ExtensionsModule.forRoot(), ExtensionsModule.forRoot(),
ThemePickerModule, ThemePickerModule,
ChartsModule, ChartsModule,

View File

@ -17,13 +17,17 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; 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 { CloudLayoutService } from './services/cloud-layout.service';
import { PreviewService } from '../../services/preview.service'; import { PreviewService } from '../../services/preview.service';
import { CloudFormRenderingService } from '@alfresco/adf-process-services-cloud';
@Component({ @Component({
templateUrl: './start-process-cloud-demo.component.html', 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 { export class StartProcessCloudDemoComponent implements OnInit {

View File

@ -44,6 +44,7 @@ Opens a [Content Node Selector](content-node-selector.component.md) in its own
| showDropdownSiteList | `boolean` | | Toggle sites list dropdown rendering | | showDropdownSiteList | `boolean` | | Toggle sites list dropdown rendering |
| showFilesInResult | `void` | | Shows the files and folders in the search result | | showFilesInResult | `void` | | Shows the files and folders in the search result |
| showSearch | `boolean` | | Toggle search input rendering | | 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 ### Events

View File

@ -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 | | 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). | | 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 | | 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 ## Details

View File

@ -140,6 +140,7 @@ export class ContentNodeDialogService {
const data: ContentNodeSelectorComponentData = { const data: ContentNodeSelectorComponentData = {
title: this.getTitleTranslation(action, contentEntry.name), title: this.getTitleTranslation(action, contentEntry.name),
actionName: action, actionName: action,
selectionMode: 'single',
currentFolderId: contentEntry.parentId, currentFolderId: contentEntry.parentId,
imageResolver: this.imageResolver.bind(this), imageResolver: this.imageResolver.bind(this),
where: '(isFolder=true)', where: '(isFolder=true)',
@ -182,6 +183,7 @@ export class ContentNodeDialogService {
const data: ContentNodeSelectorComponentData = { const data: ContentNodeSelectorComponentData = {
title: this.getTitleTranslation(action, this.translation.instant('DROPDOWN.MY_FILES_OPTION')), title: this.getTitleTranslation(action, this.translation.instant('DROPDOWN.MY_FILES_OPTION')),
actionName: action, actionName: action,
selectionMode: 'single',
currentFolderId: contentEntry.id, currentFolderId: contentEntry.id,
imageResolver: this.imageResolver.bind(this), imageResolver: this.imageResolver.bind(this),
isSelectionValid: this.hasAllowableOperationsOnNodeFolder.bind(this), isSelectionValid: this.hasAllowableOperationsOnNodeFolder.bind(this),
@ -209,6 +211,7 @@ export class ContentNodeDialogService {
const data: ContentNodeSelectorComponentData = { const data: ContentNodeSelectorComponentData = {
title: this.getTitleTranslation(action, this.translation.instant('DROPDOWN.MY_FILES_OPTION')), title: this.getTitleTranslation(action, this.translation.instant('DROPDOWN.MY_FILES_OPTION')),
actionName: action, actionName: action,
selectionMode: 'single',
currentFolderId: contentEntry.id, currentFolderId: contentEntry.id,
imageResolver: this.imageResolver.bind(this), imageResolver: this.imageResolver.bind(this),
isSelectionValid: (entry: Node) => entry.isFile, isSelectionValid: (entry: Node) => entry.isFile,

View File

@ -1,4 +1,4 @@
<div class="adf-content-node-selector-content" (node-select)="onNodeSelect($event)"> <div class="adf-content-node-selector-content">
<mat-form-field floatPlaceholder="never" class="adf-content-node-selector-content-input" *ngIf="showSearch"> <mat-form-field floatPlaceholder="never" class="adf-content-node-selector-content-input" *ngIf="showSearch">
<input matInput <input matInput
id="searchInput" id="searchInput"
@ -64,7 +64,7 @@
[rowFilter]="_rowFilter" [rowFilter]="_rowFilter"
[imageResolver]="imageResolver" [imageResolver]="imageResolver"
[currentFolderId]="folderIdToShow" [currentFolderId]="folderIdToShow"
selectionMode="single" [selectionMode]="selectionMode"
[contextMenuActions]="false" [contextMenuActions]="false"
[contentActions]="false" [contentActions]="false"
[allowDropFiles]="false" [allowDropFiles]="false"
@ -72,6 +72,7 @@
[where]="where" [where]="where"
(folderChange)="onFolderChange()" (folderChange)="onFolderChange()"
(ready)="onFolderLoaded()" (ready)="onFolderLoaded()"
(nodeSelected)="onCurrentSelection($event)"
data-automation-id="content-node-selector-document-list"> data-automation-id="content-node-selector-document-list">
<adf-custom-empty-content-template> <adf-custom-empty-content-template>

View File

@ -133,14 +133,14 @@ describe('ContentNodeSelectorComponent', () => {
}); });
it('should trigger the select event when selection has been made', (done) => { it('should trigger the select event when selection has been made', (done) => {
const expectedNode = <Node> {}; const expectedNode = <Node> { id: 'fakeid'};
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes.length).toBe(1); expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode); expect(nodes[0]).toBe(expectedNode);
done(); done();
}); });
component.chosenNode = expectedNode; component.chosenNode = [expectedNode];
}); });
it('should be able to filter out the exclude site content', () => { it('should be able to filter out the exclude site content', () => {
@ -291,7 +291,7 @@ describe('ContentNodeSelectorComponent', () => {
tick(debounceSearch); tick(debounceSearch);
const chosenNode = new Node({ path: { elements: ['one'] } }); const chosenNode = new Node({ path: { elements: ['one'] } });
component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); component.onCurrentSelection([ { entry: chosenNode } ]);
fixture.detectChanges(); fixture.detectChanges();
tick(debounceSearch); tick(debounceSearch);
@ -313,7 +313,7 @@ describe('ContentNodeSelectorComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const chosenNode = <Node> { path: { elements: [] } }; const chosenNode = <Node> { path: { elements: [] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); component.onCurrentSelection([ { entry: chosenNode } ]);
fixture.detectChanges(); fixture.detectChanges();
tick(debounceSearch); tick(debounceSearch);
@ -395,6 +395,7 @@ describe('ContentNodeSelectorComponent', () => {
describe('Search functionality', () => { describe('Search functionality', () => {
let getCorrespondingNodeIdsSpy; let getCorrespondingNodeIdsSpy;
const entry: Node = <Node> { id: 'fakeid'};
const defaultSearchOptions = (searchTerm, rootNodeId = undefined, skipCount = 0, showFiles = false) => { 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(() => { it('should reset the currently chosen node in case of starting a new search', fakeAsync(() => {
component.chosenNode = <Node> {}; component.chosenNode = [entry];
typeToSearchBox('kakarot'); typeToSearchBox('kakarot');
tick(debounceSearch); tick(debounceSearch);
@ -617,10 +618,11 @@ describe('ContentNodeSelectorComponent', () => {
})); }));
it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => { it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => {
component.chosenNode = <Node> {}; component.chosenNode = [entry];
component.nodePaging = { component.nodePaging = {
list: { list: {
entries: [{ entry: component.chosenNode }] entries: [{ entry }]
} }
}; };
component.searchTerm = 'piccolo'; component.searchTerm = 'piccolo';
@ -935,7 +937,7 @@ describe('ContentNodeSelectorComponent', () => {
describe('Chosen node', () => { describe('Chosen node', () => {
const entry: Node = <Node> {}; const entry: Node = <Node> { id: 'fakeid'};
const nodePage: NodePaging = <NodePaging> { list: {}, pagination: {} }; const nodePage: NodePaging = <NodePaging> { list: {}, pagination: {} };
let hasAllowableOperations; let hasAllowableOperations;
@ -960,11 +962,10 @@ describe('ContentNodeSelectorComponent', () => {
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).not.toBeNull(); expect(nodes).not.toBeNull();
expect(component.chosenNode).toBe(entry); expect(component.chosenNode[0]).toBe(entry);
}); });
component.documentList.ready.emit(nodePage); component.documentList.ready.emit(nodePage);
fixture.detectChanges();
})); }));
it('should be null after selecting node without the necessary permissions', async(() => { it('should be null after selecting node without the necessary permissions', async(() => {
@ -978,7 +979,6 @@ describe('ContentNodeSelectorComponent', () => {
}); });
component.documentList.ready.emit(nodePage); 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(() => { 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) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).not.toBeNull(); expect(nodes).not.toBeNull();
expect(component.chosenNode).toBe(entry); expect(component.chosenNode[0]).toBe(entry);
}); });
component.onNodeSelect({ detail: { node: { entry } } }); component.onCurrentSelection([ { entry } ]);
fixture.detectChanges();
})); }));
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; hasAllowableOperations = false;
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).toBeNull(); expect(nodes).toEqual([]);
expect(component.chosenNode).toBeNull(); expect(component.chosenNode).toEqual([]);
}); });
component.onNodeSelect({ detail: { node: { entry } } }); component.onCurrentSelection([ { entry } ]);
fixture.detectChanges();
})); }));
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) => { component.select.subscribe((nodes) => {
if (hasAllowableOperations) { if (hasAllowableOperations) {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).not.toBeNull(); expect(nodes).not.toBeNull();
expect(component.chosenNode).not.toBeNull(); expect(component.chosenNode[0]).not.toBeNull();
} else { } else {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).toBeNull(); expect(nodes).toEqual([]);
expect(component.chosenNode).toBeNull(); expect(component.chosenNode).toEqual([]);
} }
}); });
hasAllowableOperations = true; hasAllowableOperations = true;
component.onNodeSelect({ detail: { node: { entry } } }); component.onCurrentSelection([{ entry }]);
fixture.detectChanges();
hasAllowableOperations = false; hasAllowableOperations = false;
component.onNodeSelect({ detail: { node: { entry } } }); component.onCurrentSelection([{ entry }]);
fixture.detectChanges();
})); }));
it('should be null when the chosenNode is reset', async(() => { it('should be empty when the chosenNode is reset', async(() => {
hasAllowableOperations = true; hasAllowableOperations = true;
component.onNodeSelect({ detail: { node: { entry: <Node> {} } } }); component.onCurrentSelection([{ entry: <Node> {} }]);
fixture.detectChanges();
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
@ -1043,7 +1037,6 @@ describe('ContentNodeSelectorComponent', () => {
}); });
component.resetChosenNode(); component.resetChosenNode();
fixture.detectChanges();
})); }));
}); });
@ -1060,7 +1053,7 @@ describe('ContentNodeSelectorComponent', () => {
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).not.toBeNull(); expect(nodes).not.toBeNull();
expect(component.chosenNode).not.toBeNull(); expect(component.chosenNode[0]).not.toBeNull();
expect(component.isSelectionValid).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(() => { it('should NOT be null after clicking on a node in the list (onNodeSelect)', async(() => {
fixture.detectChanges(); fixture.detectChanges();
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).not.toBeNull(); 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(); expect(component.isSelectionValid).not.toBeNull();
}); });
component.onNodeSelect({ detail: { node: { entry } } }); component.onCurrentSelection([{ entry }]);
fixture.detectChanges();
})); }));
it('should be null when the chosenNode is reset', async(() => { it('should be null when the chosenNode is reset', async(() => {
fixture.detectChanges(); fixture.detectChanges();
component.onNodeSelect({ detail: { node: { entry: <Node> {} } } }); component.onCurrentSelection([{ entry: <Node> {} }]);
fixture.detectChanges();
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
@ -1111,7 +1104,7 @@ describe('ContentNodeSelectorComponent', () => {
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
expect(nodes).not.toBeNull(); expect(nodes).not.toBeNull();
expect(component.chosenNode).not.toBeNull(); expect(component.chosenNode[0]).not.toBeNull();
expect(component.isSelectionValid).not.toBeNull(); expect(component.isSelectionValid).not.toBeNull();
}); });
@ -1119,24 +1112,9 @@ describe('ContentNodeSelectorComponent', () => {
fixture.detectChanges(); 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(() => { it('should be null when the chosenNode is reset', async(() => {
fixture.detectChanges(); fixture.detectChanges();
component.onCurrentSelection([{ entry: <Node> {} }]);
component.onNodeSelect({ detail: { node: { entry: <Node> {} } } });
fixture.detectChanges();
component.select.subscribe((nodes) => { component.select.subscribe((nodes) => {
expect(nodes).toBeDefined(); expect(nodes).toBeDefined();
@ -1149,6 +1127,7 @@ describe('ContentNodeSelectorComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
})); }));
}); });
}); });
}); });
}); });

View File

@ -26,7 +26,7 @@ import {
SitesService SitesService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { FormControl } from '@angular/forms'; 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 { DocumentListComponent } from '../document-list/components/document-list.component';
import { RowFilter } from '../document-list/data/row-filter.model'; import { RowFilter } from '../document-list/data/row-filter.model';
import { ImageResolver } from '../document-list/data/image-resolver.model'; import { ImageResolver } from '../document-list/data/image-resolver.model';
@ -132,6 +132,10 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
@Input() @Input()
pageSize: number = this.DEFAULT_PAGINATION.maxItems; 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. /** Function used to decide if the selected node has permission to be selected.
* Default value is a function that always returns true. * Default value is a function that always returns true.
*/ */
@ -198,7 +202,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
showingSearchResults: boolean = false; showingSearchResults: boolean = false;
loadingSearchResults: boolean = false; loadingSearchResults: boolean = false;
inDialog: boolean = false; inDialog: boolean = false;
_chosenNode: Node = null; _chosenNode: Node [] = null;
folderIdToShow: string | null = null; folderIdToShow: string | null = null;
breadcrumbFolderTitle: string | null = null; breadcrumbFolderTitle: string | null = null;
startSiteGuid: string | null = null; startSiteGuid: string | null = null;
@ -223,13 +227,9 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
private sitesService: SitesService) { private sitesService: SitesService) {
} }
set chosenNode(value: Node) { set chosenNode(value: Node[]) {
this._chosenNode = value; this._chosenNode = value;
let valuesArray = null; this.select.next(value);
if (value) {
valuesArray = [value];
}
this.select.next(valuesArray);
} }
get chosenNode() { get chosenNode() {
@ -334,7 +334,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
let folderNode: Node; let folderNode: Node;
if (this.showingSearchResults && this.chosenNode) { if (this.showingSearchResults && this.chosenNode) {
folderNode = this.chosenNode; folderNode = this.chosenNode[0];
} else { } else {
folderNode = this.documentList.folderNode; folderNode = this.documentList.folderNode;
} }
@ -468,7 +468,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
*/ */
private attemptNodeSelection(entry: Node): void { private attemptNodeSelection(entry: Node): void {
if (entry && this.isSelectionValid(entry)) { if (entry && this.isSelectionValid(entry)) {
this.chosenNode = entry; this.chosenNode = [entry];
} else { } else {
this.resetChosenNode(); 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 { onCurrentSelection(nodesEntries: NodeEntry[]): void {
this.attemptNodeSelection(event.detail.node.entry); const validNodesEntity = nodesEntries.filter((node) => this.isSelectionValid(node.entry));
const nodes: Node[] = validNodesEntity.map((node) => node.entry );
this.chosenNode = nodes;
} }
setTitleIfCustomSite(site: SiteEntry) { setTitleIfCustomSite(site: SiteEntry) {

View File

@ -27,6 +27,7 @@ export interface ContentNodeSelectorComponentData {
rowFilter?: any; rowFilter?: any;
where?: string; where?: string;
imageResolver?: any; imageResolver?: any;
selectionMode: string;
isSelectionValid?: (entry: Node) => boolean; isSelectionValid?: (entry: Node) => boolean;
breadcrumbTransform?: (node) => any; breadcrumbTransform?: (node) => any;
excludeSiteContent?: string[]; excludeSiteContent?: string[];

View File

@ -14,6 +14,7 @@
[isSelectionValid]="data?.isSelectionValid" [isSelectionValid]="data?.isSelectionValid"
[breadcrumbTransform]="data?.breadcrumbTransform" [breadcrumbTransform]="data?.breadcrumbTransform"
[excludeSiteContent]="data?.excludeSiteContent" [excludeSiteContent]="data?.excludeSiteContent"
[selectionMode]="data?.selectionMode"
[where]="data?.where" [where]="data?.where"
[showSearch]="data?.showSearch" [showSearch]="data?.showSearch"
[showDropdownSiteList]="data?.showDropdownSiteList" [showDropdownSiteList]="data?.showDropdownSiteList"
@ -31,7 +32,7 @@
</button> </button>
<button mat-button <button mat-button
[disabled]="!chosenNode" [disabled]="!hasNodeSelected()"
class="adf-choose-action" class="adf-choose-action"
(click)="onClick()" (click)="onClick()"
data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }} data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }}

View File

@ -49,6 +49,10 @@ export class ContentNodeSelectorComponent {
this.chosenNode = nodeList; this.chosenNode = nodeList;
} }
hasNodeSelected(): boolean {
return this.chosenNode?.length > 0;
}
onSiteChange(siteTitle: string) { onSiteChange(siteTitle: string) {
this.updateTitle(siteTitle); this.updateTitle(siteTitle);
} }

View File

@ -304,6 +304,10 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
@Output() @Output()
error: EventEmitter<any> = new EventEmitter(); error: EventEmitter<any> = new EventEmitter();
/** Emitted when the node selection change */
@Output()
nodeSelected: EventEmitter<NodeEntry[]> = new EventEmitter<NodeEntry[]>();
@ViewChild('dataTable', { static: true }) @ViewChild('dataTable', { static: true })
dataTable: DataTableComponent; dataTable: DataTableComponent;
@ -761,6 +765,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
}, },
bubbles: true bubbles: true
}); });
this.nodeSelected.emit(this.selection);
this.elementRef.nativeElement.dispatchEvent(domEvent); this.elementRef.nativeElement.dispatchEvent(domEvent);
} }
@ -773,6 +778,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
}, },
bubbles: true bubbles: true
}); });
this.nodeSelected.emit(this.selection);
this.elementRef.nativeElement.dispatchEvent(domEvent); this.elementRef.nativeElement.dispatchEvent(domEvent);
} }

View File

@ -108,17 +108,22 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent
} }
openSelectDialog() { openSelectDialog() {
const filesSaved: Node[] = []; const selectedMode = this.field.params.multiple ? 'multiple' : 'single';
this.contentNodeSelectorService this.contentNodeSelectorService
.openUploadFileDialog(this.field.form.contentHost) .openUploadFileDialog(this.field.form.contentHost, '-my-', selectedMode)
.subscribe((selections: Node[]) => { .subscribe((selections: Node[]) => {
selections.forEach(node => (node['isExternal'] = true)); selections.forEach(node => (node['isExternal'] = true));
filesSaved.push(selections[0]); const selectionWithoutDuplication = this.removeExistingSelection(selections);
this.fixIncompatibilityFromPreviousAndNewForm(filesSaved); this.fixIncompatibilityFromPreviousAndNewForm(selectionWithoutDuplication);
}); });
} }
removeExistingSelection(selections: Node[]) {
const existingNode: Node[] = [...this.field.value || []];
return selections.filter(opt => !existingNode.some( (node) => node.id === opt.id));
}
isContentSourceSelected(): boolean { isContentSourceSelected(): boolean {
return ( return (
this.field.params && this.field.params &&

View File

@ -32,7 +32,7 @@ export class ContentCloudNodeSelectorService {
private dialog: MatDialog) { private dialog: MatDialog) {
} }
openUploadFileDialog(contentHost: string): Observable<Node[]> { openUploadFileDialog(contentHost: string, currentFolderId?: string, selectionMode?: string): Observable<Node[]> {
const changedConfig = this.apiService.lastConfig; const changedConfig = this.apiService.lastConfig;
changedConfig.provider = 'ALL'; changedConfig.provider = 'ALL';
changedConfig.hostEcm = contentHost.replace('/alfresco', ''); changedConfig.hostEcm = contentHost.replace('/alfresco', '');
@ -44,8 +44,9 @@ export class ContentCloudNodeSelectorService {
const data = <ContentNodeSelectorComponentData> { const data = <ContentNodeSelectorComponentData> {
title: 'Select a file', title: 'Select a file',
actionName: 'Choose', actionName: 'Choose',
currentFolderId: '-my-', currentFolderId,
select, select,
selectionMode,
isSelectionValid: (entry: Node) => entry.isFile, isSelectionValid: (entry: Node) => entry.isFile,
showFilesInResult: true showFilesInResult: true
}; };

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { NgModule } from '@angular/core'; import { NgModule, ModuleWithProviders } from '@angular/core';
import { TRANSLATION_PROVIDER, CoreModule, FormRenderingService } from '@alfresco/adf-core'; import { TRANSLATION_PROVIDER, CoreModule, FormRenderingService } from '@alfresco/adf-core';
import { AppListCloudModule } from './app/app-list-cloud.module'; import { AppListCloudModule } from './app/app-list-cloud.module';
import { TaskCloudModule } from './task/task-cloud.module'; import { TaskCloudModule } from './task/task-cloud.module';
@ -54,9 +54,7 @@ import { ProcessServicesCloudPipeModule } from './pipes/process-services-cloud-p
} }
}, },
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }
FormRenderingService,
{ provide: FormRenderingService, useClass: CloudFormRenderingService }
], ],
exports: [ exports: [
AppListCloudModule, AppListCloudModule,
@ -69,4 +67,30 @@ import { ProcessServicesCloudPipeModule } from './pipes/process-services-cloud-p
ProcessServicesCloudPipeModule ProcessServicesCloudPipeModule
] ]
}) })
export class ProcessServicesCloudModule { } export class ProcessServicesCloudModule {
static forRoot(): ModuleWithProviders<ProcessServicesCloudModule> {
return {
ngModule: ProcessServicesCloudModule,
providers: [
{
provide: TRANSLATION_PROVIDER,
multi: true,
useValue: {
name: 'adf-process-services-cloud',
source: 'assets/adf-process-services-cloud'
}
},
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
FormRenderingService,
{ provide: FormRenderingService, useClass: CloudFormRenderingService }
]
};
}
static forChild(): ModuleWithProviders<ProcessServicesCloudModule> {
return {
ngModule: ProcessServicesCloudModule
};
}
}

View File

@ -31,7 +31,6 @@ import { ProcessDefinitionCloud } from '../models/process-definition-cloud.model
import { Subject, Observable } from 'rxjs'; import { Subject, Observable } from 'rxjs';
import { TaskVariableCloud } from '../../../form/models/task-variable-cloud.model'; import { TaskVariableCloud } from '../../../form/models/task-variable-cloud.model';
import { ProcessNameCloudPipe } from '../../../pipes/process-name-cloud.pipe'; import { ProcessNameCloudPipe } from '../../../pipes/process-name-cloud.pipe';
@Component({ @Component({
selector: 'adf-cloud-start-process', selector: 'adf-cloud-start-process',
templateUrl: './start-process-cloud.component.html', templateUrl: './start-process-cloud.component.html',