[ACS-4565] add search for categories tree in admin cc (#8279)

* ACS-4565 Added possibility to search categories

* ACS-4565 Fixed unit test

* ACS-4565 Fixed lint issue

* ACS-4565 Removed extra empty line

* ACS-4565 Replaced tags label with categories label in unit tests

* ACS-4565 Replaced tags label with categories label in doc
This commit is contained in:
AleksanderSklorz 2023-02-20 16:53:22 +01:00 committed by GitHub
parent 477d49eaee
commit 02dcd4fb48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 226 additions and 20 deletions

View File

@ -13,11 +13,12 @@ Datasource service for category tree.
### 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/>
- **getSubNodes**(parentNodeId: `string`, skipCount?: `number`, maxItems?: `number`, name?: `string`): [`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
- _name:_ `string` - Optional parameter which specifies if categories should be filtered out by name or not. If not specified then returns categories without filtering.
- **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

View File

@ -33,6 +33,12 @@ Manages categories in Content Services.
Deletes category.
- _categoryId:_ `string` - Identifier of a category
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<void>` - Null object when the operation completes
- **searchCategories**(name: `string`, skipCount: `number`, maxItems?: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/search-rest-api/docs/ResultSetPaging.md)`>`<br/>
Searches categories by their name.
- _name:_ `string` - Value for name which should be used during searching categories.
- _skipCount:_ `number` - Specify how many first results should be skipped. Default 0.
- _maxItems:_ `number` - Specify max number of returned categories. Default is specified by UserPreferencesService.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/search-rest-api/docs/ResultSetPaging.md)`>` - Found categories which name contains searched name.
## Details

View File

@ -18,7 +18,8 @@ Accesses the Content Services Search API.
- _term:_ `string` - Term to search for
- _options:_ [`SearchOptions`](lib/content-services/src/lib/search/services/search.service.ts) - (Optional) Options for delivery of the search results
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`NodePaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/content-rest-api/docs/NodePaging.md)`>` - List of nodes resulting from the search
- **search**(searchTerm: `string`, maxResults: `number`, skipCount: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/search-rest-api/docs/ResultSetPaging.md)`>`<br/>
- **search**(searchTerm: `string`, maxResults: `number`, skipCount: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`]
- `>`<br/>
Performs a search.
- _searchTerm:_ `string` - Term to search for
- _maxResults:_ `number` - Maximum number of items in the list of results

View File

@ -16,7 +16,13 @@
*/
import { Injectable } from '@angular/core';
import { CategoryEntry, CategoryPaging } from '@alfresco/js-api';
import {
CategoryEntry,
CategoryPaging, Pagination, PathInfo, ResultNode,
ResultSetPaging,
ResultSetPagingList,
ResultSetRowEntry
} from '@alfresco/js-api';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
@ -26,6 +32,29 @@ export class CategoryServiceMock {
return parentNodeId ? of(this.getChildrenLevelResponse(skipCount, maxItems)) : of(this.getRootLevelResponse(skipCount, maxItems));
}
public searchCategories(): Observable<ResultSetPaging> {
const result = new ResultSetPaging();
result.list = new ResultSetPagingList();
const category1 = new ResultSetRowEntry();
category1.entry = new ResultNode();
category1.entry.name = 'some name';
category1.entry.id = 'some id 1';
category1.entry.parentId = 'parent id 1';
category1.entry.path = new PathInfo();
category1.entry.path.name = '/categories/General';
const category2 = new ResultSetRowEntry();
category2.entry = new ResultNode();
category2.entry.name = 'some other name';
category2.entry.id = 'some id 2';
category2.entry.parentId = 'parent id 2';
category2.entry.path = new PathInfo();
category2.entry.path.name = '/categories/General/Language';
result.list.entries = [category1, category2];
result.list.pagination = new Pagination();
result.list.pagination.count = 2;
return of(result);
}
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]}};

View File

