mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
[ACS-8634] "Manage Searches" - a full page list of saved searches (#4181)
* [ACS-8751] Adapt search results to handle query encoding and state propagation * Changes after CR, bug fixed * Changes after CR, bug fixed * [ACS-8634] "Manage Searches" - a full page list of saved searches * Changes after Code Review * [ACS-8634] Update package-lock * [ACS-8634] Minor fixes * [ACS-8634] Use latest ADF, fix unit tests * [ACS-8634] Fix package lock * [ACS-8634] Minor fixes * [ACS-8634] CR fixes * [ACS-8634] Sidenav state fixes * [ACS-8634] Final fix for sidenav state * [ACS-8634] CR fixes * [ACS-8634] CR fix --------- Co-authored-by: MichalKinas <michal.kinas@hyland.com>
This commit is contained in:
parent
cacc4149fa
commit
c2d2f95095
5392
package-lock.json
generated
5392
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -30,11 +30,11 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alfresco/adf-content-services": "7.0.0-alpha.4",
|
"@alfresco/adf-content-services": "7.0.0-alpha.5-11519897356",
|
||||||
"@alfresco/adf-core": "7.0.0-alpha.4",
|
"@alfresco/adf-core": "7.0.0-alpha.5-11519897356",
|
||||||
"@alfresco/adf-extensions": "7.0.0-alpha.4",
|
"@alfresco/adf-extensions": "7.0.0-alpha.5-11519897356",
|
||||||
"@alfresco/eslint-plugin-eslint-angular": "7.0.0-alpha.4",
|
"@alfresco/eslint-plugin-eslint-angular": "7.0.0-alpha.5-11519897356",
|
||||||
"@alfresco/js-api": "8.0.0-alpha.4",
|
"@alfresco/js-api": "8.0.0-alpha.5-11519897356",
|
||||||
"@angular/animations": "16.2.9",
|
"@angular/animations": "16.2.9",
|
||||||
"@angular/cdk": "16.2.9",
|
"@angular/cdk": "16.2.9",
|
||||||
"@angular/common": "16.2.9",
|
"@angular/common": "16.2.9",
|
||||||
@ -62,8 +62,8 @@
|
|||||||
"zone.js": "0.13.3"
|
"zone.js": "0.13.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alfresco/adf-cli": "7.0.0-alpha.4",
|
"@alfresco/adf-cli": "7.0.0-alpha.5-11519897356",
|
||||||
"@angular-devkit/build-angular": "16.2.16",
|
"@angular-devkit/build-angular": "16.2.9",
|
||||||
"@angular-devkit/core": "16.2.9",
|
"@angular-devkit/core": "16.2.9",
|
||||||
"@angular-devkit/schematics": "16.2.9",
|
"@angular-devkit/schematics": "16.2.9",
|
||||||
"@angular-eslint/builder": "17.0.0",
|
"@angular-eslint/builder": "17.0.0",
|
||||||
|
@ -216,9 +216,31 @@
|
|||||||
"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 occured. Search could not be saved.",
|
"SAVE_ERROR": "Error occurred. Search could not be saved.",
|
||||||
"NAVBAR": {
|
"NAVBAR": {
|
||||||
"TITLE": "Saved Searches ({{ number }})"
|
"TITLE": "Saved Searches ({{ number }})",
|
||||||
|
"MANAGE_BUTTON": "Manage searches"
|
||||||
|
},
|
||||||
|
"EDIT_DIALOG": {
|
||||||
|
"CONTEXT_OPTION": "Edit Search",
|
||||||
|
"TITLE": "Edit Saved Search",
|
||||||
|
"SUCCESS_MESSAGE": "Saved Search edited successfully",
|
||||||
|
"ERROR_MESSAGE": "Error occurred. Saved Search could not be edited."
|
||||||
|
},
|
||||||
|
"DELETE_DIALOG": {
|
||||||
|
"CONTEXT_OPTION": "Delete Search",
|
||||||
|
"TITLE": "Delete Saved Search",
|
||||||
|
"CONTENT": "Are you sure you want to delete this search",
|
||||||
|
"SUCCESS_MESSAGE": "Saved Search deleted successfully",
|
||||||
|
"ERROR_MESSAGE": "Error occurred. Saved Search could not be deleted."
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"TITLE": "Saved Searches",
|
||||||
|
"NAME": "Name",
|
||||||
|
"DESCRIPTION": "Description",
|
||||||
|
"EMPTY_LIST": "No saved searches",
|
||||||
|
"COPY_TO_CLIPBOARD": "Copy to clipboard",
|
||||||
|
"COPY_TO_CLIPBOARD_SUCCESS": "Search copied to clipboard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FOUND_RESULTS": "{{ number }} results found",
|
"FOUND_RESULTS": "{{ number }} results found",
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": ">=15.2",
|
"@angular/common": ">=15.2",
|
||||||
"@angular/core": ">=15.2",
|
"@angular/core": ">=15.2",
|
||||||
"@alfresco/adf-core": ">=7.0.0-alpha.4",
|
"@alfresco/adf-core": ">=7.0.0-alpha.5-0",
|
||||||
"@alfresco/adf-content-services": ">=7.0.0-alpha.4",
|
"@alfresco/adf-content-services": ">=7.0.0-alpha.5-0",
|
||||||
"@alfresco/adf-extensions": ">=7.0.0-alpha.4",
|
"@alfresco/adf-extensions": ">=7.0.0-alpha.5-0",
|
||||||
"@alfresco/js-api": ">=8.0.0-alpha.4",
|
"@alfresco/js-api": ">=8.0.0-alpha.5-0",
|
||||||
"@angular/animations": ">=15.2",
|
"@angular/animations": ">=15.2",
|
||||||
"@angular/cdk": ">=15.2",
|
"@angular/cdk": ">=15.2",
|
||||||
"@angular/forms": ">=15.2",
|
"@angular/forms": ">=15.2",
|
||||||
|
@ -27,7 +27,7 @@ import { LibrariesComponent } from './components/libraries/libraries.component';
|
|||||||
import { FavoriteLibrariesComponent } from './components/favorite-libraries/favorite-libraries.component';
|
import { FavoriteLibrariesComponent } from './components/favorite-libraries/favorite-libraries.component';
|
||||||
import { SearchResultsComponent } from './components/search/search-results/search-results.component';
|
import { SearchResultsComponent } from './components/search/search-results/search-results.component';
|
||||||
import { SearchLibrariesResultsComponent } from './components/search/search-libraries-results/search-libraries-results.component';
|
import { SearchLibrariesResultsComponent } from './components/search/search-libraries-results/search-libraries-results.component';
|
||||||
import { AppSharedRuleGuard, GenericErrorComponent, ExtensionRoute, ExtensionsDataLoaderGuard, PluginEnabledGuard } from '@alfresco/aca-shared';
|
import { AppSharedRuleGuard, ExtensionRoute, ExtensionsDataLoaderGuard, GenericErrorComponent, PluginEnabledGuard } from '@alfresco/aca-shared';
|
||||||
import { AuthGuard, UnsavedChangesGuard } from '@alfresco/adf-core';
|
import { AuthGuard, UnsavedChangesGuard } from '@alfresco/adf-core';
|
||||||
import { FavoritesComponent } from './components/favorites/favorites.component';
|
import { FavoritesComponent } from './components/favorites/favorites.component';
|
||||||
import { RecentFilesComponent } from './components/recent-files/recent-files.component';
|
import { RecentFilesComponent } from './components/recent-files/recent-files.component';
|
||||||
@ -41,6 +41,7 @@ import { SharedLinkViewComponent } from './components/shared-link-view/shared-li
|
|||||||
import { TrashcanComponent } from './components/trashcan/trashcan.component';
|
import { TrashcanComponent } from './components/trashcan/trashcan.component';
|
||||||
import { ShellLayoutComponent } from '@alfresco/adf-core/shell';
|
import { ShellLayoutComponent } from '@alfresco/adf-core/shell';
|
||||||
import { SearchAiResultsComponent } from './components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component';
|
import { SearchAiResultsComponent } from './components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component';
|
||||||
|
import { SavedSearchesSmartListComponent } from './components/search/search-save/list/smart-list/saved-searches-smart-list.component';
|
||||||
|
|
||||||
export const CONTENT_ROUTES: ExtensionRoute[] = [
|
export const CONTENT_ROUTES: ExtensionRoute[] = [
|
||||||
{
|
{
|
||||||
@ -354,6 +355,15 @@ export const CONTENT_LAYOUT_ROUTES: Route = {
|
|||||||
...createViewRoutes('knowledge-retrieval')
|
...createViewRoutes('knowledge-retrieval')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'saved-searches',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SavedSearchesSmartListComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
component: GenericErrorComponent
|
component: GenericErrorComponent
|
||||||
|
@ -179,9 +179,9 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
|||||||
|
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.queryBuilder.updated.subscribe((query) => {
|
this.queryBuilder.updated.subscribe((query) => {
|
||||||
|
this.isLoading = true;
|
||||||
if (query) {
|
if (query) {
|
||||||
this.sorting = this.getSorting();
|
this.sorting = this.getSorting();
|
||||||
this.isLoading = true;
|
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -203,6 +203,9 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
|
|||||||
|
|
||||||
if (this.route) {
|
if (this.route) {
|
||||||
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
|
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
|
||||||
|
if (params[this.queryParamName]) {
|
||||||
|
this.isLoading = true;
|
||||||
|
}
|
||||||
this.loadedFilters$.next();
|
this.loadedFilters$.next();
|
||||||
this.encodedQuery = params[this.queryParamName] || null;
|
this.encodedQuery = params[this.queryParamName] || null;
|
||||||
this.searchedWord = extractSearchedWordFromEncodedQuery(this.encodedQuery);
|
this.searchedWord = extractSearchedWordFromEncodedQuery(this.encodedQuery);
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<div class="aca-saved-search-delete-dialog__header">
|
||||||
|
<h2 class="aca-saved-search-delete-dialog__title">{{"APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.TITLE" | translate}}</h2>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
mat-dialog-close
|
||||||
|
[attr.aria-label]="'CLOSE' | translate"
|
||||||
|
[attr.title]="'CLOSE' | translate">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<mat-dialog-content>
|
||||||
|
<p>{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.CONTENT' | translate }}</p>
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button mat-button
|
||||||
|
mat-dialog-close
|
||||||
|
id="aca-save-search-delete-dialog-cancel-button">{{ 'CANCEL' | titlecase | translate }}</button>
|
||||||
|
<button mat-flat-button
|
||||||
|
id="aca-save-search-delete-dialog-submit-button"
|
||||||
|
color="primary"
|
||||||
|
[disabled]="isLoading"
|
||||||
|
(click)="onSubmit()">{{ 'DELETE' | titlecase | translate }}</button>
|
||||||
|
</mat-dialog-actions>
|
@ -0,0 +1,14 @@
|
|||||||
|
.aca-saved-search-delete-dialog {
|
||||||
|
.aca-saved-search-delete-dialog__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aca-saved-search-delete-dialog__title {
|
||||||
|
font-size: large;
|
||||||
|
font-weight: 200;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -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 { SavedSearchDeleteDialogComponent } from './saved-search-delete-dialog.component';
|
||||||
|
import { ContentTestingModule, SavedSearch, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||||
|
import { AppTestingModule } from '../../../../../testing/app-testing.module';
|
||||||
|
|
||||||
|
describe('SaveSearchDeleteDialogComponent', () => {
|
||||||
|
let fixture: ComponentFixture<SavedSearchDeleteDialogComponent>;
|
||||||
|
let savedSearchesService: SavedSearchesService;
|
||||||
|
let store: Store;
|
||||||
|
let submitButton: HTMLButtonElement;
|
||||||
|
let cancelButton: HTMLButtonElement;
|
||||||
|
|
||||||
|
const savedSearchToDelete: SavedSearch = {
|
||||||
|
name: 'test',
|
||||||
|
encodedUrl: '1234',
|
||||||
|
order: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogRef = {
|
||||||
|
close: jasmine.createSpy('close')
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ContentTestingModule, AppTestingModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: MatDialogRef, useValue: dialogRef },
|
||||||
|
provideMockStore(),
|
||||||
|
{ provide: SavedSearchesService, useValue: { deleteSavedSearch: () => of() } },
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: savedSearchToDelete }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
dialogRef.close.calls.reset();
|
||||||
|
fixture = TestBed.createComponent(SavedSearchDeleteDialogComponent);
|
||||||
|
savedSearchesService = TestBed.inject(SavedSearchesService);
|
||||||
|
store = TestBed.inject(Store);
|
||||||
|
|
||||||
|
submitButton = fixture.nativeElement.querySelector('#aca-save-search-delete-dialog-submit-button');
|
||||||
|
cancelButton = fixture.nativeElement.querySelector('#aca-save-search-delete-dialog-cancel-button');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not delete and close dialog window if cancel button clicked', () => {
|
||||||
|
spyOn(savedSearchesService, 'deleteSavedSearch').and.callThrough();
|
||||||
|
cancelButton.click();
|
||||||
|
expect(savedSearchesService.deleteSavedSearch).not.toHaveBeenCalled();
|
||||||
|
expect(dialogRef.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete search, show snackbar message and close modal if submit button clicked', fakeAsync(() => () => {
|
||||||
|
spyOn(savedSearchesService, 'deleteSavedSearch').and.callThrough();
|
||||||
|
clickSubmitButton();
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.SUCCESS_MESSAGE'));
|
||||||
|
expect(dialogRef.close).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show snackbar error if there is delete error', fakeAsync(() => () => {
|
||||||
|
spyOn(savedSearchesService, 'deleteSavedSearch').and.throwError('');
|
||||||
|
clickSubmitButton();
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.SUCCESS_MESSAGE'));
|
||||||
|
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
function clickSubmitButton() {
|
||||||
|
submitButton.click();
|
||||||
|
tick();
|
||||||
|
expect(savedSearchesService.deleteSavedSearch).toHaveBeenCalledWith(savedSearchToDelete);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,72 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { SavedSearch, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreModule } from '@alfresco/adf-core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [CoreModule],
|
||||||
|
selector: 'aca-saved-search-delete-dialog',
|
||||||
|
templateUrl: './saved-search-delete-dialog.component.html',
|
||||||
|
styleUrls: ['./saved-search-delete-dialog.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
host: { class: 'aca-saved-search-delete-dialog' }
|
||||||
|
})
|
||||||
|
export class SavedSearchDeleteDialogComponent {
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly dialog: MatDialogRef<SavedSearchDeleteDialogComponent>,
|
||||||
|
private readonly savedSearchesService: SavedSearchesService,
|
||||||
|
private readonly store: Store<AppStore>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) private readonly data: SavedSearch
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isLoading = true;
|
||||||
|
this.savedSearchesService
|
||||||
|
.deleteSavedSearch(this.data)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.dialog.close(this.data);
|
||||||
|
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.SUCCESS_MESSAGE'));
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.ERROR_MESSAGE'));
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
<div class="aca-saved-search-edit-dialog__header">
|
||||||
|
<h2 class="aca-saved-search-edit-dialog__title">{{"APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.TITLE" | translate}}</h2>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
mat-dialog-close
|
||||||
|
[attr.aria-label]="'CLOSE' | translate"
|
||||||
|
[attr.title]="'CLOSE' | translate">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
|
<mat-form-field class="aca-saved-search-edit-dialog__form-field">
|
||||||
|
<mat-label>{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_LABEL' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
[attr.aria-label]="'APP.BROWSE.SEARCH.SAVE_SEARCH.NAME_LABEL' | translate"
|
||||||
|
data-automation-id="saved-search-edit-name"
|
||||||
|
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-saved-search-edit-dialog__form-field">
|
||||||
|
<mat-label>{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.DESCRIPTION_LABEL' | translate }}</mat-label>
|
||||||
|
<textarea
|
||||||
|
matInput
|
||||||
|
data-automation-id="saved-search-edit-description"
|
||||||
|
[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-saved-search-edit-dialog-cancel-button"
|
||||||
|
mat-dialog-close> {{ 'CANCEL' | titlecase | translate }} </button>
|
||||||
|
|
||||||
|
<button mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
id="aca-saved-search-edit-dialog-submit-button"
|
||||||
|
(click)="submit()"
|
||||||
|
[disabled]="!form.valid || isLoading">{{ 'SAVE' | titlecase | translate}}</button>
|
||||||
|
</mat-dialog-actions>
|
@ -0,0 +1,18 @@
|
|||||||
|
.aca-saved-search-edit-dialog {
|
||||||
|
.aca-saved-search-edit-dialog__form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aca-saved-search-edit-dialog__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aca-saved-search-edit-dialog__title {
|
||||||
|
font-size: large;
|
||||||
|
font-weight: 200;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { SavedSearchEditDialogComponent } from './saved-search-edit-dialog.component';
|
||||||
|
import { ContentTestingModule, SavedSearch, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||||
|
import { AppTestingModule } from '../../../../../testing/app-testing.module';
|
||||||
|
|
||||||
|
describe('SaveSearchEditDialogComponent', () => {
|
||||||
|
let fixture: ComponentFixture<SavedSearchEditDialogComponent>;
|
||||||
|
let component: SavedSearchEditDialogComponent;
|
||||||
|
let savedSearchesService: SavedSearchesService;
|
||||||
|
let store: Store;
|
||||||
|
let submitButton: HTMLButtonElement;
|
||||||
|
|
||||||
|
const savedSearchToDelete: SavedSearch = {
|
||||||
|
name: 'test',
|
||||||
|
encodedUrl: '1234',
|
||||||
|
order: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogRef = {
|
||||||
|
close: jasmine.createSpy('close')
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ContentTestingModule, AppTestingModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: MatDialogRef, useValue: dialogRef },
|
||||||
|
provideMockStore(),
|
||||||
|
{ provide: SavedSearchesService, useValue: { editSavedSearch: () => of() } },
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: savedSearchToDelete }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
dialogRef.close.calls.reset();
|
||||||
|
fixture = TestBed.createComponent(SavedSearchEditDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
savedSearchesService = TestBed.inject(SavedSearchesService);
|
||||||
|
store = TestBed.inject(Store);
|
||||||
|
|
||||||
|
submitButton = fixture.nativeElement.querySelector('#aca-saved-search-edit-dialog-submit-button');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not save search if form is invalid', fakeAsync(() => {
|
||||||
|
spyOn(savedSearchesService, 'editSavedSearch').and.callThrough();
|
||||||
|
component.form.controls['name'].setValue('');
|
||||||
|
tick();
|
||||||
|
submitButton.click();
|
||||||
|
expect(savedSearchesService.editSavedSearch).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should save search, show snackbar message and close modal if form is valid', fakeAsync(() => () => {
|
||||||
|
spyOn(savedSearchesService, 'editSavedSearch').and.callThrough();
|
||||||
|
setFormValuesAndSubmit();
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
|
||||||
|
expect(dialogRef.close).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show snackbar error if there is save error', fakeAsync(() => () => {
|
||||||
|
spyOn(savedSearchesService, 'editSavedSearch').and.throwError('');
|
||||||
|
setFormValuesAndSubmit();
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
|
||||||
|
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.editSavedSearch).toHaveBeenCalledWith({
|
||||||
|
name: 'ABCDEF',
|
||||||
|
description: 'TEST',
|
||||||
|
encodedUrl: '1234',
|
||||||
|
order: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,97 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { AutoFocusDirective, forbidOnlySpaces, SavedSearch, SavedSearchesService } from '@alfresco/adf-content-services';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreModule } from '@alfresco/adf-core';
|
||||||
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [CoreModule, AutoFocusDirective],
|
||||||
|
selector: 'aca-saved-search-edit-dialog',
|
||||||
|
templateUrl: './saved-search-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./saved-search-edit-dialog.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
host: { class: 'aca-saved-search-edit-dialog' }
|
||||||
|
})
|
||||||
|
export class SavedSearchEditDialogComponent {
|
||||||
|
form = new FormGroup({
|
||||||
|
name: new FormControl('', [Validators.required, forbidOnlySpaces]),
|
||||||
|
description: new FormControl('')
|
||||||
|
});
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly dialog: MatDialogRef<SavedSearchEditDialogComponent>,
|
||||||
|
private readonly store: Store<AppStore>,
|
||||||
|
private readonly savedSearchesService: SavedSearchesService,
|
||||||
|
@Inject(MAT_DIALOG_DATA) private readonly data: SavedSearch
|
||||||
|
) {
|
||||||
|
this.form.patchValue({
|
||||||
|
name: this.data.name,
|
||||||
|
description: this.data.description
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
if (this.form.invalid || this.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isLoading = true;
|
||||||
|
const formValue = this.form.value;
|
||||||
|
const savedSearch: SavedSearch = {
|
||||||
|
name: formValue.name,
|
||||||
|
description: formValue.description,
|
||||||
|
encodedUrl: this.data.encodedUrl,
|
||||||
|
order: this.data.order
|
||||||
|
};
|
||||||
|
if (this.data.name === formValue.name && this.data.description === formValue.description) {
|
||||||
|
this.onEditSuccess();
|
||||||
|
}
|
||||||
|
this.savedSearchesService
|
||||||
|
.editSavedSearch(savedSearch)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.onEditSuccess();
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEditSuccess(): void {
|
||||||
|
this.dialog.close();
|
||||||
|
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,8 @@
|
|||||||
<h2 mat-dialog-title>{{"APP.BROWSE.SEARCH.SAVE_SEARCH.MODAL_HEADER" | translate}}</h2>
|
<div class="aca-save-search-dialog__header">
|
||||||
|
<h2 class="aca-save-search-dialog__title">{{"APP.BROWSE.SEARCH.SAVE_SEARCH.MODAL_HEADER" | translate}}</h2>
|
||||||
|
<button mat-icon-button mat-dialog-close><mat-icon>close</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<form [formGroup]="form" (submit)="submit()">
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
|
@ -2,4 +2,17 @@
|
|||||||
.aca-save-search-dialog__form-field {
|
.aca-save-search-dialog__form-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aca-save-search-dialog__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aca-save-search-dialog__title {
|
||||||
|
font-size: large;
|
||||||
|
font-weight: 200;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,9 +66,12 @@ describe('SaveSearchDialogComponent', () => {
|
|||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not save search if form is invalid', () => {
|
it('should disable submit button if form is invalid', () => {
|
||||||
spyOn(savedSearchesService, 'saveSearch').and.callThrough();
|
spyOn(savedSearchesService, 'saveSearch').and.callThrough();
|
||||||
submitButton.click();
|
submitButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.form.valid).toBeFalse();
|
||||||
|
expect(submitButton.disabled).toBeTrue();
|
||||||
expect(savedSearchesService.saveSearch).not.toHaveBeenCalled();
|
expect(savedSearchesService.saveSearch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { SavedSearchesListUiService } from './saved-searches-list-ui.service';
|
||||||
|
import { SavedSearch } from '@alfresco/adf-content-services';
|
||||||
|
import { SavedSearchEditDialogComponent } from '../dialog/edit/saved-search-edit-dialog.component';
|
||||||
|
import { SavedSearchDeleteDialogComponent } from '../dialog/delete/saved-search-delete-dialog.component';
|
||||||
|
|
||||||
|
describe('NodeTemplateService', () => {
|
||||||
|
let dialog: MatDialog;
|
||||||
|
let savedSearchesListUiService: SavedSearchesListUiService;
|
||||||
|
|
||||||
|
const mockedSearch: SavedSearch = { name: 'test', encodedUrl: 'test', order: 1 };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [MatDialogModule]
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog = TestBed.inject(MatDialog);
|
||||||
|
savedSearchesListUiService = TestBed.inject(SavedSearchesListUiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open edit save search dialog with proper params', () => {
|
||||||
|
spyOn(dialog, 'open');
|
||||||
|
savedSearchesListUiService.openEditSavedSearch(mockedSearch);
|
||||||
|
|
||||||
|
expect(dialog.open).toHaveBeenCalledWith(SavedSearchEditDialogComponent, { data: mockedSearch, width: '600px' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open delete save search dialog with proper params', () => {
|
||||||
|
spyOn(dialog, 'open');
|
||||||
|
savedSearchesListUiService.confirmDeleteSavedSearch(mockedSearch);
|
||||||
|
|
||||||
|
expect(dialog.open).toHaveBeenCalledWith(SavedSearchDeleteDialogComponent, { data: mockedSearch, minWidth: '500px' });
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,48 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { inject, Injectable } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { SavedSearch } from '@alfresco/adf-content-services';
|
||||||
|
import { SavedSearchDeleteDialogComponent } from '../dialog/delete/saved-search-delete-dialog.component';
|
||||||
|
import { SavedSearchEditDialogComponent } from '../dialog/edit/saved-search-edit-dialog.component';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class SavedSearchesListUiService {
|
||||||
|
private readonly dialog = inject(MatDialog);
|
||||||
|
|
||||||
|
openEditSavedSearch(savedSearch: SavedSearch): void {
|
||||||
|
this.dialog.open(SavedSearchEditDialogComponent, {
|
||||||
|
data: savedSearch,
|
||||||
|
width: '600px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteSavedSearch(savedSearch: SavedSearch): void {
|
||||||
|
this.dialog.open(SavedSearchDeleteDialogComponent, {
|
||||||
|
data: savedSearch,
|
||||||
|
minWidth: '500px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*!
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const savedSearchesListSchema = {
|
||||||
|
default: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
key: 'name',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.NAME',
|
||||||
|
class: 'adf-ellipsis-cell',
|
||||||
|
sortable: false,
|
||||||
|
draggable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
key: 'description',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.DESCRIPTION',
|
||||||
|
class: 'adf-ellipsis-cell',
|
||||||
|
sortable: false,
|
||||||
|
draggable: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
<aca-page-layout>
|
||||||
|
<div class="aca-page-layout-header">
|
||||||
|
<h1 class="aca-page-title">
|
||||||
|
{{ 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.TITLE' | translate }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="savedSearches$ | async as savedSearches else spinner"
|
||||||
|
class="aca-page-layout-content">
|
||||||
|
<div class="aca-main-content" >
|
||||||
|
<aca-saved-searches-ui-list *ngIf="savedSearches.length else emptyList"
|
||||||
|
[savedSearches]="savedSearches"
|
||||||
|
(savedSearchOrderChanged)="onOrderChanged($event)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #emptyList>
|
||||||
|
<adf-empty-content
|
||||||
|
class="aca-page-layout-content"
|
||||||
|
data-automation-id="'saved-search-list-empty-content'"
|
||||||
|
icon="library_books"
|
||||||
|
title="APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.EMPTY_LIST" />
|
||||||
|
</ng-template>
|
||||||
|
</aca-page-layout>
|
||||||
|
|
||||||
|
<ng-template #spinner>
|
||||||
|
<mat-progress-spinner
|
||||||
|
class="aca-page-layout-spinner"
|
||||||
|
data-automation-id="'saved-search-list-spinner'"
|
||||||
|
[color]="'primary'"
|
||||||
|
[mode]="'indeterminate'" />
|
||||||
|
</ng-template>
|
@ -0,0 +1,13 @@
|
|||||||
|
aca-saved-searches-smart-list {
|
||||||
|
.aca-page-layout {
|
||||||
|
background: var(--theme-page-layout-header-background-color);
|
||||||
|
|
||||||
|
.aca-content-header {
|
||||||
|
border-bottom: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aca-page-layout-spinner {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/*!
|
||||||
|
* 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, TestBed } from '@angular/core/testing';
|
||||||
|
import { CORE_PIPES, CoreTestingModule } from '@alfresco/adf-core';
|
||||||
|
import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { SavedSearchesService, SavedSearch } from '@alfresco/adf-content-services';
|
||||||
|
import { SavedSearchesSmartListComponent } from './saved-searches-smart-list.component';
|
||||||
|
import { AppService, DocumentBasePageService, DocumentBasePageServiceMock } from '@alfresco/aca-shared';
|
||||||
|
import { AppState } from '@alfresco/aca-shared/store';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
|
||||||
|
const appServiceMock = {
|
||||||
|
appNavNarMode$: new BehaviorSubject('collapsed'),
|
||||||
|
setAppNavbarMode: jasmine.createSpy('setAppNavbarMode'),
|
||||||
|
toggleAppNavBar$: new Subject()
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('SavedSearchesSmartListComponent', () => {
|
||||||
|
let fixture: ComponentFixture<SavedSearchesSmartListComponent>;
|
||||||
|
let fakeSavedSearches$: ReplaySubject<SavedSearch[]>;
|
||||||
|
let appState: Partial<AppState> = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fakeSavedSearches$ = new ReplaySubject<SavedSearch[]>(1);
|
||||||
|
appState = {
|
||||||
|
selection: {
|
||||||
|
count: 0,
|
||||||
|
isEmpty: false,
|
||||||
|
libraries: [],
|
||||||
|
nodes: []
|
||||||
|
},
|
||||||
|
navigation: {},
|
||||||
|
infoDrawerOpened: false
|
||||||
|
};
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CoreTestingModule, SavedSearchesSmartListComponent],
|
||||||
|
providers: [
|
||||||
|
provideMockStore({
|
||||||
|
initialState: { app: appState }
|
||||||
|
}),
|
||||||
|
...CORE_PIPES,
|
||||||
|
{ provide: DocumentBasePageService, useClass: DocumentBasePageServiceMock },
|
||||||
|
{ provide: SavedSearchesService, useValue: { savedSearches$: fakeSavedSearches$ } },
|
||||||
|
{ provide: AppService, useValue: appServiceMock }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SavedSearchesSmartListComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the list of searches', async () => {
|
||||||
|
const mockSavedSearches: SavedSearch[] = [
|
||||||
|
{ name: '1', order: 0, encodedUrl: '' },
|
||||||
|
{ name: '2', order: 1, encodedUrl: '' }
|
||||||
|
];
|
||||||
|
fakeSavedSearches$.next(mockSavedSearches);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const listComponent = fixture.nativeElement.querySelector('aca-saved-searches-ui-list');
|
||||||
|
expect(listComponent).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the no content template when no saved searches are found', async () => {
|
||||||
|
const mockSavedSearches: SavedSearch[] = [];
|
||||||
|
fakeSavedSearches$.next(mockSavedSearches);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const emptyContent = fixture.nativeElement.querySelector('.adf-empty-content');
|
||||||
|
expect(emptyContent).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the spinner while saved searches is loading', async () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
const matSpinnerElement = fixture.debugElement.query(By.css(`[data-automation-id="'saved-search-list-spinner'"]`));
|
||||||
|
|
||||||
|
expect(matSpinnerElement).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,50 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { CommonModule } from '@angular/common';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SavedSearchesListUiComponent } from '../ui-list/saved-searches-list.ui-component';
|
||||||
|
import { PageComponent, PageLayoutComponent } from '@alfresco/aca-shared';
|
||||||
|
import { SavedSearchesService } from '@alfresco/adf-content-services';
|
||||||
|
import { EmptyContentComponent } from '@alfresco/adf-core';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aca-saved-searches-smart-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, TranslateModule, SavedSearchesListUiComponent, PageLayoutComponent, EmptyContentComponent, MatProgressSpinnerModule],
|
||||||
|
templateUrl: './saved-searches-smart-list.component.html',
|
||||||
|
styleUrls: ['./saved-searches-smart-list.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
|
})
|
||||||
|
export class SavedSearchesSmartListComponent extends PageComponent {
|
||||||
|
savedSearchesService = inject(SavedSearchesService);
|
||||||
|
|
||||||
|
savedSearches$ = this.savedSearchesService.savedSearches$;
|
||||||
|
|
||||||
|
onOrderChanged(event: { previousIndex: number; currentIndex: number }): void {
|
||||||
|
this.savedSearchesService.changeOrder(event.previousIndex, event.currentIndex);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<adf-datatable
|
||||||
|
[rows]="savedSearches"
|
||||||
|
[columns]="columns"
|
||||||
|
[stickyHeader]="true"
|
||||||
|
[showHeader]="ShowHeaderMode.Always"
|
||||||
|
[enableDragRows]="true"
|
||||||
|
[actionsVisibleOnHover]="true"
|
||||||
|
[contextMenu]="true"
|
||||||
|
[actions]="true"
|
||||||
|
[isResizingEnabled]="false"
|
||||||
|
[blurOnResize]="false"
|
||||||
|
(showRowActionsMenu)="onShowRowActionsMenu($event)"
|
||||||
|
(dragDropped)="onSearchOrderChange($event)"
|
||||||
|
(executeRowAction)="executeMenuOption($event.value.action.key, $event.value.row.obj)"
|
||||||
|
(showRowContextMenu)="fillContextMenu($event)" />
|
@ -0,0 +1,32 @@
|
|||||||
|
.aca-saved-searches-ui-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.adf-datatable-list {
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.adf-datatable-header {
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
.adf-datatable-row {
|
||||||
|
margin-right: 15px;
|
||||||
|
padding-right: 0;
|
||||||
|
border-bottom: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adf-datatable-body {
|
||||||
|
.adf-datatable-row {
|
||||||
|
border-top: 0;
|
||||||
|
margin: 0 20px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(5) + adf-datatable-row {
|
||||||
|
border-top: 1px dashed var(--adf-theme-foreground-text-color-014);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
/*!
|
||||||
|
* 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, TestBed } from '@angular/core/testing';
|
||||||
|
import { CoreTestingModule, DataCellEvent, DataTableComponent } from '@alfresco/adf-core';
|
||||||
|
import { SavedSearchesListUiComponent } from './saved-searches-list.ui-component';
|
||||||
|
import { SavedSearchesListUiService } from '../saved-searches-list-ui.service';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { SavedSearch } from '@alfresco/adf-content-services';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
|
|
||||||
|
describe('SavedSearchesListUiComponent ', () => {
|
||||||
|
let fixture: ComponentFixture<SavedSearchesListUiComponent>;
|
||||||
|
let component: SavedSearchesListUiComponent;
|
||||||
|
let savedSearchesListUiService: SavedSearchesListUiService;
|
||||||
|
let clipboard: Clipboard;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CoreTestingModule, SavedSearchesListUiComponent],
|
||||||
|
providers: [SavedSearchesListUiService, provideMockStore()]
|
||||||
|
});
|
||||||
|
|
||||||
|
savedSearchesListUiService = TestBed.inject(SavedSearchesListUiService);
|
||||||
|
clipboard = TestBed.inject(Clipboard);
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SavedSearchesListUiComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getColumnDefinition(key: string, title: string) {
|
||||||
|
return jasmine.objectContaining({
|
||||||
|
id: '',
|
||||||
|
key,
|
||||||
|
type: 'text',
|
||||||
|
sortable: false,
|
||||||
|
title,
|
||||||
|
draggable: false,
|
||||||
|
isHidden: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Data table', () => {
|
||||||
|
let dataTable: DataTableComponent;
|
||||||
|
let dataCellEvent: DataCellEvent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
dataTable = fixture.debugElement.query(By.directive(DataTableComponent)).componentInstance;
|
||||||
|
dataCellEvent = new DataCellEvent(
|
||||||
|
{
|
||||||
|
isSelected: true,
|
||||||
|
hasValue: () => true,
|
||||||
|
getValue: () => 'Some value',
|
||||||
|
obj: {
|
||||||
|
field: 'some value',
|
||||||
|
id: 'some id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'col 1',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have assigned tags as rows', () => {
|
||||||
|
const mockSavedSearches: SavedSearch[] = [
|
||||||
|
{ name: '1', order: 0, encodedUrl: '123' },
|
||||||
|
{ name: '2', order: 1, encodedUrl: '1234' }
|
||||||
|
];
|
||||||
|
component.savedSearches = mockSavedSearches;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(dataTable.rows).toEqual(mockSavedSearches);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have assigned correct columns', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(dataTable.columns).toEqual([
|
||||||
|
getColumnDefinition('name', 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.NAME'),
|
||||||
|
getColumnDefinition('description', 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.DESCRIPTION')
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fill context menu options on showRowContextMenu event', () => {
|
||||||
|
dataTable.showRowContextMenu.emit(dataCellEvent);
|
||||||
|
expect(dataCellEvent.value.actions).toEqual([
|
||||||
|
{
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD',
|
||||||
|
key: 'copy',
|
||||||
|
subject: jasmine.any(Subject),
|
||||||
|
model: {
|
||||||
|
visible: true,
|
||||||
|
icon: 'copy'
|
||||||
|
},
|
||||||
|
data: dataCellEvent.value.row.obj
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.CONTEXT_OPTION',
|
||||||
|
key: 'edit',
|
||||||
|
subject: jasmine.any(Subject),
|
||||||
|
model: {
|
||||||
|
visible: true,
|
||||||
|
icon: 'edit'
|
||||||
|
},
|
||||||
|
data: dataCellEvent.value.row.obj
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.CONTEXT_OPTION',
|
||||||
|
key: 'delete',
|
||||||
|
subject: jasmine.any(Subject),
|
||||||
|
model: {
|
||||||
|
visible: true,
|
||||||
|
icon: 'delete'
|
||||||
|
},
|
||||||
|
data: dataCellEvent.value.row.obj
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Context menu actions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(savedSearchesListUiService, 'openEditSavedSearch');
|
||||||
|
spyOn(savedSearchesListUiService, 'confirmDeleteSavedSearch');
|
||||||
|
spyOn(clipboard, 'copy');
|
||||||
|
dataTable.showRowContextMenu.emit(dataCellEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edit save search', () => {
|
||||||
|
let editAction: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
editAction = dataCellEvent.value.actions[1];
|
||||||
|
editAction.subject.next(editAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call openEditSavedSearch when selected edit option', () => {
|
||||||
|
expect(savedSearchesListUiService.openEditSavedSearch).toHaveBeenCalledWith(editAction.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Delete save search', () => {
|
||||||
|
let deleteAction: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
deleteAction = dataCellEvent.value.actions[2];
|
||||||
|
deleteAction.subject.next(deleteAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call confirmDeleteSavedSearch when selected delete option', () => {
|
||||||
|
expect(savedSearchesListUiService.confirmDeleteSavedSearch).toHaveBeenCalledWith(deleteAction.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Copy to clipboard save search', () => {
|
||||||
|
let actionData: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actionData = dataCellEvent.value.actions[0];
|
||||||
|
actionData.subject.next(actionData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call copy to clipboard when selected delete option', () => {
|
||||||
|
expect(clipboard.copy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,148 @@
|
|||||||
|
/*!
|
||||||
|
* 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 { AfterContentInit, Component, DestroyRef, EventEmitter, inject, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||||
|
import {
|
||||||
|
AppConfigService,
|
||||||
|
DataCellEvent,
|
||||||
|
DATATABLE_DIRECTIVES,
|
||||||
|
DataTableComponent,
|
||||||
|
DataTableSchema,
|
||||||
|
ShowHeaderMode,
|
||||||
|
TEMPLATE_DIRECTIVES
|
||||||
|
} from '@alfresco/adf-core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { SavedSearchesListUiService } from '../saved-searches-list-ui.service';
|
||||||
|
import { savedSearchesListSchema } from '../smart-list/saved-searches-list-schema';
|
||||||
|
import { SavedSearch } from '@alfresco/adf-content-services';
|
||||||
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { SnackbarInfoAction } from '@alfresco/aca-shared/store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aca-saved-searches-ui-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, DATATABLE_DIRECTIVES, TEMPLATE_DIRECTIVES, DataTableComponent],
|
||||||
|
templateUrl: './saved-searches-list.ui-component.html',
|
||||||
|
styleUrls: ['./saved-searches-list.ui-component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
host: { class: 'aca-saved-searches-ui-list' }
|
||||||
|
})
|
||||||
|
export class SavedSearchesListUiComponent extends DataTableSchema implements AfterContentInit {
|
||||||
|
@Input()
|
||||||
|
savedSearches: SavedSearch[] = [];
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
savedSearchOrderChanged = new EventEmitter<{ previousIndex: number; currentIndex: number }>();
|
||||||
|
|
||||||
|
readonly ShowHeaderMode = ShowHeaderMode;
|
||||||
|
|
||||||
|
private readonly savedSearchesListUiService = inject(SavedSearchesListUiService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly contextMenuAction$ = new Subject<any>();
|
||||||
|
private readonly editSavedSearchOptionKey = 'edit';
|
||||||
|
private readonly deleteSavedSearchOptionKey = 'delete';
|
||||||
|
private readonly copyToClipboardUrlOptionKey = 'copy';
|
||||||
|
private readonly menuOptions = [
|
||||||
|
{
|
||||||
|
icon: 'copy',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD',
|
||||||
|
key: this.copyToClipboardUrlOptionKey
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'edit',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.CONTEXT_OPTION',
|
||||||
|
key: this.editSavedSearchOptionKey
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'delete',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.CONTEXT_OPTION',
|
||||||
|
key: this.deleteSavedSearchOptionKey
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(protected appConfig: AppConfigService, private readonly clipboard: Clipboard, private readonly store: Store) {
|
||||||
|
super(appConfig, '', savedSearchesListSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterContentInit() {
|
||||||
|
this.createDatatableSchema();
|
||||||
|
this.contextMenuAction$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((action) => this.executeMenuOption(action.key, action.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowRowActionsMenu(event: DataCellEvent): void {
|
||||||
|
event.value.actions = this.menuOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchOrderChange(event: { previousIndex: number; currentIndex: number }): void {
|
||||||
|
this.savedSearchOrderChanged.next(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMenuOption(optionKey: string, savedSearchData: SavedSearch): void {
|
||||||
|
switch (optionKey) {
|
||||||
|
case this.editSavedSearchOptionKey:
|
||||||
|
this.openEditSavedSearchDialog(savedSearchData);
|
||||||
|
break;
|
||||||
|
case this.deleteSavedSearchOptionKey:
|
||||||
|
this.openDeleteSavedSearchDialog(savedSearchData);
|
||||||
|
break;
|
||||||
|
case this.copyToClipboardUrlOptionKey:
|
||||||
|
this.copyToClipboard(savedSearchData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openEditSavedSearchDialog(savedSearch: SavedSearch): void {
|
||||||
|
this.savedSearchesListUiService.openEditSavedSearch(savedSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteSavedSearchDialog(savedSearch: SavedSearch): void {
|
||||||
|
this.savedSearchesListUiService.confirmDeleteSavedSearch(savedSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard(savedSearch: SavedSearch): void {
|
||||||
|
this.clipboard.copy(this.getFullUrl(savedSearch.encodedUrl));
|
||||||
|
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD_SUCCESS'));
|
||||||
|
}
|
||||||
|
|
||||||
|
fillContextMenu(event: DataCellEvent) {
|
||||||
|
event.value.actions = this.menuOptions.map((option) => ({
|
||||||
|
title: option.title,
|
||||||
|
key: option.key,
|
||||||
|
subject: this.contextMenuAction$,
|
||||||
|
model: {
|
||||||
|
visible: true,
|
||||||
|
icon: option.icon
|
||||||
|
},
|
||||||
|
data: event.value.row.obj
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFullUrl(path: string): string {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
return `${baseUrl}/#/search?q=${path}`;
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
<app-expand-menu *ngIf="item" [item]="item" (actionClicked)="onActionClick()" />
|
<app-expand-menu *ngIf="item" [item]="item" />
|
||||||
|
@ -44,9 +44,10 @@ export class SaveSearchSidenavComponent implements OnInit, OnDestroy {
|
|||||||
appService = inject(AppService);
|
appService = inject(AppService);
|
||||||
translationService = inject(TranslationService);
|
translationService = inject(TranslationService);
|
||||||
destroy$ = new Subject<void>();
|
destroy$ = new Subject<void>();
|
||||||
|
|
||||||
item: NavBarLinkRef;
|
item: NavBarLinkRef;
|
||||||
|
|
||||||
|
private readonly manageSearchesId = 'manage-saved-searches';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.savedSearchesService.innit();
|
this.savedSearchesService.innit();
|
||||||
this.savedSearchesService.savedSearches$
|
this.savedSearchesService.savedSearches$
|
||||||
@ -62,10 +63,6 @@ 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) => ({
|
||||||
@ -77,6 +74,15 @@ 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({
|
||||||
|
id: this.manageSearchesId,
|
||||||
|
icon: '',
|
||||||
|
title: 'APP.BROWSE.SEARCH.SAVE_SEARCH.NAVBAR.MANAGE_BUTTON',
|
||||||
|
route: 'saved-searches',
|
||||||
|
url: 'saved-searches'
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
icon: '',
|
icon: '',
|
||||||
title,
|
title,
|
||||||
|
@ -43,7 +43,6 @@
|
|||||||
<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, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
|
import { ChangeDetectorRef, Component, Input, OnInit, 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,9 +54,6 @@ 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, EventEmitter, HostListener, Input, Output } from '@angular/core';
|
import { Directive, HostListener, Input } 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,8 +36,6 @@ 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) {
|
||||||
@ -48,7 +46,6 @@ 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>) {}
|
||||||
|
@ -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 { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
||||||
import { SidenavComponent } from './sidenav.component';
|
import { SidenavComponent } from './sidenav.component';
|
||||||
import { AppTestingModule } from '../../testing/app-testing.module';
|
import { AppTestingModule } from '../../testing/app-testing.module';
|
||||||
@ -54,7 +54,8 @@ describe('SidenavComponent', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ provide: NavigationHistoryService, useValue: navigationHistoryServiceSpy },
|
{ provide: NavigationHistoryService, useValue: navigationHistoryServiceSpy },
|
||||||
SidenavLayoutComponent
|
SidenavLayoutComponent,
|
||||||
|
ChangeDetectorRef
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
});
|
});
|
||||||
|
@ -100,7 +100,7 @@ export function formatSearchTerm(userInput: string, fields = ['cm:name']): strin
|
|||||||
*/
|
*/
|
||||||
export function extractUserQueryFromEncodedQuery(encodedQuery: string): string {
|
export function extractUserQueryFromEncodedQuery(encodedQuery: string): string {
|
||||||
if (encodedQuery) {
|
if (encodedQuery) {
|
||||||
const decodedQuery: { [key: string]: any } = JSON.parse(Buffer.from(encodedQuery, 'base64').toString('utf8'));
|
const decodedQuery: { [key: string]: any } = JSON.parse(Buffer.from(encodedQuery, 'base64').toString('utf-8'));
|
||||||
return decodedQuery.userQuery;
|
return decodedQuery.userQuery;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@ -136,7 +136,7 @@ export function extractSearchedWordFromEncodedQuery(encodedQuery: string): strin
|
|||||||
*/
|
*/
|
||||||
export function extractFiltersFromEncodedQuery(encodedQuery: string): any {
|
export function extractFiltersFromEncodedQuery(encodedQuery: string): any {
|
||||||
if (encodedQuery) {
|
if (encodedQuery) {
|
||||||
const decodedQuery = Buffer.from(encodedQuery, 'base64').toString('utf8');
|
const decodedQuery = Buffer.from(encodedQuery, 'base64').toString('utf-8');
|
||||||
return JSON.parse(decodedQuery);
|
return JSON.parse(decodedQuery);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
"license": "LGPL-3.0",
|
"license": "LGPL-3.0",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@alfresco/adf-content-services": ">=7.0.0-alpha.4",
|
"@alfresco/adf-content-services": ">=7.0.0-alpha.5-0",
|
||||||
"@alfresco/adf-core": ">=7.0.0-alpha.4",
|
"@alfresco/adf-core": ">=7.0.0-alpha.5-0",
|
||||||
"@alfresco/adf-extensions": ">=7.0.0-alpha.4",
|
"@alfresco/adf-extensions": ">=7.0.0-alpha.5-0",
|
||||||
"@alfresco/js-api": ">=8.0.0-alpha.4",
|
"@alfresco/js-api": ">=8.0.0-alpha.5-0",
|
||||||
"@angular/animations": ">=15.2",
|
"@angular/animations": ">=15.2",
|
||||||
"@angular/common": ">=15.2",
|
"@angular/common": ">=15.2",
|
||||||
"@angular/compiler": ">=15.2",
|
"@angular/compiler": ">=15.2",
|
||||||
|
@ -65,7 +65,7 @@ export class AppService implements ShellAppService, OnDestroy {
|
|||||||
toggleAppNavBar$ = new Subject<void>();
|
toggleAppNavBar$ = new Subject<void>();
|
||||||
|
|
||||||
hideSidenavConditions = ['/preview/'];
|
hideSidenavConditions = ['/preview/'];
|
||||||
minimizeSidenavConditions = ['search'];
|
minimizeSidenavConditions = ['/search'];
|
||||||
|
|
||||||
onDestroy$ = new Subject<boolean>();
|
onDestroy$ = new Subject<boolean>();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user