mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-09-10 14:11:17 +00:00
[ACS-8751] Adapt search results to handle query encoding and state propagation
This commit is contained in:
36
package-lock.json
generated
36
package-lock.json
generated
@@ -32,6 +32,7 @@
|
||||
"@ngrx/store": "^15.2.0",
|
||||
"@ngrx/store-devtools": "^15.2.0",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"material-icons": "^1.13.12",
|
||||
"minimatch-browser": "^1.0.0",
|
||||
@@ -12362,7 +12363,6 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -12471,6 +12471,30 @@
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/blocking-proxy": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
|
||||
@@ -12652,10 +12676,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -12672,7 +12695,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
@@ -18408,7 +18431,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
@@ -53,6 +53,7 @@
|
||||
"@ngrx/store": "^15.2.0",
|
||||
"@ngrx/store-devtools": "^15.2.0",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"material-icons": "^1.13.12",
|
||||
"minimatch-browser": "^1.0.0",
|
||||
|
@@ -32,15 +32,18 @@ import { AppHookService, AppService } from '@alfresco/aca-shared';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { SearchNavigationService } from '../search-navigation.service';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, of, Subject } from 'rxjs';
|
||||
import { NotificationService } from '@alfresco/adf-core';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { Buffer } from 'buffer';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
describe('SearchInputComponent', () => {
|
||||
let fixture: ComponentFixture<SearchInputComponent>;
|
||||
let component: SearchInputComponent;
|
||||
let actions$: Actions;
|
||||
let appHookService: AppHookService;
|
||||
let route: ActivatedRoute;
|
||||
let searchInputService: SearchNavigationService;
|
||||
let showErrorSpy: jasmine.Spy;
|
||||
|
||||
@@ -50,6 +53,10 @@ describe('SearchInputComponent', () => {
|
||||
toggleAppNavBar$: new Subject()
|
||||
};
|
||||
|
||||
const encodeQuery = (query: any): string => {
|
||||
return Buffer.from(JSON.stringify(query)).toString('base64');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
appServiceMock.setAppNavbarMode.calls.reset();
|
||||
TestBed.configureTestingModule({
|
||||
@@ -68,6 +75,7 @@ describe('SearchInputComponent', () => {
|
||||
fixture = TestBed.createComponent(SearchInputComponent);
|
||||
appHookService = TestBed.inject(AppHookService);
|
||||
searchInputService = TestBed.inject(SearchNavigationService);
|
||||
route = TestBed.inject(ActivatedRoute);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
const notificationService = TestBed.inject(NotificationService);
|
||||
@@ -147,35 +155,10 @@ describe('SearchInputComponent', () => {
|
||||
});
|
||||
|
||||
describe('onSearchChange()', () => {
|
||||
it('should call search action with correct search options', (done) => {
|
||||
it('should call search action with correct searched term', () => {
|
||||
const searchedTerm = 's';
|
||||
const currentSearchOptions = [{ key: 'SEARCH.INPUT.FILES' }];
|
||||
actions$
|
||||
.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
expect(action.searchOptions[0].key).toBe(currentSearchOptions[0].key);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
component.onSearchChange(searchedTerm);
|
||||
});
|
||||
|
||||
it('should call search action with correct searched term', (done) => {
|
||||
const searchedTerm = 's';
|
||||
actions$
|
||||
.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
expect(action.payload).toBe(searchedTerm);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
component.onSearchChange(searchedTerm);
|
||||
expect(component.searchedWord).toBe(searchedTerm);
|
||||
});
|
||||
|
||||
it('should show snack for empty search', () => {
|
||||
@@ -246,4 +229,14 @@ describe('SearchInputComponent', () => {
|
||||
|
||||
expect(appServiceMock.setAppNavbarMode).toHaveBeenCalledWith('expanded');
|
||||
});
|
||||
|
||||
it('should extract searched word from query params', (done) => {
|
||||
route.queryParams = of({ q: encodeQuery({ userQuery: 'cm:name:"test*"' }) });
|
||||
route.queryParams.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.searchedWord).toBe('test');
|
||||
done();
|
||||
});
|
||||
fixture.detectChanges();
|
||||
});
|
||||
});
|
||||
|
@@ -28,10 +28,10 @@ import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { AppConfigService, NotificationService } from '@alfresco/adf-core';
|
||||
import { Component, inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { NavigationEnd, PRIMARY_OUTLET, Router, RouterEvent, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router';
|
||||
import { ActivatedRoute, Params, PRIMARY_OUTLET, Router, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { SearchInputControlComponent } from '../search-input-control/search-input-control.component';
|
||||
import { SearchNavigationService } from '../search-navigation.service';
|
||||
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
|
||||
@@ -44,6 +44,7 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { extractSearchedWordFromEncodedQuery } from '../../../utils/aca-search-utils';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -70,9 +71,6 @@ export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
private notificationService = inject(NotificationService);
|
||||
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
hasOneChange = false;
|
||||
hasNewChange = false;
|
||||
navigationTimer: any;
|
||||
has400LibraryError = false;
|
||||
hasLibrariesConstraint = false;
|
||||
searchOnChange: boolean;
|
||||
@@ -110,6 +108,7 @@ export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
private queryLibrariesBuilder: SearchLibrariesQueryBuilderService,
|
||||
private config: AppConfigService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private store: Store<AppStore>,
|
||||
private appHookService: AppHookService,
|
||||
private appService: AppService,
|
||||
@@ -121,14 +120,14 @@ export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.showInputValue();
|
||||
|
||||
this.router.events
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.pipe(filter((e) => e instanceof RouterEvent))
|
||||
.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.showInputValue();
|
||||
}
|
||||
});
|
||||
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
|
||||
const encodedQuery = params['q'] ? params['q'] : null;
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(encodedQuery);
|
||||
|
||||
if (this.searchInputControl) {
|
||||
this.searchInputControl.searchTerm = this.searchedWord;
|
||||
}
|
||||
});
|
||||
|
||||
this.appHookService.library400Error.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
|
||||
this.has400LibraryError = true;
|
||||
@@ -192,24 +191,6 @@ export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
this.has400LibraryError = false;
|
||||
this.hasLibrariesConstraint = this.evaluateLibrariesConstraint();
|
||||
this.searchedWord = searchTerm;
|
||||
|
||||
if (this.hasOneChange) {
|
||||
this.hasNewChange = true;
|
||||
} else {
|
||||
this.hasOneChange = true;
|
||||
}
|
||||
|
||||
if (this.hasNewChange) {
|
||||
clearTimeout(this.navigationTimer);
|
||||
this.hasNewChange = false;
|
||||
}
|
||||
|
||||
this.navigationTimer = setTimeout(() => {
|
||||
if (searchTerm) {
|
||||
this.store.dispatch(new SearchByTermAction(searchTerm, this.searchOptions));
|
||||
}
|
||||
this.hasOneChange = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
searchByOption() {
|
||||
@@ -305,7 +286,7 @@ export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (urlSegmentGroup) {
|
||||
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
|
||||
searchTerm = urlSegments[0].parameters['q'] ? decodeURIComponent(urlSegments[0].parameters['q']) : '';
|
||||
searchTerm = extractSearchedWordFromEncodedQuery(urlSegments[0].parameters['q']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -27,13 +27,16 @@ import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { SearchLibrariesResultsComponent } from './search-libraries-results.component';
|
||||
import { SearchLibrariesQueryBuilderService } from './search-libraries-query-builder.service';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, of, Subject } from 'rxjs';
|
||||
import { AppService } from '@alfresco/aca-shared';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
describe('SearchLibrariesResultsComponent', () => {
|
||||
let component: SearchLibrariesResultsComponent;
|
||||
let fixture: ComponentFixture<SearchLibrariesResultsComponent>;
|
||||
let route: ActivatedRoute;
|
||||
|
||||
const emptyPage = { list: { pagination: { totalItems: 0 }, entries: [] } };
|
||||
const appServiceMock = {
|
||||
@@ -42,6 +45,10 @@ describe('SearchLibrariesResultsComponent', () => {
|
||||
setAppNavbarMode: jasmine.createSpy('setAppNavbarMode')
|
||||
};
|
||||
|
||||
const encodeQuery = (query: any): string => {
|
||||
return Buffer.from(JSON.stringify(query)).toString('base64');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
appServiceMock.setAppNavbarMode.calls.reset();
|
||||
TestBed.configureTestingModule({
|
||||
@@ -56,6 +63,7 @@ describe('SearchLibrariesResultsComponent', () => {
|
||||
]
|
||||
});
|
||||
|
||||
route = TestBed.inject(ActivatedRoute);
|
||||
fixture = TestBed.createComponent(SearchLibrariesResultsComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
@@ -72,4 +80,14 @@ describe('SearchLibrariesResultsComponent', () => {
|
||||
|
||||
expect(appServiceMock.setAppNavbarMode).toHaveBeenCalledWith('collapsed');
|
||||
});
|
||||
|
||||
it('should extract searched word from query params', (done) => {
|
||||
route.queryParams = of({ q: encodeQuery({ userQuery: 'cm:name:"test*"' }) });
|
||||
route.queryParams.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.searchedWord).toBe('test');
|
||||
done();
|
||||
});
|
||||
fixture.detectChanges();
|
||||
});
|
||||
});
|
||||
|
@@ -45,6 +45,8 @@ 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 { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -128,14 +130,12 @@ export class SearchLibrariesResultsComponent extends PageComponent implements On
|
||||
);
|
||||
|
||||
if (this.route) {
|
||||
this.route.params.forEach((params: Params) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||
const query = this.formatSearchQuery(this.searchedWord);
|
||||
|
||||
if (query && query.length > 1) {
|
||||
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
|
||||
const encodedQuery = params[this.queryParamName] ? params[this.queryParamName] : null;
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(encodedQuery);
|
||||
if (this.searchedWord?.length > 1) {
|
||||
this.librariesQueryBuilder.paging.skipCount = 0;
|
||||
this.librariesQueryBuilder.userQuery = query;
|
||||
this.librariesQueryBuilder.userQuery = this.searchedWord;
|
||||
this.librariesQueryBuilder.update();
|
||||
} else {
|
||||
this.librariesQueryBuilder.userQuery = null;
|
||||
@@ -147,13 +147,6 @@ export class SearchLibrariesResultsComponent extends PageComponent implements On
|
||||
}
|
||||
}
|
||||
|
||||
private formatSearchQuery(userInput: string) {
|
||||
if (!userInput) {
|
||||
return null;
|
||||
}
|
||||
return userInput.trim();
|
||||
}
|
||||
|
||||
onSearchResultLoaded(nodePaging: NodePaging) {
|
||||
this.data = nodePaging;
|
||||
this.totalResults = this.getNumberOfResults();
|
||||
|
@@ -30,10 +30,11 @@ import { NavigateToFolder } from '@alfresco/aca-shared/store';
|
||||
import { Pagination, SearchRequest } from '@alfresco/js-api';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, of, Subject } from 'rxjs';
|
||||
import { AppTestingModule } from '../../../testing/app-testing.module';
|
||||
import { AppService } from '@alfresco/aca-shared';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
describe('SearchComponent', () => {
|
||||
let component: SearchResultsComponent;
|
||||
@@ -43,10 +44,15 @@ describe('SearchComponent', () => {
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
let translate: TranslationService;
|
||||
let router: Router;
|
||||
let route: ActivatedRoute;
|
||||
const searchRequest = {} as SearchRequest;
|
||||
let params: BehaviorSubject<any>;
|
||||
let showErrorSpy: jasmine.Spy;
|
||||
|
||||
const encodeQuery = (query: any): string => {
|
||||
return Buffer.from(JSON.stringify(query)).toString('base64');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' });
|
||||
TestBed.configureTestingModule({
|
||||
@@ -79,6 +85,8 @@ describe('SearchComponent', () => {
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
translate = TestBed.inject(TranslationService);
|
||||
router = TestBed.inject(Router);
|
||||
route = TestBed.inject(ActivatedRoute);
|
||||
route.queryParams = of({});
|
||||
|
||||
const notificationService = TestBed.inject(NotificationService);
|
||||
showErrorSpy = spyOn(notificationService, 'showError');
|
||||
@@ -149,79 +157,6 @@ describe('SearchComponent', () => {
|
||||
expect(showErrorSpy).toHaveBeenCalledWith('Generic Error');
|
||||
}));
|
||||
|
||||
it('should decode encoded URI', () => {
|
||||
expect(queryBuilder.userQuery).toEqual('(TYPE: "cm:folder" AND (=cm: name: email OR cm: name: budget))');
|
||||
});
|
||||
|
||||
it('should return null if formatting invalid query', () => {
|
||||
expect(component.formatSearchQuery(null)).toBeNull();
|
||||
expect(component.formatSearchQuery('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should use original user input if text contains colons', () => {
|
||||
const query = 'TEXT:test OR TYPE:folder';
|
||||
expect(component.formatSearchQuery(query)).toBe(query);
|
||||
});
|
||||
|
||||
it('should be able to search if search input contains https url', () => {
|
||||
const query = component.formatSearchQuery('https://alfresco.com');
|
||||
expect(query).toBe(`(cm:name:"https://alfresco.com*")`);
|
||||
});
|
||||
|
||||
it('should be able to search if search input contains http url', () => {
|
||||
const query = component.formatSearchQuery('http://alfresco.com');
|
||||
expect(query).toBe(`(cm:name:"http://alfresco.com*")`);
|
||||
});
|
||||
|
||||
it('should use original user input if text contains quotes', () => {
|
||||
const query = `"Hello World"`;
|
||||
expect(component.formatSearchQuery(query)).toBe(query);
|
||||
});
|
||||
|
||||
it('should format user input according to the configuration fields', () => {
|
||||
const query = component.formatSearchQuery('hello', ['cm:name', 'cm:title']);
|
||||
expect(query).toBe(`(cm:name:"hello*" OR cm:title:"hello*")`);
|
||||
});
|
||||
|
||||
it('should format user input as cm:name if configuration not provided', () => {
|
||||
const query = component.formatSearchQuery('hello');
|
||||
expect(query).toBe(`(cm:name:"hello*")`);
|
||||
});
|
||||
|
||||
it('should use AND operator when conjunction has no operators', () => {
|
||||
const query = component.formatSearchQuery('big yellow banana', ['cm:name']);
|
||||
|
||||
expect(query).toBe(`(cm:name:"big*") AND (cm:name:"yellow*") AND (cm:name:"banana*")`);
|
||||
});
|
||||
|
||||
it('should support conjunctions with AND operator', () => {
|
||||
const query = component.formatSearchQuery('big AND yellow AND banana', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).toBe(
|
||||
`(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', () => {
|
||||
const query = component.formatSearchQuery('big OR yellow OR banana', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).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 default fields', () => {
|
||||
const query = component.formatSearchQuery('=orange', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).toBe(`(=cm:name:"orange" OR =cm:title:"orange")`);
|
||||
});
|
||||
|
||||
it('should support exact term matching with operators', () => {
|
||||
const query = component.formatSearchQuery('=test1.pdf or =test2.pdf', ['cm:name', 'cm:title']);
|
||||
|
||||
expect(query).toBe(`(=cm:name:"test1.pdf" OR =cm:title:"test1.pdf") or (=cm:name:"test2.pdf" OR =cm:title:"test2.pdf")`);
|
||||
});
|
||||
|
||||
it('should navigate to folder on double click', () => {
|
||||
const node: any = {
|
||||
entry: {
|
||||
@@ -267,16 +202,26 @@ describe('SearchComponent', () => {
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update the user query whenever param changed', () => {
|
||||
params.next({ q: '=orange' });
|
||||
expect(queryBuilder.userQuery).toBe(`((=cm:name:"orange"))`);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
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' } };
|
||||
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' } });
|
||||
queryBuilder.filterLoaded.next();
|
||||
fixture.detectChanges();
|
||||
expect(queryBuilder.execute).toHaveBeenCalledWith(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the user query whenever configuration changed', () => {
|
||||
params.next({ q: '=orange' });
|
||||
component.searchedWord = 'orange';
|
||||
queryBuilder.configUpdated.next({ 'app:fields': ['cm:tag'] } as any);
|
||||
expect(queryBuilder.userQuery).toBe(`((=cm:tag:"orange"))`);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
expect(queryBuilder.userQuery).toBe(`((cm:tag:"orange*"))`);
|
||||
});
|
||||
});
|
||||
|
@@ -52,7 +52,6 @@ import {
|
||||
TranslationService,
|
||||
ViewerToolbarComponent
|
||||
} from '@alfresco/adf-core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import {
|
||||
ContextActionsDirective,
|
||||
InfoDrawerComponent,
|
||||
@@ -78,6 +77,12 @@ import { SearchResultsRowComponent } from '../search-results-row/search-results-
|
||||
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';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -148,14 +153,13 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
maxItems: 25
|
||||
};
|
||||
|
||||
combineLatest([this.route.params, this.queryBuilder.configUpdated])
|
||||
this.queryBuilder.configUpdated
|
||||
.asObservable()
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(([params, searchConfig]) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||
const query = this.formatSearchQuery(this.searchedWord, searchConfig['app:fields']);
|
||||
if (query) {
|
||||
this.queryBuilder.userQuery = decodeURIComponent(query);
|
||||
.subscribe((searchConfig) => {
|
||||
const updatedUserQuery = formatSearchTerm(this.searchedWord, searchConfig['app:fields']);
|
||||
if (updatedUserQuery) {
|
||||
this.queryBuilder.userQuery = updatedUserQuery;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -189,17 +193,26 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
this.columns = this.extensions.documentListPresets.searchResults || [];
|
||||
|
||||
if (this.route) {
|
||||
this.route.params.forEach((params: Params) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||
if (this.searchedWord) {
|
||||
this.queryBuilder.update();
|
||||
} else {
|
||||
this.queryBuilder.userQuery = null;
|
||||
this.queryBuilder.executed.next({
|
||||
list: { pagination: { totalItems: 0 }, entries: [] }
|
||||
});
|
||||
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
|
||||
const encodedQuery = params[this.queryParamName] ? params[this.queryParamName] : null;
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(encodedQuery);
|
||||
const filtersFromEncodedQuery = extractFiltersFromEncodedQuery(encodedQuery);
|
||||
if (filtersFromEncodedQuery !== null) {
|
||||
const filtersToLoad = Object.keys(filtersFromEncodedQuery).length;
|
||||
let loadedFilters = this.searchedWord === '' ? 0 : 1;
|
||||
this.queryBuilder.filterLoaded
|
||||
.asObservable()
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(() => {
|
||||
loadedFilters++;
|
||||
if (this.data === undefined && filtersToLoad === loadedFilters) {
|
||||
this.queryBuilder.execute(false);
|
||||
}
|
||||
});
|
||||
this.queryBuilder.populateFilters.next(filtersFromEncodedQuery);
|
||||
}
|
||||
|
||||
this.queryBuilder.userQuery = extractUserQueryFromEncodedQuery(encodedQuery);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -217,59 +230,6 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
this.notificationService.showError(message);
|
||||
}
|
||||
|
||||
private isOperator(input: string): boolean {
|
||||
if (input) {
|
||||
input = input.trim().toUpperCase();
|
||||
|
||||
const operators = ['AND', 'OR'];
|
||||
return operators.includes(input);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private formatFields(fields: string[], term: 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 ') + ')';
|
||||
}
|
||||
|
||||
formatSearchQuery(userInput: string, fields = ['cm:name']) {
|
||||
if (!userInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^http[s]?:\/\//.test(userInput)) {
|
||||
return this.formatFields(fields, userInput);
|
||||
}
|
||||
|
||||
userInput = userInput.trim();
|
||||
|
||||
if (userInput.includes(':') || userInput.includes('"')) {
|
||||
return userInput;
|
||||
}
|
||||
|
||||
const words = userInput.split(' ');
|
||||
|
||||
if (words.length > 1) {
|
||||
const separator = words.some(this.isOperator) ? ' ' : ' AND ';
|
||||
return words.map((term) => (this.isOperator(term) ? term : this.formatFields(fields, term))).join(separator);
|
||||
}
|
||||
|
||||
return this.formatFields(fields, userInput);
|
||||
}
|
||||
|
||||
onSearchResultLoaded(nodePaging: ResultSetPaging) {
|
||||
this.data = nodePaging;
|
||||
this.totalResults = this.getNumberOfResults();
|
||||
|
@@ -29,10 +29,12 @@ import { EffectsModule } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Router } from '@angular/router';
|
||||
import { SearchOptionIds, SearchByTermAction, SearchAction } from '@alfresco/aca-shared/store';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
|
||||
describe('SearchEffects', () => {
|
||||
let store: Store<any>;
|
||||
let router: Router;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -41,18 +43,21 @@ describe('SearchEffects', () => {
|
||||
|
||||
store = TestBed.inject(Store);
|
||||
router = TestBed.inject(Router);
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
|
||||
spyOn(router, 'navigateByUrl').and.stub();
|
||||
});
|
||||
|
||||
describe('searchByTerm$', () => {
|
||||
it('should navigate to `search` when search options has library false', fakeAsync(() => {
|
||||
spyOn(queryBuilder, 'navigateToSearch');
|
||||
store.dispatch(new SearchByTermAction('test', []));
|
||||
tick();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/search;q=test');
|
||||
expect(queryBuilder.navigateToSearch).toHaveBeenCalledWith('(cm:name:"test*")', '/search');
|
||||
}));
|
||||
|
||||
it('should navigate to `search-libraries` when search options has library true', fakeAsync(() => {
|
||||
spyOn(queryBuilder, 'navigateToSearch');
|
||||
store.dispatch(
|
||||
new SearchByTermAction('test', [
|
||||
{
|
||||
@@ -66,23 +71,7 @@ describe('SearchEffects', () => {
|
||||
|
||||
tick();
|
||||
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/search-libraries;q=test');
|
||||
}));
|
||||
|
||||
it('should encode search string for parentheses', fakeAsync(() => {
|
||||
store.dispatch(new SearchByTermAction('(test)', []));
|
||||
|
||||
tick();
|
||||
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/search;q=%2528test%2529');
|
||||
}));
|
||||
|
||||
it('should encode %', fakeAsync(() => {
|
||||
store.dispatch(new SearchByTermAction('%test%', []));
|
||||
|
||||
tick();
|
||||
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/search;q=%2525test%2525');
|
||||
expect(queryBuilder.navigateToSearch).toHaveBeenCalledWith('(cm:name:"test*")', '/search-libraries');
|
||||
}));
|
||||
});
|
||||
|
||||
|
@@ -23,15 +23,18 @@
|
||||
*/
|
||||
|
||||
import { Actions, ofType, createEffect } from '@ngrx/effects';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SearchAction, SearchActionTypes, SearchByTermAction, SearchOptionIds } from '@alfresco/aca-shared/store';
|
||||
import { Router } from '@angular/router';
|
||||
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 {
|
||||
constructor(private actions$: Actions, private router: Router, private searchNavigationService: SearchNavigationService) {}
|
||||
private actions$ = inject(Actions);
|
||||
private queryBuilder = inject(SearchQueryBuilderService);
|
||||
private searchNavigationService = inject(SearchNavigationService);
|
||||
|
||||
search$ = createEffect(
|
||||
() =>
|
||||
@@ -49,14 +52,13 @@ export class SearchEffects {
|
||||
this.actions$.pipe(
|
||||
ofType<SearchByTermAction>(SearchActionTypes.SearchByTerm),
|
||||
map((action) => {
|
||||
const query = action.payload.replace(/%/g, '%25').replace(/[(]/g, '%28').replace(/[)]/g, '%29');
|
||||
|
||||
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;
|
||||
if (librarySelected) {
|
||||
this.router.navigateByUrl('/search-libraries;q=' + encodeURIComponent(query));
|
||||
this.queryBuilder.navigateToSearch(query, '/search-libraries');
|
||||
} else {
|
||||
this.router.navigateByUrl('/search;q=' + encodeURIComponent(query));
|
||||
this.queryBuilder.navigateToSearch(query, '/search');
|
||||
}
|
||||
})
|
||||
),
|
||||
|
155
projects/aca-content/src/lib/utils/aca-search-utils.spec.ts
Normal file
155
projects/aca-content/src/lib/utils/aca-search-utils.spec.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
extractFiltersFromEncodedQuery,
|
||||
extractSearchedWordFromEncodedQuery,
|
||||
extractUserQueryFromEncodedQuery,
|
||||
formatSearchTerm,
|
||||
formatSearchTermByFields,
|
||||
isOperator
|
||||
} from './aca-search-utils';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
describe('SearchUtils', () => {
|
||||
const encodeQuery = (query: any): string => {
|
||||
return Buffer.from(JSON.stringify(query)).toString('base64');
|
||||
};
|
||||
|
||||
describe('isOperator', () => {
|
||||
it('should detect AND operator', () => {
|
||||
expect(isOperator('AND')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should detect OR operator', () => {
|
||||
expect(isOperator('OR')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return false when operator is not present', () => {
|
||||
expect(isOperator('WITH')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should return false when input is not valid', () => {
|
||||
expect(isOperator(null)).toBeFalse();
|
||||
expect(isOperator(undefined)).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSearchTermByFields', () => {
|
||||
it('should append "*" to search term', () => {
|
||||
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 format search term with set of fields and join with OR', () => {
|
||||
expect(formatSearchTermByFields('test', ['name', 'size'])).toBe('(name:"test*" OR size:"test*")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSearchTerm', () => {
|
||||
it('should return null when input is invalid', () => {
|
||||
expect(formatSearchTerm(null)).toBeNull();
|
||||
expect(formatSearchTerm(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not transfer custom queries', () => {
|
||||
expect(formatSearchTerm('test:"term"')).toBe('test:"term"');
|
||||
expect(formatSearchTerm('"test"')).toBe('"test"');
|
||||
});
|
||||
|
||||
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', ['name', 'size'])).toBe(
|
||||
'(name:"test*" OR size:"test*") AND (name:"word*" OR size:"word*") AND (name:"term*" OR size:"term*")'
|
||||
);
|
||||
});
|
||||
|
||||
it('should format user input as cm:name if configuration not provided', () => {
|
||||
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*")`
|
||||
);
|
||||
});
|
||||
|
||||
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")`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractUserQueryFromEncodedQuery', () => {
|
||||
it('should return empty string when encoded query is invalid', () => {
|
||||
expect(extractUserQueryFromEncodedQuery(null)).toBe('');
|
||||
expect(extractUserQueryFromEncodedQuery(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('should properly extract user query', () => {
|
||||
const query = { userQuery: 'cm:name:"test"' };
|
||||
expect(extractUserQueryFromEncodedQuery(encodeQuery(query))).toBe('cm:name:"test"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractSearchedWordFromEncodedQuery', () => {
|
||||
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('');
|
||||
});
|
||||
|
||||
it('should properly extract search term', () => {
|
||||
const query = { userQuery: 'cm:name:"test*"' };
|
||||
expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFiltersFromEncodedQuery', () => {
|
||||
it('should return null when encoded query is invalid', () => {
|
||||
expect(extractFiltersFromEncodedQuery(null)).toBeNull();
|
||||
expect(extractFiltersFromEncodedQuery(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('should properly parse encoded object', () => {
|
||||
const query = { userQuery: 'cm:name:"test*"', filterProp: 'test' };
|
||||
expect(extractFiltersFromEncodedQuery(encodeQuery(query))).toEqual(query);
|
||||
});
|
||||
});
|
||||
});
|
144
projects/aca-content/src/lib/utils/aca-search-utils.ts
Normal file
144
projects/aca-content/src/lib/utils/aca-search-utils.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
/**
|
||||
* Checks if string is an AND or OR operator
|
||||
*
|
||||
* @param input string to check if it is an operator
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isOperator(input: string): boolean {
|
||||
if (input) {
|
||||
input = input.trim().toUpperCase();
|
||||
|
||||
const operators = ['AND', 'OR'];
|
||||
return operators.includes(input);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a search term by provided fields
|
||||
*
|
||||
* @param term search term
|
||||
* @param fields array of fields
|
||||
* @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 ') + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a search term, splits by words, skips custom queries containing ':' or '"'
|
||||
*
|
||||
* @param userInput search term
|
||||
* @param fields array of fields
|
||||
* @returns string
|
||||
*/
|
||||
export function formatSearchTerm(userInput: string, fields = ['cm:name']): string {
|
||||
if (!userInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
userInput = userInput.trim();
|
||||
|
||||
if (userInput.includes(':') || userInput.includes('"')) {
|
||||
return userInput;
|
||||
}
|
||||
|
||||
const words = userInput.split(' ');
|
||||
|
||||
if (words.length > 1) {
|
||||
const separator = words.some(isOperator) ? ' ' : ' AND ';
|
||||
return words.map((term) => (isOperator(term) ? term : formatSearchTermByFields(term, fields))).join(separator);
|
||||
}
|
||||
|
||||
return formatSearchTermByFields(userInput, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a query and extracts the user query
|
||||
*
|
||||
* @param encodedQuery encoded query
|
||||
* @returns string
|
||||
*/
|
||||
export function extractUserQueryFromEncodedQuery(encodedQuery: string): string {
|
||||
if (encodedQuery) {
|
||||
const decodedQuery: { [key: string]: any } = JSON.parse(Buffer.from(encodedQuery, 'base64').toString('ascii'));
|
||||
return decodedQuery.userQuery;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts user query from encoded query and splits it to get a search term
|
||||
*
|
||||
* @param encodedQuery encoded query
|
||||
* @returns string
|
||||
*/
|
||||
export function extractSearchedWordFromEncodedQuery(encodedQuery: string): string {
|
||||
if (encodedQuery) {
|
||||
const userQuery = extractUserQueryFromEncodedQuery(encodedQuery);
|
||||
return userQuery !== '' && userQuery !== undefined
|
||||
? userQuery
|
||||
.split('AND')
|
||||
.map((searchCondition) => {
|
||||
const searchTerm = searchCondition.split('"')[1];
|
||||
return searchTerm === '*' ? searchTerm : searchTerm.slice(0, -1);
|
||||
})
|
||||
.join(' ')
|
||||
: '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts filters configuration from encoded query
|
||||
*
|
||||
* @param encodedQuery encoded query
|
||||
* @returns object containing filters configuration
|
||||
*/
|
||||
export function extractFiltersFromEncodedQuery(encodedQuery: string): any {
|
||||
if (encodedQuery) {
|
||||
const decodedQuery = Buffer.from(encodedQuery, 'base64').toString('ascii');
|
||||
return JSON.parse(decodedQuery);
|
||||
}
|
||||
return null;
|
||||
}
|
@@ -31,3 +31,4 @@ export * from './lib/aca-content.routes';
|
||||
export * from './lib/extensions/core.extensions.module';
|
||||
export * from './lib/store/initial-state';
|
||||
export * from './lib/services/content-url.service';
|
||||
export * from './lib/utils/aca-search-utils';
|
||||
|
Reference in New Issue
Block a user