@ -18,13 +18,15 @@
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 { CategoryNode, CategoryTreeDatasourceService } from '@alfresco/adf-content-services';
import { CategoryServiceMock } from '../mock/category-mock.service';
import { TreeNodeType, TreeResponse } from '../../tree';
import { CategoryNode } from '../models/category-node.interface';
import { EMPTY } from 'rxjs';
import { Pagination } from '@alfresco/js-api';
describe('CategoryTreeDatasourceService', () => {
let categoryTreeDatasourceService: CategoryTreeDatasourceService;
let categoryService: CategoryService;
beforeEach(() => {
TestBed.configureTestingModule({
@ -37,6 +39,7 @@ describe('CategoryTreeDatasourceService', () => {
});
categoryTreeDatasourceService = TestBed.inject(CategoryTreeDatasourceService);
categoryService = TestBed.inject(CategoryService);
});
it('should get root level categories', fakeAsync(() => {
@ -69,4 +72,43 @@ describe('CategoryTreeDatasourceService', () => {
expect(treeResponse.entries[1].nodeType).toBe(TreeNodeType.LoadMoreNode);
});
}));
it('should call searchCategories on CategoryService if value of name parameter is defined', () => {
spyOn(categoryService, 'searchCategories').and.returnValue(EMPTY);
const skipCount = 10;
const maxItems = 100;
const name = 'name';
categoryTreeDatasourceService.getSubNodes('id', skipCount, maxItems, name);
expect(categoryService.searchCategories).toHaveBeenCalledWith(name, skipCount, maxItems);
});
it('should return observable which emits correct categories', (done) => {
categoryTreeDatasourceService.getSubNodes('id', undefined, undefined, 'name')
.subscribe((response) => {
const pagination = new Pagination();
pagination.count = 2;
expect(response).toEqual({
pagination,
entries: [{
id: 'some id 1',
nodeName: 'some name',
parentId: 'parent id 1',
level: 0,
nodeType: TreeNodeType.RegularNode,
hasChildren: false,
isLoading: false
}, {
id: 'some id 2',
nodeName: 'Language/some other name',
parentId: 'parent id 2',
level: 0,
nodeType: TreeNodeType.RegularNode,
hasChildren: false,
isLoading: false
}]
});
done();
});
});
});

View File

@ -16,9 +16,7 @@
*/
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 { TreeNodeType, TreeResponse, TreeService } from '../../tree';
import { CategoryNode } from '../models/category-node.interface';
import { CategoryService } from './category.service';
import { CategoryEntry, CategoryPaging } from '@alfresco/js-api';
@ -32,8 +30,8 @@ export class CategoryTreeDatasourceService extends TreeService<CategoryNode> {
super();
}
public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number): Observable<TreeResponse<CategoryNode>> {
return this.categoryService.getSubcategories(parentNodeId, skipCount, maxItems).pipe(map((response: CategoryPaging) => {
public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number, name?: string): Observable<TreeResponse<CategoryNode>> {
return !name ? 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 {
@ -60,6 +58,25 @@ export class CategoryTreeDatasourceService extends TreeService<CategoryNode> {
}
const treeResponse: TreeResponse<CategoryNode> = {entries: nodesList, pagination: response.list.pagination};
return treeResponse;
})) : this.categoryService.searchCategories(name, skipCount, maxItems).pipe(map((pagingResult) => {
const nextAfterGeneralPathPartIndex = 3;
const pathSeparator = '/';
return {
entries: pagingResult.list.entries.map((category) => {
const path = category.entry.path.name.split(pathSeparator).slice(nextAfterGeneralPathPartIndex)
.join(pathSeparator);
return {
id: category.entry.id,
nodeName: path ? `${path}/${category.entry.name}` : category.entry.name,
parentId: category.entry.parentId,
level: 0,
nodeType: TreeNodeType.RegularNode,
hasChildren: false,
isLoading: false
};
}),
pagination: pagingResult.list.pagination
};
}));
}
}

View File

@ -15,13 +15,22 @@
* limitations under the License.
*/
import { CoreTestingModule } from '@alfresco/adf-core';
import { CategoryBody, CategoryEntry, CategoryPaging } from '@alfresco/js-api';
import { CoreTestingModule, UserPreferencesService } from '@alfresco/adf-core';
import {
CategoryBody,
CategoryEntry,
CategoryPaging, PathInfo,
RequestQuery, ResultNode,
ResultSetPaging,
ResultSetPagingList, ResultSetRowEntry
} from '@alfresco/js-api';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { CategoryService } from './category.service';
describe('CategoryService', () => {
let categoryService: CategoryService;
let userPreferencesService: UserPreferencesService;
const fakeParentCategoryId = 'testParentId';
const fakeCategoriesResponse: CategoryPaging = { list: { pagination: {}, entries: [] }};
const fakeCategoryEntry: CategoryEntry = { entry: { id: 'testId', name: 'testName' }};
@ -35,6 +44,7 @@ describe('CategoryService', () => {
});
categoryService = TestBed.inject(CategoryService);
userPreferencesService = TestBed.inject(UserPreferencesService);
});
it('should fetch categories with provided parentId', fakeAsync(() => {
@ -71,4 +81,66 @@ describe('CategoryService', () => {
expect(deleteSpy).toHaveBeenCalledOnceWith(fakeParentCategoryId);
});
}));
describe('searchCategories', () => {
const defaultMaxItems = 25;
let result: ResultSetPaging;
beforeEach(() => {
spyOnProperty(userPreferencesService, 'paginationSize').and.returnValue(defaultMaxItems);
result = new ResultSetPaging();
result.list = new ResultSetPagingList();
const category = new ResultSetRowEntry();
category.entry = new ResultNode();
category.entry.name = 'some name';
category.entry.path = new PathInfo();
category.entry.path.name = '/categories/General';
result.list.entries = [category];
spyOn(categoryService.searchApi, 'search').and.returnValue(Promise.resolve(result));
});
it('should call search on searchApi with correct parameters when specified all parameters', () => {
const name = 'name';
const skipCount = 10;
const maxItems = 100;
categoryService.searchCategories(name, skipCount, maxItems);
expect(categoryService.searchApi.search).toHaveBeenCalledWith({
query: {
language: RequestQuery.LanguageEnum.Afts,
query: `cm:name:"*${name}*" AND TYPE:'cm:category' AND PATH:"/cm:categoryRoot/cm:generalclassifiable//*"`
},
paging: {
skipCount,
maxItems
},
include: ['path']
});
});
it('should call search on searchApi with default parameters when skipped optional parameters', () => {
const name = 'name';
categoryService.searchCategories(name);
expect(categoryService.searchApi.search).toHaveBeenCalledWith({
query: {
language: RequestQuery.LanguageEnum.Afts,
query: `cm:name:"*${name}*" AND TYPE:'cm:category' AND PATH:"/cm:categoryRoot/cm:generalclassifiable//*"`
},
paging: {
skipCount: 0,
maxItems: defaultMaxItems
},
include: ['path']
});
});
it('should return observable which emits paging object for categories', (done) => {
categoryService.searchCategories('name').subscribe((paging) => {
expect(paging).toBe(result);
done();
});
});
});
});

