[MNT-25410] ADW - Saved Searches can load the wrong search set, and t… (#11599)

This commit is contained in:
Dominik Iwanek
2026-02-09 08:03:39 +01:00
committed by GitHub
parent 31bafd718e
commit dcf133b37e
11 changed files with 736 additions and 50 deletions
@@ -53,6 +53,7 @@ describe('SearchFilterChipsComponent', () => {
it('should fetch facet fields from response payload and show the already checked items', async () => {
spyOn(queryBuilder, 'execute').and.stub();
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -115,6 +116,7 @@ describe('SearchFilterChipsComponent', () => {
it('should fetch facet fields from response payload and show the newly checked items', async () => {
spyOn(queryBuilder, 'execute').and.stub();
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -176,6 +178,7 @@ describe('SearchFilterChipsComponent', () => {
it('should show buckets with 0 values when there are no facet fields on the response payload', async () => {
spyOn(queryBuilder, 'execute').and.stub();
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -253,7 +256,7 @@ describe('SearchFilterChipsComponent', () => {
describe('widgets', () => {
it('should not show the disabled widget', async () => {
appConfigService.config.search = { categories: disabledCategories };
appConfigService.config.search = { id: 'test-config', categories: disabledCategories };
queryBuilder.resetToDefaults();
const chips = await loader.getAllHarnesses(MatChipHarness);
@@ -262,7 +265,7 @@ describe('SearchFilterChipsComponent', () => {
});
it('should show the widgets only if configured', async () => {
appConfigService.config.search = { categories: simpleCategories };
appConfigService.config.search = { id: 'test-config', categories: simpleCategories };
queryBuilder.resetToDefaults();
const chips = await loader.getAllHarnesses(MatChipHarness);
@@ -80,6 +80,7 @@ describe('SearchFilterComponent', () => {
it('should fetch facet fields from response payload and show the already checked items', () => {
spyOn(queryBuilder, 'execute').and.stub();
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -138,6 +139,7 @@ describe('SearchFilterComponent', () => {
it('should fetch facet fields from response payload and show the newly checked items', () => {
spyOn(queryBuilder, 'execute').and.stub();
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -196,6 +198,7 @@ describe('SearchFilterComponent', () => {
it('should show buckets with 0 values when there are no facet fields on the response payload', () => {
spyOn(queryBuilder, 'execute').and.stub();
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -278,7 +281,7 @@ describe('SearchFilterComponent', () => {
});
it('should not show the disabled widget', async () => {
appConfigService.config.search = { categories: disabledCategories };
appConfigService.config.search = { id: 'test-config', categories: disabledCategories };
queryBuilder.resetToDefaults();
fixture.detectChanges();
@@ -289,7 +292,7 @@ describe('SearchFilterComponent', () => {
});
it('should show the widget in expanded mode', async () => {
appConfigService.config.search = { categories: expandedCategories };
appConfigService.config.search = { id: 'test-config', categories: expandedCategories };
queryBuilder.resetToDefaults();
fixture.detectChanges();
@@ -303,7 +306,7 @@ describe('SearchFilterComponent', () => {
});
it('should show the widgets only if configured', async () => {
appConfigService.config.search = { categories: simpleCategories };
appConfigService.config.search = { id: 'test-config', categories: simpleCategories };
queryBuilder.resetToDefaults();
fixture.detectChanges();
@@ -32,9 +32,9 @@ describe('SearchFormComponent', () => {
let component: SearchFormComponent;
let queryBuilder: SearchQueryBuilderService;
const mockSearchForms: SearchForm[] = [
{ default: false, index: 0, name: 'All', selected: false },
{ default: true, index: 1, name: 'First', selected: true },
{ default: false, index: 2, name: 'Second', selected: false }
{ id: 'form-all', default: false, index: 0, name: 'All', selected: false },
{ id: 'form-first', default: true, index: 1, name: 'First', selected: true },
{ id: 'form-second', default: false, index: 2, name: 'Second', selected: false }
];
beforeEach(() => {
@@ -75,7 +75,7 @@ describe('SearchFormComponent', () => {
});
it('should not show menu if only one config found', async () => {
queryBuilder.searchForms.next([{ name: 'one', selected: true, default: true, index: 0 }]);
queryBuilder.searchForms.next([{ id: 'form-one', name: 'one', selected: true, default: true, index: 0 }]);
fixture.detectChanges();
const button = await loader.getHarness(MatButtonHarness.with({ selector: '.adf-search-form' }));
@@ -41,7 +41,7 @@ export class SearchFormComponent {
formChange: EventEmitter<SearchForm> = new EventEmitter<SearchForm>();
onSelectionChange(form: SearchForm) {
this.queryBuilder.updateSelectedConfiguration(form.index);
this.queryBuilder.updateSelectedConfiguration(form.id);
this.formChange.emit(form);
}
@@ -23,6 +23,7 @@ import { SearchSortingDefinition } from './search-sorting-definition.interface';
import { RequestHighlight } from '@alfresco/js-api';
export interface SearchConfiguration {
id: string;
include?: string[];
fields?: string[];
categories?: SearchCategory[];
@@ -16,6 +16,7 @@
*/
export interface SearchForm {
id: string;
index: number;
name: string;
default: boolean;
@@ -0,0 +1,583 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { provideRouter, Router } from '@angular/router';
import { SearchQueryBuilderService } from './search-query-builder.service';
import { AppConfigService } from '@alfresco/adf-core';
import { SearchConfiguration } from '../models/search-configuration.interface';
import { FacetFieldBucket } from '../models/facet-field-bucket.interface';
import { ResultSetPaging } from '@alfresco/js-api';
import { skip } from 'rxjs/operators';
describe('BaseQueryBuilderService', () => {
let service: SearchQueryBuilderService;
let appConfig: AppConfigService;
let router: Router;
const mockSearchConfig: SearchConfiguration = {
id: 'config-default',
categories: [
{
id: 'cat1',
name: 'Category 1',
enabled: true,
expanded: false,
component: {
selector: 'test',
settings: undefined
}
},
{
id: 'cat2',
name: 'Category 2',
enabled: false,
expanded: false,
component: {
selector: 'test',
settings: undefined
}
}
],
filterQueries: [{ query: 'TYPE:"cm:content"' }],
sorting: {
options: [
{ key: 'name', label: 'Name', type: 'FIELD', field: 'cm:name', ascending: true },
{ key: 'date', label: 'Date', type: 'FIELD', field: 'cm:created', ascending: false }
],
defaults: [{ key: 'name', label: 'Name', type: 'FIELD', field: 'cm:name', ascending: true }]
}
};
const mockMultipleConfigs: SearchConfiguration[] = [
{ ...mockSearchConfig, id: 'config-1', name: 'Config 1', default: true },
{ ...mockSearchConfig, id: 'config-2', name: 'Config 2', default: false },
{ ...mockSearchConfig, id: 'config-3', name: 'Config 3', default: false }
];
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideRouter([]),
SearchQueryBuilderService,
{
provide: AppConfigService,
useValue: {
get: jasmine.createSpy('get').and.returnValue(mockSearchConfig)
}
}
]
});
service = TestBed.inject(SearchQueryBuilderService);
appConfig = TestBed.inject(AppConfigService);
router = TestBed.inject(Router);
});
describe('userQuery', () => {
it('should set userQuery with parentheses', () => {
service.userQuery = 'test query';
expect(service.userQuery).toBe('(test query)');
});
it('should trim userQuery', () => {
service.userQuery = ' test query ';
expect(service.userQuery).toBe('(test query)');
});
it('should return empty string for null input', () => {
service.userQuery = null;
expect(service.userQuery).toBe('');
});
});
describe('queryFragments', () => {
it('should emit queryFragmentsUpdate when queryFragments change after initial fragments emited', (done) => {
service.queryFragmentsUpdate.pipe(skip(1)).subscribe((fragments) => {
expect(fragments['testId']).toBe('test query');
done();
});
service.queryFragments['testId'] = 'test query';
});
it('should emit queryFragmentsUpdate when setting new queryFragments object after initial fragments emited', (done) => {
const newFragments = { id1: 'query1', id2: 'query2' };
service.queryFragmentsUpdate.pipe(skip(1)).subscribe((fragments) => {
expect(fragments).toEqual(newFragments);
done();
});
service.queryFragments = newFragments;
});
});
describe('filterQueries', () => {
it('should add filter query', () => {
service.filterQueries = [];
service.addFilterQuery('TYPE:"cm:folder"');
expect(service.filterQueries.length).toBe(1);
expect(service.filterQueries[0].query).toBe('TYPE:"cm:folder"');
});
it('should not add duplicate filter query', () => {
service.filterQueries = [];
service.addFilterQuery('TYPE:"cm:folder"');
service.addFilterQuery('TYPE:"cm:folder"');
expect(service.filterQueries.length).toBe(1);
});
it('should remove filter query', () => {
service.filterQueries = [{ query: 'TYPE:"cm:folder"' }, { query: 'TYPE:"cm:content"' }];
service.removeFilterQuery('TYPE:"cm:folder"');
expect(service.filterQueries.length).toBe(1);
expect(service.filterQueries[0].query).toBe('TYPE:"cm:content"');
});
it('should not add empty filter query', () => {
service.filterQueries = [];
service.addFilterQuery('');
expect(service.filterQueries.length).toBe(0);
});
});
describe('userFacetBuckets', () => {
const mockBucket: FacetFieldBucket = {
label: 'bucket1',
count: 10,
filterQuery: 'field:value1'
};
const mockBucket2: FacetFieldBucket = {
label: 'bucket2',
count: 5,
filterQuery: 'field:value2'
};
it('should add user facet bucket', () => {
service.addUserFacetBucket('field1', mockBucket);
const buckets = service.getUserFacetBuckets('field1');
expect(buckets.length).toBe(1);
expect(buckets[0].label).toBe('bucket1');
});
it('should not add duplicate bucket', () => {
service.addUserFacetBucket('field1', mockBucket);
service.addUserFacetBucket('field1', mockBucket);
const buckets = service.getUserFacetBuckets('field1');
expect(buckets.length).toBe(1);
});
it('should remove user facet bucket', () => {
service.addUserFacetBucket('field1', mockBucket);
service.addUserFacetBucket('field1', mockBucket2);
service.removeUserFacetBucket('field1', mockBucket);
const buckets = service.getUserFacetBuckets('field1');
expect(buckets.length).toBe(1);
expect(buckets[0].label).toBe('bucket2');
});
it('should reset user facet buckets', () => {
service.addUserFacetBucket('field1', mockBucket);
service.addUserFacetBucket('field2', mockBucket2);
service.resetUserFacetBucket();
expect(service.getUserFacetBuckets('field1').length).toBe(0);
expect(service.getUserFacetBuckets('field2').length).toBe(0);
});
});
describe('buildQuery', () => {
it('should return null when no query is set', () => {
service.userQuery = '';
expect(service.buildQuery()).toBeNull();
});
it('should build query with userQuery', () => {
service.userQuery = 'test';
const query = service.buildQuery();
expect(query).toBeTruthy();
expect(query.query.query).toBe('(test)');
});
it('should include scope in query when set', () => {
service.userQuery = 'test';
service.setScope({ locations: 'nodes' });
const query = service.buildQuery();
expect(query.scope).toEqual({ locations: 'nodes' });
});
it('should include default includes when none configured', () => {
service.config = { id: 'test-config', categories: [] };
service.userQuery = 'test';
const query = service.buildQuery();
expect(query.include).toContain('path');
expect(query.include).toContain('allowableOperations');
});
});
describe('update', () => {
it('should emit updated event with built query', (done) => {
service.userQuery = 'test';
service.updated.subscribe((query) => {
expect(query.query.query).toBe('(test)');
done();
});
service.update();
});
it('should emit updated event with provided query body', (done) => {
const customQuery = {
query: { query: 'custom query', language: 'afts' }
};
service.updated.subscribe((query) => {
expect(query.query.query).toBe('custom query');
done();
});
service.update(customQuery);
});
});
describe('execute', () => {
it('should update search query params by default', async () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.userQuery = 'test';
await service.execute();
expect(router.navigate).toHaveBeenCalled();
});
it('should not update search query params when disabled', async () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.userQuery = 'test';
await service.execute(false);
expect(router.navigate).not.toHaveBeenCalled();
});
it('should emit executed event on success', async () => {
const mockResult: ResultSetPaging = {
list: {
entries: [
{
entry: {
id: '1',
name: '',
nodeType: '',
isFolder: false,
isFile: false
}
}
]
}
};
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve(mockResult));
service.userQuery = 'test';
const executedSpy = jasmine.createSpy('executedSpy');
service.executed.subscribe(executedSpy);
await service.execute();
expect(executedSpy).toHaveBeenCalledWith(mockResult);
});
it('should emit error and empty result on failure', async () => {
const mockError = new Error('Search failed');
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.reject(mockError));
service.userQuery = 'test';
const errorSpy = jasmine.createSpy('errorSpy');
const executedSpy = jasmine.createSpy('executedSpy');
service.error.subscribe(errorSpy);
service.executed.subscribe(executedSpy);
await service.execute();
expect(errorSpy).toHaveBeenCalledWith(mockError);
expect(executedSpy).toHaveBeenCalledWith(
jasmine.objectContaining({
list: jasmine.objectContaining({
pagination: { totalItems: 0 },
entries: []
})
})
);
});
it('should update URL params even when query is null', async () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
service.userQuery = '';
await service.execute();
expect(router.navigate).toHaveBeenCalled();
});
});
describe('sorting', () => {
it('should return primary sorting', () => {
service.sorting = [
{ key: 'name', label: 'Name', type: 'FIELD', field: 'cm:name', ascending: true },
{ key: 'date', label: 'Date', type: 'FIELD', field: 'cm:created', ascending: false }
];
const primary = service.getPrimarySorting();
expect(primary.key).toBe('name');
});
it('should return null when no sorting defined', () => {
service.sorting = [];
expect(service.getPrimarySorting()).toBeNull();
});
it('should return sorting options from config', () => {
service.config = {
id: 'test-config',
categories: [],
sorting: {
options: [{ key: 'name', label: 'Name', type: 'FIELD', field: 'cm:name', ascending: true }],
defaults: []
}
};
const options = service.getSortingOptions();
expect(options.length).toBe(1);
expect(options[0].key).toBe('name');
});
});
describe('multiple configurations', () => {
beforeEach(() => {
(appConfig.get as jasmine.Spy).and.returnValue(mockMultipleConfigs);
service.resetToDefaults();
});
it('should return default configuration', () => {
const config = service.getDefaultConfiguration();
expect(config.name).toBe('Config 1');
});
it('should return search form details for multiple configurations', () => {
const forms = service.getSearchFormDetails();
expect(forms.length).toBe(3);
expect(forms[0].id).toBe('config-1');
expect(forms[0].name).toBe('Config 1');
expect(forms[0].default).toBe(true);
expect(forms[0].selected).toBe(true);
expect(forms[1].id).toBe('config-2');
expect(forms[1].name).toBe('Config 2');
expect(forms[1].selected).toBe(false);
});
it('should update selected configuration', (done) => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.configUpdated.subscribe((config) => {
expect(config.name).toBe('Config 2');
done();
});
service.updateSelectedConfiguration('config-2');
});
it('should update searchForms when configuration changes after initial forms emited', (done) => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
let callCount = 0;
service.searchForms.pipe(skip(1)).subscribe((forms) => {
callCount++;
expect(forms[1].selected).toBe(true);
expect(forms[0].selected).toBe(false);
done();
});
service.updateSelectedConfiguration('config-2');
});
it('should store selectedConfigurationId in filterRawParams', async () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.updateSelectedConfiguration('config-2');
expect(service.filterRawParams['selectedConfigurationId']).toBe('config-2');
});
it('should call execute when updating configuration', async () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.userQuery = 'test';
service.updateSelectedConfiguration('config-2');
expect(router.navigate).toHaveBeenCalled();
});
});
describe('populateFilters and selectedConfiguration restoration', () => {
beforeEach(() => {
(appConfig.get as jasmine.Spy<<T>(key: string, defaultValue?: T) => T>).and.returnValue(mockMultipleConfigs);
service.resetToDefaults();
});
it('should restore selectedConfiguration from populateFilters', (done) => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
service.configUpdated.subscribe((config) => {
expect(config.name).toBe('Config 3');
done();
});
service.populateFilters.next({ selectedConfigurationId: 'config-3', someOtherFilter: 'value' });
});
it('should reset to default configuration when populateFilters has no selectedConfigurationId', (done) => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.updateSelectedConfiguration('config-3');
setTimeout(() => {
service.configUpdated.subscribe((config) => {
expect(config.name).toBe('Config 1');
done();
});
service.populateFilters.next({ someOtherFilter: 'value' });
}, 0);
});
it('should not change configuration when populateFilters is empty', () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
const configUpdatedSpy = jasmine.createSpy('configUpdatedSpy');
service.configUpdated.subscribe(configUpdatedSpy);
configUpdatedSpy.calls.reset();
service.populateFilters.next({});
expect(configUpdatedSpy).not.toHaveBeenCalled();
});
it('should not change configuration when same configuration is already selected', (done) => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
spyOn(service.searchApi, 'search').and.returnValue(Promise.resolve({ list: { entries: [] } } as ResultSetPaging));
service.updateSelectedConfiguration('config-2');
setTimeout(() => {
const configUpdatedSpy = jasmine.createSpy('configUpdatedSpy');
service.configUpdated.subscribe(configUpdatedSpy);
service.populateFilters.next({ selectedConfigurationId: 'config-2' });
setTimeout(() => {
expect(configUpdatedSpy).not.toHaveBeenCalled();
done();
}, 0);
}, 0);
});
it('should update filterRawParams when restoring configuration from populateFilters', (done) => {
service.configUpdated.subscribe(() => {
expect(service.filterRawParams['selectedConfigurationId']).toBe('config-2');
done();
});
service.populateFilters.next({ selectedConfigurationId: 'config-2' });
});
});
describe('resetToDefaults', () => {
it('should emit configUpdated', (done) => {
service.configUpdated.subscribe((config) => {
expect(config).toBeTruthy();
done();
});
service.resetToDefaults();
});
it('should emit searchForms', () => {
let formsCalled = false;
service.searchForms.subscribe((forms) => {
if (!formsCalled) {
formsCalled = true;
expect(forms).toBeTruthy();
}
});
service.resetToDefaults();
expect(formsCalled).toBe(true);
});
it('should navigate when withNavigate is true', () => {
spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
service.resetToDefaults(true);
expect(router.navigate).toHaveBeenCalledWith([], jasmine.objectContaining({ queryParams: { q: null } }));
});
it('should reset categories', () => {
service.categories = [
{
id: 'test',
name: 'Test',
enabled: true,
expanded: false,
component: {
selector: 'test',
settings: undefined
}
}
];
service.resetToDefaults();
expect(service.categories.length).toBe(1);
expect(service.categories[0].id).toBe('cat1');
});
});
});
@@ -88,10 +88,11 @@ export abstract class BaseQueryBuilderService {
private encodedQuery: string;
private scope: RequestScope;
private selectedConfiguration: number;
private selectedConfigurationId: string;
private _userQuery = '';
private _queryFragments: { [id: string]: string } = {};
private readonly selectedConfigurationKey = 'selectedConfigurationId';
private readonly queryFragmentsHandler: ProxyHandler<{ [key: string]: any }> = {
set: (target: { [key: string]: any }, property: string, value: any) => {
target[property as keyof typeof target] = value;
@@ -121,7 +122,8 @@ export abstract class BaseQueryBuilderService {
}
config: SearchConfiguration = {
categories: []
categories: [],
id: 'SEARCH.UNKNOWN_CONFIGURATION'
};
// TODO: to be supported in future iterations
@@ -133,6 +135,8 @@ export abstract class BaseQueryBuilderService {
) {
this.resetToDefaults();
this._queryFragments = this.createQueryFragmentsProxy({});
this.populateFilters.subscribe((filters) => this.handleSelectedConfigurationChange(filters));
}
public abstract loadConfiguration(): SearchConfiguration | SearchConfiguration[];
@@ -158,8 +162,8 @@ export abstract class BaseQueryBuilderService {
const configurations = this.loadConfiguration();
if (Array.isArray(configurations)) {
if (this.selectedConfiguration !== undefined) {
return configurations[this.selectedConfiguration];
if (this.selectedConfigurationId !== undefined) {
return configurations.find((config) => config.id === this.selectedConfigurationId);
}
return configurations.find((configuration) => configuration.default);
@@ -167,15 +171,19 @@ export abstract class BaseQueryBuilderService {
return configurations;
}
public updateSelectedConfiguration(index: number): void {
public updateSelectedConfiguration(id: string): void {
const currentConfig = this.loadConfiguration();
if (Array.isArray(currentConfig) && currentConfig[index] !== undefined) {
this.selectedConfiguration = index;
this.searchForms.next(this.getSearchFormDetails());
this.resetSearchOptions();
this.setUpSearchConfiguration(currentConfig[index]);
this.configUpdated.next(currentConfig[index]);
this.update();
if (Array.isArray(currentConfig)) {
const selectedConfig = currentConfig.find((config) => config.id === id);
if (selectedConfig) {
this.selectedConfigurationId = id;
this.searchForms.next(this.getSearchFormDetails());
this.resetSearchOptions();
this.setUpSearchConfiguration(selectedConfig);
this.filterRawParams[this.selectedConfigurationKey] = id;
this.configUpdated.next(selectedConfig);
this.execute();
}
}
}
@@ -196,14 +204,16 @@ export abstract class BaseQueryBuilderService {
const configurations = this.loadConfiguration();
if (Array.isArray(configurations)) {
return configurations.map((configuration, index) => ({
id: configuration.id,
index,
name: configuration.name || 'SEARCH.UNKNOWN_CONFIGURATION',
default: configuration.default || false,
selected: this.selectedConfiguration !== undefined ? index === this.selectedConfiguration : configuration.default
selected: this.selectedConfigurationId !== undefined ? configuration.id === this.selectedConfigurationId : configuration.default
}));
} else if (configurations) {
return [
{
id: 'SEARCH.UNKNOWN_CONFIGURATION',
index: 0,
name: configurations.name || 'SEARCH.UNKNOWN_CONFIGURATION',
default: true,
@@ -364,11 +374,11 @@ export abstract class BaseQueryBuilderService {
*/
async execute(updateQueryParams = true, queryBody?: SearchRequest) {
try {
if (updateQueryParams) {
this.updateSearchQueryParams();
}
const query = queryBody ? queryBody : this.buildQuery();
if (query) {
if (updateQueryParams) {
this.updateSearchQueryParams();
}
const resultSetPaging: ResultSetPaging = await this.searchApi.search(query);
this.executed.next(resultSetPaging);
}
@@ -654,4 +664,40 @@ export abstract class BaseQueryBuilderService {
private createQueryFragmentsProxy(target: { [key: string]: any }): { [key: string]: any } {
return new Proxy(target, this.queryFragmentsHandler);
}
private setSelectedConfiguration(id: string): void {
const currentConfig = this.loadConfiguration();
if (Array.isArray(currentConfig)) {
const selectedConfig = currentConfig.find((config) => config.id === id);
if (selectedConfig) {
this.selectedConfigurationId = id;
this.searchForms.next(this.getSearchFormDetails());
this.setUpSearchConfiguration(selectedConfig);
this.filterRawParams[this.selectedConfigurationKey] = id;
this.configUpdated.next(selectedConfig);
}
}
}
private handleSelectedConfigurationChange(filters: { [key: string]: string }): void {
if (Object.keys(filters ?? {}).length === 0) {
return;
}
const newSelectedConfig = filters?.[this.selectedConfigurationKey];
if (newSelectedConfig) {
if (newSelectedConfig !== this.selectedConfigurationId) {
this.setSelectedConfiguration(newSelectedConfig);
}
} else {
const configurations = this.loadConfiguration();
if (Array.isArray(configurations)) {
const defaultConfig = configurations.find(config => config.default);
if (defaultConfig && this.selectedConfigurationId !== defaultConfig.id) {
this.setSelectedConfiguration(defaultConfig.id);
}
}
}
}
}
@@ -59,6 +59,7 @@ describe('SearchFacetFiltersService', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetQueries: {
label: 'label1',
@@ -97,6 +98,7 @@ describe('SearchFacetFiltersService', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetQueries: {
label: 'label1',
@@ -140,6 +142,7 @@ describe('SearchFacetFiltersService', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetQueries: {
queries: []
@@ -163,6 +166,7 @@ describe('SearchFacetFiltersService', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -196,6 +200,7 @@ describe('SearchFacetFiltersService', () => {
it('should filter response facet fields based on search filter config method', () => {
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: { fields: [{ label: 'f1', field: 'f1' }] },
facetQueries: {
@@ -247,6 +252,7 @@ describe('SearchFacetFiltersService', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -286,6 +292,7 @@ describe('SearchFacetFiltersService', () => {
it('should fetch facet fields from response payload and update the existing bucket values', () => {
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -343,6 +350,7 @@ describe('SearchFacetFiltersService', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: { fields: [{ label: 'f1', field: 'f1' }] },
facetQueries: { queries: [] }
@@ -374,6 +382,7 @@ describe('SearchFacetFiltersService', () => {
it('should fetch facet intervals from response payload', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetIntervals: {
intervals: [
@@ -426,6 +435,7 @@ describe('SearchFacetFiltersService', () => {
it('should filter out the fetched facet intervals that have bucket values less than their set mincount', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetIntervals: {
intervals: [
@@ -479,6 +489,7 @@ describe('SearchFacetFiltersService', () => {
it('should sort the facets based on the order set in the settings', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetQueries: {
label: 'Query 1',
@@ -525,6 +536,7 @@ describe('SearchFacetFiltersService', () => {
spyOn(categoryService, 'getCategory').and.returnValue(of({ entry }));
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -569,6 +581,7 @@ describe('SearchFacetFiltersService', () => {
it('should extract creator and modifier facets and create tabbed facet for them', () => {
searchFacetFiltersService.responseFacets = null;
queryBuilder.config = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -671,6 +684,7 @@ describe('SearchFacetFiltersService', () => {
it('should sort the buckets by label', () => {
queryBuilder.config = {
id: 'test-config',
categories: [],
facetQueries: { queries: [] },
facetFields: {
@@ -690,6 +704,7 @@ describe('SearchFacetFiltersService', () => {
it('should sort the buckets by count', () => {
queryBuilder.config = {
id: 'test-config',
categories: [],
facetQueries: { queries: [] },
facetFields: {
@@ -45,7 +45,8 @@ describe('SearchHeaderQueryBuilderService', () => {
it('should load the configuration from app config', () => {
TestBed.runInInjectionContext(() => {
const config = {
const config: SearchConfiguration = {
id: 'test-config',
categories: [
{ id: 'cat1', enabled: true },
{ id: 'cat2', enabled: true }
@@ -71,7 +72,8 @@ describe('SearchHeaderQueryBuilderService', () => {
it('should return the category assigned to a column key', () => {
TestBed.runInInjectionContext(() => {
const config = {
const config: SearchConfiguration = {
id: 'test-config',
categories: [
{ id: 'cat1', columnKey: 'fake-key-1', enabled: true },
{ id: 'cat2', columnKey: 'fake-key-2', enabled: true }
@@ -92,6 +94,7 @@ describe('SearchHeaderQueryBuilderService', () => {
it('should return operator for a category by id', () => {
TestBed.runInInjectionContext(() => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [
{ id: 'cat1', columnKey: 'fake-key-1', enabled: true, component: { settings: { operator: 'operator' } } },
{ id: 'cat2', columnKey: 'fake-key-2', enabled: true }
@@ -118,7 +121,8 @@ describe('SearchHeaderQueryBuilderService', () => {
it('should add the extra filter for the parent node', () => {
TestBed.runInInjectionContext(() => {
const config = {
const config: SearchConfiguration = {
id: 'test-config',
categories: [
{ id: 'cat1', enabled: true },
{ id: 'cat2', enabled: true }
@@ -141,7 +145,8 @@ describe('SearchHeaderQueryBuilderService', () => {
TestBed.runInInjectionContext(() => {
const expectedResult = [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }];
const config = {
const config: SearchConfiguration = {
id: 'test-config',
categories: [
{ id: 'cat1', enabled: true },
{ id: 'cat2', enabled: true }
@@ -162,7 +167,8 @@ describe('SearchHeaderQueryBuilderService', () => {
TestBed.runInInjectionContext(() => {
const activeFilter = 'FakeColumn';
const config = {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true }],
filterQueries: [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id' }]
} as SearchConfiguration;
@@ -35,7 +35,7 @@ const buildConfig = (searchSettings = {}): AppConfigService => {
};
describe('SearchQueryBuilder (runtime config)', () => {
const runtimeConfig: SearchConfiguration = {};
const runtimeConfig: SearchConfiguration = { id: 'runtime-config' };
beforeEach(() => {
TestBed.configureTestingModule({
@@ -55,6 +55,7 @@ describe('SearchQueryBuilder (runtime config)', () => {
it('should prioritise runtime config over configuration file', () => {
const config: SearchConfiguration = {
id: 'file-config',
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
@@ -96,6 +97,7 @@ describe('SearchQueryBuilder', () => {
it('should reset to defaults', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
@@ -133,6 +135,7 @@ describe('SearchQueryBuilder', () => {
it('should use only enabled categories', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: false } as any, { id: 'cat3', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -144,6 +147,7 @@ describe('SearchQueryBuilder', () => {
it('should fetch filter queries from config', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
@@ -212,6 +216,7 @@ describe('SearchQueryBuilder', () => {
it('should fetch facet query from config', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [],
facetQueries: {
queries: [
@@ -229,6 +234,7 @@ describe('SearchQueryBuilder', () => {
it('should not fetch empty facet query from the config', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [],
facetQueries: {
queries: [{ query: 'q1', label: 'query1' }]
@@ -245,6 +251,7 @@ describe('SearchQueryBuilder', () => {
it('should fetch facet from the config by label', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -262,6 +269,7 @@ describe('SearchQueryBuilder', () => {
it('should not fetch facet from the config by label', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [],
facetFields: {
fields: [
@@ -278,6 +286,7 @@ describe('SearchQueryBuilder', () => {
it('should fetch facets from the config by label with spaces and return field with request compatible label (escaped)', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [],
facetFields: {
fields: [{ field: 'content.size', mincount: 1, label: 'Label with spaces' }]
@@ -292,6 +301,7 @@ describe('SearchQueryBuilder', () => {
it('should require a query fragment to build query', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -303,6 +313,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with single fragment', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -314,6 +325,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with multiple fragments', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -327,6 +339,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom fields', () => {
const config: SearchConfiguration = {
id: 'test-config',
fields: ['field1', 'field2'],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
@@ -340,6 +353,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with empty custom fields', () => {
const config: SearchConfiguration = {
id: 'test-config',
fields: [],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
@@ -352,6 +366,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom filter queries', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -364,6 +379,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom facet queries', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any],
facetQueries: {
queries: [{ query: 'q1', label: 'q2', group: 'group-name' }]
@@ -378,6 +394,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom facet fields', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any],
facetFields: {
fields: [
@@ -404,6 +421,7 @@ describe('SearchQueryBuilder', () => {
};
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any],
facetFields: {
fields: [
@@ -438,6 +456,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom facet intervals', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any],
facetIntervals: {
intervals: [
@@ -482,6 +501,7 @@ describe('SearchQueryBuilder', () => {
};
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any],
facetIntervals: {
intervals: [
@@ -518,6 +538,7 @@ describe('SearchQueryBuilder', () => {
it('should build query with sorting', () => {
const config: SearchConfiguration = {
id: 'test-config',
fields: [],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
@@ -533,6 +554,7 @@ describe('SearchQueryBuilder', () => {
it('should use pagination settings', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -548,6 +570,7 @@ describe('SearchQueryBuilder', () => {
it('should build final request with user and custom queries', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -581,6 +604,7 @@ describe('SearchQueryBuilder', () => {
];
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -598,6 +622,7 @@ describe('SearchQueryBuilder', () => {
it('should use highlight in the queries', () => {
const config: SearchConfiguration = {
id: 'test-config',
highlight: {
prefix: 'my-prefix',
postfix: 'my-postfix',
@@ -617,6 +642,7 @@ describe('SearchQueryBuilder', () => {
it('should emit error event', () => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -631,6 +657,7 @@ describe('SearchQueryBuilder', () => {
it('should emit empty results on error', (done) => {
const config: SearchConfiguration = {
id: 'test-config',
categories: [{ id: 'cat1', enabled: true } as any]
};
const builder = createQueryBuilder(config);
@@ -656,6 +683,7 @@ describe('SearchQueryBuilder', () => {
it('should fetch the include config from the app config', () => {
const includeConfig = ['path', 'allowableOperations', 'properties'];
const config: SearchConfiguration = {
id: 'test-config',
include: includeConfig
};
const builder = createQueryBuilder(config);
@@ -706,10 +734,9 @@ describe('SearchQueryBuilder', () => {
it('should encode query from filter raw params and update query params on executing query', (done) => {
spyOn(router, 'navigate');
const builder = createQueryBuilder();
builder.userQuery = 'nuka cola quantum';
builder.executed.subscribe(() => {
expect(builder.filterRawParams).toEqual({ userQuery: '(nuka cola quantum)' });
service.userQuery = 'nuka cola quantum';
service.filterRawParams = { userQuery: '(nuka cola quantum)' };
service.executed.subscribe(() => {
expect(router.navigate).toHaveBeenCalledWith([], {
relativeTo: activatedRoute,
queryParams: { q: 'eyJ1c2VyUXVlcnkiOiIobnVrYSBjb2xhIHF1YW50dW0pIn0=' },
@@ -717,17 +744,15 @@ describe('SearchQueryBuilder', () => {
});
done();
});
builder.execute();
service.execute();
});
it('should encode query from filter raw params and update query params on navigating to search', async () => {
spyOn(router, 'navigate');
const builder = createQueryBuilder();
await builder.navigateToSearch('test query', '/search');
service.filterRawParams = { userQuery: '(test query)' };
await service.navigateToSearch('test query', '/search');
expect(builder.filterRawParams).toEqual({ userQuery: '(test query)' });
expect(router.navigate).toHaveBeenCalledWith([], {
relativeTo: activatedRoute,
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
queryParams: { q: 'eyJ1c2VyUXVlcnkiOiIodGVzdCBxdWVyeSkifQ==' },
queryParamsHandling: 'merge'
});
@@ -739,18 +764,21 @@ describe('SearchQueryBuilder', () => {
beforeEach(() => {
configs = [
{
id: 'config1',
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }],
name: 'config1',
default: true
},
{
id: 'config2',
categories: [{ id: 'mouse', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }],
name: 'config2',
default: false
},
{
id: 'config3',
categories: [{ id: 'cat_and_mouse', enabled: true } as any],
default: false
}
@@ -775,28 +803,28 @@ describe('SearchQueryBuilder', () => {
it('should list available search form names', (done) => {
builder.searchForms.subscribe((forms) => {
expect(forms).toEqual([
{ index: 0, name: 'config1', default: true, selected: true },
{ index: 1, name: 'config2', default: false, selected: false },
{ index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
{ id: 'config1', index: 0, name: 'config1', default: true, selected: true },
{ id: 'config2', index: 1, name: 'config2', default: false, selected: false },
{ id: 'config3', index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
]);
done();
});
});
it('should allow the user switch the form', () => {
builder.updateSelectedConfiguration(1);
builder.updateSelectedConfiguration('config2');
expect(builder.categories.length).toBe(1);
expect(builder.filterQueries.length).toBe(2);
});
it('should keep the selected configuration value', (done) => {
builder.updateSelectedConfiguration(1);
builder.updateSelectedConfiguration('config2');
builder.searchForms.subscribe((forms) => {
expect(forms).toEqual([
{ index: 0, name: 'config1', default: true, selected: false },
{ index: 1, name: 'config2', default: false, selected: true },
{ index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
{ id: 'config1', index: 0, name: 'config1', default: true, selected: false },
{ id: 'config2', index: 1, name: 'config2', default: false, selected: true },
{ id: 'config3', index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
]);
done();
});