[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
This commit is contained in:
Nikita Maliarchuk
2023-04-20 13:28:16 +02:00
committed by GitHub
parent 10e6cf1d6f
commit 4fd1e3093f
5 changed files with 139 additions and 22 deletions

View File

@@ -13,32 +13,36 @@ Manages categories in Content Services.
### Methods ### Methods
- **createSubcategories**(parentCategoryId: `string`, payload: `CategoryBody[]`): [`Observable`](http://reactivex.io/documentation/observable.html)`<CategoryPaging|CategoryEntry>`<br/> - **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)`>`<br/>
Creates subcategories under category with provided categoryId Gets subcategories of a given parent category.
- _parentCategoryId:_ `string` - The identifier of a parent category. - _parentCategoryId:_ `string` - Identifier of a parent category
- _payload:_ `CategoryBody[]` - List of categories to be created. - _skipCount:_ `number` - Number of top categories to skip
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<CategoryPaging|CategoryEntry>` - [`Observable`](http://reactivex.io/documentation/observable.html)&lt;CategoryPaging | CategoryEntry> - _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)`>`<br/>
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)`>`<br/>
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)`>`<br/>
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)`<void>`<br/> - **deleteCategory**(categoryId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<void>`<br/>
Deletes category Deletes category
- _categoryId:_ `string` - The identifier of a category. - _categoryId:_ `string` - The identifier of a category.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<void>` - [`Observable`](http://reactivex.io/documentation/observable.html)&lt;void> - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<void>` - [`Observable`](http://reactivex.io/documentation/observable.html)&lt;void>
- **getSubcategories**(parentCategoryId: `string`, skipCount?: `number`, maxItems?: `number`): [`Observable`](http://reactivex.io/documentation/observable.html)`<CategoryPaging>`<br/>
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)`<CategoryPaging>` - [`Observable`](http://reactivex.io/documentation/observable.html)&lt;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)`>`<br/> - **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)`>`<br/>
Searches categories by their name. Searches categories by their name.
- _name:_ `string` - Value for name which should be used during searching categories. - _name:_ `string` - Value for name which should be used during searching categories.
- _skipCount:_ `number` - Specify how many first results should be skipped. Default 0. - _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). - _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)&lt;ResultSetPaging> Found categories which name contains searched name. - **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)&lt;ResultSetPaging> Found categories which name contains searched name.
- **updateCategory**(categoryId: `string`, payload: `CategoryBody`): [`Observable`](http://reactivex.io/documentation/observable.html)`<CategoryEntry>`<br/>
Updates category
- _categoryId:_ `string` - The identifier of a category.
- _payload:_ `CategoryBody` - Updated category body
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<CategoryEntry>` - [`Observable`](http://reactivex.io/documentation/observable.html)&lt;CategoryEntry>
## Details ## Details

View File

@@ -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(() => { it('should create subcategory', fakeAsync(() => {
const createSpy = spyOn(categoryService.categoriesApi, 'createSubcategories').and.returnValue(Promise.resolve(fakeCategoryEntry)); const createSpy = spyOn(categoryService.categoriesApi, 'createSubcategories').and.returnValue(Promise.resolve(fakeCategoryEntry));
categoryService.createSubcategories(fakeParentCategoryId, [fakeCategoryEntry.entry]).subscribe(() => { categoryService.createSubcategories(fakeParentCategoryId, [fakeCategoryEntry.entry]).subscribe(() => {

View File

@@ -57,6 +57,16 @@ export class CategoryService {
return from(this.categoriesApi.getSubcategories(parentCategoryId ?? '-root-', {skipCount, maxItems})); return from(this.categoriesApi.getSubcategories(parentCategoryId ?? '-root-', {skipCount, maxItems}));
} }
/**
* Get a category by ID
*
* @param categoryId The identifier of a category.
* @return Observable<CategoryEntry>
*/
getCategory(categoryId: string): Observable<CategoryEntry> {
return from(this.categoriesApi.getCategory(categoryId));
}
/** /**
* Creates subcategories under category with provided categoryId * Creates subcategories under category with provided categoryId
* *

View File

@@ -20,16 +20,27 @@ import { TestBed } from '@angular/core/testing';
import { SearchFacetFiltersService } from './search-facet-filters.service'; import { SearchFacetFiltersService } from './search-facet-filters.service';
import { ContentTestingModule } from '../../testing/content.testing.module'; import { ContentTestingModule } from '../../testing/content.testing.module';
import { SearchQueryBuilderService } from './search-query-builder.service'; 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', () => { describe('SearchFacetFiltersService', () => {
let searchFacetFiltersService: SearchFacetFiltersService; let searchFacetFiltersService: SearchFacetFiltersService;
let queryBuilder: SearchQueryBuilderService; let queryBuilder: SearchQueryBuilderService;
let categoryService: CategoryService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ContentTestingModule] imports: [ContentTestingModule],
providers: [{
provide: CategoryService,
useValue: {
getCategory: () => EMPTY,
searchCategories: () => EMPTY
}
}]
}); });
categoryService = TestBed.inject(CategoryService);
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService); searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
queryBuilder = TestBed.inject(SearchQueryBuilderService); queryBuilder = TestBed.inject(SearchQueryBuilderService);
}); });
@@ -460,6 +471,63 @@ describe('SearchFacetFiltersService', () => {
expect(searchFacetFiltersService.responseFacets.map(f => f.field)).toEqual(['field4', 'field1', 'Query 1', 'field2', 'field3']); 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', () => { describe('Bucket sorting', () => {
let data; let data;

View File

@@ -17,15 +17,16 @@
import { Inject, Injectable, OnDestroy } from '@angular/core'; import { Inject, Injectable, OnDestroy } from '@angular/core';
import { FacetBucketSortBy, FacetBucketSortDirection, FacetField } from '../models/facet-field.interface'; 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 { SEARCH_QUERY_SERVICE_TOKEN } from '../search-query-service.token';
import { SearchQueryBuilderService } from './search-query-builder.service'; import { SearchQueryBuilderService } from './search-query-builder.service';
import { TranslationService } from '@alfresco/adf-core'; import { TranslationService } from '@alfresco/adf-core';
import { SearchService } from '../services/search.service'; import { SearchService } from './search.service';
import { takeUntil } from 'rxjs/operators'; import { catchError, concatMap, takeUntil } from 'rxjs/operators';
import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api'; import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api';
import { SearchFilterList } from '../models/search-filter-list.model'; import { SearchFilterList } from '../models/search-filter-list.model';
import { FacetFieldBucket } from '../models/facet-field-bucket.interface'; import { FacetFieldBucket } from '../models/facet-field-bucket.interface';
import { CategoryService } from '../../category';
export interface SelectedBucket { export interface SelectedBucket {
field: FacetField; field: FacetField;
@@ -53,7 +54,9 @@ export class SearchFacetFiltersService implements OnDestroy {
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService, constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService,
private searchService: SearchService, private searchService: SearchService,
private translationService: TranslationService) { private translationService: TranslationService,
private categoryService: CategoryService
) {
if (queryBuilder.config && queryBuilder.config.facetQueries) { if (queryBuilder.config && queryBuilder.config.facetQueries) {
this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || DEFAULT_PAGE_SIZE; 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); this.sortFacetBuckets(responseBuckets, field.settings?.bucketSortBy, field.settings?.bucketSortDirection ?? FacetBucketSortDirection.ASCENDING);
const alreadyExistingField = this.findResponseFacet(itemType, field.label); const alreadyExistingField = this.findResponseFacet(itemType, field.label);
if (field.field === 'cm:categories'){
this.loadCategoryNames(responseBuckets);
}
if (alreadyExistingField) { if (alreadyExistingField) {
const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || []; 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) { unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
if (bucket) { if (bucket) {
bucket.checked = false; bucket.checked = false;