From 4fd1e3093fd53c68956c579fca713a1d25b28196 Mon Sep 17 00:00:00 2001 From: Nikita Maliarchuk <84377976+nikita-web-ua@users.noreply.github.com> Date: Thu, 20 Apr 2023 13:28:16 +0200 Subject: [PATCH] [ACS-4788] Add support for the Categories facet (#8470) * [ACS-4525] load category paths * [ACS-4525] fix for buckets to update properly * [ACS-4525] getCategory docs * [ACS-4525] unit tests * [ACS-4525] small fixes * [ACS-4525] bug fix * [ACS-4525] code improvements * [ACS-4525] alignment * [ACS-4525] linting --- .../services/category.service.md | 36 +++++----- .../services/category.service.spec.ts | 7 ++ .../lib/category/services/category.service.ts | 10 +++ .../search-facet-filters.service.spec.ts | 72 ++++++++++++++++++- .../services/search-facet-filters.service.ts | 36 ++++++++-- 5 files changed, 139 insertions(+), 22 deletions(-) diff --git a/docs/content-services/services/category.service.md b/docs/content-services/services/category.service.md index c2e871853d..402dcf6657 100644 --- a/docs/content-services/services/category.service.md +++ b/docs/content-services/services/category.service.md @@ -13,32 +13,36 @@ Manages categories in Content Services. ### Methods -- **createSubcategories**(parentCategoryId: `string`, payload: `CategoryBody[]`): [`Observable`](http://reactivex.io/documentation/observable.html)``
- Creates subcategories under category with provided categoryId - - _parentCategoryId:_ `string` - The identifier of a parent category. - - _payload:_ `CategoryBody[]` - List of categories to be created. - - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - [`Observable`](http://reactivex.io/documentation/observable.html)<CategoryPaging | CategoryEntry> +- **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 +- **getCategory**(categoryId: `string`): [`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)`>`
+ Gets a specific category by categoryId. + - _categoryId:_ `string` - The identifier of a category + - **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 information about the category. +- **createSubcategories**(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)`<`[`CategoryPaging`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryPaging.md) | [`CategoryEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/api/content-rest-api/docs/CategoryEntry.md)`>`
+ Creates subcategories 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) - List of categories to be created + - **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) | [`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` - The identifier of a category. - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - [`Observable`](http://reactivex.io/documentation/observable.html)<void> -- **getSubcategories**(parentCategoryId: `string`, skipCount?: `number`, maxItems?: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)``
- Get subcategories of a given parent category - - _parentCategoryId:_ `string` - The identifier of a parent category. - - _skipCount:_ `number` - (Optional) Number of top categories to skip. - - _maxItems:_ `number` - (Optional) Maximum number of subcategories returned from [Observable](http://reactivex.io/documentation/observable.html). - - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - [`Observable`](http://reactivex.io/documentation/observable.html)<CategoryPaging> - **searchCategories**(name: `string`, skipCount: `number` = `0`, 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` - (Optional) Specify max number of returned categories. Default is specified by [UserPreferencesService](../../core/services/user-preferences.service.md). - **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)`>` - [`Observable`](http://reactivex.io/documentation/observable.html)<ResultSetPaging> Found categories which name contains searched name. -- **updateCategory**(categoryId: `string`, payload: `CategoryBody`): [`Observable`](http://reactivex.io/documentation/observable.html)``
- Updates category - - _categoryId:_ `string` - The identifier of a category. - - _payload:_ `CategoryBody` - Updated category body - - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - [`Observable`](http://reactivex.io/documentation/observable.html)<CategoryEntry> ## Details 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 78eb6380e2..73cd1425fb 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 @@ -61,6 +61,13 @@ describe('CategoryService', () => { }); })); + it('should fetch the category with the provided categoryId', fakeAsync(() => { + const getSpy = spyOn(categoryService.categoriesApi, 'getCategory').and.returnValue(Promise.resolve(fakeCategoryEntry)); + categoryService.getCategory(fakeParentCategoryId).subscribe(() => { + expect(getSpy).toHaveBeenCalledOnceWith(fakeParentCategoryId); + }); + })); + it('should create subcategory', fakeAsync(() => { const createSpy = spyOn(categoryService.categoriesApi, 'createSubcategories').and.returnValue(Promise.resolve(fakeCategoryEntry)); categoryService.createSubcategories(fakeParentCategoryId, [fakeCategoryEntry.entry]).subscribe(() => { 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 855a378b8a..3512511b01 100644 --- a/lib/content-services/src/lib/category/services/category.service.ts +++ b/lib/content-services/src/lib/category/services/category.service.ts @@ -57,6 +57,16 @@ export class CategoryService { return from(this.categoriesApi.getSubcategories(parentCategoryId ?? '-root-', {skipCount, maxItems})); } + /** + * Get a category by ID + * + * @param categoryId The identifier of a category. + * @return Observable + */ + getCategory(categoryId: string): Observable { + return from(this.categoriesApi.getCategory(categoryId)); + } + /** * Creates subcategories under category with provided categoryId * diff --git a/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts b/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts index e30f701535..e415b8cad5 100644 --- a/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts +++ b/lib/content-services/src/lib/search/services/search-facet-filters.service.spec.ts @@ -20,16 +20,27 @@ import { TestBed } from '@angular/core/testing'; import { SearchFacetFiltersService } from './search-facet-filters.service'; import { ContentTestingModule } from '../../testing/content.testing.module'; import { SearchQueryBuilderService } from './search-query-builder.service'; -import { FacetBucketSortBy, FacetBucketSortDirection } from '@alfresco/adf-content-services'; +import { CategoryService, FacetBucketSortBy, FacetBucketSortDirection } from '@alfresco/adf-content-services'; +import { EMPTY, of } from 'rxjs'; describe('SearchFacetFiltersService', () => { let searchFacetFiltersService: SearchFacetFiltersService; let queryBuilder: SearchQueryBuilderService; + let categoryService: CategoryService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ContentTestingModule] + imports: [ContentTestingModule], + providers: [{ + provide: CategoryService, + useValue: { + getCategory: () => EMPTY, + searchCategories: () => EMPTY + } + }] }); + + categoryService = TestBed.inject(CategoryService); searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService); queryBuilder = TestBed.inject(SearchQueryBuilderService); }); @@ -460,6 +471,63 @@ describe('SearchFacetFiltersService', () => { expect(searchFacetFiltersService.responseFacets.map(f => f.field)).toEqual(['field4', 'field1', 'Query 1', 'field2', 'field3']); }); + it('should load category names for cm:categories facet', () => { + const entry = {id: 'test-id-test', name: 'name'}; + searchFacetFiltersService.responseFacets = null; + spyOn(categoryService, 'getCategory').and.returnValue(of({entry})); + spyOn(categoryService, 'searchCategories').and.returnValue(of({ + list: { + entries: [{ + entry: { + ...entry, + nodeType: 'node-type', + path: { name: '/categories/General/Test Category/Subcategory'}, + isFolder: false, + isFile: false + } + }] + } + })); + + queryBuilder.config = { + categories: [], + facetFields: { + fields: [ + {label: 'f1', field: 'f1', mincount: 0}, + {label: 'categories', field: 'cm:categories', mincount: 0} + ] + }, + facetQueries: { + queries: [] + } + }; + + const fields: any = [ + {type: 'field', label: 'f1', buckets: [{label: 'a1'}, {label: 'a2'}]}, + { + type: 'field', label: 'categories', buckets: [{ + label: `workspace://SpacesStore/${entry.id}`, + filterQuery: `cm:categories:"workspace://SpacesStore/${entry.id}"` + }] + } + ]; + let data = { + list: { + context: { + facets: fields + } + } + }; + + searchFacetFiltersService.onDataLoaded(data); + + expect(categoryService.getCategory).toHaveBeenCalledWith(entry.id); + expect(categoryService.searchCategories).toHaveBeenCalledWith(entry.name); + expect(searchFacetFiltersService.responseFacets[1].buckets.items[0].display).toBe(`Test Category/Subcategory/${entry.name}`); + expect(searchFacetFiltersService.responseFacets[1].buckets.length).toEqual(1); + expect(searchFacetFiltersService.responseFacets.length).toEqual(2); + }); + describe('Bucket sorting', () => { let data; diff --git a/lib/content-services/src/lib/search/services/search-facet-filters.service.ts b/lib/content-services/src/lib/search/services/search-facet-filters.service.ts index 632437896a..0b1e0fea86 100644 --- a/lib/content-services/src/lib/search/services/search-facet-filters.service.ts +++ b/lib/content-services/src/lib/search/services/search-facet-filters.service.ts @@ -17,15 +17,16 @@ import { Inject, Injectable, OnDestroy } from '@angular/core'; import { FacetBucketSortBy, FacetBucketSortDirection, FacetField } from '../models/facet-field.interface'; -import { Subject } from 'rxjs'; +import { Subject, throwError } from 'rxjs'; import { SEARCH_QUERY_SERVICE_TOKEN } from '../search-query-service.token'; import { SearchQueryBuilderService } from './search-query-builder.service'; import { TranslationService } from '@alfresco/adf-core'; -import { SearchService } from '../services/search.service'; -import { takeUntil } from 'rxjs/operators'; +import { SearchService } from './search.service'; +import { catchError, concatMap, takeUntil } from 'rxjs/operators'; import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api'; import { SearchFilterList } from '../models/search-filter-list.model'; import { FacetFieldBucket } from '../models/facet-field-bucket.interface'; +import { CategoryService } from '../../category'; export interface SelectedBucket { field: FacetField; @@ -53,7 +54,9 @@ export class SearchFacetFiltersService implements OnDestroy { constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService, private searchService: SearchService, - private translationService: TranslationService) { + private translationService: TranslationService, + private categoryService: CategoryService + ) { if (queryBuilder.config && queryBuilder.config.facetQueries) { this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || DEFAULT_PAGE_SIZE; } @@ -102,6 +105,10 @@ export class SearchFacetFiltersService implements OnDestroy { this.sortFacetBuckets(responseBuckets, field.settings?.bucketSortBy, field.settings?.bucketSortDirection ?? FacetBucketSortDirection.ASCENDING); const alreadyExistingField = this.findResponseFacet(itemType, field.label); + if (field.field === 'cm:categories'){ + this.loadCategoryNames(responseBuckets); + } + if (alreadyExistingField) { const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || []; @@ -335,6 +342,27 @@ export class SearchFacetFiltersService implements OnDestroy { }; } + private loadCategoryNames(bucketList: FacetFieldBucket[]) { + bucketList.forEach((item) => { + const categoryId = item.label.split('/').pop(); + this.categoryService.getCategory(categoryId) + .pipe( + concatMap((categoryEntry) => this.categoryService.searchCategories(categoryEntry.entry.name)), + catchError(error => throwError(error)) + ) + .subscribe( + result => { + const nextAfterGeneralPathPartIndex = 3; + const pathSeparator = '/'; + const currentCat = result.list.entries.filter(entry => entry.entry.id === categoryId)[0]; + const path = currentCat.entry.path.name.split(pathSeparator).slice(nextAfterGeneralPathPartIndex).join('/'); + + item.display = path ? `${path}/${currentCat.entry.name}` : currentCat.entry.name; + } + ); + }); + } + unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) { if (bucket) { bucket.checked = false;