101127: Refactor VocabularyTreeView to add multiSelect & checkboxes

This commit is contained in:
Nona Luypaert
2023-04-17 15:35:27 +02:00
parent 3da2b3c0ef
commit 7a876c0276
7 changed files with 78 additions and 43 deletions

View File

@@ -225,7 +225,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple
const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' }); const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' });
modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions; modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions;
modalRef.componentInstance.preloadLevel = preloadLevel; modalRef.componentInstance.preloadLevel = preloadLevel;
modalRef.componentInstance.selectedItem = this.currentValue ? this.currentValue : ''; modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue.value] : [];
modalRef.result.then((result: VocabularyEntryDetail) => { modalRef.result.then((result: VocabularyEntryDetail) => {
if (result) { if (result) {
this.currentValue = result; this.currentValue = result;

View File

@@ -8,8 +8,9 @@
<div class="p-3"> <div class="p-3">
<ds-vocabulary-treeview [vocabularyOptions]="vocabularyOptions" <ds-vocabulary-treeview [vocabularyOptions]="vocabularyOptions"
[preloadLevel]="preloadLevel" [preloadLevel]="preloadLevel"
[selectedItem]="selectedItem" [selectedItems]="selectedItems"
[activeModal]="activeModal"> [activeModal]="activeModal"
[multiSelect]="multiSelect">
</ds-vocabulary-treeview> </ds-vocabulary-treeview>
</div> </div>
</div> </div>

View File

@@ -23,9 +23,14 @@ export class VocabularyTreeviewModalComponent {
@Input() preloadLevel = 2; @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 * Initialize instance variables

View File

@@ -21,7 +21,8 @@ export class TreeviewNode {
public pageInfo: PageInfo = new PageInfo(), public pageInfo: PageInfo = new PageInfo(),
public loadMoreParentItem: VocabularyEntryDetail | null = null, public loadMoreParentItem: VocabularyEntryDetail | null = null,
public isSearchNode = false, public isSearchNode = false,
public isInInitValueHierarchy = false) { public isInInitValueHierarchy = false,
public isSelected = false) {
} }
updatePageInfo(pageInfo: PageInfo) { updatePageInfo(pageInfo: PageInfo) {
@@ -38,7 +39,8 @@ export class TreeviewFlatNode {
public pageInfo: PageInfo = new PageInfo(), public pageInfo: PageInfo = new PageInfo(),
public loadMoreParentItem: VocabularyEntryDetail | null = null, public loadMoreParentItem: VocabularyEntryDetail | null = null,
public isSearchNode = false, public isSearchNode = false,
public isInInitValueHierarchy = false) { public isInInitValueHierarchy = false,
public isSelected = false) {
} }
} }

View File

@@ -26,12 +26,18 @@
<span class="fas fa-angle-right fa-2x invisible" aria-hidden="true"></span> <span class="fas fa-angle-right fa-2x invisible" aria-hidden="true"></span>
</button> </button>
<button class="btn btn-outline-link btn-sm text-left" <button class="btn btn-outline-link btn-sm text-left"
[class.text-success]="node.item?.value === selectedItem?.value" [class.text-success]="node.isSelected"
[disabled]="!node.item?.selectable" [disabled]="!node.item?.selectable"
[ngbTooltip]="node.item?.otherInformation?.note" [ngbTooltip]="node.item?.otherInformation?.note"
[openDelay]="500" [openDelay]="500"
container="body" container="body"
(click)="onSelect(node.item)">{{node.item.display}}</button> (click)="onSelect(node.item)">
<span *ngIf="multiSelect" class="form-check">
<input class="form-check-input" type="checkbox" id="leaf-node-checkbox" [checked]="node.isSelected">
<label class="form-check-label" for="leaf-node-checkbox">{{node.item.display}}</label>
</span>
<span *ngIf="!multiSelect">{{node.item.display}}</span>
</button>
</cdk-tree-node> </cdk-tree-node>
<!-- expandable node --> <!-- expandable node -->
@@ -44,12 +50,18 @@
</button> </button>
<button class="btn btn-outline-link btn-sm text-left" <button class="btn btn-outline-link btn-sm text-left"
[class.text-success]="node.item?.value === selectedItem?.value" [class.text-success]="node.isSelected"
[disabled]="!node.item?.selectable" [disabled]="!node.item?.selectable"
[ngbTooltip]="node.item?.otherInformation?.note" [ngbTooltip]="node.item?.otherInformation?.note"
[openDelay]="500" [openDelay]="500"
container="body" container="body"
(click)="onSelect(node.item)">{{node.item.display}}</button> (click)="onSelect(node.item)">
<span *ngIf="multiSelect" class="form-check">
<input class="form-check-input" type="checkbox" id="expandable-node-checkbox" [checked]="node.isSelected">
<label class="form-check-label" for="expandable-node-checkbox">{{node.item.display}}</label>
</span>
<span *ngIf="!multiSelect">{{node.item.display}}</span>
</button>
</cdk-tree-node> </cdk-tree-node>
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding> <cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding>

View File

@@ -40,15 +40,20 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
@Input() preloadLevel = 2; @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 * The active modal
*/ */
@Input() activeModal?: NgbActiveModal; @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 * Contain a descriptive message for this vocabulary retrieved from i18n files
*/ */
@@ -151,7 +156,8 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
node.pageInfo, node.pageInfo,
node.loadMoreParentItem, node.loadMoreParentItem,
node.isSearchNode, node.isSearchNode,
node.isInInitValueHierarchy node.isInInitValueHierarchy,
node.isSelected
); );
this.nodeMap.set(node.item.id, newNode); this.nodeMap.set(node.item.id, newNode);
@@ -214,7 +220,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
this.loading = this.vocabularyTreeviewService.isLoading(); 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 * @param item The VocabularyEntryDetail for which to load more nodes
*/ */
loadMore(item: VocabularyEntryDetail) { 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 * @param node The TreeviewFlatNode for which to load more nodes
*/ */
loadMoreRoot(node: TreeviewFlatNode) { 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 * @param node The TreeviewFlatNode for which to load children nodes
*/ */
loadChildren(node: TreeviewFlatNode) { loadChildren(node: TreeviewFlatNode) {
this.vocabularyTreeviewService.loadMore(node.item, true); this.vocabularyTreeviewService.loadMore(node.item, this.selectedItems, true);
} }
/** /**
* Method called on entry select * Method called on entry select
* Emit a new select Event
*/ */
onSelect(item: VocabularyEntryDetail) { onSelect(item: VocabularyEntryDetail) {
this.select.emit(item); this.selectedItems.push(item.id);
this.activeModal.close(item); this.activeModal.close(item);
} }
@@ -259,7 +264,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
this.storedNodeMap = this.nodeMap; this.storedNodeMap = this.nodeMap;
} }
this.nodeMap = new Map<string, TreeviewFlatNode>(); this.nodeMap = new Map<string, TreeviewFlatNode>();
this.vocabularyTreeviewService.searchByQuery(this.searchText); this.vocabularyTreeviewService.searchByQuery(this.searchText, this.selectedItems);
} }
} }

