Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ACS-8634] Add new option to edit changes in saved search or save as new #4229

Merged
merged 10 commits into from
Nov 8, 2024
6 changes: 5 additions & 1 deletion projects/aca-content/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,15 @@
"RESET_ACTION": "Reset search filters",
"SAVE_SEARCH": {
"ACTION_BUTTON": "Save Search",
"SAVE_CHANGES": "Save changes",
"SAVE_AS_NEW": "Save as new",
"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 occurred. Search could not be saved.",
"SEARCH_NAME_NOT_UNIQUE_ERROR": "Saved Search with '{{ name }}' name already exists.",
"NAVBAR": {
"TITLE": "Saved Searches ({{ number }})",
"MANAGE_BUTTON": "Manage searches"
Expand All @@ -240,7 +243,8 @@
"DESCRIPTION": "Description",
"EMPTY_LIST": "No saved searches",
"COPY_TO_CLIPBOARD": "Copy to clipboard",
"COPY_TO_CLIPBOARD_SUCCESS": "Search copied to clipboard"
"COPY_TO_CLIPBOARD_SUCCESS": "Search copied to clipboard",
"EXECUTE_SEARCH": "Execute Search"
}
},
"FOUND_RESULTS": "{{ number }} results found",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,45 @@
<p>{{ 'APP.BROWSE.SEARCH.ADVANCED_FILTERS' | translate }}</p>
<div class="aca-content__advanced-filters--header--action-buttons">
<button
*ngIf="initialSavedSearch !== undefined else saveSearchButton"
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 ">
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate "
[matMenuTriggerFor]="saveSearchOptionsMenu">
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}
<mat-icon iconPositionEnd>keyboard_arrow_down</mat-icon>
</button>
<mat-menu #saveSearchOptionsMenu="matMenu">
<button
mat-menu-item
(click)="editSavedSearch(initialSavedSearch)"
title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_CHANGES' | translate }}"
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_CHANGES' | translate ">
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_CHANGES' | translate }}
</button>
<button
mat-menu-item
acaSaveSearch
[acaSaveSearchQuery]="encodedQuery"
title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_AS_NEW' | translate }}"
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_AS_NEW' | translate ">
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_AS_NEW' | translate }}
</button>
</mat-menu>
<ng-template #saveSearchButton>
<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>
</ng-template>
<button
mat-button
adf-reset-search
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin
import { SearchResultsComponent } from './search-results.component';
import { AppConfigService, NotificationService, TranslationService } from '@alfresco/adf-core';
import { Store } from '@ngrx/store';
import { NavigateToFolder } from '@alfresco/aca-shared/store';
import { NavigateToFolder, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
import { Pagination, SearchRequest } from '@alfresco/js-api';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { SavedSearchesService, SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, of, Subject } from 'rxjs';
import { BehaviorSubject, of, Subject, throwError } 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';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatMenuModule } from '@angular/material/menu';
import { MatMenuHarness } from '@angular/material/menu/testing';

describe('SearchComponent', () => {
let component: SearchResultsComponent;
Expand All @@ -49,6 +54,10 @@ describe('SearchComponent', () => {
const searchRequest = {} as SearchRequest;
let params: BehaviorSubject<any>;
let showErrorSpy: jasmine.Spy;
let loader: HarnessLoader;

const editSavedSearchesSpy = jasmine.createSpy('editSavedSearch');
const getSavedSearchButton = (): HTMLButtonElement => fixture.nativeElement.querySelector('.aca-content__save-search-action');

const encodeQuery = (query: any): string => {
return Buffer.from(JSON.stringify(query)).toString('base64');
Expand All @@ -57,7 +66,7 @@ describe('SearchComponent', () => {
beforeEach(() => {
params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' });
TestBed.configureTestingModule({
imports: [AppTestingModule, SearchResultsComponent, MatSnackBarModule],
imports: [AppTestingModule, SearchResultsComponent, MatSnackBarModule, MatMenuModule, NoopAnimationsModule],
providers: [
{
provide: AppService,
Expand All @@ -67,6 +76,15 @@ describe('SearchComponent', () => {
setAppNavbarMode: jasmine.createSpy('setAppNavbarMode')
}
},
{
provide: SavedSearchesService,
useValue: {
getSavedSearches: jasmine
.createSpy('getSavedSearches')
.and.returnValue(of([{ name: 'test', encodedUrl: encodeQuery({ name: 'test' }), order: 0 }])),
editSavedSearch: editSavedSearchesSpy
}
},
{
provide: ActivatedRoute,
useValue: {
Expand Down Expand Up @@ -102,6 +120,7 @@ describe('SearchComponent', () => {
spyOn(queryBuilder, 'update').and.stub();

fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
});

afterEach(() => {
Expand Down Expand Up @@ -225,5 +244,66 @@ describe('SearchComponent', () => {
expect(queryBuilder.userQuery).toBe(`((cm:tag:"orange*"))`);
});

it('should get initial saved search when url matches', fakeAsync(() => {
route.queryParams = of({ q: encodeQuery({ name: 'test' }) });
component.ngOnInit();
tick();
expect(component.initialSavedSearch).toEqual({ name: 'test', encodedUrl: encodeQuery({ name: 'test' }), order: 0 });
}));

it('should render a menu with 2 options when initial saved search is found', async () => {
route.queryParams = of({ q: encodeQuery({ name: 'test' }) });
component.ngOnInit();
fixture.detectChanges();
const saveSearchButton = getSavedSearchButton();
expect(saveSearchButton).toBeDefined();
expect(saveSearchButton.textContent.trim()).toBe('APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON keyboard_arrow_down');

const menu = await loader.getHarness(MatMenuHarness.with({ selector: '.aca-content__save-search-action' }));
expect(await menu.isDisabled()).toBeFalse();
await menu.open();
expect(await menu.isOpen()).toBeTrue();
const menuItems = await menu.getItems();
expect(menuItems.length).toBe(2);
expect(await menuItems[0].getText()).toBe('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_CHANGES');
expect(await menuItems[1].getText()).toBe('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_AS_NEW');
});

it('should not get initial saved search when url does not match', fakeAsync(() => {
route.snapshot.queryParams = { q: 'test2' };
tick();
component.ngOnInit();
tick();
expect(component.initialSavedSearch).toBeUndefined();
}));

it('should render regular save search button when there is no initial saved search', fakeAsync(() => {
route.snapshot.queryParams = { q: 'test2' };
tick();
component.ngOnInit();
tick();
fixture.detectChanges();
const saveSearchButton = getSavedSearchButton();
expect(saveSearchButton).toBeDefined();
expect(saveSearchButton.textContent.trim()).toBe('APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON');
expect(saveSearchButton.getAttribute('aria-label')).toBe('APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON');
}));

it('should dispatch success snackbar action when editing saved search is successful', fakeAsync(() => {
spyOn(store, 'dispatch').and.stub();
editSavedSearchesSpy.and.returnValue(of({}));
component.editSavedSearch({ name: 'test', encodedUrl: 'test', order: 0 });
tick();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
}));

it('should dispatch error snackbar action when editing saved search failed', fakeAsync(() => {
spyOn(store, 'dispatch').and.stub();
editSavedSearchesSpy.and.returnValue(throwError(() => new Error('')));
component.editSavedSearch({ name: 'test', encodedUrl: 'test', order: 0 });
tick();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
}));

testHeader(SearchResultsComponent, false);
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/

import { ChangeDetectorRef, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { ChangeDetectorRef, Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { NodeEntry, Pagination, ResultSetPaging } from '@alfresco/js-api';
import { ActivatedRoute, Params } from '@angular/router';
import {
AlfrescoViewerComponent,
DocumentListComponent,
ResetSearchDirective,
SavedSearch,
SavedSearchesService,
SearchConfiguration,
SearchFilterChipsComponent,
SearchFormComponent,
Expand All @@ -41,7 +43,9 @@ import {
SetInfoDrawerPreviewStateAction,
SetInfoDrawerStateAction,
SetSearchItemsTotalCountAction,
ShowInfoDrawerPreviewAction
ShowInfoDrawerPreviewAction,
SnackbarErrorAction,
SnackbarInfoAction
} from '@alfresco/aca-shared/store';
import {
CustomEmptyContentTemplateDirective,
Expand All @@ -62,7 +66,7 @@ import {
ToolbarComponent
} from '@alfresco/aca-shared';
import { SearchSortingDefinition } from '@alfresco/adf-content-services/lib/search/models/search-sorting-definition.interface';
import { takeUntil } from 'rxjs/operators';
import { take, takeUntil } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { SearchInputComponent } from '../search-input/search-input.component';
Expand All @@ -86,6 +90,8 @@ import {
} from '../../../utils/aca-search-utils';
import { SaveSearchDirective } from '../search-save/directive/save-search.directive';
import { Subject } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatMenuModule } from '@angular/material/menu';

@Component({
standalone: true,
Expand All @@ -96,6 +102,7 @@ import { Subject } from 'rxjs';
MatProgressBarModule,
MatDividerModule,
MatButtonModule,
MatMenuModule,
DocumentListDirective,
ContextActionsDirective,
ThumbnailColumnComponent,
Expand Down Expand Up @@ -140,18 +147,21 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
isLoading = false;
totalResults: number;
isTagsEnabled = false;
initialSavedSearch: SavedSearch = undefined;
columns: DocumentListPresetRef[] = [];
encodedQuery: string;
searchConfig: SearchConfiguration;

private readonly loadedFilters$ = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);

constructor(
tagsService: TagService,
private readonly queryBuilder: SearchQueryBuilderService,
private readonly changeDetectorRef: ChangeDetectorRef,
private readonly route: ActivatedRoute,
private readonly translationService: TranslationService
private readonly translationService: TranslationService,
private readonly savedSearchesService: SavedSearchesService
) {
super();

Expand All @@ -164,7 +174,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {

this.queryBuilder.configUpdated
.asObservable()
.pipe(takeUntil(this.onDestroy$))
.pipe(takeUntilDestroyed())
.subscribe((searchConfig) => {
this.searchConfig = searchConfig;
this.updateUserQuery();
Expand Down Expand Up @@ -202,7 +212,14 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.searchResults || [];

if (this.route) {
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params: Params) => {
this.savedSearchesService
.getSavedSearches()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((savedSearches) => {
const savedSearchFound = savedSearches.find((savedSearch) => savedSearch.encodedUrl === encodeURIComponent(params[this.queryParamName]));
this.initialSavedSearch = savedSearchFound !== undefined ? savedSearchFound : this.initialSavedSearch;
});
if (params[this.queryParamName]) {
this.isLoading = true;
}
Expand All @@ -216,7 +233,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
let loadedFilters = this.searchedWord === '' ? 0 : 1;
this.queryBuilder.filterLoaded
.asObservable()
.pipe(takeUntil(this.onDestroy$), takeUntil(this.loadedFilters$))
.pipe(takeUntilDestroyed(this.destroyRef), takeUntil(this.loadedFilters$))
.subscribe(() => {
loadedFilters++;
if (filtersToLoad === loadedFilters) {
Expand Down Expand Up @@ -307,6 +324,21 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
this.queryBuilder.update();
}

editSavedSearch(searchToSave: SavedSearch) {
searchToSave.encodedUrl = this.encodedQuery;
this.savedSearchesService
.editSavedSearch(searchToSave)
.pipe(take(1))
.subscribe({
next: () => {
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
},
error: () => {
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
}
});
}

private updateUserQuery(): void {
const updatedUserQuery = formatSearchTerm(this.searchedWord, this.searchConfig['app:fields']);
this.queryBuilder.userQuery = updatedUserQuery;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h2 class="aca-saved-search-edit-dialog__title">{{"APP.BROWSE.SEARCH.SAVE_SEARCH
{{ '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 }}
{{ form.controls['name'].errors?.message | translate : { name: form.controls.name.value } }}
</span>
</mat-error>
</mat-form-field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('SaveSearchEditDialogComponent', () => {
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
provideMockStore(),
{ provide: SavedSearchesService, useValue: { editSavedSearch: () => of() } },
{ provide: SavedSearchesService, useValue: { editSavedSearch: () => of(), getSavedSearches: () => of([]) } },
{ provide: MAT_DIALOG_DATA, useValue: savedSearchToDelete }
]
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca
import { Store } from '@ngrx/store';
import { CoreModule } from '@alfresco/adf-core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { UniqueSearchNameValidator } from '../unique-search-name-validator';

@Component({
standalone: true,
Expand All @@ -42,7 +43,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
})
export class SavedSearchEditDialogComponent {
form = new FormGroup({
name: new FormControl('', [Validators.required, forbidOnlySpaces]),
name: new FormControl('', {
validators: [Validators.required, forbidOnlySpaces],
asyncValidators: [this.uniqueSearchNameValidator.validate.bind(this.uniqueSearchNameValidator)],
dominikiwanekhyland marked this conversation as resolved.
Show resolved Hide resolved
updateOn: 'blur'
}),
description: new FormControl('')
});

Expand All @@ -52,6 +57,7 @@ export class SavedSearchEditDialogComponent {
private readonly dialog: MatDialogRef<SavedSearchEditDialogComponent>,
private readonly store: Store<AppStore>,
private readonly savedSearchesService: SavedSearchesService,
private readonly uniqueSearchNameValidator: UniqueSearchNameValidator,
@Inject(MAT_DIALOG_DATA) private readonly data: SavedSearch
) {
this.form.patchValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h2 class="aca-save-search-dialog__title">{{"APP.BROWSE.SEARCH.SAVE_SEARCH.MODAL
{{ '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 }}
{{ form.controls['name'].errors?.message | translate : { name: form.controls.name.value } }}
</span>
</mat-error>
</mat-form-field>
Expand Down
Loading
Loading