mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[MNT-25681] Search refactoring and unification (#5257)
* [MNT-25681] Search refactoring and unification * [MNT-25681] CR fixes * [MNT-25681] ADF version bump * [MNT-25681] Two way binding on search input * MNT-25681] search e2es fixes * [MNT-25681] search e2es stabilization pt 2 * [MNT-25149] Add unit tests for multivalued select parameter default value (#5262) * [MNT-25149] Add unit tests for multivalued select parameter default value * address comment on pr * [MNT-25681] Fix content node selector panel issues * [PRODSEC-13253] Upgraded angular version to fix vulnerability and to have the same versions like ADF * [MNT-25681] Unit test fix * [MNT-25681] Search input fix --------- Co-authored-by: Adam Świderski <adam.tomasz.swiderski@gmail.com> Co-authored-by: Akash Rathod <41251473+akashrathod28@users.noreply.github.com> Co-authored-by: Aleksander Sklorz <Aleksander.Sklorz@hyland.com>
This commit is contained in:
co-authored by
Adam Świderski
Akash Rathod
Aleksander Sklorz
parent
e2df1e352c
commit
1695f17b7c
@@ -1770,7 +1770,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"aca:triggeredOnChange": false,
|
||||
"resetButton": true,
|
||||
"filterQueries": [
|
||||
{ "query": "+TYPE:'cm:folder' OR +TYPE:'cm:content'" },
|
||||
@@ -1991,7 +1990,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"aca:triggeredOnChange": false,
|
||||
"resetButton": true,
|
||||
"filterQueries": [
|
||||
{ "query": "+TYPE:'cm:folder' OR +TYPE:'cm:content' AND +ASPECT:'cm:dublincore'" },
|
||||
@@ -2167,7 +2165,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"aca:triggeredOnChange": false,
|
||||
"resetButton": true,
|
||||
"filterQueries": [
|
||||
{ "query": "+TYPE:'cm:folder' OR +TYPE:'cm:content' AND +ASPECT:'cm:effectivity'" },
|
||||
|
||||
@@ -623,11 +623,13 @@
|
||||
"MIN_LENGTH": "Search input must have at least 2 alphanumeric characters.",
|
||||
"REQUIRED": "Search input is required.",
|
||||
"WHITESPACE": "Search input cannot be only whitespace.",
|
||||
"OPERATORS": "Search input cannot begin with, end with or contain only operators.",
|
||||
"IN_PREFIX": "In",
|
||||
"FILES_AND_FOLDERS": "Files and folders",
|
||||
"RESET": "Reset",
|
||||
"SEARCH_IN": "Search in"
|
||||
"SEARCH_IN": "Search in",
|
||||
"SEARCH_MODE_TOGGLE_DESCRIPTION": "Switch search mode.\n\nRegular search: Find text as you enter it.\n\nFormula search: Use special characters to build a query. The system reads symbols as search syntax.",
|
||||
"SEARCH_MODE_TOGGLE_DESCRIPTION_REGULAR": "Regular search",
|
||||
"SEARCH_MODE_TOGGLE_DESCRIPTION_FORMULA": "Formula search"
|
||||
},
|
||||
"SORT": {
|
||||
"SORTING_OPTION": "Sort by",
|
||||
|
||||
+4
-2
@@ -187,7 +187,8 @@ describe('RuleActionUiComponent', () => {
|
||||
title: 'ACA_FOLDER_RULES.RULE_DETAILS.PLACEHOLDER.CHOOSE_FOLDER',
|
||||
actionName: NodeAction.CHOOSE,
|
||||
currentFolderId: component.nodeId,
|
||||
select: jasmine.any(Subject)
|
||||
select: jasmine.any(Subject),
|
||||
showFilesInResult: false
|
||||
};
|
||||
const dialogSpy = spyOn(dialog, 'open').and.returnValue({ afterClosed: () => of({}) } as MatDialogRef<any>);
|
||||
fixture.detectChanges();
|
||||
@@ -361,7 +362,8 @@ describe('RuleActionUiComponent', () => {
|
||||
title: 'ACA_FOLDER_RULES.RULE_DETAILS.PLACEHOLDER.CHOOSE_FOLDER',
|
||||
actionName: NodeAction.CHOOSE,
|
||||
currentFolderId: 'test-folder-id',
|
||||
select: jasmine.any(Subject)
|
||||
select: jasmine.any(Subject),
|
||||
showFilesInResult: false
|
||||
},
|
||||
panelClass: 'adf-content-node-selector-dialog',
|
||||
width: '630px'
|
||||
|
||||
+2
-1
@@ -310,7 +310,8 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnCh
|
||||
title: this.translate.instant('ACA_FOLDER_RULES.RULE_DETAILS.PLACEHOLDER.CHOOSE_FOLDER'),
|
||||
actionName: NodeAction.CHOOSE,
|
||||
currentFolderId: this.nodeId,
|
||||
select: new Subject<Node[]>()
|
||||
select: new Subject<Node[]>(),
|
||||
showFilesInResult: false
|
||||
};
|
||||
|
||||
this.dialog.open(ContentNodeSelectorComponent, {
|
||||
|
||||
@@ -45,7 +45,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { testHeader } from '../../testing/document-base-page-utils';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { getCurrentFolder } from '@alfresco/aca-shared/store';
|
||||
import { UnitTestingUtils } from '@alfresco/adf-core';
|
||||
import { ShowHeaderMode, UnitTestingUtils } from '@alfresco/adf-core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
describe('FilesComponent', () => {
|
||||
@@ -496,6 +496,28 @@ describe('FilesComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFilterSelected', () => {
|
||||
it('should activate the filter header without navigating when filters are selected', () => {
|
||||
router.navigate['calls'].reset();
|
||||
|
||||
component.onFilterSelected([{ key: 'name', value: 'aaa' } as FilterSearch]);
|
||||
|
||||
expect(component.isFilterHeaderActive).toBeTrue();
|
||||
expect(component.showHeader).toBe(ShowHeaderMode.Always);
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deactivate the filter header and navigate to the current route when no filters are selected', () => {
|
||||
router.navigate['calls'].reset();
|
||||
|
||||
component.onFilterSelected([]);
|
||||
|
||||
expect(component.isFilterHeaderActive).toBeFalse();
|
||||
expect(component.showHeader).toBe(ShowHeaderMode.Data);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['.'], jasmine.objectContaining({ relativeTo: route }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSiteContainer', () => {
|
||||
it('should return false if node has no aspectNames', () => {
|
||||
const mock: any = { aspectNames: [] };
|
||||
|
||||
@@ -386,7 +386,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
||||
if (activeFilters.length) {
|
||||
this.showHeader = ShowHeaderMode.Always;
|
||||
this.isFilterHeaderActive = true;
|
||||
this.navigateToFilter(activeFilters);
|
||||
} else {
|
||||
void this.router.navigate(['.'], { relativeTo: this.route });
|
||||
this.isFilterHeaderActive = false;
|
||||
@@ -394,21 +393,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
navigateToFilter(activeFilters: FilterSearch[]) {
|
||||
const objectFromMap = {};
|
||||
activeFilters.forEach((filter: FilterSearch) => {
|
||||
let paramValue;
|
||||
if (filter?.value?.from && filter?.value?.to) {
|
||||
paramValue = `${filter.value.from}||${filter.value.to}`;
|
||||
} else {
|
||||
paramValue = filter.value;
|
||||
}
|
||||
objectFromMap[filter.key] = paramValue;
|
||||
});
|
||||
|
||||
void this.router.navigate([], { relativeTo: this.route, queryParams: objectFromMap });
|
||||
}
|
||||
|
||||
onError(error: HttpErrorResponse) {
|
||||
this.isValidPath = false;
|
||||
if (this.router.url.includes('libraries')) {
|
||||
|
||||
@@ -27,7 +27,6 @@ import { Store } from '@ngrx/store';
|
||||
import { SearchExecutionService } from './search-execution.service';
|
||||
import { SearchFilterService } from './search-filter.service';
|
||||
import { SearchNavigationService } from './search-navigation.service';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { SearchLibrariesQueryBuilderService } from './search-libraries-results/search-libraries-query-builder.service';
|
||||
import { AppStore, SearchActionTypes } from '@alfresco/aca-shared/store';
|
||||
|
||||
@@ -36,7 +35,6 @@ describe('SearchExecutionService', () => {
|
||||
let store: jasmine.SpyObj<Store<AppStore>>;
|
||||
let filterService: jasmine.SpyObj<SearchFilterService>;
|
||||
let navigationService: jasmine.SpyObj<SearchNavigationService>;
|
||||
let queryBuilder: jasmine.SpyObj<SearchQueryBuilderService>;
|
||||
let queryLibrariesBuilder: jasmine.SpyObj<SearchLibrariesQueryBuilderService>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -48,7 +46,6 @@ describe('SearchExecutionService', () => {
|
||||
onSearchResults: false,
|
||||
onLibrariesSearchResults: false
|
||||
});
|
||||
queryBuilder = jasmine.createSpyObj('SearchQueryBuilderService', ['update']);
|
||||
queryLibrariesBuilder = jasmine.createSpyObj('SearchLibrariesQueryBuilderService', ['update']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -57,7 +54,6 @@ describe('SearchExecutionService', () => {
|
||||
{ provide: Store, useValue: store },
|
||||
{ provide: SearchFilterService, useValue: filterService },
|
||||
{ provide: SearchNavigationService, useValue: navigationService },
|
||||
{ provide: SearchQueryBuilderService, useValue: queryBuilder },
|
||||
{ provide: SearchLibrariesQueryBuilderService, useValue: queryLibrariesBuilder }
|
||||
]
|
||||
});
|
||||
@@ -87,6 +83,24 @@ describe('SearchExecutionService', () => {
|
||||
service.execute('test');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ type: SearchActionTypes.SearchByTerm, payload: 'test' }));
|
||||
});
|
||||
|
||||
it('should NOT dispatch when already on search results with the same term', () => {
|
||||
Object.defineProperty(navigationService, 'onSearchResults', { get: () => true });
|
||||
navigationService.isSameSearchTerm.and.returnValue(true);
|
||||
|
||||
service.execute('test');
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should dispatch when already on search results with a different term', () => {
|
||||
Object.defineProperty(navigationService, 'onSearchResults', { get: () => true });
|
||||
navigationService.isSameSearchTerm.and.returnValue(false);
|
||||
|
||||
service.execute('test');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ type: SearchActionTypes.SearchByTerm, payload: 'test' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('libraries search', () => {
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore, SearchByTermAction } from '@alfresco/aca-shared/store';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { SearchFilterService } from './search-filter.service';
|
||||
import { SearchNavigationService } from './search-navigation.service';
|
||||
import { SearchLibrariesQueryBuilderService } from './search-libraries-results/search-libraries-query-builder.service';
|
||||
@@ -33,7 +32,6 @@ import { SearchLibrariesQueryBuilderService } from './search-libraries-results/s
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SearchExecutionService {
|
||||
private readonly store = inject<Store<AppStore>>(Store);
|
||||
private readonly queryBuilder = inject(SearchQueryBuilderService);
|
||||
private readonly queryLibrariesBuilder = inject(SearchLibrariesQueryBuilderService);
|
||||
private readonly filterService = inject(SearchFilterService);
|
||||
private readonly searchNavigationService = inject(SearchNavigationService);
|
||||
@@ -61,9 +59,7 @@ export class SearchExecutionService {
|
||||
}
|
||||
|
||||
private executeContentSearch(searchedWord: string) {
|
||||
if (this.searchNavigationService.onSearchResults && this.searchNavigationService.isSameSearchTerm(searchedWord)) {
|
||||
this.queryBuilder.update();
|
||||
} else {
|
||||
if (!(this.searchNavigationService.onSearchResults && this.searchNavigationService.isSameSearchTerm(searchedWord))) {
|
||||
this.store.dispatch(new SearchByTermAction(searchedWord, this.filterService.searchOptions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,10 @@ describe('SearchFilterService', () => {
|
||||
expect(service.validateSearchTerm(' ')).toBe('SEARCH.INPUT.WHITESPACE');
|
||||
});
|
||||
|
||||
it('should return error for term starting with operator', () => {
|
||||
expect(service.validateSearchTerm('+test')).toBe('SEARCH.INPUT.OPERATORS');
|
||||
it('should not return an operator error for terms containing special characters', () => {
|
||||
expect(service.validateSearchTerm('+test')).toBeNull();
|
||||
expect(service.validateSearchTerm('AND test')).toBeNull();
|
||||
expect(service.validateSearchTerm('test*')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return error for single char in libraries mode', () => {
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { SearchOptionIds, SearchOptionModel } from '@alfresco/aca-shared/store';
|
||||
import { isOperator } from '../../utils/aca-search-utils';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SearchFilterService {
|
||||
@@ -96,17 +95,10 @@ export class SearchFilterService {
|
||||
return 'SEARCH.INPUT.WHITESPACE';
|
||||
}
|
||||
|
||||
const words = term.trim().split(/\s+/);
|
||||
if (isOperator(words[0]) || isOperator(words[words.length - 1])) {
|
||||
return 'SEARCH.INPUT.OPERATORS';
|
||||
}
|
||||
|
||||
if (/^[+\-|!(){}[\]^"~*?:\\/]/.test(term) || /[+\-|!(){}[\]^"~*?:\\/]$/.test(term)) {
|
||||
return 'SEARCH.INPUT.OPERATORS';
|
||||
}
|
||||
if (this.searchInMode === SearchOptionIds.Libraries && term.length < 2) {
|
||||
return 'SEARCH.INPUT.MIN_LENGTH';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -17,12 +17,21 @@
|
||||
[attr.aria-label]="'SEARCH.INPUT.ARIA-LABEL' | translate"
|
||||
type="text"
|
||||
[(ngModel)]="searchedWord"
|
||||
(keydown.enter)="onSearchSubmit(searchedWord)"
|
||||
(keydown.enter)="onSearchSubmit($event)"
|
||||
[placeholder]="'SEARCH.INPUT.PLACEHOLDER' | translate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<div matSuffix class="aca-search-input--suffix">
|
||||
<mat-button-toggle-group class="aca-search-input--suffix-button-toggle-group" [attr.aria-label]="'SEARCH.INPUT.SEARCH_MODE_TOGGLE_DESCRIPTION' | translate" [attr.title]="'SEARCH.INPUT.SEARCH_MODE_TOGGLE_DESCRIPTION' | translate" [(ngModel)]="queryBuilder.searchMode">
|
||||
<mat-button-toggle value="regular" [attr.aria-label]="'SEARCH.INPUT.SEARCH_MODE_TOGGLE_DESCRIPTION_REGULAR' | translate">
|
||||
<mat-icon>title</mat-icon>
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="formula" [attr.aria-label]="'SEARCH.INPUT.SEARCH_MODE_TOGGLE_DESCRIPTION_FORMULA' | translate">
|
||||
<mat-icon>extension</mat-icon>
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<aca-search-in-menu (filtersApplied)="onFiltersApplied()" />
|
||||
|
||||
<button
|
||||
|
||||
+15
@@ -1,4 +1,5 @@
|
||||
@use '@angular/material' as mat;
|
||||
@use '../../../ui/mat-selectors' as ms;
|
||||
|
||||
$search-height: 48px;
|
||||
$search-border-radius: 4px;
|
||||
@@ -56,6 +57,20 @@ $search-border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&-button-toggle-group {
|
||||
@include mat.button-toggle-overrides(
|
||||
(
|
||||
height: 30px
|
||||
)
|
||||
);
|
||||
|
||||
#{ms.$mat-icon} {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width <= 959px) {
|
||||
|
||||
+65
-35
@@ -26,35 +26,47 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { SearchInputComponent } from './search-input.component';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ActivatedRoute, Event, NavigationStart, Params, Router } from '@angular/router';
|
||||
import { SearchConfiguration, SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { ActivatedRoute, Event, Params, Router } from '@angular/router';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { SearchNavigationService } from '../search-navigation.service';
|
||||
import { SearchFilterService } from '../search-filter.service';
|
||||
import { SearchExecutionService } from '../search-execution.service';
|
||||
import { AppHookService } from '@alfresco/aca-shared';
|
||||
import { UnitTestingUtils } from '@alfresco/adf-core';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { MatButtonToggleGroupHarness } from '@angular/material/button-toggle/testing';
|
||||
|
||||
describe('SearchInputComponent', () => {
|
||||
let fixture: ComponentFixture<SearchInputComponent>;
|
||||
let component: SearchInputComponent;
|
||||
let router: Router;
|
||||
let queryBuilder: Partial<SearchQueryBuilderService>;
|
||||
let searchExecutionService: jasmine.SpyObj<SearchExecutionService>;
|
||||
let searchFilterService: jasmine.SpyObj<SearchFilterService>;
|
||||
let searchNavigationService: jasmine.SpyObj<SearchNavigationService>;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
let loader: HarnessLoader;
|
||||
|
||||
const routerEventsSubject = new Subject<Event>();
|
||||
const configUpdatedSubject = new Subject<SearchConfiguration>();
|
||||
const queryParamsSubject = new Subject<Params>();
|
||||
const library400ErrorSubject = new Subject<void>();
|
||||
|
||||
const submitSearch = (value: string) => {
|
||||
const input = testingUtils.getInputByCSS('.app-search-input');
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
testingUtils.keyBoardEventByCSS('.app-search-input', 'keydown', 'Enter', 'Enter');
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const queryBuilderSpy = {
|
||||
configUpdated: configUpdatedSubject,
|
||||
queryBuilder = {
|
||||
searchMode: 'regular',
|
||||
removeFilterQuery: jasmine.createSpy('removeFilterQuery'),
|
||||
addFilterQuery: jasmine.createSpy('addFilterQuery'),
|
||||
update: jasmine.createSpy('update')
|
||||
addFilterQuery: jasmine.createSpy('addFilterQuery')
|
||||
} as Partial<SearchQueryBuilderService>;
|
||||
|
||||
searchExecutionService = jasmine.createSpyObj<SearchExecutionService>('SearchExecutionService', ['execute']);
|
||||
@@ -83,7 +95,7 @@ describe('SearchInputComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule, SearchInputComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: SearchQueryBuilderService, useValue: queryBuilderSpy },
|
||||
{ provide: SearchQueryBuilderService, useValue: queryBuilder },
|
||||
{ provide: SearchExecutionService, useValue: searchExecutionService },
|
||||
{ provide: SearchFilterService, useValue: searchFilterService },
|
||||
{ provide: SearchNavigationService, useValue: searchNavigationService },
|
||||
@@ -100,6 +112,7 @@ describe('SearchInputComponent', () => {
|
||||
fixture = TestBed.createComponent(SearchInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -134,38 +147,54 @@ describe('SearchInputComponent', () => {
|
||||
|
||||
describe('onSearchSubmit', () => {
|
||||
it('should execute search on submit with valid term', () => {
|
||||
component.onSearchSubmit('happy faces only');
|
||||
submitSearch('happy faces only');
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledWith('happy faces only');
|
||||
});
|
||||
|
||||
it('should not execute search when search term is empty', () => {
|
||||
searchFilterService.validateSearchTerm.and.returnValue('SEARCH.ERRORS.EMPTY_QUERY');
|
||||
component.onSearchSubmit('');
|
||||
submitSearch('');
|
||||
expect(searchExecutionService.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not execute search when search term is whitespace', () => {
|
||||
searchFilterService.validateSearchTerm.and.returnValue('SEARCH.ERRORS.EMPTY_QUERY');
|
||||
component.onSearchSubmit(' ');
|
||||
submitSearch(' ');
|
||||
expect(searchExecutionService.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set error when validation fails', () => {
|
||||
searchFilterService.validateSearchTerm.and.returnValue('SEARCH.ERRORS.EMPTY_QUERY');
|
||||
component.onSearchSubmit('');
|
||||
submitSearch('');
|
||||
expect(component.error).toBe('SEARCH.ERRORS.EMPTY_QUERY');
|
||||
});
|
||||
|
||||
it('should clear error on valid submission', () => {
|
||||
component.error = 'some error';
|
||||
component.onSearchSubmit('valid term');
|
||||
submitSearch('valid term');
|
||||
expect(component.error).toBe('');
|
||||
});
|
||||
|
||||
it('should trim whitespace from search term', () => {
|
||||
component.onSearchSubmit(' hello ');
|
||||
expect(component.searchedWord).toBe('hello');
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledWith('hello');
|
||||
it('should track the trimmed term as the last searched word', () => {
|
||||
submitSearch(' hello ');
|
||||
expect(component.lastSearchedWord).toBe('hello');
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not re-execute search when submitted term is unchanged', () => {
|
||||
submitSearch('hello');
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledTimes(1);
|
||||
|
||||
submitSearch('hello');
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not re-execute search when only surrounding whitespace changes', () => {
|
||||
submitSearch('hello');
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledTimes(1);
|
||||
|
||||
submitSearch(' hello ');
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,29 +283,30 @@ describe('SearchInputComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryBuilder configUpdated handling', () => {
|
||||
it('should execute search when searchedWord is set and navigation has query params', () => {
|
||||
component.searchedWord = 'term';
|
||||
routerEventsSubject.next(new NavigationStart(1, '/path?q=term'));
|
||||
configUpdatedSubject.next({ id: 'config1' } as SearchConfiguration);
|
||||
describe('search mode toggle', () => {
|
||||
const getToggleGroup = (): Promise<MatButtonToggleGroupHarness> =>
|
||||
loader.getHarness(MatButtonToggleGroupHarness.with({ selector: '.aca-search-input--suffix-button-toggle-group' }));
|
||||
|
||||
expect(searchExecutionService.execute).toHaveBeenCalledWith('term');
|
||||
it('should render the regular and formula search mode toggles', async () => {
|
||||
const group = await getToggleGroup();
|
||||
const toggles = await group.getToggles();
|
||||
expect(toggles.length).toBe(2);
|
||||
expect(await toggles[0].getText()).toBe('title');
|
||||
expect(await toggles[1].getText()).toBe('extension');
|
||||
});
|
||||
|
||||
it('should NOT execute search when navigation has no query params', () => {
|
||||
searchExecutionService.execute.calls.reset();
|
||||
component.searchedWord = 'term';
|
||||
routerEventsSubject.next(new NavigationStart(1, '/path'));
|
||||
configUpdatedSubject.next({} as SearchConfiguration);
|
||||
expect(searchExecutionService.execute).not.toHaveBeenCalled();
|
||||
it('should reflect the current queryBuilder search mode', async () => {
|
||||
const group = await getToggleGroup();
|
||||
const toggles = await group.getToggles();
|
||||
expect(await toggles[0].isChecked()).toBeTrue();
|
||||
expect(await toggles[1].isChecked()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should NOT execute search when searchedWord is not set', () => {
|
||||
searchExecutionService.execute.calls.reset();
|
||||
component.searchedWord = null;
|
||||
routerEventsSubject.next(new NavigationStart(1, '/path?q=term'));
|
||||
configUpdatedSubject.next({} as SearchConfiguration);
|
||||
expect(searchExecutionService.execute).not.toHaveBeenCalled();
|
||||
it('should update queryBuilder search mode to formula when formula toggle is selected', async () => {
|
||||
const group = await getToggleGroup();
|
||||
const toggles = await group.getToggles();
|
||||
await toggles[1].check();
|
||||
expect(queryBuilder.searchMode).toBe('formula');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+25
-34
@@ -23,9 +23,8 @@
|
||||
*/
|
||||
|
||||
import { AppHookService } from '@alfresco/aca-shared';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import { AfterViewInit, Component, DestroyRef, ElementRef, inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationSkipped, NavigationStart, Params, Router } from '@angular/router';
|
||||
import { ActivatedRoute, NavigationSkipped, Params, Router } from '@angular/router';
|
||||
import { SearchNavigationService } from '../search-navigation.service';
|
||||
import { SearchFilterService } from '../search-filter.service';
|
||||
import { SearchExecutionService } from '../search-execution.service';
|
||||
@@ -39,12 +38,23 @@ import { FormsModule } from '@angular/forms';
|
||||
import { SearchInMenuComponent } from '../search-in-menu/search-in-menu.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { merge } from 'rxjs/internal/observable/merge';
|
||||
import { filter, map, startWith, withLatestFrom } from 'rxjs';
|
||||
import { filter, map, withLatestFrom } from 'rxjs';
|
||||
import { extractUserQueryFromEncodedQuery } from '../../../utils/aca-search-utils';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { extractSearchedWordFromEncodedQuery } from '../../../utils/aca-search-utils';
|
||||
|
||||
@Component({
|
||||
imports: [CommonModule, TranslatePipe, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, FormsModule, SearchInMenuComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslatePipe,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
FormsModule,
|
||||
SearchInMenuComponent,
|
||||
MatButtonToggleModule
|
||||
],
|
||||
selector: 'aca-search-input',
|
||||
templateUrl: './search-input.component.html',
|
||||
styleUrls: ['./search-input.component.scss'],
|
||||
@@ -52,18 +62,17 @@ import { extractSearchedWordFromEncodedQuery } from '../../../utils/aca-search-u
|
||||
host: { class: 'aca-search-input' }
|
||||
})
|
||||
export class SearchInputComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private readonly queryBuilder = inject(SearchQueryBuilderService);
|
||||
private readonly config = inject(AppConfigService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly appHookService = inject(AppHookService);
|
||||
private readonly filterService = inject(SearchFilterService);
|
||||
private readonly searchExecutionService = inject(SearchExecutionService);
|
||||
readonly searchNavigationService = inject(SearchNavigationService);
|
||||
readonly queryBuilder = inject(SearchQueryBuilderService);
|
||||
|
||||
has400LibraryError = false;
|
||||
searchOnChange: boolean;
|
||||
searchedWord: string = null;
|
||||
lastSearchedWord: string = null;
|
||||
error = '';
|
||||
|
||||
@ViewChild('searchInputField')
|
||||
@@ -71,10 +80,6 @@ export class SearchInputComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor() {
|
||||
this.searchOnChange = this.config.get<boolean>('search.aca:triggeredOnChange', true);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initSearchState();
|
||||
this.subscribeToRouteParams();
|
||||
@@ -98,8 +103,8 @@ export class SearchInputComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.searchNavigationService.navigateBack();
|
||||
}
|
||||
|
||||
onSearchSubmit(searchTerm: string) {
|
||||
const trimmedTerm = searchTerm?.trim();
|
||||
onSearchSubmit(event: Event) {
|
||||
const trimmedTerm = (event.target as HTMLInputElement).value?.trim();
|
||||
const validationError = this.filterService.validateSearchTerm(trimmedTerm);
|
||||
|
||||
if (validationError) {
|
||||
@@ -107,9 +112,12 @@ export class SearchInputComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.searchedWord = trimmedTerm;
|
||||
this.error = '';
|
||||
this.executeSearch();
|
||||
if (this.lastSearchedWord !== trimmedTerm) {
|
||||
this.lastSearchedWord = trimmedTerm;
|
||||
this.searchedWord = trimmedTerm;
|
||||
this.executeSearch();
|
||||
}
|
||||
}
|
||||
|
||||
onFiltersApplied() {
|
||||
@@ -143,24 +151,7 @@ export class SearchInputComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
.subscribe((params: Params) => {
|
||||
const encodedQuery = params['q'];
|
||||
if (encodedQuery) {
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(encodedQuery);
|
||||
}
|
||||
});
|
||||
|
||||
this.queryBuilder.configUpdated
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
withLatestFrom(
|
||||
this.router.events.pipe(
|
||||
filter((event): event is NavigationStart => event instanceof NavigationStart),
|
||||
startWith(null)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(([, navigationStartEvent]) => {
|
||||
const hasQueryParams = navigationStartEvent?.url.includes('?');
|
||||
if (this.searchedWord && hasQueryParams) {
|
||||
this.searchExecutionService.execute(this.searchedWord);
|
||||
this.searchedWord = extractUserQueryFromEncodedQuery(encodedQuery);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -75,8 +75,8 @@ describe('SearchLibrariesResultsComponent', () => {
|
||||
expect(component.onSearchResultLoaded).toHaveBeenCalledWith(emptyPage);
|
||||
});
|
||||
|
||||
it('should extract searched word from query params', (done) => {
|
||||
route.queryParams = of({ q: encodeQuery({ userQuery: 'cm:name:"test*"' }) });
|
||||
it('should extract the user query from query params', (done) => {
|
||||
route.queryParams = of({ q: encodeQuery({ userQuery: 'test' }) });
|
||||
route.queryParams.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.searchedWord).toBe('test');
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ import { CustomEmptyContentTemplateDirective, DataColumnComponent, DataColumnLis
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { DocumentListDirective } from '../../../directives/document-list.directive';
|
||||
import { DocumentListComponent } from '@alfresco/adf-content-services';
|
||||
import { extractSearchedWordFromEncodedQuery } from '../../../utils/aca-search-utils';
|
||||
import { extractUserQueryFromEncodedQuery } from '../../../utils/aca-search-utils';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
@@ -129,7 +129,7 @@ export class SearchLibrariesResultsComponent extends PageComponent implements On
|
||||
if (this.route) {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params: Params) => {
|
||||
const encodedQuery = params[this.queryParamName] || null;
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(encodedQuery);
|
||||
this.searchedWord = extractUserQueryFromEncodedQuery(encodedQuery);
|
||||
if (this.searchedWord?.length > 1) {
|
||||
this.librariesQueryBuilder.paging.skipCount = 0;
|
||||
this.librariesQueryBuilder.userQuery = this.searchedWord;
|
||||
|
||||
@@ -26,11 +26,14 @@ import { TestBed } from '@angular/core/testing';
|
||||
import { SearchNavigationService } from './search-navigation.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { AppTestingModule } from '../../testing/app-testing.module';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
describe('SearchNavigationService', () => {
|
||||
let service: SearchNavigationService;
|
||||
let router: Router;
|
||||
|
||||
const encodeQuery = (query: any): string => Buffer.from(JSON.stringify(query)).toString('base64');
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule]
|
||||
@@ -69,4 +72,31 @@ describe('SearchNavigationService', () => {
|
||||
|
||||
expect(routerNavigate).toHaveBeenCalledWith(['/personal-files']);
|
||||
});
|
||||
|
||||
describe('getUrlSearchTerm', () => {
|
||||
it('should return empty string when not on a search results route', () => {
|
||||
spyOnProperty(router, 'url', 'get').and.returnValue('/personal-files');
|
||||
expect(service.getUrlSearchTerm()).toBe('');
|
||||
});
|
||||
|
||||
it('should return the raw user query extracted from the encoded q parameter', () => {
|
||||
const encodedQuery = encodeQuery({ userQuery: 'my term' });
|
||||
spyOnProperty(router, 'url', 'get').and.returnValue(`/search;q=${encodedQuery}`);
|
||||
expect(service.getUrlSearchTerm()).toBe('my term');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSameSearchTerm', () => {
|
||||
it('should return true when the provided term matches the url search term', () => {
|
||||
const encodedQuery = encodeQuery({ userQuery: 'my term' });
|
||||
spyOnProperty(router, 'url', 'get').and.returnValue(`/search;q=${encodedQuery}`);
|
||||
expect(service.isSameSearchTerm('my term')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return false when the provided term differs from the url search term', () => {
|
||||
const encodedQuery = encodeQuery({ userQuery: 'my term' });
|
||||
spyOnProperty(router, 'url', 'get').and.returnValue(`/search;q=${encodedQuery}`);
|
||||
expect(service.isSameSearchTerm('other term')).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { PRIMARY_OUTLET, Router, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router';
|
||||
import { extractSearchedWordFromEncodedQuery } from '../../utils/aca-search-utils';
|
||||
import { extractUserQueryFromEncodedQuery } from '../../utils/aca-search-utils';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -73,7 +73,7 @@ export class SearchNavigationService {
|
||||
|
||||
if (urlSegmentGroup) {
|
||||
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
|
||||
return extractSearchedWordFromEncodedQuery(urlSegments[0].parameters['q']);
|
||||
return extractUserQueryFromEncodedQuery(urlSegments[0].parameters['q']);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
+44
-26
@@ -61,7 +61,6 @@ describe('SearchComponent', () => {
|
||||
let showErrorSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
|
||||
let showInfoSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
|
||||
let loader: HarnessLoader;
|
||||
let updatedSubjectMock: Subject<SearchRequest>;
|
||||
|
||||
const editSavedSearchesSpy = jasmine.createSpy('editSavedSearch');
|
||||
const getSavedSearchButton = (): HTMLButtonElement => fixture.nativeElement.querySelector('.aca-content__save-search-action');
|
||||
@@ -76,7 +75,6 @@ describe('SearchComponent', () => {
|
||||
params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' });
|
||||
queryParams = new Subject();
|
||||
routerEvents = new Subject();
|
||||
updatedSubjectMock = new Subject();
|
||||
|
||||
const routerMock = jasmine.createSpyObj<Router>('Router', ['navigate'], {
|
||||
url: '/mock-search-url',
|
||||
@@ -128,8 +126,6 @@ describe('SearchComponent', () => {
|
||||
router = TestBed.inject(Router);
|
||||
route = TestBed.inject(ActivatedRoute);
|
||||
|
||||
queryBuilder.updated = updatedSubjectMock;
|
||||
|
||||
const notificationService = TestBed.inject(NotificationService);
|
||||
showErrorSpy = spyOn(notificationService, 'showError');
|
||||
showInfoSpy = spyOn(notificationService, 'showInfo');
|
||||
@@ -141,8 +137,6 @@ describe('SearchComponent', () => {
|
||||
fixture = TestBed.createComponent(SearchResultsComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
fixture.detectChanges();
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
});
|
||||
@@ -232,6 +226,7 @@ describe('SearchComponent', () => {
|
||||
});
|
||||
|
||||
it('should re-run search on pagination change', () => {
|
||||
const executeSpy = spyOn(queryBuilder, 'execute');
|
||||
const page = new Pagination({
|
||||
maxItems: 10,
|
||||
skipCount: 0
|
||||
@@ -243,25 +238,60 @@ describe('SearchComponent', () => {
|
||||
maxItems: 10,
|
||||
skipCount: 0
|
||||
});
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
expect(executeSpy).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should re-run search on sorting change', () => {
|
||||
const executeSpy = spyOn(queryBuilder, 'execute');
|
||||
|
||||
component.onSearchSortingUpdate({ key: 'name', ascending: false } as any);
|
||||
|
||||
expect(queryBuilder.sorting).toEqual([jasmine.objectContaining({ key: 'name', ascending: false })]);
|
||||
expect(executeSpy).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should update the user query, populate filters state and execute query whenever param changed', (done) => {
|
||||
spyOn(queryBuilder.populateFilters, 'next');
|
||||
spyOn(queryBuilder, 'execute');
|
||||
const query = { userQuery: 'cm:tag:"orange*"', filterProp: { prop: 'test' } };
|
||||
const query = { userQuery: 'orange', filterProp: { prop: 'test' } };
|
||||
route.queryParams = of({ q: encodeQuery(query) });
|
||||
component.ngOnInit();
|
||||
route.queryParams.subscribe(() => {
|
||||
expect(component.searchedWord).toBe(`orange`);
|
||||
expect(queryBuilder.userQuery).toBe(`(cm:tag:"orange*")`);
|
||||
expect(queryBuilder.populateFilters.next).toHaveBeenCalledWith({ userQuery: 'cm:tag:"orange*"', filterProp: { prop: 'test' } });
|
||||
expect(queryBuilder.userQuery).toBe(`orange`);
|
||||
expect(queryBuilder.populateFilters.next).toHaveBeenCalledWith({ userQuery: 'orange', filterProp: { prop: 'test' } });
|
||||
queryBuilder.filterLoaded.next();
|
||||
fixture.detectChanges();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply search mode and selected configuration from the encoded query', (done) => {
|
||||
spyOn(queryBuilder, 'updateSelectedConfiguration');
|
||||
spyOn(queryBuilder, 'execute');
|
||||
const query = { userQuery: 'orange', searchMode: 'formula', selectedConfigurationId: 'config-1' };
|
||||
route.queryParams = of({ q: encodeQuery(query) });
|
||||
component.ngOnInit();
|
||||
route.queryParams.subscribe(() => {
|
||||
expect(queryBuilder.searchMode).toBe('formula');
|
||||
expect(queryBuilder.updateSelectedConfiguration).toHaveBeenCalledWith('config-1', false, false);
|
||||
queryBuilder.filterLoaded.next();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should default search mode to regular when not present in the encoded query', (done) => {
|
||||
spyOn(queryBuilder, 'execute');
|
||||
const query = { userQuery: 'orange' };
|
||||
route.queryParams = of({ q: encodeQuery(query) });
|
||||
component.ngOnInit();
|
||||
route.queryParams.subscribe(() => {
|
||||
expect(queryBuilder.searchMode).toBe('regular');
|
||||
queryBuilder.filterLoaded.next();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get initial saved search when url matches', () => {
|
||||
route.queryParams = of({ q: encodeQuery({ name: 'test' }) });
|
||||
component.ngOnInit();
|
||||
@@ -370,30 +400,18 @@ describe('SearchComponent', () => {
|
||||
expect(executeSpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
it('should format userQuery when url parameters changed and userQuery is not contained by url', () => {
|
||||
routerEvents.next(new NavigationStart(1, ''));
|
||||
queryParams.next({ q: encodeQuery('') });
|
||||
expect(queryBuilder.userQuery).toBe('((cm:name:"*"))');
|
||||
});
|
||||
|
||||
it('should not format userQuery when url parameters changed when userQuery is already contained by url', () => {
|
||||
it('should set the raw userQuery from the encoded query when url parameters change', () => {
|
||||
routerEvents.next(new NavigationStart(1, ''));
|
||||
queryParams.next({ q: encodeQuery({ userQuery: 'test' }) });
|
||||
expect(queryBuilder.userQuery).toBe('(test)');
|
||||
expect(queryBuilder.userQuery).toBe('test');
|
||||
});
|
||||
|
||||
it('should set loading to true in updated stream for non-nullish query', fakeAsync(() => {
|
||||
it('should set loading to true when filterQueryUpdate emits', fakeAsync(() => {
|
||||
spyOn(queryBuilder, 'execute').and.stub();
|
||||
|
||||
expect(component.isLoading).toBeFalse();
|
||||
|
||||
updatedSubjectMock.next(null);
|
||||
|
||||
tick();
|
||||
|
||||
expect(component.isLoading).toBeFalse();
|
||||
|
||||
updatedSubjectMock.next({} as SearchRequest);
|
||||
queryBuilder.filterQueryUpdate.next();
|
||||
|
||||
tick();
|
||||
|
||||
|
||||
+16
-28
@@ -30,7 +30,6 @@ import {
|
||||
DocumentListComponent,
|
||||
ResetSearchDirective,
|
||||
SavedSearch,
|
||||
SearchConfiguration,
|
||||
SearchFilterChipsComponent,
|
||||
SearchFormComponent,
|
||||
SearchSortingDefinition,
|
||||
@@ -77,12 +76,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions';
|
||||
import { BulkActionsDropdownComponent } from '../../bulk-actions-dropdown/bulk-actions-dropdown.component';
|
||||
import { SearchAiInputContainerComponent } from '../../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component';
|
||||
import {
|
||||
extractFiltersFromEncodedQuery,
|
||||
extractSearchedWordFromEncodedQuery,
|
||||
extractUserQueryFromEncodedQuery,
|
||||
formatSearchTerm
|
||||
} from '../../../utils/aca-search-utils';
|
||||
import { extractFiltersFromEncodedQuery, extractUserQueryFromEncodedQuery } from '../../../utils/aca-search-utils';
|
||||
import { SaveSearchDirective } from '../search-save/directive/save-search.directive';
|
||||
import { combineLatest, Observable, of } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -137,7 +131,6 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly translationService = inject(TranslationService);
|
||||
private readonly savedSearchesService = inject(SavedSearchesContextService);
|
||||
|
||||
private readonly notificationService = inject(NotificationService);
|
||||
|
||||
infoDrawerPreview$ = this.store.select(infoDrawerPreview);
|
||||
@@ -154,7 +147,6 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
initialSavedSearch: SavedSearch = undefined;
|
||||
columns: DocumentListPresetRef[] = [];
|
||||
encodedQuery: string;
|
||||
searchConfig: SearchConfiguration;
|
||||
isSmallScreen = window.innerWidth < 320;
|
||||
|
||||
private previousEncodedQuery: string;
|
||||
@@ -175,10 +167,6 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
maxItems: 25
|
||||
};
|
||||
|
||||
this.queryBuilder.configUpdated.pipe(takeUntilDestroyed()).subscribe((searchConfig) => {
|
||||
this.searchConfig = searchConfig;
|
||||
});
|
||||
|
||||
this.areFiltersActive$ = combineLatest([this.queryBuilder.queryFragmentsUpdate, this.queryBuilder.userFacetBucketsUpdate]).pipe(
|
||||
takeUntilDestroyed(),
|
||||
map((filters) => {
|
||||
@@ -197,12 +185,6 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
this.sorting = this.getSorting();
|
||||
|
||||
this.subscriptions.push(
|
||||
this.queryBuilder.updated.pipe(filter(Boolean)).subscribe(() => {
|
||||
this.isLoading = true;
|
||||
this.sorting = this.getSorting();
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}),
|
||||
|
||||
this.queryBuilder.executed.subscribe((data) => {
|
||||
this.queryBuilder.paging.skipCount = 0;
|
||||
|
||||
@@ -237,13 +219,16 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
tap(([params]) => {
|
||||
this.queryBuilder.userQuery = '';
|
||||
this.encodedQuery = params[this.queryParamName];
|
||||
this.isLoading = !!this.encodedQuery;
|
||||
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(this.encodedQuery);
|
||||
this.searchedWord = extractUserQueryFromEncodedQuery(this.encodedQuery);
|
||||
|
||||
const filtersFromEncodedQuery = extractFiltersFromEncodedQuery(this.encodedQuery);
|
||||
this.queryBuilder.searchMode = filtersFromEncodedQuery?.['searchMode'] ?? 'regular';
|
||||
if (filtersFromEncodedQuery?.['selectedConfigurationId']) {
|
||||
this.queryBuilder.updateSelectedConfiguration(filtersFromEncodedQuery['selectedConfigurationId'], false, false);
|
||||
}
|
||||
this.queryBuilder.populateFilters.next(filtersFromEncodedQuery || {});
|
||||
}),
|
||||
switchMap(([, navigationStartEvent]) => {
|
||||
@@ -257,9 +242,6 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
.subscribe((navigationStartEvent) => {
|
||||
const shouldExecuteQuery = this.shouldExecuteQuery(navigationStartEvent, this.encodedQuery);
|
||||
this.queryBuilder.userQuery = extractUserQueryFromEncodedQuery(this.encodedQuery);
|
||||
if (!this.searchedWord && !this.queryBuilder.userQuery && this.encodedQuery) {
|
||||
this.queryBuilder.userQuery = formatSearchTerm('*', this.searchConfig['app:fields']);
|
||||
}
|
||||
|
||||
if (shouldExecuteQuery) {
|
||||
this.queryBuilder.execute(false);
|
||||
@@ -311,7 +293,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
maxItems: pagination.maxItems,
|
||||
skipCount: pagination.skipCount
|
||||
};
|
||||
this.queryBuilder.update();
|
||||
this.queryBuilder.execute(false);
|
||||
}
|
||||
|
||||
private getSorting(): string[] {
|
||||
@@ -350,7 +332,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
|
||||
onSearchSortingUpdate(option: SearchSortingDefinition) {
|
||||
this.queryBuilder.sorting = [{ ...option, ascending: option.ascending }];
|
||||
this.queryBuilder.update();
|
||||
this.queryBuilder.execute(false);
|
||||
}
|
||||
|
||||
editSavedSearch(searchToSave: SavedSearch) {
|
||||
@@ -381,9 +363,15 @@ export class SearchResultsComponent extends PageComponent implements OnInit, OnD
|
||||
const hasQueryChanged = query !== this.previousEncodedQuery;
|
||||
this.previousEncodedQuery = query;
|
||||
|
||||
if (!navigationStartEvent || navigationStartEvent.navigationTrigger === 'popstate' || navigationStartEvent.navigationTrigger === 'hashchange') {
|
||||
if (!navigationStartEvent && this.data === undefined) {
|
||||
return true;
|
||||
} else if (navigationStartEvent.navigationTrigger === 'imperative') {
|
||||
} else if (query === this.queryBuilder.encodedQuery) {
|
||||
return false;
|
||||
} else if (
|
||||
navigationStartEvent?.navigationTrigger === 'popstate' ||
|
||||
navigationStartEvent?.navigationTrigger === 'hashchange' ||
|
||||
navigationStartEvent?.navigationTrigger === 'imperative'
|
||||
) {
|
||||
return hasQueryChanged;
|
||||
} else {
|
||||
return !!query;
|
||||
|
||||
@@ -255,7 +255,8 @@ export class NodeActionsService {
|
||||
isSelectionValid: this.canCopyMoveInsideIt.bind(this),
|
||||
breadcrumbTransform: this.customizeBreadcrumb.bind(this),
|
||||
select: new Subject<Node[]>(),
|
||||
excludeSiteContent: ContentNodeDialogService.nonDocumentSiteContent
|
||||
excludeSiteContent: ContentNodeDialogService.nonDocumentSiteContent,
|
||||
showFilesInResult: false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ export class NodeTemplateService {
|
||||
showSearch: false,
|
||||
showDropdownSiteList: false,
|
||||
isSelectionValid: this.isSelectionValid.bind(this),
|
||||
rowFilter: this.rowFilter.bind(this)
|
||||
rowFilter: this.rowFilter.bind(this),
|
||||
showFilesInResult: false
|
||||
};
|
||||
|
||||
const query = {
|
||||
|
||||
@@ -50,14 +50,14 @@ describe('SearchEffects', () => {
|
||||
});
|
||||
|
||||
describe('searchByTerm$', () => {
|
||||
it('should navigate to `search` when search options has library false', fakeAsync(() => {
|
||||
it('should navigate to `search` with the raw term when search options has library false', fakeAsync(() => {
|
||||
spyOn(queryBuilder, 'navigateToSearch');
|
||||
store.dispatch(new SearchByTermAction('test', []));
|
||||
tick();
|
||||
expect(queryBuilder.navigateToSearch).toHaveBeenCalledWith('(cm:name:"test*")', '/search');
|
||||
expect(queryBuilder.navigateToSearch).toHaveBeenCalledWith('test', '/search');
|
||||
}));
|
||||
|
||||
it('should navigate to `search-libraries` when search options has library true', fakeAsync(() => {
|
||||
it('should navigate to `search-libraries` with the raw term when search options has library true', fakeAsync(() => {
|
||||
spyOn(queryBuilder, 'navigateToSearch');
|
||||
store.dispatch(
|
||||
new SearchByTermAction('test', [
|
||||
@@ -72,7 +72,7 @@ describe('SearchEffects', () => {
|
||||
|
||||
tick();
|
||||
|
||||
expect(queryBuilder.navigateToSearch).toHaveBeenCalledWith('(cm:name:"test*")', '/search-libraries');
|
||||
expect(queryBuilder.navigateToSearch).toHaveBeenCalledWith('test', '/search-libraries');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import { map } from 'rxjs/operators';
|
||||
import { SearchAction, SearchActionTypes, SearchByTermAction, SearchOptionIds } from '@alfresco/aca-shared/store';
|
||||
import { SearchNavigationService } from '../../components/search/search-navigation.service';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { formatSearchTerm } from '../../utils/aca-search-utils';
|
||||
|
||||
@Injectable()
|
||||
export class SearchEffects {
|
||||
@@ -52,10 +51,9 @@ export class SearchEffects {
|
||||
this.actions$.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
const query = formatSearchTerm(action.payload, this.queryBuilder.config['app:fields']);
|
||||
const libItem = action.searchOptions.find((item) => item.id === SearchOptionIds.Libraries);
|
||||
const librarySelected = !!libItem && libItem.value;
|
||||
this.queryBuilder.navigateToSearch(query, librarySelected ? '/search-libraries' : '/search');
|
||||
this.queryBuilder.navigateToSearch(action.payload, librarySelected ? '/search-libraries' : '/search');
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import {
|
||||
extractFiltersFromEncodedQuery,
|
||||
extractSearchedWordFromEncodedQuery,
|
||||
extractParsedQueryFromEncodedQuery,
|
||||
extractUserQueryFromEncodedQuery,
|
||||
formatSearchTerm,
|
||||
formatSearchTermByFields,
|
||||
@@ -62,20 +62,20 @@ describe('SearchUtils', () => {
|
||||
});
|
||||
|
||||
describe('formatSearchTermByFields', () => {
|
||||
it('should append "*" to search term', () => {
|
||||
expect(formatSearchTermByFields('test', ['name'])).toBe('(name:"test*")');
|
||||
it('should not append wildcard by default', () => {
|
||||
expect(formatSearchTermByFields('test', ['name'])).toBe('(name:"test")');
|
||||
});
|
||||
|
||||
it('should not prefix when search term equals "*"', () => {
|
||||
expect(formatSearchTermByFields('*', ['name'])).toBe('(name:"*")');
|
||||
});
|
||||
|
||||
it('should properly handle search terms starting with "="', () => {
|
||||
expect(formatSearchTermByFields('=test', ['name'])).toBe('(=name:"test")');
|
||||
it('should append "*" to search term when wildcards are enabled', () => {
|
||||
expect(formatSearchTermByFields('test', ['name'], true)).toBe('(name:"test*")');
|
||||
});
|
||||
|
||||
it('should format search term with set of fields and join with OR', () => {
|
||||
expect(formatSearchTermByFields('test', ['name', 'size'])).toBe('(name:"test*" OR size:"test*")');
|
||||
expect(formatSearchTermByFields('test', ['name', 'size'])).toBe('(name:"test" OR size:"test")');
|
||||
});
|
||||
|
||||
it('should format search term with set of fields and append wildcards when enabled', () => {
|
||||
expect(formatSearchTermByFields('test', ['name', 'size'], true)).toBe('(name:"test*" OR size:"test*")');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,43 +85,43 @@ describe('SearchUtils', () => {
|
||||
expect(formatSearchTerm(undefined)).toEqual('');
|
||||
});
|
||||
|
||||
it('should not transfer custom queries', () => {
|
||||
expect(formatSearchTerm('test:"term"')).toBe('test:"term"');
|
||||
expect(formatSearchTerm('"test"')).toBe('"test"');
|
||||
it('should return the raw input untouched in formula mode', () => {
|
||||
expect(formatSearchTerm('test:"term"', ['cm:name'], 'formula')).toBe('test:"term"');
|
||||
expect(formatSearchTerm('cm:name:"foo" AND TEXT:bar', ['cm:name'], 'formula')).toBe('cm:name:"foo" AND TEXT:bar');
|
||||
});
|
||||
|
||||
it('should properly join multiple word search term', () => {
|
||||
expect(formatSearchTerm('test word term')).toBe('(cm:name:"test*") AND (cm:name:"word*") AND (cm:name:"term*")');
|
||||
expect(formatSearchTerm('test word term')).toBe('((cm:name:"test") AND (cm:name:"word") AND (cm:name:"term"))');
|
||||
expect(formatSearchTerm('test word term', ['name', 'size'])).toBe(
|
||||
'(name:"test*" OR size:"test*") AND (name:"word*" OR size:"word*") AND (name:"term*" OR size:"term*")'
|
||||
'((name:"test" OR size:"test") AND (name:"word" OR size:"word") AND (name:"term" OR size:"term"))'
|
||||
);
|
||||
});
|
||||
|
||||
it('should append wildcards to every word when wildcards are enabled', () => {
|
||||
expect(formatSearchTerm('test word term', ['cm:name'], 'regular', true)).toBe(
|
||||
'((cm:name:"test*") AND (cm:name:"word*") AND (cm:name:"term*"))'
|
||||
);
|
||||
});
|
||||
|
||||
it('should format user input as cm:name if configuration not provided', () => {
|
||||
expect(formatSearchTerm('hello')).toBe(`(cm:name:"hello*")`);
|
||||
expect(formatSearchTerm('hello')).toBe(`((cm:name:"hello"))`);
|
||||
});
|
||||
|
||||
it('should support conjunctions with AND operator', () => {
|
||||
expect(formatSearchTerm('big AND yellow AND banana', ['cm:name', 'cm:title'])).toBe(
|
||||
`(cm:name:"big*" OR cm:title:"big*") AND (cm:name:"yellow*" OR cm:title:"yellow*") AND (cm:name:"banana*" OR cm:title:"banana*")`
|
||||
`((cm:name:"big" OR cm:title:"big") AND (cm:name:"yellow" OR cm:title:"yellow") AND (cm:name:"banana" OR cm:title:"banana"))`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support conjunctions with OR operator', () => {
|
||||
expect(formatSearchTerm('big OR yellow OR banana', ['cm:name', 'cm:title'])).toBe(
|
||||
`(cm:name:"big*" OR cm:title:"big*") OR (cm:name:"yellow*" OR cm:title:"yellow*") OR (cm:name:"banana*" OR cm:title:"banana*")`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support exact term matching with operators', () => {
|
||||
expect(formatSearchTerm('=test1.pdf OR =test2.pdf', ['cm:name', 'cm:title'])).toBe(
|
||||
`(=cm:name:"test1.pdf" OR =cm:title:"test1.pdf") OR (=cm:name:"test2.pdf" OR =cm:title:"test2.pdf")`
|
||||
`((cm:name:"big" OR cm:title:"big") OR (cm:name:"yellow" OR cm:title:"yellow") OR (cm:name:"banana" OR cm:title:"banana"))`
|
||||
);
|
||||
});
|
||||
|
||||
it('should split words correctly when multiple whitespaces are present', () => {
|
||||
expect(formatSearchTerm(' big yellow ', ['cm:name', 'cm:title'])).toBe(
|
||||
`(cm:name:"big*" OR cm:title:"big*") AND (cm:name:"yellow*" OR cm:title:"yellow*")`
|
||||
`((cm:name:"big" OR cm:title:"big") AND (cm:name:"yellow" OR cm:title:"yellow"))`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -137,62 +137,26 @@ describe('SearchUtils', () => {
|
||||
expect(extractUserQueryFromEncodedQuery(encodeQuery(query))).toBe('cm:name:"test"');
|
||||
});
|
||||
|
||||
it('should properly trim set of parentheses from extracted user query', () => {
|
||||
it('should return the raw user query without trimming parentheses', () => {
|
||||
const query = { userQuery: '(cm:name:"test")' };
|
||||
expect(extractUserQueryFromEncodedQuery(encodeQuery(query))).toBe('cm:name:"test"');
|
||||
expect(extractUserQueryFromEncodedQuery(encodeQuery(query))).toBe('(cm:name:"test")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractSearchedWordFromEncodedQuery', () => {
|
||||
describe('extractParsedQueryFromEncodedQuery', () => {
|
||||
it('should return empty string when encoded query is invalid', () => {
|
||||
const query = { otherProp: 'test' };
|
||||
expect(extractSearchedWordFromEncodedQuery(null)).toBe('');
|
||||
expect(extractSearchedWordFromEncodedQuery(undefined)).toBe('');
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('');
|
||||
expect(extractParsedQueryFromEncodedQuery(null)).toBe('');
|
||||
expect(extractParsedQueryFromEncodedQuery(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('should properly extract search term', () => {
|
||||
const query = { userQuery: 'cm:name:"test*"' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('test');
|
||||
it('should properly extract parsed query', () => {
|
||||
const query = { parsedQuery: '(cm:name:"test*")' };
|
||||
expect(extractParsedQueryFromEncodedQuery(encodeQuery(query))).toBe('(cm:name:"test*")');
|
||||
});
|
||||
|
||||
it('should preserve quotes in search term for custom search', () => {
|
||||
const query = { userQuery: '"test"' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('"test"');
|
||||
});
|
||||
|
||||
it('should properly extract search term when userQuery does not contain quotes', () => {
|
||||
const query = { userQuery: 'TEXT:abcdef' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('TEXT:abcdef');
|
||||
});
|
||||
|
||||
it('should properly extract search term when userQuery contains field without quotes', () => {
|
||||
const query = { userQuery: 'cm:name:searchterm' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('cm:name:searchterm');
|
||||
});
|
||||
|
||||
it('should handle mixed conditions with and without quotes', () => {
|
||||
const query = { userQuery: 'cm:name:"quoted term" AND TEXT:unquoted' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('quoted term TEXT:unquoted');
|
||||
});
|
||||
|
||||
it('should handle complex search query', () => {
|
||||
const query = {
|
||||
userQuery: `((cm:name:"a*" OR cm:title:"a*" OR cm:description:"a*" OR TEXT:"a*" OR TAG:"a*") AND
|
||||
(cm:name:"b*" OR cm:title:"b*" OR cm:description:"b*" OR TEXT:"b*" OR TAG:"b*") OR
|
||||
(cm:name:"c*" OR cm:title:"c*" OR cm:description:"c*" OR TEXT:"c*" OR TAG:"c*"))`
|
||||
};
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('a b OR c');
|
||||
});
|
||||
|
||||
it('should not treat operator as a searched word', () => {
|
||||
const query = { userQuery: 'AND' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('');
|
||||
});
|
||||
|
||||
it('should not unquote when searching for phrase', () => {
|
||||
const query = { userQuery: '"exact phrase search"' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('"exact phrase search"');
|
||||
it('should return empty string when parsed query is not present', () => {
|
||||
const query = { userQuery: 'cm:name:"test"' };
|
||||
expect(extractParsedQueryFromEncodedQuery(encodeQuery(query))).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -41,24 +41,12 @@ export function isOperator(input: string): boolean {
|
||||
*
|
||||
* @param term search term
|
||||
* @param fields array of fields
|
||||
* @param wildcardsEnabled whether wildcards are enabled
|
||||
* @returns string
|
||||
*/
|
||||
export function formatSearchTermByFields(term: string, fields: string[]): string {
|
||||
let prefix = '';
|
||||
let suffix = '*';
|
||||
|
||||
if (term.startsWith('=')) {
|
||||
prefix = '=';
|
||||
suffix = '';
|
||||
term = term.substring(1);
|
||||
}
|
||||
|
||||
if (term === '*') {
|
||||
prefix = '';
|
||||
suffix = '';
|
||||
}
|
||||
|
||||
return '(' + fields.map((field) => `${prefix}${field}:"${term}${suffix}"`).join(' OR ') + ')';
|
||||
export function formatSearchTermByFields(term: string, fields: string[], wildcardsEnabled = false): string {
|
||||
const suffix = wildcardsEnabled ? '*' : '';
|
||||
return '(' + fields.map((field) => `${field}:"${term}${suffix}"`).join(' OR ') + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,15 +54,22 @@ export function formatSearchTermByFields(term: string, fields: string[]): string
|
||||
*
|
||||
* @param userInput search term
|
||||
* @param fields array of fields
|
||||
* @param searchMode regular or formula search mode
|
||||
* @param wildcardsEnabled whether wildcards are enabled
|
||||
* @returns string
|
||||
*/
|
||||
export function formatSearchTerm(userInput: string, fields = ['cm:name']): string {
|
||||
export function formatSearchTerm(
|
||||
userInput: string,
|
||||
fields = ['cm:name'],
|
||||
searchMode: 'regular' | 'formula' = 'regular',
|
||||
wildcardsEnabled = false
|
||||
): string {
|
||||
if (!userInput) {
|
||||
return '';
|
||||
}
|
||||
userInput = userInput.trim();
|
||||
|
||||
if (userInput.includes(':') || userInput.includes('"')) {
|
||||
if (searchMode === 'formula') {
|
||||
return userInput;
|
||||
}
|
||||
|
||||
@@ -82,10 +77,10 @@ export function formatSearchTerm(userInput: string, fields = ['cm:name']): strin
|
||||
|
||||
if (words.length > 1) {
|
||||
const separator = words.some(isOperator) ? ' ' : ' AND ';
|
||||
return words.map((term) => (isOperator(term) ? term : formatSearchTermByFields(term, fields))).join(separator);
|
||||
return '(' + words.map((term) => (isOperator(term) ? term : formatSearchTermByFields(term, fields, wildcardsEnabled))).join(separator) + ')';
|
||||
}
|
||||
|
||||
return formatSearchTermByFields(userInput, fields);
|
||||
return '(' + formatSearchTermByFields(userInput, fields, wildcardsEnabled) + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,66 +92,21 @@ export function formatSearchTerm(userInput: string, fields = ['cm:name']): strin
|
||||
export function extractUserQueryFromEncodedQuery(encodedQuery: string): string {
|
||||
if (encodedQuery) {
|
||||
const decodedQuery: { [key: string]: any } = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(encodedQuery), (c) => c.charCodeAt(0))));
|
||||
return trimUserQuery(decodedQuery.userQuery);
|
||||
return decodedQuery.userQuery ?? '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts user query from encoded query and splits it to get a search term
|
||||
* Decodes a query and extracts parsed query
|
||||
*
|
||||
* @param encodedQuery encoded query
|
||||
* @returns string
|
||||
*/
|
||||
export function extractSearchedWordFromEncodedQuery(encodedQuery: string): string {
|
||||
if (!encodedQuery) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const userQuery = extractUserQueryFromEncodedQuery(encodedQuery);
|
||||
if (!userQuery) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tokenRegex = /\(([^()]+)\)|\b(AND|OR)\b/g;
|
||||
const fragments: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = tokenRegex.exec(userQuery))) {
|
||||
if (match[1]) {
|
||||
fragments.push(extractWordFromQuery(match[1]));
|
||||
} else if (match[2] === 'OR') {
|
||||
fragments.push('OR');
|
||||
}
|
||||
}
|
||||
|
||||
if (fragments.length === 0) {
|
||||
return userQuery
|
||||
.split(/\bAND\b|\bOR\b/)
|
||||
.map((part) => extractWordFromQuery(part))
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
return fragments.join(' ').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the searched word from a part of search query
|
||||
*
|
||||
* @param queryPart encoded query
|
||||
* @returns searched word
|
||||
*/
|
||||
function extractWordFromQuery(queryPart: string): string {
|
||||
const regex = /:"([^"]+)"/;
|
||||
const quoted = regex.exec(queryPart);
|
||||
if (quoted) {
|
||||
return quoted[1].replace(/\*$/, '');
|
||||
}
|
||||
const trimmedPart = queryPart.trim();
|
||||
if (trimmedPart && !isOperator(trimmedPart)) {
|
||||
return trimmedPart;
|
||||
export function extractParsedQueryFromEncodedQuery(encodedQuery: string): string {
|
||||
if (encodedQuery) {
|
||||
const decodedQuery: { [key: string]: any } = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(encodedQuery), (c) => c.charCodeAt(0))));
|
||||
return decodedQuery.parsedQuery ?? '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -174,14 +124,3 @@ export function extractFiltersFromEncodedQuery(encodedQuery: string): any {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims one set of parentheses from parsed user query.
|
||||
*
|
||||
* @param userQuery user query parsed from encoded query
|
||||
* @returns string
|
||||
*/
|
||||
function trimUserQuery(userQuery: string): string {
|
||||
const trimmedQuery = userQuery?.replace(/^\(/, '');
|
||||
return trimmedQuery?.replace(/\)$/, '') ?? '';
|
||||
}
|
||||
|
||||
+2
@@ -31,6 +31,8 @@ export class SearchInputComponent extends BaseComponent {
|
||||
public searchButton = this.page.locator('.aca-search-input--search-button');
|
||||
public searchCloseButton = this.page.locator('.aca-search-input--close-button');
|
||||
public searchInButton = this.getChild('aca-search-in-menu button');
|
||||
public formulaSearchButton = this.getChild('[value="formula"]');
|
||||
public regularSearchButton = this.getChild('[value="regular"]');
|
||||
|
||||
/**
|
||||
* Method used in cases where user have possibility to navigate "inside" the element (it's clickable and has link attribute).
|
||||
|
||||
@@ -46,6 +46,7 @@ import { AdfConfirmDialogComponent, AdfFolderDialogComponent, ManageVersionsDial
|
||||
import { SearchInDialogComponent } from '../components/search/search-in-dialog.components';
|
||||
|
||||
export type SearchType = 'files' | 'folders' | 'filesAndFolders' | 'libraries';
|
||||
export type SearchMode = 'regular' | 'formula';
|
||||
|
||||
export class SearchPage extends BasePage {
|
||||
private static readonly pageUrl = 'search';
|
||||
@@ -76,11 +77,17 @@ export class SearchPage extends BasePage {
|
||||
public folderInformationDialog = new FolderInformationDialogComponent(this.page);
|
||||
public searchMenuCard = new SearchMenuCard(this.page);
|
||||
|
||||
async searchWithin(searchText: string, searchType?: SearchType): Promise<void> {
|
||||
async searchWithin(searchText: string, searchType?: SearchType, searchMode?: SearchMode): Promise<void> {
|
||||
if (!(await this.searchInputComponent.searchInput.isVisible())) {
|
||||
await this.acaHeader.searchButton.click();
|
||||
}
|
||||
await this.searchInputComponent.searchFor(searchText);
|
||||
await this.dataTable.spinnerWaitForReload();
|
||||
if (searchMode === 'formula') {
|
||||
await this.searchInputComponent.formulaSearchButton.click();
|
||||
} else {
|
||||
await this.searchInputComponent.regularSearchButton.click();
|
||||
}
|
||||
await this.searchInputComponent.searchInButton.click();
|
||||
switch (searchType) {
|
||||
case 'files':
|
||||
|
||||
Reference in New Issue
Block a user