mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
ADW Saved Search (#4173)
* [ACS-8751] Adapt search results to handle query encoding and state propagation * ADW Saved Search * ADW Saved Search * Changes after CR, bug fixed * Changes after CR, bug fixed * Changes after CR, bug fixed * Changes after CR, bug fixed * Changes after CR, bug fixed * Changes after code review * Changes after code review * Changes after code review * Changes after code review * Changes after code review * Changes after code review * Changes after code review * Changes after code review * Changes after code review * Changes after code review * ACS-8751 fix e2e * ACS-8751 fix e2e * ACS-8751 fix e2e * ACS-8751 fix e2e saved-search * ACS-8751 fix e2e recent file * ACS-8751 fix e2e recent file * [ACS-8751] Change encoding from ascii to utf8 to handle special language characters --------- Co-authored-by: MichalKinas <michal.kinas@hyland.com> Co-authored-by: akash.rathod@hyland.com <akash.rathod@hyland.com>
This commit is contained in:
parent
b9213f345c
commit
a7573dc933
@ -335,7 +335,7 @@ test.describe('Library actions ', () => {
|
||||
});
|
||||
|
||||
test('[C306959] Join a public library from Search Results', async ({ searchPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search-libraries;q=${adminLibrary2}` });
|
||||
await searchPage.searchWithin(adminLibrary2, 'libraries');
|
||||
await searchPage.reload({ waitUntil: loadString });
|
||||
await expect(libraryTable.getCellByColumnNameAndRowItem(adminLibrary2, notMemberString)).toBeVisible();
|
||||
await libraryTable.performActionFromExpandableMenu(adminLibrary2, joinButton);
|
||||
@ -344,7 +344,7 @@ test.describe('Library actions ', () => {
|
||||
});
|
||||
|
||||
test('[C306960] Join a moderated library from Search Results', async ({ myLibrariesPage, searchPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search-libraries;q=${adminModerateLibrary2}` });
|
||||
await searchPage.searchWithin(adminModerateLibrary2, 'libraries');
|
||||
await expect(libraryTable.getCellByColumnNameAndRowItem(adminModerateLibrary2, notMemberString)).toBeVisible();
|
||||
await libraryTable.performActionFromExpandableMenu(adminModerateLibrary2, joinButton);
|
||||
await expect.soft(snackBar.getByMessageLocator(requestToJoinMessage)).toBeVisible();
|
||||
@ -356,7 +356,7 @@ test.describe('Library actions ', () => {
|
||||
|
||||
test('[C306961] Leave a library from Search Results', async ({ searchPage }) => {
|
||||
const confirmDialog = searchPage.confirmDialogComponent;
|
||||
await searchPage.navigate({ remoteUrl: `#/search-libraries;q=${user1Library3}` });
|
||||
await searchPage.searchWithin(user1Library3, 'libraries');
|
||||
|
||||
await expect(libraryTable.getCellByColumnNameAndRowItem(user1Library3, 'Collaborator')).toBeVisible();
|
||||
await libraryTable.performActionFromExpandableMenu(user1Library3, leaveLibraryButton);
|
||||
@ -367,7 +367,7 @@ test.describe('Library actions ', () => {
|
||||
|
||||
test('[C306962] Cancel join from Search Results', async ({ searchPage }) => {
|
||||
await user2SitesApi.createSiteMembershipRequestForPerson(username2, adminModerateLibrary4);
|
||||
await searchPage.navigate({ remoteUrl: `#/search-libraries;q=${adminModerateLibrary4}` });
|
||||
await searchPage.searchWithin(adminModerateLibrary4, 'libraries');
|
||||
await expect(libraryTable.getCellByColumnNameAndRowItem(adminModerateLibrary4, notMemberString)).toBeVisible();
|
||||
await libraryTable.performActionFromExpandableMenu(adminModerateLibrary4, cancelJoinRequestButton);
|
||||
await expect.soft(snackBar.getByMessageLocator(cancelJoinRequestMessage)).toBeVisible();
|
||||
@ -415,7 +415,7 @@ test.describe('Library actions ', () => {
|
||||
const libraryMenu = searchPage.matMenu;
|
||||
const trashTable = trashPage.dataTable;
|
||||
|
||||
await searchPage.navigate({ remoteUrl: `#/search-libraries;q=${user2Library7Delete}` });
|
||||
await searchPage.searchWithin(user2Library7Delete, 'libraries');
|
||||
await expect(libraryTable.getRowByName(user2Library7Delete)).toBeVisible();
|
||||
await libraryTable.getRowByName(user2Library7Delete).click();
|
||||
await searchHeader.clickMoreActions();
|
||||
|
@ -88,10 +88,10 @@ test.describe('Special permissions', () => {
|
||||
|
||||
test('[C213173] on Recent Files', async ({ recentFilesPage }) => {
|
||||
await recentFilesPage.navigate();
|
||||
expect(await recentFilesPage.dataTable.getRowsCount(), 'Incorrect number of items').toBe(1);
|
||||
expect(await recentFilesPage.dataTable.getRowsCount(), 'Incorrect number of items').toBeGreaterThanOrEqual(1);
|
||||
await siteApiAdmin.deleteSiteMember(sitePrivate, username);
|
||||
await recentFilesPage.reload();
|
||||
expect(await recentFilesPage.dataTable.isEmpty(), 'Items are still displayed').toBe(true);
|
||||
expect(await recentFilesPage.dataTable.isItemPresent(fileName), 'Items are still displayed').toBe(false);
|
||||
});
|
||||
|
||||
test('[C213227] on Favorites', async ({ favoritePage }) => {
|
||||
|
@ -168,7 +168,7 @@ test.describe('Remember sorting', () => {
|
||||
|
||||
test('[C261147] Sort order is retained when user changes the page from pagination', async ({ personalFiles }) => {
|
||||
const lastFileInArray = testData.user1.files.jpg.slice(-1).pop();
|
||||
const firstFileInArray = testData.user1.files.pdf[0];
|
||||
const firstFileInArray = testData.user1.files.pdf[1];
|
||||
|
||||
await personalFiles.pagination.clickOnNextPage();
|
||||
await personalFiles.dataTable.spinnerWaitForReload();
|
||||
|
@ -89,7 +89,7 @@ export function collaboratorTests(userCollaborator: string, siteName: string) {
|
||||
});
|
||||
|
||||
test('on Search Results - [C297653]', async ({ searchPage, myLibrariesPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search;q=${testData.fileSharedFav.name}` });
|
||||
await searchPage.searchWithin(testData.fileSharedFav.name, 'filesAndFolders');
|
||||
await checkActionsAvailable(
|
||||
myLibrariesPage,
|
||||
testData.fileSharedFav.name,
|
||||
@ -131,7 +131,7 @@ export function collaboratorTests(userCollaborator: string, siteName: string) {
|
||||
});
|
||||
|
||||
test('file opened from Search Results - [C306992]', async ({ searchPage, myLibrariesPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search;q=${testData.fileDocxSharedFav.name}` });
|
||||
await searchPage.searchWithin(testData.fileDocxSharedFav.name, 'filesAndFolders');
|
||||
await checkActionsViewerAvailable(
|
||||
myLibrariesPage,
|
||||
testData.fileDocxSharedFav.name,
|
||||
@ -183,7 +183,7 @@ export function filesLockedByCurrentUser(userDemoted: string, siteName?: string)
|
||||
});
|
||||
|
||||
test('on Search Results - [C297660]', async ({ searchPage, myLibrariesPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search;q=${testData.fileLockedByUser}` });
|
||||
await searchPage.searchWithin(testData.fileLockedByUser, 'filesAndFolders');
|
||||
await checkActionsAvailable(
|
||||
myLibrariesPage,
|
||||
testData.fileLockedByUser,
|
||||
@ -226,7 +226,7 @@ export function filesLockedByCurrentUser(userDemoted: string, siteName?: string)
|
||||
});
|
||||
|
||||
test('file opened from Search Results - [C306993]', async ({ searchPage, myLibrariesPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search;q=${testData.fileLockedByUser}` });
|
||||
await searchPage.searchWithin(testData.fileLockedByUser, 'filesAndFolders');
|
||||
await checkActionsViewerAvailable(
|
||||
myLibrariesPage,
|
||||
testData.fileLockedByUser,
|
||||
@ -281,7 +281,7 @@ export function filesLockedByOtherUser(userManager: string, siteName?: string) {
|
||||
});
|
||||
|
||||
test('on Search Results - [C297667]', async ({ searchPage, myLibrariesPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search;q=${testData.fileLockedByUser}` });
|
||||
await searchPage.searchWithin(testData.fileLockedByUser, 'filesAndFolders');
|
||||
await checkActionsAvailable(
|
||||
myLibrariesPage,
|
||||
testData.fileLockedByUser,
|
||||
@ -324,7 +324,7 @@ export function filesLockedByOtherUser(userManager: string, siteName?: string) {
|
||||
|
||||
// TODO: add 'Move' and 'Delete' when ACA-2319 is fixed
|
||||
test('file opened from Search Results - [C306994]', async ({ searchPage, myLibrariesPage }) => {
|
||||
await searchPage.navigate({ remoteUrl: `#/search;q=${testData.fileLockedByUser}` });
|
||||
await searchPage.searchWithin(testData.fileLockedByUser, 'filesAndFolders');
|
||||
await checkActionsViewerAvailable(
|
||||
myLibrariesPage,
|
||||
testData.fileLockedByUser,
|
||||
|
@ -330,7 +330,7 @@ export function viewerTests(userConsumer: string, siteName: string) {
|
||||
): Promise<void> {
|
||||
await loginPage.navigate();
|
||||
await loginPage.loginUser({ username: userConsumer, password: userConsumer });
|
||||
await searchPage.navigate({ remoteUrl: `#/search;q=${item}` });
|
||||
await searchPage.searchWithin(item, 'filesAndFolders');
|
||||
await searchPage.searchInput.performDoubleClickFolderOrFileToOpen(item);
|
||||
expect(await searchPage.viewer.isViewerOpened(), 'Viewer is not opened').toBe(true);
|
||||
await searchPage.viewer.verifyViewerPrimaryActions(expectedToolbarPrimary);
|
||||
|
@ -260,6 +260,15 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.navbar.secondary",
|
||||
"items": [
|
||||
{
|
||||
"id": "app.search.navbar",
|
||||
"component": "app.search.navbar"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"toolbar": [
|
||||
|
@ -209,6 +209,18 @@
|
||||
"ADVANCED_FILTERS": "Advanced Filters:",
|
||||
"RESET": "Reset",
|
||||
"RESET_ACTION": "Reset search filters",
|
||||
"SAVE_SEARCH": {
|
||||
"ACTION_BUTTON": "Save Search",
|
||||
"MODAL_HEADER": "Save this search",
|
||||
"NAME_LABEL": "Name",
|
||||
"NAME_REQUIRED_ERROR": "This field is required",
|
||||
"DESCRIPTION_LABEL": "Description",
|
||||
"SAVE_SUCCESS": "Search Saved",
|
||||
"SAVE_ERROR": "Error occured. Search could not be saved.",
|
||||
"NAVBAR": {
|
||||
"TITLE": "Saved Searches ({{ number }})"
|
||||
}
|
||||
},
|
||||
"FOUND_RESULTS": "{{ number }} results found",
|
||||
"FOUND_ONE_RESULT": "{{ number }} result found",
|
||||
"CUSTOM_ROW": {
|
||||
|
@ -25,7 +25,7 @@
|
||||
import { HammerModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TRANSLATION_PROVIDER, AuthGuardEcm, LanguagePickerComponent, NotificationHistoryComponent } from '@alfresco/adf-core';
|
||||
import { AuthGuardEcm, LanguagePickerComponent, NotificationHistoryComponent, TRANSLATION_PROVIDER } from '@alfresco/adf-core';
|
||||
import {
|
||||
ContentModule,
|
||||
ContentVersionService,
|
||||
@ -71,13 +71,14 @@ import { TagsColumnComponent } from './components/dl-custom-components/tags-colu
|
||||
import { UserInfoComponent } from './components/common/user-info/user-info.component';
|
||||
import { SidenavComponent } from './components/sidenav/sidenav.component';
|
||||
import { ContentManagementService } from './services/content-management.service';
|
||||
import { ShellLayoutComponent, SHELL_NAVBAR_MIN_WIDTH } from '@alfresco/adf-core/shell';
|
||||
import { SHELL_NAVBAR_MIN_WIDTH, ShellLayoutComponent } from '@alfresco/adf-core/shell';
|
||||
import { UserMenuComponent } from './components/sidenav/user-menu/user-menu.component';
|
||||
import { ContextMenuComponent } from './components/context-menu/context-menu.component';
|
||||
import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
|
||||
import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component';
|
||||
import { BulkActionsDropdownComponent } from './components/bulk-actions-dropdown/bulk-actions-dropdown.component';
|
||||
import { AgentsButtonComponent } from './components/knowledge-retrieval/search-ai/agents-button/agents-button.component';
|
||||
import { SaveSearchSidenavComponent } from './components/search/search-save/sidenav/save-search-sidenav.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -157,7 +158,8 @@ export class ContentServiceExtensionModule {
|
||||
'app.user': UserInfoComponent,
|
||||
'app.notification-center': NotificationHistoryComponent,
|
||||
'app.user.menu': UserMenuComponent,
|
||||
'app.search.columns.name': SearchResultsRowComponent
|
||||
'app.search.columns.name': SearchResultsRowComponent,
|
||||
'app.search.navbar': SaveSearchSidenavComponent
|
||||
});
|
||||
|
||||
extensions.setEvaluators({
|
||||
|
@ -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,
|
||||
@ -67,12 +68,9 @@ import { FormsModule } from '@angular/forms';
|
||||
host: { class: 'aca-search-input' }
|
||||
})
|
||||
export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
private notificationService = inject(NotificationService);
|
||||
private readonly notificationService = inject(NotificationService);
|
||||
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
hasOneChange = false;
|
||||
hasNewChange = false;
|
||||
navigationTimer: any;
|
||||
has400LibraryError = false;
|
||||
hasLibrariesConstraint = false;
|
||||
searchOnChange: boolean;
|
||||
@ -106,14 +104,15 @@ export class SearchInputComponent implements OnInit, OnDestroy {
|
||||
trigger: MatMenuTrigger;
|
||||
|
||||
constructor(
|
||||
private queryBuilder: SearchQueryBuilderService,
|
||||
private queryLibrariesBuilder: SearchLibrariesQueryBuilderService,
|
||||
private config: AppConfigService,
|
||||
private router: Router,
|
||||
private store: Store<AppStore>,
|
||||
private appHookService: AppHookService,
|
||||
private appService: AppService,
|
||||
public searchInputService: SearchNavigationService
|
||||
private readonly queryBuilder: SearchQueryBuilderService,
|
||||
private readonly queryLibrariesBuilder: SearchLibrariesQueryBuilderService,
|
||||
private readonly config: AppConfigService,
|
||||
private readonly router: Router,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly store: Store<AppStore>,
|
||||
private readonly appHookService: AppHookService,
|
||||
private readonly appService: AppService,
|
||||
public readonly searchInputService: SearchNavigationService
|
||||
) {
|
||||
this.searchOnChange = this.config.get<boolean>('search.aca:triggeredOnChange', true);
|
||||
}
|
||||
@ -121,14 +120,13 @@ 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'];
|
||||
if (encodedQuery && this.searchInputControl) {
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(encodedQuery);
|
||||
this.searchInputControl.searchTerm = this.searchedWord;
|
||||
}
|
||||
});
|
||||
|
||||
this.appHookService.library400Error.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
|
||||
this.has400LibraryError = true;
|
||||
@ -192,24 +190,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 +285,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] || 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();
|
||||
|
@ -26,14 +26,26 @@
|
||||
<div class="aca-content__advanced-filters">
|
||||
<div class="aca-content__advanced-filters--header">
|
||||
<p>{{ 'APP.BROWSE.SEARCH.ADVANCED_FILTERS' | translate }}</p>
|
||||
<button
|
||||
mat-button
|
||||
adf-reset-search
|
||||
class="aca-content__reset-action"
|
||||
title="{{ 'APP.BROWSE.SEARCH.RESET_ACTION' | translate }}"
|
||||
[attr.aria-label]="'APP.BROWSE.SEARCH.RESET_ACTION' | translate ">
|
||||
{{ 'APP.BROWSE.SEARCH.RESET' | translate }}
|
||||
</button>
|
||||
<div class="aca-content__advanced-filters--header--action-buttons">
|
||||
<button
|
||||
mat-button
|
||||
acaSaveSearch
|
||||
[acaSaveSearchQuery]="encodedQuery"
|
||||
[disabled]="!encodedQuery"
|
||||
class="aca-content__save-search-action"
|
||||
title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}"
|
||||
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate ">
|
||||
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
adf-reset-search
|
||||
class="aca-content__reset-action"
|
||||
title="{{ 'APP.BROWSE.SEARCH.RESET_ACTION' | translate }}"
|
||||
[attr.aria-label]="'APP.BROWSE.SEARCH.RESET_ACTION' | translate ">
|
||||
{{ 'APP.BROWSE.SEARCH.RESET' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<adf-search-filter-chips></adf-search-filter-chips>
|
||||
</div>
|
||||
|
@ -155,10 +155,15 @@ aca-search-results {
|
||||
&--header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&--action-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__reset-action {
|
||||
&__reset-action,
|
||||
&__save-search-action {
|
||||
margin-top: 5px;
|
||||
margin-right: 2px;
|
||||
line-height: 33px;
|
||||
|
@ -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';
|
||||
import { testHeader } from '../../../testing/document-base-page-utils';
|
||||
|
||||
describe('SearchComponent', () => {
|
||||
@ -44,10 +45,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({
|
||||
@ -80,6 +86,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');
|
||||
@ -150,79 +158,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: {
|
||||
@ -268,17 +203,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();
|
||||
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*"))`);
|
||||
});
|
||||
|
||||
testHeader(SearchResultsComponent, false);
|
||||
|
@ -22,13 +22,14 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { NodeEntry, Pagination, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import {
|
||||
AlfrescoViewerComponent,
|
||||
DocumentListComponent,
|
||||
ResetSearchDirective,
|
||||
SearchConfiguration,
|
||||
SearchFilterChipsComponent,
|
||||
SearchFormComponent,
|
||||
SearchQueryBuilderService,
|
||||
@ -39,8 +40,8 @@ import {
|
||||
NavigateToFolder,
|
||||
SetInfoDrawerPreviewStateAction,
|
||||
SetInfoDrawerStateAction,
|
||||
ShowInfoDrawerPreviewAction,
|
||||
SetSearchItemsTotalCountAction
|
||||
SetSearchItemsTotalCountAction,
|
||||
ShowInfoDrawerPreviewAction
|
||||
} from '@alfresco/aca-shared/store';
|
||||
import {
|
||||
CustomEmptyContentTemplateDirective,
|
||||
@ -52,7 +53,6 @@ import {
|
||||
TranslationService,
|
||||
ViewerToolbarComponent
|
||||
} from '@alfresco/adf-core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import {
|
||||
ContextActionsDirective,
|
||||
InfoDrawerComponent,
|
||||
@ -78,6 +78,14 @@ 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';
|
||||
import { SaveSearchDirective } from '../search-save/directive/save-search.directive';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@ -112,7 +120,8 @@ import { SearchAiInputContainerComponent } from '../../knowledge-retrieval/searc
|
||||
CustomEmptyContentTemplateDirective,
|
||||
ViewerToolbarComponent,
|
||||
BulkActionsDropdownComponent,
|
||||
SearchAiInputContainerComponent
|
||||
SearchAiInputContainerComponent,
|
||||
SaveSearchDirective
|
||||
],
|
||||
selector: 'aca-search-results',
|
||||
templateUrl: './search-results.component.html',
|
||||
@ -132,12 +141,17 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
totalResults: number;
|
||||
isTagsEnabled = false;
|
||||
columns: DocumentListPresetRef[] = [];
|
||||
encodedQuery: string;
|
||||
searchConfig: SearchConfiguration;
|
||||
|
||||
private readonly loadedFilters$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
tagsService: TagService,
|
||||
private queryBuilder: SearchQueryBuilderService,
|
||||
private route: ActivatedRoute,
|
||||
private translationService: TranslationService
|
||||
private readonly queryBuilder: SearchQueryBuilderService,
|
||||
private readonly changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly translationService: TranslationService
|
||||
) {
|
||||
super();
|
||||
|
||||
@ -148,15 +162,12 @@ 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) => {
|
||||
this.searchConfig = searchConfig;
|
||||
this.updateUserQuery();
|
||||
});
|
||||
}
|
||||
|
||||
@ -171,6 +182,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
if (query) {
|
||||
this.sorting = this.getSorting();
|
||||
this.isLoading = true;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}),
|
||||
|
||||
@ -179,6 +191,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
|
||||
this.onSearchResultLoaded(data);
|
||||
this.isLoading = false;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}),
|
||||
|
||||
this.queryBuilder.error.subscribe((err: any) => {
|
||||
@ -189,17 +202,31 @@ 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();
|
||||
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
|
||||
this.loadedFilters$.next();
|
||||
this.encodedQuery = params[this.queryParamName] || null;
|
||||
this.searchedWord = extractSearchedWordFromEncodedQuery(this.encodedQuery);
|
||||
this.updateUserQuery();
|
||||
const filtersFromEncodedQuery = extractFiltersFromEncodedQuery(this.encodedQuery);
|
||||
if (filtersFromEncodedQuery !== null) {
|
||||
const filtersToLoad = this.queryBuilder.categories.length;
|
||||
let loadedFilters = this.searchedWord === '' ? 0 : 1;
|
||||
this.queryBuilder.filterLoaded
|
||||
.asObservable()
|
||||
.pipe(takeUntil(this.onDestroy$), takeUntil(this.loadedFilters$))
|
||||
.subscribe(() => {
|
||||
loadedFilters++;
|
||||
if (filtersToLoad === loadedFilters) {
|
||||
this.loadedFilters$.next();
|
||||
this.queryBuilder.execute(false);
|
||||
}
|
||||
});
|
||||
this.queryBuilder.populateFilters.next(filtersFromEncodedQuery);
|
||||
} else {
|
||||
this.queryBuilder.userQuery = null;
|
||||
this.queryBuilder.executed.next({
|
||||
list: { pagination: { totalItems: 0 }, entries: [] }
|
||||
});
|
||||
this.queryBuilder.populateFilters.next({});
|
||||
this.queryBuilder.execute(false);
|
||||
}
|
||||
this.queryBuilder.userQuery = extractUserQueryFromEncodedQuery(this.encodedQuery);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -217,59 +244,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();
|
||||
@ -329,4 +303,9 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
||||
this.queryBuilder.sorting = [{ ...option, ascending: option.ascending }];
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
|
||||
private updateUserQuery(): void {
|
||||
const updatedUserQuery = formatSearchTerm(this.searchedWord, this.searchConfig['app:fields']);
|
||||
this.queryBuilder.userQuery = updatedUserQuery;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
<h2 mat-dialog-title>{{"APP.BROWSE.SEARCH.SAVE_SEARCH.MODAL_HEADER" | translate}}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<form [formGroup]="form" (submit)="submit()">
|
||||
<mat-form-field class="aca-save-search-dialog__form-field">
|
||||
<mat-label>{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_LABEL' | translate }}</mat-label>
|
||||
<input
|
||||
id="aca-save-search-dialog-name-input"
|
||||
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_LABEL' | translate"
|
||||
matInput
|
||||
required
|
||||
[formControlName]="'name'"
|
||||
adf-auto-focus
|
||||
/>
|
||||
|
||||
<mat-error *ngIf="form.controls['name'].touched">
|
||||
<span *ngIf="form.controls['name'].errors?.required">
|
||||
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_REQUIRED_ERROR' | translate }}
|
||||
</span>
|
||||
<span *ngIf="!form.controls['name'].errors?.required && form.controls['name'].errors?.message">
|
||||
{{ form.controls['name'].errors?.message | translate }}
|
||||
</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="aca-save-search-dialog__form-field">
|
||||
<mat-label>{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.DESCRIPTION_LABEL' | translate }}</mat-label>
|
||||
<textarea
|
||||
id="aca-save-search-dialog-description-input"
|
||||
matInput
|
||||
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.DESCRIPTION_LABEL' | translate"
|
||||
rows="4"
|
||||
[formControlName]="'description'"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button
|
||||
mat-button
|
||||
id="aca-save-search-dialog-cancel-button"
|
||||
mat-dialog-close>
|
||||
{{ 'CANCEL' | titlecase | translate }}
|
||||
</button>
|
||||
|
||||
<button id="aca-save-search-dialog-save-button"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="submit()"
|
||||
[disabled]="!form.valid || disableSubmitButton">
|
||||
{{ 'SAVE' | titlecase | translate}}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
@ -0,0 +1,5 @@
|
||||
.aca-save-search-dialog {
|
||||
.aca-save-search-dialog__form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/*!
|
||||
* 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 { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { of } from 'rxjs';
|
||||
import { SaveSearchDialogComponent } from './save-search-dialog.component';
|
||||
import { ContentTestingModule, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { AppTestingModule } from '../../../../testing/app-testing.module';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||
|
||||
describe('SaveSearchDialogComponent', () => {
|
||||
let fixture: ComponentFixture<SaveSearchDialogComponent>;
|
||||
let component: SaveSearchDialogComponent;
|
||||
let savedSearchesService: SavedSearchesService;
|
||||
let store: Store;
|
||||
let submitButton: HTMLButtonElement;
|
||||
|
||||
const dialogRef = {
|
||||
close: jasmine.createSpy('close')
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ContentTestingModule, AppTestingModule],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: dialogRef },
|
||||
provideMockStore(),
|
||||
{ provide: SavedSearchesService, useValue: { saveSearch: () => of() } },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { searchUrl: 'abcdef' } }
|
||||
]
|
||||
});
|
||||
dialogRef.close.calls.reset();
|
||||
fixture = TestBed.createComponent(SaveSearchDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
savedSearchesService = TestBed.inject(SavedSearchesService);
|
||||
store = TestBed.inject(Store);
|
||||
|
||||
submitButton = fixture.nativeElement.querySelector('#aca-save-search-dialog-save-button');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should not save search if form is invalid', () => {
|
||||
spyOn(savedSearchesService, 'saveSearch').and.callThrough();
|
||||
submitButton.click();
|
||||
expect(savedSearchesService.saveSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save search, show snackbar message and close modal if form is valid', fakeAsync(() => () => {
|
||||
spyOn(savedSearchesService, 'saveSearch').and.callThrough();
|
||||
setFormValuesAndSubmit();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS'));
|
||||
expect(dialogRef.close).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should show snackbar error if there is save error', fakeAsync(() => () => {
|
||||
spyOn(savedSearchesService, 'saveSearch').and.throwError('');
|
||||
setFormValuesAndSubmit();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_ERROR'));
|
||||
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
function setFormValuesAndSubmit() {
|
||||
spyOn(store, 'dispatch');
|
||||
component.form.controls['name'].setValue('ABCDEF');
|
||||
component.form.controls['description'].setValue('TEST');
|
||||
submitButton.click();
|
||||
tick();
|
||||
expect(savedSearchesService.saveSearch).toHaveBeenCalledWith({
|
||||
name: 'ABCDEF',
|
||||
description: 'TEST',
|
||||
encodedUrl: 'abcdef'
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,104 @@
|
||||
/*!
|
||||
* 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 { Component, Inject, ViewEncapsulation } from '@angular/core';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { SearchInputControlComponent } from '../../search-input-control/search-input-control.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { FormControl, FormGroup, FormsModule, Validators } from '@angular/forms';
|
||||
import { CoreModule } from '@alfresco/adf-core';
|
||||
import { AutoFocusDirective, forbidOnlySpaces, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
MatMenuModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
A11yModule,
|
||||
MatCheckboxModule,
|
||||
FormsModule,
|
||||
SearchInputControlComponent,
|
||||
CoreModule,
|
||||
AutoFocusDirective
|
||||
],
|
||||
selector: 'aca-save-search-dialog',
|
||||
templateUrl: './save-search-dialog.component.html',
|
||||
styleUrls: ['./save-search-dialog.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: { class: 'aca-save-search-dialog' }
|
||||
})
|
||||
export class SaveSearchDialogComponent {
|
||||
form = new FormGroup({
|
||||
name: new FormControl('', [Validators.required, forbidOnlySpaces]),
|
||||
description: new FormControl('')
|
||||
});
|
||||
|
||||
disableSubmitButton = false;
|
||||
|
||||
constructor(
|
||||
private readonly dialog: MatDialogRef<SaveSearchDialogComponent>,
|
||||
private readonly store: Store<AppStore>,
|
||||
private readonly savedSearchesService: SavedSearchesService,
|
||||
@Inject(MAT_DIALOG_DATA) private readonly data: { searchUrl: string }
|
||||
) {}
|
||||
|
||||
submit() {
|
||||
if (this.form.invalid || this.disableSubmitButton) {
|
||||
return;
|
||||
}
|
||||
this.disableSubmitButton = true;
|
||||
const formValue = this.form.value;
|
||||
const saveSearch = { name: formValue.name, description: formValue.description, encodedUrl: encodeURIComponent(this.data.searchUrl) };
|
||||
this.savedSearchesService
|
||||
.saveSearch(saveSearch)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.dialog.close();
|
||||
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS'));
|
||||
this.disableSubmitButton = false;
|
||||
},
|
||||
error: () => {
|
||||
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_ERROR'));
|
||||
this.disableSubmitButton = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*!
|
||||
* 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 { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { of } from 'rxjs';
|
||||
import { SaveSearchDirective } from './save-search.directive';
|
||||
import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-test-component',
|
||||
template: '<div acaSaveSearch="searchQuery" acaSaveSearchQuery="encodedQuery"></div>',
|
||||
standalone: true,
|
||||
imports: [SaveSearchDirective]
|
||||
})
|
||||
class TestComponent {
|
||||
searchQuery = 'encodedQuery';
|
||||
}
|
||||
|
||||
describe('SaveSearchDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let element: DebugElement;
|
||||
let dialog: MatDialog;
|
||||
|
||||
const event = {
|
||||
type: 'click',
|
||||
preventDefault: jasmine.createSpy('preventDefault')
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [SaveSearchDirective, TestComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: MatDialog,
|
||||
useValue: {
|
||||
open: () => ({
|
||||
afterClosed: jasmine.createSpy('afterClosed').and.returnValue(of(null))
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
element = fixture.debugElement.query(By.directive(SaveSearchDirective));
|
||||
dialog = TestBed.inject(MatDialog);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should prevent the default click action', () => {
|
||||
element.triggerEventHandler('click', event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open the dialog with the correct configuration', () => {
|
||||
spyOn(dialog, 'open');
|
||||
element.triggerEventHandler('click', event);
|
||||
|
||||
const expectedConfig = {
|
||||
data: { searchUrl: 'encodedQuery' }
|
||||
};
|
||||
|
||||
expect(dialog.open).toHaveBeenCalledWith(SaveSearchDialogComponent, expectedConfig);
|
||||
});
|
||||
});
|
@ -0,0 +1,59 @@
|
||||
/*!
|
||||
* 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 { Directive, HostListener, Input } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component';
|
||||
|
||||
interface SaveSearchDirectiveDialogData {
|
||||
searchUrl: string;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[acaSaveSearch]',
|
||||
standalone: true
|
||||
})
|
||||
export class SaveSearchDirective {
|
||||
/** Encoded search query */
|
||||
@Input()
|
||||
acaSaveSearchQuery: string;
|
||||
|
||||
constructor(private readonly dialogRef: MatDialog) {}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
this.openDialog();
|
||||
}
|
||||
|
||||
private openDialog(): void {
|
||||
this.dialogRef.open(SaveSearchDialogComponent, this.getDialogConfig());
|
||||
}
|
||||
|
||||
private getDialogConfig(): { data: SaveSearchDirectiveDialogData } {
|
||||
return {
|
||||
data: { searchUrl: this.acaSaveSearchQuery }
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<app-expand-menu *ngIf="item" [item]="item" (actionClicked)="onActionClick()"></app-expand-menu>
|
@ -0,0 +1,84 @@
|
||||
/*!
|
||||
* 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 { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { SaveSearchSidenavComponent } from './save-search-sidenav.component';
|
||||
import { SavedSearchesService } from '@alfresco/adf-content-services';
|
||||
import { AppTestingModule } from '../../../../testing/app-testing.module';
|
||||
import { of, ReplaySubject } from 'rxjs';
|
||||
|
||||
describe('SaveSearchSidenavComponent', () => {
|
||||
let fixture: ComponentFixture<SaveSearchSidenavComponent>;
|
||||
let component: SaveSearchSidenavComponent;
|
||||
let savedSearchesService: SavedSearchesService;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockService = {
|
||||
innit: () => {},
|
||||
getSavedSearches: () => of(),
|
||||
savedSearches$: new ReplaySubject(1)
|
||||
};
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule, SaveSearchSidenavComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: SavedSearchesService,
|
||||
useValue: mockService
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(SaveSearchSidenavComponent);
|
||||
component = fixture.componentInstance;
|
||||
savedSearchesService = TestBed.inject(SavedSearchesService);
|
||||
});
|
||||
|
||||
it('should set navbar object if no search is saved', async () => {
|
||||
savedSearchesService.savedSearches$.next([]);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.item).toEqual({
|
||||
icon: '',
|
||||
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.TITLE',
|
||||
children: [],
|
||||
route: '/',
|
||||
id: 'search-navbar'
|
||||
});
|
||||
});
|
||||
|
||||
it('should set navbar object with children is searches are saved', fakeAsync(() => {
|
||||
savedSearchesService.savedSearches$.next([{ name: '1', order: 0, encodedUrl: 'abc' }]);
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(component.item.children[0]).toEqual({
|
||||
icon: '',
|
||||
title: '1',
|
||||
route: 'search?q=abc',
|
||||
url: 'search?q=abc',
|
||||
id: 'search1'
|
||||
});
|
||||
}));
|
||||
});
|
@ -0,0 +1,88 @@
|
||||
/*!
|
||||
* 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 { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { SavedSearch, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { CoreModule, TranslationService } from '@alfresco/adf-core';
|
||||
import { DynamicExtensionComponent, NavBarLinkRef } from '@alfresco/adf-extensions';
|
||||
import { ExpandMenuComponent } from '../../../sidenav/components/expand-menu.component';
|
||||
import { SidenavHeaderComponent } from '../../../sidenav/components/sidenav-header.component';
|
||||
import { AppService } from '@alfresco/aca-shared';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-save-search-sidenav',
|
||||
standalone: true,
|
||||
imports: [CoreModule, DynamicExtensionComponent, ExpandMenuComponent, SidenavHeaderComponent],
|
||||
templateUrl: './save-search-sidenav.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SaveSearchSidenavComponent implements OnInit, OnDestroy {
|
||||
savedSearchesService = inject(SavedSearchesService);
|
||||
appService = inject(AppService);
|
||||
translationService = inject(TranslationService);
|
||||
destroy$ = new Subject<void>();
|
||||
|
||||
item: NavBarLinkRef;
|
||||
|
||||
ngOnInit() {
|
||||
this.savedSearchesService.innit();
|
||||
this.savedSearchesService.savedSearches$
|
||||
.asObservable()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((savedSearches) => {
|
||||
this.item = this.createNavBarLinkRef(savedSearches);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
onActionClick(): void {
|
||||
this.appService.appNavNarMode$.next('collapsed');
|
||||
}
|
||||
|
||||
private createNavBarLinkRef(children: SavedSearch[]): NavBarLinkRef {
|
||||
const mappedChildren = children
|
||||
.map((child) => ({
|
||||
id: 'search' + child.name,
|
||||
icon: '',
|
||||
title: child.name,
|
||||
route: `search?q=${child.encodedUrl}`,
|
||||
url: `search?q=${child.encodedUrl}`
|
||||
}))
|
||||
.slice(0, 5);
|
||||
const title = this.translationService.instant('APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.TITLE', { number: children.length });
|
||||
return {
|
||||
icon: '',
|
||||
title,
|
||||
children: mappedChildren,
|
||||
route: '/',
|
||||
id: 'search-navbar'
|
||||
};
|
||||
}
|
||||
}
|
@ -20,7 +20,6 @@
|
||||
<mat-expansion-panel
|
||||
class="aca-expansion-panel"
|
||||
[expanded]="true"
|
||||
[acaExpansionPanel]="item"
|
||||
[@.disabled]="true"
|
||||
>
|
||||
<mat-expansion-panel-header expandedHeight="32px" collapsedHeight="32px" role="group" class="aca-expansion-panel-header">
|
||||
@ -44,6 +43,7 @@
|
||||
<button
|
||||
acaActiveLink="aca-action-button--active"
|
||||
[action]="child"
|
||||
(actionClicked)="actionClicked.emit()"
|
||||
[attr.aria-label]="child.title | translate"
|
||||
[id]="child.id"
|
||||
[attr.data-automation-id]="child.id"
|
||||
|
@ -22,7 +22,7 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, Input, ViewEncapsulation, ChangeDetectorRef } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
|
||||
import { NavBarLinkRef } from '@alfresco/adf-extensions';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@ -54,6 +54,9 @@ export class ExpandMenuComponent implements OnInit {
|
||||
@Input()
|
||||
item: NavBarLinkRef;
|
||||
|
||||
@Output()
|
||||
actionClicked = new EventEmitter<void>();
|
||||
|
||||
constructor(private cd: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -23,34 +23,46 @@
|
||||
*/
|
||||
|
||||
import { ActionDirective } from './action.directive';
|
||||
import { provideRouter, Router } from '@angular/router';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '@alfresco/aca-shared/store';
|
||||
|
||||
describe('ActionDirective', () => {
|
||||
let directive: ActionDirective;
|
||||
const routeMock: any = {
|
||||
navigate: jasmine.createSpy('navigate'),
|
||||
parseUrl: () => ({
|
||||
root: {
|
||||
children: []
|
||||
}
|
||||
})
|
||||
};
|
||||
const storeMock: any = {
|
||||
dispatch: jasmine.createSpy('dispatch')
|
||||
};
|
||||
let router: Router;
|
||||
let store: Store<AppStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
directive = new ActionDirective(routeMock, storeMock);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ActionDirective],
|
||||
providers: [provideRouter([]), provideMockStore()]
|
||||
});
|
||||
|
||||
store = TestBed.inject(Store);
|
||||
router = TestBed.inject(Router);
|
||||
directive = new ActionDirective(router, store);
|
||||
});
|
||||
|
||||
it('should navigate if action is route', () => {
|
||||
spyOn(router, 'navigate');
|
||||
directive.action = { url: 'dummy' };
|
||||
directive.onClick();
|
||||
expect(routeMock.navigate).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['dummy', {}], { queryParams: {} });
|
||||
});
|
||||
|
||||
it('should get query params correctly from URL', () => {
|
||||
spyOn(router, 'navigate');
|
||||
directive.action = { url: 'dummy?q=12345' };
|
||||
directive.onClick();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['dummy', {}], { queryParams: { q: '12345' } });
|
||||
});
|
||||
|
||||
it('should dispatch store action', () => {
|
||||
spyOn(store, 'dispatch');
|
||||
directive.action = { click: {} };
|
||||
directive.onClick();
|
||||
expect(storeMock.dispatch).toHaveBeenCalled();
|
||||
expect(store.dispatch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -22,8 +22,8 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, Input, HostListener } from '@angular/core';
|
||||
import { PRIMARY_OUTLET, Router } from '@angular/router';
|
||||
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
|
||||
import { Params, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '@alfresco/aca-shared/store';
|
||||
|
||||
@ -36,20 +36,22 @@ import { AppStore } from '@alfresco/aca-shared/store';
|
||||
export class ActionDirective {
|
||||
@Input() action;
|
||||
|
||||
@Output() actionClicked = new EventEmitter<void>();
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
if (this.action.url) {
|
||||
this.router.navigate(this.getNavigationCommands(this.action.url));
|
||||
this.router.navigate(this.getNavigationCommands(this.action.url), { queryParams: this.getNavigationQueryParams(this.action.url) });
|
||||
} else if (this.action.click) {
|
||||
this.store.dispatch({
|
||||
type: this.action.click.action,
|
||||
payload: this.getNavigationCommands(this.action.click.payload)
|
||||
});
|
||||
}
|
||||
this.actionClicked.next();
|
||||
}
|
||||
|
||||
constructor(private router: Router, private store: Store<AppStore>) {}
|
||||
|
||||
private getNavigationCommands(url: string): any[] {
|
||||
const urlTree = this.router.parseUrl(url);
|
||||
const urlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
||||
@ -65,4 +67,8 @@ export class ActionDirective {
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
private getNavigationQueryParams(url: string): Params {
|
||||
return this.router.parseUrl(url).queryParams;
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,8 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Directive, OnInit, Input, ElementRef, Renderer2, ContentChildren, QueryList, AfterContentInit, Optional } from '@angular/core';
|
||||
import { Router, NavigationEnd } from '@angular/router';
|
||||
import { AfterContentInit, ContentChildren, Directive, ElementRef, Input, OnInit, Optional, QueryList, Renderer2 } from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ActionDirective } from './action.directive';
|
||||
|
@ -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');
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -22,16 +22,19 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Actions, ofType, createEffect } from '@ngrx/effects';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
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 readonly actions$ = inject(Actions);
|
||||
private readonly queryBuilder = inject(SearchQueryBuilderService);
|
||||
private readonly searchNavigationService = inject(SearchNavigationService);
|
||||
|
||||
search$ = createEffect(
|
||||
() =>
|
||||
@ -49,15 +52,10 @@ 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));
|
||||
} else {
|
||||
this.router.navigateByUrl('/search;q=' + encodeURIComponent(query));
|
||||
}
|
||||
this.queryBuilder.navigateToSearch(query, librarySelected ? '/search-libraries' : '/search');
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
|
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 empty string when input is invalid', () => {
|
||||
expect(formatSearchTerm(null)).toEqual('');
|
||||
expect(formatSearchTerm(undefined)).toEqual('');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
143
projects/aca-content/src/lib/utils/aca-search-utils.ts
Normal file
143
projects/aca-content/src/lib/utils/aca-search-utils.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/*!
|
||||
* 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 '';
|
||||
}
|
||||
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('utf8'));
|
||||
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('utf8');
|
||||
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';
|
||||
|
@ -32,7 +32,7 @@ export class SearchOverlayComponent extends BaseComponent {
|
||||
public searchFoldersOption = this.getChild('input#folder-input');
|
||||
public searchLibrariesOption = this.getChild('input#libraries-input');
|
||||
public searchInput = this.page.locator('#app-control-input');
|
||||
public searchButton = this.page.locator('.aca-search-input--search-button');
|
||||
public searchButton = this.page.locator('app-search-input-control button[title="Search"]');
|
||||
public searchInputControl = this.page.locator('.app-search-control');
|
||||
public searchOptions = this.page.locator('.app-search-options');
|
||||
|
||||
|
@ -72,7 +72,7 @@ export class SearchPage extends BasePage {
|
||||
public uploadNewVersionDialog = new UploadNewVersionDialog(this.page);
|
||||
public manageVersionsDialog = new ManageVersionsDialog(this.page);
|
||||
|
||||
async searchWithin(searchText: string, searchType: SearchType): Promise<void> {
|
||||
async searchWithin(searchText: string, searchType?: SearchType): Promise<void> {
|
||||
await this.acaHeader.searchButton.click();
|
||||
await this.clickSearchButton();
|
||||
switch (searchType) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user