[ACS-8751] Rework search filters to allow initial state and query encoding

This commit is contained in:
MichalKinas
2024-10-03 15:43:34 +02:00
parent 0d79ff534c
commit b81299e57e
36 changed files with 975 additions and 727 deletions

View File

@@ -23,6 +23,8 @@ Stores information from all the custom search and faceted search widgets, compil
- **buildQuery**(): `SearchRequest`<br/>
Builds the current query.
- **Returns** `SearchRequest` - The finished query
- **encodeQuery**()<br/>
Encodes query shards stored in `filterRawParams` property.
- **execute**(queryBody?: `SearchRequest`)<br/>
Builds and executes the current query.
- _queryBody:_ `SearchRequest` - (Optional)
@@ -72,6 +74,11 @@ Stores information from all the custom search and faceted search widgets, compil
- **Returns** [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts) -
- **navigateToSearch**(query: `string`, searchUrl: `string`) <br/>
Updates user query, executes existing search configuration, encodes the query and navigates to searchUrl.
- _query:_ `string` - The query to use as user query
- _searchUrl:_ `string` - Search url to navigate to
- **removeFilterQuery**(query: `string`)<br/>
Removes an existing filter query.
- _query:_ `string` - The query to remove
@@ -93,6 +100,8 @@ Stores information from all the custom search and faceted search widgets, compil
- **update**(queryBody?: `SearchRequest`)<br/>
Builds the current query and triggers the `updated` event.
- _queryBody:_ `SearchRequest` - (Optional)
- **updateSearchQueryParams**() <br/>
Encodes the query and navigates to existing search route adding encoded query as a search param.
- **updateSelectedConfiguration**(index: `number`)<br/>
- _index:_ `number` -

View File

