Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to display search results as a tree?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does not display it as tree in reality. Please check PR for applications, there is screenshot. It is tree with 0 level so in reality it will be always flat list. I did not see sense to use separated component just to render flat list if I can have tree list with 0 level which is still flat list :)

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
Expand Down
6 changes: 6 additions & 0 deletions docs/content-services/services/category.service.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion docs/core/services/search.service.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand All @@ -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]}};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -37,6 +39,7 @@ describe('CategoryTreeDatasourceService', () => {
});

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

it('should get root level categories', fakeAsync(() => {
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -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
};
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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' }};
Expand All @@ -35,6 +44,7 @@ describe('CategoryService', () => {
});

categoryService = TestBed.inject(CategoryService);
userPreferencesService = TestBed.inject(UserPreferencesService);
});

it('should fetch categories with provided parentId', fakeAsync(() => {
Expand Down Expand Up @@ -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();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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']
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading