mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
101127: Refactor VocabularyTreeView to add multiSelect & checkboxes
This commit is contained in:
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user