mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-06-30 18:15:11 +00:00
[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:
parent
41f9974919
commit
9863149a28
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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';
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user