View File

@ -16,20 +16,34 @@
*/
import { Injectable } from '@angular/core';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { CategoriesApi, CategoryBody, CategoryEntry, CategoryPaging } from '@alfresco/js-api';
import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core';
import {
CategoriesApi,
CategoryBody,
CategoryEntry,
CategoryPaging,
RequestQuery,
ResultSetPaging,
SearchApi
} from '@alfresco/js-api';
import { from, Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CategoryService {
private _categoriesApi: CategoriesApi;
private _searchApi: SearchApi;
get categoriesApi(): CategoriesApi {
this._categoriesApi = this._categoriesApi ?? new CategoriesApi(this.apiService.getInstance());
return this._categoriesApi;
}
constructor(private apiService: AlfrescoApiService) {}
get searchApi(): SearchApi {
this._searchApi = this._searchApi ?? new SearchApi(this.apiService.getInstance());
return this._searchApi;
}
constructor(private apiService: AlfrescoApiService, private userPreferencesService: UserPreferencesService) {}
/**
* Get subcategories of a given parent category
@ -74,4 +88,27 @@ export class CategoryService {
deleteCategory(categoryId: string): Observable<void> {
return from(this.categoriesApi.deleteCategory(categoryId));
}
/**
* Searches categories by their name.
*
* @param name Value for name which should be used during searching categories.
* @param skipCount Specify how many first results should be skipped. Default 0.
* @param maxItems Specify max number of returned categories. Default is specified by UserPreferencesService.
* @return Observable<ResultSetPaging> Found categories which name contains searched name.
*/
searchCategories(name: string, skipCount = 0, maxItems?: number): Observable<ResultSetPaging> {
maxItems = maxItems || this.userPreferencesService.paginationSize;
return from(this.searchApi.search({
query: {
language: RequestQuery.LanguageEnum.Afts,
query: `cm:name:"*${name}*" AND TYPE:'cm:category' AND PATH:"/cm:categoryRoot/cm:generalclassifiable//*"`
},
paging: {
skipCount,
maxItems
},
include: ['path']
}));
}
}

View File

@ -156,9 +156,9 @@ describe('TreeComponent', () => {
it('should clear the selection and load root nodes on refresh', () => {
const selectionSpy = spyOn(component.treeNodesSelection, 'clear');
const getNodesSpy = spyOn(component.treeService, 'getSubNodes').and.callThrough();
component.refreshTree(0, 25);
component.refreshTree(0, 25, 'some term');
expect(selectionSpy).toHaveBeenCalled();
expect(getNodesSpy).toHaveBeenCalledWith('-root-', 0, 25);
expect(getNodesSpy).toHaveBeenCalledWith('-root-', 0, 25, 'some term');
});
it('should call correct server method on collapsing node', () => {

View File

@ -122,11 +122,12 @@ export class TreeComponent<T extends TreeNode> implements OnInit {
*
* @param skipCount Number of root nodes to skip.
* @param maxItems Maximum number of nodes returned from Observable.
* @param searchTerm Specifies if categories should be filtered out by name or not. If not specified then returns categories without filtering.
*/
public refreshTree(skipCount?: number, maxItems?: number): void {
public refreshTree(skipCount?: number, maxItems?: number, searchTerm?: string): void {
this.loadingRootSource.next(true);
this.treeNodesSelection.clear();
this.treeService.getSubNodes('-root-', skipCount, maxItems).subscribe((response: TreeResponse<T>) => {
this.treeService.getSubNodes('-root-', skipCount, maxItems, searchTerm).subscribe((response: TreeResponse<T>) => {
this.treeService.treeNodes = response.entries;
this.treeNodesSelection.deselect(...response.entries);
this.paginationChanged.emit(response.pagination);

View File

@ -42,7 +42,7 @@ export abstract class TreeService<T extends TreeNode> extends DataSource<T> {
this.treeNodes = [];
}
public abstract getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number): Observable<TreeResponse<T>>;
public abstract getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number, searchTerm?: string): Observable<TreeResponse<T>>;
/**
* Expands node applying subnodes to it.