mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
[CST-3088] Added component to show hierarchical vocabulary as a tree
This commit is contained in:
@@ -62,6 +62,7 @@
|
|||||||
"@angular/compiler": "~8.2.14",
|
"@angular/compiler": "~8.2.14",
|
||||||
"@angular/core": "~8.2.14",
|
"@angular/core": "~8.2.14",
|
||||||
"@angular/forms": "~8.2.14",
|
"@angular/forms": "~8.2.14",
|
||||||
|
"@angular/material": "8.2.3",
|
||||||
"@angular/platform-browser": "~8.2.14",
|
"@angular/platform-browser": "~8.2.14",
|
||||||
"@angular/platform-browser-dynamic": "~8.2.14",
|
"@angular/platform-browser-dynamic": "~8.2.14",
|
||||||
"@angular/platform-server": "~8.2.14",
|
"@angular/platform-server": "~8.2.14",
|
||||||
|
@@ -4,7 +4,6 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
import { CommunityListPageComponent } from './community-list-page.component';
|
import { CommunityListPageComponent } from './community-list-page.component';
|
||||||
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
|
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
|
||||||
import { CommunityListComponent } from './community-list/community-list.component';
|
import { CommunityListComponent } from './community-list/community-list.component';
|
||||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page which houses a title and the community list, as described in community-list.component
|
* The page which houses a title and the community list, as described in community-list.component
|
||||||
@@ -13,8 +12,7 @@ import { CdkTreeModule } from '@angular/cdk/tree';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CommunityListPageRoutingModule,
|
CommunityListPageRoutingModule
|
||||||
CdkTreeModule,
|
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CommunityListPageComponent,
|
CommunityListPageComponent,
|
||||||
|
@@ -2,25 +2,27 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||||
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
|
|
||||||
import { NouisliderModule } from 'ng2-nouislider';
|
import { NouisliderModule } from 'ng2-nouislider';
|
||||||
|
|
||||||
import { NgbDatepickerModule, NgbModule, NgbTimepickerModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbDatepickerModule, NgbModule, NgbTimepickerModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core';
|
import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { NgxPaginationModule } from 'ngx-pagination';
|
import { NgxPaginationModule } from 'ngx-pagination';
|
||||||
|
import { FileUploadModule } from 'ng2-file-upload';
|
||||||
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
|
import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||||
|
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||||
|
import { TextMaskModule } from 'angular2-text-mask';
|
||||||
|
import { MomentModule } from 'ngx-moment';
|
||||||
|
import { TooltipModule } from 'ngx-bootstrap';
|
||||||
|
|
||||||
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
|
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
|
||||||
import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
|
import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
|
||||||
|
|
||||||
import { FileUploadModule } from 'ng2-file-upload';
|
|
||||||
|
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
|
||||||
|
|
||||||
import { EnumKeysPipe } from './utils/enum-keys-pipe';
|
import { EnumKeysPipe } from './utils/enum-keys-pipe';
|
||||||
import { FileSizePipe } from './utils/file-size-pipe';
|
import { FileSizePipe } from './utils/file-size-pipe';
|
||||||
import { SafeUrlPipe } from './utils/safe-url-pipe';
|
import { SafeUrlPipe } from './utils/safe-url-pipe';
|
||||||
import { ConsolePipe } from './utils/console.pipe';
|
import { ConsolePipe } from './utils/console.pipe';
|
||||||
|
|
||||||
import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component';
|
import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component';
|
||||||
import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component';
|
import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component';
|
||||||
import { SearchResultListElementComponent } from './object-list/search-result-list-element/search-result-list-element.component';
|
import { SearchResultListElementComponent } from './object-list/search-result-list-element/search-result-list-element.component';
|
||||||
@@ -53,9 +55,6 @@ import {
|
|||||||
dsDynamicFormControlMapFn
|
dsDynamicFormControlMapFn
|
||||||
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
||||||
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
|
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
|
||||||
import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
|
||||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
|
||||||
import { TextMaskModule } from 'angular2-text-mask';
|
|
||||||
import { DragClickDirective } from './utils/drag-click.directive';
|
import { DragClickDirective } from './utils/drag-click.directive';
|
||||||
import { TruncatePipe } from './utils/truncate.pipe';
|
import { TruncatePipe } from './utils/truncate.pipe';
|
||||||
import { TruncatableComponent } from './truncatable/truncatable.component';
|
import { TruncatableComponent } from './truncatable/truncatable.component';
|
||||||
@@ -95,13 +94,11 @@ import { EmphasizePipe } from './utils/emphasize.pipe';
|
|||||||
import { InputSuggestionsComponent } from './input-suggestions/input-suggestions.component';
|
import { InputSuggestionsComponent } from './input-suggestions/input-suggestions.component';
|
||||||
import { CapitalizePipe } from './utils/capitalize.pipe';
|
import { CapitalizePipe } from './utils/capitalize.pipe';
|
||||||
import { ObjectKeysPipe } from './utils/object-keys-pipe';
|
import { ObjectKeysPipe } from './utils/object-keys-pipe';
|
||||||
import { MomentModule } from 'ngx-moment';
|
|
||||||
import { AuthorityConfidenceStateDirective } from './authority-confidence/authority-confidence-state.directive';
|
import { AuthorityConfidenceStateDirective } from './authority-confidence/authority-confidence-state.directive';
|
||||||
import { MenuModule } from './menu/menu.module';
|
import { MenuModule } from './menu/menu.module';
|
||||||
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||||
import { PlainTextMetadataListElementComponent } from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
|
import { PlainTextMetadataListElementComponent } from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
|
||||||
import { ItemMetadataListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component';
|
import { ItemMetadataListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component';
|
||||||
import { TooltipModule } from 'ngx-bootstrap';
|
|
||||||
import { MetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/metadata-representation-list-element.component';
|
import { MetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/metadata-representation-list-element.component';
|
||||||
import { ComColFormComponent } from './comcol-forms/comcol-form/comcol-form.component';
|
import { ComColFormComponent } from './comcol-forms/comcol-form/comcol-form.component';
|
||||||
import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/create-comcol-page.component';
|
import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||||
@@ -177,7 +174,6 @@ import { SelectableListItemControlComponent } from './object-collection/shared/s
|
|||||||
import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component';
|
import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component';
|
||||||
import { ExternalSourceEntryImportModalComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
|
import { ExternalSourceEntryImportModalComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
|
||||||
import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
|
import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
|
||||||
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||||
import { ItemVersionsComponent } from './item/item-versions/item-versions.component';
|
import { ItemVersionsComponent } from './item/item-versions/item-versions.component';
|
||||||
import { SortablejsModule } from 'ngx-sortablejs';
|
import { SortablejsModule } from 'ngx-sortablejs';
|
||||||
@@ -202,6 +198,7 @@ import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/reso
|
|||||||
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
|
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
|
||||||
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
|
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
|
||||||
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
|
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
|
||||||
|
import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -223,7 +220,8 @@ const MODULES = [
|
|||||||
MomentModule,
|
MomentModule,
|
||||||
TextMaskModule,
|
TextMaskModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
DragDropModule
|
DragDropModule,
|
||||||
|
CdkTreeModule
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROOT_MODULES = [
|
const ROOT_MODULES = [
|
||||||
@@ -386,7 +384,8 @@ const COMPONENTS = [
|
|||||||
ResourcePolicyFormComponent,
|
ResourcePolicyFormComponent,
|
||||||
EpersonGroupListComponent,
|
EpersonGroupListComponent,
|
||||||
EpersonSearchBoxComponent,
|
EpersonSearchBoxComponent,
|
||||||
GroupSearchBoxComponent
|
GroupSearchBoxComponent,
|
||||||
|
VocabularyTreeviewComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -459,7 +458,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
ClaimedTaskActionsApproveComponent,
|
ClaimedTaskActionsApproveComponent,
|
||||||
ClaimedTaskActionsRejectComponent,
|
ClaimedTaskActionsRejectComponent,
|
||||||
ClaimedTaskActionsReturnToPoolComponent,
|
ClaimedTaskActionsReturnToPoolComponent,
|
||||||
ClaimedTaskActionsEditMetadataComponent
|
ClaimedTaskActionsEditMetadataComponent,
|
||||||
|
VocabularyTreeviewComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
export const LOAD_MORE = 'LOAD_MORE';
|
||||||
|
export const LOAD_MORE_ROOT = 'LOAD_MORE_ROOT';
|
||||||
|
export const LOAD_MORE_NODE: any = { id: LOAD_MORE };
|
||||||
|
export const LOAD_MORE_ROOT_NODE: any = { id: LOAD_MORE_ROOT };
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
/** Nested node */
|
||||||
|
export class TreeviewNode {
|
||||||
|
childrenChange = new BehaviorSubject<TreeviewNode[]>([]);
|
||||||
|
|
||||||
|
get children(): TreeviewNode[] {
|
||||||
|
return this.childrenChange.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(public item: VocabularyEntryDetail,
|
||||||
|
public hasChildren = false,
|
||||||
|
public pageInfo: PageInfo = new PageInfo(),
|
||||||
|
public loadMoreParentItem: VocabularyEntryDetail | null = null,
|
||||||
|
public isSearchNode = false,
|
||||||
|
public isInInitValueHierarchy = false) {
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePageInfo(pageInfo: PageInfo) {
|
||||||
|
this.pageInfo = pageInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flat node with expandable and level information */
|
||||||
|
export class TreeviewFlatNode {
|
||||||
|
constructor(public item: VocabularyEntryDetail,
|
||||||
|
public level = 1,
|
||||||
|
public expandable = false,
|
||||||
|
public pageInfo: PageInfo = new PageInfo(),
|
||||||
|
public loadMoreParentItem: VocabularyEntryDetail | null = null,
|
||||||
|
public isSearchNode = false,
|
||||||
|
public isInInitValueHierarchy = false) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
@@ -0,0 +1,77 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">{{'treeview.header' | translate}}</h4>
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="p-3">
|
||||||
|
<ds-alert *ngIf="description | async" [content]="description | async" [type]="'alert-info'"></ds-alert>
|
||||||
|
<div class="treeview-header row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" [(ngModel)]="searchText" (keyup.enter)="search()">
|
||||||
|
<div class="input-group-append" id="button-addon4">
|
||||||
|
<button class="btn btn-outline-primary" type="button" (click)="search()" [disabled]="!isSearchEnabled()">
|
||||||
|
{{'treeview.search.form.search' | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" (click)="reset()">
|
||||||
|
{{'treeview.search.form.reset' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="treeview-container">
|
||||||
|
<ds-loading *ngIf="searching | async" [showMessage]="false"></ds-loading>
|
||||||
|
<h4 *ngIf="!(searching | async) && dataSource.data.length === 0" class="text-center text-muted mt-4" >
|
||||||
|
<span>{{'treeview.search.no-result' | translate}}</span>
|
||||||
|
</h4>
|
||||||
|
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||||
|
<!-- Leaf node -->
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding class="d-flex">
|
||||||
|
<button type="button" class="btn btn-default" cdkTreeNodeToggle>
|
||||||
|
<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"
|
||||||
|
[disabled]="!node.item?.selectable"
|
||||||
|
[ngbTooltip]="node.item?.otherInformation?.note"
|
||||||
|
[openDelay]="500"
|
||||||
|
container="body"
|
||||||
|
(click)="onSelect(node.item)">{{node.item.display}}</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
|
||||||
|
<!-- expandable node -->
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChildren" cdkTreeNodePadding class="d-flex">
|
||||||
|
<button type="button" class="btn btn-default" cdkTreeNodeToggle
|
||||||
|
[attr.aria-label]="'toggle ' + node.name"
|
||||||
|
(click)="loadChildren(node)">
|
||||||
|
<span class="fas {{treeControl.isExpanded(node) ? 'fa-angle-down' : 'fa-angle-right'}} fa-2x"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-outline-link btn-sm text-left"
|
||||||
|
[class.text-success]="node.item?.value === selectedItem?.value"
|
||||||
|
[disabled]="!node.item?.selectable"
|
||||||
|
[ngbTooltip]="node.item?.otherInformation?.note"
|
||||||
|
[openDelay]="500"
|
||||||
|
container="body"
|
||||||
|
(click)="onSelect(node.item)">{{node.item.display}}</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" (click)="loadMore(node.loadMoreParentItem)">
|
||||||
|
{{'treeview.load-more' | translate}}...
|
||||||
|
</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMoreRoot">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" (click)="loadMoreRoot(node)">
|
||||||
|
{{'treeview.load-more' | translate}}...
|
||||||
|
</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
</cdk-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,7 @@
|
|||||||
|
::ng-deep .tooltip-inner {
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
cdk-tree .btn:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
@@ -0,0 +1,221 @@
|
|||||||
|
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { createTestComponent } from '../testing/utils.test';
|
||||||
|
import { VocabularyTreeviewComponent } from './vocabulary-treeview.component';
|
||||||
|
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
||||||
|
import { CoreState } from '../../core/core.reducers';
|
||||||
|
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { TreeviewFlatNode } from './vocabulary-treeview-node.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
|
||||||
|
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
describe('VocabularyTreeviewComponent test suite', () => {
|
||||||
|
|
||||||
|
let comp: VocabularyTreeviewComponent;
|
||||||
|
let compAsAny: any;
|
||||||
|
let fixture: ComponentFixture<VocabularyTreeviewComponent>;
|
||||||
|
|
||||||
|
const item = new VocabularyEntryDetail();
|
||||||
|
item.id = 'node1';
|
||||||
|
const item2 = new VocabularyEntryDetail();
|
||||||
|
item2.id = 'node2';
|
||||||
|
const emptyNodeMap = new Map<string, TreeviewFlatNode>();
|
||||||
|
const storedNodeMap = new Map<string, TreeviewFlatNode>().set('test', new TreeviewFlatNode(item2));
|
||||||
|
const nodeMap = new Map<string, TreeviewFlatNode>().set('test', new TreeviewFlatNode(item));
|
||||||
|
const vocabularyOptions = new VocabularyOptions('vocabularyTest', 'metadata.test', '123456');
|
||||||
|
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||||
|
const vocabularyTreeviewServiceStub = jasmine.createSpyObj('VocabularyTreeviewService', {
|
||||||
|
initialize: jasmine.createSpy('initialize'),
|
||||||
|
getData: jasmine.createSpy('getData'),
|
||||||
|
loadMore: jasmine.createSpy('loadMore'),
|
||||||
|
loadMoreRoot: jasmine.createSpy('loadMoreRoot'),
|
||||||
|
isSearching: jasmine.createSpy('isSearching'),
|
||||||
|
searchByQuery: jasmine.createSpy('searchByQuery'),
|
||||||
|
restoreNodes: jasmine.createSpy('restoreNodes'),
|
||||||
|
cleanTree: jasmine.createSpy('cleanTree'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const store: Store<CoreState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
pipe: observableOf(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CdkTreeModule,
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
VocabularyTreeviewComponent,
|
||||||
|
TestComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: VocabularyTreeviewService, useValue: vocabularyTreeviewServiceStub },
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
ChangeDetectorRef,
|
||||||
|
VocabularyTreeviewComponent
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
let testComp: TestComponent;
|
||||||
|
let testFixture: ComponentFixture<TestComponent>;
|
||||||
|
|
||||||
|
// synchronous beforeEach
|
||||||
|
beforeEach(() => {
|
||||||
|
const html = `
|
||||||
|
<ds-authority-treeview [vocabularyOptions]="vocabularyOptions" [preloadLevel]="preloadLevel"></ds-authority-treeview>`;
|
||||||
|
|
||||||
|
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||||
|
testComp = testFixture.componentInstance;
|
||||||
|
vocabularyTreeviewServiceStub.getData.and.returnValue(observableOf([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testFixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create VocabularyTreeviewComponent', inject([VocabularyTreeviewComponent], (app: VocabularyTreeviewComponent) => {
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(VocabularyTreeviewComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
compAsAny = comp;
|
||||||
|
vocabularyTreeviewServiceStub.getData.and.returnValue(observableOf([]));
|
||||||
|
vocabularyTreeviewServiceStub.isSearching.and.returnValue(observableOf(false));
|
||||||
|
comp.vocabularyOptions = vocabularyOptions;
|
||||||
|
comp.selectedItem = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
comp = null;
|
||||||
|
compAsAny = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should should init component properly', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(comp.dataSource.data).toEqual([]);
|
||||||
|
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should should init component properly with init value as FormFieldMetadataValueObject', () => {
|
||||||
|
comp.selectedItem = new FormFieldMetadataValueObject('test', null, 'auth001');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(comp.dataSource.data).toEqual([]);
|
||||||
|
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'auth001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should should init component properly with init value as AuthorityEntry', () => {
|
||||||
|
const authority = new VocabularyEntryDetail();
|
||||||
|
authority.id = 'auth001';
|
||||||
|
comp.selectedItem = authority;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(comp.dataSource.data).toEqual([]);
|
||||||
|
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'auth001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call loadMore function', () => {
|
||||||
|
comp.loadMore(item);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call loadMoreRoot function', () => {
|
||||||
|
const node = new TreeviewFlatNode(item);
|
||||||
|
comp.loadMoreRoot(node);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(vocabularyTreeviewServiceStub.loadMoreRoot).toHaveBeenCalledWith(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call loadChildren function', () => {
|
||||||
|
const node = new TreeviewFlatNode(item);
|
||||||
|
comp.loadChildren(node);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(node.item, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit select event', () => {
|
||||||
|
spyOn(comp, 'onSelect');
|
||||||
|
comp.onSelect(item);
|
||||||
|
|
||||||
|
expect(comp.onSelect).toHaveBeenCalledWith(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call searchByQuery function and set storedNodeMap properly', () => {
|
||||||
|
comp.searchText = 'test search';
|
||||||
|
comp.nodeMap.set('test', new TreeviewFlatNode(item));
|
||||||
|
comp.search();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search');
|
||||||
|
expect(comp.storedNodeMap).toEqual(nodeMap);
|
||||||
|
expect(comp.nodeMap).toEqual(emptyNodeMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call searchByQuery function and not set storedNodeMap', () => {
|
||||||
|
comp.searchText = 'test search';
|
||||||
|
comp.nodeMap.set('test', new TreeviewFlatNode(item));
|
||||||
|
comp.storedNodeMap.set('test', new TreeviewFlatNode(item2));
|
||||||
|
comp.search();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search');
|
||||||
|
expect(comp.storedNodeMap).toEqual(storedNodeMap);
|
||||||
|
expect(comp.nodeMap).toEqual(emptyNodeMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call restoreNodes function and restore nodeMap properly', () => {
|
||||||
|
comp.nodeMap.set('test', new TreeviewFlatNode(item));
|
||||||
|
comp.storedNodeMap.set('test', new TreeviewFlatNode(item2));
|
||||||
|
comp.reset();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(vocabularyTreeviewServiceStub.restoreNodes).toHaveBeenCalled();
|
||||||
|
expect(comp.storedNodeMap).toEqual(emptyNodeMap);
|
||||||
|
expect(comp.nodeMap).toEqual(storedNodeMap);
|
||||||
|
expect(comp.searchText).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear search string', () => {
|
||||||
|
comp.nodeMap.set('test', new TreeviewFlatNode(item));
|
||||||
|
comp.reset();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(comp.storedNodeMap).toEqual(emptyNodeMap);
|
||||||
|
expect(comp.nodeMap).toEqual(nodeMap);
|
||||||
|
expect(comp.searchText).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call cleanTree method on destroy', () => {
|
||||||
|
compAsAny.ngOnDestroy();
|
||||||
|
expect(vocabularyTreeviewServiceStub.cleanTree).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
|
||||||
|
vocabularyOptions: VocabularyOptions = new VocabularyOptions('vocabularyTest', 'metadata.test', '123456');
|
||||||
|
preloadLevel = 2;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,296 @@
|
|||||||
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||||
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
|
||||||
|
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { filter, find, startWith } from 'rxjs/operators';
|
||||||
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
import { select, Store } from '@ngrx/store';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
|
||||||
|
import { isAuthenticated } from '../../core/auth/selectors';
|
||||||
|
import { CoreState } from '../../core/core.reducers';
|
||||||
|
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
||||||
|
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
|
||||||
|
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that show a hierarchical vocabulary in a tree view
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-authority-treeview',
|
||||||
|
templateUrl: './vocabulary-treeview.component.html',
|
||||||
|
styleUrls: ['./vocabulary-treeview.component.scss']
|
||||||
|
})
|
||||||
|
export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link VocabularyOptions} object
|
||||||
|
*/
|
||||||
|
@Input() vocabularyOptions: VocabularyOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representing how many tree level load at initialization
|
||||||
|
*/
|
||||||
|
@Input() preloadLevel = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vocabulary entry already selected, if any
|
||||||
|
*/
|
||||||
|
@Input() selectedItem: any = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contain a descriptive message for this vocabulary retrieved from i18n files
|
||||||
|
*/
|
||||||
|
description: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map containing the current node showed by the tree
|
||||||
|
*/
|
||||||
|
nodeMap = new Map<string, TreeviewFlatNode>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map containing all the node already created for building the tree
|
||||||
|
*/
|
||||||
|
storedNodeMap = new Map<string, TreeviewFlatNode>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flat tree control object. Able to expand/collapse a subtree recursively for flattened tree.
|
||||||
|
*/
|
||||||
|
treeControl: FlatTreeControl<TreeviewFlatNode>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree flattener object. Able to convert a normal type of node to node with children and level information.
|
||||||
|
*/
|
||||||
|
treeFlattener: MatTreeFlattener<TreeviewNode, TreeviewFlatNode>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flat tree data source
|
||||||
|
*/
|
||||||
|
dataSource: MatTreeFlatDataSource<TreeviewNode, TreeviewFlatNode>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content of the search box used to search for a vocabulary entry
|
||||||
|
*/
|
||||||
|
searchText: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if a search operation is pending
|
||||||
|
*/
|
||||||
|
searching: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when a vocabulary entry is selected.
|
||||||
|
* Event's payload equals to {@link VocabularyEntryDetail} selected.
|
||||||
|
*/
|
||||||
|
@Output() select: EventEmitter<VocabularyEntryDetail> = new EventEmitter<VocabularyEntryDetail>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if user is authenticated
|
||||||
|
*/
|
||||||
|
private isAuthenticated: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {NgbActiveModal} activeModal
|
||||||
|
* @param {VocabularyTreeviewService} vocabularyTreeviewService
|
||||||
|
* @param {Store<CoreState>} store
|
||||||
|
* @param {TranslateService} translate
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
public activeModal: NgbActiveModal,
|
||||||
|
private vocabularyTreeviewService: VocabularyTreeviewService,
|
||||||
|
private store: Store<CoreState>,
|
||||||
|
private translate: TranslateService
|
||||||
|
) {
|
||||||
|
this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel,
|
||||||
|
this.isExpandable, this.getChildren);
|
||||||
|
|
||||||
|
this.treeControl = new FlatTreeControl<TreeviewFlatNode>(this.getLevel, this.isExpandable);
|
||||||
|
|
||||||
|
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get children for a given node
|
||||||
|
* @param node The node for which to retrieve the children
|
||||||
|
*/
|
||||||
|
getChildren = (node: TreeviewNode): Observable<TreeviewNode[]> => node.childrenChange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a {@link TreeviewNode} to {@link TreeviewFlatNode}
|
||||||
|
* @param node The node to transform
|
||||||
|
* @param level The node level information
|
||||||
|
*/
|
||||||
|
transformer = (node: TreeviewNode, level: number) => {
|
||||||
|
const existingNode = this.nodeMap.get(node.item.id);
|
||||||
|
|
||||||
|
if (existingNode && existingNode.item.id !== LOAD_MORE && existingNode.item.id !== LOAD_MORE_ROOT) {
|
||||||
|
return existingNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNode: TreeviewFlatNode = new TreeviewFlatNode(
|
||||||
|
node.item,
|
||||||
|
level,
|
||||||
|
node.hasChildren,
|
||||||
|
node.pageInfo,
|
||||||
|
node.loadMoreParentItem,
|
||||||
|
node.isSearchNode,
|
||||||
|
node.isInInitValueHierarchy
|
||||||
|
);
|
||||||
|
this.nodeMap.set(node.item.id, newNode);
|
||||||
|
|
||||||
|
if ((((level + 1) < this.preloadLevel) && newNode.expandable) || newNode.isSearchNode || newNode.isInInitValueHierarchy) {
|
||||||
|
if (!newNode.isSearchNode) {
|
||||||
|
this.loadChildren(newNode);
|
||||||
|
}
|
||||||
|
this.treeControl.expand(newNode);
|
||||||
|
}
|
||||||
|
return newNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tree level for a given node
|
||||||
|
* @param node The node for which to retrieve the level
|
||||||
|
*/
|
||||||
|
getLevel = (node: TreeviewFlatNode) => node.level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given node is expandable
|
||||||
|
* @param node The node for which to retrieve the information
|
||||||
|
*/
|
||||||
|
isExpandable = (node: TreeviewFlatNode) => node.expandable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given node has children
|
||||||
|
* @param _nodeData The node for which to retrieve the information
|
||||||
|
*/
|
||||||
|
hasChildren = (_: number, _nodeData: TreeviewFlatNode) => _nodeData.expandable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given node has more children to load
|
||||||
|
* @param _nodeData The node for which to retrieve the information
|
||||||
|
*/
|
||||||
|
isLoadMore = (_: number, _nodeData: TreeviewFlatNode) => _nodeData.item.id === LOAD_MORE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are more node to load at root level
|
||||||
|
* @param _nodeData The node for which to retrieve the information
|
||||||
|
*/
|
||||||
|
isLoadMoreRoot = (_: number, _nodeData: TreeviewFlatNode) => _nodeData.item.id === LOAD_MORE_ROOT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component, setting up the data to build the tree
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(
|
||||||
|
this.vocabularyTreeviewService.getData().subscribe((data) => {
|
||||||
|
this.dataSource.data = data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const descriptionLabel = 'tree.description.' + this.vocabularyOptions.name;
|
||||||
|
this.description = this.translate.get(descriptionLabel).pipe(
|
||||||
|
filter((msg) => msg !== descriptionLabel),
|
||||||
|
startWith('')
|
||||||
|
);
|
||||||
|
|
||||||
|
// set isAuthenticated
|
||||||
|
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||||
|
|
||||||
|
this.searching = this.vocabularyTreeviewService.isSearching();
|
||||||
|
|
||||||
|
this.isAuthenticated.pipe(
|
||||||
|
find((isAuth) => isAuth)
|
||||||
|
).subscribe(() => {
|
||||||
|
const valueId: string = (this.selectedItem) ? (this.selectedItem.authority || this.selectedItem.id) : null;
|
||||||
|
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), valueId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand a node whose children are not loaded
|
||||||
|
* @param item The VocabularyEntryDetail for which to load more nodes
|
||||||
|
*/
|
||||||
|
loadMore(item: VocabularyEntryDetail) {
|
||||||
|
this.vocabularyTreeviewService.loadMore(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand the root node whose children are not loaded
|
||||||
|
* @param node The TreeviewFlatNode for which to load more nodes
|
||||||
|
*/
|
||||||
|
loadMoreRoot(node: TreeviewFlatNode) {
|
||||||
|
this.vocabularyTreeviewService.loadMoreRoot(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load children nodes for a node
|
||||||
|
* @param node The TreeviewFlatNode for which to load children nodes
|
||||||
|
*/
|
||||||
|
loadChildren(node: TreeviewFlatNode) {
|
||||||
|
this.vocabularyTreeviewService.loadMore(node.item, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called on entry select
|
||||||
|
* Emit a new select Event
|
||||||
|
*/
|
||||||
|
onSelect(item: VocabularyEntryDetail) {
|
||||||
|
this.select.emit(item);
|
||||||
|
this.activeModal.close(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for a vocabulary entry by query
|
||||||
|
*/
|
||||||
|
search() {
|
||||||
|
if (isNotEmpty(this.searchText)) {
|
||||||
|
if (isEmpty(this.storedNodeMap)) {
|
||||||
|
this.storedNodeMap = this.nodeMap;
|
||||||
|
}
|
||||||
|
this.nodeMap = new Map<string, TreeviewFlatNode>();
|
||||||
|
this.vocabularyTreeviewService.searchByQuery(this.searchText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if search box contains any text
|
||||||
|
*/
|
||||||
|
isSearchEnabled() {
|
||||||
|
return isNotEmpty(this.searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset tree resulting from a previous search
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
if (isNotEmpty(this.storedNodeMap)) {
|
||||||
|
this.nodeMap = this.storedNodeMap;
|
||||||
|
this.storedNodeMap = new Map<string, TreeviewFlatNode>();
|
||||||
|
this.vocabularyTreeviewService.restoreNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchText = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.vocabularyTreeviewService.cleanTree();
|
||||||
|
this.subs
|
||||||
|
.filter((sub) => hasValue(sub))
|
||||||
|
.forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,329 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
|
||||||
|
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
||||||
|
import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
|
||||||
|
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { LOAD_MORE_NODE, LOAD_MORE_ROOT_NODE, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||||
|
|
||||||
|
describe('VocabularyTreeviewService test suite', () => {
|
||||||
|
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: VocabularyTreeviewService;
|
||||||
|
let serviceAsAny: any;
|
||||||
|
let loadMoreNode: TreeviewNode;
|
||||||
|
let loadMoreRootNode: TreeviewNode;
|
||||||
|
let loadMoreRootFlatNode: TreeviewFlatNode;
|
||||||
|
let item: VocabularyEntryDetail;
|
||||||
|
let itemNode: TreeviewNode;
|
||||||
|
let item2: VocabularyEntryDetail;
|
||||||
|
let itemNode2: TreeviewNode;
|
||||||
|
let item3: VocabularyEntryDetail;
|
||||||
|
let itemNode3: TreeviewNode;
|
||||||
|
let item5: VocabularyEntryDetail;
|
||||||
|
let itemNode5: TreeviewNode;
|
||||||
|
let child: VocabularyEntryDetail;
|
||||||
|
let childNode: TreeviewNode;
|
||||||
|
let child2: VocabularyEntryDetail;
|
||||||
|
let childNode2: TreeviewNode;
|
||||||
|
let child3: VocabularyEntryDetail;
|
||||||
|
let childNode3: TreeviewNode;
|
||||||
|
let searchItemNode: TreeviewNode;
|
||||||
|
let searchChildNode: TreeviewNode;
|
||||||
|
let searchChildNode3: TreeviewNode;
|
||||||
|
let initValueChildNode: TreeviewNode;
|
||||||
|
let initValueChildNode2: TreeviewNode;
|
||||||
|
|
||||||
|
let treeNodeList: TreeviewNode[];
|
||||||
|
let treeNodeListWithChildren: TreeviewNode[];
|
||||||
|
let treeNodeListWithLoadMore: TreeviewNode[];
|
||||||
|
let treeNodeListWithLoadMoreRoot: TreeviewNode[];
|
||||||
|
let nodeMap: Map<string, TreeviewNode>;
|
||||||
|
let nodeMapWithChildren: Map<string, TreeviewNode>;
|
||||||
|
let searchNodeMap: Map<string, TreeviewNode>;
|
||||||
|
let vocabularyOptions;
|
||||||
|
|
||||||
|
const vocabularyServiceStub = jasmine.createSpyObj('VocabularyService', {
|
||||||
|
getVocabularyEntriesByValue: jasmine.createSpy('getVocabularyEntriesByValue'),
|
||||||
|
getEntryDetailParent: jasmine.createSpy('getEntryDetailParent'),
|
||||||
|
findEntryDetailByValue: jasmine.createSpy('findEntryDetailByValue'),
|
||||||
|
searchTopEntries: jasmine.createSpy('searchTopEntries'),
|
||||||
|
getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren')
|
||||||
|
});
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
|
||||||
|
loadMoreNode = new TreeviewNode(LOAD_MORE_NODE, false, new PageInfo(), item);
|
||||||
|
loadMoreRootNode = new TreeviewNode(LOAD_MORE_ROOT_NODE, false, new PageInfo(), null);
|
||||||
|
loadMoreRootFlatNode = new TreeviewFlatNode(LOAD_MORE_ROOT_NODE, 1, false, new PageInfo(), null);
|
||||||
|
item = new VocabularyEntryDetail();
|
||||||
|
item.id = item.value = item.display = 'root1';
|
||||||
|
item.otherInformation = { children: 'root1-child1::root1-child2', id: 'root1' };
|
||||||
|
itemNode = new TreeviewNode(item, true);
|
||||||
|
searchItemNode = new TreeviewNode(item, true, new PageInfo(), null, true);
|
||||||
|
|
||||||
|
item2 = new VocabularyEntryDetail();
|
||||||
|
item2.id = item2.value = item2.display = 'root2';
|
||||||
|
item2.otherInformation = { id: 'root2' };
|
||||||
|
itemNode2 = new TreeviewNode(item2);
|
||||||
|
|
||||||
|
item3 = new VocabularyEntryDetail();
|
||||||
|
item3.id = item3.value = item3.display = 'root3';
|
||||||
|
item3.otherInformation = { id: 'root3' };
|
||||||
|
itemNode3 = new TreeviewNode(item3);
|
||||||
|
|
||||||
|
child = new VocabularyEntryDetail();
|
||||||
|
child.id = child.value = child.display = 'root1-child1';
|
||||||
|
child.otherInformation = { parent: 'root1', children: 'root1-child1-child1', id: 'root1-child1' };
|
||||||
|
childNode = new TreeviewNode(child);
|
||||||
|
searchChildNode = new TreeviewNode(child, true, new PageInfo(), item, true);
|
||||||
|
|
||||||
|
child3 = new VocabularyEntryDetail();
|
||||||
|
child3.id = child3.value = child3.display = 'root1-child1-child1';
|
||||||
|
child3.otherInformation = { parent: 'root1-child1', id: 'root1-child1-child1' };
|
||||||
|
childNode3 = new TreeviewNode(child3);
|
||||||
|
searchChildNode3 = new TreeviewNode(child3, false, new PageInfo(), child, true);
|
||||||
|
|
||||||
|
child2 = new VocabularyEntryDetail();
|
||||||
|
child2.id = child2.value = child2.display = 'root1-child2';
|
||||||
|
child2.otherInformation = { parent: 'root1', id: 'root1-child2' };
|
||||||
|
childNode2 = new TreeviewNode(child2, true);
|
||||||
|
initValueChildNode2 = new TreeviewNode(child2, false, new PageInfo(), item, false, true);
|
||||||
|
initValueChildNode = new TreeviewNode(child, true, new PageInfo(), item, false, true);
|
||||||
|
initValueChildNode.childrenChange.next([initValueChildNode2]);
|
||||||
|
|
||||||
|
item5 = new VocabularyEntryDetail();
|
||||||
|
item5.id = item5.value = item5.display = 'root4';
|
||||||
|
item5.otherInformation = { id: 'root4' };
|
||||||
|
itemNode5 = new TreeviewNode(item5);
|
||||||
|
|
||||||
|
treeNodeList = [
|
||||||
|
itemNode,
|
||||||
|
itemNode2,
|
||||||
|
itemNode3
|
||||||
|
];
|
||||||
|
treeNodeListWithChildren = [
|
||||||
|
itemNode,
|
||||||
|
itemNode2,
|
||||||
|
itemNode3,
|
||||||
|
childNode
|
||||||
|
];
|
||||||
|
treeNodeListWithLoadMoreRoot = treeNodeList;
|
||||||
|
treeNodeListWithLoadMore = treeNodeListWithChildren;
|
||||||
|
treeNodeListWithLoadMoreRoot.push(loadMoreRootNode);
|
||||||
|
treeNodeListWithLoadMore.push(loadMoreNode);
|
||||||
|
|
||||||
|
nodeMap = new Map<string, TreeviewNode>([
|
||||||
|
[item.id, itemNode],
|
||||||
|
[item2.id, itemNode2],
|
||||||
|
[item3.id, itemNode3]
|
||||||
|
]);
|
||||||
|
|
||||||
|
nodeMapWithChildren = new Map<string, TreeviewNode>([
|
||||||
|
[item.id, itemNode],
|
||||||
|
[item2.id, itemNode2],
|
||||||
|
[item3.id, itemNode3],
|
||||||
|
[child.id, childNode],
|
||||||
|
]);
|
||||||
|
|
||||||
|
searchNodeMap = new Map<string, TreeviewNode>([
|
||||||
|
[item.id, searchItemNode],
|
||||||
|
]);
|
||||||
|
vocabularyOptions = new VocabularyOptions('vocabularyTest', 'metadata.test', '123456');
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: VocabularyService, useValue: vocabularyServiceStub },
|
||||||
|
VocabularyTreeviewService,
|
||||||
|
TranslateService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = TestBed.get(VocabularyTreeviewService);
|
||||||
|
serviceAsAny = service;
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialize', () => {
|
||||||
|
it('should set vocabularyName and call retrieveTopNodes method', () => {
|
||||||
|
const pageInfo = Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: 1,
|
||||||
|
totalElements: 3,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
serviceAsAny.vocabularyService.searchTopEntries.and.returnValue(hot('-a', {
|
||||||
|
a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3]))
|
||||||
|
}));
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name);
|
||||||
|
expect(serviceAsAny.dataChange.value).toEqual([itemNode, itemNode2, itemNode3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set initValueHierarchy', () => {
|
||||||
|
const pageInfo = Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: 1,
|
||||||
|
totalElements: 3,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
serviceAsAny.vocabularyService.searchTopEntries.and.returnValue(hot('-c', {
|
||||||
|
a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3]))
|
||||||
|
}));
|
||||||
|
serviceAsAny.vocabularyService.findEntryDetailByValue.and.returnValue(
|
||||||
|
hot('-a', {
|
||||||
|
a: createSuccessfulRemoteDataObject(child2)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
serviceAsAny.vocabularyService.getEntryDetailParent.and.returnValue(
|
||||||
|
hot('-b', {
|
||||||
|
b: createSuccessfulRemoteDataObject(item)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo, 'root2'));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name);
|
||||||
|
expect(serviceAsAny.initValueHierarchy).toEqual(['root1', 'root1-child2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getData', () => {
|
||||||
|
it('should return dataChange', () => {
|
||||||
|
const result = service.getData();
|
||||||
|
|
||||||
|
expect(result).toEqual(serviceAsAny.dataChange);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadMoreRoot', () => {
|
||||||
|
it('should call retrieveTopNodes properly', () => {
|
||||||
|
spyOn(serviceAsAny, 'retrieveTopNodes');
|
||||||
|
service.initialize(vocabularyOptions, new PageInfo());
|
||||||
|
serviceAsAny.dataChange.next(treeNodeListWithLoadMoreRoot);
|
||||||
|
service.loadMoreRoot(loadMoreRootFlatNode);
|
||||||
|
|
||||||
|
expect(serviceAsAny.retrieveTopNodes).toHaveBeenCalledWith(loadMoreRootFlatNode.pageInfo, treeNodeList);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadMore', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
itemNode.childrenChange.next([childNode]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add children nodes properly', () => {
|
||||||
|
const pageInfo = Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: 1,
|
||||||
|
totalElements: 2,
|
||||||
|
totalPages: 2,
|
||||||
|
currentPage: 2
|
||||||
|
});
|
||||||
|
spyOn(serviceAsAny, 'getChildrenNodesByParent').and.returnValue(hot('a', {
|
||||||
|
a: new PaginatedList(pageInfo, [child2])
|
||||||
|
}));
|
||||||
|
|
||||||
|
serviceAsAny.dataChange.next(treeNodeListWithLoadMore);
|
||||||
|
serviceAsAny.nodeMap = nodeMapWithChildren;
|
||||||
|
treeNodeListWithChildren.push(new TreeviewNode(child2, false, new PageInfo(), item));
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.loadMore(item));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add loadMore node properly', () => {
|
||||||
|
const pageInfo = Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: 1,
|
||||||
|
totalElements: 2,
|
||||||
|
totalPages: 2,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
spyOn(serviceAsAny, 'getChildrenNodesByParent').and.returnValue(hot('a', {
|
||||||
|
a: new PaginatedList(pageInfo, [child2])
|
||||||
|
}));
|
||||||
|
|
||||||
|
serviceAsAny.dataChange.next(treeNodeListWithLoadMore);
|
||||||
|
serviceAsAny.nodeMap = nodeMapWithChildren;
|
||||||
|
treeNodeListWithChildren.push(childNode2);
|
||||||
|
treeNodeListWithChildren.push(loadMoreNode);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.loadMore(item));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchByQuery', () => {
|
||||||
|
it('should set tree data properly after a search', () => {
|
||||||
|
const pageInfo = Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: 1,
|
||||||
|
totalElements: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
serviceAsAny.vocabularyService.getVocabularyEntriesByValue.and.returnValue(hot('-a', {
|
||||||
|
a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [child3]))
|
||||||
|
}));
|
||||||
|
|
||||||
|
serviceAsAny.vocabularyService.getEntryDetailParent.and.returnValues(
|
||||||
|
hot('-a', {
|
||||||
|
a: createSuccessfulRemoteDataObject(child)
|
||||||
|
}),
|
||||||
|
hot('-b', {
|
||||||
|
b: createSuccessfulRemoteDataObject(item)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
vocabularyOptions.query = 'root1-child1-child1';
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.searchByQuery(vocabularyOptions));
|
||||||
|
scheduler.flush();
|
||||||
|
searchChildNode.childrenChange.next([searchChildNode3]);
|
||||||
|
searchItemNode.childrenChange.next([searchChildNode]);
|
||||||
|
expect(serviceAsAny.dataChange.value.length).toEqual(1);
|
||||||
|
expect(serviceAsAny.dataChange.value).toEqual([searchItemNode]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('restoreNodes', () => {
|
||||||
|
it('should restore nodes properly', () => {
|
||||||
|
serviceAsAny.storedNodes = treeNodeList;
|
||||||
|
serviceAsAny.storedNodeMap = nodeMap;
|
||||||
|
serviceAsAny.nodeMap = searchNodeMap;
|
||||||
|
|
||||||
|
service.restoreNodes();
|
||||||
|
|
||||||
|
expect(serviceAsAny.nodeMap).toEqual(nodeMap);
|
||||||
|
expect(serviceAsAny.dataChange.value).toEqual(treeNodeList);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,366 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { flatMap, map, merge, scan, take, tap } from 'rxjs/operators';
|
||||||
|
import { findIndex } from 'lodash';
|
||||||
|
|
||||||
|
import { LOAD_MORE_NODE, LOAD_MORE_ROOT_NODE, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
|
||||||
|
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
|
||||||
|
import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { isEmpty, isNotEmpty } from '../empty.util';
|
||||||
|
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../../core/shared/operators';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides methods to deal with vocabulary tree
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class VocabularyTreeviewService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map containing the current node showed by the tree
|
||||||
|
*/
|
||||||
|
nodeMap = new Map<string, TreeviewNode>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map containing all the node already created for building the tree
|
||||||
|
*/
|
||||||
|
storedNodeMap = new Map<string, TreeviewNode>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array containing all the node already created for building the tree
|
||||||
|
*/
|
||||||
|
storedNodes: TreeviewNode[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link VocabularyOptions} object
|
||||||
|
*/
|
||||||
|
private vocabularyOptions: VocabularyOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vocabulary name
|
||||||
|
*/
|
||||||
|
private vocabularyName = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the current tree data
|
||||||
|
*/
|
||||||
|
private dataChange = new BehaviorSubject<TreeviewNode[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array containing node'ids hierarchy
|
||||||
|
*/
|
||||||
|
private initValueHierarchy: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if a search operation is pending
|
||||||
|
*/
|
||||||
|
private searching = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable to change the searching status
|
||||||
|
*/
|
||||||
|
private hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.searching.next(false));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {VocabularyService} vocabularyService
|
||||||
|
*/
|
||||||
|
constructor(private vocabularyService: VocabularyService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove nodes saved from maps and array
|
||||||
|
*/
|
||||||
|
cleanTree() {
|
||||||
|
this.nodeMap = new Map<string, TreeviewNode>();
|
||||||
|
this.storedNodeMap = new Map<string, TreeviewNode>();
|
||||||
|
this.storedNodes = [];
|
||||||
|
this.initValueHierarchy = [];
|
||||||
|
this.dataChange.next([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the tree's nodes
|
||||||
|
*
|
||||||
|
* @param options The {@link VocabularyOptions} object
|
||||||
|
* @param pageInfo The {@link PageInfo} object
|
||||||
|
* @param initValueId The entry id of the node to mark as selected, if any
|
||||||
|
*/
|
||||||
|
initialize(options: VocabularyOptions, pageInfo: PageInfo, initValueId?: string): void {
|
||||||
|
this.vocabularyOptions = options;
|
||||||
|
this.vocabularyName = options.name;
|
||||||
|
if (isNotEmpty(initValueId)) {
|
||||||
|
this.getNodeHierarchyById(initValueId)
|
||||||
|
.subscribe((hierarchy: string[]) => {
|
||||||
|
this.initValueHierarchy = hierarchy;
|
||||||
|
this.retrieveTopNodes(pageInfo, []);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.retrieveTopNodes(pageInfo, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array of the tree's nodes
|
||||||
|
*/
|
||||||
|
getData(): Observable<TreeviewNode[]> {
|
||||||
|
return this.dataChange
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand the root node whose children are not loaded
|
||||||
|
* @param node The root node
|
||||||
|
*/
|
||||||
|
loadMoreRoot(node: TreeviewFlatNode) {
|
||||||
|
const nodes = this.dataChange.value;
|
||||||
|
nodes.pop();
|
||||||
|
this.retrieveTopNodes(node.pageInfo, nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand a node whose children are not loaded
|
||||||
|
* @param item
|
||||||
|
* @param onlyFirstTime
|
||||||
|
*/
|
||||||
|
loadMore(item: VocabularyEntryDetail, onlyFirstTime = false) {
|
||||||
|
if (!this.nodeMap.has(item.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parent: TreeviewNode = this.nodeMap.get(item.id)!;
|
||||||
|
const children = this.nodeMap.get(item.id)!.children || [];
|
||||||
|
children.pop();
|
||||||
|
this.getChildrenNodesByParent(item.id, parent.pageInfo).subscribe((list: PaginatedList<VocabularyEntryDetail>) => {
|
||||||
|
|
||||||
|
if (onlyFirstTime && parent.children!.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNodes: TreeviewNode[] = list.page.map((entry) => this._generateNode(entry));
|
||||||
|
children.push(...newNodes);
|
||||||
|
|
||||||
|
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
|
||||||
|
// Update page info
|
||||||
|
const newPageInfo: PageInfo = Object.assign(new PageInfo(), list.pageInfo, {
|
||||||
|
currentPage: list.pageInfo.currentPage + 1
|
||||||
|
});
|
||||||
|
parent.updatePageInfo(newPageInfo);
|
||||||
|
|
||||||
|
// Need a new load more node
|
||||||
|
children.push(new TreeviewNode(LOAD_MORE_NODE, false, newPageInfo, item));
|
||||||
|
}
|
||||||
|
parent.childrenChange.next(children);
|
||||||
|
this.dataChange.next(this.dataChange.value);
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a search operation is pending
|
||||||
|
*/
|
||||||
|
isSearching(): Observable<boolean> {
|
||||||
|
return this.searching;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a search operation by query
|
||||||
|
*/
|
||||||
|
searchByQuery(query: string) {
|
||||||
|
this.searching.next(true);
|
||||||
|
if (isEmpty(this.storedNodes)) {
|
||||||
|
this.storedNodes = this.dataChange.value;
|
||||||
|
this.storedNodeMap = this.nodeMap;
|
||||||
|
}
|
||||||
|
this.nodeMap = new Map<string, TreeviewNode>();
|
||||||
|
this.dataChange.next([]);
|
||||||
|
|
||||||
|
this.vocabularyService.getVocabularyEntriesByValue(query, false, this.vocabularyOptions, new PageInfo()).pipe(
|
||||||
|
getFirstSucceededRemoteListPayload(),
|
||||||
|
flatMap((result: VocabularyEntry[]) => (result.length > 0) ? result : observableOf(null)),
|
||||||
|
flatMap((entry: VocabularyEntry) => this.getNodeHierarchy(entry)),
|
||||||
|
scan((acc: TreeviewNode[], value: TreeviewNode) => {
|
||||||
|
if (isEmpty(value) || findIndex(acc, (node) => node.item.id === value.item.id) !== -1) {
|
||||||
|
return acc;
|
||||||
|
} else {
|
||||||
|
return [...acc, value]
|
||||||
|
}
|
||||||
|
}, []),
|
||||||
|
merge(this.hideSearchingWhenUnsubscribed$)
|
||||||
|
).subscribe((nodes: TreeviewNode[]) => {
|
||||||
|
this.dataChange.next(nodes);
|
||||||
|
this.searching.next(false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset tree state with the one before the search
|
||||||
|
*/
|
||||||
|
restoreNodes() {
|
||||||
|
this.searching.next(false);
|
||||||
|
this.dataChange.next(this.storedNodes);
|
||||||
|
this.nodeMap = this.storedNodeMap;
|
||||||
|
|
||||||
|
this.storedNodeMap = new Map<string, TreeviewNode>();
|
||||||
|
this.storedNodes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a {@link TreeviewNode} object from vocabulary entry
|
||||||
|
*
|
||||||
|
* @param entry The vocabulary entry
|
||||||
|
* @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: VocabularyEntry, isSearchNode = false, toStore = true): TreeviewNode {
|
||||||
|
const entryId = entry.otherInformation.id;
|
||||||
|
if (this.nodeMap.has(entryId)) {
|
||||||
|
return this.nodeMap.get(entryId)!;
|
||||||
|
}
|
||||||
|
const entryDetail: VocabularyEntryDetail = Object.assign(new VocabularyEntryDetail(), entry, {
|
||||||
|
id: entryId
|
||||||
|
});
|
||||||
|
const hasChildren = entry.hasOtherInformation() && isNotEmpty((entry.otherInformation as any).children);
|
||||||
|
const pageInfo: PageInfo = new PageInfo();
|
||||||
|
const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId);
|
||||||
|
const result = new TreeviewNode(
|
||||||
|
entryDetail,
|
||||||
|
hasChildren,
|
||||||
|
pageInfo,
|
||||||
|
null,
|
||||||
|
isSearchNode,
|
||||||
|
isInInitValueHierarchy);
|
||||||
|
|
||||||
|
if (toStore) {
|
||||||
|
this.nodeMap.set(entryId, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the node Hierarchy by a given node's id
|
||||||
|
* @param id The node id
|
||||||
|
* @return Observable<string[]>
|
||||||
|
*/
|
||||||
|
private getNodeHierarchyById(id: string): Observable<string[]> {
|
||||||
|
return this.getById(id).pipe(
|
||||||
|
flatMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, [], false)),
|
||||||
|
map((node: TreeviewNode) => this.getNodeHierarchyIds(node))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the vocabulary entry's children
|
||||||
|
* @param parentId The node id
|
||||||
|
* @param pageInfo The {@link PageInfo} object
|
||||||
|
* @return Observable<PaginatedList<VocabularyEntryDetail>>
|
||||||
|
*/
|
||||||
|
private getChildrenNodesByParent(parentId: string, pageInfo: PageInfo): Observable<PaginatedList<VocabularyEntryDetail>> {
|
||||||
|
return this.vocabularyService.getEntryDetailChildren(parentId, this.vocabularyName, pageInfo).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the vocabulary entry's parent
|
||||||
|
* @param entryId The entry id
|
||||||
|
*/
|
||||||
|
private getParentNode(entryId: string): Observable<VocabularyEntryDetail> {
|
||||||
|
return this.vocabularyService.getEntryDetailParent(entryId, this.vocabularyName).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the vocabulary entry by id
|
||||||
|
* @param entryId The entry id
|
||||||
|
* @return Observable<VocabularyEntryDetail>
|
||||||
|
*/
|
||||||
|
private getById(entryId: string): Observable<VocabularyEntryDetail> {
|
||||||
|
return this.vocabularyService.findEntryDetailByValue(entryId, this.vocabularyName).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the top level vocabulary entries
|
||||||
|
* @param pageInfo The {@link PageInfo} object
|
||||||
|
* @param nodes The top level nodes already loaded, if any
|
||||||
|
*/
|
||||||
|
private retrieveTopNodes(pageInfo: PageInfo, nodes: TreeviewNode[]): void {
|
||||||
|
this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
).subscribe((list: PaginatedList<VocabularyEntryDetail>) => {
|
||||||
|
const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry))
|
||||||
|
nodes.push(...newNodes);
|
||||||
|
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
|
||||||
|
// Need a new load more node
|
||||||
|
const newPageInfo: PageInfo = Object.assign(new PageInfo(), list.pageInfo, {
|
||||||
|
currentPage: list.pageInfo.currentPage + 1
|
||||||
|
});
|
||||||
|
const loadMoreNode = new TreeviewNode(LOAD_MORE_ROOT_NODE, false, newPageInfo);
|
||||||
|
loadMoreNode.updatePageInfo(newPageInfo);
|
||||||
|
nodes.push(loadMoreNode);
|
||||||
|
}
|
||||||
|
// Notify the change.
|
||||||
|
this.dataChange.next(nodes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and return the tree node hierarchy by a given vocabulary entry
|
||||||
|
*
|
||||||
|
* @param item The vocabulary entry
|
||||||
|
* @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: VocabularyEntry, children?: TreeviewNode[], toStore = true): Observable<TreeviewNode> {
|
||||||
|
if (isEmpty(item)) {
|
||||||
|
return observableOf(null);
|
||||||
|
}
|
||||||
|
const node = this._generateNode(item, toStore, toStore);
|
||||||
|
|
||||||
|
if (isNotEmpty(children)) {
|
||||||
|
const newChildren = children
|
||||||
|
.filter((entry: TreeviewNode) => {
|
||||||
|
const ii = findIndex(node.children, (nodeEntry) => nodeEntry.item.id === entry.item.id);
|
||||||
|
return ii === -1;
|
||||||
|
});
|
||||||
|
newChildren.forEach((entry: TreeviewNode) => {
|
||||||
|
entry.loadMoreParentItem = node.item
|
||||||
|
});
|
||||||
|
node.children.push(...newChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) {
|
||||||
|
return this.getParentNode(node.item.id).pipe(
|
||||||
|
flatMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, [node], toStore))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return observableOf(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and return the node Hierarchy ids by a given node
|
||||||
|
*
|
||||||
|
* @param node The given node
|
||||||
|
* @param hierarchyIds The ids already present in the Hierarchy's array
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private getNodeHierarchyIds(node: TreeviewNode, hierarchyIds: string[] = []): string[] {
|
||||||
|
if (!hierarchyIds.includes(node.item.id)) {
|
||||||
|
hierarchyIds.push(node.item.id);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(node.children)) {
|
||||||
|
return this.getNodeHierarchyIds(node.children[0], hierarchyIds);
|
||||||
|
} else {
|
||||||
|
return hierarchyIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -211,6 +211,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-8.2.14.tgz#e18b27a6841577ce489ad31540150da5a444ca37"
|
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-8.2.14.tgz#e18b27a6841577ce489ad31540150da5a444ca37"
|
||||||
integrity sha512-7EhN9JJbAJcH2xCa+rIOmekjiEuB0qwPdHuD5qn/wwMfRzMZo+Db4hHbR9KHrLH6H82PTwYKye/LLpDaZqoHOA==
|
integrity sha512-7EhN9JJbAJcH2xCa+rIOmekjiEuB0qwPdHuD5qn/wwMfRzMZo+Db4hHbR9KHrLH6H82PTwYKye/LLpDaZqoHOA==
|
||||||
|
|
||||||
|
"@angular/material@8.2.3":
|
||||||
|
version "8.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@angular/material/-/material-8.2.3.tgz#16543e4e06a3fde2651a25cfe126e88e714ae105"
|
||||||
|
integrity sha512-SOczkIaqes+r+9XF/UUiokidfFKBpHkOPIaFK857sFD0FBNPvPEpOr5oHKCG3feERRwAFqHS7Wo2ohVEWypb5A==
|
||||||
|
dependencies:
|
||||||
|
tslib "^1.7.1"
|
||||||
|
|
||||||
"@angular/platform-browser-dynamic@~8.2.14":
|
"@angular/platform-browser-dynamic@~8.2.14":
|
||||||
version "8.2.14"
|
version "8.2.14"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.14.tgz#4439a79fe10ec45170e6940a28835e9ff0918950"
|
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.14.tgz#4439a79fe10ec45170e6940a28835e9ff0918950"
|
||||||
|
Reference in New Issue
Block a user