From 7a876c0276c124e6cc8f0220fb8b7d98fb1b1fb6 Mon Sep 17 00:00:00 2001 From: Nona Luypaert Date: Mon, 17 Apr 2023 15:35:27 +0200 Subject: [PATCH] 101127: Refactor VocabularyTreeView to add multiSelect & checkboxes --- .../models/onebox/dynamic-onebox.component.ts | 2 +- .../vocabulary-treeview-modal.component.html | 5 +- .../vocabulary-treeview-modal.component.ts | 9 +++- .../vocabulary-treeview-node.model.ts | 6 ++- .../vocabulary-treeview.component.html | 20 +++++-- .../vocabulary-treeview.component.ts | 25 +++++---- .../vocabulary-treeview.service.ts | 54 +++++++++++-------- 7 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index 63eca80dfe..5e1b81a9cf 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -225,7 +225,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' }); modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions; modalRef.componentInstance.preloadLevel = preloadLevel; - modalRef.componentInstance.selectedItem = this.currentValue ? this.currentValue : ''; + modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue.value] : []; modalRef.result.then((result: VocabularyEntryDetail) => { if (result) { this.currentValue = result; diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html index 55f5ab5092..8df07a3879 100644 --- a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html @@ -8,8 +8,9 @@
+ [selectedItems]="selectedItems" + [activeModal]="activeModal" + [multiSelect]="multiSelect">
diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts index 48ff82f499..fef230d4d7 100644 --- a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts @@ -23,9 +23,14 @@ export class VocabularyTreeviewModalComponent { @Input() preloadLevel = 2; /** - * The vocabulary entry already selected, if any + * The vocabulary entries already selected, if any */ - @Input() selectedItem: any = null; + @Input() selectedItems: string[] = []; + + /** + * Whether to allow selecting multiple values with checkboxes + */ + @Input() multiSelect = false; /** * Initialize instance variables diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts index c167328cab..4ac1b08425 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts @@ -21,7 +21,8 @@ export class TreeviewNode { public pageInfo: PageInfo = new PageInfo(), public loadMoreParentItem: VocabularyEntryDetail | null = null, public isSearchNode = false, - public isInInitValueHierarchy = false) { + public isInInitValueHierarchy = false, + public isSelected = false) { } updatePageInfo(pageInfo: PageInfo) { @@ -38,7 +39,8 @@ export class TreeviewFlatNode { public pageInfo: PageInfo = new PageInfo(), public loadMoreParentItem: VocabularyEntryDetail | null = null, public isSearchNode = false, - public isInInitValueHierarchy = false) { + public isInInitValueHierarchy = false, + public isSelected = false) { } } diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html index 18a61b73d7..2de1d375cb 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html @@ -26,12 +26,18 @@ + (click)="onSelect(node.item)"> + + + + + {{node.item.display}} + @@ -44,12 +50,18 @@ + (click)="onSelect(node.item)"> + + + + + {{node.item.display}} + diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts index 60dfa0ced8..b9c30e6343 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts @@ -40,15 +40,20 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { @Input() preloadLevel = 2; /** - * The vocabulary entry already selected, if any + * The vocabulary entries already selected, if any */ - @Input() selectedItem: any = null; + @Input() selectedItems: string[] = []; /** * The active modal */ @Input() activeModal?: NgbActiveModal; + /** + * Whether to allow selecting multiple values with checkboxes + */ + @Input() multiSelect = false; + /** * Contain a descriptive message for this vocabulary retrieved from i18n files */ @@ -151,7 +156,8 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { node.pageInfo, node.loadMoreParentItem, node.isSearchNode, - node.isInInitValueHierarchy + node.isInInitValueHierarchy, + node.isSelected ); this.nodeMap.set(node.item.id, newNode); @@ -214,7 +220,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { this.loading = this.vocabularyTreeviewService.isLoading(); - this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null); + this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.selectedItems, null); } /** @@ -222,7 +228,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * @param item The VocabularyEntryDetail for which to load more nodes */ loadMore(item: VocabularyEntryDetail) { - this.vocabularyTreeviewService.loadMore(item); + this.vocabularyTreeviewService.loadMore(item, this.selectedItems); } /** @@ -230,7 +236,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * @param node The TreeviewFlatNode for which to load more nodes */ loadMoreRoot(node: TreeviewFlatNode) { - this.vocabularyTreeviewService.loadMoreRoot(node); + this.vocabularyTreeviewService.loadMoreRoot(node, this.selectedItems); } /** @@ -238,15 +244,14 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * @param node The TreeviewFlatNode for which to load children nodes */ loadChildren(node: TreeviewFlatNode) { - this.vocabularyTreeviewService.loadMore(node.item, true); + this.vocabularyTreeviewService.loadMore(node.item, this.selectedItems, true); } /** * Method called on entry select - * Emit a new select Event */ onSelect(item: VocabularyEntryDetail) { - this.select.emit(item); + this.selectedItems.push(item.id); this.activeModal.close(item); } @@ -259,7 +264,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { this.storedNodeMap = this.nodeMap; } this.nodeMap = new Map(); - this.vocabularyTreeviewService.searchByQuery(this.searchText); + this.vocabularyTreeviewService.searchByQuery(this.searchText, this.selectedItems); } } diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts index 8804716927..d400615f1e 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts @@ -101,21 +101,22 @@ export class VocabularyTreeviewService { * * @param options The {@link VocabularyOptions} object * @param pageInfo The {@link PageInfo} object + * @param selectedItems The currently selected items * @param initValueId The entry id of the node to mark as selected, if any */ - initialize(options: VocabularyOptions, pageInfo: PageInfo, initValueId?: string): void { + initialize(options: VocabularyOptions, pageInfo: PageInfo, selectedItems: string[], initValueId?: string): void { this.loading.next(true); this.vocabularyOptions = options; this.vocabularyName = options.name; this.pageInfo = pageInfo; if (isNotEmpty(initValueId)) { - this.getNodeHierarchyById(initValueId) + this.getNodeHierarchyById(initValueId, selectedItems) .subscribe((hierarchy: string[]) => { this.initValueHierarchy = hierarchy; - this.retrieveTopNodes(pageInfo, []); + this.retrieveTopNodes(pageInfo, [], selectedItems); }); } else { - this.retrieveTopNodes(pageInfo, []); + this.retrieveTopNodes(pageInfo, [], selectedItems); } } @@ -129,19 +130,21 @@ export class VocabularyTreeviewService { /** * Expand the root node whose children are not loaded * @param node The root node + * @param selectedItems The currently selected items */ - loadMoreRoot(node: TreeviewFlatNode) { + loadMoreRoot(node: TreeviewFlatNode, selectedItems: string[]) { const nodes = this.dataChange.value; nodes.pop(); - this.retrieveTopNodes(node.pageInfo, nodes); + this.retrieveTopNodes(node.pageInfo, nodes, selectedItems); } /** * Expand a node whose children are not loaded * @param item + * @param selectedItems * @param onlyFirstTime */ - loadMore(item: VocabularyEntryDetail, onlyFirstTime = false) { + loadMore(item: VocabularyEntryDetail, selectedItems: string[], onlyFirstTime = false) { if (!this.nodeMap.has(item.otherInformation.id)) { return; } @@ -154,7 +157,7 @@ export class VocabularyTreeviewService { return; } - const newNodes: TreeviewNode[] = list.page.map((entry) => this._generateNode(entry)); + const newNodes: TreeviewNode[] = list.page.map((entry) => this._generateNode(entry, selectedItems)); children.push(...newNodes); if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { @@ -183,7 +186,7 @@ export class VocabularyTreeviewService { /** * Perform a search operation by query */ - searchByQuery(query: string) { + searchByQuery(query: string, selectedItems: string[]) { this.loading.next(true); if (isEmpty(this.storedNodes)) { this.storedNodes = this.dataChange.value; @@ -200,7 +203,7 @@ export class VocabularyTreeviewService { getFirstSucceededRemoteDataPayload() ) ), - mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry)), + mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems)), scan((acc: TreeviewNode[], value: TreeviewNode) => { if (isEmpty(value) || findIndex(acc, (node) => node.item.otherInformation.id === value.item.otherInformation.id) !== -1) { return acc; @@ -231,11 +234,12 @@ export class VocabularyTreeviewService { * Generate a {@link TreeviewNode} object from vocabulary entry * * @param entry The vocabulary entry detail + * @param selectedItems An array containing the currently selected items * @param isSearchNode A Boolean representing if given entry is the result of a search * @param toStore A Boolean representing if the node created is to store or not * @return TreeviewNode */ - private _generateNode(entry: VocabularyEntryDetail, isSearchNode = false, toStore = true): TreeviewNode { + private _generateNode(entry: VocabularyEntryDetail, selectedItems: string[], isSearchNode = false, toStore = true): TreeviewNode { const entryId = entry.otherInformation.id; if (this.nodeMap.has(entryId)) { return this.nodeMap.get(entryId)!; @@ -243,13 +247,15 @@ export class VocabularyTreeviewService { const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren === 'true'; const pageInfo: PageInfo = this.pageInfo; const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId); + const isSelected: boolean = selectedItems.some(() => selectedItems.includes(entry.id)); const result = new TreeviewNode( entry, hasChildren, pageInfo, null, isSearchNode, - isInInitValueHierarchy); + isInInitValueHierarchy, + isSelected); if (toStore) { this.nodeMap.set(entryId, result); @@ -260,12 +266,13 @@ export class VocabularyTreeviewService { /** * Return the node Hierarchy by a given node's id * @param id The node id + * @param selectedItems The currently selected items * @return Observable */ - private getNodeHierarchyById(id: string): Observable { + private getNodeHierarchyById(id: string, selectedItems: string[]): Observable { return this.getById(id).pipe( - mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, [], false)), - map((node: TreeviewNode) => this.getNodeHierarchyIds(node)) + mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems,[], false)), + map((node: TreeviewNode) => this.getNodeHierarchyIds(node, selectedItems)) ); } @@ -306,13 +313,14 @@ export class VocabularyTreeviewService { * Retrieve the top level vocabulary entries * @param pageInfo The {@link PageInfo} object * @param nodes The top level nodes already loaded, if any + * @param selectedItems The currently selected items */ - private retrieveTopNodes(pageInfo: PageInfo, nodes: TreeviewNode[]): void { + private retrieveTopNodes(pageInfo: PageInfo, nodes: TreeviewNode[], selectedItems: string[]): void { this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe( getFirstSucceededRemoteDataPayload() ).subscribe((list: PaginatedList) => { this.vocabularyService.clearSearchTopRequests(); - const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry)); + const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry, selectedItems)); nodes.push(...newNodes); if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { @@ -334,15 +342,16 @@ export class VocabularyTreeviewService { * Build and return the tree node hierarchy by a given vocabulary entry * * @param item The vocabulary entry + * @param selectedItems The currently selected items * @param children The vocabulary entry * @param toStore A Boolean representing if the node created is to store or not * @return Observable */ - private getNodeHierarchy(item: VocabularyEntryDetail, children?: TreeviewNode[], toStore = true): Observable { + private getNodeHierarchy(item: VocabularyEntryDetail, selectedItems: string[], children?: TreeviewNode[], toStore = true): Observable { if (isEmpty(item)) { return observableOf(null); } - const node = this._generateNode(item, toStore, toStore); + const node = this._generateNode(item, selectedItems, toStore, toStore); if (isNotEmpty(children)) { const newChildren = children @@ -357,7 +366,7 @@ export class VocabularyTreeviewService { if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) { return this.getParentNode(node.item.otherInformation.id).pipe( - mergeMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, [node], toStore)) + mergeMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, selectedItems, [node], toStore)) ); } else { return observableOf(node); @@ -368,15 +377,16 @@ export class VocabularyTreeviewService { * Build and return the node Hierarchy ids by a given node * * @param node The given node + * @param selectedItems The currently selected items * @param hierarchyIds The ids already present in the Hierarchy's array * @return string[] */ - private getNodeHierarchyIds(node: TreeviewNode, hierarchyIds: string[] = []): string[] { + private getNodeHierarchyIds(node: TreeviewNode, selectedItems: string[], hierarchyIds: string[] = []): string[] { if (!hierarchyIds.includes(node.item.otherInformation.id)) { hierarchyIds.push(node.item.otherInformation.id); } if (isNotEmpty(node.children)) { - return this.getNodeHierarchyIds(node.children[0], hierarchyIds); + return this.getNodeHierarchyIds(node.children[0], selectedItems, hierarchyIds); } else { return hierarchyIds; }