[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

@@ -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(() => {

View File

@@ -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<CategoryEntry>
*/
getCategory(categoryId: string): Observable<CategoryEntry> {
return from(this.categoriesApi.getCategory(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 { 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;

View File

@@ -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;