[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:
dominikiwanekhyland 2024-10-31 14:16:23 +01:00 committed by GitHub
parent cacc4149fa
commit c2d2f95095
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 2665 additions and 4088 deletions

5392
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,11 +30,11 @@
},
"private": true,
"dependencies": {
"@alfresco/adf-content-services": "7.0.0-alpha.4",
"@alfresco/adf-core": "7.0.0-alpha.4",
"@alfresco/adf-extensions": "7.0.0-alpha.4",
"@alfresco/eslint-plugin-eslint-angular": "7.0.0-alpha.4",
"@alfresco/js-api": "8.0.0-alpha.4",
"@alfresco/adf-content-services": "7.0.0-alpha.5-11519897356",
"@alfresco/adf-core": "7.0.0-alpha.5-11519897356",
"@alfresco/adf-extensions": "7.0.0-alpha.5-11519897356",
"@alfresco/eslint-plugin-eslint-angular": "7.0.0-alpha.5-11519897356",
"@alfresco/js-api": "8.0.0-alpha.5-11519897356",
"@angular/animations": "16.2.9",
"@angular/cdk": "16.2.9",
"@angular/common": "16.2.9",
@ -62,8 +62,8 @@
"zone.js": "0.13.3"
},
"devDependencies": {
"@alfresco/adf-cli": "7.0.0-alpha.4",
"@angular-devkit/build-angular": "16.2.16",
"@alfresco/adf-cli": "7.0.0-alpha.5-11519897356",
"@angular-devkit/build-angular": "16.2.9",
"@angular-devkit/core": "16.2.9",
"@angular-devkit/schematics": "16.2.9",
"@angular-eslint/builder": "17.0.0",

View File

@ -216,9 +216,31 @@
"NAME_REQUIRED_ERROR": "This field is required",
"DESCRIPTION_LABEL": "Description",
"SAVE_SUCCESS": "Search Saved",
"SAVE_ERROR": "Error occured. Search could not be saved.",
"SAVE_ERROR": "Error occurred. Search could not be saved.",
"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",

View File

@ -5,10 +5,10 @@
"peerDependencies": {
"@angular/common": ">=15.2",
"@angular/core": ">=15.2",
"@alfresco/adf-core": ">=7.0.0-alpha.4",
"@alfresco/adf-content-services": ">=7.0.0-alpha.4",
"@alfresco/adf-extensions": ">=7.0.0-alpha.4",
"@alfresco/js-api": ">=8.0.0-alpha.4",
"@alfresco/adf-core": ">=7.0.0-alpha.5-0",
"@alfresco/adf-content-services": ">=7.0.0-alpha.5-0",
"@alfresco/adf-extensions": ">=7.0.0-alpha.5-0",
"@alfresco/js-api": ">=8.0.0-alpha.5-0",
"@angular/animations": ">=15.2",
"@angular/cdk": ">=15.2",
"@angular/forms": ">=15.2",

View File

@ -27,7 +27,7 @@ import { LibrariesComponent } from './components/libraries/libraries.component';
import { FavoriteLibrariesComponent } from './components/favorite-libraries/favorite-libraries.component';
import { SearchResultsComponent } from './components/search/search-results/search-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 { FavoritesComponent } from './components/favorites/favorites.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 { ShellLayoutComponent } from '@alfresco/adf-core/shell';
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[] = [
{
@ -354,6 +355,15 @@ export const CONTENT_LAYOUT_ROUTES: Route = {
...createViewRoutes('knowledge-retrieval')
]
},
{
path: 'saved-searches',
children: [
{
path: '',
component: SavedSearchesSmartListComponent
}
]
},
{
path: '**',
component: GenericErrorComponent

View File

@ -179,9 +179,9 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
this.subscriptions.push(
this.queryBuilder.updated.subscribe((query) => {
this.isLoading = true;
if (query) {
this.sorting = this.getSorting();
this.isLoading = true;
this.changeDetectorRef.detectChanges();
}
}),
@ -203,6 +203,9 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
if (this.route) {
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
if (params[this.queryParamName]) {
this.isLoading = true;
}
this.loadedFilters$.next();
this.encodedQuery = params[this.queryParamName] || null;
this.searchedWord = extractSearchedWordFromEncodedQuery(this.encodedQuery);

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
});

View File

@ -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;
}
});
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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
});
}
});

View File

@ -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'));
}
}

View File

@ -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>
<form [formGroup]="form" (submit)="submit()">

View File

@ -2,4 +2,17 @@
.aca-save-search-dialog__form-field {
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;
}
}

View File

@ -66,9 +66,12 @@ describe('SaveSearchDialogComponent', () => {
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();
submitButton.click();
fixture.detectChanges();
expect(component.form.valid).toBeFalse();
expect(submitButton.disabled).toBeTrue();
expect(savedSearchesService.saveSearch).not.toHaveBeenCalled();
});

