diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 58d8d0d4c..804a04774 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -612,42 +612,42 @@ Error retrieving config src/app/components/admin/config/config.component.ts - 103 + 104 Invalid JSON src/app/components/admin/config/config.component.ts - 129 + 132 Configuration updated src/app/components/admin/config/config.component.ts - 173 + 176 An error occurred updating configuration src/app/components/admin/config/config.component.ts - 178 + 181 File successfully updated src/app/components/admin/config/config.component.ts - 200 + 204 An error occurred uploading file src/app/components/admin/config/config.component.ts - 205 + 210 @@ -1375,7 +1375,7 @@ 340 - src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html + src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.html 11 @@ -1500,64 +1500,64 @@ Use system language src/app/components/admin/settings/settings.component.ts - 76 + 79 Use date format of display language src/app/components/admin/settings/settings.component.ts - 79 + 82 Error retrieving users src/app/components/admin/settings/settings.component.ts - 220 + 224 src/app/components/admin/users-groups/users-groups.component.ts - 59 + 60 Error retrieving groups src/app/components/admin/settings/settings.component.ts - 239 + 246 src/app/components/admin/users-groups/users-groups.component.ts - 71 + 75 Settings were saved successfully. src/app/components/admin/settings/settings.component.ts - 535 + 544 Settings were saved successfully. Reload is required to apply some changes. src/app/components/admin/settings/settings.component.ts - 539 + 548 Reload now src/app/components/admin/settings/settings.component.ts - 540 + 549 An error occurred while saving settings. src/app/components/admin/settings/settings.component.ts - 550 + 559 src/app/components/app-frame/app-frame.component.ts @@ -2222,23 +2222,23 @@ src/app/components/admin/users-groups/users-groups.component.ts - 124 + 130 src/app/components/admin/users-groups/users-groups.component.ts - 177 + 187 src/app/components/manage/custom-fields/custom-fields.component.ts - 108 + 110 src/app/components/manage/mail/mail.component.ts - 195 + 200 src/app/components/manage/mail/mail.component.ts - 296 + 303 src/app/components/manage/management-list/management-list.component.ts @@ -2490,66 +2490,66 @@ Password has been changed, you will be logged out momentarily. src/app/components/admin/users-groups/users-groups.component.ts - 97 + 103 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 198 + 200 Saved user "". src/app/components/admin/users-groups/users-groups.component.ts - 104 + 110 Error saving user. src/app/components/admin/users-groups/users-groups.component.ts - 114 + 120 Confirm delete user account src/app/components/admin/users-groups/users-groups.component.ts - 122 + 128 This operation will permanently delete this user account. src/app/components/admin/users-groups/users-groups.component.ts - 123 + 129 Proceed src/app/components/admin/users-groups/users-groups.component.ts - 126 + 132 src/app/components/admin/users-groups/users-groups.component.ts - 179 + 189 src/app/components/document-detail/document-detail.component.ts - 958 + 964 src/app/components/document-detail/document-detail.component.ts - 1318 + 1324 src/app/components/document-detail/document-detail.component.ts - 1357 + 1363 src/app/components/document-detail/document-detail.component.ts - 1398 + 1404 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -2565,15 +2565,15 @@ src/app/components/manage/custom-fields/custom-fields.component.ts - 110 + 112 src/app/components/manage/mail/mail.component.ts - 197 + 202 src/app/components/manage/mail/mail.component.ts - 298 + 305 src/app/components/manage/management-list/management-list.component.ts @@ -2588,56 +2588,56 @@ Deleted user "" src/app/components/admin/users-groups/users-groups.component.ts - 132 + 139 Error deleting user "". src/app/components/admin/users-groups/users-groups.component.ts - 139 + 147 Saved group "". src/app/components/admin/users-groups/users-groups.component.ts - 159 + 168 Error saving group. src/app/components/admin/users-groups/users-groups.component.ts - 167 + 177 Confirm delete user group src/app/components/admin/users-groups/users-groups.component.ts - 175 + 185 This operation will permanently delete this user group. src/app/components/admin/users-groups/users-groups.component.ts - 176 + 186 Deleted group "" src/app/components/admin/users-groups/users-groups.component.ts - 185 + 196 Error deleting group "". src/app/components/admin/users-groups/users-groups.component.ts - 192 + 204 @@ -2915,14 +2915,14 @@ Error updating sidebar views src/app/components/app-frame/app-frame.component.ts - 248 + 249 An error occurred while saving update checking settings. src/app/components/app-frame/app-frame.component.ts - 269 + 272 @@ -3092,35 +3092,35 @@ Successfully updated object. src/app/components/app-frame/global-search/global-search.component.ts - 209 + 210 src/app/components/app-frame/global-search/global-search.component.ts - 247 + 253 Error occurred saving object. src/app/components/app-frame/global-search/global-search.component.ts - 212 + 215 src/app/components/app-frame/global-search/global-search.component.ts - 250 + 258 Clear All - src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html + src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.html 16 No notifications - src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html + src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.html 20 @@ -3157,7 +3157,7 @@ src/app/components/document-detail/document-detail.component.ts - 911 + 914 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -3318,22 +3318,22 @@ Saved field "". src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.ts - 126 + 127 src/app/components/manage/custom-fields/custom-fields.component.ts - 89 + 90 Error saving field. src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.ts - 135 + 137 src/app/components/manage/custom-fields/custom-fields.component.ts - 98 + 100 @@ -3395,7 +3395,7 @@ src/app/components/document-detail/document-detail.component.ts - 1375 + 1381 src/app/guards/dirty-saved-view.guard.ts @@ -4099,6 +4099,10 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html 111 + + src/app/components/common/notification/notification.component.html + 30 + src/app/components/common/system-status-dialog/system-status-dialog.component.html 175 @@ -4111,10 +4115,6 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 243 - - src/app/components/common/toast/toast.component.html - 30 - Only process attachments @@ -4523,11 +4523,11 @@ Totp deactivation failed src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts - 134 + 135 src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts - 139 + 142 @@ -5137,7 +5137,7 @@ Error emailing document src/app/components/common/email-document-dialog/email-document-dialog.component.ts - 69 + 70 @@ -5452,6 +5452,32 @@ 41 + + Status + + src/app/components/common/notification/notification.component.html + 28 + + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 47 + + + src/app/components/manage/mail/mail.component.html + 114 + + + src/app/components/manage/workflows/workflows.component.html + 19 + + + + Copy Raw Error + + src/app/components/common/notification/notification.component.html + 43 + + Read more @@ -5777,71 +5803,71 @@ Profile updated successfully src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 195 + 196 Error saving profile src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 207 + 210 Error generating auth token src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 224 + 229 Error disconnecting social account src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 249 + 254 Error fetching TOTP settings src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 268 + 273 TOTP activated successfully src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 289 + 295 Error activating TOTP src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 291 + 298 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 297 + 305 TOTP deactivated successfully src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 313 + 324 Error deactivating TOTP src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 315 + 328 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 320 + 335 @@ -5933,14 +5959,14 @@ Error deleting link src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 133 + 134 Error creating link src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 161 + 166 @@ -5999,25 +6025,6 @@ 41 - - Status - - src/app/components/common/system-status-dialog/system-status-dialog.component.html - 47 - - - src/app/components/common/toast/toast.component.html - 28 - - - src/app/components/manage/mail/mail.component.html - 114 - - - src/app/components/manage/workflows/workflows.component.html - 19 - - Migration Status @@ -6131,13 +6138,6 @@ 241 - - Copy Raw Error - - src/app/components/common/toast/toast.component.html - 43 - - Hello , welcome to @@ -6163,7 +6163,7 @@ Error updating dashboard src/app/components/dashboard/dashboard.component.ts - 93 + 94 @@ -6868,21 +6868,21 @@ Error saving document src/app/components/document-detail/document-detail.component.ts - 880 + 881 Do you really want to move the document "" to the trash? src/app/components/document-detail/document-detail.component.ts - 912 + 915 Documents can be restored prior to permanent deletion. src/app/components/document-detail/document-detail.component.ts - 913 + 916 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -6893,7 +6893,7 @@ Move to trash src/app/components/document-detail/document-detail.component.ts - 915 + 918 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -6904,14 +6904,14 @@ Error deleting document src/app/components/document-detail/document-detail.component.ts - 934 + 938 Reprocess confirm src/app/components/document-detail/document-detail.component.ts - 954 + 960 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -6922,77 +6922,77 @@ This operation will permanently recreate the archive file for this document. src/app/components/document-detail/document-detail.component.ts - 955 + 961 The archive file will be re-generated with the current settings. src/app/components/document-detail/document-detail.component.ts - 956 + 962 Reprocess operation for "" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content. src/app/components/document-detail/document-detail.component.ts - 966 + 972 Error executing operation src/app/components/document-detail/document-detail.component.ts - 977 + 983 Error downloading document src/app/components/document-detail/document-detail.component.ts - 1024 + 1030 Page Fit src/app/components/document-detail/document-detail.component.ts - 1103 + 1109 Split confirm src/app/components/document-detail/document-detail.component.ts - 1316 + 1322 This operation will split the selected document(s) into new documents. src/app/components/document-detail/document-detail.component.ts - 1317 + 1323 Split operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1333 + 1339 Error executing split operation src/app/components/document-detail/document-detail.component.ts - 1342 + 1348 Rotate confirm src/app/components/document-detail/document-detail.component.ts - 1355 + 1361 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -7003,60 +7003,60 @@ This operation will permanently rotate the original version of the current document. src/app/components/document-detail/document-detail.component.ts - 1356 + 1362 Rotation of "" will begin in the background. Close and re-open the document after the operation has completed to see the changes. src/app/components/document-detail/document-detail.component.ts - 1372 + 1378 Error executing rotate operation src/app/components/document-detail/document-detail.component.ts - 1384 + 1390 Delete pages confirm src/app/components/document-detail/document-detail.component.ts - 1396 + 1402 This operation will permanently delete the selected pages from the original document. src/app/components/document-detail/document-detail.component.ts - 1397 + 1403 Delete pages operation for "" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes. src/app/components/document-detail/document-detail.component.ts - 1412 + 1418 Error executing delete pages operation src/app/components/document-detail/document-detail.component.ts - 1421 + 1427 An error occurred loading tiff: src/app/components/document-detail/document-detail.component.ts - 1481 + 1487 src/app/components/document-detail/document-detail.component.ts - 1485 + 1491 @@ -8270,28 +8270,28 @@ Confirm delete field src/app/components/manage/custom-fields/custom-fields.component.ts - 106 + 108 This operation will permanently delete this field. src/app/components/manage/custom-fields/custom-fields.component.ts - 107 + 109 Deleted field "" src/app/components/manage/custom-fields/custom-fields.component.ts - 116 + 119 Error deleting field "". src/app/components/manage/custom-fields/custom-fields.component.ts - 125 + 129 @@ -8425,154 +8425,154 @@ Error retrieving mail rules src/app/components/manage/mail/mail.component.ts - 130 + 131 OAuth2 authentication success src/app/components/manage/mail/mail.component.ts - 138 + 142 OAuth2 authentication failed, see logs for details src/app/components/manage/mail/mail.component.ts - 149 + 154 Saved account "". src/app/components/manage/mail/mail.component.ts - 173 + 178 Error saving account. src/app/components/manage/mail/mail.component.ts - 185 + 190 Confirm delete mail account src/app/components/manage/mail/mail.component.ts - 193 + 198 This operation will permanently delete this mail account. src/app/components/manage/mail/mail.component.ts - 194 + 199 Deleted mail account "" src/app/components/manage/mail/mail.component.ts - 204 + 209 Error deleting mail account "". src/app/components/manage/mail/mail.component.ts - 215 + 220 Processing mail account "" src/app/components/manage/mail/mail.component.ts - 227 + 232 Error processing mail account "" src/app/components/manage/mail/mail.component.ts - 232 + 237 Saved rule "". src/app/components/manage/mail/mail.component.ts - 250 + 256 Error saving rule. src/app/components/manage/mail/mail.component.ts - 261 + 268 Rule "" enabled. src/app/components/manage/mail/mail.component.ts - 277 + 284 Rule "" disabled. src/app/components/manage/mail/mail.component.ts - 278 + 285 Error toggling rule "". src/app/components/manage/mail/mail.component.ts - 283 + 290 Confirm delete mail rule src/app/components/manage/mail/mail.component.ts - 294 + 301 This operation will permanently delete this mail rule. src/app/components/manage/mail/mail.component.ts - 295 + 302 Deleted mail rule "" src/app/components/manage/mail/mail.component.ts - 305 + 312 Error deleting mail rule "". src/app/components/manage/mail/mail.component.ts - 316 + 323 Permissions updated src/app/components/manage/mail/mail.component.ts - 340 + 347 Error updating permissions src/app/components/manage/mail/mail.component.ts - 345 + 352 src/app/components/manage/management-list/management-list.component.ts @@ -8737,14 +8737,14 @@ Objects deleted successfully src/app/components/manage/management-list/management-list.component.ts - 352 + 353 Error deleting objects src/app/components/manage/management-list/management-list.component.ts - 358 + 360 @@ -8807,14 +8807,14 @@ Views saved successfully. src/app/components/manage/saved-views/saved-views.component.ts - 158 + 159 Error while saving views. src/app/components/manage/saved-views/saved-views.component.ts - 163 + 165 diff --git a/src-ui/src/app/app.component.html b/src-ui/src/app/app.component.html index 5ffe4aebe..40322c0e3 100644 --- a/src-ui/src/app/app.component.html +++ b/src-ui/src/app/app.component.html @@ -1,4 +1,4 @@ - + diff --git a/src-ui/src/app/app.component.spec.ts b/src-ui/src/app/app.component.spec.ts index bc59f78dc..b4e46e3ed 100644 --- a/src-ui/src/app/app.component.spec.ts +++ b/src-ui/src/app/app.component.spec.ts @@ -14,14 +14,17 @@ import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap' import { Subject } from 'rxjs' import { routes } from './app-routing.module' import { AppComponent } from './app.component' -import { ToastsComponent } from './components/common/toasts/toasts.component' +import { NotificationListComponent } from './components/common/notification-list/notification-list.component' import { FileDropComponent } from './components/file-drop/file-drop.component' import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard' import { PermissionsGuard } from './guards/permissions.guard' import { HotKeyService } from './services/hot-key.service' +import { + Notification, + NotificationService, +} from './services/notification.service' import { PermissionsService } from './services/permissions.service' import { SettingsService } from './services/settings.service' -import { Toast, ToastService } from './services/toast.service' import { FileStatus, WebsocketStatusService, @@ -33,7 +36,7 @@ describe('AppComponent', () => { let tourService: TourService let websocketStatusService: WebsocketStatusService let permissionsService: PermissionsService - let toastService: ToastService + let notificationService: NotificationService let router: Router let settingsService: SettingsService let hotKeyService: HotKeyService @@ -46,7 +49,7 @@ describe('AppComponent', () => { NgxFileDropModule, NgbModalModule, AppComponent, - ToastsComponent, + NotificationListComponent, FileDropComponent, NgxBootstrapIconsModule.pick(allIcons), ], @@ -62,7 +65,7 @@ describe('AppComponent', () => { websocketStatusService = TestBed.inject(WebsocketStatusService) permissionsService = TestBed.inject(PermissionsService) settingsService = TestBed.inject(SettingsService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) router = TestBed.inject(Router) hotKeyService = TestBed.inject(HotKeyService) fixture = TestBed.createComponent(AppComponent) @@ -82,12 +85,14 @@ describe('AppComponent', () => { expect(document.body.classList).not.toContain('tour-active') })) - it('should display toast on document consumed with link if user has access', () => { + it('should display notification on document consumed with link if user has access', () => { const navigateSpy = jest.spyOn(router, 'navigate') jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) - let toast: Toast - toastService.getToasts().subscribe((toasts) => (toast = toasts[0])) - const toastSpy = jest.spyOn(toastService, 'show') + let notification: Notification + notificationService + .getNotifications() + .subscribe((notifications) => (notification = notifications[0])) + const notificationSpy = jest.spyOn(notificationService, 'show') const fileStatusSubject = new Subject() jest .spyOn(websocketStatusService, 'onDocumentConsumptionFinished') @@ -96,63 +101,65 @@ describe('AppComponent', () => { const status = new FileStatus() status.documentId = 1 fileStatusSubject.next(status) - expect(toastSpy).toHaveBeenCalled() - expect(toast.action).not.toBeUndefined() - toast.action() + expect(notificationSpy).toHaveBeenCalled() + expect(notification.action).not.toBeUndefined() + notification.action() expect(navigateSpy).toHaveBeenCalledWith(['documents', status.documentId]) }) - it('should display toast on document consumed without link if user does not have access', () => { + it('should display notification on document consumed without link if user does not have access', () => { jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false) - let toast: Toast - toastService.getToasts().subscribe((toasts) => (toast = toasts[0])) - const toastSpy = jest.spyOn(toastService, 'show') + let notification: Notification + notificationService + .getNotifications() + .subscribe((notifications) => (notification = notifications[0])) + const notificationSpy = jest.spyOn(notificationService, 'show') const fileStatusSubject = new Subject() jest .spyOn(websocketStatusService, 'onDocumentConsumptionFinished') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) - expect(toastSpy).toHaveBeenCalled() - expect(toast.action).toBeUndefined() + expect(notificationSpy).toHaveBeenCalled() + expect(notification.action).toBeUndefined() }) - it('should display toast on document added', () => { + it('should display notification on document added', () => { jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) - const toastSpy = jest.spyOn(toastService, 'show') + const notificationSpy = jest.spyOn(notificationService, 'show') const fileStatusSubject = new Subject() jest .spyOn(websocketStatusService, 'onDocumentDetected') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should suppress dashboard notifications if set', () => { jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest.spyOn(settingsService, 'get').mockReturnValue(true) jest.spyOn(router, 'url', 'get').mockReturnValue('/dashboard') - const toastSpy = jest.spyOn(toastService, 'show') + const notificationSpy = jest.spyOn(notificationService, 'show') const fileStatusSubject = new Subject() jest .spyOn(websocketStatusService, 'onDocumentDetected') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) - expect(toastSpy).not.toHaveBeenCalled() + expect(notificationSpy).not.toHaveBeenCalled() }) - it('should display toast on document failed', () => { + it('should display notification on document failed', () => { jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') const fileStatusSubject = new Subject() jest .spyOn(websocketStatusService, 'onDocumentConsumptionFailed') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should support hotkeys', () => { diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index a6c4702b7..51de90414 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -2,11 +2,12 @@ import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core' import { Router, RouterOutlet } from '@angular/router' import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap' import { first, Subscription } from 'rxjs' -import { ToastsComponent } from './components/common/toasts/toasts.component' +import { NotificationListComponent } from './components/common/notification-list/notification-list.component' import { FileDropComponent } from './components/file-drop/file-drop.component' import { SETTINGS_KEYS } from './data/ui-settings' import { ComponentRouterService } from './services/component-router.service' import { HotKeyService } from './services/hot-key.service' +import { NotificationService } from './services/notification.service' import { PermissionAction, PermissionsService, @@ -14,7 +15,6 @@ import { } from './services/permissions.service' import { SettingsService } from './services/settings.service' import { TasksService } from './services/tasks.service' -import { ToastService } from './services/toast.service' import { WebsocketStatusService } from './services/websocket-status.service' @Component({ @@ -23,7 +23,7 @@ import { WebsocketStatusService } from './services/websocket-status.service' styleUrls: ['./app.component.scss'], imports: [ FileDropComponent, - ToastsComponent, + NotificationListComponent, TourNgBootstrapModule, RouterOutlet, ], @@ -36,7 +36,7 @@ export class AppComponent implements OnInit, OnDestroy { constructor( private settings: SettingsService, private websocketStatusService: WebsocketStatusService, - private toastService: ToastService, + private notificationService: NotificationService, private router: Router, private tasksService: TasksService, public tourService: TourService, @@ -91,7 +91,7 @@ export class AppComponent implements OnInit, OnDestroy { PermissionType.Document ) ) { - this.toastService.show({ + this.notificationService.show({ content: $localize`Document ${status.filename} was added to Paperless-ngx.`, delay: 10000, actionName: $localize`Open document`, @@ -100,7 +100,7 @@ export class AppComponent implements OnInit, OnDestroy { }, }) } else { - this.toastService.show({ + this.notificationService.show({ content: $localize`Document ${status.filename} was added to Paperless-ngx.`, delay: 10000, }) @@ -115,7 +115,7 @@ export class AppComponent implements OnInit, OnDestroy { if ( this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED) ) { - this.toastService.showError( + this.notificationService.showError( $localize`Could not add ${status.filename}\: ${status.message}` ) } @@ -130,7 +130,7 @@ export class AppComponent implements OnInit, OnDestroy { SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT ) ) { - this.toastService.show({ + this.notificationService.show({ content: $localize`Document ${status.filename} is being processed by Paperless-ngx.`, delay: 5000, }) diff --git a/src-ui/src/app/components/admin/config/config.component.spec.ts b/src-ui/src/app/components/admin/config/config.component.spec.ts index 191532590..818d49111 100644 --- a/src-ui/src/app/components/admin/config/config.component.spec.ts +++ b/src-ui/src/app/components/admin/config/config.component.spec.ts @@ -10,8 +10,8 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' import { OutputTypeConfig } from 'src/app/data/paperless-config' import { ConfigService } from 'src/app/services/config.service' +import { NotificationService } from 'src/app/services/notification.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { FileComponent } from '../../common/input/file/file.component' import { NumberComponent } from '../../common/input/number/number.component' import { SelectComponent } from '../../common/input/select/select.component' @@ -24,7 +24,7 @@ describe('ConfigComponent', () => { let component: ConfigComponent let fixture: ComponentFixture let configService: ConfigService - let toastService: ToastService + let notificationService: NotificationService let settingService: SettingsService beforeEach(async () => { @@ -51,7 +51,7 @@ describe('ConfigComponent', () => { }).compileComponents() configService = TestBed.inject(ConfigService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) settingService = TestBed.inject(SettingsService) fixture = TestBed.createComponent(ConfigComponent) component = fixture.componentInstance @@ -60,7 +60,7 @@ describe('ConfigComponent', () => { it('should load config on init, show error if necessary', () => { const getSpy = jest.spyOn(configService, 'getConfig') - const errorSpy = jest.spyOn(toastService, 'showError') + const errorSpy = jest.spyOn(notificationService, 'showError') getSpy.mockReturnValueOnce( throwError(() => new Error('Error getting config')) ) @@ -78,7 +78,7 @@ describe('ConfigComponent', () => { it('should save config, show error if necessary', () => { const saveSpy = jest.spyOn(configService, 'saveConfig') - const errorSpy = jest.spyOn(toastService, 'showError') + const errorSpy = jest.spyOn(notificationService, 'showError') saveSpy.mockReturnValueOnce( throwError(() => new Error('Error saving config')) ) @@ -112,7 +112,7 @@ describe('ConfigComponent', () => { it('should upload file, show error if necessary', () => { const uploadSpy = jest.spyOn(configService, 'uploadFile') - const errorSpy = jest.spyOn(toastService, 'showError') + const errorSpy = jest.spyOn(notificationService, 'showError') uploadSpy.mockReturnValueOnce( throwError(() => new Error('Error uploading file')) ) diff --git a/src-ui/src/app/components/admin/config/config.component.ts b/src-ui/src/app/components/admin/config/config.component.ts index 76f6b8795..f89959abf 100644 --- a/src-ui/src/app/components/admin/config/config.component.ts +++ b/src-ui/src/app/components/admin/config/config.component.ts @@ -25,8 +25,8 @@ import { PaperlessConfigOptions, } from 'src/app/data/paperless-config' import { ConfigService } from 'src/app/services/config.service' +import { NotificationService } from 'src/app/services/notification.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { FileComponent } from '../../common/input/file/file.component' import { NumberComponent } from '../../common/input/number/number.component' import { SelectComponent } from '../../common/input/select/select.component' @@ -79,7 +79,7 @@ export class ConfigComponent constructor( private configService: ConfigService, - private toastService: ToastService, + private notificationService: NotificationService, private settingsService: SettingsService ) { super() @@ -100,7 +100,10 @@ export class ConfigComponent }, error: (e) => { this.loading = false - this.toastService.showError($localize`Error retrieving config`, e) + this.notificationService.showError( + $localize`Error retrieving config`, + e + ) }, }) @@ -170,11 +173,11 @@ export class ConfigComponent this.initialize(config) this.store.next(config) this.settingsService.initializeSettings().subscribe() - this.toastService.showInfo($localize`Configuration updated`) + this.notificationService.showInfo($localize`Configuration updated`) }, error: (e) => { this.loading = false - this.toastService.showError( + this.notificationService.showError( $localize`An error occurred updating configuration`, e ) @@ -197,11 +200,13 @@ export class ConfigComponent this.initialize(config) this.store.next(config) this.settingsService.initializeSettings().subscribe() - this.toastService.showInfo($localize`File successfully updated`) + this.notificationService.showInfo( + $localize`File successfully updated` + ) }, error: (e) => { this.loading = false - this.toastService.showError( + this.notificationService.showError( $localize`An error occurred uploading file`, e ) diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index c6eeaf896..28d0c9e1b 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -29,12 +29,15 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' +import { + Notification, + NotificationService, +} from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { GroupService } from 'src/app/services/rest/group.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' import { SystemStatusService } from 'src/app/services/system-status.service' -import { Toast, ToastService } from 'src/app/services/toast.service' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { CheckComponent } from '../../common/input/check/check.component' @@ -66,7 +69,7 @@ describe('SettingsComponent', () => { let settingsService: SettingsService let activatedRoute: ActivatedRoute let viewportScroller: ViewportScroller - let toastService: ToastService + let notificationService: NotificationService let userService: UserService let permissionsService: PermissionsService let groupService: GroupService @@ -115,7 +118,7 @@ describe('SettingsComponent', () => { router = TestBed.inject(Router) activatedRoute = TestBed.inject(ActivatedRoute) viewportScroller = TestBed.inject(ViewportScroller) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) settingsService = TestBed.inject(SettingsService) settingsService.currentUser = users[0] userService = TestBed.inject(UserService) @@ -194,8 +197,8 @@ describe('SettingsComponent', () => { it('should support save local settings updating appearance settings and calling API, show error', () => { completeSetup() - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastSpy = jest.spyOn(toastService, 'show') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'show') const storeSpy = jest.spyOn(settingsService, 'storeSettings') const appearanceSettingsSpy = jest.spyOn( settingsService, @@ -209,7 +212,7 @@ describe('SettingsComponent', () => { ) component.saveSettings() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled() expect(setSpy).toHaveBeenCalledTimes(29) @@ -217,14 +220,14 @@ describe('SettingsComponent', () => { // succeed storeSpy.mockReturnValueOnce(of(true)) component.saveSettings() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() expect(appearanceSettingsSpy).toHaveBeenCalled() }) it('should offer reload if settings changes require', () => { completeSetup() - let toast: Toast - toastService.getToasts().subscribe((t) => (toast = t[0])) + let toast: Notification + notificationService.getNotifications().subscribe((t) => (toast = t[0])) component.initialize(true) // reset component.store.getValue()['displayLanguage'] = 'en-US' component.store.getValue()['updateCheckingEnabled'] = false @@ -258,7 +261,7 @@ describe('SettingsComponent', () => { }) it('should show errors on load if load users failure', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(userService, 'listAll') .mockImplementation(() => @@ -266,11 +269,11 @@ describe('SettingsComponent', () => { ) completeSetup(userService) fixture.detectChanges() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() }) it('should show errors on load if load groups failure', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(groupService, 'listAll') .mockImplementation(() => @@ -278,7 +281,7 @@ describe('SettingsComponent', () => { ) completeSetup(groupService) fixture.detectChanges() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() }) it('should load system status on initialize, show errors if needed', () => { diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts index 8737be160..14f58bf23 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.ts @@ -43,6 +43,10 @@ import { User } from 'src/app/data/user' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { + Notification, + NotificationService, +} from 'src/app/services/notification.service' import { PermissionAction, PermissionType, @@ -55,7 +59,6 @@ import { SettingsService, } from 'src/app/services/settings.service' import { SystemStatusService } from 'src/app/services/system-status.service' -import { Toast, ToastService } from 'src/app/services/toast.service' import { CheckComponent } from '../../common/input/check/check.component' import { ColorComponent } from '../../common/input/color/color.component' import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component' @@ -181,7 +184,7 @@ export class SettingsComponent constructor( private documentListViewService: DocumentListViewService, - private toastService: ToastService, + private notificationService: NotificationService, private settings: SettingsService, @Inject(LOCALE_ID) public currentLocale: string, private viewportScroller: ViewportScroller, @@ -217,7 +220,10 @@ export class SettingsComponent this.users = r.results }, error: (e) => { - this.toastService.showError($localize`Error retrieving users`, e) + this.notificationService.showError( + $localize`Error retrieving users`, + e + ) }, }) } @@ -236,7 +242,10 @@ export class SettingsComponent this.groups = r.results }, error: (e) => { - this.toastService.showError($localize`Error retrieving groups`, e) + this.notificationService.showError( + $localize`Error retrieving groups`, + e + ) }, }) } @@ -531,7 +540,7 @@ export class SettingsComponent this.store.next(this.settingsForm.value) this.settings.updateAppearanceSettings() this.settings.initializeDisplayFields() - let savedToast: Toast = { + let savedToast: Notification = { content: $localize`Settings were saved successfully.`, delay: 5000, } @@ -543,10 +552,10 @@ export class SettingsComponent } } - this.toastService.show(savedToast) + this.notificationService.show(savedToast) }, error: (error) => { - this.toastService.showError( + this.notificationService.showError( $localize`An error occurred while saving settings.`, error ) diff --git a/src-ui/src/app/components/admin/trash/trash.component.spec.ts b/src-ui/src/app/components/admin/trash/trash.component.spec.ts index aa5a8af0f..fd9d8020c 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.spec.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.spec.ts @@ -12,7 +12,7 @@ import { import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' -import { ToastService } from 'src/app/services/toast.service' +import { NotificationService } from 'src/app/services/notification.service' import { TrashService } from 'src/app/services/trash.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' @@ -38,7 +38,7 @@ describe('TrashComponent', () => { let fixture: ComponentFixture let trashService: TrashService let modalService: NgbModal - let toastService: ToastService + let notificationService: NotificationService let router: Router beforeEach(async () => { @@ -60,7 +60,7 @@ describe('TrashComponent', () => { fixture = TestBed.createComponent(TrashComponent) trashService = TestBed.inject(TrashService) modalService = TestBed.inject(NgbModal) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) router = TestBed.inject(Router) component = fixture.componentInstance fixture.detectChanges() @@ -88,13 +88,13 @@ describe('TrashComponent', () => { modalService.activeInstances.subscribe((instances) => { modal = instances[0] }) - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') // fail first trashSpy.mockReturnValue(throwError(() => 'Error')) component.delete(documentsInTrash[0]) modal.componentInstance.confirmClicked.next() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() trashSpy.mockReturnValue(of('OK')) component.delete(documentsInTrash[0]) @@ -109,13 +109,13 @@ describe('TrashComponent', () => { modalService.activeInstances.subscribe((instances) => { modal = instances[instances.length - 1] }) - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') // fail first trashSpy.mockReturnValue(throwError(() => 'Error')) component.emptyTrash() modal.componentInstance.confirmClicked.next() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() trashSpy.mockReturnValue(of('OK')) component.emptyTrash() @@ -131,12 +131,12 @@ describe('TrashComponent', () => { it('should support restore document, show error if needed', () => { const restoreSpy = jest.spyOn(trashService, 'restoreDocuments') const reloadSpy = jest.spyOn(component, 'reload') - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') // fail first restoreSpy.mockReturnValue(throwError(() => 'Error')) component.restore(documentsInTrash[0]) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() restoreSpy.mockReturnValue(of('OK')) @@ -148,12 +148,12 @@ describe('TrashComponent', () => { it('should support restore all documents, show error if needed', () => { const restoreSpy = jest.spyOn(trashService, 'restoreDocuments') const reloadSpy = jest.spyOn(component, 'reload') - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') // fail first restoreSpy.mockReturnValue(throwError(() => 'Error')) component.restoreAll() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() restoreSpy.mockReturnValue(of('OK')) @@ -167,7 +167,7 @@ describe('TrashComponent', () => { it('should offer link to restored document', () => { let toasts const navigateSpy = jest.spyOn(router, 'navigate') - toastService.getToasts().subscribe((allToasts) => { + notificationService.getNotifications().subscribe((allToasts) => { toasts = [...allToasts] }) jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK')) diff --git a/src-ui/src/app/components/admin/trash/trash.component.ts b/src-ui/src/app/components/admin/trash/trash.component.ts index 1df6ceff4..625fa146f 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.ts @@ -10,8 +10,8 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { delay, takeUntil, tap } from 'rxjs' import { Document } from 'src/app/data/document' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { NotificationService } from 'src/app/services/notification.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { TrashService } from 'src/app/services/trash.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' @@ -44,7 +44,7 @@ export class TrashComponent constructor( private trashService: TrashService, - private toastService: ToastService, + private notificationService: NotificationService, private modalService: NgbModal, private settingsService: SettingsService, private router: Router @@ -86,14 +86,14 @@ export class TrashComponent modal.componentInstance.buttonsEnabled = false this.trashService.emptyTrash([document.id]).subscribe({ next: () => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Document "${document.title}" deleted` ) modal.close() this.reload() }, error: (err) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting document "${document.title}"`, err ) @@ -121,13 +121,13 @@ export class TrashComponent .emptyTrash(documents ? Array.from(documents) : null) .subscribe({ next: () => { - this.toastService.showInfo($localize`Document(s) deleted`) + this.notificationService.showInfo($localize`Document(s) deleted`) this.allToggled = false modal.close() this.reload() }, error: (err) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting document(s)`, err ) @@ -140,7 +140,7 @@ export class TrashComponent restore(document: Document) { this.trashService.restoreDocuments([document.id]).subscribe({ next: () => { - this.toastService.show({ + this.notificationService.show({ content: $localize`Document "${document.title}" restored`, delay: 5000, actionName: $localize`Open document`, @@ -151,7 +151,7 @@ export class TrashComponent this.reload() }, error: (err) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error restoring document "${document.title}"`, err ) @@ -164,12 +164,12 @@ export class TrashComponent .restoreDocuments(documents ? Array.from(documents) : null) .subscribe({ next: () => { - this.toastService.showInfo($localize`Document(s) restored`) + this.notificationService.showInfo($localize`Document(s) restored`) this.allToggled = false this.reload() }, error: (err) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error restoring document(s)`, err ) diff --git a/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts b/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts index 559b03f51..5c95aa8a2 100644 --- a/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts +++ b/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts @@ -14,11 +14,11 @@ import { Group } from 'src/app/data/group' import { User } from 'src/app/data/user' import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { GroupService } from 'src/app/services/rest/group.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component' import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component' @@ -38,7 +38,7 @@ describe('UsersAndGroupsComponent', () => { let fixture: ComponentFixture let settingsService: SettingsService let modalService: NgbModal - let toastService: ToastService + let notificationService: NotificationService let userService: UserService let permissionsService: PermissionsService let groupService: GroupService @@ -59,7 +59,7 @@ describe('UsersAndGroupsComponent', () => { settingsService.currentUser = users[0] userService = TestBed.inject(UserService) modalService = TestBed.inject(NgbModal) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) permissionsService = TestBed.inject(PermissionsService) jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest @@ -104,13 +104,13 @@ describe('UsersAndGroupsComponent', () => { modalService.activeInstances.subscribe((refs) => (modal = refs[0])) component.editUser(users[0]) const editDialog = modal.componentInstance as UserEditDialogComponent - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') editDialog.failed.emit() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() settingsService.currentUser = users[1] // simulate logged in as different user editDialog.succeeded.emit(users[0]) - expect(toastInfoSpy).toHaveBeenCalledWith( + expect(notificationInfoSpy).toHaveBeenCalledWith( `Saved user "${users[0].username}".` ) component.editUser() @@ -123,18 +123,18 @@ describe('UsersAndGroupsComponent', () => { component.deleteUser(users[0]) const deleteDialog = modal.componentInstance as ConfirmDialogComponent const deleteSpy = jest.spyOn(userService, 'delete') - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const listAllSpy = jest.spyOn(userService, 'listAll') deleteSpy.mockReturnValueOnce( throwError(() => new Error('error deleting user')) ) deleteDialog.confirm() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user "user1"') + expect(notificationInfoSpy).toHaveBeenCalledWith('Deleted user "user1"') }) it('should logout current user if password changed, after delay', fakeAsync(() => { @@ -163,12 +163,12 @@ describe('UsersAndGroupsComponent', () => { modalService.activeInstances.subscribe((refs) => (modal = refs[0])) component.editGroup(groups[0]) const editDialog = modal.componentInstance as GroupEditDialogComponent - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') editDialog.failed.emit() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() editDialog.succeeded.emit(groups[0]) - expect(toastInfoSpy).toHaveBeenCalledWith( + expect(notificationInfoSpy).toHaveBeenCalledWith( `Saved group "${groups[0].name}".` ) component.editGroup() @@ -181,18 +181,18 @@ describe('UsersAndGroupsComponent', () => { component.deleteGroup(groups[0]) const deleteDialog = modal.componentInstance as ConfirmDialogComponent const deleteSpy = jest.spyOn(groupService, 'delete') - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const listAllSpy = jest.spyOn(groupService, 'listAll') deleteSpy.mockReturnValueOnce( throwError(() => new Error('error deleting group')) ) deleteDialog.confirm() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group "group1"') + expect(notificationInfoSpy).toHaveBeenCalledWith('Deleted group "group1"') }) it('should get group name', () => { @@ -202,7 +202,7 @@ describe('UsersAndGroupsComponent', () => { }) it('should show errors on load if load users failure', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(userService, 'listAll') .mockImplementation(() => @@ -210,11 +210,11 @@ describe('UsersAndGroupsComponent', () => { ) completeSetup(userService) fixture.detectChanges() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() }) it('should show errors on load if load groups failure', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(groupService, 'listAll') .mockImplementation(() => @@ -222,6 +222,6 @@ describe('UsersAndGroupsComponent', () => { ) completeSetup(groupService) fixture.detectChanges() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() }) }) diff --git a/src-ui/src/app/components/admin/users-groups/users-groups.component.ts b/src-ui/src/app/components/admin/users-groups/users-groups.component.ts index 9ed73cde4..3d3f27fa9 100644 --- a/src-ui/src/app/components/admin/users-groups/users-groups.component.ts +++ b/src-ui/src/app/components/admin/users-groups/users-groups.component.ts @@ -5,11 +5,11 @@ import { Subject, first, takeUntil } from 'rxjs' import { Group } from 'src/app/data/group' import { User } from 'src/app/data/user' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { GroupService } from 'src/app/services/rest/group.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component' @@ -39,7 +39,7 @@ export class UsersAndGroupsComponent constructor( private usersService: UserService, private groupsService: GroupService, - private toastService: ToastService, + private notificationService: NotificationService, private modalService: NgbModal, public permissionsService: PermissionsService, private settings: SettingsService @@ -56,7 +56,10 @@ export class UsersAndGroupsComponent this.users = r.results }, error: (e) => { - this.toastService.showError($localize`Error retrieving users`, e) + this.notificationService.showError( + $localize`Error retrieving users`, + e + ) }, }) @@ -68,7 +71,10 @@ export class UsersAndGroupsComponent this.groups = r.results }, error: (e) => { - this.toastService.showError($localize`Error retrieving groups`, e) + this.notificationService.showError( + $localize`Error retrieving groups`, + e + ) }, }) } @@ -93,14 +99,14 @@ export class UsersAndGroupsComponent newUser.id === this.settings.currentUser.id && (modal.componentInstance as UserEditDialogComponent).passwordIsSet ) { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Password has been changed, you will be logged out momentarily.` ) setTimeout(() => { window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/` }, 2500) } else { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Saved user "${newUser.username}".` ) this.usersService.listAll().subscribe((r) => { @@ -111,7 +117,7 @@ export class UsersAndGroupsComponent modal.componentInstance.failed .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((e) => { - this.toastService.showError($localize`Error saving user.`, e) + this.notificationService.showError($localize`Error saving user.`, e) }) } @@ -129,13 +135,15 @@ export class UsersAndGroupsComponent this.usersService.delete(user).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted user "${user.username}"`) + this.notificationService.showInfo( + $localize`Deleted user "${user.username}"` + ) this.usersService.listAll().subscribe((r) => { this.users = r.results }) }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting user "${user.username}".`, e ) @@ -156,7 +164,9 @@ export class UsersAndGroupsComponent modal.componentInstance.succeeded .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((newGroup) => { - this.toastService.showInfo($localize`Saved group "${newGroup.name}".`) + this.notificationService.showInfo( + $localize`Saved group "${newGroup.name}".` + ) this.groupsService.listAll().subscribe((r) => { this.groups = r.results }) @@ -164,7 +174,7 @@ export class UsersAndGroupsComponent modal.componentInstance.failed .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((e) => { - this.toastService.showError($localize`Error saving group.`, e) + this.notificationService.showError($localize`Error saving group.`, e) }) } @@ -182,13 +192,15 @@ export class UsersAndGroupsComponent this.groupsService.delete(group).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted group "${group.name}"`) + this.notificationService.showInfo( + $localize`Deleted group "${group.name}"` + ) this.groupsService.listAll().subscribe((r) => { this.groups = r.results }) }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting group "${group.name}".`, e ) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index b3d515274..d2fd2c34e 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -30,7 +30,7 @@ - + diff --git a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts index f1d54ba70..bb57b94ff 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts @@ -26,13 +26,13 @@ import { DjangoMessageLevel, DjangoMessagesService, } from 'src/app/services/django-messages.service' +import { NotificationService } from 'src/app/services/notification.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { PermissionsService } from 'src/app/services/permissions.service' import { RemoteVersionService } from 'src/app/services/rest/remote-version.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SearchService } from 'src/app/services/rest/search.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component' import { DocumentDetailComponent } from '../document-detail/document-detail.component' @@ -86,7 +86,7 @@ describe('AppFrameComponent', () => { let settingsService: SettingsService let permissionsService: PermissionsService let remoteVersionService: RemoteVersionService - let toastService: ToastService + let notificationService: NotificationService let messagesService: DjangoMessagesService let openDocumentsService: OpenDocumentsService let router: Router @@ -126,7 +126,7 @@ describe('AppFrameComponent', () => { PermissionsService, RemoteVersionService, IfPermissionsDirective, - ToastService, + NotificationService, DjangoMessagesService, OpenDocumentsService, SearchService, @@ -157,7 +157,7 @@ describe('AppFrameComponent', () => { const savedViewService = TestBed.inject(SavedViewService) permissionsService = TestBed.inject(PermissionsService) remoteVersionService = TestBed.inject(RemoteVersionService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) messagesService = TestBed.inject(DjangoMessagesService) openDocumentsService = TestBed.inject(OpenDocumentsService) modalService = TestBed.inject(NgbModal) @@ -216,7 +216,7 @@ describe('AppFrameComponent', () => { it('should show error on toggle update checking if store settings fails', () => { jest.spyOn(console, 'warn').mockImplementation(() => {}) - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false) component.setUpdateChecking(true) httpTestingController @@ -225,7 +225,7 @@ describe('AppFrameComponent', () => { status: 500, statusText: 'error', }) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should support toggling slim sidebar and saving', fakeAsync(() => { @@ -245,7 +245,7 @@ describe('AppFrameComponent', () => { it('should show error on toggle slim sidebar if store settings fails', () => { jest.spyOn(console, 'warn').mockImplementation(() => {}) - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') component.toggleSlimSidebar() httpTestingController .expectOne(`${environment.apiBaseUrl}ui_settings/`) @@ -253,7 +253,7 @@ describe('AppFrameComponent', () => { status: 500, statusText: 'error', }) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should support collapsible menu', () => { @@ -305,7 +305,7 @@ describe('AppFrameComponent', () => { it('should update saved view sorting on drag + drop, show info', () => { const settingsSpy = jest.spyOn(settingsService, 'updateSidebarViewsSort') - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true)) component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop< SavedView[] @@ -315,7 +315,7 @@ describe('AppFrameComponent', () => { saved_views[0], saved_views[3], ]) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should update saved view sorting on drag + drop, show error', () => { @@ -326,14 +326,14 @@ describe('AppFrameComponent', () => { fixture = TestBed.createComponent(AppFrameComponent) component = fixture.componentInstance fixture.detectChanges() - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(settingsService, 'storeSettings') .mockReturnValue(throwError(() => new Error('unable to save'))) component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop< SavedView[] >) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should support edit profile', () => { @@ -345,9 +345,9 @@ describe('AppFrameComponent', () => { }) }) - it('should show toasts for django messages', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + it('should show notifications for django messages', () => { + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') jest.spyOn(messagesService, 'get').mockReturnValue([ { level: DjangoMessageLevel.WARNING, message: 'Test warning' }, { level: DjangoMessageLevel.ERROR, message: 'Test error' }, @@ -356,7 +356,7 @@ describe('AppFrameComponent', () => { { level: DjangoMessageLevel.DEBUG, message: 'Test debug' }, ]) component.ngOnInit() - expect(toastErrorSpy).toHaveBeenCalledTimes(2) - expect(toastInfoSpy).toHaveBeenCalledTimes(3) + expect(notificationErrorSpy).toHaveBeenCalledTimes(2) + expect(notificationInfoSpy).toHaveBeenCalledTimes(3) }) }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index fabcbf7d1..9ea0a6aea 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -29,6 +29,7 @@ import { DjangoMessageLevel, DjangoMessagesService, } from 'src/app/services/django-messages.service' +import { NotificationService } from 'src/app/services/notification.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { PermissionAction, @@ -42,13 +43,12 @@ import { import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' import { TasksService } from 'src/app/services/tasks.service' -import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component' import { DocumentDetailComponent } from '../document-detail/document-detail.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' import { GlobalSearchComponent } from './global-search/global-search.component' -import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component' +import { NotificationsDropdownComponent } from './notifications-dropdown/notifications-dropdown.component' @Component({ selector: 'pngx-app-frame', @@ -58,7 +58,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo GlobalSearchComponent, DocumentTitlePipe, IfPermissionsDirective, - ToastsDropdownComponent, + NotificationsDropdownComponent, RouterModule, NgClass, NgbDropdownModule, @@ -89,7 +89,7 @@ export class AppFrameComponent private remoteVersionService: RemoteVersionService, public settingsService: SettingsService, public tasksService: TasksService, - private readonly toastService: ToastService, + private readonly notificationService: NotificationService, private modalService: NgbModal, public permissionsService: PermissionsService, private djangoMessagesService: DjangoMessagesService @@ -123,12 +123,12 @@ export class AppFrameComponent switch (message.level) { case DjangoMessageLevel.ERROR: case DjangoMessageLevel.WARNING: - this.toastService.showError(message.message) + this.notificationService.showError(message.message) break case DjangoMessageLevel.SUCCESS: case DjangoMessageLevel.INFO: case DjangoMessageLevel.DEBUG: - this.toastService.showInfo(message.message) + this.notificationService.showInfo(message.message) break } }) @@ -157,7 +157,7 @@ export class AppFrameComponent .pipe(first()) .subscribe({ error: (error) => { - this.toastService.showError( + this.notificationService.showError( $localize`An error occurred while saving settings.` ) console.warn(error) @@ -242,10 +242,13 @@ export class AppFrameComponent this.settingsService.updateSidebarViewsSort(sidebarViews).subscribe({ next: () => { - this.toastService.showInfo($localize`Sidebar views updated`) + this.notificationService.showInfo($localize`Sidebar views updated`) }, error: (e) => { - this.toastService.showError($localize`Error updating sidebar views`, e) + this.notificationService.showError( + $localize`Error updating sidebar views`, + e + ) }, }) } @@ -265,7 +268,7 @@ export class AppFrameComponent .pipe(first()) .subscribe({ error: (error) => { - this.toastService.showError( + this.notificationService.showError( $localize`An error occurred while saving update checking settings.` ) console.warn(error) diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts index db407c228..e09d2e667 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts @@ -28,10 +28,10 @@ import { } from 'src/app/data/filter-rule-type' import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { DocumentService } from 'src/app/services/rest/document.service' import { SearchService } from 'src/app/services/rest/search.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { queryParamsFromFilterRules } from 'src/app/utils/query-params' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' @@ -133,7 +133,7 @@ describe('GlobalSearchComponent', () => { let modalService: NgbModal let documentService: DocumentService let documentListViewService: DocumentListViewService - let toastService: ToastService + let notificationService: NotificationService let settingsService: SettingsService beforeEach(async () => { @@ -157,7 +157,7 @@ describe('GlobalSearchComponent', () => { modalService = TestBed.inject(NgbModal) documentService = TestBed.inject(DocumentService) documentListViewService = TestBed.inject(DocumentListViewService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) settingsService = TestBed.inject(SettingsService) fixture = TestBed.createComponent(GlobalSearchComponent) @@ -397,16 +397,16 @@ describe('GlobalSearchComponent', () => { }) const editDialog = modal.componentInstance as CustomFieldEditDialogComponent - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') // fail first editDialog.failed.emit({ error: 'error creating item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() // succeed editDialog.succeeded.emit(true) - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() }) it('should support secondary action', () => { @@ -448,16 +448,16 @@ describe('GlobalSearchComponent', () => { }) const editDialog = modal.componentInstance as CustomFieldEditDialogComponent - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') // fail first editDialog.failed.emit({ error: 'error creating item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() // succeed editDialog.succeeded.emit(true) - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() }) it('should support reset', () => { diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts index 8ef466d5b..dcf2c8cfc 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts @@ -31,6 +31,7 @@ import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { HotKeyService } from 'src/app/services/hot-key.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionAction, PermissionsService, @@ -41,7 +42,6 @@ import { SearchService, } from 'src/app/services/rest/search.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { paramsFromViewState } from 'src/app/utils/query-params' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' @@ -97,7 +97,7 @@ export class GlobalSearchComponent implements OnInit { private documentService: DocumentService, private documentListViewService: DocumentListViewService, private permissionsService: PermissionsService, - private toastService: ToastService, + private notificationService: NotificationService, private hotkeyService: HotKeyService, private settingsService: SettingsService ) { @@ -206,10 +206,15 @@ export class GlobalSearchComponent implements OnInit { modalRef.componentInstance.dialogMode = EditDialogMode.EDIT modalRef.componentInstance.object = object modalRef.componentInstance.succeeded.subscribe(() => { - this.toastService.showInfo($localize`Successfully updated object.`) + this.notificationService.showInfo( + $localize`Successfully updated object.` + ) }) modalRef.componentInstance.failed.subscribe((e) => { - this.toastService.showError($localize`Error occurred saving object.`, e) + this.notificationService.showError( + $localize`Error occurred saving object.`, + e + ) }) } } @@ -244,10 +249,15 @@ export class GlobalSearchComponent implements OnInit { modalRef.componentInstance.dialogMode = EditDialogMode.EDIT modalRef.componentInstance.object = object modalRef.componentInstance.succeeded.subscribe(() => { - this.toastService.showInfo($localize`Successfully updated object.`) + this.notificationService.showInfo( + $localize`Successfully updated object.` + ) }) modalRef.componentInstance.failed.subscribe((e) => { - this.toastService.showError($localize`Error occurred saving object.`, e) + this.notificationService.showError( + $localize`Error occurred saving object.`, + e + ) }) } } diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.html similarity index 61% rename from src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html rename to src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.html index 6e49c1763..5710e67ef 100644 --- a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html +++ b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.html @@ -1,7 +1,7 @@ - @if (toasts.length) { - {{ toasts.length }} + @if (notifications.length) { + {{ notifications.length }} } @@ -11,17 +11,17 @@ Notifications Clear All - @if (toasts.length === 0) { + @if (notifications.length === 0) { No notifications } - @for (toast of toasts; track toast.id) { - + @for (notification of notifications; track notification.id) { + } diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.scss b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.scss similarity index 86% rename from src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.scss rename to src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.scss index 2332e710d..87bddd134 100644 --- a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.scss +++ b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.scss @@ -1,5 +1,5 @@ .dropdown-menu { - width: var(--pngx-toast-max-width); + width: var(--pngx-notification-max-width); } .dropdown-menu .scroll-list { diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.spec.ts similarity index 58% rename from src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts rename to src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.spec.ts index 33b948f30..981c6ae8e 100644 --- a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts +++ b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.spec.ts @@ -9,10 +9,13 @@ import { } from '@angular/core/testing' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { Subject } from 'rxjs' -import { Toast, ToastService } from 'src/app/services/toast.service' -import { ToastsDropdownComponent } from './toasts-dropdown.component' +import { + Notification, + NotificationService, +} from 'src/app/services/notification.service' +import { NotificationsDropdownComponent } from './notifications-dropdown.component' -const toasts = [ +const notifications = [ { id: 'abc-123', content: 'foo bar', @@ -38,16 +41,16 @@ const toasts = [ }, ] -describe('ToastsDropdownComponent', () => { - let component: ToastsDropdownComponent - let fixture: ComponentFixture - let toastService: ToastService - let toastsSubject: Subject = new Subject() +describe('NotificationsDropdownComponent', () => { + let component: NotificationsDropdownComponent + let fixture: ComponentFixture + let notificationService: NotificationService + let notificationsSubject: Subject = new Subject() beforeEach(async () => { TestBed.configureTestingModule({ imports: [ - ToastsDropdownComponent, + NotificationsDropdownComponent, NgxBootstrapIconsModule.pick(allIcons), ], providers: [ @@ -56,24 +59,26 @@ describe('ToastsDropdownComponent', () => { ], }).compileComponents() - fixture = TestBed.createComponent(ToastsDropdownComponent) - toastService = TestBed.inject(ToastService) - jest.spyOn(toastService, 'getToasts').mockReturnValue(toastsSubject) + fixture = TestBed.createComponent(NotificationsDropdownComponent) + notificationService = TestBed.inject(NotificationService) + jest + .spyOn(notificationService, 'getNotifications') + .mockReturnValue(notificationsSubject) component = fixture.componentInstance fixture.detectChanges() }) - it('should call getToasts and return toasts', fakeAsync(() => { - const spy = jest.spyOn(toastService, 'getToasts') + it('should call getNotifications and return notifications', fakeAsync(() => { + const spy = jest.spyOn(notificationService, 'getNotifications') component.ngOnInit() - toastsSubject.next(toasts) + notificationsSubject.next(notifications) fixture.detectChanges() expect(spy).toHaveBeenCalled() - expect(component.toasts).toContainEqual({ + expect(component.notifications).toContainEqual({ id: 'abc-123', content: 'foo bar', delay: 5000, @@ -84,9 +89,9 @@ describe('ToastsDropdownComponent', () => { discardPeriodicTasks() })) - it('should show a toast', fakeAsync(() => { + it('should show a notification', fakeAsync(() => { component.ngOnInit() - toastsSubject.next(toasts) + notificationsSubject.next(notifications) fixture.detectChanges() expect(fixture.nativeElement.textContent).toContain('foo bar') @@ -96,12 +101,16 @@ describe('ToastsDropdownComponent', () => { discardPeriodicTasks() })) - it('should toggle suppressPopupToasts', fakeAsync((finish) => { + it('should toggle suppressPopupNotifications', fakeAsync((finish) => { component.ngOnInit() fixture.detectChanges() - toastsSubject.next(toasts) + notificationsSubject.next(notifications) - const spy = jest.spyOn(toastService, 'suppressPopupToasts', 'set') + const spy = jest.spyOn( + notificationService, + 'suppressPopupNotifications', + 'set' + ) component.onOpenChange(true) expect(spy).toHaveBeenCalledWith(true) diff --git a/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.ts b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.ts new file mode 100644 index 000000000..a610dd7d0 --- /dev/null +++ b/src-ui/src/app/components/app-frame/notifications-dropdown/notifications-dropdown.component.ts @@ -0,0 +1,47 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { + NgbDropdownModule, + NgbProgressbarModule, +} from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subscription } from 'rxjs' +import { + Notification, + NotificationService, +} from 'src/app/services/notification.service' +import { NotificationComponent } from '../../common/notification/notification.component' + +@Component({ + selector: 'pngx-notifications-dropdown', + templateUrl: './notifications-dropdown.component.html', + styleUrls: ['./notifications-dropdown.component.scss'], + imports: [ + NotificationComponent, + NgbDropdownModule, + NgbProgressbarModule, + NgxBootstrapIconsModule, + ], +}) +export class NotificationsDropdownComponent implements OnInit, OnDestroy { + constructor(public notificationService: NotificationService) {} + + private subscription: Subscription + + public notifications: Notification[] = [] + + ngOnDestroy(): void { + this.subscription?.unsubscribe() + } + + ngOnInit(): void { + this.subscription = this.notificationService + .getNotifications() + .subscribe((notifications) => { + this.notifications = [...notifications] + }) + } + + onOpenChange(open: boolean): void { + this.notificationService.suppressPopupNotifications = open + } +} diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.ts b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.ts deleted file mode 100644 index c04d758af..000000000 --- a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' -import { - NgbDropdownModule, - NgbProgressbarModule, -} from '@ng-bootstrap/ng-bootstrap' -import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' -import { Subscription } from 'rxjs' -import { Toast, ToastService } from 'src/app/services/toast.service' -import { ToastComponent } from '../../common/toast/toast.component' - -@Component({ - selector: 'pngx-toasts-dropdown', - templateUrl: './toasts-dropdown.component.html', - styleUrls: ['./toasts-dropdown.component.scss'], - imports: [ - ToastComponent, - NgbDropdownModule, - NgbProgressbarModule, - NgxBootstrapIconsModule, - ], -}) -export class ToastsDropdownComponent implements OnInit, OnDestroy { - constructor(public toastService: ToastService) {} - - private subscription: Subscription - - public toasts: Toast[] = [] - - ngOnDestroy(): void { - this.subscription?.unsubscribe() - } - - ngOnInit(): void { - this.subscription = this.toastService.getToasts().subscribe((toasts) => { - this.toasts = [...toasts] - }) - } - - onOpenChange(open: boolean): void { - this.toastService.suppressPopupToasts = open - } -} diff --git a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts index 78df9c74f..93f9e3571 100644 --- a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts @@ -18,9 +18,9 @@ import { NgSelectModule } from '@ng-select/ng-select' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of } from 'rxjs' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' +import { NotificationService } from 'src/app/services/notification.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { SelectComponent } from '../input/select/select.component' import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component' @@ -42,7 +42,7 @@ describe('CustomFieldsDropdownComponent', () => { let component: CustomFieldsDropdownComponent let fixture: ComponentFixture let customFieldService: CustomFieldsService - let toastService: ToastService + let notificationService: NotificationService let modalService: NgbModal let settingsService: SettingsService @@ -64,7 +64,7 @@ describe('CustomFieldsDropdownComponent', () => { ], }) customFieldService = TestBed.inject(CustomFieldsService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) modalService = TestBed.inject(NgbModal) jest.spyOn(customFieldService, 'listAll').mockReturnValue( of({ @@ -113,8 +113,8 @@ describe('CustomFieldsDropdownComponent', () => { it('should support creating field, show error if necessary, then add', fakeAsync(() => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const getFieldsSpy = jest.spyOn( CustomFieldsDropdownComponent.prototype as any, 'getFields' @@ -129,13 +129,13 @@ describe('CustomFieldsDropdownComponent', () => { // fail first editDialog.failed.emit({ error: 'error creating field' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(getFieldsSpy).not.toHaveBeenCalled() // succeed editDialog.succeeded.emit(fields[0]) tick(100) - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(getFieldsSpy).toHaveBeenCalled() expect(addFieldSpy).toHaveBeenCalled() })) diff --git a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.ts b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.ts index 9e211edd0..ba23d5d05 100644 --- a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.ts +++ b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.ts @@ -14,13 +14,13 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { first, takeUntil } from 'rxjs' import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field' import { CustomFieldInstance } from 'src/app/data/custom-field-instance' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionAction, PermissionType, PermissionsService, } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' -import { ToastService } from 'src/app/services/toast.service' import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' @@ -78,7 +78,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio constructor( private customFieldsService: CustomFieldsService, private modalService: NgbModal, - private toastService: ToastService, + private notificationService: NotificationService, private permissionsService: PermissionsService ) { super() @@ -123,7 +123,9 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio modal.componentInstance.succeeded .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((newField) => { - this.toastService.showInfo($localize`Saved field "${newField.name}".`) + this.notificationService.showInfo( + $localize`Saved field "${newField.name}".` + ) this.customFieldsService.clearCache() this.getFields() this.created.emit(newField) @@ -132,7 +134,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio modal.componentInstance.failed .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((e) => { - this.toastService.showError($localize`Error saving field.`, e) + this.notificationService.showError($localize`Error saving field.`, e) }) } diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts index 9ffa1ea95..b090a28af 100644 --- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.spec.ts @@ -12,11 +12,11 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { GroupService } from 'src/app/services/rest/group.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { PasswordComponent } from '../../input/password/password.component' import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component' import { SelectComponent } from '../../input/select/select.component' @@ -29,7 +29,7 @@ describe('UserEditDialogComponent', () => { let component: UserEditDialogComponent let settingsService: SettingsService let permissionsService: PermissionsService - let toastService: ToastService + let notificationService: NotificationService let fixture: ComponentFixture beforeEach(async () => { @@ -75,7 +75,7 @@ describe('UserEditDialogComponent', () => { settingsService = TestBed.inject(SettingsService) settingsService.currentUser = { id: 99, username: 'user99' } permissionsService = TestBed.inject(PermissionsService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) component = fixture.componentInstance fixture.detectChanges() @@ -133,22 +133,22 @@ describe('UserEditDialogComponent', () => { component['service'] as UserService, 'deactivateTotp' ) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') deactivateSpy.mockReturnValueOnce(throwError(() => new Error('error'))) component.deactivateTotp() expect(deactivateSpy).toHaveBeenCalled() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() deactivateSpy.mockReturnValueOnce(of(false)) component.deactivateTotp() expect(deactivateSpy).toHaveBeenCalled() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() deactivateSpy.mockReturnValueOnce(of(true)) component.deactivateTotp() expect(deactivateSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() }) it('should check superuser status of current user', () => { diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts index 7ba0f5ceb..298651c6a 100644 --- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts @@ -10,11 +10,11 @@ import { first } from 'rxjs' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { Group } from 'src/app/data/group' import { User } from 'src/app/data/user' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { GroupService } from 'src/app/services/rest/group.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { PasswordComponent } from '../../input/password/password.component' import { SelectComponent } from '../../input/select/select.component' import { TextComponent } from '../../input/text/text.component' @@ -46,7 +46,7 @@ export class UserEditDialogComponent activeModal: NgbActiveModal, groupsService: GroupService, settingsService: SettingsService, - private toastService: ToastService, + private notificationService: NotificationService, private permissionsService: PermissionsService ) { super(service, activeModal, service, settingsService) @@ -128,15 +128,20 @@ export class UserEditDialogComponent next: (result) => { this.totpLoading = false if (result) { - this.toastService.showInfo($localize`Totp deactivated`) + this.notificationService.showInfo($localize`Totp deactivated`) this.object.is_mfa_enabled = false } else { - this.toastService.showError($localize`Totp deactivation failed`) + this.notificationService.showError( + $localize`Totp deactivation failed` + ) } }, error: (e) => { this.totpLoading = false - this.toastService.showError($localize`Totp deactivation failed`, e) + this.notificationService.showError( + $localize`Totp deactivation failed`, + e + ) }, }) } diff --git a/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.spec.ts b/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.spec.ts index 7a3659205..15495b369 100644 --- a/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.spec.ts @@ -6,9 +6,9 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { DocumentService } from 'src/app/services/rest/document.service' -import { ToastService } from 'src/app/services/toast.service' import { EmailDocumentDialogComponent } from './email-document-dialog.component' describe('EmailDocumentDialogComponent', () => { @@ -16,7 +16,7 @@ describe('EmailDocumentDialogComponent', () => { let fixture: ComponentFixture let documentService: DocumentService let permissionsService: PermissionsService - let toastService: ToastService + let notificationService: NotificationService beforeEach(async () => { await TestBed.configureTestingModule({ @@ -34,7 +34,7 @@ describe('EmailDocumentDialogComponent', () => { fixture = TestBed.createComponent(EmailDocumentDialogComponent) documentService = TestBed.inject(DocumentService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) component = fixture.componentInstance fixture.detectChanges() }) @@ -47,8 +47,8 @@ describe('EmailDocumentDialogComponent', () => { }) it('should support sending document via email, showing error if needed', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastSuccessSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationSuccessSpy = jest.spyOn(notificationService, 'showInfo') component.emailAddress = 'hello@paperless-ngx.com' component.emailSubject = 'Hello' component.emailMessage = 'World' @@ -56,11 +56,11 @@ describe('EmailDocumentDialogComponent', () => { .spyOn(documentService, 'emailDocument') .mockReturnValue(throwError(() => new Error('Unable to email document'))) component.emailDocument() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() jest.spyOn(documentService, 'emailDocument').mockReturnValue(of(true)) component.emailDocument() - expect(toastSuccessSpy).toHaveBeenCalled() + expect(notificationSuccessSpy).toHaveBeenCalled() }) it('should close the dialog', () => { diff --git a/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.ts b/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.ts index ab8b9768b..a028ba702 100644 --- a/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.ts +++ b/src-ui/src/app/components/common/email-document-dialog/email-document-dialog.component.ts @@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core' import { FormsModule } from '@angular/forms' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { NotificationService } from 'src/app/services/notification.service' import { DocumentService } from 'src/app/services/rest/document.service' -import { ToastService } from 'src/app/services/toast.service' import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' @Component({ @@ -40,7 +40,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission constructor( private activeModal: NgbActiveModal, private documentService: DocumentService, - private toastService: ToastService + private notificationService: NotificationService ) { super() this.loading = false @@ -62,11 +62,14 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission this.emailAddress = '' this.emailSubject = '' this.emailMessage = '' - this.toastService.showInfo($localize`Email sent`) + this.notificationService.showInfo($localize`Email sent`) }, error: (e) => { this.loading = false - this.toastService.showError($localize`Error emailing document`, e) + this.notificationService.showError( + $localize`Error emailing document`, + e + ) }, }) } diff --git a/src-ui/src/app/components/common/notification-list/notification-list.component.html b/src-ui/src/app/components/common/notification-list/notification-list.component.html new file mode 100644 index 000000000..b383a142b --- /dev/null +++ b/src-ui/src/app/components/common/notification-list/notification-list.component.html @@ -0,0 +1,3 @@ +@for (notification of notifications; track notification.id) { + +} diff --git a/src-ui/src/app/components/common/toasts/toasts.component.scss b/src-ui/src/app/components/common/notification-list/notification-list.component.scss similarity index 73% rename from src-ui/src/app/components/common/toasts/toasts.component.scss rename to src-ui/src/app/components/common/notification-list/notification-list.component.scss index e0a069dda..14729fb15 100644 --- a/src-ui/src/app/components/common/toasts/toasts.component.scss +++ b/src-ui/src/app/components/common/notification-list/notification-list.component.scss @@ -1,7 +1,7 @@ :host { position: fixed; top: 0; - right: calc(50% - (var(--pngx-toast-max-width) / 2)); + right: calc(50% - (var(--pngx-notification-max-width) / 2)); margin: 0.3em; z-index: 1200; } diff --git a/src-ui/src/app/components/common/notification-list/notification-list.component.spec.ts b/src-ui/src/app/components/common/notification-list/notification-list.component.spec.ts new file mode 100644 index 000000000..69614aad2 --- /dev/null +++ b/src-ui/src/app/components/common/notification-list/notification-list.component.spec.ts @@ -0,0 +1,84 @@ +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { provideHttpClientTesting } from '@angular/common/http/testing' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { Subject } from 'rxjs' +import { + Notification, + NotificationService, +} from 'src/app/services/notification.service' +import { NotificationListComponent } from './notification-list.component' + +const notification = { + content: 'Error 2 content', + delay: 5000, + error: { + url: 'https://example.com', + status: 500, + statusText: 'Internal Server Error', + message: 'Internal server error 500 message', + error: { detail: 'Error 2 message details' }, + }, +} + +describe('NotificationListComponent', () => { + let component: NotificationListComponent + let fixture: ComponentFixture + let notificationService: NotificationService + let notificationSubject: Subject = new Subject() + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ + NotificationListComponent, + NgxBootstrapIconsModule.pick(allIcons), + ], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }).compileComponents() + + fixture = TestBed.createComponent(NotificationListComponent) + notificationService = TestBed.inject(NotificationService) + jest.replaceProperty( + notificationService, + 'showNotification', + notificationSubject + ) + + component = fixture.componentInstance + + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should close notification', () => { + component.notifications = [notification] + const closenotificationSpy = jest.spyOn( + notificationService, + 'closeNotification' + ) + component.closeNotification() + expect(component.notifications).toEqual([]) + expect(closenotificationSpy).toHaveBeenCalledWith(notification) + }) + + it('should unsubscribe', () => { + const unsubscribeSpy = jest.spyOn( + (component as any).subscription, + 'unsubscribe' + ) + component.ngOnDestroy() + expect(unsubscribeSpy).toHaveBeenCalled() + }) + + it('should subscribe to notificationService', () => { + component.ngOnInit() + notificationSubject.next(notification) + expect(component.notifications).toEqual([notification]) + }) +}) diff --git a/src-ui/src/app/components/common/notification-list/notification-list.component.ts b/src-ui/src/app/components/common/notification-list/notification-list.component.ts new file mode 100644 index 000000000..8028f7801 --- /dev/null +++ b/src-ui/src/app/components/common/notification-list/notification-list.component.ts @@ -0,0 +1,48 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { + NgbAccordionModule, + NgbProgressbarModule, +} from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subscription } from 'rxjs' +import { + Notification, + NotificationService, +} from 'src/app/services/notification.service' +import { NotificationComponent } from '../notification/notification.component' + +@Component({ + selector: 'pngx-notification-list', + templateUrl: './notification-list.component.html', + styleUrls: ['./notification-list.component.scss'], + imports: [ + NotificationComponent, + NgbAccordionModule, + NgbProgressbarModule, + NgxBootstrapIconsModule, + ], +}) +export class NotificationListComponent implements OnInit, OnDestroy { + constructor(public notificationService: NotificationService) {} + + private subscription: Subscription + + public notifications: Notification[] = [] // array to force change detection + + ngOnDestroy(): void { + this.subscription?.unsubscribe() + } + + ngOnInit(): void { + this.subscription = this.notificationService.showNotification.subscribe( + (notification) => { + this.notifications = notification ? [notification] : [] + } + ) + } + + closeNotification() { + this.notificationService.closeNotification(this.notifications[0]) + this.notifications = [] + } +} diff --git a/src-ui/src/app/components/common/toast/toast.component.html b/src-ui/src/app/components/common/notification/notification.component.html similarity index 56% rename from src-ui/src/app/components/common/toast/toast.component.html rename to src-ui/src/app/components/common/notification/notification.component.html index fc8e85e9c..4c46a1fa3 100644 --- a/src-ui/src/app/components/common/toast/toast.component.html +++ b/src-ui/src/app/components/common/notification/notification.component.html @@ -1,39 +1,39 @@ + (shown)="onShown(notification)" + (hidden)="hidden.emit(notification)"> @if (autohide) { - - {{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds + + {{ notification.delayRemaining / 1000 | number: '1.0-0' }} seconds } - @if (!toast.error) { + @if (!notification.error) { } - @if (toast.error) { + @if (notification.error) { } - {{toast.content}} - @if (toast.error) { + {{notification.content}} + @if (notification.error) { - @if (isDetailedError(toast.error)) { + @if (isDetailedError(notification.error)) { URL - {{ toast.error.url }} + {{ notification.error.url }} Status - {{ toast.error.status }} {{ toast.error.statusText }} + {{ notification.error.status }} {{ notification.error.statusText }} Error - {{ getErrorText(toast.error) }} + {{ getErrorText(notification.error) }} } - + @if (!copied) { } @@ -47,10 +47,10 @@ } - @if (toast.action) { - {{toast.actionName}} + @if (notification.action) { + {{notification.actionName}} } - + diff --git a/src-ui/src/app/components/common/toast/toast.component.scss b/src-ui/src/app/components/common/notification/notification.component.scss similarity index 100% rename from src-ui/src/app/components/common/toast/toast.component.scss rename to src-ui/src/app/components/common/notification/notification.component.scss diff --git a/src-ui/src/app/components/common/toast/toast.component.spec.ts b/src-ui/src/app/components/common/notification/notification.component.spec.ts similarity index 70% rename from src-ui/src/app/components/common/toast/toast.component.spec.ts rename to src-ui/src/app/components/common/notification/notification.component.spec.ts index c5d52a28f..05e4c7a37 100644 --- a/src-ui/src/app/components/common/toast/toast.component.spec.ts +++ b/src-ui/src/app/components/common/notification/notification.component.spec.ts @@ -9,15 +9,15 @@ import { import { Clipboard } from '@angular/cdk/clipboard' import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' -import { ToastComponent } from './toast.component' +import { NotificationComponent } from './notification.component' -const toast1 = { +const notification1 = { content: 'Error 1 content', delay: 5000, error: 'Error 1 string', } -const toast2 = { +const notification2 = { content: 'Error 2 content', delay: 5000, error: { @@ -29,17 +29,17 @@ const toast2 = { }, } -describe('ToastComponent', () => { - let component: ToastComponent - let fixture: ComponentFixture +describe('NotificationComponent', () => { + let component: NotificationComponent + let fixture: ComponentFixture let clipboard: Clipboard beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ToastComponent, NgxBootstrapIconsModule.pick(allIcons)], + imports: [NotificationComponent, NgxBootstrapIconsModule.pick(allIcons)], }).compileComponents() - fixture = TestBed.createComponent(ToastComponent) + fixture = TestBed.createComponent(NotificationComponent) clipboard = TestBed.inject(Clipboard) component = fixture.componentInstance }) @@ -48,18 +48,18 @@ describe('ToastComponent', () => { expect(component).toBeTruthy() }) - it('should countdown toast', fakeAsync(() => { - component.toast = toast2 + it('should countdown notification', fakeAsync(() => { + component.notification = notification2 fixture.detectChanges() - component.onShown(toast2) + component.onShown(notification2) tick(5000) - expect(component.toast.delayRemaining).toEqual(0) + expect(component.notification.delayRemaining).toEqual(0) flush() discardPeriodicTasks() })) - it('should show an error if given with toast', fakeAsync(() => { - component.toast = toast1 + it('should show an error if given with notification', fakeAsync(() => { + component.notification = notification1 fixture.detectChanges() expect(fixture.nativeElement.querySelector('details')).not.toBeNull() @@ -70,7 +70,7 @@ describe('ToastComponent', () => { })) it('should show error details, support copy', fakeAsync(() => { - component.toast = toast2 + component.notification = notification2 fixture.detectChanges() expect(fixture.nativeElement.querySelector('details')).not.toBeNull() @@ -79,7 +79,7 @@ describe('ToastComponent', () => { ) const copySpy = jest.spyOn(clipboard, 'copy') - component.copyError(toast2.error) + component.copyError(notification2.error) expect(copySpy).toHaveBeenCalled() flush() @@ -87,7 +87,7 @@ describe('ToastComponent', () => { })) it('should parse error text, add ellipsis', () => { - expect(component.getErrorText(toast2.error)).toEqual( + expect(component.getErrorText(notification2.error)).toEqual( 'Error 2 message details' ) expect(component.getErrorText({ error: 'Error string no detail' })).toEqual( diff --git a/src-ui/src/app/components/common/toast/toast.component.ts b/src-ui/src/app/components/common/notification/notification.component.ts similarity index 71% rename from src-ui/src/app/components/common/toast/toast.component.ts rename to src-ui/src/app/components/common/notification/notification.component.ts index 5ebfdbe82..1c7bed37e 100644 --- a/src-ui/src/app/components/common/toast/toast.component.ts +++ b/src-ui/src/app/components/common/notification/notification.component.ts @@ -7,42 +7,43 @@ import { } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { interval, take } from 'rxjs' -import { Toast } from 'src/app/services/toast.service' +import { Notification } from 'src/app/services/notification.service' @Component({ - selector: 'pngx-toast', + selector: 'pngx-notification', imports: [ DecimalPipe, NgbToastModule, NgbProgressbarModule, NgxBootstrapIconsModule, ], - templateUrl: './toast.component.html', - styleUrl: './toast.component.scss', + templateUrl: './notification.component.html', + styleUrl: './notification.component.scss', }) -export class ToastComponent { - @Input() toast: Toast +export class NotificationComponent { + @Input() notification: Notification @Input() autohide: boolean = true - @Output() hidden: EventEmitter = new EventEmitter() + @Output() hidden: EventEmitter = + new EventEmitter() - @Output() close: EventEmitter = new EventEmitter() + @Output() close: EventEmitter = new EventEmitter() public copied: boolean = false constructor(private clipboard: Clipboard) {} - onShown(toast: Toast) { + onShown(notification: Notification) { if (!this.autohide) return const refreshInterval = 150 - const delay = toast.delay - 500 // for fade animation + const delay = notification.delay - 500 // for fade animation interval(refreshInterval) .pipe(take(Math.round(delay / refreshInterval))) .subscribe((count) => { - toast.delayRemaining = Math.max( + notification.delayRemaining = Math.max( 0, delay - refreshInterval * (count + 1) ) diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts index 64e122612..1edeedf68 100644 --- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts @@ -16,8 +16,8 @@ import { } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' +import { NotificationService } from 'src/app/services/notification.service' import { ProfileService } from 'src/app/services/profile.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component' import { PasswordComponent } from '../input/password/password.component' import { TextComponent } from '../input/text/text.component' @@ -44,7 +44,7 @@ describe('ProfileEditDialogComponent', () => { let component: ProfileEditDialogComponent let fixture: ComponentFixture let profileService: ProfileService - let toastService: ToastService + let notificationService: NotificationService let clipboard: Clipboard beforeEach(() => { @@ -64,7 +64,7 @@ describe('ProfileEditDialogComponent', () => { providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())], }) profileService = TestBed.inject(ProfileService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) clipboard = TestBed.inject(Clipboard) fixture = TestBed.createComponent(ProfileEditDialogComponent) component = fixture.componentInstance @@ -94,13 +94,13 @@ describe('ProfileEditDialogComponent', () => { auth_token: profile.auth_token, } const updateSpy = jest.spyOn(profileService, 'update') - const errorSpy = jest.spyOn(toastService, 'showError') + const errorSpy = jest.spyOn(notificationService, 'showError') updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save'))) component.save() expect(errorSpy).toHaveBeenCalled() updateSpy.mockClear() - const infoSpy = jest.spyOn(toastService, 'showInfo') + const infoSpy = jest.spyOn(notificationService, 'showInfo') component.form.patchValue(newProfile) updateSpy.mockReturnValueOnce(of(newProfile)) component.save() @@ -239,7 +239,7 @@ describe('ProfileEditDialogComponent', () => { getSpy.mockReturnValue(of(profile)) const generateSpy = jest.spyOn(profileService, 'generateAuthToken') - const errorSpy = jest.spyOn(toastService, 'showError') + const errorSpy = jest.spyOn(notificationService, 'showError') generateSpy.mockReturnValueOnce( throwError(() => new Error('failed to generate')) ) @@ -275,7 +275,7 @@ describe('ProfileEditDialogComponent', () => { getSpy.mockImplementation(() => of(profile)) component.ngOnInit() - const errorSpy = jest.spyOn(toastService, 'showError') + const errorSpy = jest.spyOn(notificationService, 'showError') expect(component.socialAccounts).toContainEqual(socialAccount) @@ -300,13 +300,13 @@ describe('ProfileEditDialogComponent', () => { secret: 'secret', } const getSpy = jest.spyOn(profileService, 'getTotpSettings') - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') getSpy.mockReturnValueOnce( throwError(() => new Error('failed to get settings')) ) component.gettotpSettings() expect(getSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() getSpy.mockReturnValue(of(settings)) component.gettotpSettings() @@ -316,8 +316,8 @@ describe('ProfileEditDialogComponent', () => { it('should activate totp', () => { const activateSpy = jest.spyOn(profileService, 'activateTotp') - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const error = new Error('failed to activate totp') activateSpy.mockReturnValueOnce(throwError(() => error)) component.totpSettings = { @@ -331,38 +331,44 @@ describe('ProfileEditDialogComponent', () => { component.totpSettings.secret, component.form.get('totp_code').value ) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() activateSpy.mockReturnValueOnce(of({ success: false, recovery_codes: [] })) component.activateTotp() - expect(toastErrorSpy).toHaveBeenCalledWith('Error activating TOTP', error) + expect(notificationErrorSpy).toHaveBeenCalledWith( + 'Error activating TOTP', + error + ) activateSpy.mockReturnValueOnce( of({ success: true, recovery_codes: ['1', '2', '3'] }) ) component.activateTotp() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(component.isTotpEnabled).toBeTruthy() expect(component.recoveryCodes).toEqual(['1', '2', '3']) }) it('should deactivate totp', () => { const deactivateSpy = jest.spyOn(profileService, 'deactivateTotp') - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const error = new Error('failed to deactivate totp') deactivateSpy.mockReturnValueOnce(throwError(() => error)) component.deactivateTotp() expect(deactivateSpy).toHaveBeenCalled() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() deactivateSpy.mockReturnValueOnce(of(false)) component.deactivateTotp() - expect(toastErrorSpy).toHaveBeenCalledWith('Error deactivating TOTP', error) + expect(notificationErrorSpy).toHaveBeenCalledWith( + 'Error deactivating TOTP', + error + ) deactivateSpy.mockReturnValueOnce(of(true)) component.deactivateTotp() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(component.isTotpEnabled).toBeFalsy() }) diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts index afffa7693..ea681a145 100644 --- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts @@ -19,8 +19,8 @@ import { TotpSettings, } from 'src/app/data/user-profile' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' +import { NotificationService } from 'src/app/services/notification.service' import { ProfileService } from 'src/app/services/profile.service' -import { ToastService } from 'src/app/services/toast.service' import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component' import { PasswordComponent } from '../input/password/password.component' @@ -86,7 +86,7 @@ export class ProfileEditDialogComponent constructor( private profileService: ProfileService, public activeModal: NgbActiveModal, - private toastService: ToastService, + private notificationService: NotificationService, private clipboard: Clipboard ) { super() @@ -192,9 +192,11 @@ export class ProfileEditDialogComponent .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe({ next: () => { - this.toastService.showInfo($localize`Profile updated successfully`) + this.notificationService.showInfo( + $localize`Profile updated successfully` + ) if (passwordChanged) { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Password has been changed, you will be logged out momentarily.` ) setTimeout(() => { @@ -204,7 +206,10 @@ export class ProfileEditDialogComponent this.activeModal.close() }, error: (error) => { - this.toastService.showError($localize`Error saving profile`, error) + this.notificationService.showError( + $localize`Error saving profile`, + error + ) this.networkActive = false }, }) @@ -220,7 +225,7 @@ export class ProfileEditDialogComponent this.form.patchValue({ auth_token: token }) }, error: (error) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error generating auth token`, error ) @@ -245,7 +250,7 @@ export class ProfileEditDialogComponent this.socialAccounts = this.socialAccounts.filter((a) => a.id != id) }, error: (error) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error disconnecting social account`, error ) @@ -264,7 +269,7 @@ export class ProfileEditDialogComponent this.totpSettings = totpSettings }, error: (error) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error fetching TOTP settings`, error ) @@ -286,15 +291,20 @@ export class ProfileEditDialogComponent this.recoveryCodes = activationResponse.recovery_codes this.form.get('totp_code').enable() if (activationResponse.success) { - this.toastService.showInfo($localize`TOTP activated successfully`) + this.notificationService.showInfo( + $localize`TOTP activated successfully` + ) } else { - this.toastService.showError($localize`Error activating TOTP`) + this.notificationService.showError($localize`Error activating TOTP`) } }, error: (error) => { this.totpLoading = false this.form.get('totp_code').enable() - this.toastService.showError($localize`Error activating TOTP`, error) + this.notificationService.showError( + $localize`Error activating TOTP`, + error + ) }, }) } @@ -310,14 +320,21 @@ export class ProfileEditDialogComponent this.isTotpEnabled = !success this.recoveryCodes = null if (success) { - this.toastService.showInfo($localize`TOTP deactivated successfully`) + this.notificationService.showInfo( + $localize`TOTP deactivated successfully` + ) } else { - this.toastService.showError($localize`Error deactivating TOTP`) + this.notificationService.showError( + $localize`Error deactivating TOTP` + ) } }, error: (error) => { this.totpLoading = false - this.toastService.showError($localize`Error deactivating TOTP`, error) + this.notificationService.showError( + $localize`Error deactivating TOTP`, + error + ) }, }) } diff --git a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts index 3f60b6733..b4c02289e 100644 --- a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts @@ -15,8 +15,8 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' import { FileVersion, ShareLink } from 'src/app/data/share-link' +import { NotificationService } from 'src/app/services/notification.service' import { ShareLinkService } from 'src/app/services/rest/share-link.service' -import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' import { ShareLinksDialogComponent } from './share-links-dialog.component' @@ -24,7 +24,7 @@ describe('ShareLinksDialogComponent', () => { let component: ShareLinksDialogComponent let fixture: ComponentFixture let shareLinkService: ShareLinkService - let toastService: ToastService + let notificationService: NotificationService let httpController: HttpTestingController let clipboard: Clipboard @@ -43,7 +43,7 @@ describe('ShareLinksDialogComponent', () => { fixture = TestBed.createComponent(ShareLinksDialogComponent) shareLinkService = TestBed.inject(ShareLinkService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) httpController = TestBed.inject(HttpTestingController) clipboard = TestBed.inject(Clipboard) @@ -89,7 +89,7 @@ describe('ShareLinksDialogComponent', () => { }) it('should show error on refresh if needed', () => { - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(shareLinkService, 'getLinksForDocument') .mockReturnValueOnce(throwError(() => new Error('Unable to get links'))) @@ -97,7 +97,7 @@ describe('ShareLinksDialogComponent', () => { component.ngOnInit() fixture.detectChanges() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should support link creation then refresh & copy url', fakeAsync(() => { @@ -138,7 +138,7 @@ describe('ShareLinksDialogComponent', () => { const expiration = new Date() expiration.setDate(expiration.getDate() + 7) - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') component.createLink() @@ -150,7 +150,7 @@ describe('ShareLinksDialogComponent', () => { ) fixture.detectChanges() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should support delete links & refresh', () => { @@ -165,13 +165,13 @@ describe('ShareLinksDialogComponent', () => { }) it('should show error on delete if needed', () => { - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(shareLinkService, 'delete') .mockReturnValueOnce(throwError(() => new Error('Unable to delete link'))) component.delete(null) fixture.detectChanges() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should format days remaining', () => { diff --git a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts index 19123f73e..9d1350dcf 100644 --- a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts +++ b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts @@ -5,8 +5,8 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { first } from 'rxjs' import { FileVersion, ShareLink } from 'src/app/data/share-link' +import { NotificationService } from 'src/app/services/notification.service' import { ShareLinkService } from 'src/app/services/rest/share-link.service' -import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' @Component({ @@ -61,7 +61,7 @@ export class ShareLinksDialogComponent implements OnInit { constructor( private activeModal: NgbActiveModal, private shareLinkService: ShareLinkService, - private toastService: ToastService, + private notificationService: NotificationService, private clipboard: Clipboard ) {} @@ -81,7 +81,7 @@ export class ShareLinksDialogComponent implements OnInit { this.shareLinks = results }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error retrieving links`, 10000, e @@ -130,7 +130,11 @@ export class ShareLinksDialogComponent implements OnInit { this.refresh() }, error: (e) => { - this.toastService.showError($localize`Error deleting link`, 10000, e) + this.notificationService.showError( + $localize`Error deleting link`, + 10000, + e + ) }, }) } @@ -158,7 +162,11 @@ export class ShareLinksDialogComponent implements OnInit { }, error: (e) => { this.loading = false - this.toastService.showError($localize`Error creating link`, 10000, e) + this.notificationService.showError( + $localize`Error creating link`, + 10000, + e + ) }, }) } diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts index 28a0889ab..45786900b 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts @@ -16,9 +16,9 @@ import { SystemStatus, SystemStatusItemStatus, } from 'src/app/data/system-status' +import { NotificationService } from 'src/app/services/notification.service' import { SystemStatusService } from 'src/app/services/system-status.service' import { TasksService } from 'src/app/services/tasks.service' -import { ToastService } from 'src/app/services/toast.service' import { SystemStatusDialogComponent } from './system-status-dialog.component' const status: SystemStatus = { @@ -61,7 +61,7 @@ describe('SystemStatusDialogComponent', () => { let clipboard: Clipboard let tasksService: TasksService let systemStatusService: SystemStatusService - let toastService: ToastService + let notificationService: NotificationService beforeEach(async () => { await TestBed.configureTestingModule({ @@ -82,7 +82,7 @@ describe('SystemStatusDialogComponent', () => { clipboard = TestBed.inject(Clipboard) tasksService = TestBed.inject(TasksService) systemStatusService = TestBed.inject(SystemStatusService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) fixture.detectChanges() }) @@ -116,9 +116,9 @@ describe('SystemStatusDialogComponent', () => { expect(component.isRunning(PaperlessTaskName.SanityCheck)).toBeFalsy() }) - it('should support running tasks, refresh status and show toasts', () => { - const toastSpy = jest.spyOn(toastService, 'showInfo') - const toastErrorSpy = jest.spyOn(toastService, 'showError') + it('should support running tasks, refresh status and show notifications', () => { + const notificationSpy = jest.spyOn(notificationService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') const getStatusSpy = jest.spyOn(systemStatusService, 'get') const runSpy = jest.spyOn(tasksService, 'run') @@ -126,7 +126,7 @@ describe('SystemStatusDialogComponent', () => { runSpy.mockReturnValue(throwError(() => new Error('error'))) component.runTask(PaperlessTaskName.IndexOptimize) expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize) - expect(toastErrorSpy).toHaveBeenCalledWith( + expect(notificationErrorSpy).toHaveBeenCalledWith( `Failed to start task ${PaperlessTaskName.IndexOptimize}, see the logs for more details`, expect.any(Error) ) @@ -138,7 +138,7 @@ describe('SystemStatusDialogComponent', () => { expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize) expect(getStatusSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith( + expect(notificationSpy).toHaveBeenCalledWith( `Task ${PaperlessTaskName.IndexOptimize} started` ) }) diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts index c7ba3c57a..b039b6244 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts @@ -14,10 +14,10 @@ import { } from 'src/app/data/system-status' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { FileSizePipe } from 'src/app/pipes/file-size.pipe' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { SystemStatusService } from 'src/app/services/system-status.service' import { TasksService } from 'src/app/services/tasks.service' -import { ToastService } from 'src/app/services/toast.service' @Component({ selector: 'pngx-system-status-dialog', @@ -51,7 +51,7 @@ export class SystemStatusDialogComponent { private clipboard: Clipboard, private systemStatusService: SystemStatusService, private tasksService: TasksService, - private toastService: ToastService, + private notificationService: NotificationService, private permissionsService: PermissionsService ) {} @@ -79,7 +79,7 @@ export class SystemStatusDialogComponent { public runTask(taskName: PaperlessTaskName) { this.runningTasks.add(taskName) - this.toastService.showInfo(`Task ${taskName} started`) + this.notificationService.showInfo(`Task ${taskName} started`) this.tasksService.run(taskName).subscribe({ next: () => { this.runningTasks.delete(taskName) @@ -91,7 +91,7 @@ export class SystemStatusDialogComponent { }, error: (err) => { this.runningTasks.delete(taskName) - this.toastService.showError( + this.notificationService.showError( `Failed to start task ${taskName}, see the logs for more details`, err ) diff --git a/src-ui/src/app/components/common/toasts/toasts.component.html b/src-ui/src/app/components/common/toasts/toasts.component.html deleted file mode 100644 index 2178a2023..000000000 --- a/src-ui/src/app/components/common/toasts/toasts.component.html +++ /dev/null @@ -1,3 +0,0 @@ -@for (toast of toasts; track toast.id) { - -} diff --git a/src-ui/src/app/components/common/toasts/toasts.component.spec.ts b/src-ui/src/app/components/common/toasts/toasts.component.spec.ts deleted file mode 100644 index bbea04c9c..000000000 --- a/src-ui/src/app/components/common/toasts/toasts.component.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' -import { provideHttpClientTesting } from '@angular/common/http/testing' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' -import { Subject } from 'rxjs' -import { Toast, ToastService } from 'src/app/services/toast.service' -import { ToastsComponent } from './toasts.component' - -const toast = { - content: 'Error 2 content', - delay: 5000, - error: { - url: 'https://example.com', - status: 500, - statusText: 'Internal Server Error', - message: 'Internal server error 500 message', - error: { detail: 'Error 2 message details' }, - }, -} - -describe('ToastsComponent', () => { - let component: ToastsComponent - let fixture: ComponentFixture - let toastService: ToastService - let toastSubject: Subject = new Subject() - - beforeEach(async () => { - TestBed.configureTestingModule({ - imports: [ToastsComponent, NgxBootstrapIconsModule.pick(allIcons)], - providers: [ - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - ], - }).compileComponents() - - fixture = TestBed.createComponent(ToastsComponent) - toastService = TestBed.inject(ToastService) - jest.replaceProperty(toastService, 'showToast', toastSubject) - - component = fixture.componentInstance - - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - it('should close toast', () => { - component.toasts = [toast] - const closeToastSpy = jest.spyOn(toastService, 'closeToast') - component.closeToast() - expect(component.toasts).toEqual([]) - expect(closeToastSpy).toHaveBeenCalledWith(toast) - }) - - it('should unsubscribe', () => { - const unsubscribeSpy = jest.spyOn( - (component as any).subscription, - 'unsubscribe' - ) - component.ngOnDestroy() - expect(unsubscribeSpy).toHaveBeenCalled() - }) - - it('should subscribe to toastService', () => { - component.ngOnInit() - toastSubject.next(toast) - expect(component.toasts).toEqual([toast]) - }) -}) diff --git a/src-ui/src/app/components/common/toasts/toasts.component.ts b/src-ui/src/app/components/common/toasts/toasts.component.ts deleted file mode 100644 index 53b6e1895..000000000 --- a/src-ui/src/app/components/common/toasts/toasts.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' -import { - NgbAccordionModule, - NgbProgressbarModule, -} from '@ng-bootstrap/ng-bootstrap' -import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' -import { Subscription } from 'rxjs' -import { Toast, ToastService } from 'src/app/services/toast.service' -import { ToastComponent } from '../toast/toast.component' - -@Component({ - selector: 'pngx-toasts', - templateUrl: './toasts.component.html', - styleUrls: ['./toasts.component.scss'], - imports: [ - ToastComponent, - NgbAccordionModule, - NgbProgressbarModule, - NgxBootstrapIconsModule, - ], -}) -export class ToastsComponent implements OnInit, OnDestroy { - constructor(public toastService: ToastService) {} - - private subscription: Subscription - - public toasts: Toast[] = [] // array to force change detection - - ngOnDestroy(): void { - this.subscription?.unsubscribe() - } - - ngOnInit(): void { - this.subscription = this.toastService.showToast.subscribe((toast) => { - this.toasts = toast ? [toast] : [] - }) - } - - closeToast() { - this.toastService.closeToast(this.toasts[0]) - this.toasts = [] - } -} diff --git a/src-ui/src/app/components/dashboard/dashboard.component.spec.ts b/src-ui/src/app/components/dashboard/dashboard.component.spec.ts index 2ff5eab78..2459233dd 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.spec.ts +++ b/src-ui/src/app/components/dashboard/dashboard.component.spec.ts @@ -12,10 +12,10 @@ import { SavedView } from 'src/app/data/saved-view' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { PermissionsGuard } from 'src/app/guards/permissions.guard' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { LogoComponent } from '../common/logo/logo.component' import { PageHeaderComponent } from '../common/page-header/page-header.component' import { DashboardComponent } from './dashboard.component' @@ -68,7 +68,7 @@ describe('DashboardComponent', () => { let fixture: ComponentFixture let settingsService: SettingsService let tourService: TourService - let toastService: ToastService + let notificationService: NotificationService beforeEach(async () => { TestBed.configureTestingModule({ @@ -121,7 +121,7 @@ describe('DashboardComponent', () => { if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return [0, 2, 3] }) tourService = TestBed.inject(TourService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) fixture = TestBed.createComponent(DashboardComponent) component = fixture.componentInstance @@ -166,7 +166,7 @@ describe('DashboardComponent', () => { it('should update saved view sorting on drag + drop, show info', () => { const settingsSpy = jest.spyOn(settingsService, 'updateDashboardViewsSort') - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true)) component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop< SavedView[] @@ -176,7 +176,7 @@ describe('DashboardComponent', () => { saved_views[0], saved_views[3], ]) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should update saved view sorting on drag + drop, show error', () => { @@ -187,13 +187,13 @@ describe('DashboardComponent', () => { fixture = TestBed.createComponent(DashboardComponent) component = fixture.componentInstance fixture.detectChanges() - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(settingsService, 'storeSettings') .mockReturnValue(throwError(() => new Error('unable to save'))) component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop< SavedView[] >) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) }) diff --git a/src-ui/src/app/components/dashboard/dashboard.component.ts b/src-ui/src/app/components/dashboard/dashboard.component.ts index 94637ba0f..5ebd39a61 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.ts +++ b/src-ui/src/app/components/dashboard/dashboard.component.ts @@ -9,9 +9,9 @@ import { Component } from '@angular/core' import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap' import { SavedView } from 'src/app/data/saved-view' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' import { LogoComponent } from '../common/logo/logo.component' import { PageHeaderComponent } from '../common/page-header/page-header.component' @@ -43,7 +43,7 @@ export class DashboardComponent extends ComponentWithPermissions { public settingsService: SettingsService, public savedViewService: SavedViewService, private tourService: TourService, - private toastService: ToastService + private notificationService: NotificationService ) { super() @@ -87,10 +87,13 @@ export class DashboardComponent extends ComponentWithPermissions { .updateDashboardViewsSort(this.dashboardViews) .subscribe({ next: () => { - this.toastService.showInfo($localize`Dashboard updated`) + this.notificationService.showInfo($localize`Dashboard updated`) }, error: (e) => { - this.toastService.showError($localize`Error updating dashboard`, e) + this.notificationService.showError( + $localize`Error updating dashboard`, + e + ) }, }) } diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index b85a7eaf4..28b2d85b5 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -48,6 +48,7 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { ComponentRouterService } from 'src/app/services/component-router.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { PermissionsService } from 'src/app/services/permissions.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service' @@ -58,7 +59,6 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { TagService } from 'src/app/services/rest/tag.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' @@ -127,7 +127,7 @@ describe('DocumentDetailComponent', () => { let documentService: DocumentService let openDocumentsService: OpenDocumentsService let modalService: NgbModal - let toastService: ToastService + let notificationService: NotificationService let documentListViewService: DocumentListViewService let settingsService: SettingsService let customFieldsService: CustomFieldsService @@ -264,7 +264,7 @@ describe('DocumentDetailComponent', () => { openDocumentsService = TestBed.inject(OpenDocumentsService) documentService = TestBed.inject(DocumentService) modalService = TestBed.inject(NgbModal) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) documentListViewService = TestBed.inject(DocumentListViewService) settingsService = TestBed.inject(SettingsService) settingsService.currentUser = { id: 1 } @@ -447,68 +447,68 @@ describe('DocumentDetailComponent', () => { expect(navigateSpy).toHaveBeenCalledWith(['404'], { replaceUrl: true }) }) - it('should support save, close and show success toast', () => { + it('should support save, close and show success notification', () => { initNormally() component.title = 'Foo Bar' const closeSpy = jest.spyOn(component, 'close') const updateSpy = jest.spyOn(documentService, 'update') - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') updateSpy.mockImplementation((o) => of(doc)) component.save(true) expect(updateSpy).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith( + expect(notificationSpy).toHaveBeenCalledWith( 'Document "Doc 3" saved successfully.' ) }) - it('should support save without close and show success toast', () => { + it('should support save without close and show success notification', () => { initNormally() component.title = 'Foo Bar' const closeSpy = jest.spyOn(component, 'close') const updateSpy = jest.spyOn(documentService, 'update') - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') updateSpy.mockImplementation((o) => of(doc)) component.save() expect(updateSpy).toHaveBeenCalled() expect(closeSpy).not.toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith( + expect(notificationSpy).toHaveBeenCalledWith( 'Document "Doc 3" saved successfully.' ) }) - it('should show toast error on save if error occurs', () => { + it('should show notification error on save if error occurs', () => { currentUserHasObjectPermissions = true initNormally() component.title = 'Foo Bar' const closeSpy = jest.spyOn(component, 'close') const updateSpy = jest.spyOn(documentService, 'update') - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') const error = new Error('failed to save') updateSpy.mockImplementation(() => throwError(() => error)) component.save() expect(updateSpy).toHaveBeenCalled() expect(closeSpy).not.toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith( + expect(notificationSpy).toHaveBeenCalledWith( 'Error saving document "Doc 3"', error ) }) - it('should show error toast on save but close if user can no longer edit', () => { + it('should show error notification on save but close if user can no longer edit', () => { currentUserHasObjectPermissions = false initNormally() component.title = 'Foo Bar' const closeSpy = jest.spyOn(component, 'close') const updateSpy = jest.spyOn(documentService, 'update') - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') updateSpy.mockImplementation(() => throwError(() => new Error('failed to save')) ) component.save(true) expect(updateSpy).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith( + expect(notificationSpy).toHaveBeenCalledWith( 'Document "Doc 3" saved successfully.' ) }) @@ -531,19 +531,19 @@ describe('DocumentDetailComponent', () => { expect }) - it('should show toast error on save & next if error occurs', () => { + it('should show notification error on save & next if error occurs', () => { currentUserHasObjectPermissions = true initNormally() component.title = 'Foo Bar' const closeSpy = jest.spyOn(component, 'close') const updateSpy = jest.spyOn(documentService, 'update') - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') const error = new Error('failed to save') updateSpy.mockImplementation(() => throwError(() => error)) component.saveEditNext() expect(updateSpy).toHaveBeenCalled() expect(closeSpy).not.toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith('Error saving document', error) + expect(notificationSpy).toHaveBeenCalledWith('Error saving document', error) }) it('should show save button and save & close or save & next', () => { @@ -668,13 +668,13 @@ describe('DocumentDetailComponent', () => { let openModal: NgbModalRef modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) const modalSpy = jest.spyOn(modalService, 'open') - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') component.reprocess() const modalCloseSpy = jest.spyOn(openModal, 'close') openModal.componentInstance.confirmClicked.next() expect(bulkEditSpy).toHaveBeenCalledWith([doc.id], 'reprocess', {}) expect(modalSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() expect(modalCloseSpy).toHaveBeenCalled() }) @@ -683,12 +683,12 @@ describe('DocumentDetailComponent', () => { const bulkEditSpy = jest.spyOn(documentService, 'bulkEdit') let openModal: NgbModalRef modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') component.reprocess() const modalCloseSpy = jest.spyOn(openModal, 'close') bulkEditSpy.mockReturnValue(throwError(() => new Error('error occurred'))) openModal.componentInstance.confirmClicked.next() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() expect(modalCloseSpy).not.toHaveBeenCalled() }) @@ -942,9 +942,12 @@ describe('DocumentDetailComponent', () => { jest .spyOn(documentService, 'getMetadata') .mockReturnValue(throwError(() => error)) - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') initNormally() - expect(toastSpy).toHaveBeenCalledWith('Error retrieving metadata', error) + expect(notificationSpy).toHaveBeenCalledWith( + 'Error retrieving metadata', + error + ) }) it('should display custom fields', () => { @@ -1028,7 +1031,7 @@ describe('DocumentDetailComponent', () => { it('should show error if needed for get suggestions', () => { const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions') - const errorSpy = jest.spyOn(toastService, 'showError') + const errorSpy = jest.spyOn(notificationService, 'showError') suggestionsSpy.mockImplementationOnce(() => throwError(() => new Error('failed to get suggestions')) ) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 27a74cfcd..ab8a8943e 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -63,6 +63,7 @@ import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' import { ComponentRouterService } from 'src/app/services/component-router.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { HotKeyService } from 'src/app/services/hot-key.service' +import { NotificationService } from 'src/app/services/notification.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { PermissionAction, @@ -76,7 +77,6 @@ import { DocumentService } from 'src/app/services/rest/document.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' import * as UTIF from 'utif' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' @@ -268,7 +268,7 @@ export class DocumentDetailComponent private openDocumentService: OpenDocumentsService, private documentListViewService: DocumentListViewService, private documentTitlePipe: DocumentTitlePipe, - private toastService: ToastService, + private notificationService: NotificationService, private settings: SettingsService, private storagePathService: StoragePathService, private permissionsService: PermissionsService, @@ -628,7 +628,7 @@ export class DocumentDetailComponent }, error: (error) => { this.metadata = {} // allow display to fallback to tag - this.toastService.showError( + this.notificationService.showError( $localize`Error retrieving metadata`, error ) @@ -657,7 +657,7 @@ export class DocumentDetailComponent }, error: (error) => { this.suggestions = null - this.toastService.showError( + this.notificationService.showError( $localize`Error retrieving suggestions.`, error ) @@ -809,7 +809,7 @@ export class DocumentDetailComponent this.store.next(newValues) this.openDocumentService.setDirty(this.document, false) this.openDocumentService.save() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Document "${newValues.title}" saved successfully.` ) this.networkActive = false @@ -825,13 +825,13 @@ export class DocumentDetailComponent error: (error) => { this.networkActive = false if (!this.userCanEdit) { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Document "${this.document.title}" saved successfully.` ) close && this.close() } else { this.error = error.error - this.toastService.showError( + this.notificationService.showError( $localize`Error saving document "${this.document.title}"`, error ) @@ -877,7 +877,10 @@ export class DocumentDetailComponent error: (error) => { this.networkActive = false this.error = error.error - this.toastService.showError($localize`Error saving document`, error) + this.notificationService.showError( + $localize`Error saving document`, + error + ) }, }) } @@ -931,7 +934,10 @@ export class DocumentDetailComponent this.close() }, error: (error) => { - this.toastService.showError($localize`Error deleting document`, error) + this.notificationService.showError( + $localize`Error deleting document`, + error + ) modal.componentInstance.buttonsEnabled = true this.subscribeModalDelete(modal) }, @@ -962,7 +968,7 @@ export class DocumentDetailComponent .bulkEdit([this.document.id], 'reprocess', {}) .subscribe({ next: () => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Reprocess operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.` ) if (modal) { @@ -973,7 +979,7 @@ export class DocumentDetailComponent if (modal) { modal.componentInstance.buttonsEnabled = true } - this.toastService.showError( + this.notificationService.showError( $localize`Error executing operation`, error ) @@ -1020,7 +1026,7 @@ export class DocumentDetailComponent }, error: (error) => { this.downloading = false - this.toastService.showError( + this.notificationService.showError( $localize`Error downloading document`, error ) @@ -1329,7 +1335,7 @@ export class DocumentDetailComponent .pipe(first(), takeUntil(this.unsubscribeNotifier)) .subscribe({ next: () => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Split operation for "${this.document.title}" will begin in the background.` ) modal.close() @@ -1338,7 +1344,7 @@ export class DocumentDetailComponent if (modal) { modal.componentInstance.buttonsEnabled = true } - this.toastService.showError( + this.notificationService.showError( $localize`Error executing split operation`, error ) @@ -1368,7 +1374,7 @@ export class DocumentDetailComponent .pipe(first(), takeUntil(this.unsubscribeNotifier)) .subscribe({ next: () => { - this.toastService.show({ + this.notificationService.show({ content: $localize`Rotation of "${this.document.title}" will begin in the background. Close and re-open the document after the operation has completed to see the changes.`, delay: 8000, action: this.close.bind(this), @@ -1380,7 +1386,7 @@ export class DocumentDetailComponent if (modal) { modal.componentInstance.buttonsEnabled = true } - this.toastService.showError( + this.notificationService.showError( $localize`Error executing rotate operation`, error ) @@ -1408,7 +1414,7 @@ export class DocumentDetailComponent .pipe(first(), takeUntil(this.unsubscribeNotifier)) .subscribe({ next: () => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Delete pages operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.` ) modal.close() @@ -1417,7 +1423,7 @@ export class DocumentDetailComponent if (modal) { modal.componentInstance.buttonsEnabled = true } - this.toastService.showError( + this.notificationService.showError( $localize`Error executing delete pages operation`, error ) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts index aa4a07d12..58e8d9beb 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -16,6 +16,7 @@ import { StoragePath } from 'src/app/data/storage-path' import { Tag } from 'src/app/data/tag' import { FilterPipe } from 'src/app/pipes/filter.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' @@ -29,7 +30,6 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { TagService } from 'src/app/services/rest/tag.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' @@ -64,7 +64,7 @@ describe('BulkEditorComponent', () => { let permissionsService: PermissionsService let documentListViewService: DocumentListViewService let documentService: DocumentService - let toastService: ToastService + let notificationService: NotificationService let modalService: NgbModal let tagService: TagService let correspondentsService: CorrespondentService @@ -160,7 +160,7 @@ describe('BulkEditorComponent', () => { permissionsService = TestBed.inject(PermissionsService) documentListViewService = TestBed.inject(DocumentListViewService) documentService = TestBed.inject(DocumentService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) modalService = TestBed.inject(NgbModal) tagService = TestBed.inject(TagService) correspondentsService = TestBed.inject(CorrespondentService) @@ -884,7 +884,7 @@ describe('BulkEditorComponent', () => { expect(button.nativeElement.disabled).toBeTruthy() }) - it('should show a warning toast on bulk edit error', () => { + it('should show a warning notification on bulk edit error', () => { jest .spyOn(documentService, 'bulkEdit') .mockReturnValue( @@ -902,12 +902,12 @@ describe('BulkEditorComponent', () => { .mockReturnValue(true) component.showConfirmationDialogs = false fixture.detectChanges() - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') component.setTags({ itemsToAdd: [{ id: 0 }], itemsToRemove: [], }) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) it('should support redo ocr', () => { @@ -1391,8 +1391,14 @@ describe('BulkEditorComponent', () => { .spyOn(documentListViewService, 'selected', 'get') .mockReturnValue(new Set([3, 4])) fixture.detectChanges() - const toastServiceShowInfoSpy = jest.spyOn(toastService, 'showInfo') - const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError') + const notificationServiceShowInfoSpy = jest.spyOn( + notificationService, + 'showInfo' + ) + const notificationServiceShowErrorSpy = jest.spyOn( + notificationService, + 'showError' + ) const listReloadSpy = jest.spyOn(documentListViewService, 'reload') component.customFields = [ @@ -1410,11 +1416,11 @@ describe('BulkEditorComponent', () => { expect(modal.componentInstance.documents).toEqual([3, 4]) modal.componentInstance.failed.emit() - expect(toastServiceShowErrorSpy).toHaveBeenCalled() + expect(notificationServiceShowErrorSpy).toHaveBeenCalled() expect(listReloadSpy).not.toHaveBeenCalled() modal.componentInstance.succeeded.emit() - expect(toastServiceShowInfoSpy).toHaveBeenCalled() + expect(notificationServiceShowInfoSpy).toHaveBeenCalled() expect(listReloadSpy).toHaveBeenCalled() httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true` diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 9864761fa..72fccac0d 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -23,6 +23,7 @@ import { Tag } from 'src/app/data/tag' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { PermissionAction, @@ -39,7 +40,6 @@ import { import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { TagService } from 'src/app/services/rest/tag.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' @@ -113,7 +113,7 @@ export class BulkEditorComponent private modalService: NgbModal, private openDocumentService: OpenDocumentsService, private settings: SettingsService, - private toastService: ToastService, + private notificationService: NotificationService, private storagePathService: StoragePathService, private customFieldService: CustomFieldsService, private permissionService: PermissionsService @@ -284,7 +284,7 @@ export class BulkEditorComponent if (modal) { modal.componentInstance.buttonsEnabled = true } - this.toastService.showError( + this.notificationService.showError( $localize`Error executing bulk operation`, error ) @@ -859,7 +859,7 @@ export class BulkEditorComponent } mergeDialog.buttonsEnabled = false this.executeBulkOperation(modal, 'merge', args, mergeDialog.documentIDs) - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Merged document will be queued for consumption.` ) }) @@ -882,7 +882,7 @@ export class BulkEditorComponent dialog.documents = Array.from(this.list.selected) dialog.succeeded.subscribe((result) => { - this.toastService.showInfo($localize`Custom fields updated.`) + this.notificationService.showInfo($localize`Custom fields updated.`) this.list.reload() this.list.reduceSelectionToFilter() this.list.selected.forEach((id) => { @@ -890,7 +890,7 @@ export class BulkEditorComponent }) }) dialog.failed.subscribe((error) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error updating custom fields.`, error ) diff --git a/src-ui/src/app/components/document-list/document-list.component.spec.ts b/src-ui/src/app/components/document-list/document-list.component.spec.ts index aae043fdb..290892411 100644 --- a/src-ui/src/app/components/document-list/document-list.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-list.component.spec.ts @@ -39,11 +39,11 @@ import { FilterPipe } from 'src/app/pipes/filter.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { UsernamePipe } from 'src/app/pipes/username.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { DocumentService } from 'src/app/services/rest/document.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { FileStatus, WebsocketStatusService, @@ -85,7 +85,7 @@ describe('DocumentListComponent', () => { let savedViewService: SavedViewService let router: Router let activatedRoute: ActivatedRoute - let toastService: ToastService + let notificationService: NotificationService let modalService: NgbModal let settingsService: SettingsService let permissionService: PermissionsService @@ -116,7 +116,7 @@ describe('DocumentListComponent', () => { savedViewService = TestBed.inject(SavedViewService) router = TestBed.inject(Router) activatedRoute = TestBed.inject(ActivatedRoute) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) modalService = TestBed.inject(NgbModal) settingsService = TestBed.inject(SettingsService) permissionService = TestBed.inject(PermissionsService) @@ -405,11 +405,11 @@ describe('DocumentListComponent', () => { delete modifiedView.name const savedViewServicePatch = jest.spyOn(savedViewService, 'patch') savedViewServicePatch.mockReturnValue(of(modifiedView)) - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') component.saveViewConfig() expect(savedViewServicePatch).toHaveBeenCalledWith(modifiedView) - expect(toastSpy).toHaveBeenCalledWith( + expect(notificationSpy).toHaveBeenCalledWith( `View "${view.name}" saved successfully.` ) }) @@ -427,12 +427,12 @@ describe('DocumentListComponent', () => { }, ], }) - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(savedViewService, 'patch') .mockReturnValueOnce(throwError(() => new Error('Error saving view'))) component.saveViewConfig() - expect(toastErrorSpy).toHaveBeenCalledWith( + expect(notificationErrorSpy).toHaveBeenCalledWith( 'Failed to save view "Saved View 10".', expect.any(Error) ) @@ -467,7 +467,7 @@ describe('DocumentListComponent', () => { let openModal: NgbModalRef modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) const modalSpy = jest.spyOn(modalService, 'open') - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') const savedViewServiceCreate = jest.spyOn(savedViewService, 'create') savedViewServiceCreate.mockReturnValueOnce(of(modifiedView)) component.saveViewConfigAs() @@ -480,7 +480,7 @@ describe('DocumentListComponent', () => { }) expect(savedViewServiceCreate).toHaveBeenCalled() expect(modalSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() expect(modalCloseSpy).toHaveBeenCalled() }) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index f6b7c181b..397cd0e53 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -45,11 +45,11 @@ import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe' import { UsernamePipe } from 'src/app/pipes/username.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { HotKeyService } from 'src/app/services/hot-key.service' +import { NotificationService } from 'src/app/services/notification.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { PermissionsService } from 'src/app/services/permissions.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { WebsocketStatusService } from 'src/app/services/websocket-status.service' import { filterRulesDiffer, @@ -111,7 +111,7 @@ export class DocumentListComponent public savedViewService: SavedViewService, public route: ActivatedRoute, private router: Router, - private toastService: ToastService, + private notificationService: NotificationService, private modalService: NgbModal, private websocketStatusService: WebsocketStatusService, public openDocumentsService: OpenDocumentsService, @@ -380,13 +380,13 @@ export class DocumentListComponent .subscribe({ next: (view) => { this.unmodifiedSavedView = view - this.toastService.showInfo( + this.notificationService.showInfo( $localize`View "${this.list.activeSavedViewTitle}" saved successfully.` ) this.unmodifiedFilterRules = this.list.filterRules }, error: (err) => { - this.toastService.showError( + this.notificationService.showError( $localize`Failed to save view "${this.list.activeSavedViewTitle}".`, err ) @@ -430,7 +430,7 @@ export class DocumentListComponent .subscribe({ next: () => { modal.close() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`View "${savedView.name}" created successfully.` ) }, diff --git a/src-ui/src/app/components/document-notes/document-notes.component.spec.ts b/src-ui/src/app/components/document-notes/document-notes.component.spec.ts index 1c86c03a5..ff5714538 100644 --- a/src-ui/src/app/components/document-notes/document-notes.component.spec.ts +++ b/src-ui/src/app/components/document-notes/document-notes.component.spec.ts @@ -9,10 +9,10 @@ import { of, throwError } from 'rxjs' import { DocumentNote } from 'src/app/data/document-note' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { DocumentNotesService } from 'src/app/services/rest/document-notes.service' import { UserService } from 'src/app/services/rest/user.service' -import { ToastService } from 'src/app/services/toast.service' import { DocumentNotesComponent } from './document-notes.component' const notes: DocumentNote[] = [ @@ -52,7 +52,7 @@ describe('DocumentNotesComponent', () => { let component: DocumentNotesComponent let fixture: ComponentFixture let notesService: DocumentNotesService - let toastService: ToastService + let notificationService: NotificationService beforeEach(async () => { TestBed.configureTestingModule({ @@ -103,7 +103,7 @@ describe('DocumentNotesComponent', () => { }).compileComponents() notesService = TestBed.inject(DocumentNotesService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) fixture = TestBed.createComponent(DocumentNotesComponent) component = fixture.componentInstance fixture.detectChanges() @@ -162,11 +162,11 @@ describe('DocumentNotesComponent', () => { fixture.detectChanges() const addSpy = jest.spyOn(notesService, 'addNote') addSpy.mockReturnValueOnce(throwError(() => new Error('error saving note'))) - const toastsSpy = jest.spyOn(toastService, 'showError') + const notificationsSpy = jest.spyOn(notificationService, 'showError') const addButton = fixture.debugElement.query(By.css('button')) addButton.triggerEventHandler('click') expect(addSpy).toHaveBeenCalledWith(12, note) - expect(toastsSpy).toHaveBeenCalled() + expect(notificationsSpy).toHaveBeenCalled() addSpy.mockReturnValueOnce( of([...notes, { id: 31, note, user: { id: 1 } }]) @@ -194,7 +194,7 @@ describe('DocumentNotesComponent', () => { fixture.detectChanges() const deleteButton = fixture.debugElement.queryAll(By.css('button'))[1] // 0 is add button const deleteSpy = jest.spyOn(notesService, 'deleteNote') - const toastsSpy = jest.spyOn(toastService, 'showError') + const toastsSpy = jest.spyOn(notificationService, 'showError') deleteSpy.mockReturnValueOnce( throwError(() => new Error('error deleting note')) ) diff --git a/src-ui/src/app/components/document-notes/document-notes.component.ts b/src-ui/src/app/components/document-notes/document-notes.component.ts index 685d2d93e..32877826f 100644 --- a/src-ui/src/app/components/document-notes/document-notes.component.ts +++ b/src-ui/src/app/components/document-notes/document-notes.component.ts @@ -10,9 +10,9 @@ import { DocumentNote } from 'src/app/data/document-note' import { User } from 'src/app/data/user' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' +import { NotificationService } from 'src/app/services/notification.service' import { DocumentNotesService } from 'src/app/services/rest/document-notes.service' import { UserService } from 'src/app/services/rest/user.service' -import { ToastService } from 'src/app/services/toast.service' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' @Component({ @@ -50,7 +50,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions { constructor( private notesService: DocumentNotesService, - private toastService: ToastService, + private notificationService: NotificationService, private usersService: UserService ) { super() @@ -78,7 +78,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions { }, error: (e) => { this.networkActive = false - this.toastService.showError($localize`Error saving note`, e) + this.notificationService.showError($localize`Error saving note`, e) }, }) } @@ -92,7 +92,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions { }, error: (e) => { this.networkActive = false - this.toastService.showError($localize`Error deleting note`, e) + this.notificationService.showError($localize`Error deleting note`, e) }, }) } diff --git a/src-ui/src/app/components/file-drop/file-drop.component.spec.ts b/src-ui/src/app/components/file-drop/file-drop.component.spec.ts index bd3a56a3f..fa5ebc807 100644 --- a/src-ui/src/app/components/file-drop/file-drop.component.spec.ts +++ b/src-ui/src/app/components/file-drop/file-drop.component.spec.ts @@ -10,24 +10,28 @@ import { } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { UploadDocumentsService } from 'src/app/services/upload-documents.service' -import { ToastsComponent } from '../common/toasts/toasts.component' +import { NotificationListComponent } from '../common/notification-list/notification-list.component' import { FileDropComponent } from './file-drop.component' describe('FileDropComponent', () => { let component: FileDropComponent let fixture: ComponentFixture let permissionsService: PermissionsService - let toastService: ToastService + let notificationService: NotificationService let settingsService: SettingsService let uploadDocumentsService: UploadDocumentsService beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxFileDropModule, FileDropComponent, ToastsComponent], + imports: [ + NgxFileDropModule, + FileDropComponent, + NotificationListComponent, + ], providers: [ provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), @@ -36,7 +40,7 @@ describe('FileDropComponent', () => { permissionsService = TestBed.inject(PermissionsService) settingsService = TestBed.inject(SettingsService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) uploadDocumentsService = TestBed.inject(UploadDocumentsService) fixture = TestBed.createComponent(FileDropComponent) @@ -101,7 +105,7 @@ describe('FileDropComponent', () => { fixture.detectChanges() expect(dropzone.classes['hide']).toBeTruthy() // drop - const toastSpy = jest.spyOn(toastService, 'show') + const notificationSpy = jest.spyOn(notificationService, 'show') const uploadSpy = jest.spyOn( UploadDocumentsService.prototype as any, 'uploadFile' @@ -135,7 +139,7 @@ describe('FileDropComponent', () => { } as unknown as NgxFileDropEntry, ]) tick(3000) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() expect(uploadSpy).toHaveBeenCalled() discardPeriodicTasks() })) diff --git a/src-ui/src/app/components/file-drop/file-drop.component.ts b/src-ui/src/app/components/file-drop/file-drop.component.ts index 49eb423b2..65ebe6523 100644 --- a/src-ui/src/app/components/file-drop/file-drop.component.ts +++ b/src-ui/src/app/components/file-drop/file-drop.component.ts @@ -4,13 +4,13 @@ import { NgxFileDropEntry, NgxFileDropModule, } from 'ngx-file-drop' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionAction, PermissionsService, PermissionType, } from 'src/app/services/permissions.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { UploadDocumentsService } from 'src/app/services/upload-documents.service' @Component({ @@ -26,7 +26,7 @@ export class FileDropComponent { constructor( private settings: SettingsService, - private toastService: ToastService, + private notificationService: NotificationService, private uploadDocumentsService: UploadDocumentsService, private permissionsService: PermissionsService ) {} @@ -90,7 +90,7 @@ export class FileDropComponent { public dropped(files: NgxFileDropEntry[]) { this.uploadDocumentsService.onNgxFileDrop(files) if (files.length > 0) - this.toastService.showInfo($localize`Initiating upload...`, 3000) + this.notificationService.showInfo($localize`Initiating upload...`, 3000) } @HostListener('window:blur', ['$event']) public onWindowBlur() { diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts index 62427f5b4..e47422a58 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts @@ -13,12 +13,12 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct import { SortableDirective } from 'src/app/directives/sortable.directive' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService, PermissionType, } from 'src/app/services/permissions.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service' -import { ToastService } from 'src/app/services/toast.service' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { ManagementListComponent } from '../management-list/management-list.component' @@ -45,7 +45,7 @@ export class CorrespondentListComponent extends ManagementListComponent { let fixture: ComponentFixture let customFieldsService: CustomFieldsService let modalService: NgbModal - let toastService: ToastService + let notificationService: NotificationService let listViewService: DocumentListViewService let settingsService: SettingsService @@ -89,7 +89,7 @@ describe('CustomFieldsComponent', () => { }) ) modalService = TestBed.inject(NgbModal) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) listViewService = TestBed.inject(DocumentListViewService) settingsService = TestBed.inject(SettingsService) settingsService.currentUser = { id: 0, username: 'test' } @@ -104,8 +104,8 @@ describe('CustomFieldsComponent', () => { it('should support create, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reload') const createButton = fixture.debugElement.queryAll(By.css('button'))[1] @@ -116,12 +116,12 @@ describe('CustomFieldsComponent', () => { // fail first editDialog.failed.emit({ error: 'error creating item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed editDialog.succeeded.emit(fields[0]) - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled() jest.advanceTimersByTime(100) }) @@ -129,8 +129,8 @@ describe('CustomFieldsComponent', () => { it('should support edit, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reload') const editButton = fixture.debugElement.queryAll(By.css('button'))[2] @@ -142,19 +142,19 @@ describe('CustomFieldsComponent', () => { // fail first editDialog.failed.emit({ error: 'error editing item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed editDialog.succeeded.emit(fields[0]) - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled() }) it('should support delete, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') const deleteSpy = jest.spyOn(customFieldsService, 'delete') const reloadSpy = jest.spyOn(component, 'reload') @@ -167,7 +167,7 @@ describe('CustomFieldsComponent', () => { // fail first deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting'))) editDialog.confirmClicked.emit() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts index b4fd9738d..caf20bbb1 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts @@ -14,12 +14,12 @@ import { import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { DocumentService } from 'src/app/services/rest/document.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' @@ -48,7 +48,7 @@ export class CustomFieldsComponent private customFieldsService: CustomFieldsService, public permissionsService: PermissionsService, private modalService: NgbModal, - private toastService: ToastService, + private notificationService: NotificationService, private documentListViewService: DocumentListViewService, private settingsService: SettingsService, private documentService: DocumentService, @@ -86,7 +86,9 @@ export class CustomFieldsComponent modal.componentInstance.succeeded .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((newField) => { - this.toastService.showInfo($localize`Saved field "${newField.name}".`) + this.notificationService.showInfo( + $localize`Saved field "${newField.name}".` + ) this.customFieldsService.clearCache() this.settingsService.initializeDisplayFields() this.documentService.reload() @@ -95,7 +97,7 @@ export class CustomFieldsComponent modal.componentInstance.failed .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((e) => { - this.toastService.showError($localize`Error saving field.`, e) + this.notificationService.showError($localize`Error saving field.`, e) }) } @@ -113,7 +115,9 @@ export class CustomFieldsComponent this.customFieldsService.delete(field).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted field "${field.name}"`) + this.notificationService.showInfo( + $localize`Deleted field "${field.name}"` + ) this.customFieldsService.clearCache() this.settingsService.initializeDisplayFields() this.documentService.reload() @@ -121,7 +125,7 @@ export class CustomFieldsComponent this.reload() }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting field "${field.name}".`, e ) diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts index 0bca3df1b..6c353f91d 100644 --- a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts +++ b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts @@ -12,12 +12,12 @@ import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { SortableDirective } from 'src/app/directives/sortable.directive' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService, PermissionType, } from 'src/app/services/permissions.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service' -import { ToastService } from 'src/app/services/toast.service' import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { ManagementListComponent } from '../management-list/management-list.component' @@ -43,7 +43,7 @@ export class DocumentTypeListComponent extends ManagementListComponent { let mailAccountService: MailAccountService let mailRuleService: MailRuleService let modalService: NgbModal - let toastService: ToastService + let notificationService: NotificationService let permissionsService: PermissionsService let activatedRoute: ActivatedRoute let settingsService: SettingsService @@ -111,7 +111,7 @@ describe('MailComponent', () => { mailAccountService = TestBed.inject(MailAccountService) mailRuleService = TestBed.inject(MailRuleService) modalService = TestBed.inject(NgbModal) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) permissionsService = TestBed.inject(PermissionsService) activatedRoute = TestBed.inject(ActivatedRoute) settingsService = TestBed.inject(SettingsService) @@ -157,25 +157,25 @@ describe('MailComponent', () => { } it('should show errors on load if load mailAccounts failure', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(mailAccountService, 'listAll') .mockImplementation(() => throwError(() => new Error('failed to load mail accounts')) ) completeSetup(mailAccountService) - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() }) it('should show errors on load if load mailRules failure', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') jest .spyOn(mailRuleService, 'listAll') .mockImplementation(() => throwError(() => new Error('failed to load mail rules')) ) completeSetup(mailRuleService) - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() }) it('should support edit / create mail account, show error if needed', () => { @@ -184,12 +184,12 @@ describe('MailComponent', () => { modalService.activeInstances.subscribe((refs) => (modal = refs[0])) component.editMailAccount(mailAccounts[0] as MailAccount) let editDialog = modal.componentInstance as MailAccountEditDialogComponent - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') editDialog.failed.emit() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() editDialog.succeeded.emit(mailAccounts[0]) - expect(toastInfoSpy).toHaveBeenCalledWith( + expect(notificationInfoSpy).toHaveBeenCalledWith( `Saved account "${mailAccounts[0].name}".` ) editDialog.cancel() @@ -203,35 +203,37 @@ describe('MailComponent', () => { component.deleteMailAccount(mailAccounts[0] as MailAccount) const deleteDialog = modal.componentInstance as ConfirmDialogComponent const deleteSpy = jest.spyOn(mailAccountService, 'delete') - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const listAllSpy = jest.spyOn(mailAccountService, 'listAll') deleteSpy.mockReturnValueOnce( throwError(() => new Error('error deleting mail account')) ) deleteDialog.confirm() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account "account1"') + expect(notificationInfoSpy).toHaveBeenCalledWith( + 'Deleted mail account "account1"' + ) }) it('should support process mail account, show error if needed', () => { completeSetup() const processSpy = jest.spyOn(mailAccountService, 'processAccount') - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') component.processAccount(mailAccounts[0] as MailAccount) expect(processSpy).toHaveBeenCalled() processSpy.mockReturnValueOnce( throwError(() => new Error('error processing mail account')) ) component.processAccount(mailAccounts[0] as MailAccount) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() processSpy.mockReturnValueOnce(of(true)) component.processAccount(mailAccounts[0] as MailAccount) - expect(toastInfoSpy).toHaveBeenCalledWith( + expect(notificationInfoSpy).toHaveBeenCalledWith( 'Processing mail account "account1"' ) }) @@ -242,12 +244,12 @@ describe('MailComponent', () => { modalService.activeInstances.subscribe((refs) => (modal = refs[0])) component.editMailRule(mailRules[0] as MailRule) const editDialog = modal.componentInstance as MailRuleEditDialogComponent - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') editDialog.failed.emit() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() editDialog.succeeded.emit(mailRules[0]) - expect(toastInfoSpy).toHaveBeenCalledWith( + expect(notificationInfoSpy).toHaveBeenCalledWith( `Saved rule "${mailRules[0].name}".` ) editDialog.cancel() @@ -272,18 +274,20 @@ describe('MailComponent', () => { component.deleteMailRule(mailRules[0] as MailRule) const deleteDialog = modal.componentInstance as ConfirmDialogComponent const deleteSpy = jest.spyOn(mailRuleService, 'delete') - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const listAllSpy = jest.spyOn(mailRuleService, 'listAll') deleteSpy.mockReturnValueOnce( throwError(() => new Error('error deleting mail rule "rule1"')) ) deleteDialog.confirm() - expect(toastErrorSpy).toBeCalled() + expect(notificationErrorSpy).toBeCalled() deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule "rule1"') + expect(notificationInfoSpy).toHaveBeenCalledWith( + 'Deleted mail rule "rule1"' + ) }) it('should support edit permissions on mail rule objects', () => { @@ -303,8 +307,8 @@ describe('MailComponent', () => { } let modal: NgbModalRef modalService.activeInstances.subscribe((refs) => (modal = refs[0])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const rulePatchSpy = jest.spyOn(mailRuleService, 'patch') component.editPermissions(mailRules[0] as MailRule) expect(modal).not.toBeUndefined() @@ -316,10 +320,10 @@ describe('MailComponent', () => { ) dialog.confirmClicked.emit({ permissions: perms, merge: true }) expect(rulePatchSpy).toHaveBeenCalled() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule)) dialog.confirmClicked.emit({ permissions: perms, merge: true }) - expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated') + expect(notificationInfoSpy).toHaveBeenCalledWith('Permissions updated') modalService.dismissAll() }) @@ -356,15 +360,15 @@ describe('MailComponent', () => { const toggleInput = fixture.debugElement.query( By.css('input[type="checkbox"]') ) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') // fail first patchSpy.mockReturnValueOnce( throwError(() => new Error('Error getting config')) ) toggleInput.nativeElement.click() expect(patchSpy).toHaveBeenCalled() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() // succeed second patchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule)) toggleInput.nativeElement.click() @@ -373,7 +377,7 @@ describe('MailComponent', () => { ) toggleInput.nativeElement.click() expect(patchSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() }) it('should show success message when oauth account is connected', () => { @@ -381,9 +385,9 @@ describe('MailComponent', () => { jest .spyOn(activatedRoute, 'queryParamMap', 'get') .mockReturnValue(of(convertToParamMap(queryParams))) - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') completeSetup() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() }) it('should show error message when oauth account connect fails', () => { @@ -391,9 +395,9 @@ describe('MailComponent', () => { jest .spyOn(activatedRoute, 'queryParamMap', 'get') .mockReturnValue(of(convertToParamMap(queryParams))) - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') completeSetup() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() }) it('should open account edit dialog if oauth account is connected', () => { diff --git a/src-ui/src/app/components/manage/mail/mail.component.ts b/src-ui/src/app/components/manage/mail/mail.component.ts index 8d4222516..ebe5231e5 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.ts +++ b/src-ui/src/app/components/manage/mail/mail.component.ts @@ -11,6 +11,7 @@ import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionAction, PermissionsService, @@ -19,7 +20,6 @@ import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperle import { MailAccountService } from 'src/app/services/rest/mail-account.service' import { MailRuleService } from 'src/app/services/rest/mail-rule.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' @@ -71,7 +71,7 @@ export class MailComponent constructor( public mailAccountService: MailAccountService, public mailRuleService: MailRuleService, - private toastService: ToastService, + private notificationService: NotificationService, private modalService: NgbModal, public permissionsService: PermissionsService, private settingsService: SettingsService, @@ -104,7 +104,7 @@ export class MailComponent this.showAccounts = true }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error retrieving mail accounts`, e ) @@ -127,7 +127,10 @@ export class MailComponent this.showRules = true }, error: (e) => { - this.toastService.showError($localize`Error retrieving mail rules`, e) + this.notificationService.showError( + $localize`Error retrieving mail rules`, + e + ) }, }) @@ -135,7 +138,9 @@ export class MailComponent if (params.get('oauth_success')) { const success = params.get('oauth_success') === '1' if (success) { - this.toastService.showInfo($localize`OAuth2 authentication success`) + this.notificationService.showInfo( + $localize`OAuth2 authentication success` + ) this.oAuthAccountId = parseInt(params.get('account_id')) if (this.mailAccounts.length > 0) { this.editMailAccount( @@ -145,7 +150,7 @@ export class MailComponent ) } } else { - this.toastService.showError( + this.notificationService.showError( $localize`OAuth2 authentication failed, see logs for details` ) } @@ -169,7 +174,7 @@ export class MailComponent modal.componentInstance.succeeded .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((newMailAccount) => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Saved account "${newMailAccount.name}".` ) this.mailAccountService.clearCache() @@ -182,7 +187,7 @@ export class MailComponent modal.componentInstance.failed .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((e) => { - this.toastService.showError($localize`Error saving account.`, e) + this.notificationService.showError($localize`Error saving account.`, e) }) } @@ -200,7 +205,7 @@ export class MailComponent this.mailAccountService.delete(account).subscribe({ next: () => { modal.close() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Deleted mail account "${account.name}"` ) this.mailAccountService.clearCache() @@ -211,7 +216,7 @@ export class MailComponent }) }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting mail account "${account.name}".`, e ) @@ -223,12 +228,12 @@ export class MailComponent processAccount(account: MailAccount) { this.mailAccountService.processAccount(account).subscribe({ next: () => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Processing mail account "${account.name}"` ) }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error processing mail account "${account.name}"`, e ) @@ -247,7 +252,9 @@ export class MailComponent modal.componentInstance.succeeded .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((newMailRule) => { - this.toastService.showInfo($localize`Saved rule "${newMailRule.name}".`) + this.notificationService.showInfo( + $localize`Saved rule "${newMailRule.name}".` + ) this.mailRuleService.clearCache() this.mailRuleService .listAll(null, null, { full_perms: true }) @@ -258,7 +265,7 @@ export class MailComponent modal.componentInstance.failed .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((e) => { - this.toastService.showError($localize`Error saving rule.`, e) + this.notificationService.showError($localize`Error saving rule.`, e) }) } @@ -272,14 +279,14 @@ export class MailComponent onMailRuleEnableToggled(rule: MailRule) { this.mailRuleService.patch(rule).subscribe({ next: () => { - this.toastService.showInfo( + this.notificationService.showInfo( rule.enabled ? $localize`Rule "${rule.name}" enabled.` : $localize`Rule "${rule.name}" disabled.` ) }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error toggling rule "${rule.name}".`, e ) @@ -301,7 +308,7 @@ export class MailComponent this.mailRuleService.delete(rule).subscribe({ next: () => { modal.close() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Deleted mail rule "${rule.name}"` ) this.mailRuleService.clearCache() @@ -312,7 +319,7 @@ export class MailComponent }) }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting mail rule "${rule.name}".`, e ) @@ -337,11 +344,11 @@ export class MailComponent object['set_permissions'] = permissions['set_permissions'] service.patch(object).subscribe({ next: () => { - this.toastService.showInfo($localize`Permissions updated`) + this.notificationService.showInfo($localize`Permissions updated`) modal.close() }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error updating permissions`, e ) diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts index 291a71fa3..a167edecc 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts @@ -35,13 +35,13 @@ import { SortableDirective } from 'src/app/directives/sortable.directive' import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionAction, PermissionsService, } from 'src/app/services/permissions.service' import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-filter-service' import { TagService } from 'src/app/services/rest/tag.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { EditDialogComponent } from '../../common/edit-dialog/edit-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' @@ -76,7 +76,7 @@ describe('ManagementListComponent', () => { let fixture: ComponentFixture> let tagService: TagService let modalService: NgbModal - let toastService: ToastService + let notificationService: NotificationService let documentListViewService: DocumentListViewService let permissionsService: PermissionsService @@ -129,7 +129,7 @@ describe('ManagementListComponent', () => { .spyOn(permissionsService, 'currentUserOwnsObject') .mockReturnValue(true) modalService = TestBed.inject(NgbModal) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) documentListViewService = TestBed.inject(DocumentListViewService) fixture = TestBed.createComponent(TagListComponent) component = fixture.componentInstance @@ -160,8 +160,8 @@ describe('ManagementListComponent', () => { it('should support create, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reloadData') const createButton = fixture.debugElement.queryAll(By.css('button'))[3] @@ -172,20 +172,20 @@ describe('ManagementListComponent', () => { // fail first editDialog.failed.emit({ error: 'error creating item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed editDialog.succeeded.emit() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled() }) it('should support edit, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reloadData') const editButton = fixture.debugElement.queryAll(By.css('button'))[6] @@ -197,19 +197,19 @@ describe('ManagementListComponent', () => { // fail first editDialog.failed.emit({ error: 'error editing item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed editDialog.succeeded.emit() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled() }) it('should support delete, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') const deleteSpy = jest.spyOn(tagService, 'delete') const reloadSpy = jest.spyOn(component, 'reloadData') @@ -222,7 +222,7 @@ describe('ManagementListComponent', () => { // fail first deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting'))) editDialog.confirmClicked.emit() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed @@ -293,22 +293,22 @@ describe('ManagementListComponent', () => { bulkEditPermsSpy.mockReturnValueOnce( throwError(() => new Error('error setting permissions')) ) - const errorToastSpy = jest.spyOn(toastService, 'showError') + const errornotificationSpy = jest.spyOn(notificationService, 'showError') modal.componentInstance.confirmClicked.emit({ permissions: {}, merge: true, }) expect(bulkEditPermsSpy).toHaveBeenCalled() - expect(errorToastSpy).toHaveBeenCalled() + expect(errornotificationSpy).toHaveBeenCalled() - const successToastSpy = jest.spyOn(toastService, 'showInfo') + const successnotificationSpy = jest.spyOn(notificationService, 'showInfo') bulkEditPermsSpy.mockReturnValueOnce(of('OK')) modal.componentInstance.confirmClicked.emit({ permissions: {}, merge: true, }) expect(bulkEditPermsSpy).toHaveBeenCalled() - expect(successToastSpy).toHaveBeenCalled() + expect(successnotificationSpy).toHaveBeenCalled() }) it('should support bulk delete objects', () => { @@ -327,19 +327,19 @@ describe('ManagementListComponent', () => { bulkEditSpy.mockReturnValueOnce( throwError(() => new Error('error setting permissions')) ) - const errorToastSpy = jest.spyOn(toastService, 'showError') + const errornotificationSpy = jest.spyOn(notificationService, 'showError') modal.componentInstance.confirmClicked.emit(null) expect(bulkEditSpy).toHaveBeenCalledWith( Array.from(selected), BulkEditObjectOperation.Delete ) - expect(errorToastSpy).toHaveBeenCalled() + expect(errornotificationSpy).toHaveBeenCalled() - const successToastSpy = jest.spyOn(toastService, 'showInfo') + const successnotificationSpy = jest.spyOn(notificationService, 'showInfo') bulkEditSpy.mockReturnValueOnce(of('OK')) modal.componentInstance.confirmClicked.emit(null) expect(bulkEditSpy).toHaveBeenCalled() - expect(successToastSpy).toHaveBeenCalled() + expect(successnotificationSpy).toHaveBeenCalled() }) it('should disallow bulk permissions or delete objects if no global perms', () => { diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index 7f7721485..6d43741f6 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -27,6 +27,7 @@ import { SortEvent, } from 'src/app/directives/sortable.directive' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionAction, PermissionsService, @@ -36,7 +37,6 @@ import { AbstractNameFilterService, BulkEditObjectOperation, } from 'src/app/services/rest/abstract-name-filter-service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component' @@ -63,7 +63,7 @@ export abstract class ManagementListComponent protected service: AbstractNameFilterService, private modalService: NgbModal, private editDialogComponent: any, - private toastService: ToastService, + private notificationService: NotificationService, private documentListViewService: DocumentListViewService, private permissionsService: PermissionsService, protected filterRuleType: number, @@ -173,12 +173,12 @@ export abstract class ManagementListComponent activeModal.componentInstance.dialogMode = EditDialogMode.CREATE activeModal.componentInstance.succeeded.subscribe(() => { this.reloadData() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Successfully created ${this.typeName}.` ) }) activeModal.componentInstance.failed.subscribe((e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error occurred while creating ${this.typeName}.`, e ) @@ -193,12 +193,12 @@ export abstract class ManagementListComponent activeModal.componentInstance.dialogMode = EditDialogMode.EDIT activeModal.componentInstance.succeeded.subscribe(() => { this.reloadData() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Successfully updated ${this.typeName} "${object.name}".` ) }) activeModal.componentInstance.failed.subscribe((e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error occurred while saving ${this.typeName}.`, e ) @@ -234,7 +234,7 @@ export abstract class ManagementListComponent }, error: (error) => { activeModal.componentInstance.buttonsEnabled = true - this.toastService.showError( + this.notificationService.showError( $localize`Error while deleting element`, error ) @@ -313,14 +313,14 @@ export abstract class ManagementListComponent .subscribe({ next: () => { modal.close() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Permissions updated successfully` ) this.reloadData() }, error: (error) => { modal.componentInstance.buttonsEnabled = true - this.toastService.showError( + this.notificationService.showError( $localize`Error updating permissions`, error ) @@ -349,12 +349,14 @@ export abstract class ManagementListComponent .subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Objects deleted successfully`) + this.notificationService.showInfo( + $localize`Objects deleted successfully` + ) this.reloadData() }, error: (error) => { modal.componentInstance.buttonsEnabled = true - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting objects`, error ) diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts index 10bc5db8e..18cc3e4b7 100644 --- a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts +++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts @@ -10,10 +10,10 @@ import { of, throwError } from 'rxjs' import { SavedView } from 'src/app/data/saved-view' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { PermissionsGuard } from 'src/app/guards/permissions.guard' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' import { CheckComponent } from '../../common/input/check/check.component' import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component' @@ -32,7 +32,7 @@ describe('SavedViewsComponent', () => { let component: SavedViewsComponent let fixture: ComponentFixture let savedViewService: SavedViewService - let toastService: ToastService + let notificationService: NotificationService beforeEach(async () => { TestBed.configureTestingModule({ @@ -77,7 +77,7 @@ describe('SavedViewsComponent', () => { }).compileComponents() savedViewService = TestBed.inject(SavedViewService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) fixture = TestBed.createComponent(SavedViewsComponent) component = fixture.componentInstance @@ -93,8 +93,8 @@ describe('SavedViewsComponent', () => { }) it('should support save saved views, show error', () => { - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastSpy = jest.spyOn(toastService, 'show') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'show') const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany') const toggle = fixture.debugElement.query( @@ -108,16 +108,16 @@ describe('SavedViewsComponent', () => { throwError(() => new Error('unable to save saved views')) ) component.save() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(savedViewPatchSpy).toHaveBeenCalled() - toastSpy.mockClear() - toastErrorSpy.mockClear() + notificationSpy.mockClear() + notificationErrorSpy.mockClear() savedViewPatchSpy.mockClear() // succeed saved views savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[])) component.save() - expect(toastErrorSpy).not.toHaveBeenCalled() + expect(notificationErrorSpy).not.toHaveBeenCalled() expect(savedViewPatchSpy).toHaveBeenCalled() }) @@ -150,12 +150,12 @@ describe('SavedViewsComponent', () => { }) it('should support delete saved view', () => { - const toastSpy = jest.spyOn(toastService, 'showInfo') + const notificationSpy = jest.spyOn(notificationService, 'showInfo') const deleteSpy = jest.spyOn(savedViewService, 'delete') deleteSpy.mockReturnValue(of(true)) component.deleteSavedView(savedViews[0] as SavedView) expect(deleteSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith( + expect(notificationSpy).toHaveBeenCalledWith( `Saved view "${savedViews[0].name}" deleted.` ) }) diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.ts b/src-ui/src/app/components/manage/saved-views/saved-views.component.ts index ee2b00c2b..1803e2b1c 100644 --- a/src-ui/src/app/components/manage/saved-views/saved-views.component.ts +++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.ts @@ -11,9 +11,9 @@ import { BehaviorSubject, Observable, takeUntil } from 'rxjs' import { DisplayMode } from 'src/app/data/document' import { SavedView } from 'src/app/data/saved-view' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component' import { NumberComponent } from '../../common/input/number/number.component' @@ -58,7 +58,7 @@ export class SavedViewsComponent constructor( private savedViewService: SavedViewService, private settings: SettingsService, - private toastService: ToastService + private notificationService: NotificationService ) { super() this.settings.organizingSidebarSavedViews = true @@ -129,7 +129,7 @@ export class SavedViewsComponent this.savedViewService.delete(savedView).subscribe(() => { this.savedViewsGroup.removeControl(savedView.id.toString()) this.savedViews.splice(this.savedViews.indexOf(savedView), 1) - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Saved view "${savedView.name}" deleted.` ) this.savedViewService.clearCache() @@ -155,11 +155,13 @@ export class SavedViewsComponent if (changed.length) { this.savedViewService.patchMany(changed).subscribe({ next: () => { - this.toastService.showInfo($localize`Views saved successfully.`) + this.notificationService.showInfo( + $localize`Views saved successfully.` + ) this.store.next(this.savedViewsForm.value) }, error: (error) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error while saving views.`, error ) diff --git a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts index 89a243324..d49bf4255 100644 --- a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts +++ b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts @@ -13,12 +13,12 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct import { SortableDirective } from 'src/app/directives/sortable.directive' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService, PermissionType, } from 'src/app/services/permissions.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' -import { ToastService } from 'src/app/services/toast.service' import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { ManagementListComponent } from '../management-list/management-list.component' @@ -45,7 +45,7 @@ export class StoragePathListComponent extends ManagementListComponent { constructor( tagService: TagService, modalService: NgbModal, - toastService: ToastService, + notificationService: NotificationService, documentListViewService: DocumentListViewService, permissionsService: PermissionsService ) { @@ -53,7 +53,7 @@ export class TagListComponent extends ManagementListComponent { tagService, modalService, TagEditDialogComponent, - toastService, + notificationService, documentListViewService, permissionsService, FILTER_HAS_TAGS_ALL, diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts index 636e03d54..58f8fbe9a 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts @@ -19,9 +19,9 @@ import { WorkflowTriggerType, } from 'src/app/data/workflow-trigger' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { WorkflowService } from 'src/app/services/rest/workflow.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component' @@ -77,7 +77,7 @@ describe('WorkflowsComponent', () => { let fixture: ComponentFixture let workflowService: WorkflowService let modalService: NgbModal - let toastService: ToastService + let notificationService: NotificationService beforeEach(() => { TestBed.configureTestingModule({ @@ -116,7 +116,7 @@ describe('WorkflowsComponent', () => { }) ) modalService = TestBed.inject(NgbModal) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) jest.useFakeTimers() fixture = TestBed.createComponent(WorkflowsComponent) component = fixture.componentInstance @@ -127,8 +127,8 @@ describe('WorkflowsComponent', () => { it('should support create, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reload') const createButton = fixture.debugElement.queryAll(By.css('button'))[1] @@ -139,20 +139,20 @@ describe('WorkflowsComponent', () => { // fail first editDialog.failed.emit({ error: 'error creating item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed editDialog.succeeded.emit(workflows[0]) - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled() }) it('should support edit, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reload') const editButton = fixture.debugElement.queryAll(By.css('button'))[2] @@ -164,12 +164,12 @@ describe('WorkflowsComponent', () => { // fail first editDialog.failed.emit({ error: 'error editing item' }) - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed editDialog.succeeded.emit(workflows[0]) - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled() }) @@ -240,7 +240,7 @@ describe('WorkflowsComponent', () => { it('should support delete, show notification on error / success', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) - const toastErrorSpy = jest.spyOn(toastService, 'showError') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') const deleteSpy = jest.spyOn(workflowService, 'delete') const reloadSpy = jest.spyOn(component, 'reload') @@ -253,7 +253,7 @@ describe('WorkflowsComponent', () => { // fail first deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting'))) editDialog.confirmClicked.emit() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled() // succeed @@ -267,21 +267,21 @@ describe('WorkflowsComponent', () => { const toggleInput = fixture.debugElement.query( By.css('input[type="checkbox"]') ) - const toastErrorSpy = jest.spyOn(toastService, 'showError') - const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + const notificationErrorSpy = jest.spyOn(notificationService, 'showError') + const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo') // fail first patchSpy.mockReturnValueOnce( throwError(() => new Error('Error getting config')) ) toggleInput.nativeElement.click() expect(patchSpy).toHaveBeenCalled() - expect(toastErrorSpy).toHaveBeenCalled() + expect(notificationErrorSpy).toHaveBeenCalled() // succeed second patchSpy.mockReturnValueOnce(of(workflows[0])) toggleInput.nativeElement.click() patchSpy.mockReturnValueOnce(of({ ...workflows[0], enabled: false })) toggleInput.nativeElement.click() expect(patchSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalled() + expect(notificationInfoSpy).toHaveBeenCalled() }) }) diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.ts b/src-ui/src/app/components/manage/workflows/workflows.component.ts index edbca44c8..056e48111 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.ts @@ -5,9 +5,9 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { delay, takeUntil, tap } from 'rxjs' import { Workflow } from 'src/app/data/workflow' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { NotificationService } from 'src/app/services/notification.service' import { PermissionsService } from 'src/app/services/permissions.service' import { WorkflowService } from 'src/app/services/rest/workflow.service' -import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { @@ -40,7 +40,7 @@ export class WorkflowsComponent private workflowService: WorkflowService, public permissionsService: PermissionsService, private modalService: NgbModal, - private toastService: ToastService + private notificationService: NotificationService ) { super() } @@ -90,7 +90,7 @@ export class WorkflowsComponent modal.componentInstance.succeeded .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((newWorkflow) => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Saved workflow "${newWorkflow.name}".` ) this.workflowService.clearCache() @@ -99,7 +99,7 @@ export class WorkflowsComponent modal.componentInstance.failed .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((e) => { - this.toastService.showError($localize`Error saving workflow.`, e) + this.notificationService.showError($localize`Error saving workflow.`, e) }) } @@ -142,14 +142,14 @@ export class WorkflowsComponent this.workflowService.delete(workflow).subscribe({ next: () => { modal.close() - this.toastService.showInfo( + this.notificationService.showInfo( $localize`Deleted workflow "${workflow.name}".` ) this.workflowService.clearCache() this.reload() }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error deleting workflow "${workflow.name}".`, e ) @@ -161,7 +161,7 @@ export class WorkflowsComponent toggleWorkflowEnabled(workflow: Workflow) { this.workflowService.patch(workflow).subscribe({ next: () => { - this.toastService.showInfo( + this.notificationService.showInfo( workflow.enabled ? $localize`Enabled workflow "${workflow.name}"` : $localize`Disabled workflow "${workflow.name}"` @@ -170,7 +170,7 @@ export class WorkflowsComponent this.reload() }, error: (e) => { - this.toastService.showError( + this.notificationService.showError( $localize`Error toggling workflow "${workflow.name}".`, e ) diff --git a/src-ui/src/app/guards/permissions.guard.spec.ts b/src-ui/src/app/guards/permissions.guard.spec.ts index 77fe615e1..76ce781a3 100644 --- a/src-ui/src/app/guards/permissions.guard.spec.ts +++ b/src-ui/src/app/guards/permissions.guard.spec.ts @@ -1,12 +1,12 @@ import { TestBed } from '@angular/core/testing' import { ActivatedRoute, RouterState } from '@angular/router' import { TourService } from 'ngx-ui-tour-ng-bootstrap' +import { NotificationService } from '../services/notification.service' import { PermissionAction, PermissionType, PermissionsService, } from '../services/permissions.service' -import { ToastService } from '../services/toast.service' import { PermissionsGuard } from './permissions.guard' describe('PermissionsGuard', () => { @@ -15,7 +15,7 @@ describe('PermissionsGuard', () => { let route: ActivatedRoute let routerState: RouterState let tourService: TourService - let toastService: ToastService + let notificationService: NotificationService beforeEach(() => { TestBed.configureTestingModule({ @@ -44,13 +44,13 @@ describe('PermissionsGuard', () => { }, }, TourService, - ToastService, + NotificationService, ], }) permissionsService = TestBed.inject(PermissionsService) tourService = TestBed.inject(TourService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) guard = TestBed.inject(PermissionsGuard) route = TestBed.inject(ActivatedRoute) routerState = TestBed.inject(RouterState) @@ -88,11 +88,11 @@ describe('PermissionsGuard', () => { }) jest.spyOn(tourService, 'getStatus').mockImplementation(() => 2) - const toastSpy = jest.spyOn(toastService, 'showError') + const notificationSpy = jest.spyOn(notificationService, 'showError') const canActivate = guard.canActivate(route.snapshot, routerState.snapshot) expect(canActivate).toHaveProperty('root') // returns UrlTree - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() }) }) diff --git a/src-ui/src/app/guards/permissions.guard.ts b/src-ui/src/app/guards/permissions.guard.ts index c820edea2..2b3486887 100644 --- a/src-ui/src/app/guards/permissions.guard.ts +++ b/src-ui/src/app/guards/permissions.guard.ts @@ -6,15 +6,15 @@ import { UrlTree, } from '@angular/router' import { TourService } from 'ngx-ui-tour-ng-bootstrap' +import { NotificationService } from '../services/notification.service' import { PermissionsService } from '../services/permissions.service' -import { ToastService } from '../services/toast.service' @Injectable() export class PermissionsGuard { constructor( private permissionsService: PermissionsService, private router: Router, - private toastService: ToastService, + private notificationService: NotificationService, private tourService: TourService ) {} @@ -32,7 +32,7 @@ export class PermissionsGuard { ) { // Check if tour is running 1 = TourState.ON if (this.tourService.getStatus() !== 1) { - this.toastService.showError( + this.notificationService.showError( $localize`You don't have permissions to do that` ) } diff --git a/src-ui/src/app/services/notification.service.spec.ts b/src-ui/src/app/services/notification.service.spec.ts new file mode 100644 index 000000000..2729dd89e --- /dev/null +++ b/src-ui/src/app/services/notification.service.spec.ts @@ -0,0 +1,109 @@ +import { TestBed } from '@angular/core/testing' +import { NotificationService } from './notification.service' + +describe('NotificationService', () => { + let notificationService: NotificationService + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [NotificationService], + }) + + notificationService = TestBed.inject(NotificationService) + }) + + it('adds notification on show', () => { + const notification = { + title: 'Title', + content: 'content', + delay: 5000, + } + notificationService.show(notification) + + notificationService.getNotifications().subscribe((notifications) => { + expect(notifications).toContainEqual(notification) + }) + }) + + it('adds a unique id to notification on show', () => { + const notification = { + title: 'Title', + content: 'content', + delay: 5000, + } + notificationService.show(notification) + + notificationService.getNotifications().subscribe((notifications) => { + expect(notifications[0].id).toBeDefined() + }) + }) + + it('parses error string to object on show', () => { + const notification = { + title: 'Title', + content: 'content', + delay: 5000, + error: 'Error string', + } + notificationService.show(notification) + + notificationService.getNotifications().subscribe((notifications) => { + expect(notifications[0].error).toEqual('Error string') + }) + }) + + it('creates notifications with defaults on showInfo and showError', () => { + notificationService.showInfo('Info notification') + notificationService.showError('Error notification') + + notificationService.getNotifications().subscribe((notifications) => { + expect(notifications).toContainEqual({ + content: 'Info notification', + delay: 5000, + }) + expect(notifications).toContainEqual({ + content: 'Error notification', + delay: 10000, + }) + }) + }) + + it('removes notification on close', () => { + const notification = { + title: 'Title', + content: 'content', + delay: 5000, + } + notificationService.show(notification) + notificationService.closeNotification(notification) + + notificationService.getNotifications().subscribe((notifications) => { + expect(notifications).toHaveLength(0) + }) + }) + + it('clears all notifications on clear', () => { + notificationService.showInfo('Info notification') + notificationService.showError('Error notification') + notificationService.clearNotifications() + + notificationService.getNotifications().subscribe((notifications) => { + expect(notifications).toHaveLength(0) + }) + }) + + it('suppresses popup notifications if suppressPopupNotifications is true', (finish) => { + notificationService.showNotification.subscribe((notification) => { + expect(notification).not.toBeNull() + }) + notificationService.showInfo('Info notification') + + notificationService.showNotification.subscribe((notification) => { + expect(notification).toBeNull() + finish() + }) + + notificationService.suppressPopupNotifications = true + notificationService.showInfo('Info notification') + }) +}) diff --git a/src-ui/src/app/services/notification.service.ts b/src-ui/src/app/services/notification.service.ts new file mode 100644 index 000000000..63ed69fe2 --- /dev/null +++ b/src-ui/src/app/services/notification.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@angular/core' +import { Subject } from 'rxjs' +import { v4 as uuidv4 } from 'uuid' + +export interface Notification { + id?: string + + content: string + + delay: number + + delayRemaining?: number + + action?: any + + actionName?: string + + classname?: string + + error?: any +} + +@Injectable({ + providedIn: 'root', +}) +export class NotificationService { + constructor() {} + _suppressPopupNotifications: boolean + + set suppressPopupNotifications(value: boolean) { + this._suppressPopupNotifications = value + this.showNotification.next(null) + } + + private notifications: Notification[] = [] + + private notificationsSubject: Subject = new Subject() + + public showNotification: Subject = new Subject() + + show(notification: Notification) { + if (!notification.id) { + notification.id = uuidv4() + } + if (typeof notification.error === 'string') { + try { + notification.error = JSON.parse(notification.error) + } catch (e) {} + } + this.notifications.unshift(notification) + if (!this._suppressPopupNotifications) { + this.showNotification.next(notification) + } + this.notificationsSubject.next(this.notifications) + } + + showError(content: string, error: any = null, delay: number = 10000) { + this.show({ + content: content, + delay: delay, + classname: 'error', + error, + }) + } + + showInfo(content: string, delay: number = 5000) { + this.show({ content: content, delay: delay }) + } + + closeNotification(notification: Notification) { + let index = this.notifications.findIndex((t) => t.id == notification.id) + if (index > -1) { + this.notifications.splice(index, 1) + this.notificationsSubject.next(this.notifications) + } + } + + getNotifications() { + return this.notificationsSubject + } + + clearNotifications() { + this.notifications = [] + this.notificationsSubject.next(this.notifications) + this.showNotification.next(null) + } +} diff --git a/src-ui/src/app/services/settings.service.spec.ts b/src-ui/src/app/services/settings.service.spec.ts index df44013f4..5353a6f1b 100644 --- a/src-ui/src/app/services/settings.service.spec.ts +++ b/src-ui/src/app/services/settings.service.spec.ts @@ -14,10 +14,10 @@ import { CustomFieldDataType } from '../data/custom-field' import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document' import { SavedView } from '../data/saved-view' import { SETTINGS_KEYS, UiSettings } from '../data/ui-settings' +import { NotificationService } from './notification.service' import { PermissionsService } from './permissions.service' import { CustomFieldsService } from './rest/custom-fields.service' import { SettingsService } from './settings.service' -import { ToastService } from './toast.service' const customFields = [ { @@ -41,7 +41,7 @@ describe('SettingsService', () => { let customFieldsService: CustomFieldsService let permissionService: PermissionsService let subscription: Subscription - let toastService: ToastService + let notificationService: NotificationService const ui_settings: UiSettings = { user: { @@ -105,7 +105,7 @@ describe('SettingsService', () => { customFieldsService = TestBed.inject(CustomFieldsService) permissionService = TestBed.inject(PermissionsService) settingsService = TestBed.inject(SettingsService) - toastService = TestBed.inject(ToastService) + notificationService = TestBed.inject(NotificationService) // Normally done in app initializer settingsService.initializeSettings().subscribe() }) @@ -122,8 +122,8 @@ describe('SettingsService', () => { expect(req.request.method).toEqual('GET') }) - it('should catch error and show toast on retrieve ui_settings error', fakeAsync(() => { - const toastSpy = jest.spyOn(toastService, 'showError') + it('should catch error and show notification on retrieve ui_settings error', fakeAsync(() => { + const notificationSpy = jest.spyOn(notificationService, 'showError') httpTestingController .expectOne(`${environment.apiBaseUrl}ui_settings/`) .flush( @@ -131,7 +131,7 @@ describe('SettingsService', () => { { status: 403, statusText: 'Forbidden' } ) tick(500) - expect(toastSpy).toHaveBeenCalled() + expect(notificationSpy).toHaveBeenCalled() })) it('calls ui_settings api endpoint with POST on store', () => { diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 80e3b3474..5ad7a0d4a 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -26,13 +26,13 @@ import { UiSettings, } from '../data/ui-settings' import { User } from '../data/user' +import { NotificationService } from './notification.service' import { PermissionAction, PermissionsService, PermissionType, } from './permissions.service' import { CustomFieldsService } from './rest/custom-fields.service' -import { ToastService } from './toast.service' export interface LanguageOption { code: string @@ -294,7 +294,7 @@ export class SettingsService { private meta: Meta, @Inject(LOCALE_ID) private localeId: string, protected http: HttpClient, - private toastService: ToastService, + private notificationService: NotificationService, private permissionsService: PermissionsService, private customFieldsService: CustomFieldsService ) { @@ -307,7 +307,7 @@ export class SettingsService { first(), catchError((error) => { setTimeout(() => { - this.toastService.showError('Error loading settings', error) + this.notificationService.showError('Error loading settings', error) }, 500) return of({ settings: { @@ -601,7 +601,7 @@ export class SettingsService { this.cookieService.get(this.getLanguageCookieName()) ) } catch (error) { - this.toastService.showError(errorMessage) + this.notificationService.showError(errorMessage) console.log(error) } @@ -610,10 +610,10 @@ export class SettingsService { .subscribe({ next: () => { this.updateAppearanceSettings() - this.toastService.showInfo(successMessage) + this.notificationService.showInfo(successMessage) }, error: (e) => { - this.toastService.showError(errorMessage) + this.notificationService.showError(errorMessage) console.log(e) }, }) @@ -633,7 +633,7 @@ export class SettingsService { .pipe(first()) .subscribe({ error: (e) => { - this.toastService.showError( + this.notificationService.showError( 'Error migrating update checking setting' ) console.log(e) @@ -663,7 +663,7 @@ export class SettingsService { this.storeSettings() .pipe(first()) .subscribe(() => { - this.toastService.showInfo( + this.notificationService.showInfo( $localize`You can restart the tour from the settings page.` ) }) diff --git a/src-ui/src/app/services/toast.service.spec.ts b/src-ui/src/app/services/toast.service.spec.ts deleted file mode 100644 index ce50b165e..000000000 --- a/src-ui/src/app/services/toast.service.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { TestBed } from '@angular/core/testing' -import { ToastService } from './toast.service' - -describe('ToastService', () => { - let toastService: ToastService - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ToastService], - }) - - toastService = TestBed.inject(ToastService) - }) - - it('adds toast on show', () => { - const toast = { - title: 'Title', - content: 'content', - delay: 5000, - } - toastService.show(toast) - - toastService.getToasts().subscribe((toasts) => { - expect(toasts).toContainEqual(toast) - }) - }) - - it('adds a unique id to toast on show', () => { - const toast = { - title: 'Title', - content: 'content', - delay: 5000, - } - toastService.show(toast) - - toastService.getToasts().subscribe((toasts) => { - expect(toasts[0].id).toBeDefined() - }) - }) - - it('parses error string to object on show', () => { - const toast = { - title: 'Title', - content: 'content', - delay: 5000, - error: 'Error string', - } - toastService.show(toast) - - toastService.getToasts().subscribe((toasts) => { - expect(toasts[0].error).toEqual('Error string') - }) - }) - - it('creates toasts with defaults on showInfo and showError', () => { - toastService.showInfo('Info toast') - toastService.showError('Error toast') - - toastService.getToasts().subscribe((toasts) => { - expect(toasts).toContainEqual({ - content: 'Info toast', - delay: 5000, - }) - expect(toasts).toContainEqual({ - content: 'Error toast', - delay: 10000, - }) - }) - }) - - it('removes toast on close', () => { - const toast = { - title: 'Title', - content: 'content', - delay: 5000, - } - toastService.show(toast) - toastService.closeToast(toast) - - toastService.getToasts().subscribe((toasts) => { - expect(toasts).toHaveLength(0) - }) - }) - - it('clears all toasts on clearToasts', () => { - toastService.showInfo('Info toast') - toastService.showError('Error toast') - toastService.clearToasts() - - toastService.getToasts().subscribe((toasts) => { - expect(toasts).toHaveLength(0) - }) - }) - - it('suppresses popup toasts if suppressPopupToasts is true', (finish) => { - toastService.showToast.subscribe((toast) => { - expect(toast).not.toBeNull() - }) - toastService.showInfo('Info toast') - - toastService.showToast.subscribe((toast) => { - expect(toast).toBeNull() - finish() - }) - - toastService.suppressPopupToasts = true - toastService.showInfo('Info toast') - }) -}) diff --git a/src-ui/src/app/services/toast.service.ts b/src-ui/src/app/services/toast.service.ts deleted file mode 100644 index b917bf94b..000000000 --- a/src-ui/src/app/services/toast.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Injectable } from '@angular/core' -import { Subject } from 'rxjs' -import { v4 as uuidv4 } from 'uuid' - -export interface Toast { - id?: string - - content: string - - delay: number - - delayRemaining?: number - - action?: any - - actionName?: string - - classname?: string - - error?: any -} - -@Injectable({ - providedIn: 'root', -}) -export class ToastService { - constructor() {} - _suppressPopupToasts: boolean - - set suppressPopupToasts(value: boolean) { - this._suppressPopupToasts = value - this.showToast.next(null) - } - - private toasts: Toast[] = [] - - private toastsSubject: Subject = new Subject() - - public showToast: Subject = new Subject() - - show(toast: Toast) { - if (!toast.id) { - toast.id = uuidv4() - } - if (typeof toast.error === 'string') { - try { - toast.error = JSON.parse(toast.error) - } catch (e) {} - } - this.toasts.unshift(toast) - if (!this._suppressPopupToasts) { - this.showToast.next(toast) - } - this.toastsSubject.next(this.toasts) - } - - showError(content: string, error: any = null, delay: number = 10000) { - this.show({ - content: content, - delay: delay, - classname: 'error', - error, - }) - } - - showInfo(content: string, delay: number = 5000) { - this.show({ content: content, delay: delay }) - } - - closeToast(toast: Toast) { - let index = this.toasts.findIndex((t) => t.id == toast.id) - if (index > -1) { - this.toasts.splice(index, 1) - this.toastsSubject.next(this.toasts) - } - } - - getToasts() { - return this.toastsSubject - } - - clearToasts() { - this.toasts = [] - this.toastsSubject.next(this.toasts) - this.showToast.next(null) - } -} diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index a3f385ed5..103b1b8ba 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -571,7 +571,7 @@ table.table { } .toast { - --bs-toast-max-width: var(--pngx-toast-max-width); + --bs-toast-max-width: var(--pngx-notification-max-width); } .alert-primary { diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index cc60d3851..94d78d3c4 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -24,11 +24,11 @@ --pngx-bg-alt2: var(--bs-gray-200); // #e9ecef --pngx-bg-disabled: #f7f7f7; --pngx-focus-alpha: 0.3; - --pngx-toast-max-width: 360px; + --pngx-notification-max-width: 360px; --bs-info: var(--pngx-bg-alt2); --bs-info-rgb: 233, 236, 239; @media screen and (min-width: 1024px) { - --pngx-toast-max-width: 450px; + --pngx-notification-max-width: 450px; } }
No notifications
{{toast.content}}
{{notification.content}}
{{toast.actionName}}
{{notification.actionName}}