mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -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,
|
||||
|
@@ -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">
|
||||
<input matInput
|
||||
id="searchInput"
|
||||
@@ -64,7 +64,7 @@
|
||||
[rowFilter]="_rowFilter"
|
||||
[imageResolver]="imageResolver"
|
||||
[currentFolderId]="folderIdToShow"
|
||||
selectionMode="single"
|
||||
[selectionMode]="selectionMode"
|
||||
[contextMenuActions]="false"
|
||||
[contentActions]="false"
|
||||
[allowDropFiles]="false"
|
||||
@@ -72,6 +72,7 @@
|
||||
[where]="where"
|
||||
(folderChange)="onFolderChange()"
|
||||
(ready)="onFolderLoaded()"
|
||||
(nodeSelected)="onCurrentSelection($event)"
|
||||
data-automation-id="content-node-selector-document-list">
|
||||
|
||||
<adf-custom-empty-content-template>
|
||||
|
@@ -133,14 +133,14 @@ describe('ContentNodeSelectorComponent', () => {
|
||||
});
|
||||
|
||||
it('should trigger the select event when selection has been made', (done) => {
|
||||
const expectedNode = <Node> {};
|
||||
const expectedNode = <Node> { 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 = <Node> { 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 = <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 = <Node> {};
|
||||
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 = <Node> {};
|
||||
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 = <Node> {};
|
||||
const entry: Node = <Node> { id: 'fakeid'};
|
||||
const nodePage: NodePaging = <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: <Node> {} } } });
|
||||
fixture.detectChanges();
|
||||
component.onCurrentSelection([{ entry: <Node> {} }]);
|
||||
|
||||
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: <Node> {} } } });
|
||||
fixture.detectChanges();
|
||||
component.onCurrentSelection([{ entry: <Node> {} }]);
|
||||
|
||||
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: <Node> {} } } });
|
||||
fixture.detectChanges();
|
||||
component.onCurrentSelection([{ entry: <Node> {} }]);
|
||||
|
||||
component.select.subscribe((nodes) => {
|
||||
expect(nodes).toBeDefined();
|
||||
@@ -1149,6 +1127,7 @@ describe('ContentNodeSelectorComponent', () => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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) {
|
||||
|
@@ -27,6 +27,7 @@ export interface ContentNodeSelectorComponentData {
|
||||
rowFilter?: any;
|
||||
where?: string;
|
||||
imageResolver?: any;
|
||||
selectionMode: string;
|
||||
isSelectionValid?: (entry: Node) => boolean;
|
||||
breadcrumbTransform?: (node) => any;
|
||||
excludeSiteContent?: string[];
|
||||
|
@@ -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 @@
|
||||
</button>
|
||||
|
||||
<button mat-button
|
||||
[disabled]="!chosenNode"
|
||||
[disabled]="!hasNodeSelected()"
|
||||
class="adf-choose-action"
|
||||
(click)="onClick()"
|
||||
data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }}
|
||||
|
@@ -49,6 +49,10 @@ export class ContentNodeSelectorComponent {
|
||||
this.chosenNode = nodeList;
|
||||
}
|
||||
|
||||
hasNodeSelected(): boolean {
|
||||
return this.chosenNode?.length > 0;
|
||||
}
|
||||
|
||||
onSiteChange(siteTitle: string) {
|
||||
this.updateTitle(siteTitle);
|
||||
}
|
||||
|
@@ -304,6 +304,10 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/** Emitted when the node selection change */
|
||||
@Output()
|
||||
nodeSelected: EventEmitter<NodeEntry[]> = new EventEmitter<NodeEntry[]>();
|
||||
|
||||
@ViewChild('dataTable', { static: true })
|
||||
dataTable: DataTableComponent;
|
||||
|
||||
@@ -761,6 +765,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
},
|
||||
bubbles: true
|
||||
});
|
||||
this.nodeSelected.emit(this.selection);
|
||||
this.elementRef.nativeElement.dispatchEvent(domEvent);
|
||||
}
|
||||
|
||||
@@ -773,6 +778,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
},
|
||||
bubbles: true
|
||||
});
|
||||
this.nodeSelected.emit(this.selection);
|
||||
this.elementRef.nativeElement.dispatchEvent(domEvent);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user