mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
[ACS-8634] Add new option to edit changes in saved search or save as new (#4229)
* [ACS-8634] Add uniqueness validation for saved searches * [ACS-8634] Saved searches edit or save as new * [ACS-8634] CR fixes * [ACS-8634] Sonar fixes * [ACS-8634] Accept last saved searches when navigated from * [ACS-8634] Display empty list of saved searches * [ACS-8634] Proper url check * [ACS-8634] Add new option to execute saved search * [ACS-8634] CR fixes * [ACS-8634] Sonar fix
This commit is contained in:
parent
71764b09e2
commit
a74d189167
@ -211,12 +211,15 @@
|
|||||||
"RESET_ACTION": "Reset search filters",
|
"RESET_ACTION": "Reset search filters",
|
||||||
"SAVE_SEARCH": {
|
"SAVE_SEARCH": {
|
||||||
"ACTION_BUTTON": "Save Search",
|
"ACTION_BUTTON": "Save Search",
|
||||||
|
"SAVE_CHANGES": "Save changes",
|
||||||
|
"SAVE_AS_NEW": "Save as new",
|
||||||
"MODAL_HEADER": "Save this search",
|
"MODAL_HEADER": "Save this search",
|
||||||
"NAME_LABEL": "Name",
|
"NAME_LABEL": "Name",
|
||||||
"NAME_REQUIRED_ERROR": "This field is required",
|
"NAME_REQUIRED_ERROR": "This field is required",
|
||||||
"DESCRIPTION_LABEL": "Description",
|
"DESCRIPTION_LABEL": "Description",
|
||||||
"SAVE_SUCCESS": "Search Saved",
|
"SAVE_SUCCESS": "Search Saved",
|
||||||
"SAVE_ERROR": "Error occurred. Search could not be saved.",
|
"SAVE_ERROR": "Error occurred. Search could not be saved.",
|
||||||
|
"SEARCH_NAME_NOT_UNIQUE_ERROR": "Saved Search with '{{ name }}' name already exists.",
|
||||||
"NAVBAR": {
|
"NAVBAR": {
|
||||||
"TITLE": "Saved Searches ({{ number }})",
|
"TITLE": "Saved Searches ({{ number }})",
|
||||||
"MANAGE_BUTTON": "Manage searches"
|
"MANAGE_BUTTON": "Manage searches"
|
||||||
@ -240,7 +243,8 @@
|
|||||||
"DESCRIPTION": "Description",
|
"DESCRIPTION": "Description",
|
||||||
"EMPTY_LIST": "No saved searches",
|
"EMPTY_LIST": "No saved searches",
|
||||||
"COPY_TO_CLIPBOARD": "Copy to clipboard",
|
"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",
|
"FOUND_RESULTS": "{{ number }} results found",
|
||||||
|
@ -27,15 +27,45 @@
|
|||||||
<p>{{ 'APP.BROWSE.SEARCH.ADVANCED_FILTERS' | translate }}</p>
|
<p>{{ 'APP.BROWSE.SEARCH.ADVANCED_FILTERS' | translate }}</p>
|
||||||
<div class="aca-content__advanced-filters--header--action-buttons">
|
<div class="aca-content__advanced-filters--header--action-buttons">
|
||||||
<button
|
<button
|
||||||
|
*ngIf="initialSavedSearch !== undefined else saveSearchButton"
|
||||||
mat-button
|
mat-button
|
||||||
acaSaveSearch
|
|
||||||
[acaSaveSearchQuery]="encodedQuery"
|
|
||||||
[disabled]="!encodedQuery"
|
[disabled]="!encodedQuery"
|
||||||
class="aca-content__save-search-action"
|
class="aca-content__save-search-action"
|
||||||
title="{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}"
|
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 }}
|
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.ACTION_BUTTON' | translate }}
|
||||||
|
<mat-icon iconPositionEnd>keyboard_arrow_down</mat-icon>
|
||||||
</button>
|
</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
|
<button
|
||||||
mat-button
|
mat-button
|
||||||
adf-reset-search
|
adf-reset-search
|
||||||
|
@ -26,16 +26,21 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin
|
|||||||
import { SearchResultsComponent } from './search-results.component';
|
import { SearchResultsComponent } from './search-results.component';
|
||||||
import { AppConfigService, NotificationService, TranslationService } from '@alfresco/adf-core';
|
import { AppConfigService, NotificationService, TranslationService } from '@alfresco/adf-core';
|
||||||
import { Store } from '@ngrx/store';
|
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 { 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 { 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 { AppTestingModule } from '../../../testing/app-testing.module';
|
||||||
import { AppService } from '@alfresco/aca-shared';
|
import { AppService } from '@alfresco/aca-shared';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { testHeader } from '../../../testing/document-base-page-utils';
|
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', () => {
|
describe('SearchComponent', () => {
|
||||||
let component: SearchResultsComponent;
|
let component: SearchResultsComponent;
|
||||||
@ -49,6 +54,10 @@ describe('SearchComponent', () => {
|
|||||||
const searchRequest = {} as SearchRequest;
|
const searchRequest = {} as SearchRequest;
|
||||||
let params: BehaviorSubject<any>;
|
let params: BehaviorSubject<any>;
|
||||||
let showErrorSpy: jasmine.Spy;
|
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 => {
|
const encodeQuery = (query: any): string => {
|
||||||
return Buffer.from(JSON.stringify(query)).toString('base64');
|
return Buffer.from(JSON.stringify(query)).toString('base64');
|
||||||
@ -57,7 +66,7 @@ describe('SearchComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' });
|
params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' });
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [AppTestingModule, SearchResultsComponent, MatSnackBarModule],
|
imports: [AppTestingModule, SearchResultsComponent, MatSnackBarModule, MatMenuModule, NoopAnimationsModule],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: AppService,
|
provide: AppService,
|
||||||
@ -67,6 +76,15 @@ describe('SearchComponent', () => {
|
|||||||
setAppNavbarMode: jasmine.createSpy('setAppNavbarMode')
|
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,
|
provide: ActivatedRoute,
|
||||||
useValue: {
|
useValue: {
|
||||||
@ -102,6 +120,7 @@ describe('SearchComponent', () => {
|
|||||||
spyOn(queryBuilder, 'update').and.stub();
|
spyOn(queryBuilder, 'update').and.stub();
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -225,5 +244,66 @@ describe('SearchComponent', () => {
|
|||||||
expect(queryBuilder.userQuery).toBe(`((cm:tag:"orange*"))`);
|
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);
|
testHeader(SearchResultsComponent, false);
|
||||||
});
|
});
|
||||||
|
@ -22,13 +22,15 @@
|
|||||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
* 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 { NodeEntry, Pagination, ResultSetPaging } from '@alfresco/js-api';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
AlfrescoViewerComponent,
|
AlfrescoViewerComponent,
|
||||||
DocumentListComponent,
|
DocumentListComponent,
|
||||||
ResetSearchDirective,
|
ResetSearchDirective,
|
||||||
|
SavedSearch,
|
||||||
|
SavedSearchesService,
|
||||||
SearchConfiguration,
|
SearchConfiguration,
|
||||||
SearchFilterChipsComponent,
|
SearchFilterChipsComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
@ -41,7 +43,9 @@ import {
|
|||||||
SetInfoDrawerPreviewStateAction,
|
SetInfoDrawerPreviewStateAction,
|
||||||
SetInfoDrawerStateAction,
|
SetInfoDrawerStateAction,
|
||||||
SetSearchItemsTotalCountAction,
|
SetSearchItemsTotalCountAction,
|
||||||
ShowInfoDrawerPreviewAction
|
ShowInfoDrawerPreviewAction,
|
||||||
|
SnackbarErrorAction,
|
||||||
|
SnackbarInfoAction
|
||||||
} from '@alfresco/aca-shared/store';
|
} from '@alfresco/aca-shared/store';
|
||||||
import {
|
import {
|
||||||
CustomEmptyContentTemplateDirective,
|
CustomEmptyContentTemplateDirective,
|
||||||
@ -62,7 +66,7 @@ import {
|
|||||||
ToolbarComponent
|
ToolbarComponent
|
||||||
} from '@alfresco/aca-shared';
|
} from '@alfresco/aca-shared';
|
||||||
import { SearchSortingDefinition } from '@alfresco/adf-content-services/lib/search/models/search-sorting-definition.interface';
|
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 { CommonModule } from '@angular/common';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { SearchInputComponent } from '../search-input/search-input.component';
|
import { SearchInputComponent } from '../search-input/search-input.component';
|
||||||
@ -86,6 +90,8 @@ import {
|
|||||||
} from '../../../utils/aca-search-utils';
|
} from '../../../utils/aca-search-utils';
|
||||||
import { SaveSearchDirective } from '../search-save/directive/save-search.directive';
|
import { SaveSearchDirective } from '../search-save/directive/save-search.directive';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -96,6 +102,7 @@ import { Subject } from 'rxjs';
|
|||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatMenuModule,
|
||||||
DocumentListDirective,
|
DocumentListDirective,
|
||||||
ContextActionsDirective,
|
ContextActionsDirective,
|
||||||
ThumbnailColumnComponent,
|
ThumbnailColumnComponent,
|
||||||
@ -140,18 +147,21 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
totalResults: number;
|
totalResults: number;
|
||||||
isTagsEnabled = false;
|
isTagsEnabled = false;
|
||||||
|
initialSavedSearch: SavedSearch = undefined;
|
||||||
columns: DocumentListPresetRef[] = [];
|
columns: DocumentListPresetRef[] = [];
|
||||||
encodedQuery: string;
|
encodedQuery: string;
|
||||||
searchConfig: SearchConfiguration;
|
searchConfig: SearchConfiguration;
|
||||||
|
|
||||||
private readonly loadedFilters$ = new Subject<void>();
|
private readonly loadedFilters$ = new Subject<void>();
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
tagsService: TagService,
|
tagsService: TagService,
|
||||||
private readonly queryBuilder: SearchQueryBuilderService,
|
private readonly queryBuilder: SearchQueryBuilderService,
|
||||||
private readonly changeDetectorRef: ChangeDetectorRef,
|
private readonly changeDetectorRef: ChangeDetectorRef,
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly translationService: TranslationService
|
private readonly translationService: TranslationService,
|
||||||
|
private readonly savedSearchesService: SavedSearchesService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -164,7 +174,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
|||||||
|
|
||||||
this.queryBuilder.configUpdated
|
this.queryBuilder.configUpdated
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.pipe(takeUntil(this.onDestroy$))
|
.pipe(takeUntilDestroyed())
|
||||||
.subscribe((searchConfig) => {
|
.subscribe((searchConfig) => {
|
||||||
this.searchConfig = searchConfig;
|
this.searchConfig = searchConfig;
|
||||||
this.updateUserQuery();
|
this.updateUserQuery();
|
||||||
@ -202,7 +212,14 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
|||||||
this.columns = this.extensions.documentListPresets.searchResults || [];
|
this.columns = this.extensions.documentListPresets.searchResults || [];
|
||||||
|
|
||||||
if (this.route) {
|
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]) {
|
if (params[this.queryParamName]) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
}
|
}
|
||||||
@ -216,7 +233,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
|||||||
let loadedFilters = this.searchedWord === '' ? 0 : 1;
|
let loadedFilters = this.searchedWord === '' ? 0 : 1;
|
||||||
this.queryBuilder.filterLoaded
|
this.queryBuilder.filterLoaded
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.pipe(takeUntil(this.onDestroy$), takeUntil(this.loadedFilters$))
|
.pipe(takeUntilDestroyed(this.destroyRef), takeUntil(this.loadedFilters$))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
loadedFilters++;
|
loadedFilters++;
|
||||||
if (filtersToLoad === loadedFilters) {
|
if (filtersToLoad === loadedFilters) {
|
||||||
@ -307,6 +324,21 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
|||||||
this.queryBuilder.update();
|
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 {
|
private updateUserQuery(): void {
|
||||||
const updatedUserQuery = formatSearchTerm(this.searchedWord, this.searchConfig['app:fields']);
|
const updatedUserQuery = formatSearchTerm(this.searchedWord, this.searchConfig['app:fields']);
|
||||||
this.queryBuilder.userQuery = updatedUserQuery;
|
this.queryBuilder.userQuery = updatedUserQuery;
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_REQUIRED_ERROR' | translate }}
|
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_REQUIRED_ERROR' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="!form.controls['name'].errors?.required && form.controls['name'].errors?.message">
|
<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>
|
</span>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@ -55,7 +55,7 @@ describe('SaveSearchEditDialogComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: MatDialogRef, useValue: dialogRef },
|
{ provide: MatDialogRef, useValue: dialogRef },
|
||||||
provideMockStore(),
|
provideMockStore(),
|
||||||
{ provide: SavedSearchesService, useValue: { editSavedSearch: () => of() } },
|
{ provide: SavedSearchesService, useValue: { editSavedSearch: () => of(), getSavedSearches: () => of([]) } },
|
||||||
{ provide: MAT_DIALOG_DATA, useValue: savedSearchToDelete }
|
{ provide: MAT_DIALOG_DATA, useValue: savedSearchToDelete }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -30,6 +30,7 @@ import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CoreModule } from '@alfresco/adf-core';
|
import { CoreModule } from '@alfresco/adf-core';
|
||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { UniqueSearchNameValidator } from '../unique-search-name-validator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -42,7 +43,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|||||||
})
|
})
|
||||||
export class SavedSearchEditDialogComponent {
|
export class SavedSearchEditDialogComponent {
|
||||||
form = new FormGroup({
|
form = new FormGroup({
|
||||||
name: new FormControl('', [Validators.required, forbidOnlySpaces]),
|
name: new FormControl('', {
|
||||||
|
validators: [Validators.required, forbidOnlySpaces],
|
||||||
|
asyncValidators: [this.uniqueSearchNameValidator.validate.bind(this.uniqueSearchNameValidator)],
|
||||||
|
updateOn: 'blur'
|
||||||
|
}),
|
||||||
description: new FormControl('')
|
description: new FormControl('')
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,6 +57,7 @@ export class SavedSearchEditDialogComponent {
|
|||||||
private readonly dialog: MatDialogRef<SavedSearchEditDialogComponent>,
|
private readonly dialog: MatDialogRef<SavedSearchEditDialogComponent>,
|
||||||
private readonly store: Store<AppStore>,
|
private readonly store: Store<AppStore>,
|
||||||
private readonly savedSearchesService: SavedSearchesService,
|
private readonly savedSearchesService: SavedSearchesService,
|
||||||
|
private readonly uniqueSearchNameValidator: UniqueSearchNameValidator,
|
||||||
@Inject(MAT_DIALOG_DATA) private readonly data: SavedSearch
|
@Inject(MAT_DIALOG_DATA) private readonly data: SavedSearch
|
||||||
) {
|
) {
|
||||||
this.form.patchValue({
|
this.form.patchValue({
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_REQUIRED_ERROR' | translate }}
|
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_REQUIRED_ERROR' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="!form.controls['name'].errors?.required && form.controls['name'].errors?.message">
|
<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>
|
</span>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@ -49,7 +49,7 @@ describe('SaveSearchDialogComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: MatDialogRef, useValue: dialogRef },
|
{ provide: MatDialogRef, useValue: dialogRef },
|
||||||
provideMockStore(),
|
provideMockStore(),
|
||||||
{ provide: SavedSearchesService, useValue: { saveSearch: () => of() } },
|
{ provide: SavedSearchesService, useValue: { saveSearch: () => of(), getSavedSearches: () => of([]) } },
|
||||||
{ provide: MAT_DIALOG_DATA, useValue: { searchUrl: 'abcdef' } }
|
{ provide: MAT_DIALOG_DATA, useValue: { searchUrl: 'abcdef' } }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -40,6 +40,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||||
|
import { UniqueSearchNameValidator } from './unique-search-name-validator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -66,7 +67,11 @@ import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca
|
|||||||
})
|
})
|
||||||
export class SaveSearchDialogComponent {
|
export class SaveSearchDialogComponent {
|
||||||
form = new FormGroup({
|
form = new FormGroup({
|
||||||
name: new FormControl('', [Validators.required, forbidOnlySpaces]),
|
name: new FormControl('', {
|
||||||
|
validators: [Validators.required, forbidOnlySpaces],
|
||||||
|
asyncValidators: [this.uniqueSearchNameValidator.validate.bind(this.uniqueSearchNameValidator)],
|
||||||
|
updateOn: 'blur'
|
||||||
|
}),
|
||||||
description: new FormControl('')
|
description: new FormControl('')
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,6 +81,7 @@ export class SaveSearchDialogComponent {
|
|||||||
private readonly dialog: MatDialogRef<SaveSearchDialogComponent>,
|
private readonly dialog: MatDialogRef<SaveSearchDialogComponent>,
|
||||||
private readonly store: Store<AppStore>,
|
private readonly store: Store<AppStore>,
|
||||||
private readonly savedSearchesService: SavedSearchesService,
|
private readonly savedSearchesService: SavedSearchesService,
|
||||||
|
private readonly uniqueSearchNameValidator: UniqueSearchNameValidator,
|
||||||
@Inject(MAT_DIALOG_DATA) private readonly data: { searchUrl: string }
|
@Inject(MAT_DIALOG_DATA) private readonly data: { searchUrl: string }
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { TestBed } from '@angular/core/testing';
|
||||||
|
import { UniqueSearchNameValidator } from './unique-search-name-validator';
|
||||||
|
import { ContentTestingModule, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
|
describe('UniqueSearchNameValidator', () => {
|
||||||
|
let validator: UniqueSearchNameValidator;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ContentTestingModule]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Save searches returns results', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.overrideProvider(SavedSearchesService, { useValue: { getSavedSearches: () => of([{ name: 'test' }]) } });
|
||||||
|
validator = TestBed.inject(UniqueSearchNameValidator);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when name is unique', (done) => {
|
||||||
|
validator.validate(new FormControl({ value: 'unique', disabled: false })).subscribe((result) => {
|
||||||
|
expect(result).toBe(null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when name is not unique', (done) => {
|
||||||
|
validator.validate(new FormControl({ value: 'test', disabled: false })).subscribe((result) => {
|
||||||
|
expect(result).toEqual({ message: 'APP.BROWSE.SEARCH.SAVE_SEARCH.SEARCH_NAME_NOT_UNIQUE_ERROR' });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Save searches returns error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.overrideProvider(SavedSearchesService, { useValue: { getSavedSearches: () => of(null) } });
|
||||||
|
validator = TestBed.inject(UniqueSearchNameValidator);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when error occurs', (done) => {
|
||||||
|
validator.validate(new FormControl({ value: 'test', disabled: false })).subscribe((result) => {
|
||||||
|
expect(result).toBe(null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,42 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { SavedSearchesService } from '@alfresco/adf-content-services';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
|
||||||
|
import { catchError, map, Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class UniqueSearchNameValidator implements AsyncValidator {
|
||||||
|
constructor(private readonly savedSearchesService: SavedSearchesService) {}
|
||||||
|
|
||||||
|
validate(control: AbstractControl): Observable<ValidationErrors | null> {
|
||||||
|
return this.savedSearchesService.getSavedSearches().pipe(
|
||||||
|
map((searches) =>
|
||||||
|
searches.some((search) => search.name === control.value) ? { message: 'APP.BROWSE.SEARCH.SAVE_SEARCH.SEARCH_NAME_NOT_UNIQUE_ERROR' } : null
|
||||||
|
),
|
||||||
|
catchError(() => of(null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
.aca-saved-searches-ui-list {
|
.aca-saved-searches-ui-list {
|
||||||
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.adf-datatable-list {
|
.adf-datatable-list {
|
||||||
|
@ -31,12 +31,14 @@ import { SavedSearch } from '@alfresco/adf-content-services';
|
|||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { Clipboard } from '@angular/cdk/clipboard';
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
describe('SavedSearchesListUiComponent ', () => {
|
describe('SavedSearchesListUiComponent ', () => {
|
||||||
let fixture: ComponentFixture<SavedSearchesListUiComponent>;
|
let fixture: ComponentFixture<SavedSearchesListUiComponent>;
|
||||||
let component: SavedSearchesListUiComponent;
|
let component: SavedSearchesListUiComponent;
|
||||||
let savedSearchesListUiService: SavedSearchesListUiService;
|
let savedSearchesListUiService: SavedSearchesListUiService;
|
||||||
let clipboard: Clipboard;
|
let clipboard: Clipboard;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -46,6 +48,7 @@ describe('SavedSearchesListUiComponent ', () => {
|
|||||||
|
|
||||||
savedSearchesListUiService = TestBed.inject(SavedSearchesListUiService);
|
savedSearchesListUiService = TestBed.inject(SavedSearchesListUiService);
|
||||||
clipboard = TestBed.inject(Clipboard);
|
clipboard = TestBed.inject(Clipboard);
|
||||||
|
router = TestBed.inject(Router);
|
||||||
|
|
||||||
fixture = TestBed.createComponent(SavedSearchesListUiComponent);
|
fixture = TestBed.createComponent(SavedSearchesListUiComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
@ -76,6 +79,8 @@ describe('SavedSearchesListUiComponent ', () => {
|
|||||||
hasValue: () => true,
|
hasValue: () => true,
|
||||||
getValue: () => 'Some value',
|
getValue: () => 'Some value',
|
||||||
obj: {
|
obj: {
|
||||||
|
name: 'test',
|
||||||
|
encodedUrl: 'test',
|
||||||
field: 'some value',
|
field: 'some value',
|
||||||
id: 'some id'
|
id: 'some id'
|
||||||
}
|
}
|
||||||
@ -120,6 +125,16 @@ describe('SavedSearchesListUiComponent ', () => {
|
|||||||
},
|
},
|
||||||
data: dataCellEvent.value.row.obj
|
data: dataCellEvent.value.row.obj
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.EXECUTE_SEARCH',
|
||||||
|
key: 'execute',
|
||||||
|
subject: jasmine.any(Subject),
|
||||||
|
model: {
|
||||||
|
visible: true,
|
||||||
|
icon: 'exit_to_app'
|
||||||
|
},
|
||||||
|
data: dataCellEvent.value.row.obj
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.CONTEXT_OPTION',
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.CONTEXT_OPTION',
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
@ -155,7 +170,7 @@ describe('SavedSearchesListUiComponent ', () => {
|
|||||||
let editAction: any;
|
let editAction: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editAction = dataCellEvent.value.actions[1];
|
editAction = dataCellEvent.value.actions[2];
|
||||||
editAction.subject.next(editAction);
|
editAction.subject.next(editAction);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -168,7 +183,7 @@ describe('SavedSearchesListUiComponent ', () => {
|
|||||||
let deleteAction: any;
|
let deleteAction: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
deleteAction = dataCellEvent.value.actions[2];
|
deleteAction = dataCellEvent.value.actions[3];
|
||||||
deleteAction.subject.next(deleteAction);
|
deleteAction.subject.next(deleteAction);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -189,6 +204,20 @@ describe('SavedSearchesListUiComponent ', () => {
|
|||||||
expect(clipboard.copy).toHaveBeenCalled();
|
expect(clipboard.copy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Execute search', () => {
|
||||||
|
let actionData: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(router, 'navigate').and.stub();
|
||||||
|
actionData = dataCellEvent.value.actions[1];
|
||||||
|
actionData.subject.next(actionData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to search when selected execute search option', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/search'], { queryParams: { q: 'test' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -41,6 +41,7 @@ import { SavedSearch } from '@alfresco/adf-content-services';
|
|||||||
import { Clipboard } from '@angular/cdk/clipboard';
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
import { SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aca-saved-searches-ui-list',
|
selector: 'aca-saved-searches-ui-list',
|
||||||
@ -66,12 +67,18 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
|
|||||||
private readonly editSavedSearchOptionKey = 'edit';
|
private readonly editSavedSearchOptionKey = 'edit';
|
||||||
private readonly deleteSavedSearchOptionKey = 'delete';
|
private readonly deleteSavedSearchOptionKey = 'delete';
|
||||||
private readonly copyToClipboardUrlOptionKey = 'copy';
|
private readonly copyToClipboardUrlOptionKey = 'copy';
|
||||||
|
private readonly executeSearchOptionKey = 'execute';
|
||||||
private readonly menuOptions = [
|
private readonly menuOptions = [
|
||||||
{
|
{
|
||||||
icon: 'copy',
|
icon: 'copy',
|
||||||
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD',
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD',
|
||||||
key: this.copyToClipboardUrlOptionKey
|
key: this.copyToClipboardUrlOptionKey
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'exit_to_app',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.EXECUTE_SEARCH',
|
||||||
|
key: this.executeSearchOptionKey
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'edit',
|
icon: 'edit',
|
||||||
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.CONTEXT_OPTION',
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.CONTEXT_OPTION',
|
||||||
@ -84,7 +91,12 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(protected appConfig: AppConfigService, private readonly clipboard: Clipboard, private readonly store: Store) {
|
constructor(
|
||||||
|
protected appConfig: AppConfigService,
|
||||||
|
private readonly clipboard: Clipboard,
|
||||||
|
private readonly store: Store,
|
||||||
|
private readonly router: Router
|
||||||
|
) {
|
||||||
super(appConfig, '', savedSearchesListSchema);
|
super(appConfig, '', savedSearchesListSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +124,9 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
|
|||||||
case this.copyToClipboardUrlOptionKey:
|
case this.copyToClipboardUrlOptionKey:
|
||||||
this.copyToClipboard(savedSearchData);
|
this.copyToClipboard(savedSearchData);
|
||||||
break;
|
break;
|
||||||
|
case this.executeSearchOptionKey:
|
||||||
|
this.executeSearch(savedSearchData);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +143,12 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
|
|||||||
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD_SUCCESS'));
|
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD_SUCCESS'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeSearch(savedSearch: SavedSearch): void {
|
||||||
|
this.router.navigate(['/search'], {
|
||||||
|
queryParams: { q: decodeURIComponent(savedSearch.encodedUrl) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fillContextMenu(event: DataCellEvent) {
|
fillContextMenu(event: DataCellEvent) {
|
||||||
event.value.actions = this.menuOptions.map((option) => ({
|
event.value.actions = this.menuOptions.map((option) => ({
|
||||||
title: option.title,
|
title: option.title,
|
||||||
|
@ -1 +1 @@
|
|||||||
<app-expand-menu *ngIf="item" [item]="item" />
|
<app-expand-menu *ngIf="item" [item]="item" (actionClicked)="onActionClick()" />
|
||||||
|
@ -62,7 +62,15 @@ describe('SaveSearchSidenavComponent', () => {
|
|||||||
expect(component.item).toEqual({
|
expect(component.item).toEqual({
|
||||||
icon: '',
|
icon: '',
|
||||||
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.TITLE',
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.TITLE',
|
||||||
children: [],
|
children: [
|
||||||
|
{
|
||||||
|
id: 'manage-saved-searches',
|
||||||
|
icon: '',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.MANAGE_BUTTON',
|
||||||
|
route: 'saved-searches',
|
||||||
|
url: 'saved-searches'
|
||||||
|
}
|
||||||
|
],
|
||||||
route: '/',
|
route: '/',
|
||||||
id: 'search-navbar'
|
id: 'search-navbar'
|
||||||
});
|
});
|
||||||
|
@ -63,6 +63,10 @@ export class SaveSearchSidenavComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onActionClick(): void {
|
||||||
|
this.appService.appNavNarMode$.next('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
private createNavBarLinkRef(children: SavedSearch[]): NavBarLinkRef {
|
private createNavBarLinkRef(children: SavedSearch[]): NavBarLinkRef {
|
||||||
const mappedChildren = children
|
const mappedChildren = children
|
||||||
.map((child) => ({
|
.map((child) => ({
|
||||||
@ -74,15 +78,13 @@ export class SaveSearchSidenavComponent implements OnInit, OnDestroy {
|
|||||||
}))
|
}))
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
const title = this.translationService.instant('APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.TITLE', { number: children.length });
|
const title = this.translationService.instant('APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.TITLE', { number: children.length });
|
||||||
if (children.length) {
|
mappedChildren.push({
|
||||||
mappedChildren.push({
|
id: this.manageSearchesId,
|
||||||
id: this.manageSearchesId,
|
icon: '',
|
||||||
icon: '',
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.MANAGE_BUTTON',
|
||||||
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.MANAGE_BUTTON',
|
route: 'saved-searches',
|
||||||
route: 'saved-searches',
|
url: 'saved-searches'
|
||||||
url: 'saved-searches'
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
icon: '',
|
icon: '',
|
||||||
title,
|
title,
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
<button
|
<button
|
||||||
acaActiveLink="aca-action-button--active"
|
acaActiveLink="aca-action-button--active"
|
||||||
[action]="child"
|
[action]="child"
|
||||||
|
(actionClicked)="actionClicked.emit()"
|
||||||
[attr.aria-label]="child.title | translate"
|
[attr.aria-label]="child.title | translate"
|
||||||
[id]="child.id"
|
[id]="child.id"
|
||||||
[attr.data-automation-id]="child.id"
|
[attr.data-automation-id]="child.id"
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
|
||||||
import { NavBarLinkRef } from '@alfresco/adf-extensions';
|
import { NavBarLinkRef } from '@alfresco/adf-extensions';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@ -54,6 +54,9 @@ export class ExpandMenuComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
item: NavBarLinkRef;
|
item: NavBarLinkRef;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
actionClicked = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(private cd: ChangeDetectorRef) {}
|
constructor(private cd: ChangeDetectorRef) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Directive, HostListener, Input } from '@angular/core';
|
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
|
||||||
import { Params, PRIMARY_OUTLET, Router } from '@angular/router';
|
import { Params, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppStore } from '@alfresco/aca-shared/store';
|
import { AppStore } from '@alfresco/aca-shared/store';
|
||||||
@ -36,6 +36,8 @@ import { AppStore } from '@alfresco/aca-shared/store';
|
|||||||
export class ActionDirective {
|
export class ActionDirective {
|
||||||
@Input() action;
|
@Input() action;
|
||||||
|
|
||||||
|
@Output() actionClicked = new EventEmitter<void>();
|
||||||
|
|
||||||
@HostListener('click')
|
@HostListener('click')
|
||||||
onClick() {
|
onClick() {
|
||||||
if (this.action.url) {
|
if (this.action.url) {
|
||||||
@ -46,6 +48,7 @@ export class ActionDirective {
|
|||||||
payload: this.getNavigationCommands(this.action.click.payload)
|
payload: this.getNavigationCommands(this.action.click.payload)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.actionClicked.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private router: Router, private store: Store<AppStore>) {}
|
constructor(private router: Router, private store: Store<AppStore>) {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user