View File

@@ -101,21 +101,22 @@ export class VocabularyTreeviewService {
* *
* @param options The {@link VocabularyOptions} object * @param options The {@link VocabularyOptions} object
* @param pageInfo The {@link PageInfo} 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 * @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.loading.next(true);
this.vocabularyOptions = options; this.vocabularyOptions = options;
this.vocabularyName = options.name; this.vocabularyName = options.name;
this.pageInfo = pageInfo; this.pageInfo = pageInfo;
if (isNotEmpty(initValueId)) { if (isNotEmpty(initValueId)) {
this.getNodeHierarchyById(initValueId) this.getNodeHierarchyById(initValueId, selectedItems)
.subscribe((hierarchy: string[]) => { .subscribe((hierarchy: string[]) => {
this.initValueHierarchy = hierarchy; this.initValueHierarchy = hierarchy;
this.retrieveTopNodes(pageInfo, []); this.retrieveTopNodes(pageInfo, [], selectedItems);
}); });
} else { } else {
this.retrieveTopNodes(pageInfo, []); this.retrieveTopNodes(pageInfo, [], selectedItems);
} }
} }
@@ -129,19 +130,21 @@ export class VocabularyTreeviewService {
/** /**
* Expand the root node whose children are not loaded * Expand the root node whose children are not loaded
* @param node The root node * @param node The root node
* @param selectedItems The currently selected items
*/ */
loadMoreRoot(node: TreeviewFlatNode) { loadMoreRoot(node: TreeviewFlatNode, selectedItems: string[]) {
const nodes = this.dataChange.value; const nodes = this.dataChange.value;
nodes.pop(); nodes.pop();
this.retrieveTopNodes(node.pageInfo, nodes); this.retrieveTopNodes(node.pageInfo, nodes, selectedItems);
} }
/** /**
* Expand a node whose children are not loaded * Expand a node whose children are not loaded
* @param item * @param item
* @param selectedItems
* @param onlyFirstTime * @param onlyFirstTime
*/ */
loadMore(item: VocabularyEntryDetail, onlyFirstTime = false) { loadMore(item: VocabularyEntryDetail, selectedItems: string[], onlyFirstTime = false) {
if (!this.nodeMap.has(item.otherInformation.id)) { if (!this.nodeMap.has(item.otherInformation.id)) {
return; return;
} }
@@ -154,7 +157,7 @@ export class VocabularyTreeviewService {
return; 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); children.push(...newNodes);
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
@@ -183,7 +186,7 @@ export class VocabularyTreeviewService {
/** /**
* Perform a search operation by query * Perform a search operation by query
*/ */
searchByQuery(query: string) { searchByQuery(query: string, selectedItems: string[]) {
this.loading.next(true); this.loading.next(true);
if (isEmpty(this.storedNodes)) { if (isEmpty(this.storedNodes)) {
this.storedNodes = this.dataChange.value; this.storedNodes = this.dataChange.value;
@@ -200,7 +203,7 @@ export class VocabularyTreeviewService {
getFirstSucceededRemoteDataPayload() getFirstSucceededRemoteDataPayload()
) )
), ),
mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry)), mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems)),
scan((acc: TreeviewNode[], value: TreeviewNode) => { scan((acc: TreeviewNode[], value: TreeviewNode) => {
if (isEmpty(value) || findIndex(acc, (node) => node.item.otherInformation.id === value.item.otherInformation.id) !== -1) { if (isEmpty(value) || findIndex(acc, (node) => node.item.otherInformation.id === value.item.otherInformation.id) !== -1) {
return acc; return acc;
@@ -231,11 +234,12 @@ export class VocabularyTreeviewService {
* Generate a {@link TreeviewNode} object from vocabulary entry * Generate a {@link TreeviewNode} object from vocabulary entry
* *
* @param entry The vocabulary entry detail * @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 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 * @param toStore A Boolean representing if the node created is to store or not
* @return TreeviewNode * @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; const entryId = entry.otherInformation.id;
if (this.nodeMap.has(entryId)) { if (this.nodeMap.has(entryId)) {
return this.nodeMap.get(entryId)!; return this.nodeMap.get(entryId)!;
@@ -243,13 +247,15 @@ export class VocabularyTreeviewService {
const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren === 'true'; const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren === 'true';
const pageInfo: PageInfo = this.pageInfo; const pageInfo: PageInfo = this.pageInfo;
const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId); const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId);
const isSelected: boolean = selectedItems.some(() => selectedItems.includes(entry.id));
const result = new TreeviewNode( const result = new TreeviewNode(
entry, entry,
hasChildren, hasChildren,
pageInfo, pageInfo,
null, null,
isSearchNode, isSearchNode,
isInInitValueHierarchy); isInInitValueHierarchy,
isSelected);
if (toStore) { if (toStore) {
this.nodeMap.set(entryId, result); this.nodeMap.set(entryId, result);
@@ -260,12 +266,13 @@ export class VocabularyTreeviewService {
/** /**
* Return the node Hierarchy by a given node's id * Return the node Hierarchy by a given node's id
* @param id The node id * @param id The node id
* @param selectedItems The currently selected items
* @return Observable<string[]> * @return Observable<string[]>
*/ */
private getNodeHierarchyById(id: string): Observable<string[]> { private getNodeHierarchyById(id: string, selectedItems: string[]): Observable<string[]> {
return this.getById(id).pipe( return this.getById(id).pipe(
mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, [], false)), mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems,[], false)),
map((node: TreeviewNode) => this.getNodeHierarchyIds(node)) map((node: TreeviewNode) => this.getNodeHierarchyIds(node, selectedItems))
); );
} }
@@ -306,13 +313,14 @@ export class VocabularyTreeviewService {
* Retrieve the top level vocabulary entries * Retrieve the top level vocabulary entries
* @param pageInfo The {@link PageInfo} object * @param pageInfo The {@link PageInfo} object
* @param nodes The top level nodes already loaded, if any * @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( this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe(
getFirstSucceededRemoteDataPayload() getFirstSucceededRemoteDataPayload()
).subscribe((list: PaginatedList<VocabularyEntryDetail>) => { ).subscribe((list: PaginatedList<VocabularyEntryDetail>) => {
this.vocabularyService.clearSearchTopRequests(); 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); nodes.push(...newNodes);
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { 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 * Build and return the tree node hierarchy by a given vocabulary entry
* *
* @param item The vocabulary entry * @param item The vocabulary entry
* @param selectedItems The currently selected items
* @param children The vocabulary entry * @param children The vocabulary entry
* @param toStore A Boolean representing if the node created is to store or not * @param toStore A Boolean representing if the node created is to store or not
* @return Observable<string[]> * @return Observable<string[]>
*/ */
private getNodeHierarchy(item: VocabularyEntryDetail, children?: TreeviewNode[], toStore = true): Observable<TreeviewNode> { private getNodeHierarchy(item: VocabularyEntryDetail, selectedItems: string[], children?: TreeviewNode[], toStore = true): Observable<TreeviewNode> {
if (isEmpty(item)) { if (isEmpty(item)) {
return observableOf(null); return observableOf(null);
} }
const node = this._generateNode(item, toStore, toStore); const node = this._generateNode(item, selectedItems, toStore, toStore);
if (isNotEmpty(children)) { if (isNotEmpty(children)) {
const newChildren = children const newChildren = children
@@ -357,7 +366,7 @@ export class VocabularyTreeviewService {
if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) { if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) {
return this.getParentNode(node.item.otherInformation.id).pipe( 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 { } else {
return observableOf(node); return observableOf(node);
@@ -368,15 +377,16 @@ export class VocabularyTreeviewService {
* Build and return the node Hierarchy ids by a given node * Build and return the node Hierarchy ids by a given node
* *
* @param node The given node * @param node The given node
* @param selectedItems The currently selected items
* @param hierarchyIds The ids already present in the Hierarchy's array * @param hierarchyIds The ids already present in the Hierarchy's array
* @return string[] * @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)) { if (!hierarchyIds.includes(node.item.otherInformation.id)) {
hierarchyIds.push(node.item.otherInformation.id); hierarchyIds.push(node.item.otherInformation.id);
} }
if (isNotEmpty(node.children)) { if (isNotEmpty(node.children)) {
return this.getNodeHierarchyIds(node.children[0], hierarchyIds); return this.getNodeHierarchyIds(node.children[0], selectedItems, hierarchyIds);
} else { } else {
return hierarchyIds; return hierarchyIds;
} }