[ACS-4592] missing right click menu on row and left click is only allow on indicator rotate (#8327)

* ACS-4592 Displaying context menu for tree list row and allow to expand row by clicking at label

* ACS-4592 Added clicking on label for load more button

* ACS-4592 Unit tests

* ACS-4592 Added documentation for context menu for tree component and fixed lint issues

* ACS-4592 Trigger stuck check
This commit is contained in:
AleksanderSklorz 2023-03-08 14:11:48 +01:00 committed by GitHub
parent 41f9974919
commit 9863149a28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 315 additions and 49 deletions

View File

@ -27,23 +27,25 @@ Shows the nodes in tree structure, each node containing children is collapsible/
### Properties ### Properties
| Name | Type | Default value | Description | | Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- | | ---- |---------------| --------- | ----------- |
| emptyContentTemplate | `TemplateRef` | | Template that will be rendered when no nodes are loaded. | | emptyContentTemplate | `TemplateRef` | | Template that will be rendered when no nodes are loaded. |
| nodeActionsMenuTemplate | `TemplateRef` | | Template that will be rendered when context menu for given node is opened. | | nodeActionsMenuTemplate | `TemplateRef` | | Template that will be rendered when context menu for given node is opened. |
| stickyHeader | `boolean` | false | If set to true header will be sticky. | | stickyHeader | `boolean` | false | If set to true header will be sticky. |
| selectableNodes | `boolean` | false | If set to true nodes will be selectable. | | selectableNodes | `boolean` | false | If set to true nodes will be selectable. |
| displayName | `string` | | Display name for tree title. | | displayName | `string` | | Display name for tree title. |
| loadMoreSuffix | `string` | | Suffix added to `Load more` string inside load more node. | | loadMoreSuffix | `string` | | Suffix added to `Load more` string inside load more node. |
| expandIcon | `string` | `chevron_right` | Icon shown when node is collapsed. | | expandIcon | `string` | `chevron_right` | Icon shown when node is collapsed. |
| collapseIcon | `string` | `expand_more` | Icon showed when node is expanded. | | collapseIcon | `string` | `expand_more` | Icon showed when node is expanded. |
| contextMenuOptions | `any[]` | | Array of context menu options which should be displayed for each row. |
### Events ### Events
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- |----------------------------------------------------------------------------------------|------------------------------------------------------------------|
| paginationChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<PaginationModel>` | Emitted when during loading additional nodes pagination changes. | | paginationChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<PaginationModel>` | Emitted when during loading additional nodes pagination changes. |
| contextMenuOptionSelected | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<TreeContextMenuResult<T>>` | Emitted when any context menu option is selected. |
## Details ## Details

View File

@ -33,7 +33,9 @@
</button> </button>
</div> </div>
<div class="adf-tree-cell"> <div class="adf-tree-cell">
<span class="adf-tree-cell-value"> <span
class="adf-tree-cell-value"
(click)="loadMoreSubnodes(node)">
{{ 'ADF-TREE.LOAD-MORE-BUTTON' | translate: { name: loadMoreSuffix } }} {{ 'ADF-TREE.LOAD-MORE-BUTTON' | translate: { name: loadMoreSuffix } }}
</span> </span>
</div> </div>
@ -42,7 +44,10 @@
class="adf-tree-row" class="adf-tree-row"
[attr.data-automation-id]="'node_' + node.id" [attr.data-automation-id]="'node_' + node.id"
*matTreeNodeDef="let node" *matTreeNodeDef="let node"
matTreeNodePadding> matTreeNodePadding
[adf-context-menu]="contextMenuOptions"
[adf-context-menu-enabled]="!!contextMenuOptions"
(contextmenu)="contextMenuSource = node">
<div class="adf-tree-expand-collapse-container"> <div class="adf-tree-expand-collapse-container">
<button *ngIf="node.hasChildren" <button *ngIf="node.hasChildren"
class="adf-tree-expand-collapse-button" class="adf-tree-expand-collapse-button"
@ -80,7 +85,10 @@
</ng-template> </ng-template>
</ng-container> </ng-container>
<div class="adf-tree-cell"> <div class="adf-tree-cell">
<span class="adf-tree-cell-value"> <span
class="adf-tree-cell-value"
[class.adf-tree-clickable-cell-value]="node.hasChildren"
(click)="expandCollapseNode(node)">
{{ node.nodeName }} {{ node.nodeName }}
</span> </span>
</div> </div>

View File

@ -11,6 +11,11 @@ $tree-header-font-size: 12px !default;
} }
} }
.adf-tree-load-more-row .adf-tree-cell-value,
.adf-tree-clickable-cell-value {
cursor: pointer;
}
.adf-tree-row, .adf-tree-row,
.adf-tree-load-more-row { .adf-tree-load-more-row {
transition: all 0.3s ease; transition: all 0.3s ease;
@ -21,7 +26,6 @@ $tree-header-font-size: 12px !default;
transition-property: background-color; transition-property: background-color;
border-bottom: 1px solid var(--theme-border-color); border-bottom: 1px solid var(--theme-border-color);
min-height: $tree-row-height; min-height: $tree-row-height;
cursor: pointer;
user-select: none; user-select: none;
.adf-tree-expand-collapse-container { .adf-tree-expand-collapse-container {
@ -55,7 +59,7 @@ $tree-header-font-size: 12px !default;
width: 100%; width: 100%;
.adf-tree-cell-value { .adf-tree-cell-value {
display: block; display: inline-block;
padding: 10px; padding: 10px;
word-break: break-word; word-break: break-word;
} }

View File

@ -17,32 +17,41 @@
import { TreeComponent } from './tree.component'; import { TreeComponent } from './tree.component';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, UserPreferencesService } from '@alfresco/adf-core'; import { ContextMenuDirective, CoreTestingModule, UserPreferencesService } from '@alfresco/adf-core';
import { MatTreeModule } from '@angular/material/tree'; import { MatTreeModule } from '@angular/material/tree';
import { TreeNode, TreeNodeType } from '../models/tree-node.interface'; import { TreeNode, TreeNodeType } from '../models/tree-node.interface';
import { singleNode, treeNodesChildrenMockExpanded, treeNodesMock, treeNodesMockExpanded } from '../mock/tree-node.mock'; import {
import { of } from 'rxjs'; singleNode,
treeNodesChildrenMockExpanded,
treeNodesMock,
treeNodesMockExpanded,
treeNodesNoChildrenMock
} from '../mock/tree-node.mock';
import { of, Subject } from 'rxjs';
import { TreeService } from '../services/tree.service'; import { TreeService } from '../services/tree.service';
import { TreeServiceMock } from '../mock/tree-service.service.mock'; import { TreeServiceMock } from '../mock/tree-service.service.mock';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { SelectionChange } from '@angular/cdk/collections'; import { SelectionChange } from '@angular/cdk/collections';
import { DebugElement } from '@angular/core';
describe('TreeComponent', () => { describe('TreeComponent', () => {
let fixture: ComponentFixture<TreeComponent<TreeNode>>; let fixture: ComponentFixture<TreeComponent<TreeNode>>;
let component: TreeComponent<TreeNode>; let component: TreeComponent<TreeNode>;
let userPreferenceService: UserPreferencesService; let userPreferenceService: UserPreferencesService;
const getDisplayNameValue = (nodeId: string) => const composeNodeSelector = (nodeId: string) => `.mat-tree-node[data-automation-id="node_${nodeId}"]`;
fixture.nativeElement.querySelector(`.mat-tree-node[data-automation-id="node_${nodeId}"] .adf-tree-cell-value`).innerText.trim();
const getNodePadding = (nodeId: string) => { const getNode = (nodeId: string) => fixture.debugElement.query(By.css(composeNodeSelector(nodeId)));
const element = fixture.nativeElement.querySelector(`.mat-tree-node[data-automation-id="node_${nodeId}"]`);
return parseInt(window.getComputedStyle(element).paddingLeft, 10);
};
const getNodeSpinner = (nodeId: string) => fixture.nativeElement.querySelector(`.mat-tree-node[data-automation-id="node_${nodeId}"] .mat-progress-spinner`); const getDisplayNameElement = (nodeId: string) => fixture.nativeElement.querySelector(`${composeNodeSelector(nodeId)} .adf-tree-cell-value`);
const getExpandCollapseBtn = (nodeId: string) => fixture.nativeElement.querySelector(`.mat-tree-node[data-automation-id="node_${nodeId}"] .adf-icon`); const getDisplayNameValue = (nodeId: string) => getDisplayNameElement(nodeId).innerText.trim();
const getNodePadding = (nodeId: string) => parseInt(getComputedStyle(getNode(nodeId).nativeElement).paddingLeft, 10);
const getNodeSpinner = (nodeId: string) => fixture.nativeElement.querySelector(`${composeNodeSelector(nodeId)} .mat-progress-spinner`);
const getExpandCollapseBtn = (nodeId: string) => fixture.nativeElement.querySelector(`${composeNodeSelector(nodeId)} .adf-icon`);
const tickCheckbox = (index: number) => { const tickCheckbox = (index: number) => {
const nodeCheckboxes = fixture.debugElement.queryAll(By.css('mat-checkbox')); const nodeCheckboxes = fixture.debugElement.queryAll(By.css('mat-checkbox'));
@ -179,6 +188,56 @@ describe('TreeComponent', () => {
expect(collapseSpy).toHaveBeenCalledWith(component.treeService.treeNodes[0], treeNodesMockExpanded); expect(collapseSpy).toHaveBeenCalledWith(component.treeService.treeNodes[0], treeNodesMockExpanded);
}); });
it('should call collapseNode on TreeService when collapsing node by clicking at node label and node has children', () => {
component.refreshTree();
fixture.detectChanges();
spyOn(component.treeService, 'collapseNode');
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(true);
getDisplayNameElement(component.treeService.treeNodes[0].id).dispatchEvent(new Event('click'));
expect(component.treeService.collapseNode).toHaveBeenCalledWith(component.treeService.treeNodes[0]);
});
it('should call expandNode on TreeService when expanding node by clicking at node label and node has children', () => {
component.refreshTree();
fixture.detectChanges();
spyOn(component.treeService, 'expandNode');
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(false);
getDisplayNameElement(component.treeService.treeNodes[0].id).dispatchEvent(new Event('click'));
expect(component.treeService.expandNode).toHaveBeenCalledWith(component.treeService.treeNodes[0], treeNodesMockExpanded);
});
it('should not call collapseNode on TreeService when collapsing node and node has not children', () => {
spyOn(component.treeService, 'getSubNodes').and.returnValue(of({
pagination: {
skipCount: 0,
maxItems: 25
},
entries: Array.from(treeNodesNoChildrenMock)
}));
component.refreshTree();
fixture.detectChanges();
spyOn(component.treeService, 'collapseNode');
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(true);
getDisplayNameElement(component.treeService.treeNodes[0].id).dispatchEvent(new Event('click'));
expect(component.treeService.collapseNode).not.toHaveBeenCalled();
});
it('should not call expandNode on TreeService when expanding node by clicking at node label and node has not children', () => {
spyOn(component.treeService, 'getSubNodes').and.returnValue(of({
pagination: {
skipCount: 0,
maxItems: 25
},
entries: Array.from(treeNodesNoChildrenMock)
}));
component.refreshTree();
fixture.detectChanges();
spyOn(component.treeService, 'expandNode');
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(false);
getDisplayNameElement(component.treeService.treeNodes[0].id).dispatchEvent(new Event('click'));
expect(component.treeService.expandNode).not.toHaveBeenCalled();
});
it('should load more subnodes and remove load more button when load more button is clicked', () => { it('should load more subnodes and remove load more button when load more button is clicked', () => {
component.refreshTree(); component.refreshTree();
fixture.detectChanges(); fixture.detectChanges();
@ -193,6 +252,22 @@ describe('TreeComponent', () => {
expect(loadMoreNodes).toBeUndefined(); expect(loadMoreNodes).toBeUndefined();
}); });
it('should load more subnodes and remove load more button when label of load more button is clicked', () => {
component.refreshTree();
fixture.detectChanges();
spyOn(component.treeService, 'getSubNodes').and.returnValue(of({
pagination: {},
entries: Array.from(singleNode)
}));
spyOn(component.treeService, 'appendNodes');
fixture.debugElement.query(By.css('.adf-tree-load-more-row .adf-tree-cell-value')).nativeElement.click();
fixture.whenStable();
fixture.detectChanges();
expect(component.treeService.appendNodes).toHaveBeenCalledWith(component.treeService.treeNodes[0], Array.from(singleNode));
expect(component.treeService.treeNodes.find((node) => node.nodeType === TreeNodeType.LoadMoreNode))
.toBeUndefined();
});
it('selection should be disabled by default, no checkboxes should be displayed', () => { it('selection should be disabled by default, no checkboxes should be displayed', () => {
component.refreshTree(); component.refreshTree();
fixture.detectChanges(); fixture.detectChanges();
@ -257,4 +332,96 @@ describe('TreeComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
}); });
describe('Context menu', () => {
let contextMenu: ContextMenuDirective;
let contextMenuOption1: any;
let contextMenuOption2: any;
let node: DebugElement;
const optionTitle1 = 'option 1';
const optionTitle2 = 'option 2';
beforeEach(() => {
fixture.detectChanges();
node = getNode('testId1');
contextMenu = node.injector.get(ContextMenuDirective);
contextMenuOption1 = {
title: optionTitle1,
subject: new Subject()
};
contextMenuOption2 = {
title: optionTitle2,
subject: new Subject()
};
});
it('should have assigned correct value to links property of context menu for row', () => {
component.contextMenuOptions = [contextMenuOption1, contextMenuOption2];
fixture.detectChanges();
expect(contextMenu.links).toEqual(component.contextMenuOptions);
});
it('should have assigned default subject to each context menu option', () => {
contextMenuOption1.subject = undefined;
contextMenuOption2.subject = undefined;
component.contextMenuOptions = [contextMenuOption1, contextMenuOption2];
fixture.detectChanges();
expect(contextMenu.links).toEqual([{
title: optionTitle1,
subject: jasmine.any(Subject)
}, {
title: optionTitle2,
subject: jasmine.any(Subject)
}]);
});
it('should have assigned false to enabled property of context menu for row by default', () => {
expect(contextMenu.enabled).toBeFalse();
});
it('should have assigned true to enabled property of context menu for row if contextMenuOptions is set', () => {
component.contextMenuOptions = [contextMenuOption1, contextMenuOption2];
fixture.detectChanges();
expect(contextMenu.enabled).toBeTrue();
});
it('should have assigned false to enabled property of context menu for row if contextMenuOptions is not set', () => {
component.contextMenuOptions = [contextMenuOption1, contextMenuOption2];
fixture.detectChanges();
component.contextMenuOptions = null;
fixture.detectChanges();
expect(contextMenu.enabled).toBeFalse();
});
it('should emit contextMenuOptionSelected with correct parameters when any context menu option has been selected', () => {
spyOn(component.contextMenuOptionSelected, 'emit');
component.contextMenuOptions = [contextMenuOption1, contextMenuOption2];
const option = component.contextMenuOptions[0];
component.contextMenuOptions[0].subject.next(option);
expect(component.contextMenuOptionSelected.emit).toHaveBeenCalledWith({
contextMenuOption: option,
row: undefined
});
});
it('should emit contextMenuOptionSelected including row when any context menu option has been selected and contextmenu event has been triggered earlier', () => {
spyOn(component.contextMenuOptionSelected, 'emit');
component.contextMenuOptions = [contextMenuOption1, contextMenuOption2];
fixture.detectChanges();
node.nativeElement.dispatchEvent(new MouseEvent('contextmenu'));
const option = component.contextMenuOptions[0];
component.contextMenuOptions[0].subject.next(option);
expect(component.contextMenuOptionSelected.emit).toHaveBeenCalledWith({
contextMenuOption: option,
row: treeNodesMockExpanded[0]
});
});
});
}); });

View File

@ -15,14 +15,28 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, EventEmitter, HostBinding, Input, OnInit, Output, QueryList, TemplateRef, ViewChildren, ViewEncapsulation } from '@angular/core'; import {
import { BehaviorSubject, Observable } from 'rxjs'; Component,
EventEmitter,
HostBinding,
Input,
OnDestroy,
OnInit,
Output,
QueryList,
TemplateRef,
ViewChildren,
ViewEncapsulation
} from '@angular/core';
import { BehaviorSubject, merge, Observable, Subject } from 'rxjs';
import { TreeNode, TreeNodeType } from '../models/tree-node.interface'; import { TreeNode, TreeNodeType } from '../models/tree-node.interface';
import { TreeService } from '../services/tree.service'; import { TreeService } from '../services/tree.service';
import { PaginationModel, UserPreferencesService } from '@alfresco/adf-core'; import { PaginationModel, UserPreferencesService } from '@alfresco/adf-core';
import { SelectionChange, SelectionModel } from '@angular/cdk/collections'; import { SelectionChange, SelectionModel } from '@angular/cdk/collections';
import { TreeResponse } from '../models/tree-response.interface'; import { TreeResponse } from '../models/tree-response.interface';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { TreeContextMenuResult } from '../models/tree-context-menu-result.interface';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'adf-tree', selector: 'adf-tree',
@ -31,7 +45,7 @@ import { MatCheckbox } from '@angular/material/checkbox';
host: { class: 'adf-tree' }, host: { class: 'adf-tree' },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class TreeComponent<T extends TreeNode> implements OnInit { export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
/** TemplateRef to provide empty template when no nodes are loaded */ /** TemplateRef to provide empty template when no nodes are loaded */
@Input() @Input()
@ -70,16 +84,57 @@ export class TreeComponent<T extends TreeNode> implements OnInit {
@Output() @Output()
public paginationChanged: EventEmitter<PaginationModel> = new EventEmitter(); public paginationChanged: EventEmitter<PaginationModel> = new EventEmitter();
/** Emitted when any context menu option is selected */
@Output()
public contextMenuOptionSelected = new EventEmitter<TreeContextMenuResult<T>>();
@ViewChildren(MatCheckbox) @ViewChildren(MatCheckbox)
public nodeCheckboxes: QueryList<MatCheckbox>; public nodeCheckboxes: QueryList<MatCheckbox>;
private loadingRootSource = new BehaviorSubject<boolean>(false); private loadingRootSource = new BehaviorSubject<boolean>(false);
private _contextMenuSource: T;
private _contextMenuOptions: any[];
private contextMenuOptionsChanged$ = new Subject<void>();
public loadingRoot$: Observable<boolean>; public loadingRoot$: Observable<boolean>;
public treeNodesSelection = new SelectionModel<T>(true, [], true, (node1: T, node2: T) => node1.id === node2.id); public treeNodesSelection = new SelectionModel<T>(true, [], true, (node1: T, node2: T) => node1.id === node2.id);
constructor(public treeService: TreeService<T>, constructor(public treeService: TreeService<T>,
private userPreferenceService: UserPreferencesService) {} private userPreferenceService: UserPreferencesService) {}
set contextMenuSource(contextMenuSource: T) {
this._contextMenuSource = contextMenuSource;
}
/** Array of context menu options which should be displayed for each row. */
@Input()
set contextMenuOptions(contextMenuOptions: any[]) {
this.contextMenuOptionsChanged$.next();
if (contextMenuOptions) {
this._contextMenuOptions = contextMenuOptions.map((option) => {
if (!option.subject) {
option = {
...option,
subject: new Subject()
};
}
return option;
});
merge(...this.contextMenuOptions.map((option) => option.subject)).pipe(takeUntil(this.contextMenuOptionsChanged$))
.subscribe((option) => {
this.contextMenuOptionSelected.emit({
row: this._contextMenuSource,
contextMenuOption: option
});
});
} else {
this._contextMenuOptions = contextMenuOptions;
}
}
get contextMenuOptions(): any[] {
return this._contextMenuOptions;
}
ngOnInit(): void { ngOnInit(): void {
this.loadingRoot$ = this.loadingRootSource.asObservable(); this.loadingRoot$ = this.loadingRootSource.asObservable();
this.refreshTree(0, this.userPreferenceService.paginationSize); this.refreshTree(0, this.userPreferenceService.paginationSize);
@ -88,6 +143,11 @@ export class TreeComponent<T extends TreeNode> implements OnInit {
}); });
} }
ngOnDestroy() {
this.contextMenuOptionsChanged$.next();
this.contextMenuOptionsChanged$.complete();
}
/** /**
* Checks if node is LoadMoreNode node * Checks if node is LoadMoreNode node
* *
@ -141,21 +201,23 @@ export class TreeComponent<T extends TreeNode> implements OnInit {
* @param node node to be collapsed/expanded * @param node node to be collapsed/expanded
*/ */
public expandCollapseNode(node: T): void { public expandCollapseNode(node: T): void {
if (this.treeService.treeControl.isExpanded(node)) { if (node.hasChildren) {
this.treeService.collapseNode(node); if (this.treeService.treeControl.isExpanded(node)) {
} else { this.treeService.collapseNode(node);
node.isLoading = true; } else {
this.treeService.getSubNodes(node.id, 0, this.userPreferenceService.paginationSize).subscribe((response: TreeResponse<T>) => { node.isLoading = true;
this.treeService.expandNode(node, response.entries); this.treeService.getSubNodes(node.id, 0, this.userPreferenceService.paginationSize).subscribe((response: TreeResponse<T>) => {
this.paginationChanged.emit(response.pagination); this.treeService.expandNode(node, response.entries);
node.isLoading = false; this.paginationChanged.emit(response.pagination);
if (this.treeNodesSelection.isSelected(node)) { node.isLoading = false;
//timeout used to update nodeCheckboxes query list after new nodes are added so they can be selected if (this.treeNodesSelection.isSelected(node)) {
setTimeout(() => { //timeout used to update nodeCheckboxes query list after new nodes are added so they can be selected
this.treeNodesSelection.select(...response.entries); setTimeout(() => {
}); this.treeNodesSelection.select(...response.entries);
} });
}); }
});
}
} }
} }

View File

@ -0,0 +1,21 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface TreeContextMenuResult<T> {
row: T;
contextMenuOption: any;
}

View File

@ -18,5 +18,6 @@
export * from './tree.module'; export * from './tree.module';
export * from './models/tree-response.interface'; export * from './models/tree-response.interface';
export * from './models/tree-node.interface'; export * from './models/tree-node.interface';
export * from './models/tree-context-menu-result.interface';
export * from './services/tree.service'; export * from './services/tree.service';
export * from './components/tree.component'; export * from './components/tree.component';

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { CoreModule } from '@alfresco/adf-core'; import { ContextMenuModule, CoreModule } from '@alfresco/adf-core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@ -27,7 +27,8 @@ import { TreeComponent } from './components/tree.component';
CommonModule, CommonModule,
CoreModule, CoreModule,
MaterialModule, MaterialModule,
TranslateModule TranslateModule,
ContextMenuModule
], ],
declarations: [ declarations: [
TreeComponent TreeComponent