[ACS-8746] use notification service instead of store, yellow warnings, warning on no library permission (#4577)

* [ACS-8746] use notification service instead of store, yellow warnings, warning on no library permissions

* [ACS-8746] move snackbar bg styling to app level, review comments

* [ACS-8746] change warning color for improved accessibility

* [ACS-8746] fix wrong css class for copy and move notifications

* [ACS-8746] completely replace SnackbarActions with notifications service, update warning color

* [ACS-8746] improve copy and move nodes notifications

* ACS-8746 fix undo delete notification action

* ACS-8746 review comments

* ACS-8746 review comments
This commit is contained in:
Grzegorz Jaśkowski 2025-06-16 10:14:27 +02:00 committed by GitHub
parent 1072c7d2f3
commit f48fc8c2d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 476 additions and 786 deletions

View File

@ -92,9 +92,7 @@ export const myExtensionLoader = (route: ActivatedRouteSnapshot) => {
tap((status) => {
if (!status) {
// If the BE is down, let the user know what to expect
store.dispatch(
new SnackbarErrorAction("Backend error. My Extension's features are disabled.")
);
notificationService.showError("Backend error. My Extension's features are disabled.");
}
})
);

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "هذه المكتبة لم تعد موجودة.",
"LIBRARY_NO_PERMISSIONS": "ليس لديك الإذن لعرض هذه المكتبة.",
"LIBRARY_LOADING_ERROR": "حدثت بعض المشاكل أثناء تحميل هذه المكتبة."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "ليس لديك الإذن لعرض هذه المكتبة."
},
"SHARED": {
"TITLE": "الملفات المشتركة",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Tato knihovna již neexistuje.",
"LIBRARY_NO_PERMISSIONS": "K zobrazení této knihovny nemáte oprávnění.",
"LIBRARY_LOADING_ERROR": "Při načítání této knihovny došlo k problému."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "K zobrazení této knihovny nemáte oprávnění."
},
"SHARED": {
"TITLE": "Sdílené soubory",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Dette bibliotek findes ikke mere.",
"LIBRARY_NO_PERMISSIONS": "Du har ikke tilladelse til at se dette bibliotek.",
"LIBRARY_LOADING_ERROR": "Der var problemer under hentning af dette bibliotek."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Du har ikke tilladelse til at se dette bibliotek."
},
"SHARED": {
"TITLE": "Delte filer",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Diese Bibliothek ist nicht länger vorhanden.",
"LIBRARY_NO_PERMISSIONS": "Sie haben keine Berechtigung, diese Bibliothek anzuzeigen.",
"LIBRARY_LOADING_ERROR": "Beim Laden dieser Bibliothek ist ein Fehler aufgetreten."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Sie haben keine Berechtigung, diese Bibliothek anzuzeigen."
},
"SHARED": {
"TITLE": "Freigegebene Dateien",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "This library no longer exists.",
"LIBRARY_NO_PERMISSIONS": "You do not have permission to view this library.",
"LIBRARY_LOADING_ERROR": "There was some problem during loading this library."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "You do not have permission to view this library."
},
"SHARED": {
"TITLE": "Shared Files",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Esta biblioteca ya no existe.",
"LIBRARY_NO_PERMISSIONS": "No tiene permiso para ver esta biblioteca.",
"LIBRARY_LOADING_ERROR": "Ha habido un problema al cargar esta biblioteca."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "No tiene permiso para ver esta biblioteca."
},
"SHARED": {
"TITLE": "Ficheros compartidos",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Tätä kirjastoa ei ole enää olemassa",
"LIBRARY_NO_PERMISSIONS": "Sinulla ei ole oikeutta tarkastella tätä kirjastoa.",
"LIBRARY_LOADING_ERROR": "Tämän kirjaston lataamisessa ilmeni ongelma."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Sinulla ei ole oikeutta tarkastella tätä kirjastoa."
},
"SHARED": {
"TITLE": "Jaetut tiedostot",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Cette bibliothèque n'existe plus.",
"LIBRARY_NO_PERMISSIONS": "Vous n'avez pas la permission de consulter cette bibliothèque.",
"LIBRARY_LOADING_ERROR": "Un problème s'est produit pendant le chargement de cette bibliothèque."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Vous n'avez pas la permission de consulter cette bibliothèque."
},
"SHARED": {
"TITLE": "Fichiers partagés",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Questa libreria non esiste più.",
"LIBRARY_NO_PERMISSIONS": "L'utente non dispone dell'autorizzazione per visualizzare questa libreria.",
"LIBRARY_LOADING_ERROR": "Ci sono stati dei problemi nel caricamento di questa libreria."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "L'utente non dispone dell'autorizzazione per visualizzare questa libreria."
},
"SHARED": {
"TITLE": "File condivisi",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "このライブラリはもはや存在しません。",
"LIBRARY_NO_PERMISSIONS": "このライブラリを閲覧する権限がありません。",
"LIBRARY_LOADING_ERROR": "このライブラリのロード中に何らかの問題が発生しました。"
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "このライブラリを閲覧する権限がありません。"
},
"SHARED": {
"TITLE": "共有ファイル",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Deze bibliotheek bestaat niet meer.",
"LIBRARY_NO_PERMISSIONS": "U hebt geen machtiging om deze bibliotheek te bekijken.",
"LIBRARY_LOADING_ERROR": "Er was een probleem bij het laden van deze bibliotheek."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "U hebt geen machtiging om deze bibliotheek te bekijken."
},
"SHARED": {
"TITLE": "Gedeelde bestanden",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Dette biblioteket eksisterer ikke lenger.",
"LIBRARY_NO_PERMISSIONS": "Du har ikke tillatelse til å vise dette biblioteket.",
"LIBRARY_LOADING_ERROR": "Det oppsto et problem under lasting av dette biblioteket."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Du har ikke tillatelse til å vise dette biblioteket."
},
"SHARED": {
"TITLE": "Delte filer",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Ta biblioteka już nie istnieje.",
"LIBRARY_NO_PERMISSIONS": "Nie masz uprawnień do wyświetlenia tej biblioteki.",
"LIBRARY_LOADING_ERROR": "Wystąpił problem podczas ładowania tej biblioteki."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Nie masz uprawnień do wyświetlenia tej biblioteki."
},
"SHARED": {
"TITLE": "Udostępnione pliki",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Esta biblioteca já não existe.",
"LIBRARY_NO_PERMISSIONS": "Você não tem permissão para visualizar esta biblioteca.",
"LIBRARY_LOADING_ERROR": "Ocorreu um problema ao carregar esta biblioteca."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Você não tem permissão para visualizar esta biblioteca."
},
"SHARED": {
"TITLE": "Arquivos compartilhados",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Данная библиотека не существует.",
"LIBRARY_NO_PERMISSIONS": "Нет прав для просмотра данной библиотеки.",
"LIBRARY_LOADING_ERROR": "Ошибка загрузки библиотеки."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Нет прав для просмотра данной библиотеки."
},
"SHARED": {
"TITLE": "Общие файлы",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "Detta bibliotek finns inte längre.",
"LIBRARY_NO_PERMISSIONS": "Du har inte behörighet att visa detta bibliotek.",
"LIBRARY_LOADING_ERROR": "Ett problem uppstod när detta bibliotek skulle läsas in."
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "Du har inte behörighet att visa detta bibliotek."
},
"SHARED": {
"TITLE": "Delade filer",

View File

@ -153,9 +153,9 @@
},
"ERRORS": {
"LIBRARY_NOT_FOUND": "此收藏库不再存在。",
"LIBRARY_NO_PERMISSIONS": "您无权查看此收藏库。",
"LIBRARY_LOADING_ERROR": "加载此收藏库时出现了问题。"
}
},
"LIBRARY_NO_PERMISSIONS_WARNING": "您无权查看此收藏库。"
},
"SHARED": {
"TITLE": "共享文件",

View File

@ -548,7 +548,7 @@ describe('FilesComponent', () => {
);
fixture.detectChanges();
expect(getGenericErrorText()).toBe('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NO_PERMISSIONS');
expect(getGenericErrorText()).toBe('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
});
it('should have set text to library not found error if library does not exist and actual url is libraries', () => {
@ -638,7 +638,7 @@ describe('FilesComponent', () => {
);
fixture.detectChanges();
expect(getGenericErrorText()).toBe('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NO_PERMISSIONS');
expect(getGenericErrorText()).toBe('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
});
it('should have set text to library not found error if library does not exist issue and actual url is libraries', () => {

View File

@ -409,7 +409,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
if (this.router.url.includes('libraries')) {
switch (error.status) {
case 403:
this._errorTranslationKey = 'APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NO_PERMISSIONS';
this._errorTranslationKey = 'APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING';
break;
case 404:
this._errorTranslationKey = 'APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NOT_FOUND';

View File

@ -25,28 +25,30 @@
import { LibraryMetadataFormComponent } from './library-metadata-form.component';
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { SnackbarAction, SnackbarErrorAction, SnackbarInfoAction, UpdateLibraryAction } from '@alfresco/aca-shared/store';
import { UpdateLibraryAction } from '@alfresco/aca-shared/store';
import { AppHookService } from '@alfresco/aca-shared';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Site, SiteBodyCreate, SitePaging } from '@alfresco/js-api';
import { Actions } from '@ngrx/effects';
import { Site, SiteBodyCreate, SiteEntry, SitePaging } from '@alfresco/js-api';
import { of, Subject } from 'rxjs';
describe('LibraryMetadataFormComponent', () => {
let fixture: ComponentFixture<LibraryMetadataFormComponent>;
let component: LibraryMetadataFormComponent;
let store: Store<any>;
let actions$: Subject<SnackbarAction>;
let siteEntryModel: SiteBodyCreate;
let appHookService: AppHookService;
beforeEach(() => {
actions$ = new Subject<SnackbarAction>();
TestBed.configureTestingModule({
imports: [AppTestingModule, LibraryMetadataFormComponent],
providers: [
{
provide: Actions,
useValue: actions$
provide: AppHookService,
useValue: {
libraryUpdated: new Subject<SiteEntry>(),
libraryUpdateFailed: new Subject<void>()
}
},
{
provide: Store,
@ -59,6 +61,7 @@ describe('LibraryMetadataFormComponent', () => {
schemas: [NO_ERRORS_SCHEMA]
});
appHookService = TestBed.inject(AppHookService);
store = TestBed.inject(Store);
fixture = TestBed.createComponent(LibraryMetadataFormComponent);
@ -120,24 +123,10 @@ describe('LibraryMetadataFormComponent', () => {
component.ngOnInit();
component.form.setValue(entry);
actions$.next(new SnackbarInfoAction('LIBRARY.SUCCESS.LIBRARY_UPDATED'));
appHookService.libraryUpdated.next({ entry: entry } as SiteEntry);
expect(component.node.entry).toEqual(jasmine.objectContaining(entry));
});
it('should not assign form value to node entry if info snackbar was displayed for different action than updating library', () => {
const entry = {
id: 'libraryId',
title: 'some different title',
description: 'some different description',
visibility: Site.VisibilityEnum.PUBLIC
} as Site;
component.ngOnInit();
component.form.setValue(entry);
actions$.next(new SnackbarInfoAction('Some different action'));
expect(component.node.entry).not.toEqual(jasmine.objectContaining(entry));
});
it('should call markAsDirty on form if updating of form is finished with error', () => {
component.node = {
entry: {
@ -151,18 +140,10 @@ describe('LibraryMetadataFormComponent', () => {
component.ngOnInit();
spyOn(component.form, 'markAsDirty');
actions$.next(new SnackbarErrorAction('LIBRARY.ERRORS.LIBRARY_UPDATE_ERROR'));
appHookService.libraryUpdateFailed.next();
expect(component.form.markAsDirty).toHaveBeenCalled();
});
it('should not call markAsDirty on form if error snackbar was displayed for different action than updating library', () => {
component.ngOnInit();
spyOn(component.form, 'markAsDirty');
actions$.next(new SnackbarErrorAction('Some different action'));
expect(component.form.markAsDirty).not.toHaveBeenCalled();
});
it('should update library node if form is valid', () => {
component.node.entry.role = Site.RoleEnum.SiteManager;

View File

@ -36,16 +36,9 @@ import {
} from '@angular/forms';
import { QueriesApi, SiteEntry, SitePaging } from '@alfresco/js-api';
import { Store } from '@ngrx/store';
import {
AppStore,
isAdmin,
SnackbarAction,
SnackbarActionTypes,
SnackbarErrorAction,
SnackbarInfoAction,
UpdateLibraryAction
} from '@alfresco/aca-shared/store';
import { debounceTime, filter, mergeMap } from 'rxjs/operators';
import { AppStore, isAdmin, UpdateLibraryAction } from '@alfresco/aca-shared/store';
import { AppHookService } from '@alfresco/aca-shared';
import { debounceTime, mergeMap } from 'rxjs/operators';
import { AlfrescoApiService } from '@alfresco/adf-content-services';
import { from, Observable } from 'rxjs';
import { ErrorStateMatcher, MatOptionModule } from '@angular/material/core';
@ -57,7 +50,6 @@ import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { A11yModule } from '@angular/cdk/a11y';
import { MatButtonModule } from '@angular/material/button';
import { Actions, ofType } from '@ngrx/effects';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class InstantErrorStateMatcher implements ErrorStateMatcher {
@ -127,7 +119,7 @@ export class LibraryMetadataFormComponent implements OnInit, OnChanges {
constructor(
private readonly alfrescoApiService: AlfrescoApiService,
protected readonly store: Store<AppStore>,
private readonly actions$: Actions
private readonly appHookService: AppHookService
) {}
toggleEdit() {
@ -186,10 +178,12 @@ export class LibraryMetadataFormComponent implements OnInit, OnChanges {
this.isAdmin = value;
});
this.canUpdateLibrary = this.node?.entry?.role === 'SiteManager' || this.isAdmin;
this.handleUpdatingEvent<SnackbarInfoAction>(SnackbarActionTypes.Info, 'LIBRARY.SUCCESS.LIBRARY_UPDATED', () =>
Object.assign(this.node.entry, this.form.value)
);
this.handleUpdatingEvent<SnackbarErrorAction>(SnackbarActionTypes.Error, 'LIBRARY.ERRORS.LIBRARY_UPDATE_ERROR', () => this.form.markAsDirty());
this.appHookService.libraryUpdated.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
Object.assign(this.node.entry, this.form.value);
});
this.appHookService.libraryUpdateFailed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.form.markAsDirty();
});
}
ngOnChanges() {
@ -231,16 +225,6 @@ export class LibraryMetadataFormComponent implements OnInit, OnChanges {
);
}
private handleUpdatingEvent<T extends SnackbarAction>(actionType: SnackbarActionTypes, payload: string, handle: () => void): void {
this.actions$
.pipe(
ofType<T>(actionType),
filter((action) => action.payload === payload),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(handle);
}
private validateEmptyName(control: FormControl<string>): ValidationErrors {
return control.value.length && !control.value.trim() ? { empty: true } : null;
}

View File

@ -26,14 +26,14 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin
import { SearchResultsComponent } from './search-results.component';
import { AppConfigService, NotificationService, TranslationService } from '@alfresco/adf-core';
import { Store } from '@ngrx/store';
import { NavigateToFolder, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
import { NavigateToFolder } from '@alfresco/aca-shared/store';
import { Pagination, SearchRequest } from '@alfresco/js-api';
import { SavedSearchesService, SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, of, Subject, throwError } from 'rxjs';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { AppService } from '@alfresco/aca-shared';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSnackBarModule, MatSnackBarRef } from '@angular/material/snack-bar';
import { Buffer } from 'buffer';
import { testHeader } from '../../../testing/document-base-page-utils';
import { HarnessLoader } from '@angular/cdk/testing';
@ -53,7 +53,8 @@ describe('SearchComponent', () => {
let route: ActivatedRoute;
const searchRequest = {} as SearchRequest;
let params: BehaviorSubject<any>;
let showErrorSpy: jasmine.Spy;
let showErrorSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
let showInfoSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
let loader: HarnessLoader;
const editSavedSearchesSpy = jasmine.createSpy('editSavedSearch');
@ -109,6 +110,7 @@ describe('SearchComponent', () => {
const notificationService = TestBed.inject(NotificationService);
showErrorSpy = spyOn(notificationService, 'showError');
showInfoSpy = spyOn(notificationService, 'showInfo');
config.config = {
search: {}
@ -290,19 +292,17 @@ describe('SearchComponent', () => {
}));
it('should dispatch success snackbar action when editing saved search is successful', fakeAsync(() => {
spyOn(store, 'dispatch').and.stub();
editSavedSearchesSpy.and.returnValue(of({}));
component.editSavedSearch({ name: 'test', encodedUrl: 'test', order: 0 });
tick();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
expect(showInfoSpy).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE');
}));
it('should dispatch error snackbar action when editing saved search failed', fakeAsync(() => {
spyOn(store, 'dispatch').and.stub();
editSavedSearchesSpy.and.returnValue(throwError(() => new Error('')));
component.editSavedSearch({ name: 'test', encodedUrl: 'test', order: 0 });
tick();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
expect(showErrorSpy).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE');
}));
testHeader(SearchResultsComponent, false);

View File

@ -43,9 +43,7 @@ import {
SetInfoDrawerPreviewStateAction,
SetInfoDrawerStateAction,
SetSearchItemsTotalCountAction,
ShowInfoDrawerPreviewAction,
SnackbarErrorAction,
SnackbarInfoAction
ShowInfoDrawerPreviewAction
} from '@alfresco/aca-shared/store';
import {
CustomEmptyContentTemplateDirective,
@ -333,10 +331,10 @@ export class SearchResultsComponent extends PageComponent implements OnInit {
.pipe(take(1))
.subscribe({
next: () => {
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
this.notificationService.showInfo('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE');
},
error: () => {
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
this.notificationService.showError('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE');
}
});
}

View File

@ -24,18 +24,16 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { of } from 'rxjs';
import { of, throwError } 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 { NotificationService } from '@alfresco/adf-core';
import { AppTestingModule } from '../../../../../testing/app-testing.module';
describe('SaveSearchDeleteDialogComponent', () => {
let fixture: ComponentFixture<SavedSearchDeleteDialogComponent>;
let notificationService: NotificationService;
let savedSearchesService: SavedSearchesService;
let store: Store;
let submitButton: HTMLButtonElement;
let cancelButton: HTMLButtonElement;
@ -54,15 +52,14 @@ describe('SaveSearchDeleteDialogComponent', () => {
imports: [ContentTestingModule, AppTestingModule],
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
provideMockStore(),
{ provide: SavedSearchesService, useValue: { deleteSavedSearch: () => of() } },
{ 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);
notificationService = TestBed.inject(NotificationService);
submitButton = fixture.nativeElement.querySelector('#aca-save-search-delete-dialog-submit-button');
cancelButton = fixture.nativeElement.querySelector('#aca-save-search-delete-dialog-cancel-button');
@ -79,17 +76,19 @@ describe('SaveSearchDeleteDialogComponent', () => {
expect(dialogRef.close).toHaveBeenCalled();
});
it('should delete search, show snackbar message and close modal if submit button clicked', fakeAsync(() => () => {
it('should delete search, show snackbar message and close modal if submit button clicked', fakeAsync(() => {
spyOn(savedSearchesService, 'deleteSavedSearch').and.callThrough();
spyOn(notificationService, 'showInfo');
clickSubmitButton();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.SUCCESS_MESSAGE'));
expect(notificationService.showInfo).toHaveBeenCalledWith('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('');
it('should show snackbar error if there is delete error', fakeAsync(() => {
spyOn(savedSearchesService, 'deleteSavedSearch').and.returnValue(throwError(() => new Error('Error')));
spyOn(notificationService, 'showError');
clickSubmitButton();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.SUCCESS_MESSAGE'));
expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.ERROR_MESSAGE');
expect(dialogRef.close).not.toHaveBeenCalled();
}));

View File

@ -26,9 +26,7 @@ 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';
import { CoreModule, NotificationService } from '@alfresco/adf-core';
@Component({
standalone: true,
@ -44,8 +42,8 @@ export class SavedSearchDeleteDialogComponent {
constructor(
private readonly dialog: MatDialogRef<SavedSearchDeleteDialogComponent>,
private readonly notificationService: NotificationService,
private readonly savedSearchesService: SavedSearchesService,
private readonly store: Store<AppStore>,
@Inject(MAT_DIALOG_DATA) private readonly data: SavedSearch
) {}
@ -60,11 +58,11 @@ export class SavedSearchDeleteDialogComponent {
.subscribe({
next: () => {
this.dialog.close(this.data);
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.SUCCESS_MESSAGE'));
this.notificationService.showInfo('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.notificationService.showError('APP.BROWSE.SEARCH.SAVE_SEARCH.DELETE_DIALOG.ERROR_MESSAGE');
this.isLoading = false;
}
});

View File

@ -24,19 +24,18 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { of } from 'rxjs';
import { of, throwError } 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 { NotificationService } from '@alfresco/adf-core';
import { AppTestingModule } from '../../../../../testing/app-testing.module';
describe('SaveSearchEditDialogComponent', () => {
let fixture: ComponentFixture<SavedSearchEditDialogComponent>;
let component: SavedSearchEditDialogComponent;
let notificationService: NotificationService;
let savedSearchesService: SavedSearchesService;
let store: Store;
let submitButton: HTMLButtonElement;
const savedSearchToDelete: SavedSearch = {
@ -55,15 +54,15 @@ describe('SaveSearchEditDialogComponent', () => {
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
provideMockStore(),
{ provide: SavedSearchesService, useValue: { editSavedSearch: () => of(), getSavedSearches: () => of([]) } },
{ provide: SavedSearchesService, useValue: { editSavedSearch: () => of({}), getSavedSearches: () => of([]) } },
{ provide: MAT_DIALOG_DATA, useValue: savedSearchToDelete }
]
});
dialogRef.close.calls.reset();
fixture = TestBed.createComponent(SavedSearchEditDialogComponent);
component = fixture.componentInstance;
notificationService = TestBed.inject(NotificationService);
savedSearchesService = TestBed.inject(SavedSearchesService);
store = TestBed.inject(Store);
submitButton = fixture.nativeElement.querySelector('#aca-saved-search-edit-dialog-submit-button');
});
@ -80,22 +79,23 @@ describe('SaveSearchEditDialogComponent', () => {
expect(savedSearchesService.editSavedSearch).not.toHaveBeenCalled();
}));
it('should save search, show snackbar message and close modal if form is valid', fakeAsync(() => () => {
it('should save search, show snackbar message and close modal if form is valid', fakeAsync(() => {
spyOn(savedSearchesService, 'editSavedSearch').and.callThrough();
spyOn(notificationService, 'showInfo');
setFormValuesAndSubmit();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
expect(notificationService.showInfo).toHaveBeenCalledWith('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('');
it('should show snackbar error if there is save error', fakeAsync(() => {
spyOn(savedSearchesService, 'editSavedSearch').and.returnValue(throwError(() => new Error('Error')));
spyOn(notificationService, 'showError');
setFormValuesAndSubmit();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
expect(notificationService.showError).toHaveBeenCalledWith('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();

View File

@ -26,9 +26,7 @@ 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 { CoreModule, NotificationService } from '@alfresco/adf-core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { UniqueSearchNameValidator } from '../unique-search-name-validator';
import { SavedSearchForm } from '../saved-search-form.interface';
@ -48,7 +46,7 @@ export class SavedSearchEditDialogComponent {
constructor(
private readonly dialog: MatDialogRef<SavedSearchEditDialogComponent>,
private readonly store: Store<AppStore>,
private readonly notificationService: NotificationService,
private readonly savedSearchesService: SavedSearchesService,
private readonly uniqueSearchNameValidator: UniqueSearchNameValidator,
@Inject(MAT_DIALOG_DATA) private readonly data: SavedSearch
@ -92,7 +90,7 @@ export class SavedSearchEditDialogComponent {
this.isLoading = false;
},
error: () => {
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE'));
this.notificationService.showError('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.ERROR_MESSAGE');
this.isLoading = false;
}
});
@ -100,6 +98,6 @@ export class SavedSearchEditDialogComponent {
private onEditSuccess(): void {
this.dialog.close();
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE'));
this.notificationService.showInfo('APP.BROWSE.SEARCH.SAVE_SEARCH.EDIT_DIALOG.SUCCESS_MESSAGE');
}
}

View File

@ -24,19 +24,18 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { of } from 'rxjs';
import { of, throwError } from 'rxjs';
import { SaveSearchDialogComponent } from './save-search-dialog.component';
import { ContentTestingModule, SavedSearchesService } from '@alfresco/adf-content-services';
import { provideMockStore } from '@ngrx/store/testing';
import { AppTestingModule } from '../../../../testing/app-testing.module';
import { Store } from '@ngrx/store';
import { SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
import { NotificationService } from '@alfresco/adf-core';
describe('SaveSearchDialogComponent', () => {
let fixture: ComponentFixture<SaveSearchDialogComponent>;
let component: SaveSearchDialogComponent;
let notificationService: NotificationService;
let savedSearchesService: SavedSearchesService;
let store: Store;
let submitButton: HTMLButtonElement;
const dialogRef = {
@ -49,15 +48,15 @@ describe('SaveSearchDialogComponent', () => {
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
provideMockStore(),
{ provide: SavedSearchesService, useValue: { saveSearch: () => of(), getSavedSearches: () => of([]) } },
{ provide: SavedSearchesService, useValue: { saveSearch: () => of({}), getSavedSearches: () => of([]) } },
{ provide: MAT_DIALOG_DATA, useValue: { searchUrl: 'abcdef' } }
]
});
dialogRef.close.calls.reset();
fixture = TestBed.createComponent(SaveSearchDialogComponent);
component = fixture.componentInstance;
notificationService = TestBed.inject(NotificationService);
savedSearchesService = TestBed.inject(SavedSearchesService);
store = TestBed.inject(Store);
submitButton = fixture.nativeElement.querySelector('#aca-save-search-dialog-save-button');
});
@ -75,22 +74,22 @@ describe('SaveSearchDialogComponent', () => {
expect(savedSearchesService.saveSearch).not.toHaveBeenCalled();
});
it('should save search, show snackbar message and close modal if form is valid', fakeAsync(() => () => {
it('should save search, show snackbar message and close modal if form is valid', fakeAsync(() => {
spyOn(savedSearchesService, 'saveSearch').and.callThrough();
spyOn(notificationService, 'showInfo');
setFormValuesAndSubmit();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS'));
expect(notificationService.showInfo).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS');
expect(dialogRef.close).toHaveBeenCalled();
}));
it('should show snackbar error if there is save error', fakeAsync(() => () => {
spyOn(savedSearchesService, 'saveSearch').and.throwError('');
it('should show snackbar error if there is save error', fakeAsync(() => {
spyOn(savedSearchesService, 'saveSearch').and.returnValue(throwError(() => new Error('Error')));
spyOn(notificationService, 'showError');
setFormValuesAndSubmit();
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_ERROR'));
expect(dialogRef.close).not.toHaveBeenCalled();
expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_ERROR');
}));
function setFormValuesAndSubmit() {
spyOn(store, 'dispatch');
component.form.controls['name'].setValue('ABCDEF');
component.form.controls['description'].setValue('TEST');
submitButton.click();

View File

@ -34,12 +34,10 @@ import { MatInputModule } from '@angular/material/input';
import { A11yModule } from '@angular/cdk/a11y';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { FormControl, FormGroup, FormsModule, Validators } from '@angular/forms';
import { CoreModule } from '@alfresco/adf-core';
import { CoreModule, NotificationService } from '@alfresco/adf-core';
import { AutoFocusDirective, forbidOnlySpaces, SavedSearchesService } from '@alfresco/adf-content-services';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppStore, SnackbarErrorAction, SnackbarInfoAction } from '@alfresco/aca-shared/store';
import { UniqueSearchNameValidator } from './unique-search-name-validator';
import { SavedSearchForm } from './saved-search-form.interface';
@ -72,7 +70,7 @@ export class SaveSearchDialogComponent {
constructor(
private readonly dialog: MatDialogRef<SaveSearchDialogComponent>,
private readonly store: Store<AppStore>,
private readonly notificationService: NotificationService,
private readonly savedSearchesService: SavedSearchesService,
private readonly uniqueSearchNameValidator: UniqueSearchNameValidator,
@Inject(MAT_DIALOG_DATA) private readonly data: { searchUrl: string }
@ -100,11 +98,11 @@ export class SaveSearchDialogComponent {
.subscribe({
next: () => {
this.dialog.close();
this.store.dispatch(new SnackbarInfoAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS'));
this.notificationService.showInfo('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_SUCCESS');
this.disableSubmitButton = false;
},
error: () => {
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_ERROR'));
this.notificationService.showError('APP.BROWSE.SEARCH.SAVE_SEARCH.SAVE_ERROR');
this.disableSubmitButton = false;
}
});

View File

@ -23,12 +23,11 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, DataCellEvent, DataTableComponent } from '@alfresco/adf-core';
import { CoreTestingModule, DataCellEvent, DataTableComponent, NotificationService } 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';
import { Router } from '@angular/router';
@ -36,6 +35,7 @@ import { Router } from '@angular/router';
describe('SavedSearchesListUiComponent ', () => {
let fixture: ComponentFixture<SavedSearchesListUiComponent>;
let component: SavedSearchesListUiComponent;
let notificationService: NotificationService;
let savedSearchesListUiService: SavedSearchesListUiService;
let clipboard: Clipboard;
let router: Router;
@ -43,9 +43,10 @@ describe('SavedSearchesListUiComponent ', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule, SavedSearchesListUiComponent],
providers: [SavedSearchesListUiService, provideMockStore()]
providers: [SavedSearchesListUiService]
});
notificationService = TestBed.inject(NotificationService);
savedSearchesListUiService = TestBed.inject(SavedSearchesListUiService);
clipboard = TestBed.inject(Clipboard);
router = TestBed.inject(Router);
@ -196,6 +197,7 @@ describe('SavedSearchesListUiComponent ', () => {
let actionData: any;
beforeEach(() => {
spyOn(notificationService, 'showInfo');
actionData = dataCellEvent.value.actions[0];
actionData.subject.next(actionData);
});
@ -203,6 +205,10 @@ describe('SavedSearchesListUiComponent ', () => {
it('should call copy to clipboard when selected delete option', () => {
expect(clipboard.copy).toHaveBeenCalled();
});
it('should show snackbar message when selected delete option', () => {
expect(notificationService.showInfo).toHaveBeenCalledWith('APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD_SUCCESS');
});
});
describe('Execute search', () => {

View File

@ -29,6 +29,7 @@ import {
DATATABLE_DIRECTIVES,
DataTableComponent,
DataTableSchema,
NotificationService,
ShowHeaderMode,
TEMPLATE_DIRECTIVES
} from '@alfresco/adf-core';
@ -39,8 +40,6 @@ 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';
import { Router } from '@angular/router';
@Component({
@ -61,6 +60,7 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
readonly ShowHeaderMode = ShowHeaderMode;
private readonly notificationService = inject(NotificationService);
private readonly savedSearchesListUiService = inject(SavedSearchesListUiService);
private readonly destroyRef = inject(DestroyRef);
private readonly contextMenuAction$ = new Subject<any>();
@ -94,7 +94,6 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
constructor(
protected appConfig: AppConfigService,
private readonly clipboard: Clipboard,
private readonly store: Store,
private readonly router: Router
) {
super(appConfig, '', savedSearchesListSchema);
@ -140,7 +139,7 @@ export class SavedSearchesListUiComponent extends DataTableSchema implements Aft
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'));
this.notificationService.showInfo('APP.BROWSE.SEARCH.SAVE_SEARCH.LIST.COPY_TO_CLIPBOARD_SUCCESS');
}
executeSearch(savedSearch: SavedSearch): void {

View File

@ -24,7 +24,7 @@
import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { BehaviorSubject, EMPTY, of, Subject, throwError } from 'rxjs';
import { Actions, EffectsModule, ofType } from '@ngrx/effects';
import { EffectsModule } from '@ngrx/effects';
import {
AppStore,
CopyNodesAction,
@ -32,21 +32,15 @@ import {
MoveNodesAction,
NavigateRouteAction,
NavigateToParentFolder,
NodeActionTypes,
PurgeDeletedNodesAction,
RefreshPreviewAction,
RestoreDeletedNodesAction,
SetSelectedNodesAction,
ShareNodeAction,
SnackbarActionTypes,
SnackbarErrorAction,
SnackbarInfoAction,
SnackbarWarningAction,
UnlockWriteAction,
ViewNodeExtras,
ViewNodeVersionAction
} from '@alfresco/aca-shared/store';
import { map } from 'rxjs/operators';
import { NodeEffects } from '../store/effects/node.effects';
import { AppTestingModule } from '../testing/app-testing.module';
import { AppHookService, AppSettingsService, ContentApiService } from '@alfresco/aca-shared';
@ -56,7 +50,7 @@ import { NodeActionsService } from './node-actions.service';
import { ConfirmDialogComponent, DialogComponent, DialogSize, NotificationService, TranslationService } from '@alfresco/adf-core';
import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBarModule, MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';
import { Node, NodeEntry, UserInfo, VersionPaging } from '@alfresco/js-api';
import { Node, NodeEntry, SiteBodyCreate, SiteEntry, UserInfo, VersionPaging } from '@alfresco/js-api';
import {
DocumentListService,
FileModel,
@ -71,7 +65,6 @@ import { FolderInformationComponent } from '../dialogs/folder-details/folder-inf
describe('ContentManagementService', () => {
let dialog: MatDialog;
let actions$: Actions;
let contentApi: ContentApiService;
let store: Store<AppStore>;
let contentManagementService: ContentManagementService;
@ -84,9 +77,10 @@ describe('ContentManagementService', () => {
let appHookService: AppHookService;
let newVersionUploaderService: NewVersionUploaderService;
let appSettingsService: AppSettingsService;
let showErrorSpy: jasmine.Spy;
let showInfoSpy: jasmine.Spy;
let showWarningSpy: jasmine.Spy;
let showErrorSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
let showInfoSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
let showWarningSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
let openSnackMessageActionSpy: jasmine.Spy<(message: string, action?: string, interpolateArgs?: any, showAction?: boolean) => MatSnackBarRef<any>>;
beforeEach(() => {
TestBed.configureTestingModule({
@ -94,13 +88,13 @@ describe('ContentManagementService', () => {
});
contentApi = TestBed.inject(ContentApiService);
actions$ = TestBed.inject(Actions);
store = TestBed.inject(Store);
contentManagementService = TestBed.inject(ContentManagementService);
notificationService = TestBed.inject(NotificationService);
showErrorSpy = spyOn(notificationService, 'showError');
showInfoSpy = spyOn(notificationService, 'showInfo');
showWarningSpy = spyOn(notificationService, 'showWarning');
openSnackMessageActionSpy = spyOn(notificationService, 'openSnackMessageAction');
nodeActions = TestBed.inject(NodeActionsService);
documentListService = TestBed.inject(DocumentListService);
translationService = TestBed.inject(TranslationService);
@ -118,7 +112,7 @@ describe('ContentManagementService', () => {
beforeEach(() => {
subject = new Subject<string>();
spyOn(notificationService, 'openSnackMessageAction').and.callThrough();
openSnackMessageActionSpy.and.callThrough();
});
afterEach(() => subject.complete());
@ -132,9 +126,11 @@ describe('ContentManagementService', () => {
store.dispatch(new CopyNodesAction(selection));
nodeActions.contentCopied.next(createdItems);
subject.next('OPERATION.SUCCESS.CONTENT.COPY');
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(snackMessageCall[2].panelClass).toBe('adf-info-snackbar');
});
it('notifies successful copy of multiple nodes', () => {
@ -146,9 +142,11 @@ describe('ContentManagementService', () => {
store.dispatch(new CopyNodesAction(selection));
nodeActions.contentCopied.next(createdItems);
subject.next('OPERATION.SUCCESS.CONTENT.COPY');
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
expect(snackMessageCall[2].panelClass).toBe('adf-info-snackbar');
});
it('notifies partially copy of one node out of a multiple selection of nodes', () => {
@ -160,9 +158,11 @@ describe('ContentManagementService', () => {
store.dispatch(new CopyNodesAction(selection));
nodeActions.contentCopied.next(createdItems);
subject.next('OPERATION.SUCCESS.CONTENT.COPY');
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_SINGULAR');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_SINGULAR');
expect(snackMessageCall[2].panelClass).toBe('adf-warning-snackbar');
});
it('notifies partially copy of more nodes out of a multiple selection of nodes', () => {
@ -178,9 +178,11 @@ describe('ContentManagementService', () => {
store.dispatch(new CopyNodesAction(selection));
nodeActions.contentCopied.next(createdItems);
subject.next('OPERATION.SUCCESS.CONTENT.COPY');
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_PLURAL');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_PLURAL');
expect(snackMessageCall[2].panelClass).toBe('adf-warning-snackbar');
});
it('notifies of failed copy of multiple nodes', () => {
@ -196,9 +198,11 @@ describe('ContentManagementService', () => {
store.dispatch(new CopyNodesAction(selection));
nodeActions.contentCopied.next(createdItems);
subject.next('OPERATION.SUCCESS.CONTENT.COPY');
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_PLURAL');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_PLURAL');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies of failed copy of one node', () => {
@ -210,9 +214,11 @@ describe('ContentManagementService', () => {
store.dispatch(new CopyNodesAction(selection));
nodeActions.contentCopied.next(createdItems);
subject.next('OPERATION.SUCCESS.CONTENT.COPY');
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_SINGULAR');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_SINGULAR');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies error if success message was not emitted', () => {
@ -221,11 +227,13 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: 'node-to-copy-id', name: 'name' } }];
store.dispatch(new CopyNodesAction(selection));
nodeActions.contentCopied.next({} as any);
subject.next('');
nodeActions.contentCopied.next([] as any);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies permission error on copy of node', () => {
@ -234,9 +242,11 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name' } }];
store.dispatch(new CopyNodesAction(selection));
subject.error(new Error(JSON.stringify({ error: { statusCode: 403 } })));
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies generic error message on all errors, but 403', () => {
@ -246,9 +256,11 @@ describe('ContentManagementService', () => {
store.dispatch(new CopyNodesAction(selection));
subject.error(new Error(JSON.stringify({ error: { statusCode: 404 } })));
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
});
@ -259,7 +271,7 @@ describe('ContentManagementService', () => {
subject = new Subject<string>();
spyOn(nodeActions, 'copyNodes').and.returnValue(subject);
spyOn(notificationService, 'openSnackMessageAction').and.returnValue({
openSnackMessageActionSpy.and.returnValue({
onAction: () => of(null)
} as MatSnackBarRef<SimpleSnackBar>);
});
@ -275,7 +287,7 @@ describe('ContentManagementService', () => {
nodeActions.contentCopied.next(createdItems);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(contentApi.deleteNode).toHaveBeenCalledWith(createdItems[0].entry.id, { permanent: true });
});
@ -312,7 +324,7 @@ describe('ContentManagementService', () => {
nodeActions.contentCopied.next(createdItems);
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL');
expect(spyOnDeleteNode).toHaveBeenCalled();
expect(spyOnDeleteNode.calls.allArgs()).toEqual([
@ -333,7 +345,7 @@ describe('ContentManagementService', () => {
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(contentApi.deleteNode).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
});
it('notifies when some error of type Error occurs on Undo action', () => {
@ -348,7 +360,7 @@ describe('ContentManagementService', () => {
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(contentApi.deleteNode).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
});
it('notifies permission error when it occurs on Undo action', () => {
@ -363,7 +375,7 @@ describe('ContentManagementService', () => {
expect(nodeActions.copyNodes).toHaveBeenCalled();
expect(contentApi.deleteNode).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR');
});
});
@ -386,7 +398,7 @@ describe('ContentManagementService', () => {
beforeEach(() => {
subject = new Subject<string>();
spyOn(notificationService, 'openSnackMessageAction').and.callThrough();
openSnackMessageActionSpy.and.callThrough();
});
afterEach(() => subject.complete());
@ -406,9 +418,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(selection));
nodeActions.moveNodes(null).next('OPERATION.SUCCESS.CONTENT.MOVE');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
expect(snackMessageCall[2].panelClass).toBe('adf-info-snackbar');
});
it('notifies successful move of multiple nodes', () => {
@ -427,9 +441,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(selection));
nodeActions.moveNodes(null).next('OPERATION.SUCCESS.CONTENT.MOVE');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PLURAL');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PLURAL');
expect(snackMessageCall[2].panelClass).toBe('adf-info-snackbar');
});
it('notifies partial move of a node', () => {
@ -446,9 +462,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(nodes));
nodeActions.moveNodes(null).next('OPERATION.SUCCESS.CONTENT.MOVE');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR');
expect(snackMessageCall[2].panelClass).toBe('adf-warning-snackbar');
});
it('notifies partial move of multiple nodes', () => {
@ -465,9 +483,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(nodes));
nodeActions.moveNodes(null).next('OPERATION.SUCCESS.CONTENT.MOVE');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.PLURAL');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.PLURAL');
expect(snackMessageCall[2].panelClass).toBe('adf-warning-snackbar');
});
it('notifies successful move and the number of nodes that could not be moved', () => {
@ -484,11 +504,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(nodes));
nodeActions.moveNodes(null).next('OPERATION.SUCCESS.CONTENT.MOVE');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe(
'APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.FAIL'
);
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.FAIL');
expect(snackMessageCall[2].panelClass).toBe('adf-warning-snackbar');
});
it('notifies successful move and the number of partially moved ones', () => {
@ -505,11 +525,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(nodes));
nodeActions.moveNodes(null).next('OPERATION.SUCCESS.CONTENT.MOVE');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe(
'APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR'
);
expect(snackMessageCall[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR');
expect(snackMessageCall[2].panelClass).toBe('adf-warning-snackbar');
});
it('notifies error if success message was not emitted', () => {
@ -525,9 +545,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(nodes));
nodeActions.moveNodes(null).next('');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies permission error on move of node', () => {
@ -536,9 +558,11 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name' } }];
store.dispatch(new MoveNodesAction(selection));
nodeActions.moveNodes(null).error(new Error(JSON.stringify({ error: { statusCode: 403 } })));
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies generic error message on all errors, but 403', () => {
@ -547,9 +571,11 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name' } }];
store.dispatch(new MoveNodesAction(selection));
nodeActions.moveNodes(null).error(new Error(JSON.stringify({ error: { statusCode: 404 } })));
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies conflict error message on 409', () => {
@ -558,9 +584,11 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name' } }];
store.dispatch(new MoveNodesAction(selection));
nodeActions.moveNodes(null).error(new Error(JSON.stringify({ error: { statusCode: 409 } })));
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.NODE_MOVE');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.NODE_MOVE');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
it('notifies error if move response has only failed items', () => {
@ -577,9 +605,11 @@ describe('ContentManagementService', () => {
store.dispatch(new MoveNodesAction(nodes));
nodeActions.moveNodes(null).next('OPERATION.SUCCESS.CONTENT.MOVE');
nodeActions.contentMoved.next(moveResponse);
const snackMessageCall = openSnackMessageActionSpy.calls.argsFor(0);
expect(nodeActions.moveNodes).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[0]).toBe('APP.MESSAGES.ERRORS.GENERIC');
expect(snackMessageCall[2].panelClass).toBe('adf-error-snackbar');
});
});
@ -604,8 +634,8 @@ describe('ContentManagementService', () => {
subject = new Subject<string>();
spyOn(nodeActions, 'moveNodes').and.returnValue(subject);
spyOn(notificationService, 'openSnackMessageAction').and.returnValue({
onAction: () => of(null)
openSnackMessageActionSpy.and.returnValue({
onAction: () => of(undefined)
} as MatSnackBarRef<SimpleSnackBar>);
});
@ -630,7 +660,7 @@ describe('ContentManagementService', () => {
nodeActions.contentMoved.next(movedItems);
expect(nodeActions.moveNodeAction).toHaveBeenCalledWith(movedItems.succeeded[0].itemMoved.entry, movedItems.succeeded[0].initialParentId);
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
});
it('should move node back to initial parent, after succeeded move of a single file', () => {
@ -658,7 +688,7 @@ describe('ContentManagementService', () => {
nodeActions.contentMoved.next(movedItems);
expect(nodeActions.moveNodeAction).toHaveBeenCalledWith(node.entry, initialParent);
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
});
it('should restore deleted folder back to initial parent, after succeeded moving all its files', () => {
@ -690,17 +720,12 @@ describe('ContentManagementService', () => {
nodeActions.contentMoved.next(movedItems);
expect(contentApi.restoreNode).toHaveBeenCalled();
expect(notificationService.openSnackMessageAction['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
expect(openSnackMessageActionSpy['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR');
});
it('should notify when error occurs on Undo Move action', fakeAsync((done) => {
it('should notify when error occurs on Undo Move action', fakeAsync(() => {
spyOn(contentApi, 'restoreNode').and.returnValue(throwError(null));
actions$.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map(() => done())
);
const initialParent = 'parent-id-0';
const node = {
entry: {
@ -734,14 +759,9 @@ describe('ContentManagementService', () => {
expect(contentApi.restoreNode).toHaveBeenCalled();
}));
it('should notify when some error of type Error occurs on Undo Move action', fakeAsync((done) => {
it('should notify when some error of type Error occurs on Undo Move action', fakeAsync(() => {
spyOn(contentApi, 'restoreNode').and.returnValue(throwError(new Error('oops!')));
actions$.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map(() => done())
);
const initialParent = 'parent-id-0';
const node: any = {
entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent }
@ -766,14 +786,9 @@ describe('ContentManagementService', () => {
expect(contentApi.restoreNode).toHaveBeenCalled();
}));
it('should notify permission error when it occurs on Undo Move action', fakeAsync((done) => {
it('should notify permission error when it occurs on Undo Move action', fakeAsync(() => {
spyOn(contentApi, 'restoreNode').and.returnValue(throwError(new Error(JSON.stringify({ error: { statusCode: 403 } }))));
actions$.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map(() => done())
);
const initialParent = 'parent-id-0';
const node = {
entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent }
@ -801,67 +816,52 @@ describe('ContentManagementService', () => {
});
describe('Delete action', () => {
it('should raise info message on successful single file deletion', (done) => {
beforeEach(() => {
openSnackMessageActionSpy.and.returnValue({
onAction: () => of(null)
} as MatSnackBarRef<SimpleSnackBar>);
});
it('should raise info message on successful single file deletion', () => {
spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
actions$
.pipe(
ofType<SnackbarInfoAction>(SnackbarActionTypes.Info),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }];
store.dispatch(new DeleteNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-info-snackbar');
});
it('should raise error message on failed single file deletion', (done) => {
spyOn(contentApi, 'deleteNode').and.returnValue(throwError(null));
actions$
.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }];
store.dispatch(new DeleteNodesAction(selection));
setTimeout(() => {
expect(showErrorSpy).toHaveBeenCalled();
done();
});
});
it('should raise info message on successful multiple files deletion', (done) => {
it('should raise info message on successful multiple files deletion', () => {
spyOn(contentApi, 'deleteNode').and.returnValue(of(null));
actions$
.pipe(
ofType<SnackbarInfoAction>(SnackbarActionTypes.Info),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } }];
store.dispatch(new DeleteNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-info-snackbar');
});
it('should raise error message failed multiple files deletion', (done) => {
spyOn(contentApi, 'deleteNode').and.returnValue(throwError(null));
actions$
.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } }];
store.dispatch(new DeleteNodesAction(selection));
setTimeout(() => {
expect(showErrorSpy).toHaveBeenCalled();
done();
});
});
it('should raise warning message when only one file is successful', (done) => {
it('should raise warning message when only one file is successful', () => {
spyOn(contentApi, 'deleteNode').and.callFake((id) => {
if (id === '1') {
return throwError(null);
@ -869,20 +869,13 @@ describe('ContentManagementService', () => {
return of(null);
}
});
actions$
.pipe(
ofType<SnackbarWarningAction>(SnackbarActionTypes.Warning),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } }];
store.dispatch(new DeleteNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-warning-snackbar');
});
it('should raise warning message when some files are successfully deleted', (done) => {
it('should raise warning message when some files are successfully deleted', () => {
spyOn(contentApi, 'deleteNode').and.callFake((id) => {
if (id === '1') {
return throwError(null);
@ -898,17 +891,10 @@ describe('ContentManagementService', () => {
return of(null);
});
actions$
.pipe(
ofType<SnackbarWarningAction>(SnackbarActionTypes.Warning),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } }, { entry: { id: '3', name: 'name3' } }];
store.dispatch(new DeleteNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-warning-snackbar');
});
});
@ -936,7 +922,7 @@ describe('ContentManagementService', () => {
});
describe('notification', () => {
it('raises warning on multiple fail and one success', () => {
it('raises warning on multiple fail and one success', (done) => {
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return of({});
@ -960,10 +946,13 @@ describe('ContentManagementService', () => {
];
store.dispatch(new PurgeDeletedNodesAction(selection));
setTimeout(() => {
expect(showWarningSpy).toHaveBeenCalled();
done();
});
});
it('raises warning on multiple success and multiple fail', () => {
it('raises warning on multiple success and multiple fail', (done) => {
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return of({});
@ -992,28 +981,37 @@ describe('ContentManagementService', () => {
];
store.dispatch(new PurgeDeletedNodesAction(selection));
setTimeout(() => {
expect(showWarningSpy).toHaveBeenCalled();
done();
});
});
it('raises info on one selected node success', () => {
it('raises info on one selected node success', (done) => {
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(of({}));
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }];
store.dispatch(new PurgeDeletedNodesAction(selection));
setTimeout(() => {
expect(showInfoSpy).toHaveBeenCalled();
done();
});
});
it('raises error on one selected node fail', () => {
it('raises error on one selected node fail', (done) => {
spyOn(contentApi, 'purgeDeletedNode').and.returnValue(throwError({}));
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }];
store.dispatch(new PurgeDeletedNodesAction(selection));
setTimeout(() => {
expect(showErrorSpy).toHaveBeenCalled();
done();
});
});
it('raises info on all nodes success', () => {
it('raises info on all nodes success', (done) => {
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return of({});
@ -1029,10 +1027,13 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } }];
store.dispatch(new PurgeDeletedNodesAction(selection));
setTimeout(() => {
expect(showInfoSpy).toHaveBeenCalled();
done();
});
});
it('raises error on all nodes fail', () => {
it('raises error on all nodes fail', (done) => {
spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => {
if (id === '1') {
return throwError({});
@ -1048,7 +1049,10 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name1' } }, { entry: { id: '2', name: 'name2' } }];
store.dispatch(new PurgeDeletedNodesAction(selection));
setTimeout(() => {
expect(showErrorSpy).toHaveBeenCalled();
done();
});
});
});
});
@ -1098,7 +1102,9 @@ describe('ContentManagementService', () => {
}
}
];
openSnackMessageActionSpy.and.returnValue({
onAction: () => of(null)
} as MatSnackBarRef<SimpleSnackBar>);
store.dispatch(new RestoreDeletedNodesAction(selection));
expect(contentApi.restoreNode).toHaveBeenCalled();
@ -1128,6 +1134,7 @@ describe('ContentManagementService', () => {
const selection: any[] = [
{
status: 1,
entry: {
id: '1',
path
@ -1135,9 +1142,14 @@ describe('ContentManagementService', () => {
}
];
const actionSubject = new Subject<void>();
openSnackMessageActionSpy.and.returnValue({
onAction: () => actionSubject.asObservable()
} as MatSnackBarRef<SimpleSnackBar>);
store.dispatch(new RestoreDeletedNodesAction(selection));
actionSubject.next();
expect(store.dispatch['calls'].argsFor(1)[0].userAction.action instanceof NavigateToParentFolder).toBe(true);
expect(store.dispatch).toHaveBeenCalledWith(new NavigateToParentFolder(selection[0]));
});
describe('notification', () => {
@ -1147,18 +1159,13 @@ describe('ContentManagementService', () => {
list: { entries: [] }
})
);
openSnackMessageActionSpy.and.returnValue({
onAction: () => of(null)
} as MatSnackBarRef<SimpleSnackBar>);
});
it('should raise error message on partial multiple fail ', (done) => {
it('should raise error message on partial multiple fail ', () => {
const error = { message: '{ "error": {} }' };
actions$
.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
spyOn(contentApi, 'restoreNode').and.callFake((id) => {
if (id === '1') {
return of({} as NodeEntry);
@ -1191,19 +1198,12 @@ describe('ContentManagementService', () => {
];
store.dispatch(new RestoreDeletedNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-error-snackbar');
});
it('should raise error message when restored node exist, error 409', (done) => {
it('should raise error message when restored node exist, error 409', () => {
const error = { message: '{ "error": { "statusCode": 409 } }' };
spyOn(contentApi, 'restoreNode').and.returnValue(throwError(error));
actions$
.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const path = {
elements: [
{
@ -1216,20 +1216,13 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name1', path } }];
store.dispatch(new RestoreDeletedNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-error-snackbar');
});
it('should raise error message when restored node returns different statusCode', (done) => {
it('should raise error message when restored node returns different statusCode', () => {
const error = { message: '{ "error": { "statusCode": 404 } }' };
spyOn(contentApi, 'restoreNode').and.returnValue(throwError(error));
actions$
.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const path = {
elements: [
{
@ -1242,20 +1235,13 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name1', path } }];
store.dispatch(new RestoreDeletedNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-error-snackbar');
});
it('should raise error message when restored node location is missing', (done) => {
it('should raise error message when restored node location is missing', () => {
const error = { message: '{ "error": { } }' };
spyOn(contentApi, 'restoreNode').and.returnValue(throwError(error));
actions$
.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const path = {
elements: [
{
@ -1268,9 +1254,10 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name1', path } }];
store.dispatch(new RestoreDeletedNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-error-snackbar');
});
it('should raise info message when restore multiple nodes', (done) => {
it('should raise info message when restore multiple nodes', () => {
spyOn(contentApi, 'restoreNode').and.callFake((id) => {
const entry = {} as NodeEntry;
if (id === '1') {
@ -1283,14 +1270,6 @@ describe('ContentManagementService', () => {
return of(entry);
});
actions$
.pipe(
ofType<SnackbarInfoAction>(SnackbarActionTypes.Info),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const path = {
elements: [
{
@ -1303,18 +1282,11 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name1', path } }, { entry: { id: '2', name: 'name2', path } }];
store.dispatch(new RestoreDeletedNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-info-snackbar');
});
it('should raise info message when restore selected node', (done) => {
it('should raise info message when restore selected node', () => {
spyOn(contentApi, 'restoreNode').and.returnValue(of({} as NodeEntry));
actions$
.pipe(
ofType<SnackbarInfoAction>(SnackbarActionTypes.Info),
map((action) => expect(action).toBeDefined())
)
.subscribe(() => done());
const path = {
elements: [
{
@ -1327,39 +1299,7 @@ describe('ContentManagementService', () => {
const selection: any[] = [{ entry: { id: '1', name: 'name1', path } }];
store.dispatch(new RestoreDeletedNodesAction(selection));
});
it('navigate to restore selected node location onAction', (done) => {
spyOn(contentApi, 'restoreNode').and.returnValue(of({} as NodeEntry));
const path = {
elements: [
{
id: '1-1',
name: 'somewhere-over-the-rainbow'
}
]
};
const selection: any[] = [
{
entry: {
id: '1',
name: 'name1',
path
}
}
];
actions$
.pipe(
ofType<RestoreDeletedNodesAction>(NodeActionTypes.RestoreDeleted),
map((action) => {
expect(action).toBeDefined();
})
)
.subscribe(() => done());
store.dispatch(new RestoreDeletedNodesAction(selection));
expect(openSnackMessageActionSpy.calls.argsFor(0)[2].panelClass).toBe('adf-info-snackbar');
});
});
});
@ -1440,7 +1380,6 @@ describe('ContentManagementService', () => {
const elementToFocus = document.createElement(elementToFocusSelector);
spyOn(elementToFocus, 'focus');
spyOn(document, 'querySelector').withArgs(elementToFocusSelector).and.returnValue(elementToFocus);
spyOn(store, 'select').and.returnValue(new BehaviorSubject(''));
contentManagementService.shareNode(
{
entry: {}
@ -1859,6 +1798,36 @@ describe('ContentManagementService', () => {
}));
});
describe('updateLibrary', () => {
const siteId = 'mock-site-id';
const siteBody: SiteBodyCreate = {
title: 'Updated Library',
description: 'Updated description',
visibility: 'PUBLIC'
};
const mockSiteEntry = { entry: { id: siteId, ...siteBody } } as SiteEntry;
it('should call content api and dispatch success action on successful update', () => {
spyOn(contentApi, 'updateLibrary').and.returnValue(of(mockSiteEntry));
spyOn(appHookService.libraryUpdated, 'next');
contentManagementService.updateLibrary(siteId, siteBody);
expect(appHookService.libraryUpdated.next).toHaveBeenCalledWith(mockSiteEntry);
expect(showInfoSpy).toHaveBeenCalledWith('LIBRARY.SUCCESS.LIBRARY_UPDATED');
});
it('should show error notification and dispatch failure action on update error', () => {
spyOn(contentApi, 'updateLibrary').and.returnValue(throwError(() => new Error('error')));
spyOn(appHookService.libraryUpdateFailed, 'next');
contentManagementService.updateLibrary(siteId, siteBody);
expect(appHookService.libraryUpdateFailed.next).toHaveBeenCalled();
expect(showErrorSpy).toHaveBeenCalledWith('LIBRARY.ERRORS.LIBRARY_UPDATE_ERROR');
});
});
describe('folderInformationDialog', () => {
it('should open folder information dialog', () => {
spyOn(dialog, 'open');
@ -1916,56 +1885,49 @@ describe('ContentManagementService', () => {
it('should call proper content api and display proper snackbar message if one node is provided for addFavorite', () => {
spyOn(contentApi, 'addFavorite').and.returnValue(of({ entry: { targetGuid: '', target: '' } }));
spyOn(store, 'dispatch');
contentManagementService.addFavorite([fakeNode1]);
expect(contentApi.addFavorite).toHaveBeenCalledWith([fakeNode1]);
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODE_ADDED', { name: 'mock-folder1-name' }));
expect(showInfoSpy).toHaveBeenCalledWith('APP.MESSAGES.INFO.FAVORITE_NODE_ADDED', null, { name: 'mock-folder1-name' });
});
it('should call proper content api and display proper snackbar message if more than one node is provided for addFavorite', () => {
spyOn(contentApi, 'addFavorite').and.returnValue(of({ entry: { targetGuid: '', target: '' } }));
spyOn(store, 'dispatch');
contentManagementService.addFavorite([fakeNode1, fakeNode2]);
expect(contentApi.addFavorite).toHaveBeenCalledWith([fakeNode1, fakeNode2]);
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODES_ADDED', { number: 2 }));
expect(showInfoSpy).toHaveBeenCalledWith('APP.MESSAGES.INFO.FAVORITE_NODES_ADDED', null, { number: 2 });
});
it('should call proper content api and display proper snackbar message if one node is provided for removeFavorite', () => {
spyOn(contentApi, 'removeFavorite').and.returnValue(of({}));
spyOn(store, 'dispatch');
contentManagementService.removeFavorite([fakeNode1]);
expect(contentApi.removeFavorite).toHaveBeenCalledWith([fakeNode1]);
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODE_REMOVED', { name: 'mock-folder1-name' }));
expect(showInfoSpy).toHaveBeenCalledWith('APP.MESSAGES.INFO.FAVORITE_NODE_REMOVED', null, { name: 'mock-folder1-name' });
});
it('should call proper content api and display proper snackbar message if more than one node is provided for removeFavorite', () => {
spyOn(contentApi, 'removeFavorite').and.returnValue(of({}));
spyOn(store, 'dispatch');
contentManagementService.removeFavorite([fakeNode1, fakeNode2]);
expect(contentApi.removeFavorite).toHaveBeenCalledWith([fakeNode1, fakeNode2]);
expect(store.dispatch).toHaveBeenCalledWith(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODES_REMOVED', { number: 2 }));
expect(showInfoSpy).toHaveBeenCalledWith('APP.MESSAGES.INFO.FAVORITE_NODES_REMOVED', null, { number: 2 });
});
it('should display snackbar error message when removeFavorite api fails', () => {
spyOn(contentApi, 'removeFavorite').and.returnValue(
throwError(() => new Error(JSON.stringify({ error: { statusCode: 404, briefSummary: ' relationship id of folder-node2-id' } })))
);
spyOn(store, 'dispatch');
contentManagementService.removeFavorite([fakeNode1, fakeNode2]);
expect(contentApi.removeFavorite).toHaveBeenCalledWith([fakeNode1, fakeNode2]);
expect(store.dispatch).toHaveBeenCalledWith(
new SnackbarErrorAction('APP.MESSAGES.INFO.FAVORITE_NODE_NOT_FOUND', { name: 'mock-folder2-name' })
);
expect(showErrorSpy).toHaveBeenCalledWith('APP.MESSAGES.ERRORS.FAVORITE_NODE_NOT_FOUND', null, { name: 'mock-folder2-name' });
});
});
});

View File

@ -34,12 +34,6 @@ import {
RefreshPreviewAction,
SetSelectedNodesAction,
ShowLoaderAction,
SnackbarAction,
SnackbarErrorAction,
SnackbarInfoAction,
SnackbarUserAction,
SnackbarWarningAction,
UndoDeleteNodesAction,
UnlockWriteAction,
ViewNodeVersionAction
} from '@alfresco/aca-shared/store';
@ -71,6 +65,13 @@ interface RestoredNode {
statusCode?: number;
}
interface SnackbarMessageData {
key: string;
params: { [key: string]: any };
userActionLabel?: string;
type: 'info' | 'warning' | 'error';
}
@Injectable({
providedIn: 'root'
})
@ -104,9 +105,9 @@ export class ContentManagementService {
});
if (nodes.length > 1) {
this.store.dispatch(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODES_ADDED', { number: nodes.length }));
this.notificationService.showInfo('APP.MESSAGES.INFO.FAVORITE_NODES_ADDED', null, { number: nodes.length });
} else {
this.store.dispatch(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODE_ADDED', { name: nodes[0].entry.name }));
this.notificationService.showInfo('APP.MESSAGES.INFO.FAVORITE_NODE_ADDED', null, { name: nodes[0].entry.name });
}
this.store.dispatch(new SetSelectedNodesAction(favoriteNodes));
});
@ -124,9 +125,9 @@ export class ContentManagementService {
});
if (nodes.length > 1) {
this.store.dispatch(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODES_REMOVED', { number: nodes.length }));
this.notificationService.showInfo('APP.MESSAGES.INFO.FAVORITE_NODES_REMOVED', null, { number: nodes.length });
} else {
this.store.dispatch(new SnackbarInfoAction('APP.MESSAGES.INFO.FAVORITE_NODE_REMOVED', { name: nodes[0].entry.name }));
this.notificationService.showInfo('APP.MESSAGES.INFO.FAVORITE_NODE_REMOVED', null, { name: nodes[0].entry.name });
}
this.store.dispatch(new SetSelectedNodesAction(favoriteNodes));
},
@ -134,7 +135,7 @@ export class ContentManagementService {
if (JSON.parse(error.message).error.statusCode === 404) {
const nodeId = JSON.parse(error.message).error.briefSummary.split('relationship id of ')[1];
const nodeName = nodes.find((node) => node.entry.id === nodeId)?.entry.name;
this.store.dispatch(new SnackbarErrorAction('APP.MESSAGES.INFO.FAVORITE_NODE_NOT_FOUND', { name: nodeName }));
this.notificationService.showError('APP.MESSAGES.ERRORS.FAVORITE_NODE_NOT_FOUND', null, { name: nodeName });
}
}
});
@ -353,6 +354,7 @@ export class ContentManagementService {
this.notificationService.showInfo('LIBRARY.SUCCESS.LIBRARY_UPDATED');
},
() => {
this.appHookService.libraryUpdateFailed.next();
this.notificationService.showError('LIBRARY.ERRORS.LIBRARY_UPDATE_ERROR');
}
);
@ -549,9 +551,18 @@ export class ContentManagementService {
failed: failedItems
});
let messageType: string;
if (numberOfCopiedItems === 0) {
messageType = 'adf-error-snackbar';
} else if (failedItems > 0) {
messageType = 'adf-warning-snackbar';
} else {
messageType = 'adf-info-snackbar';
}
this.notificationService
.openSnackMessageAction(message, undo, {
panelClass: 'info-snackbar'
panelClass: messageType
})
.onAction()
.subscribe(() => this.undoCopyNodes(newItems));
@ -690,13 +701,27 @@ export class ContentManagementService {
forkJoin(...batch).subscribe((data: DeletedNodeInfo[]) => {
const status = this.processStatus(data);
const message = this.getDeleteMessage(status);
const messageData = this.getDeleteMessageData(status);
if (message && status.someSucceeded && allowUndo) {
message.userAction = new SnackbarUserAction('APP.ACTIONS.UNDO', new UndoDeleteNodesAction([...status.success]));
if (messageData && status.someSucceeded) {
const translatedMessage: string = this.translation.instant(messageData.key, messageData.params);
const action: string | null = allowUndo ? this.translation.instant('APP.ACTIONS.UNDO') : null;
const snackBarRef = this.notificationService.openSnackMessageAction(
translatedMessage,
action,
{ panelClass: `adf-${messageData.type}-snackbar` },
messageData.params
);
if (action) {
snackBarRef.onAction().subscribe(() => {
this.undoDeleteNodes([...status.success]);
});
}
} else if (messageData) {
this.notificationService.showError(this.translation.instant(messageData.key, messageData.params));
}
this.store.dispatch(message);
if (status.someSucceeded) {
this.appHookService.nodesDeleted.next();
@ -717,8 +742,7 @@ export class ContentManagementService {
const processedData = this.processStatus(data);
if (processedData.fail.length) {
const message = this.getUndoDeleteMessage(processedData);
this.store.dispatch(message);
this.showUndoDeleteMessage(processedData);
}
if (processedData.someSucceeded) {
@ -746,18 +770,14 @@ export class ContentManagementService {
);
}
private getUndoDeleteMessage(status: DeleteStatus): SnackbarAction {
private showUndoDeleteMessage(status: DeleteStatus): void {
if (status.someFailed && !status.oneFailed) {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', { number: status.fail.length });
this.notificationService.showError('APP.MESSAGES.ERRORS.NODE_RESTORE.PLURAL', null, { number: status.fail.length });
}
if (status.oneFailed) {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_RESTORE', {
name: status.fail[0].name
});
this.notificationService.showError('APP.MESSAGES.ERRORS.NODE_RESTORE', null, { name: status.fail[0].name });
}
return null;
}
private restoreNode(node: NodeEntry): Observable<RestoredNode> {
@ -895,27 +915,29 @@ export class ContentManagementService {
}
private showRestoreNotification(status: DeleteStatus): void {
const message = this.getRestoreMessage(status);
const messageData = this.getRestoreMessageData(status);
if (message) {
if (status.oneSucceeded && !status.someFailed) {
if (messageData) {
const translatedMessage: string = this.translation.instant(messageData.key, messageData.params);
const action: string = messageData.userActionLabel ? this.translation.instant(messageData.userActionLabel) : '';
const panelClass = messageData.type === 'error' ? 'adf-error-snackbar' : 'adf-info-snackbar';
const snackBarRef = this.notificationService.openSnackMessageAction(translatedMessage, action, { panelClass }, messageData.params);
if (messageData.userActionLabel && status.oneSucceeded && !status.someFailed) {
snackBarRef.onAction().subscribe(() => {
const isSite = this.isSite(status.success[0].entry);
const path: PathInfo = status.success[0].entry.path;
const parent = path.elements[path.elements.length - 1];
const route = isSite ? ['/libraries', parent.id] : ['/personal-files', parent.id];
let navigate;
if (this.isLibraryContent(path)) {
navigate = new NavigateToParentFolder(status.success[0]);
this.store.dispatch(new NavigateToParentFolder(status.success[0]));
} else {
navigate = new NavigateRouteAction(route);
this.store.dispatch(new NavigateRouteAction(route));
}
message.userAction = new SnackbarUserAction('APP.ACTIONS.VIEW', navigate);
});
}
this.store.dispatch(message);
}
}
@ -927,39 +949,49 @@ export class ContentManagementService {
return path && path.elements.length >= 2 && path.elements[1].name === 'Sites';
}
private getRestoreMessage(status: DeleteStatus): SnackbarAction {
private getRestoreMessageData(status: DeleteStatus): SnackbarMessageData | null {
if (status.someFailed && !status.oneFailed) {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL', {
number: status.fail.length
});
return {
key: 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.PARTIAL_PLURAL',
params: { number: status.fail.length },
type: 'error'
};
}
if (status.oneFailed && status.fail[0].statusCode) {
if (status.fail[0].statusCode === 409) {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS', {
name: status.fail[0].entry.name
});
} else {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC', {
name: status.fail[0].entry.name
});
}
return {
key:
status.fail[0].statusCode === 409
? 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.NODE_EXISTS'
: 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.GENERIC',
params: { name: status.fail[0].entry.name },
type: 'error'
};
}
if (status.oneFailed && !status.fail[0].statusCode) {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.LOCATION_MISSING', {
name: status.fail[0].entry.name
});
return {
key: 'APP.MESSAGES.ERRORS.TRASH.NODES_RESTORE.LOCATION_MISSING',
params: { name: status.fail[0].entry.name },
type: 'error'
};
}
if (status.allSucceeded && !status.oneSucceeded) {
return new SnackbarInfoAction('APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL');
return {
key: 'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.PLURAL',
params: {},
type: 'info'
};
}
if (status.allSucceeded && status.oneSucceeded) {
return new SnackbarInfoAction('APP.MESSAGES.INFO.TRASH.NODES_RESTORE.SINGULAR', {
name: status.success[0].entry.name
});
return {
key: 'APP.MESSAGES.INFO.TRASH.NODES_RESTORE.SINGULAR',
params: { name: status.success[0].entry.name },
userActionLabel: 'APP.ACTIONS.VIEW',
type: 'info'
};
}
return null;
@ -997,39 +1029,53 @@ export class ContentManagementService {
);
}
private getDeleteMessage(status: DeleteStatus): SnackbarAction {
private getDeleteMessageData(status: DeleteStatus): SnackbarMessageData | null {
if (status.allFailed && !status.oneFailed) {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL', { number: status.fail.length });
return {
key: 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL',
params: { number: status.fail.length },
type: 'error'
};
}
if (status.allSucceeded && !status.oneSucceeded) {
return new SnackbarInfoAction('APP.MESSAGES.INFO.NODE_DELETION.PLURAL', {
number: status.success.length
});
return {
key: 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL',
params: { number: status.success.length },
type: 'info'
};
}
if (status.someFailed && status.someSucceeded && !status.oneSucceeded) {
return new SnackbarWarningAction('APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL', {
success: status.success.length,
failed: status.fail.length
});
return {
key: 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL',
params: { success: status.success.length, failed: status.fail.length },
type: 'warning'
};
}
if (status.someFailed && status.oneSucceeded) {
return new SnackbarWarningAction('APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR', {
success: status.success.length,
failed: status.fail.length
});
return {
key: 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR',
params: { success: status.success.length, failed: status.fail.length },
type: 'warning'
};
}
if (status.oneFailed && !status.someSucceeded) {
return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_DELETION', {
name: status.fail[0].name
});
return {
key: 'APP.MESSAGES.ERRORS.NODE_DELETION',
params: { name: status.fail[0].name },
type: 'error'
};
}
if (status.oneSucceeded && !status.someFailed) {
return new SnackbarInfoAction('APP.MESSAGES.INFO.NODE_DELETION.SINGULAR', { name: status.success[0].name });
return {
key: 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR',
params: { name: status.success[0].name },
type: 'info'
};
}
return null;
@ -1092,13 +1138,20 @@ export class ContentManagementService {
partially: partiallySucceeded
});
let notificationType = 'adf-warning-snackbar';
if (partiallySucceeded === 0 && succeeded === 0) {
notificationType = 'adf-error-snackbar';
} else if (failures === 0 && partiallySucceeded === 0) {
notificationType = 'adf-info-snackbar';
}
// TODO: review in terms of i18n
this.notificationService
.openSnackMessageAction(
messages[successMessage] + beforePartialSuccessMessage + messages[partialSuccessMessage] + beforeFailedMessage + messages[failedMessage],
undo,
{
panelClass: 'info-snackbar'
panelClass: notificationType
}
)
.onAction()

View File

@ -27,7 +27,7 @@ import { StoreModule } from '@ngrx/store';
import { appReducer } from './reducers/app.reducer';
import { StoreRouterConnectingModule, FullRouterStateSerializer } from '@ngrx/router-store';
import { EffectsModule } from '@ngrx/effects';
import { RouterEffects, SnackbarEffects } from '@alfresco/aca-shared/store';
import { RouterEffects } from '@alfresco/aca-shared/store';
import {
AppEffects,
NodeEffects,
@ -71,7 +71,6 @@ import { SearchAiEffects } from './effects/search-ai.effects';
TemplateEffects,
ContextMenuEffects,
SearchAiEffects,
SnackbarEffects,
RouterEffects
])
]

View File

@ -55,10 +55,11 @@ describe('LibraryEffects', () => {
spyOn(notificationService, 'showError');
});
it('should display library no permission error if user does not have permission', () => {
it('should display library no permission warning if user does not have permission', () => {
spyOn(notificationService, 'showWarning');
store.dispatch(new NavigateLibraryAction('libraryId'));
node$.error(new HttpErrorResponse({ status: 403 }));
expect(notificationService.showError).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NO_PERMISSIONS');
expect(notificationService.showWarning).toHaveBeenCalledWith('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
});
it('should display library not found error if library does not exist', () => {

View File

@ -120,18 +120,16 @@ export class LibraryEffects {
this.store.dispatch(new NavigateRouteAction([route, id]));
},
(error: HttpErrorResponse) => {
let errorTranslationKey: string;
switch (error.status) {
case 403:
errorTranslationKey = 'APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NO_PERMISSIONS';
this.notificationService.showWarning('APP.BROWSE.LIBRARIES.LIBRARY_NO_PERMISSIONS_WARNING');
break;
case 404:
errorTranslationKey = 'APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NOT_FOUND';
this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_NOT_FOUND');
break;
default:
errorTranslationKey = 'APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR';
this.notificationService.showError('APP.BROWSE.LIBRARIES.ERRORS.LIBRARY_LOADING_ERROR');
}
this.notificationService.showError(errorTranslationKey);
}
);
}

View File

@ -49,7 +49,6 @@ import {
SetSelectedNodesAction,
ShareNodeAction,
ShowLoaderAction,
SnackbarEffects,
UndoDeleteNodesAction,
UnlockWriteAction,
UnshareNodesAction
@ -71,12 +70,7 @@ describe('NodeEffects', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AppTestingModule,
EffectsModule.forRoot([NodeEffects, ViewerEffects, SnackbarEffects, RouterEffects]),
MatDialogModule,
MatSnackBarModule
],
imports: [AppTestingModule, EffectsModule.forRoot([NodeEffects, ViewerEffects, RouterEffects]), MatDialogModule, MatSnackBarModule],
providers: [RenditionService, { provide: ActivatedRoute, useValue: { queryParams: of({ location: 'test-page' }) } }]
});

View File

@ -252,7 +252,7 @@ mat-snack-bar-container {
}
.adf-warning-snackbar {
--mdc-snackbar-container-color: var(--theme-accent-color);
--mdc-snackbar-container-color: var(--theme-warning-snackbar-background);
}
.adf-info-snackbar {

View File

@ -47,6 +47,7 @@ $disabled-chip-background-color: #f5f5f5;
$contrast-gray: mat.m2-get-color-from-palette($foreground, 'secondary-tex');
$search-highlight-background-color: #ffd180;
$info-snackbar-background: #1f74db;
$warning-snackbar-background: #8c7012;
$text-light-color: rgba(33, 35, 40, 0.7);
$card-background-grey-color: rgb(248, 248, 248);
$light-grey-1: #d5d5d5;
@ -79,6 +80,7 @@ $defaults: (
--theme-grey-hover-background-color: $grey-hover-background,
--theme-blue-button-color: $blue-save-button-background,
--theme-info-snackbar-background: $info-snackbar-background,
--theme-warning-snackbar-background: $warning-snackbar-background,
--theme-blue-checkbox-color: $blue-checkbox-background,
--theme-blue-active-table-row-color: $blue-active-table-row,
--theme-heading-color: $black-heading,

View File

@ -50,6 +50,11 @@ export class AppHookService {
*/
libraryUpdated = new Subject<SiteEntry>();
/**
* Gets emitted when library update fails
*/
libraryUpdateFailed = new Subject<void>();
/**
* Gets emitted when user join the library
*/

View File

@ -1,78 +0,0 @@
/*!
* Copyright © 2005-2025 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 { Action } from '@ngrx/store';
export enum SnackbarActionTypes {
Info = 'SNACKBAR_INFO',
Warning = 'SNACKBAR_WARNING',
Error = 'SNACKBAR_ERROR'
}
export interface SnackbarAction extends Action {
payload: string;
params?: any;
userAction?: SnackbarUserAction;
duration?: number;
}
export class SnackbarUserAction {
constructor(
public title: string,
public action: Action
) {}
}
export class SnackbarInfoAction implements SnackbarAction {
readonly type = SnackbarActionTypes.Info;
userAction?: SnackbarUserAction;
constructor(
public payload: string,
public params?: any
) {}
}
export class SnackbarWarningAction implements SnackbarAction {
readonly type = SnackbarActionTypes.Warning;
userAction?: SnackbarUserAction;
constructor(
public payload: string,
public params?: any
) {}
}
export class SnackbarErrorAction implements SnackbarAction {
readonly type = SnackbarActionTypes.Error;
userAction?: SnackbarUserAction;
constructor(
public payload: string,
public params?: any
) {}
}

View File

@ -1,98 +0,0 @@
/*!
* Copyright © 2005-2025 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 { EffectsModule } from '@ngrx/effects';
import { Store, StoreModule } from '@ngrx/store';
import { MatSnackBar } from '@angular/material/snack-bar';
import { CoreTestingModule, SnackbarContentComponent } from '@alfresco/adf-core';
import { SnackbarEffects } from './snackbar.effects';
import { SnackbarErrorAction, SnackbarInfoAction, SnackbarWarningAction } from '../actions/snackbar.actions';
import { AppStore } from '@alfresco/aca-shared/store';
describe('NodeEffects', () => {
let store: Store<AppStore>;
let matSnackBar: MatSnackBar;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule, StoreModule.forRoot({}), EffectsModule.forRoot([SnackbarEffects])]
});
store = TestBed.inject(Store);
matSnackBar = TestBed.inject(MatSnackBar);
spyOn(matSnackBar, 'openFromComponent');
});
describe('infoEffect', () => {
it('should open snackbar with adf-info-snackbar panel class', () => {
store.dispatch(new SnackbarInfoAction('test-snackbar-message'));
expect(matSnackBar.openFromComponent).toHaveBeenCalledWith(SnackbarContentComponent, {
panelClass: 'adf-info-snackbar',
data: {
message: 'test-snackbar-message',
actionLabel: null,
actionIcon: 'close',
actionIconAriaLabel: 'CLOSE',
showAction: true,
callActionOnIconClick: false
}
});
});
});
describe('warningEffect', () => {
it('should open snackbar with adf-info-snackbar panel class', () => {
store.dispatch(new SnackbarWarningAction('test-snackbar-message'));
expect(matSnackBar.openFromComponent).toHaveBeenCalledWith(SnackbarContentComponent, {
panelClass: 'adf-warning-snackbar',
data: {
message: 'test-snackbar-message',
actionLabel: null,
actionIcon: 'close',
actionIconAriaLabel: 'CLOSE',
showAction: true,
callActionOnIconClick: false
}
});
});
});
describe('errorEffect', () => {
it('should open snackbar with adf-info-snackbar panel class', () => {
store.dispatch(new SnackbarErrorAction('test-snackbar-message'));
expect(matSnackBar.openFromComponent).toHaveBeenCalledWith(SnackbarContentComponent, {
panelClass: 'adf-error-snackbar',
data: {
message: 'test-snackbar-message',
actionLabel: null,
actionIcon: 'close',
actionIconAriaLabel: 'CLOSE',
showAction: true,
callActionOnIconClick: false
}
});
});
});
});

View File

@ -1,104 +0,0 @@
/*!
* Copyright © 2005-2025 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 { SnackbarContentComponent, SnackBarData, TranslationService } from '@alfresco/adf-core';
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { map } from 'rxjs/operators';
import { AppStore } from '../states/app.state';
import { SnackbarAction, SnackbarActionTypes, SnackbarErrorAction, SnackbarInfoAction, SnackbarWarningAction } from '../actions/snackbar.actions';
@Injectable()
export class SnackbarEffects {
private store = inject(Store<AppStore>);
private actions$ = inject(Actions);
private snackBar = inject(MatSnackBar);
private translationService = inject(TranslationService);
infoEffect = createEffect(
() =>
this.actions$.pipe(
ofType<SnackbarInfoAction>(SnackbarActionTypes.Info),
map((action: SnackbarInfoAction) => {
this.showSnackBar(action, 'adf-info-snackbar');
})
),
{ dispatch: false }
);
warningEffect = createEffect(
() =>
this.actions$.pipe(
ofType<SnackbarWarningAction>(SnackbarActionTypes.Warning),
map((action: SnackbarWarningAction) => {
this.showSnackBar(action, 'adf-warning-snackbar');
})
),
{ dispatch: false }
);
errorEffect = createEffect(
() =>
this.actions$.pipe(
ofType<SnackbarErrorAction>(SnackbarActionTypes.Error),
map((action: SnackbarErrorAction) => {
this.showSnackBar(action, 'adf-error-snackbar');
})
),
{ dispatch: false }
);
private showSnackBar(action: SnackbarAction, panelClass: string) {
const message = this.translate(action.payload, action.params);
let actionName: string = null;
if (action.userAction) {
actionName = this.translate(action.userAction.title);
}
const snackBarRef = this.snackBar.openFromComponent<SnackbarContentComponent, SnackBarData>(SnackbarContentComponent, {
...(action.duration !== undefined && action.duration !== null && { duration: action.duration }),
panelClass,
data: {
message,
actionLabel: actionName,
actionIcon: 'close',
actionIconAriaLabel: 'CLOSE',
showAction: true,
callActionOnIconClick: false
}
});
if (action.userAction) {
snackBarRef.onAction().subscribe(() => {
this.store.dispatch(action.userAction.action);
});
}
}
private translate(message: string, params?: any): string {
return this.translationService.instant(message, params);
}
}

View File

@ -31,7 +31,6 @@ export * from './actions/library.actions';
export * from './actions/node.actions';
export * from './actions/router.actions';
export * from './actions/search.actions';
export * from './actions/snackbar.actions';
export * from './actions/upload.actions';
export * from './actions/viewer.actions';
export * from './actions/metadata-aspect.actions';
@ -40,7 +39,6 @@ export * from './actions/contextmenu.actions';
export * from './actions/search-ai.actions';
export * from './effects/router.effects';
export * from './effects/snackbar.effects';
export * from './models/ai-search-by-term-payload';
export * from './models/delete-status.model';