@@ -174,7 +174,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
tick(debounceSearch);
fixture.detectChanges();
expect(searchSpy).toHaveBeenCalledWith(mockSearchRequest);
expect(searchSpy).toHaveBeenCalledWith(false, mockSearchRequest);
}));
it('should NOT perform a search and clear the results when the search request gets updated and it is NOT defined', async () => {
@@ -212,7 +212,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
tick(debounceSearch);
fixture.detectChanges();
expect(searchSpy).toHaveBeenCalledWith(mockSearchRequest);
expect(searchSpy).toHaveBeenCalledWith(false, mockSearchRequest);
}));
it('should the query include the show files filterQuery', fakeAsync(() => {
@@ -227,7 +227,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
tick(debounceSearch);
fixture.detectChanges();
expect(searchSpy).toHaveBeenCalledWith(expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
}));
it('should reset the currently chosen node in case of starting a new search', fakeAsync(() => {
@@ -261,7 +261,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
expectedRequest.filterQueries = [{ query: `ANCESTOR:'workspace://SpacesStore/namek'` }];
expect(searchSpy.calls.count()).toBe(2);
expect(searchSpy).toHaveBeenCalledWith(expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
}));
it('should create the query with the right parameters on changing the site selectBox value from a custom dropdown menu', fakeAsync(() => {
@@ -286,8 +286,8 @@ describe('ContentNodeSelectorPanelComponent', () => {
expect(searchSpy).toHaveBeenCalled();
expect(searchSpy.calls.count()).toBe(2);
expect(searchSpy).toHaveBeenCalledWith(mockSearchRequest);
expect(searchSpy).toHaveBeenCalledWith(expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false, mockSearchRequest);
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
}));
it('should get the corresponding node ids on search when a known alias is selected from dropdown', fakeAsync(() => {
@@ -407,7 +407,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
const expectedRequest = mockSearchRequest;
expectedRequest.filterQueries = [{ query: `ANCESTOR:'workspace://SpacesStore/my-root-id'` }];
expect(searchSpy).toHaveBeenCalledWith(expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
}));
it('should emit showingSearch event with true while searching', async () => {
@@ -416,7 +416,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
spyOn(customResourcesService, 'hasCorrespondingNodeIds').and.returnValue(true);
const showingSearchSpy = spyOn(component.showingSearch, 'emit');
await searchQueryBuilderService.execute({ query: { query: 'search' } });
await searchQueryBuilderService.execute(true, { query: { query: 'search' } });
triggerSearchResults(fakeResultSetPaging);
fixture.detectChanges();
@@ -460,7 +460,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
searchQueryBuilderService.update();
getCorrespondingNodeIdsSpy.and.throwError('Failed');
const showingSearchSpy = spyOn(component.showingSearch, 'emit');
await searchQueryBuilderService.execute({ query: { query: 'search' } });
await searchQueryBuilderService.execute(true, { query: { query: 'search' } });
triggerSearchResults(fakeResultSetPaging);
fixture.detectChanges();
@@ -479,7 +479,7 @@ describe('ContentNodeSelectorPanelComponent', () => {
const expectedRequest = mockSearchRequest;
expectedRequest.filterQueries = [{ query: `ANCESTOR:'workspace://SpacesStore/my-site-id'` }];
expect(searchSpy).toHaveBeenCalledWith(expectedRequest);
expect(searchSpy).toHaveBeenCalledWith(false, expectedRequest);
});
it('should restrict the breadcrumb to the currentFolderId in case restrictedRoot is true', async () => {

View File

@@ -343,7 +343,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
if (searchRequest) {
this.hasValidQuery = true;
this.prepareDialogForNewSearch(searchRequest);
this.queryBuilderService.execute(searchRequest);
this.queryBuilderService.execute(false, searchRequest);
} else {
this.hasValidQuery = false;
this.resetFolderToShow();

View File

@@ -24,6 +24,7 @@ import { HarnessLoader, TestKey } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
import { MatButtonHarness } from '@angular/material/button/testing';
import { ReplaySubject } from 'rxjs';
describe('SearchCheckListComponent', () => {
let loader: HarnessLoader;
@@ -37,6 +38,13 @@ describe('SearchCheckListComponent', () => {
fixture = TestBed.createComponent(SearchCheckListComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
component.context = {
queryFragments: {},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy()
} as any;
});
it('should setup options from settings', () => {
@@ -87,22 +95,17 @@ describe('SearchCheckListComponent', () => {
]);
component.id = 'checklist';
component.context = {
queryFragments: {},
update: () => {}
} as any;
component.ngOnInit();
spyOn(component.context, 'update').and.stub();
component.changeHandler({ checked: true } as any, component.options.items[0]);
expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder'`);
expect(component.context.filterRawParams[component.id]).toEqual([`TYPE:'cm:folder'`]);
component.changeHandler({ checked: true } as any, component.options.items[1]);
expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder' OR TYPE:'cm:content'`);
expect(component.context.filterRawParams[component.id]).toEqual([`TYPE:'cm:folder'`, `TYPE:'cm:content'`]);
});
it('should reset selected boxes', () => {
@@ -119,13 +122,8 @@ describe('SearchCheckListComponent', () => {
it('should update query builder on reset', () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
spyOn(component.context, 'update').and.stub();
component.context.queryFragments[component.id] = 'query';
component.context.filterRawParams[component.id] = 'test';
component.ngOnInit();
component.options = new SearchFilterList<SearchListOption>([
@@ -137,17 +135,13 @@ describe('SearchCheckListComponent', () => {
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBeUndefined();
});
describe('Pagination', () => {
it('should show 5 items when pageSize not defined', async () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
component.context.queryFragments[component.id] = 'query';
component.settings = { options: sizeOptions } as any;
component.ngOnInit();
@@ -162,12 +156,7 @@ describe('SearchCheckListComponent', () => {
it('should show all items when pageSize is high', async () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
component.context.queryFragments[component.id] = 'query';
component.settings = { pageSize: 15, options: sizeOptions } as any;
component.ngOnInit();
fixture.detectChanges();
@@ -182,12 +171,7 @@ describe('SearchCheckListComponent', () => {
it('should able to check/reset the checkbox', async () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
component.context.queryFragments[component.id] = 'query';
component.settings = { options: sizeOptions } as any;
spyOn(component, 'submitValues').and.stub();
component.ngOnInit();
@@ -212,10 +196,7 @@ describe('SearchCheckListComponent', () => {
{ name: 'Document', value: `TYPE:'cm:content'`, checked: false }
]);
component.startValue = `TYPE:'cm:folder'`;
component.context = {
queryFragments: {},
update: jasmine.createSpy()
} as any;
component.context.queryFragments[component.id] = 'query';
fixture.detectChanges();
expect(component.context.queryFragments[component.id]).toBe(`TYPE:'cm:folder'`);
@@ -229,15 +210,31 @@ describe('SearchCheckListComponent', () => {
{ name: 'Document', value: `TYPE:'cm:content'`, checked: false }
]);
component.startValue = undefined;
component.context = {
queryFragments: {
checkList: `TYPE:'cm:folder'`
},
update: jasmine.createSpy()
} as any;
component.context.queryFragments[component.id] = `TYPE:'cm:folder'`;
fixture.detectChanges();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.update).not.toHaveBeenCalled();
});
it('should populate filter state when populate filters event has been observed', () => {
component.id = 'checkList';
component.options = new SearchFilterList<SearchListOption>([
{ name: 'Folder', value: `TYPE:'cm:folder'`, checked: false },
{ name: 'Document', value: `TYPE:'cm:content'`, checked: false }
]);
component.startValue = undefined;
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({ checkList: [`TYPE:'cm:content'`] });
fixture.detectChanges();
expect(component.options.items[1].checked).toBeTrue();
expect(component.displayValue$.next).toHaveBeenCalledWith('Document');
expect(component.context.filterRawParams[component.id]).toEqual([`TYPE:'cm:content'`]);
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -22,7 +22,8 @@ import { SearchWidgetSettings } from '../../models/search-widget-settings.interf
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { SearchFilterList } from '../../models/search-filter-list.model';
import { TranslationService } from '@alfresco/adf-core';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
@@ -53,7 +54,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
pageSize = 5;
isActive = false;
enableChangeUpdate = true;
displayValue$: Subject<string> = new Subject<string>();
displayValue$: ReplaySubject<string> = new ReplaySubject<string>(1);
constructor(private translationService: TranslationService) {
this.options = new SearchFilterList<SearchListOption>();
@@ -77,6 +78,21 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
this.context.queryFragments[this.id] = '';
}
}
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
filtersQueries[this.id].forEach((value) => {
const foundIndex = this.options.items.findIndex((option) => option.value === value);
if (foundIndex !== -1) {
this.options.items[foundIndex].checked = true;
}
});
this.submitValues(false);
this.context.filterLoaded.next();
}
});
}
clear() {
@@ -95,6 +111,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
if (this.id && this.context) {
this.context.queryFragments[this.id] = '';
this.context.filterRawParams[this.id] = undefined;
}
}
@@ -142,13 +159,18 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
return this.options.items.filter((option) => option.checked).map((option) => option.value);
}
submitValues() {
submitValues(updateContext = true) {
const checkedValues = this.getCheckedValues();
if (checkedValues.length !== 0) {
this.context.filterRawParams[this.id] = checkedValues;
}
const query = checkedValues.join(` ${this.operator} `);
if (this.id && this.context) {
this.context.queryFragments[this.id] = query;
this.updateDisplayValue();
if (updateContext) {
this.context.update();
}
}
}
}

View File

@@ -122,6 +122,12 @@ describe('SearchChipAutocompleteInputComponent', () => {
return fixture.debugElement.queryAll(By.css('.adf-autocomplete-added-option'));
}
it('should assign preselected values to selected options on init', () => {
component.preselectedOptions = [{ value: 'option1' }];
component.ngOnInit();
expect(component.selectedOptions).toEqual([{ value: 'option1' }]);
});
it('should add new option only if value is predefined when allowOnlyPredefinedValues = true', async () => {
addNewOption('test');
addNewOption('option1');

View File

@@ -55,6 +55,9 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy,
@Input()
autocompleteOptions: AutocompleteOption[] = [];
@Input()
preselectedOptions: AutocompleteOption[] = [];
@Input()
onReset$: Observable<void>;
@@ -106,6 +109,7 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy,
this.inputChanged.emit(value);
});
this.onReset$?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.reset());
this.selectedOptions = this.preselectedOptions ?? [];
}
ngOnChanges(changes: SimpleChanges) {

View File

@@ -5,7 +5,7 @@
[dateFormat]="settings.dateFormat"
[maxDate]="settings.maxDate"
[field]="field"
[initialValue]="startValue"
[initialValue]="preselectedValues[field]"
(changed)="onDateRangedValueChanged($event, field)"
(valid)="tabsValidity[field]=$event">
</adf-search-date-range>

View File

@@ -25,6 +25,7 @@ import { SearchDateRangeTabbedComponent } from './search-date-range-tabbed.compo
import { DateRangeType } from './search-date-range/date-range-type';
import { InLastDateType } from './search-date-range/in-last-date-type';
import { endOfDay, endOfToday, formatISO, parse, startOfDay, startOfMonth, startOfWeek, subDays, subMonths, subWeeks } from 'date-fns';
import { ReplaySubject } from 'rxjs';
@Component({
selector: 'adf-search-filter-tabbed',
@@ -75,6 +76,8 @@ describe('SearchDateRangeTabbedComponent', () => {
queryFragments: {
dateRange: ''
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
component.settings = {
@@ -163,6 +166,8 @@ describe('SearchDateRangeTabbedComponent', () => {
`createdDate:['${formatISO(startOfDay(betweenMockData.betweenStartDate))}' TO '${formatISO(endOfDay(betweenMockData.betweenEndDate))}']` +
` AND modifiedDate:['${formatISO(startOfDay(inLastStartDate))}' TO '${formatISO(endOfToday())}']`;
expect(component.combinedQuery).toEqual(query);
expect(component.context.filterRawParams[component.id].createdDate).toEqual(betweenMockData);
expect(component.context.filterRawParams[component.id].modifiedDate).toEqual(inLastMockData);
inLastMockData = {
dateRangeType: DateRangeType.IN_LAST,
@@ -178,6 +183,7 @@ describe('SearchDateRangeTabbedComponent', () => {
`createdDate:['${formatISO(startOfDay(betweenMockData.betweenStartDate))}' TO '${formatISO(endOfDay(betweenMockData.betweenEndDate))}']` +
` AND modifiedDate:['${formatISO(startOfDay(inLastStartDate))}' TO '${formatISO(endOfToday())}']`;
expect(component.combinedQuery).toEqual(query);
expect(component.context.filterRawParams[component.id].modifiedDate).toEqual(inLastMockData);
inLastMockData = {
dateRangeType: DateRangeType.IN_LAST,
@@ -193,12 +199,14 @@ describe('SearchDateRangeTabbedComponent', () => {
`createdDate:['${formatISO(startOfDay(betweenMockData.betweenStartDate))}' TO '${formatISO(endOfDay(betweenMockData.betweenEndDate))}']` +
` AND modifiedDate:['${formatISO(startOfDay(inLastStartDate))}' TO '${formatISO(endOfToday())}']`;
expect(component.combinedQuery).toEqual(query);
expect(component.combinedQuery).toEqual(query);
expect(component.context.filterRawParams[component.id].modifiedDate).toEqual(inLastMockData);
component.onDateRangedValueChanged(anyMockDate, 'createdDate');
component.onDateRangedValueChanged(anyMockDate, 'modifiedDate');
fixture.detectChanges();
expect(component.combinedQuery).toEqual('');
expect(component.context.filterRawParams[component.id].createdDate).toEqual(anyMockDate);
expect(component.context.filterRawParams[component.id].modifiedDate).toEqual(anyMockDate);
});
it('should trigger context.update() when values are submitted', () => {
@@ -224,6 +232,31 @@ describe('SearchDateRangeTabbedComponent', () => {
expect(component.displayValue$.next).toHaveBeenCalledWith('');
expect(component.context.queryFragments['dateRange']).toEqual('');
expect(component.context.update).toHaveBeenCalled();
component.fields.forEach((field) => expect(component.context.filterRawParams[field]).toBeUndefined());
});
it('should populate filter state when populate filters event has been observed', () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
const createdDateMock = {
dateRangeType: DateRangeType.BETWEEN,
inLastValueType: InLastDateType.DAYS,
inLastValue: undefined,
betweenStartDate: '2023-06-05',
betweenEndDate: '2023-06-07'
};
component.context.populateFilters.next({ dateRange: { createdDate: createdDateMock, modifiedDate: inLastMockData } });
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith(
'CREATED DATE: 05-Jun-23 - 07-Jun-23 MODIFIED DATE: SEARCH.DATE_RANGE_ADVANCED.IN_LAST_DISPLAY_LABELS.WEEKS'
);
expect(component.preselectedValues['createdDate']).toEqual(betweenMockData);
expect(component.preselectedValues['modifiedDate']).toEqual(inLastMockData);
expect(component.context.filterRawParams[component.id].createdDate).toEqual(betweenMockData);
expect(component.context.filterRawParams[component.id].modifiedDate).toEqual(inLastMockData);
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
describe('SearchDateRangeTabbedComponent getTabLabel', () => {

View File

@@ -16,7 +16,8 @@
*/
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { DateRangeType } from './search-date-range/date-range-type';
import { SearchDateRange } from './search-date-range/search-date-range';
import { SearchWidget } from '../../models/search-widget.interface';
@@ -24,7 +25,7 @@ import { SearchWidgetSettings } from '../../models/search-widget-settings.interf
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { InLastDateType } from './search-date-range/in-last-date-type';
import { TranslationService } from '@alfresco/adf-core';
import { endOfDay, endOfToday, format, formatISO, startOfDay, startOfMonth, startOfWeek, subDays, subMonths, subWeeks } from 'date-fns';
import { endOfDay, endOfToday, format, formatISO, parseISO, startOfDay, startOfMonth, startOfWeek, subDays, subMonths, subWeeks } from 'date-fns';
import { CommonModule } from '@angular/common';
import { SearchFilterTabbedComponent } from '../search-filter-tabbed/search-filter-tabbed.component';
import { SearchDateRangeComponent } from './search-date-range/search-date-range.component';
@@ -41,7 +42,7 @@ const DEFAULT_DATE_DISPLAY_FORMAT = 'dd-MMM-yy';
encapsulation: ViewEncapsulation.None
})
export class SearchDateRangeTabbedComponent implements SearchWidget, OnInit {
displayValue$ = new Subject<string>();
displayValue$ = new ReplaySubject<string>(1);
id: string;
startValue: SearchDateRange = {
dateRangeType: DateRangeType.ANY,
@@ -50,6 +51,7 @@ export class SearchDateRangeTabbedComponent implements SearchWidget, OnInit {
betweenStartDate: undefined,
betweenEndDate: undefined
};
preselectedValues = {};
settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService;
fields: string[];
@@ -66,6 +68,25 @@ export class SearchDateRangeTabbedComponent implements SearchWidget, OnInit {
ngOnInit(): void {
this.fields = this.settings?.field.split(',').map((field) => field.trim());
this.setDefaultDateFormatSettings();
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
Object.keys(filtersQueries[this.id]).forEach((field) => {
filtersQueries[this.id][field].betweenStartDate = filtersQueries[this.id][field].betweenStartDate
? parseISO(filtersQueries[this.id][field].betweenStartDate)
: undefined;
filtersQueries[this.id][field].betweenEndDate = filtersQueries[this.id][field].betweenEndDate
? parseISO(filtersQueries[this.id][field].betweenEndDate)
: undefined;
this.preselectedValues[field] = filtersQueries[this.id][field];
this.onDateRangedValueChanged(filtersQueries[this.id][field], field);
});
this.submitValues(false);
this.context.filterLoaded.next();
}
});
}
private setDefaultDateFormatSettings() {
@@ -88,6 +109,9 @@ export class SearchDateRangeTabbedComponent implements SearchWidget, OnInit {
this.startValue = {
...this.startValue
};
this.fields.forEach((field) => {
this.context.filterRawParams[field] = undefined;
});
this.submitValues();
}
@@ -99,14 +123,16 @@ export class SearchDateRangeTabbedComponent implements SearchWidget, OnInit {
return this.settings?.displayedLabelsByField?.[field] ? this.settings.displayedLabelsByField[field] : field;
}
submitValues() {
submitValues(updateContext = true) {
this.context.queryFragments[this.id] = this.combinedQuery;
this.displayValue$.next(this.combinedDisplayValue);
if (this.id && this.context) {
if (this.id && this.context && updateContext) {
this.context.update();
}
}
onDateRangedValueChanged(value: Partial<SearchDateRange>, field: string) {
this.context.filterRawParams[this.id] = this.context.filterRawParams[this.id] || {};
this.context.filterRawParams[this.id][field] = value;
this.value[field] = value;
this.updateQuery(value, field);
this.updateDisplayValue(value, field);

View File

@@ -20,7 +20,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
import { DateFnsUtils } from '@alfresco/adf-core';
import { isValid } from 'date-fns';
import { endOfMinute, isValid, startOfMinute } from 'date-fns';
import { ReplaySubject } from 'rxjs';
describe('SearchDatetimeRangeComponent', () => {
let fixture: ComponentFixture<SearchDatetimeRangeComponent>;
@@ -36,6 +37,16 @@ describe('SearchDatetimeRangeComponent', () => {
});
fixture = TestBed.createComponent(SearchDatetimeRangeComponent);
component = fixture.componentInstance;
component.id = 'createdDateRange';
component.context = {
queryFragments: {
createdDatetimeRange: ''
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
component.settings = { field: 'cm:created' };
});
afterEach(() => fixture.destroy());
@@ -76,7 +87,7 @@ describe('SearchDatetimeRangeComponent', () => {
expect(component.from.value).toBeNull();
});
it('should reset form', async () => {
it('should reset form and filter params', async () => {
fixture.detectChanges();
await fixture.whenStable();
@@ -93,6 +104,7 @@ describe('SearchDatetimeRangeComponent', () => {
expect(component.from.value).toBeNull();
expect(component.to.value).toBeNull();
expect(component.form.value).toEqual({ from: null, to: null });
expect(component.context.filterRawParams[component.id]).toBeUndefined();
});
it('should reset fromMaxDatetime on reset', async () => {
@@ -106,39 +118,19 @@ describe('SearchDatetimeRangeComponent', () => {
});
it('should update query builder on reset', async () => {
const context: any = {
queryFragments: {
createdDatetimeRange: 'query'
},
update: () => {}
};
component.id = 'createdDatetimeRange';
component.context = context;
spyOn(context, 'update').and.stub();
component.context.queryFragments[component.id] = 'query';
fixture.detectChanges();
await fixture.whenStable();
component.reset();
expect(context.queryFragments.createdDatetimeRange).toEqual('');
expect(context.update).toHaveBeenCalled();
expect(component.context.queryFragments.createdDatetimeRange).toEqual('');
expect(component.context.update).toHaveBeenCalled();
});
it('should update the query in UTC format when values change', async () => {
const context: any = {
queryFragments: {},
update: () => {}
};
component.id = 'createdDateRange';
component.context = context;
component.settings = { field: 'cm:created' };
spyOn(context, 'update').and.stub();
fixture.detectChanges();
await fixture.whenStable();
@@ -151,25 +143,19 @@ describe('SearchDatetimeRangeComponent', () => {
);
const expectedQuery = `cm:created:['2016-10-16T12:30:00.000Z' TO '2017-10-16T20:00:59.000Z']`;
const expectedFromDate = DateFnsUtils.utcToLocal(startOfMinute(fromDatetime)).toISOString();
const expectedToDate = DateFnsUtils.utcToLocal(endOfMinute(toDatetime)).toISOString();
expect(context.queryFragments[component.id]).toEqual(expectedQuery);
expect(context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toEqual(expectedQuery);
expect(component.context.filterRawParams[component.id]).toEqual({ start: expectedFromDate, end: expectedToDate });
expect(component.context.update).toHaveBeenCalled();
});
it('should be able to update the query in UTC format from a GMT format', async () => {
const context: any = {
queryFragments: {},
update: () => {}
};
const fromInGmt = new Date('2021-02-24T17:00:00+02:00');
const toInGmt = new Date('2021-02-28T15:00:00+02:00');
component.id = 'createdDateRange';
component.context = context;
component.settings = { field: 'cm:created' };
spyOn(context, 'update').and.stub();
fixture.detectChanges();
await fixture.whenStable();
@@ -181,10 +167,10 @@ describe('SearchDatetimeRangeComponent', () => {
true
);
const expectedQuery = `cm:created:['2021-02-24T15:00:00.000Z' TO '2021-02-28T13:00:59.000Z']`;
const expectedQuery = `cm:created:['2021-02-24T16:00:00.000Z' TO '2021-02-28T14:00:59.000Z']`;
expect(context.queryFragments[component.id]).toEqual(expectedQuery);
expect(context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toEqual(expectedQuery);
expect(component.context.update).toHaveBeenCalled();
});
it('should show datetime-format error when an invalid datetime is set', async () => {
@@ -232,4 +218,22 @@ describe('SearchDatetimeRangeComponent', () => {
expect(inputs[1]).toBeDefined();
expect(inputs[1]).not.toBeNull();
});
it('should populate filter state when populate filters event has been observed', () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
const fromDateString = startOfMinute(fromDatetime).toISOString();
const toDateString = endOfMinute(toDatetime).toISOString();
const expectedFromDate = DateFnsUtils.utcToLocal(startOfMinute(fromDatetime)).toISOString();
const expectedToDate = DateFnsUtils.utcToLocal(endOfMinute(toDatetime)).toISOString();
component.context.populateFilters.next({ createdDateRange: { start: fromDateString, end: toDateString } });
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith('16/10/2016 12:30 - 16/10/2017 20:00');
expect(component.context.filterRawParams[component.id].start).toEqual(expectedFromDate);
expect(component.context.filterRawParams[component.id].end).toEqual(expectedToDate);
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -22,10 +22,11 @@ import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerInputEvent, MatDatetimepickerModule } from '@mat-datetimepicker/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { isValid, isBefore, startOfMinute, endOfMinute } from 'date-fns';
import { isValid, isBefore, startOfMinute, endOfMinute, parseISO } from 'date-fns';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@@ -74,7 +75,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit {
isActive = false;
startValue: any;
enableChangeUpdate: boolean;
displayValue$: Subject<string> = new Subject<string>();
displayValue$: ReplaySubject<string> = new ReplaySubject<string>(1);
constructor(private dateAdapter: DateAdapter<Date>, private dateTimeAdapter: DatetimeAdapter<Date>) {}
@@ -133,9 +134,22 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit {
this.setFromMaxDatetime();
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
const start = parseISO(filtersQueries[this.id].start);
const end = parseISO(filtersQueries[this.id].end);
this.form.patchValue({ from: start, to: end });
this.form.markAsDirty();
this.apply({ from: start, to: end }, true, false);
this.context.filterLoaded.next();
}
});
}
apply(model: Partial<{ from: Date; to: Date }>, isValidValue: boolean) {
apply(model: Partial<{ from: Date; to: Date }>, isValidValue: boolean, updateContext = true) {
if (isValidValue && this.id && this.context && this.settings && this.settings.field) {
this.isActive = true;
@@ -143,10 +157,15 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit {
const end = DateFnsUtils.utcToLocal(endOfMinute(model.to)).toISOString();
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
this.context.filterRawParams[this.id] = this.context.filterRawParams[this.id] ?? {};
this.context.filterRawParams[this.id].start = start;
this.context.filterRawParams[this.id].end = end;
this.updateDisplayValue();
if (updateContext) {
this.context.update();
}
}
}
submitValues() {
this.apply(this.form.value, this.form.valid);
@@ -197,6 +216,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit {
});
if (this.id && this.context) {
this.context.queryFragments[this.id] = '';
this.context.filterRawParams[this.id] = undefined;
}
if (this.id && this.context && this.enableChangeUpdate) {

View File

@@ -1,5 +1,6 @@
<adf-search-chip-autocomplete-input
[autocompleteOptions]="autocompleteOptions$ | async"
[preselectedOptions]="selectedOptions"
[onReset$]="reset$"
[allowOnlyPredefinedValues]="settings.allowOnlyPredefinedValues"
(inputChanged)="onInputChange($event)"

View File

@@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component';
import { EMPTY, of } from 'rxjs';
import { EMPTY, of, ReplaySubject } from 'rxjs';
import { AutocompleteField } from '../../models/autocomplete-option.interface';
import { TagService } from '../../../tag/services/tag.service';
@@ -44,8 +44,12 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
tagService = TestBed.inject(TagService);
component.id = 'test-id';
component.context = {
queryFragments: {},
update: () => EMPTY
queryFragments: {
createdDatetimeRange: ''
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
component.settings = {
field: 'test',
@@ -109,7 +113,7 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
component.setValue([{ value: 'option1' }, { value: 'option2' }]);
fixture.detectChanges();
expect(component.selectedOptions).toEqual([{ value: 'option1' }, { value: 'option2' }]);
spyOn(component.context, 'update');
expect(component.context.filterRawParams[component.id]).toEqual([{ value: 'option1' }, { value: 'option2' }]);
spyOn(component.displayValue$, 'next');
const clearBtn: HTMLButtonElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-search-chip-autocomplete-btn-clear"]')
@@ -120,10 +124,10 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
expect(component.context.update).toHaveBeenCalled();
expect(component.selectedOptions).toEqual([]);
expect(component.displayValue$.next).toHaveBeenCalledWith('');
expect(component.context.filterRawParams[component.id]).toBeUndefined();
});
it('should correctly compose the search query', () => {
spyOn(component.context, 'update');
component.selectedOptions = [{ value: 'option2' }, { value: 'option1' }];
const applyBtn: HTMLButtonElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-search-chip-autocomplete-btn-apply"]')
@@ -133,11 +137,27 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('test:"option2" OR test:"option1"');
expect(component.context.filterRawParams[component.id]).toEqual([{ value: 'option2' }, { value: 'option1' }]);
component.settings.field = AutocompleteField.CATEGORIES;
component.selectedOptions = [{ id: 'test-id', value: 'test' }];
applyBtn.click();
fixture.detectChanges();
expect(component.context.queryFragments[component.id]).toBe('cm:categories:"workspace://SpacesStore/test-id"');
expect(component.context.filterRawParams[component.id]).toEqual([{ id: 'test-id', value: 'test' }]);
});
it('should populate filter state when populate filters event has been observed', () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({ 'test-id': [{ value: 'option2' }, { value: 'option1' }] });
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith('option2, option1');
expect(component.context.filterRawParams[component.id]).toEqual([{ value: 'option2' }, { value: 'option1' }]);
expect(component.selectedOptions).toEqual([{ value: 'option2' }, { value: 'option1' }]);
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -16,7 +16,8 @@
*/
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { BehaviorSubject, Observable, ReplaySubject, Subject } from 'rxjs';
import { first } from 'rxjs/operators';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
@@ -42,7 +43,7 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
context?: SearchQueryBuilderService;
options: SearchFilterList<AutocompleteOption[]>;
startValue: AutocompleteOption[] = [];
displayValue$ = new Subject<string>();
displayValue$ = new ReplaySubject<string>(1);
selectedOptions: AutocompleteOption[] = [];
enableChangeUpdate: boolean;
@@ -58,15 +59,26 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
ngOnInit() {
if (this.settings) {
this.setOptions();
if (this.startValue) {
if (this.startValue?.length > 0) {
this.setValue(this.startValue);
}
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
}
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
this.selectedOptions = filtersQueries[this.id];
this.updateQuery(false);
this.context.filterLoaded.next();
}
});
}
reset() {
this.selectedOptions = [];
this.context.filterRawParams[this.id] = undefined;
this.resetSubject$.next();
this.updateQuery();
}
@@ -107,7 +119,8 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
return option1.id ? option1.id.toUpperCase() === option2.id.toUpperCase() : option1.value.toUpperCase() === option2.value.toUpperCase();
}
private updateQuery() {
private updateQuery(updateContext = true) {
this.context.filterRawParams[this.id] = this.selectedOptions.length > 0 ? this.selectedOptions : undefined;
this.displayValue$.next(this.selectedOptions.map((option) => option.value).join(', '));
if (this.context && this.settings && this.settings.field) {
let queryFragments;
@@ -117,9 +130,11 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
queryFragments = this.selectedOptions.map((val) => val.query ?? `${this.settings.field}:"${val.value}"`);
}
this.context.queryFragments[this.id] = queryFragments.join(' OR ');
if (updateContext) {
this.context.update();
}
}
}
private setOptions() {
switch (this.settings.field) {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core';
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core';
import { SearchCategory } from '../../../models/search-category.interface';
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
@@ -26,6 +26,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { SearchFilterMenuCardComponent } from '../search-filter-menu-card/search-filter-menu-card.component';
import { MatButtonModule } from '@angular/material/button';
import { first } from 'rxjs/operators';
@Component({
selector: 'adf-search-widget-chip',
@@ -50,7 +51,7 @@ import { MatButtonModule } from '@angular/material/button';
],
encapsulation: ViewEncapsulation.None
})
export class SearchWidgetChipComponent {
export class SearchWidgetChipComponent implements AfterViewInit {
@Input()
category: SearchCategory;
@@ -66,7 +67,16 @@ export class SearchWidgetChipComponent {
focusTrap: ConfigurableFocusTrap;
chipIcon = 'keyboard_arrow_down';
constructor(private focusTrapFactory: ConfigurableFocusTrapFactory) {}
constructor(private cd: ChangeDetectorRef, private focusTrapFactory: ConfigurableFocusTrapFactory) {}
ngAfterViewInit(): void {
this.widgetContainerComponent
?.getDisplayValue()
.pipe(first())
.subscribe(() => {
this.cd.detectChanges();
});
}
onMenuOpen() {
if (this.menuContainer && !this.focusTrap) {

View File

@@ -19,6 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { LogicalSearchCondition, LogicalSearchFields, SearchLogicalFilterComponent } from './search-logical-filter.component';
import { ReplaySubject } from 'rxjs';
describe('SearchLogicalFilterComponent', () => {
let component: SearchLogicalFilterComponent;
@@ -36,7 +37,9 @@ describe('SearchLogicalFilterComponent', () => {
queryFragments: {
logic: ''
},
update: () => {}
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
component.settings = { field: 'field1,field2', allowUpdateOnChange: true, hideDefaultAction: false };
fixture.detectChanges();
@@ -131,51 +134,50 @@ describe('SearchLogicalFilterComponent', () => {
const searchCondition: LogicalSearchCondition = { matchAll: 'test1', matchAny: 'test2', exclude: 'test3', matchExact: 'test4' };
component.setValue(searchCondition);
fixture.detectChanges();
spyOn(component.context, 'update');
spyOn(component.displayValue$, 'next');
component.reset();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.update).toHaveBeenCalled();
expect(component.getCurrentValue()).toEqual({ matchAll: '', matchAny: '', exclude: '', matchExact: '' });
expect(component.displayValue$.next).toHaveBeenCalledWith('');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
it('should form correct query from match all field', () => {
spyOn(component.context, 'update');
enterNewPhrase(' test1 test2 ', 0);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test1" AND field1:"test2") OR (field2:"test1" AND field2:"test2"))');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
it('should form correct query from match any field', () => {
spyOn(component.context, 'update');
enterNewPhrase(' test3 test4', 1);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test3" OR field1:"test4") OR (field2:"test3" OR field2:"test4"))');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
it('should form correct query from exclude field', () => {
spyOn(component.context, 'update');
enterNewPhrase('test5 test6 ', 2);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe(
'((NOT field1:"test5" AND NOT field1:"test6") AND (NOT field2:"test5" AND NOT field2:"test6"))'
);
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
it('should form correct query from match exact field and trim it', () => {
spyOn(component.context, 'update');
enterNewPhrase(' test7 test8 ', 3);
component.submitValues();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('((field1:"test7 test8") OR (field2:"test7 test8"))');
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
it('should form correct joined query from all fields', () => {
spyOn(component.context, 'update');
enterNewPhrase('test1', 0);
enterNewPhrase('test2', 1);
enterNewPhrase('test3', 2);
@@ -187,5 +189,20 @@ describe('SearchLogicalFilterComponent', () => {
const subQuery4 = '((field1:"test4") OR (field2:"test4"))';
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe(`${subQuery1} AND ${subQuery2} AND ${subQuery4} AND ${subQuery3}`);
expect(component.context.filterRawParams[component.id]).toEqual(component.getCurrentValue());
});
it('should populate filter state when populate filters event has been observed', () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({ logic: { matchAll: 'test', matchAny: 'test2', matchExact: '', exclude: '' } });
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith(' SEARCH.LOGICAL_SEARCH.MATCH_ALL: test SEARCH.LOGICAL_SEARCH.MATCH_ANY: test2');
expect(component.context.filterRawParams[component.id]).toEqual({ matchAll: 'test', matchAny: 'test2', matchExact: '', exclude: '' });
expect(component.searchCondition).toEqual({ matchAll: 'test', matchAny: 'test2', matchExact: '', exclude: '' });
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -19,7 +19,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { TranslationService } from '@alfresco/adf-core';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -53,15 +54,25 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
searchCondition: LogicalSearchCondition;
fields = Object.keys(LogicalSearchFields);
LogicalSearchFields = LogicalSearchFields;
displayValue$: Subject<string> = new Subject();
displayValue$: ReplaySubject<string> = new ReplaySubject(1);
constructor(private translationService: TranslationService) {}
ngOnInit(): void {
this.clearSearchInputs();
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
this.searchCondition = filtersQueries[this.id];
this.submitValues(false);
this.context.filterLoaded.next();
}
});
}
submitValues() {
submitValues(updateContext = true) {
if (this.hasValidValue() && this.id && this.context && this.settings && this.settings.field) {
this.updateDisplayValue();
const fields = this.settings.field.split(',').map((field) => (field += ':'));
@@ -108,7 +119,9 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
}
});
this.context.queryFragments[this.id] = query;
if (updateContext) {
this.context.update();
}
} else {
this.reset();
}
@@ -131,11 +144,13 @@ export class SearchLogicalFilterComponent implements SearchWidget, OnInit {
if (this.id && this.context) {
this.context.queryFragments[this.id] = '';
this.clearSearchInputs();
this.context.filterRawParams[this.id] = this.searchCondition;
this.context.update();
}
}
private updateDisplayValue(): void {
this.context.filterRawParams[this.id] = this.searchCondition;
if (this.hasValidValue()) {
const displayValue = Object.keys(this.searchCondition).reduce((acc, key) => {
const fieldIndex = Object.values(LogicalSearchFields).indexOf(key as LogicalSearchFields);

View File

@@ -15,15 +15,31 @@
* limitations under the License.
*/
import { ReplaySubject } from 'rxjs';
import { SearchNumberRangeComponent } from './search-number-range.component';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../../testing/content.testing.module';
describe('SearchNumberRangeComponent', () => {
let component: SearchNumberRangeComponent;
let fixture: ComponentFixture<SearchNumberRangeComponent>;
beforeEach(() => {
component = new SearchNumberRangeComponent();
TestBed.configureTestingModule({
imports: [ContentTestingModule, SearchNumberRangeComponent]
});
fixture = TestBed.createComponent(SearchNumberRangeComponent);
component = fixture.componentInstance;
component.id = 'contentSize';
component.context = {
queryFragments: {
contentSize: ''
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
});
it('should setup form elements on init', () => {
@@ -44,46 +60,32 @@ describe('SearchNumberRangeComponent', () => {
});
it('should update query builder on reset', () => {
const context: any = {
queryFragments: {
contentSize: 'query'
},
update: () => {}
};
component.id = 'contentSize';
component.context = context;
spyOn(context, 'update').and.stub();
component.context.queryFragments[component.id] = 'query';
component.ngOnInit();
component.reset();
expect(context.queryFragments.contentSize).toEqual('');
expect(context.update).toHaveBeenCalled();
expect(component.context.queryFragments.contentSize).toEqual('');
expect(component.context.update).toHaveBeenCalled();
expect(component.context.filterRawParams[component.id]).toBeUndefined();
});
it('should update query builder on value changes', () => {
const context: any = {
queryFragments: {},
update: () => {}
};
component.id = 'contentSize';
component.context = context;
component.settings = { field: 'cm:content.size' };
spyOn(context, 'update').and.stub();
component.ngOnInit();
component.apply({
component.apply(
{
from: '10',
to: '20'
}, true);
},
true
);
const expectedQuery = 'cm:content.size:[10 TO 20]';
expect(context.queryFragments[component.id]).toEqual(expectedQuery);
expect(context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toEqual(expectedQuery);
expect(component.context.update).toHaveBeenCalled();
expect(component.context.filterRawParams[component.id].from).toEqual('10');
expect(component.context.filterRawParams[component.id].to).toEqual('20');
});
it('should fetch format from the settings', () => {
@@ -108,31 +110,27 @@ describe('SearchNumberRangeComponent', () => {
});
it('should format value based on the current pattern', () => {
const context: any = {
queryFragments: {},
update: () => {}
};
component.id = 'range1';
component.settings = {
field: 'cm:content.size',
format: '<{FROM} TO {TO}>'
};
component.context = context;
component.ngOnInit();
component.apply({ from: '0', to: '100' }, true);
expect(context.queryFragments['range1']).toEqual('cm:content.size:<0 TO 100>');
expect(component.context.queryFragments[component.id]).toEqual('cm:content.size:<0 TO 100>');
});
it('should return true if TO value is bigger than FROM value', () => {
component.ngOnInit();
component.from = new UntypedFormControl('10');
component.to = new UntypedFormControl('20');
component.form = new UntypedFormGroup({
component.form = new UntypedFormGroup(
{
from: component.from,
to: component.to
}, component.formValidator);
},
component.formValidator
);
expect(component.formValidator).toBeTruthy();
});
@@ -166,4 +164,21 @@ describe('SearchNumberRangeComponent', () => {
component.from = new UntypedFormControl(-100, component.validators);
expect(component.from.hasError('min')).toBe(true);
});
it('should populate filter state when populate filters event has been observed', () => {
component.settings = {
field: 'cm:content.size'
};
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({ contentSize: { from: '10', to: '100' } });
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith('10 - 100 ');
expect(component.context.filterRawParams[component.id]).toEqual({ from: '10', to: '100' });
expect(component.form.value).toEqual({ from: '10', to: '100' });
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -21,7 +21,8 @@ import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@@ -56,7 +57,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
validators: Validators;
enableChangeUpdate: boolean;
displayValue$: Subject<string> = new Subject<string>();
displayValue$: ReplaySubject<string> = new ReplaySubject<string>(1);
ngOnInit(): void {
if (this.settings) {
@@ -84,13 +85,24 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
this.updateDisplayValue();
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
this.form.patchValue({ from: filtersQueries[this.id].from, to: filtersQueries[this.id].to });
this.form.markAsDirty();
this.apply({ from: filtersQueries[this.id].from, to: filtersQueries[this.id].to }, true, false);
this.context.filterLoaded.next();
}
});
}
formValidator(formGroup: UntypedFormGroup) {
return parseInt(formGroup.get('from').value, 10) < parseInt(formGroup.get('to').value, 10) ? null : { mismatch: true };
}
apply(model: { from: string; to: string }, isValid: boolean) {
apply(model: { from: string; to: string }, isValid: boolean, updateContext = true) {
if (isValid && this.id && this.context && this.field) {
this.updateDisplayValue();
this.isActive = true;
@@ -102,9 +114,14 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
const value = this.formatString(this.format, map);
this.context.queryFragments[this.id] = `${this.field}:${value}`;
this.context.filterRawParams[this.id] = this.context.filterRawParams[this.id] ?? {};
this.context.filterRawParams[this.id].from = model.from;
this.context.filterRawParams[this.id].to = model.to;
if (updateContext) {
this.context.update();
}
}
}
private formatString(str: string, map: Map<string, string>): string {
let result = str;
@@ -153,6 +170,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
if (this.id && this.context) {
this.context.queryFragments[this.id] = '';
this.context.filterRawParams[this.id] = undefined;
this.updateDisplayValue();
if (this.enableChangeUpdate) {
this.context.update();

View File

@@ -15,194 +15,44 @@
* limitations under the License.
*/
import { SearchCheckListComponent, SearchListOption } from '../search-check-list/search-check-list.component';
import { SearchFilterList } from '../../models/search-filter-list.model';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { sizeOptions, stepOne, stepThree } from '../../../mock';
import { HarnessLoader, TestKey } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
import { SearchPanelComponent } from './search-panel.component';
import { By } from '@angular/platform-browser';
import { ContentNodeSelectorPanelService } from '../../../content-node-selector';
import { SearchCategory } from '../../models';
describe('SearchCheckListComponent', () => {
let loader: HarnessLoader;
let fixture: ComponentFixture<SearchCheckListComponent>;
let component: SearchCheckListComponent;
describe('SearchPanelComponent', () => {
let fixture: ComponentFixture<SearchPanelComponent>;
let contentNodeSelectorPanelService: ContentNodeSelectorPanelService;
const getSearchFilter = () => fixture.debugElement.query(By.css('.app-search-settings'));
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
});
fixture = TestBed.createComponent(SearchCheckListComponent);
component = fixture.componentInstance;
fixture = TestBed.createComponent(SearchPanelComponent);
contentNodeSelectorPanelService = TestBed.inject(ContentNodeSelectorPanelService);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should setup options from settings', () => {
const options: any = [
{ name: 'Folder', value: `TYPE:'cm:folder'` },
{ name: 'Document', value: `TYPE:'cm:content'` }
it('should not render search filter when no custom models are available', () => {
contentNodeSelectorPanelService.customModels = [];
spyOn(contentNodeSelectorPanelService, 'convertCustomModelPropertiesToSearchCategories').and.returnValue([]);
fixture.detectChanges();
expect(getSearchFilter()).toBeNull();
});
it('should render search filter when some custom models are available', () => {
const categoriesMock: SearchCategory[] = [
{ id: 'model1', name: 'model1', enabled: true, expanded: false, component: { selector: 'test', settings: { field: 'test' } } },
{ id: 'model2', name: 'model2', enabled: true, expanded: false, component: { selector: 'test2', settings: { field: 'test2' } } }
];
component.settings = { options } as any;
component.ngOnInit();
expect(component.options.items).toEqual(options);
});
it('should handle enter key as click on checkboxes', async () => {
component.options = new SearchFilterList<SearchListOption>([
{ name: 'Folder', value: `TYPE:'cm:folder'`, checked: false },
{ name: 'Document', value: `TYPE:'cm:content'`, checked: false }
]);
component.ngOnInit();
contentNodeSelectorPanelService.customModels = ['model1', 'model2'];
spyOn(contentNodeSelectorPanelService, 'convertCustomModelPropertiesToSearchCategories').and.returnValue(categoriesMock);
fixture.detectChanges();
const options = await loader.getAllHarnesses(MatCheckboxHarness);
await (await options[0].host()).sendKeys(TestKey.ENTER);
expect(await options[0].isChecked()).toBe(true);
await (await options[0].host()).sendKeys(TestKey.ENTER);
expect(await options[0].isChecked()).toBe(false);
});
it('should setup operator from the settings', () => {
component.settings = { operator: 'AND' } as any;
component.ngOnInit();
expect(component.operator).toBe('AND');
});
it('should use OR operator by default', () => {
component.settings = { operator: null } as any;
component.ngOnInit();
expect(component.operator).toBe('OR');
});
it('should update query builder on checkbox change', () => {
component.options = new SearchFilterList<SearchListOption>([
{ name: 'Folder', value: `TYPE:'cm:folder'`, checked: false },
{ name: 'Document', value: `TYPE:'cm:content'`, checked: false }
]);
component.id = 'checklist';
component.context = {
queryFragments: {},
update: () => {}
} as any;
component.ngOnInit();
spyOn(component.context, 'update').and.stub();
component.changeHandler({ checked: true } as any, component.options.items[0]);
expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder'`);
component.changeHandler({ checked: true } as any, component.options.items[1]);
expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder' OR TYPE:'cm:content'`);
});
it('should reset selected boxes', () => {
component.options = new SearchFilterList<SearchListOption>([
{ name: 'Folder', value: `TYPE:'cm:folder'`, checked: true },
{ name: 'Document', value: `TYPE:'cm:content'`, checked: true }
]);
component.reset();
expect(component.options.items[0].checked).toBeFalsy();
expect(component.options.items[1].checked).toBeFalsy();
});
it('should update query builder on reset', () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
spyOn(component.context, 'update').and.stub();
component.ngOnInit();
component.options = new SearchFilterList<SearchListOption>([
{ name: 'Folder', value: `TYPE:'cm:folder'`, checked: true },
{ name: 'Document', value: `TYPE:'cm:content'`, checked: true }
]);
component.reset();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('');
});
describe('Pagination', () => {
it('should show 5 items when pageSize not defined', async () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
component.settings = { options: sizeOptions } as any;
component.ngOnInit();
fixture.detectChanges();
const options = await loader.getAllHarnesses(MatCheckboxHarness);
expect(options.length).toEqual(5);
const labels = await Promise.all(Array.from(options).map(async (element) => element.getLabelText()));
expect(labels).toEqual(stepOne);
});
it('should show all items when pageSize is high', async () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
component.settings = { pageSize: 15, options: sizeOptions } as any;
component.ngOnInit();
fixture.detectChanges();
const options = await loader.getAllHarnesses(MatCheckboxHarness);
expect(options.length).toEqual(13);
const labels = await Promise.all(Array.from(options).map(async (element) => element.getLabelText()));
expect(labels).toEqual(stepThree);
});
});
it('should able to check/reset the checkbox', async () => {
component.id = 'checklist';
component.context = {
queryFragments: {
checklist: 'query'
},
update: () => {}
} as any;
component.settings = { options: sizeOptions } as any;
spyOn(component, 'submitValues').and.stub();
component.ngOnInit();
fixture.detectChanges();
const checkbox = await loader.getHarness(MatCheckboxHarness);
await checkbox.check();
expect(component.submitValues).toHaveBeenCalled();
const clearAllElement = fixture.debugElement.query(By.css('button[title="SEARCH.FILTER.ACTIONS.CLEAR-ALL"]'));
clearAllElement.triggerEventHandler('click', {});
fixture.detectChanges();
expect(await checkbox.isChecked()).toBe(false);
expect(getSearchFilter()).toBeDefined();
});
});

View File

@@ -46,6 +46,7 @@
<p class="adf-search-properties-file-type-label">{{ 'SEARCH.SEARCH_PROPERTIES.FILE_TYPE' | translate }}</p>
<adf-search-chip-autocomplete-input
[autocompleteOptions]="autocompleteOptions"
[preselectedOptions]="preselectedOptions"
(optionsChanged)="selectedExtensions = $event"
[onReset$]="reset$"
[allowOnlyPredefinedValues]="false"

View File

@@ -24,7 +24,7 @@ import { FileSizeUnit } from './file-size-unit.enum';
import { FileSizeOperator } from './file-size-operator.enum';
import { SearchProperties } from './search-properties';
import { SearchChipAutocompleteInputComponent } from '../search-chip-autocomplete-input';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { ReplaySubject } from 'rxjs';
describe('SearchPropertiesComponent', () => {
let component: SearchPropertiesComponent;
@@ -66,6 +66,15 @@ describe('SearchPropertiesComponent', () => {
fixture = TestBed.createComponent(SearchPropertiesComponent);
component = fixture.componentInstance;
component.id = 'properties';
component.context = {
queryFragments: {
properties: ''
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
});
describe('File size', () => {
@@ -187,14 +196,11 @@ describe('SearchPropertiesComponent', () => {
const nameField = 'cm:name';
beforeEach(() => {
component.id = 'properties';
component.settings = {
field: `${sizeField},${nameField}`
};
component.context = TestBed.inject(SearchQueryBuilderService);
fixture.detectChanges();
spyOn(component.displayValue$, 'next');
spyOn(component.context, 'update');
});
it('should not search when settings is not set', () => {
@@ -203,7 +209,6 @@ describe('SearchPropertiesComponent', () => {
component.submitValues();
expect(component.displayValue$.next).not.toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBeUndefined();
expect(component.context.update).not.toHaveBeenCalled();
});
@@ -219,6 +224,10 @@ describe('SearchPropertiesComponent', () => {
component.submitValues();
expect(component.displayValue$.next).toHaveBeenCalledWith('');
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: undefined,
fileSizeCondition: { fileSize: null, fileSizeOperator: FileSizeOperator.AT_LEAST, fileSizeUnit: FileSizeUnit.KB }
});
expect(component.context.update).toHaveBeenCalled();
});
@@ -230,6 +239,14 @@ describe('SearchPropertiesComponent', () => {
'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB'
);
expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[328704 TO MAX]`);
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: undefined,
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST',
fileSize: 321,
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
});
@@ -247,6 +264,14 @@ describe('SearchPropertiesComponent', () => {
'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.MB'
);
expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[0 TO 336592896]`);
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: undefined,
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST',
fileSize: 321,
fileSizeUnit: FileSizeUnit.MB
}
});
expect(component.context.update).toHaveBeenCalled();
});
@@ -264,6 +289,14 @@ describe('SearchPropertiesComponent', () => {
'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.EXACTLY 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.GB'
);
expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[344671125504 TO 344671125504]`);
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: undefined,
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.EXACTLY',
fileSize: 321,
fileSizeUnit: FileSizeUnit.GB
}
});
expect(component.context.update).toHaveBeenCalled();
});
@@ -274,6 +307,14 @@ describe('SearchPropertiesComponent', () => {
component.submitValues();
expect(component.displayValue$.next).toHaveBeenCalledWith('pdf');
expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.${extension.value}")`);
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: ['pdf'],
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST',
fileSize: null,
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
});
@@ -283,6 +324,14 @@ describe('SearchPropertiesComponent', () => {
component.submitValues();
expect(component.displayValue$.next).toHaveBeenCalledWith('pdf, txt');
expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.pdf" OR "*.txt")`);
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: ['pdf', 'txt'],
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST',
fileSize: null,
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
});
@@ -295,6 +344,14 @@ describe('SearchPropertiesComponent', () => {
'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB, pdf, txt'
);
expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[328704 TO MAX] AND ${nameField}:("*.pdf" OR "*.txt")`);
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: ['pdf', 'txt'],
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST',
fileSize: 321,
fileSizeUnit: FileSizeUnit.KB
}
});
expect(component.context.update).toHaveBeenCalled();
});
});
@@ -377,14 +434,12 @@ describe('SearchPropertiesComponent', () => {
});
it('should clear the queryFragments for the component id and call update', () => {
component.context = TestBed.inject(SearchQueryBuilderService);
component.id = 'test-id';
component.context.queryFragments[component.id] = 'test-query';
fixture.detectChanges();
spyOn(component.context, 'update');
component.reset();
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBeUndefined();
expect(component.context.update).toHaveBeenCalled();
});
});
@@ -411,20 +466,25 @@ describe('SearchPropertiesComponent', () => {
it('should search based on passed value', () => {
const sizeField = 'content.size';
const nameField = 'cm:name';
component.id = 'properties';
component.settings = {
field: `${sizeField},${nameField}`
};
component.context = TestBed.inject(SearchQueryBuilderService);
component.ngOnInit();
spyOn(component.displayValue$, 'next');
spyOn(component.context, 'update');
component.setValue(searchProperties);
expect(component.displayValue$.next).toHaveBeenCalledWith(
'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.MB, pdf, txt'
);
expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[0 TO 336592896] AND ${nameField}:("*.pdf" OR "*.txt")`);
expect(component.context.filterRawParams[component.id]).toEqual({
fileExtensions: ['pdf', 'txt'],
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST',
fileSize: 321,
fileSizeUnit: FileSizeUnit.MB
}
});
expect(component.context.update).toHaveBeenCalled();
});
});
@@ -470,4 +530,37 @@ describe('SearchPropertiesComponent', () => {
).toBeTrue();
});
});
it('should populate filter state when populate filters event has been observed', () => {
component.settings = {
field: 'field'
};
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({
properties: {
fileExtensions: ['pdf', 'txt'],
fileSizeCondition: {
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST',
fileSize: 321,
fileSizeUnit: FileSizeUnit.MB
}
}
});
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith(
'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.MB, pdf, txt'
);
expect(component.selectedExtensions).toEqual([{ value: 'pdf' }, { value: 'txt' }]);
expect(component.preselectedOptions).toEqual([{ value: 'pdf' }, { value: 'txt' }]);
expect(component.form.value).toEqual({
fileSizeOperator: 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST',
fileSize: 321,
fileSizeUnit: FileSizeUnit.MB
});
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -20,7 +20,7 @@ import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { FileSizeCondition } from './file-size-condition';
import { FileSizeOperator } from './file-size-operator.enum';
import { FileSizeUnit } from './file-size-unit.enum';
import { Subject } from 'rxjs';
import { ReplaySubject, Subject } from 'rxjs';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { SearchProperties } from './search-properties';
@@ -31,6 +31,7 @@ import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { SearchChipAutocompleteInputComponent } from '../search-chip-autocomplete-input';
import { first } from 'rxjs/operators';
@Component({
selector: 'adf-search-properties',
@@ -45,8 +46,9 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService;
startValue: SearchProperties;
displayValue$ = new Subject<string>();
displayValue$ = new ReplaySubject<string>(1);
autocompleteOptions: AutocompleteOption[] = [];
preselectedOptions: AutocompleteOption[] = [];
private _form = this.formBuilder.nonNullable.group<FileSizeCondition>({
fileSizeOperator: FileSizeOperator.AT_LEAST,
@@ -85,6 +87,10 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
return this._reset$;
}
get selectedExtensions(): AutocompleteOption[] {
return this.parseToAutocompleteOptions(this._selectedExtensions);
}
set selectedExtensions(extensions: AutocompleteOption[]) {
this._selectedExtensions = this.parseFromAutocompleteOptions(extensions);
}
@@ -102,6 +108,22 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
if (this.startValue) {
this.setValue(this.startValue);
}
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
filtersQueries[this.id].fileSizeCondition.fileSizeUnit = this.fileSizeUnits.find(
(fileSizeUnit) => fileSizeUnit.bytes === filtersQueries[this.id].fileSizeCondition.fileSizeUnit.bytes
);
this.form.patchValue(filtersQueries[this.id].fileSizeCondition);
this.form.updateValueAndValidity();
this._selectedExtensions = filtersQueries[this.id].fileExtensions ?? [];
this.preselectedOptions = this.parseToAutocompleteOptions(this._selectedExtensions);
this.submitValues(false);
this.context.filterLoaded.next();
}
});
}
ngAfterViewChecked() {
@@ -164,13 +186,20 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
this.form.reset();
if (this.id && this.context) {
this.context.queryFragments[this.id] = '';
this.context.filterRawParams[this.id] = undefined;
this.context.update();
}
this.reset$.next();
this.displayValue$.next('');
}
submitValues() {
submitValues(updateContext = true) {
if (this.context?.filterRawParams) {
this.context.filterRawParams[this.id] = {
fileExtensions: this._selectedExtensions,
fileSizeCondition: this.form.value
};
}
if (this.settings && this.context) {
let query = '';
let displayedValue = '';
@@ -200,9 +229,11 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
}
this.displayValue$.next(displayedValue);
this.context.queryFragments[this.id] = query;
if (updateContext) {
this.context.update();
}
}
}
hasValidValue(): boolean {
return true;
@@ -217,7 +248,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
setValue(searchProperties: SearchProperties) {
this.form.patchValue(searchProperties.fileSizeCondition);
this.selectedExtensions = this.parseToAutocompleteOptions(searchProperties.fileExtensions);
this.selectedExtensions = this.parseToAutocompleteOptions(searchProperties.fileExtensions ?? []);
this.submitValues();
}

View File

@@ -22,6 +22,7 @@ import { ContentTestingModule } from '../../../testing/content.testing.module';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatRadioButtonHarness, MatRadioGroupHarness } from '@angular/material/radio/testing';
import { ReplaySubject } from 'rxjs';
describe('SearchRadioComponent', () => {
let loader: HarnessLoader;
@@ -36,20 +37,20 @@ describe('SearchRadioComponent', () => {
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
});
describe('Pagination', () => {
it('should show 5 items when pageSize not defined', async () => {
component.id = 'radio';
component.context = {
queryFragments: {
radio: 'query'
},
update: () => {}
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
component.settings = { options: sizeOptions } as any;
});
component.ngOnInit();
describe('Pagination', () => {
it('should show 5 items when pageSize not defined', async () => {
fixture.detectChanges();
const options = await loader.getAllHarnesses(MatRadioButtonHarness);
@@ -60,15 +61,7 @@ describe('SearchRadioComponent', () => {
});
it('should show all items when pageSize is high', async () => {
component.id = 'radio';
component.context = {
queryFragments: {
radio: 'query'
},
update: () => {}
} as any;
component.settings = { pageSize: 15, options: sizeOptions } as any;
component.ngOnInit();
component.settings['pageSize'] = 15;
fixture.detectChanges();
const options = await loader.getAllHarnesses(MatRadioButtonHarness);
@@ -80,18 +73,40 @@ describe('SearchRadioComponent', () => {
});
it('should able to check the radio button', async () => {
component.id = 'radio';
component.context = {
queryFragments: {
radio: 'query'
},
update: () => {}
} as any;
component.settings = { options: sizeOptions } as any;
const group = await loader.getHarness(MatRadioGroupHarness);
await group.checkRadioButton({ selector: `[data-automation-id="search-radio-${sizeOptions[0].name}"]` });
await group.checkRadioButton({ selector: `[data-automation-id="search-radio-${sizeOptions[1].name}"]` });
expect(component.context.queryFragments[component.id]).toBe(sizeOptions[1].value);
expect(component.context.filterRawParams[component.id]).toBe(sizeOptions[1].value);
});
it('should reset to initial value ', async () => {
const group = await loader.getHarness(MatRadioGroupHarness);
await group.checkRadioButton({ selector: `[data-automation-id="search-radio-${sizeOptions[2].name}"]` });
expect(component.context.queryFragments[component.id]).toBe(sizeOptions[2].value);
expect(component.context.filterRawParams[component.id]).toBe(sizeOptions[2].value);
component.reset();
fixture.detectChanges();
expect(component.context.queryFragments[component.id]).toBe(sizeOptions[0].value);
expect(component.context.filterRawParams[component.id]).toBe(sizeOptions[0].value);
});
it('should populate filter state when populate filters event has been observed', async () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({ radio: sizeOptions[1].value });
fixture.detectChanges();
const group = await loader.getHarness(MatRadioGroupHarness);
expect(component.displayValue$.next).toHaveBeenCalledWith(sizeOptions[1].name);
expect(component.value).toEqual(sizeOptions[1].value);
expect(component.context.filterRawParams[component.id]).toBe(sizeOptions[1].value);
expect(component.context.filterLoaded.next).toHaveBeenCalled();
expect(await group.getCheckedValue()).toEqual(sizeOptions[1].value);
});
});

View File

@@ -22,7 +22,8 @@ import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { SearchFilterList } from '../../models/search-filter-list.model';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
@@ -56,7 +57,7 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
isActive = false;
startValue: any;
enableChangeUpdate: boolean;
displayValue$: Subject<string> = new Subject<string>();
displayValue$: ReplaySubject<string> = new ReplaySubject<string>(1);
constructor() {
this.options = new SearchFilterList<SearchRadioOption>();
@@ -82,6 +83,16 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
}
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
this.updateDisplayValue();
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
this.value = filtersQueries[this.id];
this.submitValues(false);
this.context.filterLoaded.next();
}
});
}
private getSelectedValue(): string {
@@ -98,11 +109,13 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
return null;
}
submitValues() {
submitValues(updateContext = true) {
this.setValue(this.value);
this.updateDisplayValue();
if (updateContext) {
this.context.update();
}
}
hasValidValue() {
const currentValue = this.getSelectedValue();
@@ -112,6 +125,7 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
setValue(newValue: string) {
this.value = newValue;
this.context.queryFragments[this.id] = newValue;
this.context.filterRawParams[this.id] = newValue;
if (this.enableChangeUpdate) {
this.updateDisplayValue();
this.context.update();

View File

@@ -18,6 +18,7 @@
import { SearchSliderComponent } from './search-slider.component';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReplaySubject } from 'rxjs';
describe('SearchSliderComponent', () => {
let fixture: ComponentFixture<SearchSliderComponent>;
@@ -29,101 +30,84 @@ describe('SearchSliderComponent', () => {
});
fixture = TestBed.createComponent(SearchSliderComponent);
component = fixture.componentInstance;
});
it('should setup slider from settings', () => {
const settings: any = {
component.id = 'slider';
component.context = {
queryFragments: {
slider: ''
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
component.settings = {
field: 'field1',
min: 10,
max: 100,
step: 2,
thumbLabel: true
};
});
component.settings = settings;
it('should setup slider from settings', () => {
fixture.detectChanges();
expect(component.min).toEqual(settings.min);
expect(component.max).toEqual(settings.max);
expect(component.step).toEqual(settings.step);
expect(component.thumbLabel).toEqual(settings.thumbLabel);
expect(component.min).toEqual(10);
expect(component.max).toEqual(100);
expect(component.step).toEqual(2);
expect(component.thumbLabel).toEqual(true);
});
it('should update its query part on slider change', () => {
const context: any = {
queryFragments: {},
update: () => {}
};
spyOn(context, 'update').and.stub();
component.context = context;
component.id = 'contentSize';
component.settings = { field: 'cm:content.size' };
component.settings['field'] = 'cm:content.size';
component.value = 10;
fixture.detectChanges();
component.onChangedHandler();
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 10]');
expect(context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 10]');
expect(component.context.filterRawParams[component.id]).toEqual(10);
expect(component.context.update).toHaveBeenCalled();
component.value = 20;
component.onChangedHandler();
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 20]');
expect(component.context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 20]');
expect(component.context.filterRawParams[component.id]).toEqual(20);
});
it('should reset the value for query builder', () => {
const settings: any = {
field: 'field1',
min: 10,
max: 100,
step: 2,
thumbLabel: true
};
const context: any = {
queryFragments: {},
update: () => {}
};
component.settings = settings;
component.context = context;
component.value = 20;
component.id = 'slider';
spyOn(context, 'update').and.stub();
fixture.detectChanges();
component.reset();
expect(component.value).toBe(settings.min);
expect(context.queryFragments[component.id]).toBe('');
expect(context.update).toHaveBeenCalled();
expect(component.value).toBe(10);
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBe(null);
expect(component.context.update).toHaveBeenCalled();
});
it('should reset to 0 if min not provided', () => {
const settings: any = {
field: 'field1',
min: null,
max: 100,
step: 2,
thumbLabel: true
};
const context: any = {
queryFragments: {},
update: () => {}
};
component.settings = settings;
component.context = context;
component.settings.min = null;
component.value = 20;
component.id = 'slider';
spyOn(context, 'update').and.stub();
fixture.detectChanges();
component.reset();
expect(component.value).toBe(0);
expect(context.queryFragments['slider']).toBe('');
expect(context.update).toHaveBeenCalled();
expect(component.context.queryFragments['slider']).toBe('');
expect(component.context.update).toHaveBeenCalled();
});
it('should populate filter state when populate filters event has been observed', async () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({ slider: 20 });
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith('20 ');
expect(component.value).toBe(20);
expect(component.context.filterRawParams[component.id]).toBe(20);
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -19,7 +19,8 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { MatSliderModule } from '@angular/material/slider';
import { FormsModule } from '@angular/forms';
@@ -47,7 +48,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
max: number;
thumbLabel = false;
enableChangeUpdate: boolean;
displayValue$: Subject<string> = new Subject<string>();
displayValue$: ReplaySubject<string> = new ReplaySubject<string>(1);
/** The numeric value represented by the slider. */
@Input()
@@ -74,6 +75,16 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
if (this.startValue) {
this.setValue(this.startValue);
}
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
this.value = filtersQueries[this.id];
this.updateQuery(this.value, false);
this.context.filterLoaded.next();
}
});
}
clear() {
@@ -111,7 +122,8 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
this.submitValues();
}
private updateQuery(value: number | null) {
private updateQuery(value: number | null, updateContext = true) {
this.context.filterRawParams[this.id] = value;
this.displayValue$.next(this.value ? `${this.value} ${this.settings.unit ?? ''}` : '');
if (this.id && this.context && this.settings && this.settings.field) {
if (value === null) {
@@ -119,7 +131,9 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
} else {
this.context.queryFragments[this.id] = `${this.settings.field}:[0 TO ${value}]`;
}
if (updateContext) {
this.context.update();
}
}
}
}

View File

@@ -17,27 +17,14 @@
import { SearchSortingPickerComponent } from './search-sorting-picker.component';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { AppConfigService } from '@alfresco/adf-core';
import { SearchConfiguration } from '../../models/search-configuration.interface';
import { TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { AlfrescoApiService } from '../../../services/alfresco-api.service';
import { SearchConfiguration } from '../../models';
describe('SearchSortingPickerComponent', () => {
let queryBuilder: SearchQueryBuilderService;
let fixture: ComponentFixture<SearchSortingPickerComponent>;
let component: SearchSortingPickerComponent;
const buildConfig = (searchSettings): AppConfigService => {
const config = TestBed.inject(AppConfigService);
config.config.search = searchSettings;
return config;
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
});
const config: SearchConfiguration = {
sorting: {
options: [
@@ -49,10 +36,26 @@ describe('SearchSortingPickerComponent', () => {
},
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
queryBuilder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
component = new SearchSortingPickerComponent(queryBuilder);
const queryBuilder = {
getSortingOptions: () => config.sorting.options,
getPrimarySorting: () => config.sorting.defaults[0],
sorting: config.sorting.options,
update: jasmine.createSpy('update')
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule],
providers: [
{
provide: SearchQueryBuilderService,
useValue: queryBuilder
}
]
});
fixture = TestBed.createComponent(SearchSortingPickerComponent);
component = fixture.componentInstance;
});
it('should load options from query builder', () => {
@@ -72,8 +75,6 @@ describe('SearchSortingPickerComponent', () => {
});
it('should update query builder each time selection is changed', () => {
spyOn(queryBuilder, 'update').and.stub();
component.ngOnInit();
component.onValueChanged('description');
@@ -84,8 +85,6 @@ describe('SearchSortingPickerComponent', () => {
});
it('should update query builder each time sorting is changed', () => {
spyOn(queryBuilder, 'update').and.stub();
component.ngOnInit();
component.onSortingChanged(false);

View File

@@ -22,6 +22,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatInputHarness } from '@angular/material/input/testing';
import { MatButtonHarness } from '@angular/material/button/testing';
import { ReplaySubject } from 'rxjs';
describe('SearchTextComponent', () => {
let loader: HarnessLoader;
@@ -40,10 +41,13 @@ describe('SearchTextComponent', () => {
field: 'cm:name',
placeholder: 'Enter the name'
};
component.context = {
queryFragments: {},
update: () => {}
queryFragments: {
slider: ''
},
filterRawParams: {},
populateFilters: new ReplaySubject(1),
update: jasmine.createSpy('update')
} as any;
loader = TestbedHarnessEnvironment.loader(fixture);
@@ -65,8 +69,6 @@ describe('SearchTextComponent', () => {
});
it('should update query builder on change', () => {
spyOn(component.context, 'update').and.stub();
component.onChangedHandler({
target: {
value: 'top-secret.doc'
@@ -75,6 +77,7 @@ describe('SearchTextComponent', () => {
expect(component.value).toBe('top-secret.doc');
expect(component.context.queryFragments[component.id]).toBe(`cm:name:'top-secret.doc'`);
expect(component.context.filterRawParams[component.id]).toBe('top-secret.doc');
expect(component.context.update).toHaveBeenCalled();
});
@@ -87,6 +90,7 @@ describe('SearchTextComponent', () => {
expect(component.value).toBe('top-secret.doc');
expect(component.context.queryFragments[component.id]).toBe(`cm:name:'top-secret.doc'`);
expect(component.context.filterRawParams[component.id]).toBe('top-secret.doc');
component.onChangedHandler({
target: {
@@ -96,6 +100,7 @@ describe('SearchTextComponent', () => {
expect(component.value).toBe('');
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBe('');
});
it('should show the custom/default name', async () => {
@@ -118,10 +123,10 @@ describe('SearchTextComponent', () => {
expect(component.value).toBe('');
expect(component.context.queryFragments[component.id]).toBe('');
expect(component.context.filterRawParams[component.id]).toBeNull();
});
it('should update query with startValue on init, if provided', () => {
spyOn(component.context, 'update');
component.startValue = 'mock-start-value';
fixture.detectChanges();
@@ -132,7 +137,6 @@ describe('SearchTextComponent', () => {
it('should parse value and set query context as blank, and not call query update, if no start value was provided', () => {
component.context.queryFragments[component.id] = `cm:name:'secret.pdf'`;
spyOn(component.context, 'update');
component.startValue = undefined;
fixture.detectChanges();
@@ -140,4 +144,18 @@ describe('SearchTextComponent', () => {
expect(component.value).toBe('secret.pdf');
expect(component.context.update).not.toHaveBeenCalled();
});
it('should populate filter state when populate filters event has been observed', async () => {
component.context.filterLoaded = new ReplaySubject(1);
spyOn(component.context.filterLoaded, 'next').and.stub();
spyOn(component.displayValue$, 'next').and.stub();
fixture.detectChanges();
component.context.populateFilters.next({ text: 'secret.pdf' });
fixture.detectChanges();
expect(component.displayValue$.next).toHaveBeenCalledWith('secret.pdf');
expect(component.value).toBe('secret.pdf');
expect(component.context.filterRawParams[component.id]).toBe('secret.pdf');
expect(component.context.filterLoaded.next).toHaveBeenCalled();
});
});

View File

@@ -19,7 +19,8 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { TranslateModule } from '@ngx-translate/core';
@@ -48,7 +49,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
startValue: string;
isActive = false;
enableChangeUpdate = true;
displayValue$: Subject<string> = new Subject<string>();
displayValue$: ReplaySubject<string> = new ReplaySubject<string>(1);
ngOnInit() {
if (this.context && this.settings && this.settings.pattern) {
@@ -70,6 +71,16 @@ export class SearchTextComponent implements SearchWidget, OnInit {
}
}
}
this.context.populateFilters
.asObservable()
.pipe(first())
.subscribe((filtersQueries) => {
if (filtersQueries[this.id]) {
this.value = filtersQueries[this.id];
this.updateQuery(this.value, false);
this.context.filterLoaded.next();
}
});
}
clear() {
@@ -93,13 +104,16 @@ export class SearchTextComponent implements SearchWidget, OnInit {
}
}
private updateQuery(value: string) {
private updateQuery(value: string, updateContext = true) {
this.context.filterRawParams[this.id] = value;
this.displayValue$.next(value);
if (this.context && this.settings && this.settings.field) {
this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${this.getSearchPrefix()}${value}${this.getSearchSuffix()}'` : '';
if (updateContext) {
this.context.update();
}
}
}
submitValues() {
this.updateQuery(this.value);

View File

@@ -17,7 +17,7 @@
import { SearchWidgetSettings } from './search-widget-settings.interface';
import { SearchQueryBuilderService } from '../services/search-query-builder.service';
import { Subject } from 'rxjs';
import { ReplaySubject } from 'rxjs';
export interface SearchWidget {
id: string;
@@ -27,7 +27,7 @@ export interface SearchWidget {
isActive?: boolean;
startValue: any;
/* stream emit value on changes */
displayValue$: Subject<string>;
displayValue$: ReplaySubject<string>;
/* reset the value and update the search */
reset(): void;
/* update the search with field value */

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Subject, Observable, from, ReplaySubject } from 'rxjs';
import { Subject, Observable, from, ReplaySubject, BehaviorSubject } from 'rxjs';
import { AppConfigService } from '@alfresco/adf-core';
import {
SearchRequest,
@@ -37,8 +37,13 @@ import { FacetField } from '../models/facet-field.interface';
import { FacetFieldBucket } from '../models/facet-field-bucket.interface';
import { SearchForm } from '../models/search-form.interface';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { Buffer } from 'buffer';
import { inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
export abstract class BaseQueryBuilderService {
private router = inject(Router);
private activatedRoute = inject(ActivatedRoute);
private _searchApi: SearchApi;
get searchApi(): SearchApi {
this._searchApi = this._searchApi ?? new SearchApi(this.alfrescoApiService.getInstance());
@@ -48,6 +53,9 @@ export abstract class BaseQueryBuilderService {
/* Stream that emits the search configuration whenever the user change the search forms */
configUpdated = new Subject<SearchConfiguration>();
/* Stream that emits the event each time when search filter finishes loading initial value */
filterLoaded = new Subject<void>();
/* Stream that emits the query before search whenever user search */
updated = new Subject<SearchRequest>();
@@ -60,12 +68,17 @@ export abstract class BaseQueryBuilderService {
/* Stream that emits search forms */
searchForms = new ReplaySubject<SearchForm[]>(1);
/* Stream that emits the initial value for some or all search filters */
populateFilters = new BehaviorSubject<{ [key: string]: any }>({});
categories: SearchCategory[] = [];
queryFragments: { [id: string]: string } = {};
filterQueries: FilterQuery[] = [];
filterRawParams: { [key: string]: any } = {};
paging: { maxItems?: number; skipCount?: number } = null;
sorting: SearchSortingDefinition[] = [];
sortingOptions: SearchSortingDefinition[] = [];
private encodedQuery: string;
private scope: RequestScope;
private selectedConfiguration: number;
private _userQuery = '';
@@ -88,10 +101,7 @@ export abstract class BaseQueryBuilderService {
// TODO: to be supported in future iterations
ranges: { [id: string]: SearchRange } = {};
protected constructor(
protected appConfig: AppConfigService,
protected alfrescoApiService: AlfrescoApiService
) {
protected constructor(protected appConfig: AppConfigService, protected alfrescoApiService: AlfrescoApiService) {
this.resetToDefaults();
}
@@ -297,12 +307,16 @@ export abstract class BaseQueryBuilderService {
/**
* Builds and executes the current query.
*
* @param updateQueryParams whether query params should be updated with encoded query
* @param queryBody query settings
*/
async execute(queryBody?: SearchRequest) {
async execute(updateQueryParams = true, queryBody?: SearchRequest) {
try {
const query = queryBody ? queryBody : this.buildQuery();
if (query) {
if (updateQueryParams) {
this.updateSearchQueryParams();
}
const resultSetPaging: ResultSetPaging = await this.searchApi.search(query);
this.executed.next(resultSetPaging);
}
@@ -461,9 +475,9 @@ export abstract class BaseQueryBuilderService {
end: set.end,
startInclusive: set.startInclusive,
endInclusive: set.endInclusive
}) as any
} as any)
)
}) as any
} as any)
)
};
}
@@ -477,6 +491,9 @@ export abstract class BaseQueryBuilderService {
protected getFinalQuery(): string {
let query = '';
if (this.userQuery) {
this.filterRawParams['userQuery'] = this.userQuery;
}
this.categories.forEach((facet) => {
const customQuery = this.queryFragments[facet.id];
@@ -522,7 +539,7 @@ export abstract class BaseQueryBuilderService {
limit: facet.limit,
offset: facet.offset,
prefix: facet.prefix
}) as any
} as any)
)
};
}
@@ -543,4 +560,38 @@ export abstract class BaseQueryBuilderService {
}
return configLabel;
}
/**
* Encodes filter configuration stored in filterRawParams object.
*/
encodeQuery() {
this.encodedQuery = Buffer.from(JSON.stringify(this.filterRawParams)).toString('base64');
}
/**
* Encodes existing filters configuration and updates search query param value.
*/
updateSearchQueryParams() {
this.encodeQuery();
this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams: { q: this.encodedQuery },
queryParamsHandling: 'merge'
});
}
/**
* Builds search query with provided user query, executes query, encodes latest filter config and navigates to search.
*
* @param query user query to search for
* @param searchUrl search url to navigate to
*/
async navigateToSearch(query: string, searchUrl: string) {
this.userQuery = query;
await this.execute();
await this.router.navigate([searchUrl], {
queryParams: { q: this.encodedQuery },
queryParamsHandling: 'merge'
});
}
}

View File

@@ -69,7 +69,7 @@ export class SearchFacetFiltersService implements OnDestroy {
this.responseFacets = null;
});
this.queryBuilder.updated.pipe(takeUntil(this.onDestroy$)).subscribe((query) => this.queryBuilder.execute(query));
this.queryBuilder.updated.pipe(takeUntil(this.onDestroy$)).subscribe((query) => this.queryBuilder.execute(true, query));
this.queryBuilder.executed.pipe(takeUntil(this.onDestroy$)).subscribe((resultSetPaging: ResultSetPaging) => {
this.onDataLoaded(resultSetPaging);

View File

@@ -23,7 +23,6 @@ import { ContentTestingModule } from '../../testing/content.testing.module';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
describe('SearchHeaderQueryBuilderService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
@@ -36,21 +35,22 @@ describe('SearchHeaderQueryBuilderService', () => {
return config;
};
const createQueryBuilder = (searchSettings): SearchHeaderQueryBuilderService => {
let builder: SearchHeaderQueryBuilderService;
TestBed.runInInjectionContext(() => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
builder = new SearchHeaderQueryBuilderService(buildConfig(searchSettings), alfrescoApiService, null);
});
return builder;
};
it('should load the configuration from app config', () => {
const config: SearchConfiguration = {
categories: [
{ id: 'cat1', enabled: true } as any,
{ id: 'cat2', enabled: true } as any
],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchHeaderQueryBuilderService(
buildConfig(config),
alfrescoApiService,
null
);
const builder = createQueryBuilder(config);
builder.categories = [];
builder.filterQueries = [];
@@ -73,12 +73,7 @@ describe('SearchHeaderQueryBuilderService', () => {
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const service = new SearchHeaderQueryBuilderService(
buildConfig(config),
alfrescoApiService,
null
);
const service = createQueryBuilder(config);
const category = service.getCategoryForColumn('fake-key-1');
expect(category).not.toBeNull();
@@ -87,34 +82,19 @@ describe('SearchHeaderQueryBuilderService', () => {
});
it('should have empty user query by default', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchHeaderQueryBuilderService(
buildConfig({}),
alfrescoApiService,
null
);
const builder = createQueryBuilder({});
expect(builder.userQuery).toBe('');
});
it('should add the extra filter for the parent node', () => {
const config: SearchConfiguration = {
categories: [
{ id: 'cat1', enabled: true } as any,
{ id: 'cat2', enabled: true } as any
],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const expectedResult = [
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }
];
const expectedResult = [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }];
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const searchHeaderService = new SearchHeaderQueryBuilderService(
buildConfig(config),
alfrescoApiService,
null
);
const searchHeaderService = createQueryBuilder(config);
searchHeaderService.setCurrentRootFolderId('fake-node-id');
@@ -122,52 +102,28 @@ describe('SearchHeaderQueryBuilderService', () => {
});
it('should not add again the parent filter if that node is already added', () => {
const expectedResult = [
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }
];
const expectedResult = [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }];
const config: SearchConfiguration = {
categories: [
{ id: 'cat1', enabled: true } as any,
{ id: 'cat2', enabled: true } as any
],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: expectedResult
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const searchHeaderService = new SearchHeaderQueryBuilderService(
buildConfig(config),
alfrescoApiService,
null
);
const searchHeaderService = createQueryBuilder(config);
searchHeaderService.setCurrentRootFolderId('fake-node-id');
expect(searchHeaderService.filterQueries).toEqual(
expectedResult,
'Filters are not as expected'
);
expect(searchHeaderService.filterQueries).toEqual(expectedResult, 'Filters are not as expected');
});
it('should not add duplicate column names in activeFilters', () => {
const activeFilter = 'FakeColumn';
const config: SearchConfiguration = {
categories: [
{ id: 'cat1', enabled: true } as any
],
filterQueries: [
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id' }
]
categories: [{ id: 'cat1', enabled: true } as any],
filterQueries: [{ query: 'PARENT:"workspace://SpacesStore/fake-node-id' }]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const searchHeaderService = new SearchHeaderQueryBuilderService(
buildConfig(config),
alfrescoApiService,
null
);
const searchHeaderService = createQueryBuilder(config);
expect(searchHeaderService.activeFilters.length).toBe(0);

View File

@@ -23,6 +23,16 @@ import { FacetField } from '../models/facet-field.interface';
import { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { ADF_SEARCH_CONFIGURATION } from '../search-configuration.token';
import { ActivatedRoute, Router } from '@angular/router';
const buildConfig = (searchSettings = {}): AppConfigService => {
let config: AppConfigService;
TestBed.runInInjectionContext(() => {
config = TestBed.inject(AppConfigService);
});
config.config.search = searchSettings;
return config;
};
describe('SearchQueryBuilder (runtime config)', () => {
const runtimeConfig: SearchConfiguration = {};
@@ -30,20 +40,15 @@ describe('SearchQueryBuilder (runtime config)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule],
providers: [
{ provide: ADF_SEARCH_CONFIGURATION, useValue: runtimeConfig }
]
providers: [{ provide: ADF_SEARCH_CONFIGURATION, useValue: runtimeConfig }]
});
});
const buildConfig = (searchSettings): AppConfigService => {
const config = TestBed.inject(AppConfigService);
config.config.search = searchSettings;
return config;
};
it('should use custom search configuration via dependency injection', () => {
const builder = TestBed.inject(SearchQueryBuilderService);
let builder: SearchQueryBuilderService;
TestBed.runInInjectionContext(() => {
builder = TestBed.inject(SearchQueryBuilderService);
});
const currentConfig = builder.loadConfiguration();
expect(currentConfig).toEqual(runtimeConfig);
@@ -54,8 +59,13 @@ describe('SearchQueryBuilder (runtime config)', () => {
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService, runtimeConfig);
let alfrescoApiService: AlfrescoApiService;
let builder: SearchQueryBuilderService;
TestBed.runInInjectionContext(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService, runtimeConfig);
});
const currentConfig = builder.loadConfiguration();
expect(currentConfig).toEqual(runtimeConfig);
@@ -63,16 +73,24 @@ describe('SearchQueryBuilder (runtime config)', () => {
});
describe('SearchQueryBuilder', () => {
let router: Router;
let activatedRoute: ActivatedRoute;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
});
router = TestBed.inject(Router);
activatedRoute = TestBed.inject(ActivatedRoute);
});
const buildConfig = (searchSettings = {}): AppConfigService => {
const config = TestBed.inject(AppConfigService);
config.config.search = searchSettings;
return config;
const createQueryBuilder = (config?: any) => {
let builder: SearchQueryBuilderService;
TestBed.runInInjectionContext(() => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
});
return builder;
};
it('should reset to defaults', () => {
@@ -80,8 +98,8 @@ describe('SearchQueryBuilder', () => {
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.categories = [];
builder.filterQueries = [];
@@ -96,23 +114,18 @@ describe('SearchQueryBuilder', () => {
});
it('should have empty user query by default', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
expect(builder.userQuery).toBe('');
});
it('should wrap user query with brackets', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.userQuery = 'my query';
expect(builder.userQuery).toEqual('(my query)');
});
it('should trim user query value', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.userQuery = ' something ';
expect(builder.userQuery).toEqual('(something)');
});
@@ -121,9 +134,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: false } as any, { id: 'cat3', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
expect(builder.categories.length).toBe(2);
expect(builder.categories[0].id).toBe('cat1');
@@ -135,8 +146,7 @@ describe('SearchQueryBuilder', () => {
categories: [],
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
expect(builder.filterQueries.length).toBe(2);
expect(builder.filterQueries[0].query).toBe('query1');
@@ -144,10 +154,7 @@ describe('SearchQueryBuilder', () => {
});
it('should add new filter query', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.addFilterQuery('q1');
expect(builder.filterQueries.length).toBe(1);
@@ -155,10 +162,7 @@ describe('SearchQueryBuilder', () => {
});
it('should not add empty filter query', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.addFilterQuery(null);
builder.addFilterQuery('');
@@ -166,10 +170,7 @@ describe('SearchQueryBuilder', () => {
});
it('should not add duplicate filter query', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.addFilterQuery('q1');
builder.addFilterQuery('q1');
builder.addFilterQuery('q1');
@@ -179,10 +180,7 @@ describe('SearchQueryBuilder', () => {
});
it('should remove filter query', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.addFilterQuery('q1');
builder.addFilterQuery('q2');
expect(builder.filterQueries.length).toBe(2);
@@ -193,9 +191,7 @@ describe('SearchQueryBuilder', () => {
});
it('should not remove empty query', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.addFilterQuery('q1');
builder.addFilterQuery('q2');
expect(builder.filterQueries.length).toBe(2);
@@ -215,9 +211,7 @@ describe('SearchQueryBuilder', () => {
]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
const query = builder.getFacetQuery('query2');
expect(query.query).toBe('q2');
@@ -231,9 +225,7 @@ describe('SearchQueryBuilder', () => {
queries: [{ query: 'q1', label: 'query1' }]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
const query1 = builder.getFacetQuery('');
expect(query1).toBeNull();
@@ -252,9 +244,7 @@ describe('SearchQueryBuilder', () => {
]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
const field = builder.getFacetField('Size');
expect(field.label).toBe('Size');
@@ -271,9 +261,7 @@ describe('SearchQueryBuilder', () => {
]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
const field = builder.getFacetField('Missing');
expect(field).toBeFalsy();
@@ -286,9 +274,7 @@ describe('SearchQueryBuilder', () => {
fields: [{ field: 'content.size', mincount: 1, label: 'Label with spaces' }]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
const field = builder.getFacetField('Label with spaces');
expect(field.label).toBe('"Label with spaces"');
@@ -299,9 +285,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = null;
const compiled = builder.buildQuery();
@@ -312,10 +296,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
@@ -326,9 +307,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
builder.queryFragments['cat2'] = 'NOT cm:creator:System';
@@ -342,9 +321,7 @@ describe('SearchQueryBuilder', () => {
fields: ['field1', 'field2'],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
@@ -357,10 +334,7 @@ describe('SearchQueryBuilder', () => {
fields: [],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
@@ -371,9 +345,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
builder.addFilterQuery('query1');
@@ -388,9 +360,7 @@ describe('SearchQueryBuilder', () => {
queries: [{ query: 'q1', label: 'q2', group: 'group-name' }]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
@@ -407,9 +377,7 @@ describe('SearchQueryBuilder', () => {
]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
@@ -449,9 +417,7 @@ describe('SearchQueryBuilder', () => {
]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
@@ -485,9 +451,7 @@ describe('SearchQueryBuilder', () => {
]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
@@ -531,9 +495,7 @@ describe('SearchQueryBuilder', () => {
]
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
@@ -550,8 +512,7 @@ describe('SearchQueryBuilder', () => {
fields: [],
categories: [{ id: 'cat1', enabled: true } as any, { id: 'cat2', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
const sorting: any = { type: 'FIELD', field: 'cm:name', ascending: true };
builder.sorting = [sorting];
@@ -565,8 +526,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.queryFragments['cat1'] = 'cm:name:test';
builder.paging = { maxItems: 5, skipCount: 5 };
@@ -581,8 +541,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.userQuery = 'my query';
builder.queryFragments['cat1'] = 'cm:name:test';
@@ -615,9 +574,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.addUserFacetBucket(field1.field, field1buckets[0]);
builder.addUserFacetBucket(field1.field, field1buckets[1]);
@@ -638,9 +595,7 @@ describe('SearchQueryBuilder', () => {
mergeContiguous: true
}
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.userQuery = 'my query';
builder.queryFragments['cat1'] = 'cm:name:test';
@@ -655,9 +610,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
spyOn(builder, 'buildQuery').and.throwError('some error');
builder.error.subscribe((error) => {
@@ -671,9 +624,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
categories: [{ id: 'cat1', enabled: true } as any]
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
spyOn(builder, 'buildQuery').and.throwError('some error');
builder.executed.subscribe((data) => {
@@ -686,9 +637,7 @@ describe('SearchQueryBuilder', () => {
});
it('should include contain the path and allowableOperations by default', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.userQuery = 'nuka cola quantum';
const searchRequest = builder.buildQuery();
@@ -700,9 +649,7 @@ describe('SearchQueryBuilder', () => {
const config: SearchConfiguration = {
include: includeConfig
};
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(config), alfrescoApiService);
const builder = createQueryBuilder(config);
builder.userQuery = 'nuka cola quantum';
const searchRequest = builder.buildQuery();
@@ -710,9 +657,7 @@ describe('SearchQueryBuilder', () => {
});
it('should the query contain the pagination', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
builder.userQuery = 'nuka cola quantum';
const mockPagination = {
maxItems: 10,
@@ -725,9 +670,7 @@ describe('SearchQueryBuilder', () => {
});
it('should the query contain the scope in case it is defined', () => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(), alfrescoApiService);
const builder = createQueryBuilder();
const mockScope = { locations: 'mock-location' };
builder.userQuery = 'nuka cola quantum';
builder.setScope(mockScope);
@@ -737,15 +680,50 @@ describe('SearchQueryBuilder', () => {
});
it('should return empty if array of search config not found', (done) => {
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
const builder = new SearchQueryBuilderService(buildConfig(null), alfrescoApiService);
const builder = createQueryBuilder(null);
builder.searchForms.subscribe((forms) => {
expect(forms).toEqual([]);
done();
});
});
it('should add user query to filter raw params when query is built', () => {
const builder = createQueryBuilder();
builder.userQuery = 'nuka cola quantum';
builder.buildQuery();
expect(builder.filterRawParams).toEqual({ userQuery: '(nuka cola quantum)' });
});
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)' });
expect(router.navigate).toHaveBeenCalledWith([], {
relativeTo: activatedRoute,
queryParams: { q: 'eyJ1c2VyUXVlcnkiOiIobnVrYSBjb2xhIHF1YW50dW0pIn0=' },
queryParamsHandling: 'merge'
});
done();
});
builder.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');
expect(builder.filterRawParams).toEqual({ userQuery: '(test query)' });
expect(router.navigate).toHaveBeenCalledWith([], {
relativeTo: activatedRoute,
queryParams: { q: 'eyJ1c2VyUXVlcnkiOiIodGVzdCBxdWVyeSkifQ==' },
queryParamsHandling: 'merge'
});
});
describe('Multiple search configuration', () => {
let configs: SearchConfiguration[];
let builder: SearchQueryBuilderService;
@@ -768,9 +746,7 @@ describe('SearchQueryBuilder', () => {
default: false
}
];
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
builder = new SearchQueryBuilderService(buildConfig(configs), alfrescoApiService);
builder = createQueryBuilder(configs);
});
it('should pick the default configuration from list', () => {