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' });
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;

View File

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

View File

@@ -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

View File

@@ -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) {
}
}

View File

@@ -26,12 +26,18 @@
<span class="fas fa-angle-right fa-2x invisible" aria-hidden="true"></span>
</button>
<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"
[ngbTooltip]="node.item?.otherInformation?.note"
[openDelay]="500"
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>
<!-- expandable node -->
@@ -44,12 +50,18 @@
</button>
<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"
[ngbTooltip]="node.item?.otherInformation?.note"
[openDelay]="500"
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 *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding>

View File

@@ -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<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 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<string[]>
*/
private getNodeHierarchyById(id: string): Observable<string[]> {
private getNodeHierarchyById(id: string, selectedItems: string[]): Observable<string[]> {
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<VocabularyEntryDetail>) => {
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<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)) {
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;
}