diff --git a/docs/content-services/services/category-tree-datasource.service.md b/docs/content-services/services/category-tree-datasource.service.md index 1b6dd60adac..489858449ed 100644 --- a/docs/content-services/services/category-tree-datasource.service.md +++ b/docs/content-services/services/category-tree-datasource.service.md @@ -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`](../../../lib/content-services/src/lib/tree/models/tree-response.interface.ts)`>`
+- **getSubNodes**(parentNodeId: `string`, skipCount?: `number`, maxItems?: `number`, name?: `string`): [`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 + - _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`](../../../lib/content-services/src/lib/tree/models/tree-response.interface.ts)`>` - TreeResponse object containing pagination object and list on nodes ## Details diff --git a/docs/content-services/services/category.service.md b/docs/content-services/services/category.service.md index 4b00b98058c..333415d5b42 100644 --- a/docs/content-services/services/category.service.md +++ b/docs/content-services/services/category.service.md @@ -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)`` - 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)`>`
+ 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 diff --git a/docs/core/services/search.service.md b/docs/core/services/search.service.md index 6ed04563d0d..040439c729b 100644 --- a/docs/core/services/search.service.md +++ b/docs/core/services/search.service.md @@ -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)`>`
+- **search**(searchTerm: `string`, maxResults: `number`, skipCount: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ResultSetPaging`] +- `>`
Performs a search. - _searchTerm:_ `string` - Term to search for - _maxResults:_ `number` - Maximum number of items in the list of results 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 index d1649341eba..c78a1619d02 100644 --- a/lib/content-services/src/lib/category/mock/category-mock.service.ts +++ b/lib/content-services/src/lib/category/mock/category-mock.service.ts @@ -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 { + 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]}}; 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 index f264c0fa01b..289c8f973d4 100644 --- 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 @@ -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(); + }); + }); }); 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 index b7d00e45c9f..bf6d36e37a1 100644 --- 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 @@ -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 { super(); } - public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number): Observable> { - return this.categoryService.getSubcategories(parentNodeId, skipCount, maxItems).pipe(map((response: CategoryPaging) => { + public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number, name?: string): Observable> { + 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 { } const treeResponse: TreeResponse = {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 + }; })); } } 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 index 073f2346640..12f66313f99 100644 --- a/lib/content-services/src/lib/category/services/category.service.spec.ts +++ b/lib/content-services/src/lib/category/services/category.service.spec.ts @@ -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(); + }); + }); + }); }); diff --git a/lib/content-services/src/lib/category/services/category.service.ts b/lib/content-services/src/lib/category/services/category.service.ts index b6941d0acbe..bfb772d9399 100644 --- a/lib/content-services/src/lib/category/services/category.service.ts +++ b/lib/content-services/src/lib/category/services/category.service.ts @@ -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 { 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 Found categories which name contains searched name. + */ + searchCategories(name: string, skipCount = 0, maxItems?: number): Observable { + 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'] + })); + } } diff --git a/lib/content-services/src/lib/tree/components/tree.component.spec.ts b/lib/content-services/src/lib/tree/components/tree.component.spec.ts index 3af05e54a3b..e6e16c325ac 100644 --- a/lib/content-services/src/lib/tree/components/tree.component.spec.ts +++ b/lib/content-services/src/lib/tree/components/tree.component.spec.ts @@ -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', () => { diff --git a/lib/content-services/src/lib/tree/components/tree.component.ts b/lib/content-services/src/lib/tree/components/tree.component.ts index a71ee140036..4b4faf3a966 100644 --- a/lib/content-services/src/lib/tree/components/tree.component.ts +++ b/lib/content-services/src/lib/tree/components/tree.component.ts @@ -122,11 +122,12 @@ export class TreeComponent 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) => { + this.treeService.getSubNodes('-root-', skipCount, maxItems, searchTerm).subscribe((response: TreeResponse) => { this.treeService.treeNodes = response.entries; this.treeNodesSelection.deselect(...response.entries); this.paginationChanged.emit(response.pagination); diff --git a/lib/content-services/src/lib/tree/services/tree.service.ts b/lib/content-services/src/lib/tree/services/tree.service.ts index ac83026edda..4ec8aa0c739 100644 --- a/lib/content-services/src/lib/tree/services/tree.service.ts +++ b/lib/content-services/src/lib/tree/services/tree.service.ts @@ -42,7 +42,7 @@ export abstract class TreeService extends DataSource { this.treeNodes = []; } - public abstract getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number): Observable>; + public abstract getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number, searchTerm?: string): Observable>; /** * Expands node applying subnodes to it.