Skip to content

Commit

Permalink
[ACS-4364] Move tree component and categories service to ADF (#8156)
Browse files Browse the repository at this point in the history
* [ACS-4364] Add tree component and categories service

* [ACS-4364] Add tree component to public api

* [ACS-4364] Refine tree unit tests

* [ACS-4364] Intergrate adding and deleting category

* [ACS-4364] Restyle load more button in tree component

* [ACS-4364] Missing semicolon

* [ACS-4364] Fix code styling

* [ACS-4364] Add docs for tree component and category service

* [ACS-4364] CR fixes

* [ACS-4364] Hide header row when displayName is not provided

* [ACS-4364] Docs fixes

* [ACS-4364] Add helper methods, code cleanup, unit tests for new methods

* [ACS-4364] Add missing semicolon
  • Loading branch information
MichalKinas authored Feb 1, 2023
1 parent afb22bb commit 52520bb
Show file tree
Hide file tree
Showing 29 changed files with 1,964 additions and 0 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"CSRF",
"datacolumn",
"datarow",
"Datasource",
"datatable",
"dateitem",
"datepicker",
Expand Down
109 changes: 109 additions & 0 deletions docs/content-services/components/tree.component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
Title: Tree component
Added: v6.0.0.0
Status: Active
Last reviewed: 2023-01-25
---

# [Tree component](../../../lib/content-services/src/lib/tree/components/tree.component.ts "Defined in tree.component.ts")

Shows the nodes in tree structure, each node containing children is collapsible/expandable. Can be integrated with any datasource extending [Tree service](../../../lib/content-services//src/lib/tree/services/tree.service.ts).

![Tree component screenshot](../../docassets/images/tree.png)

## Basic Usage

```html
<adf-tree
[displayName]="'Tree display name'"
[loadMoreSuffix]="'subnodes'"
[emptyContentTemplate]="emptyContentTemplate"
[nodeActionsMenuTemplate]="nodeActionsMenuTemplate"
(paginationChanged)="onPaginationChanged($event)">
</adf-tree>
```

## Class members

### Properties

| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| 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. |
| stickyHeader | `boolean` | false | If set to true header will be sticky. |
| selectableNodes | `boolean` | false | If set to true nodes will be selectable. |
| displayName | `string` | | Display name for tree title. |
| loadMoreSuffix | `string` | | Suffix added to `Load more` string inside load more node. |
| expandIcon | `string` | `chevron_right` | Icon shown when node is collapsed. |
| collapseIcon | `string` | `expand_more` | Icon showed when node is expanded. |


### Events

| Name | Type | Description |
| ---- | ---- | ----------- |
| paginationChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<PaginationModel>` | Emitted when during loading additional nodes pagination changes. |

## Details

### Defining your own custom datasource

First of all create custom node interface extending [`TreeNode`](../../../lib/content-services/src/lib/tree/models/tree-node.interface.ts) interface or use [`TreeNode`](../../../lib/content-services/src/lib/tree/models/tree-node.interface.ts) when none extra properties are required.

```ts
export interface CustomNode extends TreeNode
```
Next create custom datasource service extending [`TreeService`](../../../lib/content-services/src/lib/tree/services/tree.service.ts). Datasource service must implement `getSubNodes` method. It has to be able to provide both root level nodes as well as subnodes. If there are more subnodes to load for a given node it should add node with `LoadMoreNode` node type. Example of custom datasource service can be found in [`Category tree datasource service`](../services/category-tree-datasource.service.md).
```ts
@Injectable({...})
export class CustomTreeDatasourceService extends TreeService<TreeNode> {
...
public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number): Observable<TreeResponse<TreeNode>> {
...
}
```
Final step is to provide your custom datasource service as tree service in component using `TreeComponent`.
```ts
providers: [
{
provide: TreeService,
useClass: CustomTreeDatasourceService,
},
]
```
### Enabling nodes selection and listening to selection changes
First step is to provide necessary input value.
```html
<adf-tree
[displayName]="'Tree display name'"
[loadMoreSuffix]="'subnodes'"
[selectableNodes]="true"
[emptyContentTemplate]="emptyContentTemplate"
[nodeActionsMenuTemplate]="nodeActionsMenuTemplate"
(paginationChanged)="onPaginationChanged($event)">
</adf-tree>
```
Next inside your component get the `TreeComponent`
```ts
@ViewChild(TreeComponent)
public treeComponent: TreeComponent<TreeNode>;
```
and listen to selection changes.
```ts
this.treeComponent.treeNodesSelection.changed.subscribe(
(selectionChange: SelectionChange<TreeNode>) => {
this.onTreeSelectionChange(selectionChange);
}
);
```
26 changes: 26 additions & 0 deletions docs/content-services/services/category-tree-datasource.service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
Title: Category tree datasource service
Added: v6.0.0.0
Status: Active
Last reviewed: 2023-01-25
---

# [Category tree datasource service](../../../lib/content-services/src/lib/category/services/category-tree-datasource.service.ts "Defined in category-tree-datasource.service.ts")

Datasource service for category tree.

## Class members

### Methods

- **getSubNodes**(parentNodeId: `string`, skipCount?: `number`, maxItems?: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TreeResponse<CategoryNode>`](../../../lib/content-services/src/lib/tree/models/tree-response.interface.ts)`>`<br/>
Gets categories as nodes for category tree.
- _parentNodeId:_ `string` - Identifier of a parent category
- _skipCount:_ `number` - Number of top categories to skip
- _maxItems:_ `number` - Maximum number of subcategories returned from Observable
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TreeResponse<CategoryNode>`](../../../lib/content-services/src/lib/tree/models/tree-response.interface.ts)`>` - TreeResponse object containing pagination object and list on nodes

## Details

Category tree datasource service acts as datasource for tree component utilizing category service. See the
[Tree component](../../../lib/content-services/src/lib/tree/components/tree.component.ts) and [Tree service](../../../lib/content-services/src/lib/tree/services/tree.service.ts) to get more details on how datasource is used.
43 changes: 43 additions & 0 deletions docs/content-services/services/category.service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
Title: Category service
Added: v6.0.0.0
Status: Active
Last reviewed: 2023-01-25
---

# [Category service](../../../lib/content-services/src/lib/category/services/category.service.ts "Defined in category.service.ts")

Manages categories in Content Services.

## Class members

### Methods

- **getSubcategories**(parentCategoryId: `string`, skipCount?: `number`, maxItems?: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CategoryPaging`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryPaging.md)`>`<br/>
Gets subcategories of a given parent category.
- _parentCategoryId:_ `string` - Identifier of a parent category
- _skipCount:_ `number` - Number of top categories to skip
- _maxItems:_ `number` - Maximum number of subcategories returned from Observable
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CategoryPaging`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryPaging.md)`>` - CategoryPaging object (defined in JS-API) with category paging list
- **createSubcategory**(parentCategoryId: `string`, payload: [`CategoryBody`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryBody.md)): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CategoryEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryEntry.md)`>`<br/>
Creates subcategory under category with provided categoryId.
- _parentCategoryId:_ `string` - Identifier of a parent category
- _payload:_ [`CategoryBody`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryBody.md) - Created category body
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CategoryEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryEntry.md)`>` - CategoryEntry object (defined in JS-API) containing the category
- **updateCategory**(categoryId: `string`, payload: [`CategoryBody`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryBody.md)): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CategoryEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryEntry.md)`>`<br/>
Updates category.
- _categoryId:_ `string` - Identifier of a category
- _payload:_ [`CategoryBody`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryBody.md) - Created category body
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CategoryEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryEntry.md)`>` - CategoryEntry object (defined in JS-API) containing the category
- **deleteCategory**(categoryId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<void>`<br/>
Deletes category.
- _categoryId:_ `string` - Identifier of a category
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<void>` - Null object when the operation completes

## Details

See the
[Categories API](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoriesApi.md)
in the Alfresco JS API for more information about the types returned by [Category
service](category.service.md) methods and for the implementation of the REST API the service is
based on.
Binary file added docs/docassets/images/tree.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions lib/content-services/src/lib/category/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*!
* @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 * from './public-api';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*!
* @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.
*/

import { Injectable } from '@angular/core';
import { CategoryEntry, CategoryPaging } from '@alfresco/js-api';
import { Observable, of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CategoryServiceMock {

public getSubcategories(parentNodeId: string, skipCount?: number, maxItems?: number): Observable<CategoryPaging> {
return parentNodeId ? of(this.getChildrenLevelResponse(skipCount, maxItems)) : of(this.getRootLevelResponse(skipCount, maxItems));
}

private getRootLevelResponse(skipCount?: number, maxItems?: number): CategoryPaging {
const rootCategoryEntry: CategoryEntry = {entry: {id: 'testId', name: 'testNode', parentId: '-root-', hasChildren: true}};
return {list: {pagination: {skipCount, maxItems, hasMoreItems: false}, entries: [rootCategoryEntry]}};
}

private getChildrenLevelResponse(skipCount?: number, maxItems?: number): CategoryPaging {
const childCategoryEntry: CategoryEntry = {entry: {id: 'childId', name: 'childNode', parentId: 'testId', hasChildren: false}};
return {list: {pagination: {skipCount, maxItems, hasMoreItems: true}, entries: [childCategoryEntry]}};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*!
* @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.
*/

import { TreeNode } from '../../tree/models/tree-node.interface';

export interface CategoryNode extends TreeNode {
description?: string;
}
20 changes: 20 additions & 0 deletions lib/content-services/src/lib/category/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*!
* @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 * from './services/category.service';
export * from './services/category-tree-datasource.service';
export * from './models/category-node.interface';
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*!
* @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.
*/

import { CoreTestingModule } from '@alfresco/adf-core';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { CategoryService } from '../services/category.service';
import { CategoryTreeDatasourceService } from './category-tree-datasource.service';
import { CategoryServiceMock } from '../mock/category-mock.service';
import { TreeNodeType, TreeResponse } from '../../tree';
import { CategoryNode } from '../models/category-node.interface';

describe('CategoryTreeDatasourceService', () => {
let categoryTreeDatasourceService: CategoryTreeDatasourceService;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CoreTestingModule
],
providers: [
{ provide: CategoryService, useClass: CategoryServiceMock }
]
});

categoryTreeDatasourceService = TestBed.inject(CategoryTreeDatasourceService);
});

it('should get root level categories', fakeAsync(() => {
spyOn(categoryTreeDatasourceService, 'getParentNode').and.returnValue(undefined);
categoryTreeDatasourceService.getSubNodes(null, 0 , 100).subscribe((treeResponse: TreeResponse<CategoryNode>) => {
expect(treeResponse.entries.length).toBe(1);
expect(treeResponse.entries[0].level).toBe(0);
expect(treeResponse.entries[0].nodeType).toBe(TreeNodeType.RegularNode);
});
}));

it('should get child level categories and add loadMore node when there are more children to load', fakeAsync(() => {
const parentNode: CategoryNode = {
id: 'testId',
nodeName: 'testNode',
parentId: '-root-',
hasChildren: true,
level: 0,
isLoading: false,
nodeType: TreeNodeType.RegularNode
};
spyOn(categoryTreeDatasourceService, 'getParentNode').and.returnValue(parentNode);
categoryTreeDatasourceService.getSubNodes(parentNode.id, 0 , 100).subscribe((treeResponse: TreeResponse<CategoryNode>) => {
expect(treeResponse.entries.length).toBe(2);
expect(treeResponse.entries[0].parentId).toBe(parentNode.id);
expect(treeResponse.entries[0].level).toBe(1);
expect(treeResponse.entries[0].nodeType).toBe(TreeNodeType.RegularNode);
expect(treeResponse.entries[1].id).toBe('loadMore');
expect(treeResponse.entries[1].parentId).toBe(parentNode.id);
expect(treeResponse.entries[1].nodeType).toBe(TreeNodeType.LoadMoreNode);
});
}));
});
Loading

0 comments on commit 52520bb

Please sign in to comment.