diff --git a/cspell.json b/cspell.json
index cb2afaae1d..d15ae332c6 100644
--- a/cspell.json
+++ b/cspell.json
@@ -27,6 +27,7 @@
"CSRF",
"datacolumn",
"datarow",
+ "Datasource",
"datatable",
"dateitem",
"datepicker",
diff --git a/docs/content-services/components/tree.component.md b/docs/content-services/components/tree.component.md
new file mode 100644
index 0000000000..d49ddeb2cb
--- /dev/null
+++ b/docs/content-services/components/tree.component.md
@@ -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).
+
+
+
+## Basic Usage
+
+```html
+
+
+```
+
+## 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)`` | 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 {
+ ...
+ public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number): Observable> {
+ ...
+}
+```
+
+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
+
+
+```
+
+Next inside your component get the `TreeComponent`
+
+```ts
+@ViewChild(TreeComponent)
+public treeComponent: TreeComponent;
+```
+
+and listen to selection changes.
+
+```ts
+this.treeComponent.treeNodesSelection.changed.subscribe(
+ (selectionChange: SelectionChange) => {
+ this.onTreeSelectionChange(selectionChange);
+ }
+);
+```
diff --git a/docs/content-services/services/category-tree-datasource.service.md b/docs/content-services/services/category-tree-datasource.service.md
new file mode 100644
index 0000000000..1b6dd60ada
--- /dev/null
+++ b/docs/content-services/services/category-tree-datasource.service.md
@@ -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`](../../../lib/content-services/src/lib/tree/models/tree-response.interface.ts)`>`
+ 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`](../../../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.
diff --git a/docs/content-services/services/category.service.md b/docs/content-services/services/category.service.md
new file mode 100644
index 0000000000..4b00b98058
--- /dev/null
+++ b/docs/content-services/services/category.service.md
@@ -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)`>`
+ 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)`>`
+ 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)`>`
+ 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)``
+ Deletes category.
+ - _categoryId:_ `string` - Identifier of a category
+ - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - 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.
diff --git a/docs/docassets/images/tree.png b/docs/docassets/images/tree.png
new file mode 100644
index 0000000000..22c6f2e67d
Binary files /dev/null and b/docs/docassets/images/tree.png differ
diff --git a/lib/content-services/src/lib/category/index.ts b/lib/content-services/src/lib/category/index.ts
new file mode 100644
index 0000000000..a7e30cc675
--- /dev/null
+++ b/lib/content-services/src/lib/category/index.ts
@@ -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';
diff --git a/lib/content-services/src/lib/category/mock/category-mock.service.ts b/lib/content-services/src/lib/category/mock/category-mock.service.ts
new file mode 100644
index 0000000000..d1649341eb
--- /dev/null
+++ b/lib/content-services/src/lib/category/mock/category-mock.service.ts
@@ -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 {
+ 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]}};
+ }
+}
diff --git a/lib/content-services/src/lib/category/models/category-node.interface.ts b/lib/content-services/src/lib/category/models/category-node.interface.ts
new file mode 100644
index 0000000000..c681c3f520
--- /dev/null
+++ b/lib/content-services/src/lib/category/models/category-node.interface.ts
@@ -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;
+}
diff --git a/lib/content-services/src/lib/category/public-api.ts b/lib/content-services/src/lib/category/public-api.ts
new file mode 100644
index 0000000000..593e34355a
--- /dev/null
+++ b/lib/content-services/src/lib/category/public-api.ts
@@ -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';
diff --git a/lib/content-services/src/lib/category/services/category-tree-datasource.service.spec.ts b/lib/content-services/src/lib/category/services/category-tree-datasource.service.spec.ts
new file mode 100644
index 0000000000..f264c0fa01
--- /dev/null
+++ b/lib/content-services/src/lib/category/services/category-tree-datasource.service.spec.ts
@@ -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) => {
+ 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) => {
+ 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);
+ });
+ }));
+});
diff --git a/lib/content-services/src/lib/category/services/category-tree-datasource.service.ts b/lib/content-services/src/lib/category/services/category-tree-datasource.service.ts
new file mode 100644
index 0000000000..b7d00e45c9
--- /dev/null
+++ b/lib/content-services/src/lib/category/services/category-tree-datasource.service.ts
@@ -0,0 +1,65 @@
+/*!
+ * @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 { TreeNodeType } from '../../tree/models/tree-node.interface';
+import { TreeResponse } from '../../tree/models/tree-response.interface';
+import { TreeService } from '../../tree/services/tree.service';
+import { CategoryNode } from '../models/category-node.interface';
+import { CategoryService } from './category.service';
+import { CategoryEntry, CategoryPaging } from '@alfresco/js-api';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+@Injectable({ providedIn: 'root' })
+export class CategoryTreeDatasourceService extends TreeService {
+
+ constructor(private categoryService: CategoryService) {
+ super();
+ }
+
+ public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number): Observable> {
+ return this.categoryService.getSubcategories(parentNodeId, skipCount, maxItems).pipe(map((response: CategoryPaging) => {
+ const parentNode: CategoryNode = this.getParentNode(parentNodeId);
+ const nodesList: CategoryNode[] = response.list.entries.map((entry: CategoryEntry) => {
+ return {
+ id: entry.entry.id,
+ nodeName: entry.entry.name,
+ parentId: entry.entry.parentId,
+ hasChildren: entry.entry.hasChildren,
+ level: parentNode ? parentNode.level + 1 : 0,
+ isLoading: false,
+ nodeType: TreeNodeType.RegularNode
+ };
+ });
+ if (response.list.pagination.hasMoreItems && parentNode) {
+ const loadMoreNode: CategoryNode = {
+ id: 'loadMore',
+ nodeName: '',
+ parentId: parentNode.id,
+ hasChildren: false,
+ level: parentNode.level + 1,
+ isLoading: false,
+ nodeType: TreeNodeType.LoadMoreNode
+ };
+ nodesList.push(loadMoreNode);
+ }
+ const treeResponse: TreeResponse = {entries: nodesList, pagination: response.list.pagination};
+ return treeResponse;
+ }));
+ }
+}
diff --git a/lib/content-services/src/lib/category/services/category.service.spec.ts b/lib/content-services/src/lib/category/services/category.service.spec.ts
new file mode 100644
index 0000000000..073f234664
--- /dev/null
+++ b/lib/content-services/src/lib/category/services/category.service.spec.ts
@@ -0,0 +1,74 @@
+/*!
+ * @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 { CategoryBody, CategoryEntry, CategoryPaging } from '@alfresco/js-api';
+import { fakeAsync, TestBed } from '@angular/core/testing';
+import { CategoryService } from './category.service';
+
+describe('CategoryService', () => {
+ let categoryService: CategoryService;
+ const fakeParentCategoryId = 'testParentId';
+ const fakeCategoriesResponse: CategoryPaging = { list: { pagination: {}, entries: [] }};
+ const fakeCategoryEntry: CategoryEntry = { entry: { id: 'testId', name: 'testName' }};
+ const fakeCategoryBody: CategoryBody = { name: 'updatedName' };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreTestingModule
+ ]
+ });
+
+ categoryService = TestBed.inject(CategoryService);
+ });
+
+ it('should fetch categories with provided parentId', fakeAsync(() => {
+ const getSpy = spyOn(categoryService.categoriesApi, 'getSubcategories').and.returnValue(Promise.resolve(fakeCategoriesResponse));
+ categoryService.getSubcategories(fakeParentCategoryId, 0, 100).subscribe(() => {
+ expect(getSpy).toHaveBeenCalledOnceWith(fakeParentCategoryId, {skipCount: 0, maxItems: 100});
+ });
+ }));
+
+ it('should fetch root level categories when parentId not provided', fakeAsync(() => {
+ const getSpy = spyOn(categoryService.categoriesApi, 'getSubcategories').and.returnValue(Promise.resolve(fakeCategoriesResponse));
+ categoryService.getSubcategories(null, 0, 100).subscribe(() => {
+ expect(getSpy).toHaveBeenCalledOnceWith('-root-', {skipCount: 0, maxItems: 100});
+ });
+ }));
+
+ it('should create subcategory', fakeAsync(() => {
+ const createSpy = spyOn(categoryService.categoriesApi, 'createSubcategory').and.returnValue(Promise.resolve(fakeCategoryEntry));
+ categoryService.createSubcategory(fakeParentCategoryId, fakeCategoryEntry.entry).subscribe(() => {
+ expect(createSpy).toHaveBeenCalledOnceWith(fakeParentCategoryId, [fakeCategoryEntry.entry], {});
+ });
+ }));
+
+ it('should update category', fakeAsync(() => {
+ const updateSpy = spyOn(categoryService.categoriesApi, 'updateCategory').and.returnValue(Promise.resolve(fakeCategoryEntry));
+ categoryService.updateCategory(fakeParentCategoryId, fakeCategoryBody).subscribe(() => {
+ expect(updateSpy).toHaveBeenCalledOnceWith(fakeParentCategoryId, fakeCategoryBody, {});
+ });
+ }));
+
+ it('should delete category', fakeAsync(() => {
+ const deleteSpy = spyOn(categoryService.categoriesApi, 'deleteCategory').and.returnValue(Promise.resolve());
+ categoryService.deleteCategory(fakeParentCategoryId).subscribe(() => {
+ expect(deleteSpy).toHaveBeenCalledOnceWith(fakeParentCategoryId);
+ });
+ }));
+});
diff --git a/lib/content-services/src/lib/category/services/category.service.ts b/lib/content-services/src/lib/category/services/category.service.ts
new file mode 100644
index 0000000000..b6941d0acb
--- /dev/null
+++ b/lib/content-services/src/lib/category/services/category.service.ts
@@ -0,0 +1,77 @@
+/*!
+ * @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 { AlfrescoApiService } from '@alfresco/adf-core';
+import { CategoriesApi, CategoryBody, CategoryEntry, CategoryPaging } from '@alfresco/js-api';
+import { from, Observable } from 'rxjs';
+
+@Injectable({ providedIn: 'root' })
+export class CategoryService {
+ private _categoriesApi: CategoriesApi;
+
+ get categoriesApi(): CategoriesApi {
+ this._categoriesApi = this._categoriesApi ?? new CategoriesApi(this.apiService.getInstance());
+ return this._categoriesApi;
+ }
+
+ constructor(private apiService: AlfrescoApiService) {}
+
+ /**
+ * Get subcategories of a given parent category
+ *
+ * @param parentCategoryId The identifier of a parent category.
+ * @param skipCount Number of top categories to skip.
+ * @param maxItems Maximum number of subcategories returned from Observable.
+ * @return Observable
+ */
+ getSubcategories(parentCategoryId: string, skipCount?: number, maxItems?: number): Observable {
+ return from(this.categoriesApi.getSubcategories(parentCategoryId ?? '-root-', {skipCount, maxItems}));
+ }
+
+ /**
+ * Creates subcategory under category with provided categoryId
+ *
+ * @param parentCategoryId The identifier of a parent category.
+ * @param payload Created category body
+ * @return Observable
+ */
+ createSubcategory(parentCategoryId: string, payload: CategoryBody): Observable {
+ return from(this.categoriesApi.createSubcategory(parentCategoryId, [payload], {}));
+ }
+
+ /**
+ * Updates category
+ *
+ * @param categoryId The identifier of a category.
+ * @param payload Updated category body
+ * @return Observable
+ */
+ updateCategory(categoryId: string, payload: CategoryBody): Observable {
+ return from(this.categoriesApi.updateCategory(categoryId, payload, {}));
+ }
+
+ /**
+ * Deletes category
+ *
+ * @param categoryId The identifier of a category.
+ * @return Observable
+ */
+ deleteCategory(categoryId: string): Observable {
+ return from(this.categoriesApi.deleteCategory(categoryId));
+ }
+}
diff --git a/lib/content-services/src/lib/content.module.ts b/lib/content-services/src/lib/content.module.ts
index ec72e84d11..1afd54bd92 100644
--- a/lib/content-services/src/lib/content.module.ts
+++ b/lib/content-services/src/lib/content.module.ts
@@ -46,6 +46,7 @@ import { versionCompatibilityFactory } from './version-compatibility/version-com
import { VersionCompatibilityService } from './version-compatibility/version-compatibility.service';
import { ContentPipeModule } from './pipes/content-pipe.module';
import { NodeCommentsModule } from './node-comments/node-comments.module';
+import { TreeModule } from './tree/tree.module';
import { SearchTextModule } from './search-text/search-text-input.module';
@NgModule({
@@ -77,6 +78,7 @@ import { SearchTextModule } from './search-text/search-text-input.module';
AspectListModule,
VersionCompatibilityModule,
NodeCommentsModule,
+ TreeModule,
SearchTextModule
],
providers: [
@@ -112,6 +114,7 @@ import { SearchTextModule } from './search-text/search-text-input.module';
ContentTypeModule,
VersionCompatibilityModule,
NodeCommentsModule,
+ TreeModule,
SearchTextModule
]
})
diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json
index ba996c3ef0..382dbd9301 100644
--- a/lib/content-services/src/lib/i18n/en.json
+++ b/lib/content-services/src/lib/i18n/en.json
@@ -465,6 +465,9 @@
"ARIA_LABEL": "Toggle {{ name }}"
}
},
+ "ADF-TREE": {
+ "LOAD-MORE-BUTTON": "Load more {{ name }}"
+ },
"LIBRARY": {
"DIALOG": {
"CREATE_TITLE": "Create Library",
diff --git a/lib/content-services/src/lib/tree/components/tree.component.html b/lib/content-services/src/lib/tree/components/tree.component.html
new file mode 100644
index 0000000000..22b8ad1414
--- /dev/null
+++ b/lib/content-services/src/lib/tree/components/tree.component.html
@@ -0,0 +1,117 @@
+
+
+