View File

@ -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' });
});
});

View File

@ -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'
});
}
}

View File

@ -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
}
]
};

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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)" />

View File

@ -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);
}
}
}
}
}

View File

@ -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();
});
});
});
});
});

View File

@ -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}`;
}
}

View File

@ -1 +1 @@
<app-expand-menu *ngIf="item" [item]="item" (actionClicked)="onActionClick()" />
<app-expand-menu *ngIf="item" [item]="item" />

View File

@ -44,9 +44,10 @@ export class SaveSearchSidenavComponent implements OnInit, OnDestroy {
appService = inject(AppService);
translationService = inject(TranslationService);
destroy$ = new Subject<void>();
item: NavBarLinkRef;
private readonly manageSearchesId = 'manage-saved-searches';
ngOnInit() {
this.savedSearchesService.innit();
this.savedSearchesService.savedSearches$
@ -62,10 +63,6 @@ export class SaveSearchSidenavComponent implements OnInit, OnDestroy {
this.destroy$.complete();
}
onActionClick(): void {
this.appService.appNavNarMode$.next('collapsed');
}
private createNavBarLinkRef(children: SavedSearch[]): NavBarLinkRef {
const mappedChildren = children
.map((child) => ({
@ -77,6 +74,15 @@ export class SaveSearchSidenavComponent implements OnInit, OnDestroy {
}))
.slice(0, 5);
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 {
icon: '',
title,

View File

@ -43,7 +43,6 @@
<button
acaActiveLink="aca-action-button--active"
[action]="child"
(actionClicked)="actionClicked.emit()"
[attr.aria-label]="child.title | translate"
[id]="child.id"
[attr.data-automation-id]="child.id"

View File

@ -22,7 +22,7 @@
* 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 { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
@ -54,9 +54,6 @@ export class ExpandMenuComponent implements OnInit {
@Input()
item: NavBarLinkRef;
@Output()
actionClicked = new EventEmitter<void>();
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {

View File

@ -22,7 +22,7 @@
* 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 { Store } from '@ngrx/store';
import { AppStore } from '@alfresco/aca-shared/store';
@ -36,8 +36,6 @@ import { AppStore } from '@alfresco/aca-shared/store';
export class ActionDirective {
@Input() action;
@Output() actionClicked = new EventEmitter<void>();
@HostListener('click')
onClick() {
if (this.action.url) {
@ -48,7 +46,6 @@ export class ActionDirective {
payload: this.getNavigationCommands(this.action.click.payload)
});
}
this.actionClicked.next();
}
constructor(private router: Router, private store: Store<AppStore>) {}

View File

@ -22,7 +22,7 @@
* 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 { SidenavComponent } from './sidenav.component';
import { AppTestingModule } from '../../testing/app-testing.module';
@ -54,7 +54,8 @@ describe('SidenavComponent', () => {
}
},
{ provide: NavigationHistoryService, useValue: navigationHistoryServiceSpy },
SidenavLayoutComponent
SidenavLayoutComponent,
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
});

View File

@ -100,7 +100,7 @@ export function formatSearchTerm(userInput: string, fields = ['cm:name']): strin
*/
export function extractUserQueryFromEncodedQuery(encodedQuery: string): string {
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 '';
@ -136,7 +136,7 @@ export function extractSearchedWordFromEncodedQuery(encodedQuery: string): strin
*/
export function extractFiltersFromEncodedQuery(encodedQuery: string): any {
if (encodedQuery) {
const decodedQuery = Buffer.from(encodedQuery, 'base64').toString('utf8');
const decodedQuery = Buffer.from(encodedQuery, 'base64').toString('utf-8');
return JSON.parse(decodedQuery);
}
return null;

View File

@ -5,10 +5,10 @@
"license": "LGPL-3.0",
"scripts": {},
"peerDependencies": {
"@alfresco/adf-content-services": ">=7.0.0-alpha.4",
"@alfresco/adf-core": ">=7.0.0-alpha.4",
"@alfresco/adf-extensions": ">=7.0.0-alpha.4",
"@alfresco/js-api": ">=8.0.0-alpha.4",
"@alfresco/adf-content-services": ">=7.0.0-alpha.5-0",
"@alfresco/adf-core": ">=7.0.0-alpha.5-0",
"@alfresco/adf-extensions": ">=7.0.0-alpha.5-0",
"@alfresco/js-api": ">=8.0.0-alpha.5-0",
"@angular/animations": ">=15.2",
"@angular/common": ">=15.2",
"@angular/compiler": ">=15.2",

View File

@ -65,7 +65,7 @@ export class AppService implements ShellAppService, OnDestroy {
toggleAppNavBar$ = new Subject<void>();
hideSidenavConditions = ['/preview/'];
minimizeSidenavConditions = ['search'];
minimizeSidenavConditions = ['/search'];
onDestroy$ = new Subject<boolean>();