Revert "Very annoying refactor"

This reverts commit f28accb28f15ff4407aa929ecea0468b8803949d.
This commit is contained in:
shamoon 2025-03-04 08:59:32 -08:00
parent f28accb28f
commit f9c1051ef7
No known key found for this signature in database
81 changed files with 1151 additions and 1315 deletions
src-ui
messages.xlf
src
app
app.component.htmlapp.component.spec.tsapp.component.ts
components
admin
app-frame
common
dashboard
document-detail
document-list
document-notes
file-drop
manage
guards
services
styles.scsstheme.scss

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
<pngx-notification-list></pngx-notification-list>
<pngx-toasts></pngx-toasts>
<pngx-file-drop>
<ng-container content>

@ -14,17 +14,14 @@ 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 { NotificationListComponent } from './components/common/notification-list/notification-list.component'
import { ToastsComponent } from './components/common/toasts/toasts.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,
@ -36,7 +33,7 @@ describe('AppComponent', () => {
let tourService: TourService
let websocketStatusService: WebsocketStatusService
let permissionsService: PermissionsService
let notificationService: NotificationService
let toastService: ToastService
let router: Router
let settingsService: SettingsService
let hotKeyService: HotKeyService
@ -49,7 +46,7 @@ describe('AppComponent', () => {
NgxFileDropModule,
NgbModalModule,
AppComponent,
NotificationListComponent,
ToastsComponent,
FileDropComponent,
NgxBootstrapIconsModule.pick(allIcons),
],
@ -65,7 +62,7 @@ describe('AppComponent', () => {
websocketStatusService = TestBed.inject(WebsocketStatusService)
permissionsService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
router = TestBed.inject(Router)
hotKeyService = TestBed.inject(HotKeyService)
fixture = TestBed.createComponent(AppComponent)
@ -85,14 +82,12 @@ describe('AppComponent', () => {
expect(document.body.classList).not.toContain('tour-active')
}))
it('should display notification on document consumed with link if user has access', () => {
it('should display toast on document consumed with link if user has access', () => {
const navigateSpy = jest.spyOn(router, 'navigate')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
let notification: Notification
notificationService
.getNotifications()
.subscribe((notifications) => (notification = notifications[0]))
const notificationSpy = jest.spyOn(notificationService, 'show')
let toast: Toast
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
@ -101,65 +96,63 @@ describe('AppComponent', () => {
const status = new FileStatus()
status.documentId = 1
fileStatusSubject.next(status)
expect(notificationSpy).toHaveBeenCalled()
expect(notification.action).not.toBeUndefined()
notification.action()
expect(toastSpy).toHaveBeenCalled()
expect(toast.action).not.toBeUndefined()
toast.action()
expect(navigateSpy).toHaveBeenCalledWith(['documents', status.documentId])
})
it('should display notification on document consumed without link if user does not have access', () => {
it('should display toast on document consumed without link if user does not have access', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
let notification: Notification
notificationService
.getNotifications()
.subscribe((notifications) => (notification = notifications[0]))
const notificationSpy = jest.spyOn(notificationService, 'show')
let toast: Toast
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(notificationSpy).toHaveBeenCalled()
expect(notification.action).toBeUndefined()
expect(toastSpy).toHaveBeenCalled()
expect(toast.action).toBeUndefined()
})
it('should display notification on document added', () => {
it('should display toast on document added', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const notificationSpy = jest.spyOn(notificationService, 'show')
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentDetected')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'show')
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentDetected')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(notificationSpy).not.toHaveBeenCalled()
expect(toastSpy).not.toHaveBeenCalled()
})
it('should display notification on document failed', () => {
it('should display toast on document failed', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentConsumptionFailed')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
})
it('should support hotkeys', () => {

@ -2,12 +2,11 @@ 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 { NotificationListComponent } from './components/common/notification-list/notification-list.component'
import { ToastsComponent } from './components/common/toasts/toasts.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,
@ -15,6 +14,7 @@ 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,
NotificationListComponent,
ToastsComponent,
TourNgBootstrapModule,
RouterOutlet,
],
@ -36,7 +36,7 @@ export class AppComponent implements OnInit, OnDestroy {
constructor(
private settings: SettingsService,
private websocketStatusService: WebsocketStatusService,
private notificationService: NotificationService,
private toastService: ToastService,
private router: Router,
private tasksService: TasksService,
public tourService: TourService,
@ -91,7 +91,7 @@ export class AppComponent implements OnInit, OnDestroy {
PermissionType.Document
)
) {
this.notificationService.show({
this.toastService.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.notificationService.show({
this.toastService.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.notificationService.showError(
this.toastService.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.notificationService.show({
this.toastService.show({
content: $localize`Document ${status.filename} is being processed by Paperless-ngx.`,
delay: 5000,
})

@ -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<ConfigComponent>
let configService: ConfigService
let notificationService: NotificationService
let toastService: ToastService
let settingService: SettingsService
beforeEach(async () => {
@ -51,7 +51,7 @@ describe('ConfigComponent', () => {
}).compileComponents()
configService = TestBed.inject(ConfigService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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(notificationService, 'showError')
const errorSpy = jest.spyOn(toastService, '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(notificationService, 'showError')
const errorSpy = jest.spyOn(toastService, '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(notificationService, 'showError')
const errorSpy = jest.spyOn(toastService, 'showError')
uploadSpy.mockReturnValueOnce(
throwError(() => new Error('Error uploading file'))
)

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private settingsService: SettingsService
) {
super()
@ -100,10 +100,7 @@ export class ConfigComponent
},
error: (e) => {
this.loading = false
this.notificationService.showError(
$localize`Error retrieving config`,
e
)
this.toastService.showError($localize`Error retrieving config`, e)
},
})
@ -173,11 +170,11 @@ export class ConfigComponent
this.initialize(config)
this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.notificationService.showInfo($localize`Configuration updated`)
this.toastService.showInfo($localize`Configuration updated`)
},
error: (e) => {
this.loading = false
this.notificationService.showError(
this.toastService.showError(
$localize`An error occurred updating configuration`,
e
)
@ -200,13 +197,11 @@ export class ConfigComponent
this.initialize(config)
this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.notificationService.showInfo(
$localize`File successfully updated`
)
this.toastService.showInfo($localize`File successfully updated`)
},
error: (e) => {
this.loading = false
this.notificationService.showError(
this.toastService.showError(
$localize`An error occurred uploading file`,
e
)

@ -29,15 +29,12 @@ 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'
@ -69,7 +66,7 @@ describe('SettingsComponent', () => {
let settingsService: SettingsService
let activatedRoute: ActivatedRoute
let viewportScroller: ViewportScroller
let notificationService: NotificationService
let toastService: ToastService
let userService: UserService
let permissionsService: PermissionsService
let groupService: GroupService
@ -118,7 +115,7 @@ describe('SettingsComponent', () => {
router = TestBed.inject(Router)
activatedRoute = TestBed.inject(ActivatedRoute)
viewportScroller = TestBed.inject(ViewportScroller)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = users[0]
userService = TestBed.inject(UserService)
@ -197,8 +194,8 @@ describe('SettingsComponent', () => {
it('should support save local settings updating appearance settings and calling API, show error', () => {
completeSetup()
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'show')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
const appearanceSettingsSpy = jest.spyOn(
settingsService,
@ -212,7 +209,7 @@ describe('SettingsComponent', () => {
)
component.saveSettings()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(29)
@ -220,14 +217,14 @@ describe('SettingsComponent', () => {
// succeed
storeSpy.mockReturnValueOnce(of(true))
component.saveSettings()
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).toHaveBeenCalled()
})
it('should offer reload if settings changes require', () => {
completeSetup()
let toast: Notification
notificationService.getNotifications().subscribe((t) => (toast = t[0]))
let toast: Toast
toastService.getToasts().subscribe((t) => (toast = t[0]))
component.initialize(true) // reset
component.store.getValue()['displayLanguage'] = 'en-US'
component.store.getValue()['updateCheckingEnabled'] = false
@ -261,7 +258,7 @@ describe('SettingsComponent', () => {
})
it('should show errors on load if load users failure', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(userService, 'listAll')
.mockImplementation(() =>
@ -269,11 +266,11 @@ describe('SettingsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
})
it('should show errors on load if load groups failure', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(groupService, 'listAll')
.mockImplementation(() =>
@ -281,7 +278,7 @@ describe('SettingsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
})
it('should load system status on initialize, show errors if needed', () => {

@ -43,10 +43,6 @@ 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,
@ -59,6 +55,7 @@ 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'
@ -184,7 +181,7 @@ export class SettingsComponent
constructor(
private documentListViewService: DocumentListViewService,
private notificationService: NotificationService,
private toastService: ToastService,
private settings: SettingsService,
@Inject(LOCALE_ID) public currentLocale: string,
private viewportScroller: ViewportScroller,
@ -220,10 +217,7 @@ export class SettingsComponent
this.users = r.results
},
error: (e) => {
this.notificationService.showError(
$localize`Error retrieving users`,
e
)
this.toastService.showError($localize`Error retrieving users`, e)
},
})
}
@ -242,10 +236,7 @@ export class SettingsComponent
this.groups = r.results
},
error: (e) => {
this.notificationService.showError(
$localize`Error retrieving groups`,
e
)
this.toastService.showError($localize`Error retrieving groups`, e)
},
})
}
@ -540,7 +531,7 @@ export class SettingsComponent
this.store.next(this.settingsForm.value)
this.settings.updateAppearanceSettings()
this.settings.initializeDisplayFields()
let savedToast: Notification = {
let savedToast: Toast = {
content: $localize`Settings were saved successfully.`,
delay: 5000,
}
@ -552,10 +543,10 @@ export class SettingsComponent
}
}
this.notificationService.show(savedToast)
this.toastService.show(savedToast)
},
error: (error) => {
this.notificationService.showError(
this.toastService.showError(
$localize`An error occurred while saving settings.`,
error
)

@ -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 { NotificationService } from 'src/app/services/notification.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'
@ -38,7 +38,7 @@ describe('TrashComponent', () => {
let fixture: ComponentFixture<TrashComponent>
let trashService: TrashService
let modalService: NgbModal
let notificationService: NotificationService
let toastService: ToastService
let router: Router
beforeEach(async () => {
@ -60,7 +60,7 @@ describe('TrashComponent', () => {
fixture = TestBed.createComponent(TrashComponent)
trashService = TestBed.inject(TrashService)
modalService = TestBed.inject(NgbModal)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
router = TestBed.inject(Router)
component = fixture.componentInstance
fixture.detectChanges()
@ -88,13 +88,13 @@ describe('TrashComponent', () => {
modalService.activeInstances.subscribe((instances) => {
modal = instances[0]
})
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
trashSpy.mockReturnValue(throwError(() => 'Error'))
component.delete(documentsInTrash[0])
modal.componentInstance.confirmClicked.next()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
trashSpy.mockReturnValue(throwError(() => 'Error'))
component.emptyTrash()
modal.componentInstance.confirmClicked.next()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
restoreSpy.mockReturnValue(throwError(() => 'Error'))
component.restore(documentsInTrash[0])
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
restoreSpy.mockReturnValue(throwError(() => 'Error'))
component.restoreAll()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).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')
notificationService.getNotifications().subscribe((allToasts) => {
toastService.getToasts().subscribe((allToasts) => {
toasts = [...allToasts]
})
jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK'))

@ -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 notificationService: NotificationService,
private toastService: ToastService,
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.notificationService.showInfo(
this.toastService.showInfo(
$localize`Document "${document.title}" deleted`
)
modal.close()
this.reload()
},
error: (err) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error deleting document "${document.title}"`,
err
)
@ -121,13 +121,13 @@ export class TrashComponent
.emptyTrash(documents ? Array.from(documents) : null)
.subscribe({
next: () => {
this.notificationService.showInfo($localize`Document(s) deleted`)
this.toastService.showInfo($localize`Document(s) deleted`)
this.allToggled = false
modal.close()
this.reload()
},
error: (err) => {
this.notificationService.showError(
this.toastService.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.notificationService.show({
this.toastService.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.notificationService.showError(
this.toastService.showError(
$localize`Error restoring document "${document.title}"`,
err
)
@ -164,12 +164,12 @@ export class TrashComponent
.restoreDocuments(documents ? Array.from(documents) : null)
.subscribe({
next: () => {
this.notificationService.showInfo($localize`Document(s) restored`)
this.toastService.showInfo($localize`Document(s) restored`)
this.allToggled = false
this.reload()
},
error: (err) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error restoring document(s)`,
err
)

@ -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<UsersAndGroupsComponent>
let settingsService: SettingsService
let modalService: NgbModal
let notificationService: NotificationService
let toastService: ToastService
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)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
settingsService.currentUser = users[1] // simulate logged in as different user
editDialog.succeeded.emit(users[0])
expect(notificationInfoSpy).toHaveBeenCalledWith(
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(userService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting user'))
)
deleteDialog.confirm()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalledWith('Deleted user "user1"')
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(groups[0])
expect(notificationInfoSpy).toHaveBeenCalledWith(
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(groupService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting group'))
)
deleteDialog.confirm()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalledWith('Deleted group "group1"')
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(userService, 'listAll')
.mockImplementation(() =>
@ -210,11 +210,11 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
})
it('should show errors on load if load groups failure', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(groupService, 'listAll')
.mockImplementation(() =>
@ -222,6 +222,6 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
})
})

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private settings: SettingsService
@ -56,10 +56,7 @@ export class UsersAndGroupsComponent
this.users = r.results
},
error: (e) => {
this.notificationService.showError(
$localize`Error retrieving users`,
e
)
this.toastService.showError($localize`Error retrieving users`, e)
},
})
@ -71,10 +68,7 @@ export class UsersAndGroupsComponent
this.groups = r.results
},
error: (e) => {
this.notificationService.showError(
$localize`Error retrieving groups`,
e
)
this.toastService.showError($localize`Error retrieving groups`, e)
},
})
}
@ -99,14 +93,14 @@ export class UsersAndGroupsComponent
newUser.id === this.settings.currentUser.id &&
(modal.componentInstance as UserEditDialogComponent).passwordIsSet
) {
this.notificationService.showInfo(
this.toastService.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.notificationService.showInfo(
this.toastService.showInfo(
$localize`Saved user "${newUser.username}".`
)
this.usersService.listAll().subscribe((r) => {
@ -117,7 +111,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.notificationService.showError($localize`Error saving user.`, e)
this.toastService.showError($localize`Error saving user.`, e)
})
}
@ -135,15 +129,13 @@ export class UsersAndGroupsComponent
this.usersService.delete(user).subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
$localize`Deleted user "${user.username}"`
)
this.toastService.showInfo($localize`Deleted user "${user.username}"`)
this.usersService.listAll().subscribe((r) => {
this.users = r.results
})
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error deleting user "${user.username}".`,
e
)
@ -164,9 +156,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newGroup) => {
this.notificationService.showInfo(
$localize`Saved group "${newGroup.name}".`
)
this.toastService.showInfo($localize`Saved group "${newGroup.name}".`)
this.groupsService.listAll().subscribe((r) => {
this.groups = r.results
})
@ -174,7 +164,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.notificationService.showError($localize`Error saving group.`, e)
this.toastService.showError($localize`Error saving group.`, e)
})
}
@ -192,15 +182,13 @@ export class UsersAndGroupsComponent
this.groupsService.delete(group).subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
$localize`Deleted group "${group.name}"`
)
this.toastService.showInfo($localize`Deleted group "${group.name}"`)
this.groupsService.listAll().subscribe((r) => {
this.groups = r.results
})
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error deleting group "${group.name}".`,
e
)

@ -30,7 +30,7 @@
</div>
</div>
<ul ngbNav class="order-sm-3">
<pngx-notifications-dropdown></pngx-notifications-dropdown>
<pngx-toasts-dropdown></pngx-toasts-dropdown>
<li ngbDropdown class="nav-item dropdown">
<button class="btn ps-1 border-0" id="userDropdown" ngbDropdownToggle>
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>

@ -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 notificationService: NotificationService
let toastService: ToastService
let messagesService: DjangoMessagesService
let openDocumentsService: OpenDocumentsService
let router: Router
@ -126,7 +126,7 @@ describe('AppFrameComponent', () => {
PermissionsService,
RemoteVersionService,
IfPermissionsDirective,
NotificationService,
ToastService,
DjangoMessagesService,
OpenDocumentsService,
SearchService,
@ -157,7 +157,7 @@ describe('AppFrameComponent', () => {
const savedViewService = TestBed.inject(SavedViewService)
permissionsService = TestBed.inject(PermissionsService)
remoteVersionService = TestBed.inject(RemoteVersionService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
component.setUpdateChecking(true)
httpTestingController
@ -225,7 +225,7 @@ describe('AppFrameComponent', () => {
status: 500,
statusText: 'error',
})
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
component.toggleSlimSidebar()
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
@ -253,7 +253,7 @@ describe('AppFrameComponent', () => {
status: 500,
statusText: 'error',
})
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, '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(notificationSpy).toHaveBeenCalled()
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
SavedView[]
>)
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
})
it('should support edit profile', () => {
@ -345,9 +345,9 @@ describe('AppFrameComponent', () => {
})
})
it('should show notifications for django messages', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
it('should show toasts for django messages', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalledTimes(2)
expect(notificationInfoSpy).toHaveBeenCalledTimes(3)
expect(toastErrorSpy).toHaveBeenCalledTimes(2)
expect(toastInfoSpy).toHaveBeenCalledTimes(3)
})
})

@ -29,7 +29,6 @@ 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,
@ -43,12 +42,13 @@ 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 { NotificationsDropdownComponent } from './notifications-dropdown/notifications-dropdown.component'
import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component'
@Component({
selector: 'pngx-app-frame',
@ -58,7 +58,7 @@ import { NotificationsDropdownComponent } from './notifications-dropdown/notific
GlobalSearchComponent,
DocumentTitlePipe,
IfPermissionsDirective,
NotificationsDropdownComponent,
ToastsDropdownComponent,
RouterModule,
NgClass,
NgbDropdownModule,
@ -89,7 +89,7 @@ export class AppFrameComponent
private remoteVersionService: RemoteVersionService,
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly notificationService: NotificationService,
private readonly toastService: ToastService,
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.notificationService.showError(message.message)
this.toastService.showError(message.message)
break
case DjangoMessageLevel.SUCCESS:
case DjangoMessageLevel.INFO:
case DjangoMessageLevel.DEBUG:
this.notificationService.showInfo(message.message)
this.toastService.showInfo(message.message)
break
}
})
@ -157,7 +157,7 @@ export class AppFrameComponent
.pipe(first())
.subscribe({
error: (error) => {
this.notificationService.showError(
this.toastService.showError(
$localize`An error occurred while saving settings.`
)
console.warn(error)
@ -242,13 +242,10 @@ export class AppFrameComponent
this.settingsService.updateSidebarViewsSort(sidebarViews).subscribe({
next: () => {
this.notificationService.showInfo($localize`Sidebar views updated`)
this.toastService.showInfo($localize`Sidebar views updated`)
},
error: (e) => {
this.notificationService.showError(
$localize`Error updating sidebar views`,
e
)
this.toastService.showError($localize`Error updating sidebar views`, e)
},
})
}
@ -268,7 +265,7 @@ export class AppFrameComponent
.pipe(first())
.subscribe({
error: (error) => {
this.notificationService.showError(
this.toastService.showError(
$localize`An error occurred while saving update checking settings.`
)
console.warn(error)

@ -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 notificationService: NotificationService
let toastService: ToastService
let settingsService: SettingsService
beforeEach(async () => {
@ -157,7 +157,7 @@ describe('GlobalSearchComponent', () => {
modalService = TestBed.inject(NgbModal)
documentService = TestBed.inject(DocumentService)
documentListViewService = TestBed.inject(DocumentListViewService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
settingsService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(GlobalSearchComponent)
@ -397,16 +397,16 @@ describe('GlobalSearchComponent', () => {
})
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(true)
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
})
it('should support secondary action', () => {
@ -448,16 +448,16 @@ describe('GlobalSearchComponent', () => {
})
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(true)
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
})
it('should support reset', () => {

@ -31,7 +31,6 @@ 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,
@ -42,6 +41,7 @@ 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 notificationService: NotificationService,
private toastService: ToastService,
private hotkeyService: HotKeyService,
private settingsService: SettingsService
) {
@ -206,15 +206,10 @@ export class GlobalSearchComponent implements OnInit {
modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
modalRef.componentInstance.object = object
modalRef.componentInstance.succeeded.subscribe(() => {
this.notificationService.showInfo(
$localize`Successfully updated object.`
)
this.toastService.showInfo($localize`Successfully updated object.`)
})
modalRef.componentInstance.failed.subscribe((e) => {
this.notificationService.showError(
$localize`Error occurred saving object.`,
e
)
this.toastService.showError($localize`Error occurred saving object.`, e)
})
}
}
@ -249,15 +244,10 @@ export class GlobalSearchComponent implements OnInit {
modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
modalRef.componentInstance.object = object
modalRef.componentInstance.succeeded.subscribe(() => {
this.notificationService.showInfo(
$localize`Successfully updated object.`
)
this.toastService.showInfo($localize`Successfully updated object.`)
})
modalRef.componentInstance.failed.subscribe((e) => {
this.notificationService.showError(
$localize`Error occurred saving object.`,
e
)
this.toastService.showError($localize`Error occurred saving object.`, e)
})
}
}

@ -1,47 +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 {
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
}
}

@ -1,7 +1,7 @@
<li ngbDropdown class="nav-item" (openChange)="onOpenChange($event)">
@if (notifications.length) {
<span class="badge rounded-pill z-3 pe-none bg-secondary me-2 position-absolute top-0 left-0">{{ notifications.length }}</span>
@if (toasts.length) {
<span class="badge rounded-pill z-3 pe-none bg-secondary me-2 position-absolute top-0 left-0">{{ toasts.length }}</span>
}
<button class="btn border-0" id="notificationsDropdown" ngbDropdownToggle>
<i-bs width="1.3em" height="1.3em" name="bell"></i-bs>
@ -11,17 +11,17 @@
<h6 i18n>Notifications</h6>
<div class="btn-group ms-auto">
<button class="btn btn-sm btn-outline-secondary mb-2 ms-auto"
(click)="notificationService.clearNotifications()"
[disabled]="notifications.length === 0"
(click)="toastService.clearToasts()"
[disabled]="toasts.length === 0"
i18n>Clear All</button>
</div>
</div>
@if (notifications.length === 0) {
@if (toasts.length === 0) {
<p class="text-center mb-0 small text-muted"><em i18n>No notifications</em></p>
}
<div class="scroll-list">
@for (notification of notifications; track notification.id) {
<pngx-notification [autohide]="false" [notification]="notification" (hidden)="onHidden(notification)" (close)="notificationService.closeNotification(notification)"></pngx-notification>
@for (toast of toasts; track toast.id) {
<pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (close)="toastService.closeToast(toast)"></pngx-toast>
}
</div>
</div>

@ -1,5 +1,5 @@
.dropdown-menu {
width: var(--pngx-notification-max-width);
width: var(--pngx-toast-max-width);
}
.dropdown-menu .scroll-list {

@ -9,13 +9,10 @@ import {
} 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 { NotificationsDropdownComponent } from './notifications-dropdown.component'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { ToastsDropdownComponent } from './toasts-dropdown.component'
const notifications = [
const toasts = [
{
id: 'abc-123',
content: 'foo bar',
@ -41,16 +38,16 @@ const notifications = [
},
]
describe('NotificationsDropdownComponent', () => {
let component: NotificationsDropdownComponent
let fixture: ComponentFixture<NotificationsDropdownComponent>
let notificationService: NotificationService
let notificationsSubject: Subject<Notification[]> = new Subject()
describe('ToastsDropdownComponent', () => {
let component: ToastsDropdownComponent
let fixture: ComponentFixture<ToastsDropdownComponent>
let toastService: ToastService
let toastsSubject: Subject<Toast[]> = new Subject()
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
NotificationsDropdownComponent,
ToastsDropdownComponent,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [
@ -59,26 +56,24 @@ describe('NotificationsDropdownComponent', () => {
],
}).compileComponents()
fixture = TestBed.createComponent(NotificationsDropdownComponent)
notificationService = TestBed.inject(NotificationService)
jest
.spyOn(notificationService, 'getNotifications')
.mockReturnValue(notificationsSubject)
fixture = TestBed.createComponent(ToastsDropdownComponent)
toastService = TestBed.inject(ToastService)
jest.spyOn(toastService, 'getToasts').mockReturnValue(toastsSubject)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should call getNotifications and return notifications', fakeAsync(() => {
const spy = jest.spyOn(notificationService, 'getNotifications')
it('should call getToasts and return toasts', fakeAsync(() => {
const spy = jest.spyOn(toastService, 'getToasts')
component.ngOnInit()
notificationsSubject.next(notifications)
toastsSubject.next(toasts)
fixture.detectChanges()
expect(spy).toHaveBeenCalled()
expect(component.notifications).toContainEqual({
expect(component.toasts).toContainEqual({
id: 'abc-123',
content: 'foo bar',
delay: 5000,
@ -89,9 +84,9 @@ describe('NotificationsDropdownComponent', () => {
discardPeriodicTasks()
}))
it('should show a notification', fakeAsync(() => {
it('should show a toast', fakeAsync(() => {
component.ngOnInit()
notificationsSubject.next(notifications)
toastsSubject.next(toasts)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('foo bar')
@ -101,16 +96,12 @@ describe('NotificationsDropdownComponent', () => {
discardPeriodicTasks()
}))
it('should toggle suppressPopupNotifications', fakeAsync((finish) => {
it('should toggle suppressPopupToasts', fakeAsync((finish) => {
component.ngOnInit()
fixture.detectChanges()
notificationsSubject.next(notifications)
toastsSubject.next(toasts)
const spy = jest.spyOn(
notificationService,
'suppressPopupNotifications',
'set'
)
const spy = jest.spyOn(toastService, 'suppressPopupToasts', 'set')
component.onOpenChange(true)
expect(spy).toHaveBeenCalledWith(true)

@ -0,0 +1,42 @@
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
}
}

@ -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<CustomFieldsDropdownComponent>
let customFieldService: CustomFieldsService
let notificationService: NotificationService
let toastService: ToastService
let modalService: NgbModal
let settingsService: SettingsService
@ -64,7 +64,7 @@ describe('CustomFieldsDropdownComponent', () => {
],
})
customFieldService = TestBed.inject(CustomFieldsService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(getFieldsSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
tick(100)
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
expect(getFieldsSpy).toHaveBeenCalled()
expect(addFieldSpy).toHaveBeenCalled()
}))

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private permissionsService: PermissionsService
) {
super()
@ -123,9 +123,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newField) => {
this.notificationService.showInfo(
$localize`Saved field "${newField.name}".`
)
this.toastService.showInfo($localize`Saved field "${newField.name}".`)
this.customFieldsService.clearCache()
this.getFields()
this.created.emit(newField)
@ -134,7 +132,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.notificationService.showError($localize`Error saving field.`, e)
this.toastService.showError($localize`Error saving field.`, e)
})
}

@ -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 notificationService: NotificationService
let toastService: ToastService
let fixture: ComponentFixture<UserEditDialogComponent>
beforeEach(async () => {
@ -75,7 +75,7 @@ describe('UserEditDialogComponent', () => {
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
permissionsService = TestBed.inject(PermissionsService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
component = fixture.componentInstance
fixture.detectChanges()
@ -133,22 +133,22 @@ describe('UserEditDialogComponent', () => {
component['service'] as UserService,
'deactivateTotp'
)
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
deactivateSpy.mockReturnValueOnce(throwError(() => new Error('error')))
component.deactivateTotp()
expect(deactivateSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
deactivateSpy.mockReturnValueOnce(of(false))
component.deactivateTotp()
expect(deactivateSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
deactivateSpy.mockReturnValueOnce(of(true))
component.deactivateTotp()
expect(deactivateSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
})
it('should check superuser status of current user', () => {

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private permissionsService: PermissionsService
) {
super(service, activeModal, service, settingsService)
@ -128,20 +128,15 @@ export class UserEditDialogComponent
next: (result) => {
this.totpLoading = false
if (result) {
this.notificationService.showInfo($localize`Totp deactivated`)
this.toastService.showInfo($localize`Totp deactivated`)
this.object.is_mfa_enabled = false
} else {
this.notificationService.showError(
$localize`Totp deactivation failed`
)
this.toastService.showError($localize`Totp deactivation failed`)
}
},
error: (e) => {
this.totpLoading = false
this.notificationService.showError(
$localize`Totp deactivation failed`,
e
)
this.toastService.showError($localize`Totp deactivation failed`, e)
},
})
}

@ -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<EmailDocumentDialogComponent>
let documentService: DocumentService
let permissionsService: PermissionsService
let notificationService: NotificationService
let toastService: ToastService
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -34,7 +34,7 @@ describe('EmailDocumentDialogComponent', () => {
fixture = TestBed.createComponent(EmailDocumentDialogComponent)
documentService = TestBed.inject(DocumentService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
component = fixture.componentInstance
fixture.detectChanges()
})
@ -47,8 +47,8 @@ describe('EmailDocumentDialogComponent', () => {
})
it('should support sending document via email, showing error if needed', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationSuccessSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSuccessSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
jest.spyOn(documentService, 'emailDocument').mockReturnValue(of(true))
component.emailDocument()
expect(notificationSuccessSpy).toHaveBeenCalled()
expect(toastSuccessSpy).toHaveBeenCalled()
})
it('should close the dialog', () => {

@ -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 notificationService: NotificationService
private toastService: ToastService
) {
super()
this.loading = false
@ -62,14 +62,11 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
this.emailAddress = ''
this.emailSubject = ''
this.emailMessage = ''
this.notificationService.showInfo($localize`Email sent`)
this.toastService.showInfo($localize`Email sent`)
},
error: (e) => {
this.loading = false
this.notificationService.showError(
$localize`Error emailing document`,
e
)
this.toastService.showError($localize`Error emailing document`, e)
},
})
}

@ -1,3 +0,0 @@
@for (notification of notifications; track notification.id) {
<pngx-notification [notification]="notification" [autohide]="true" (close)="closeNotification()"></pngx-notification>
}

@ -1,84 +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 {
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<NotificationListComponent>
let notificationService: NotificationService
let notificationSubject: Subject<Notification> = 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])
})
})

@ -1,48 +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 {
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 = []
}
}

@ -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<ProfileEditDialogComponent>
let profileService: ProfileService
let notificationService: NotificationService
let toastService: ToastService
let clipboard: Clipboard
beforeEach(() => {
@ -64,7 +64,7 @@ describe('ProfileEditDialogComponent', () => {
providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())],
})
profileService = TestBed.inject(ProfileService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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(notificationService, 'showError')
const errorSpy = jest.spyOn(toastService, 'showError')
updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save')))
component.save()
expect(errorSpy).toHaveBeenCalled()
updateSpy.mockClear()
const infoSpy = jest.spyOn(notificationService, 'showInfo')
const infoSpy = jest.spyOn(toastService, '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(notificationService, 'showError')
const errorSpy = jest.spyOn(toastService, '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(notificationService, 'showError')
const errorSpy = jest.spyOn(toastService, 'showError')
expect(component.socialAccounts).toContainEqual(socialAccount)
@ -300,13 +300,13 @@ describe('ProfileEditDialogComponent', () => {
secret: 'secret',
}
const getSpy = jest.spyOn(profileService, 'getTotpSettings')
const notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
getSpy.mockReturnValueOnce(
throwError(() => new Error('failed to get settings'))
)
component.gettotpSettings()
expect(getSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
getSpy.mockReturnValue(of(settings))
component.gettotpSettings()
@ -316,8 +316,8 @@ describe('ProfileEditDialogComponent', () => {
it('should activate totp', () => {
const activateSpy = jest.spyOn(profileService, 'activateTotp')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const error = new Error('failed to activate totp')
activateSpy.mockReturnValueOnce(throwError(() => error))
component.totpSettings = {
@ -331,44 +331,38 @@ describe('ProfileEditDialogComponent', () => {
component.totpSettings.secret,
component.form.get('totp_code').value
)
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
activateSpy.mockReturnValueOnce(of({ success: false, recovery_codes: [] }))
component.activateTotp()
expect(notificationErrorSpy).toHaveBeenCalledWith(
'Error activating TOTP',
error
)
expect(toastErrorSpy).toHaveBeenCalledWith('Error activating TOTP', error)
activateSpy.mockReturnValueOnce(
of({ success: true, recovery_codes: ['1', '2', '3'] })
)
component.activateTotp()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
expect(component.isTotpEnabled).toBeTruthy()
expect(component.recoveryCodes).toEqual(['1', '2', '3'])
})
it('should deactivate totp', () => {
const deactivateSpy = jest.spyOn(profileService, 'deactivateTotp')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const error = new Error('failed to deactivate totp')
deactivateSpy.mockReturnValueOnce(throwError(() => error))
component.deactivateTotp()
expect(deactivateSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
deactivateSpy.mockReturnValueOnce(of(false))
component.deactivateTotp()
expect(notificationErrorSpy).toHaveBeenCalledWith(
'Error deactivating TOTP',
error
)
expect(toastErrorSpy).toHaveBeenCalledWith('Error deactivating TOTP', error)
deactivateSpy.mockReturnValueOnce(of(true))
component.deactivateTotp()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
expect(component.isTotpEnabled).toBeFalsy()
})

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private clipboard: Clipboard
) {
super()
@ -192,11 +192,9 @@ export class ProfileEditDialogComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.notificationService.showInfo(
$localize`Profile updated successfully`
)
this.toastService.showInfo($localize`Profile updated successfully`)
if (passwordChanged) {
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
@ -206,10 +204,7 @@ export class ProfileEditDialogComponent
this.activeModal.close()
},
error: (error) => {
this.notificationService.showError(
$localize`Error saving profile`,
error
)
this.toastService.showError($localize`Error saving profile`, error)
this.networkActive = false
},
})
@ -225,7 +220,7 @@ export class ProfileEditDialogComponent
this.form.patchValue({ auth_token: token })
},
error: (error) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error generating auth token`,
error
)
@ -250,7 +245,7 @@ export class ProfileEditDialogComponent
this.socialAccounts = this.socialAccounts.filter((a) => a.id != id)
},
error: (error) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error disconnecting social account`,
error
)
@ -269,7 +264,7 @@ export class ProfileEditDialogComponent
this.totpSettings = totpSettings
},
error: (error) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error fetching TOTP settings`,
error
)
@ -291,20 +286,15 @@ export class ProfileEditDialogComponent
this.recoveryCodes = activationResponse.recovery_codes
this.form.get('totp_code').enable()
if (activationResponse.success) {
this.notificationService.showInfo(
$localize`TOTP activated successfully`
)
this.toastService.showInfo($localize`TOTP activated successfully`)
} else {
this.notificationService.showError($localize`Error activating TOTP`)
this.toastService.showError($localize`Error activating TOTP`)
}
},
error: (error) => {
this.totpLoading = false
this.form.get('totp_code').enable()
this.notificationService.showError(
$localize`Error activating TOTP`,
error
)
this.toastService.showError($localize`Error activating TOTP`, error)
},
})
}
@ -320,21 +310,14 @@ export class ProfileEditDialogComponent
this.isTotpEnabled = !success
this.recoveryCodes = null
if (success) {
this.notificationService.showInfo(
$localize`TOTP deactivated successfully`
)
this.toastService.showInfo($localize`TOTP deactivated successfully`)
} else {
this.notificationService.showError(
$localize`Error deactivating TOTP`
)
this.toastService.showError($localize`Error deactivating TOTP`)
}
},
error: (error) => {
this.totpLoading = false
this.notificationService.showError(
$localize`Error deactivating TOTP`,
error
)
this.toastService.showError($localize`Error deactivating TOTP`, error)
},
})
}

@ -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<ShareLinksDialogComponent>
let shareLinkService: ShareLinkService
let notificationService: NotificationService
let toastService: ToastService
let httpController: HttpTestingController
let clipboard: Clipboard
@ -43,7 +43,7 @@ describe('ShareLinksDialogComponent', () => {
fixture = TestBed.createComponent(ShareLinksDialogComponent)
shareLinkService = TestBed.inject(ShareLinkService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
httpController = TestBed.inject(HttpTestingController)
clipboard = TestBed.inject(Clipboard)
@ -89,7 +89,7 @@ describe('ShareLinksDialogComponent', () => {
})
it('should show error on refresh if needed', () => {
const notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(shareLinkService, 'getLinksForDocument')
.mockReturnValueOnce(throwError(() => new Error('Unable to get links')))
@ -97,7 +97,7 @@ describe('ShareLinksDialogComponent', () => {
component.ngOnInit()
fixture.detectChanges()
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
component.createLink()
@ -150,7 +150,7 @@ describe('ShareLinksDialogComponent', () => {
)
fixture.detectChanges()
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
})
it('should support delete links & refresh', () => {
@ -165,13 +165,13 @@ describe('ShareLinksDialogComponent', () => {
})
it('should show error on delete if needed', () => {
const notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(shareLinkService, 'delete')
.mockReturnValueOnce(throwError(() => new Error('Unable to delete link')))
component.delete(null)
fixture.detectChanges()
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
})
it('should format days remaining', () => {

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private clipboard: Clipboard
) {}
@ -81,7 +81,7 @@ export class ShareLinksDialogComponent implements OnInit {
this.shareLinks = results
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error retrieving links`,
10000,
e
@ -130,11 +130,7 @@ export class ShareLinksDialogComponent implements OnInit {
this.refresh()
},
error: (e) => {
this.notificationService.showError(
$localize`Error deleting link`,
10000,
e
)
this.toastService.showError($localize`Error deleting link`, 10000, e)
},
})
}
@ -162,11 +158,7 @@ export class ShareLinksDialogComponent implements OnInit {
},
error: (e) => {
this.loading = false
this.notificationService.showError(
$localize`Error creating link`,
10000,
e
)
this.toastService.showError($localize`Error creating link`, 10000, e)
},
})
}

@ -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 notificationService: NotificationService
let toastService: ToastService
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -82,7 +82,7 @@ describe('SystemStatusDialogComponent', () => {
clipboard = TestBed.inject(Clipboard)
tasksService = TestBed.inject(TasksService)
systemStatusService = TestBed.inject(SystemStatusService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
fixture.detectChanges()
})
@ -116,9 +116,9 @@ describe('SystemStatusDialogComponent', () => {
expect(component.isRunning(PaperlessTaskName.SanityCheck)).toBeFalsy()
})
it('should support running tasks, refresh status and show notifications', () => {
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
it('should support running tasks, refresh status and show toasts', () => {
const toastSpy = jest.spyOn(toastService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalledWith(
expect(toastErrorSpy).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(notificationSpy).toHaveBeenCalledWith(
expect(toastSpy).toHaveBeenCalledWith(
`Task ${PaperlessTaskName.IndexOptimize} started`
)
})

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private permissionsService: PermissionsService
) {}
@ -79,7 +79,7 @@ export class SystemStatusDialogComponent {
public runTask(taskName: PaperlessTaskName) {
this.runningTasks.add(taskName)
this.notificationService.showInfo(`Task ${taskName} started`)
this.toastService.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.notificationService.showError(
this.toastService.showError(
`Failed to start task ${taskName}, see the logs for more details`,
err
)

@ -1,39 +1,39 @@
<ngb-toast
[autohide]="autohide"
[delay]="notification.delay"
[class]="notification.classname"
[delay]="toast.delay"
[class]="toast.classname"
[class.mb-2]="true"
(shown)="onShown(notification)"
(hidden)="hidden.emit(notification)">
(shown)="onShown(toast)"
(hidden)="hidden.emit(toast)">
@if (autohide) {
<ngb-progressbar class="position-absolute h-100 w-100 top-90 start-0 bottom-0 end-0 pe-none" type="dark" [max]="notification.delay" [value]="notification.delayRemaining"></ngb-progressbar>
<span class="visually-hidden">{{ notification.delayRemaining / 1000 | number: '1.0-0' }} seconds</span>
<ngb-progressbar class="position-absolute h-100 w-100 top-90 start-0 bottom-0 end-0 pe-none" type="dark" [max]="toast.delay" [value]="toast.delayRemaining"></ngb-progressbar>
<span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span>
}
<div class="d-flex align-items-top">
@if (!notification.error) {
@if (!toast.error) {
<i-bs width="0.9em" height="0.9em" name="info-circle"></i-bs>
}
@if (notification.error) {
@if (toast.error) {
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
}
<div>
<p class="ms-2 mb-0">{{notification.content}}</p>
@if (notification.error) {
<p class="ms-2 mb-0">{{toast.content}}</p>
@if (toast.error) {
<details class="ms-2">
<div class="mt-2 ms-n4 me-n2 small">
@if (isDetailedError(notification.error)) {
@if (isDetailedError(toast.error)) {
<dl class="row mb-0">
<dt class="col-sm-3 fw-normal text-end">URL</dt>
<dd class="col-sm-9">{{ notification.error.url }}</dd>
<dd class="col-sm-9">{{ toast.error.url }}</dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
<dd class="col-sm-9">{{ notification.error.status }} <em>{{ notification.error.statusText }}</em></dd>
<dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
<dd class="col-sm-9">{{ getErrorText(notification.error) }}</dd>
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
</dl>
}
<div class="row">
<div class="col offset-sm-3">
<button class="btn btn-sm btn-outline-secondary" (click)="copyError(notification.error)">
<button class="btn btn-sm btn-outline-secondary" (click)="copyError(toast.error)">
@if (!copied) {
<i-bs name="clipboard"></i-bs>&nbsp;
}
@ -47,10 +47,10 @@
</div>
</details>
}
@if (notification.action) {
<p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="close.emit(notification); notification.action()">{{notification.actionName}}</button></p>
@if (toast.action) {
<p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="close.emit(toast); toast.action()">{{toast.actionName}}</button></p>
}
</div>
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="notification" aria-label="Close" (click)="close.emit(notification);"></button>
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="close.emit(toast);"></button>
</div>
</ngb-toast>

@ -9,15 +9,15 @@ import {
import { Clipboard } from '@angular/cdk/clipboard'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { NotificationComponent } from './notification.component'
import { ToastComponent } from './toast.component'
const notification1 = {
const toast1 = {
content: 'Error 1 content',
delay: 5000,
error: 'Error 1 string',
}
const notification2 = {
const toast2 = {
content: 'Error 2 content',
delay: 5000,
error: {
@ -29,17 +29,17 @@ const notification2 = {
},
}
describe('NotificationComponent', () => {
let component: NotificationComponent
let fixture: ComponentFixture<NotificationComponent>
describe('ToastComponent', () => {
let component: ToastComponent
let fixture: ComponentFixture<ToastComponent>
let clipboard: Clipboard
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NotificationComponent, NgxBootstrapIconsModule.pick(allIcons)],
imports: [ToastComponent, NgxBootstrapIconsModule.pick(allIcons)],
}).compileComponents()
fixture = TestBed.createComponent(NotificationComponent)
fixture = TestBed.createComponent(ToastComponent)
clipboard = TestBed.inject(Clipboard)
component = fixture.componentInstance
})
@ -48,18 +48,18 @@ describe('NotificationComponent', () => {
expect(component).toBeTruthy()
})
it('should countdown notification', fakeAsync(() => {
component.notification = notification2
it('should countdown toast', fakeAsync(() => {
component.toast = toast2
fixture.detectChanges()
component.onShown(notification2)
component.onShown(toast2)
tick(5000)
expect(component.notification.delayRemaining).toEqual(0)
expect(component.toast.delayRemaining).toEqual(0)
flush()
discardPeriodicTasks()
}))
it('should show an error if given with notification', fakeAsync(() => {
component.notification = notification1
it('should show an error if given with toast', fakeAsync(() => {
component.toast = toast1
fixture.detectChanges()
expect(fixture.nativeElement.querySelector('details')).not.toBeNull()
@ -70,7 +70,7 @@ describe('NotificationComponent', () => {
}))
it('should show error details, support copy', fakeAsync(() => {
component.notification = notification2
component.toast = toast2
fixture.detectChanges()
expect(fixture.nativeElement.querySelector('details')).not.toBeNull()
@ -79,7 +79,7 @@ describe('NotificationComponent', () => {
)
const copySpy = jest.spyOn(clipboard, 'copy')
component.copyError(notification2.error)
component.copyError(toast2.error)
expect(copySpy).toHaveBeenCalled()
flush()
@ -87,7 +87,7 @@ describe('NotificationComponent', () => {
}))
it('should parse error text, add ellipsis', () => {
expect(component.getErrorText(notification2.error)).toEqual(
expect(component.getErrorText(toast2.error)).toEqual(
'Error 2 message details'
)
expect(component.getErrorText({ error: 'Error string no detail' })).toEqual(

@ -7,43 +7,42 @@ import {
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { interval, take } from 'rxjs'
import { Notification } from 'src/app/services/notification.service'
import { Toast } from 'src/app/services/toast.service'
@Component({
selector: 'pngx-notification',
selector: 'pngx-toast',
imports: [
DecimalPipe,
NgbToastModule,
NgbProgressbarModule,
NgxBootstrapIconsModule,
],
templateUrl: './notification.component.html',
styleUrl: './notification.component.scss',
templateUrl: './toast.component.html',
styleUrl: './toast.component.scss',
})
export class NotificationComponent {
@Input() notification: Notification
export class ToastComponent {
@Input() toast: Toast
@Input() autohide: boolean = true
@Output() hidden: EventEmitter<Notification> =
new EventEmitter<Notification>()
@Output() hidden: EventEmitter<Toast> = new EventEmitter<Toast>()
@Output() close: EventEmitter<Notification> = new EventEmitter<Notification>()
@Output() close: EventEmitter<Toast> = new EventEmitter<Toast>()
public copied: boolean = false
constructor(private clipboard: Clipboard) {}
onShown(notification: Notification) {
onShown(toast: Toast) {
if (!this.autohide) return
const refreshInterval = 150
const delay = notification.delay - 500 // for fade animation
const delay = toast.delay - 500 // for fade animation
interval(refreshInterval)
.pipe(take(Math.round(delay / refreshInterval)))
.subscribe((count) => {
notification.delayRemaining = Math.max(
toast.delayRemaining = Math.max(
0,
delay - refreshInterval * (count + 1)
)

@ -0,0 +1,3 @@
@for (toast of toasts; track toast.id) {
<pngx-toast [toast]="toast" [autohide]="true" (close)="closeToast()"></pngx-toast>
}

@ -1,7 +1,7 @@
:host {
position: fixed;
top: 0;
right: calc(50% - (var(--pngx-notification-max-width) / 2));
right: calc(50% - (var(--pngx-toast-max-width) / 2));
margin: 0.3em;
z-index: 1200;
}

@ -0,0 +1,71 @@
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<ToastsComponent>
let toastService: ToastService
let toastSubject: Subject<Toast> = 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])
})
})

@ -0,0 +1,43 @@
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 = []
}
}

@ -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<DashboardComponent>
let settingsService: SettingsService
let tourService: TourService
let notificationService: NotificationService
let toastService: ToastService
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)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, '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(notificationSpy).toHaveBeenCalled()
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
SavedView[]
>)
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
})
})

@ -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 notificationService: NotificationService
private toastService: ToastService
) {
super()
@ -87,13 +87,10 @@ export class DashboardComponent extends ComponentWithPermissions {
.updateDashboardViewsSort(this.dashboardViews)
.subscribe({
next: () => {
this.notificationService.showInfo($localize`Dashboard updated`)
this.toastService.showInfo($localize`Dashboard updated`)
},
error: (e) => {
this.notificationService.showError(
$localize`Error updating dashboard`,
e
)
this.toastService.showError($localize`Error updating dashboard`, e)
},
})
}

@ -48,7 +48,6 @@ 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'
@ -59,6 +58,7 @@ 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 notificationService: NotificationService
let toastService: ToastService
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)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notification', () => {
it('should support save, close and show success toast', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, 'showInfo')
updateSpy.mockImplementation((o) => of(doc))
component.save(true)
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalledWith(
expect(toastSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
)
})
it('should support save without close and show success notification', () => {
it('should support save without close and show success toast', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, 'showInfo')
updateSpy.mockImplementation((o) => of(doc))
component.save()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalledWith(
expect(toastSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
)
})
it('should show notification error on save if error occurs', () => {
it('should show toast 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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
const error = new Error('failed to save')
updateSpy.mockImplementation(() => throwError(() => error))
component.save()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalledWith(
expect(toastSpy).toHaveBeenCalledWith(
'Error saving document "Doc 3"',
error
)
})
it('should show error notification on save but close if user can no longer edit', () => {
it('should show error toast 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 notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, 'showInfo')
updateSpy.mockImplementation(() =>
throwError(() => new Error('failed to save'))
)
component.save(true)
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalledWith(
expect(toastSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
)
})
@ -531,19 +531,19 @@ describe('DocumentDetailComponent', () => {
expect
})
it('should show notification error on save & next if error occurs', () => {
it('should show toast 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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
const error = new Error('failed to save')
updateSpy.mockImplementation(() => throwError(() => error))
component.saveEditNext()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalledWith('Error saving document', error)
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, 'showInfo')
component.reprocess()
const modalCloseSpy = jest.spyOn(openModal, 'close')
openModal.componentInstance.confirmClicked.next()
expect(bulkEditSpy).toHaveBeenCalledWith([doc.id], 'reprocess', {})
expect(modalSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
component.reprocess()
const modalCloseSpy = jest.spyOn(openModal, 'close')
bulkEditSpy.mockReturnValue(throwError(() => new Error('error occurred')))
openModal.componentInstance.confirmClicked.next()
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(modalCloseSpy).not.toHaveBeenCalled()
})
@ -942,12 +942,9 @@ describe('DocumentDetailComponent', () => {
jest
.spyOn(documentService, 'getMetadata')
.mockReturnValue(throwError(() => error))
const notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
initNormally()
expect(notificationSpy).toHaveBeenCalledWith(
'Error retrieving metadata',
error
)
expect(toastSpy).toHaveBeenCalledWith('Error retrieving metadata', error)
})
it('should display custom fields', () => {
@ -1031,7 +1028,7 @@ describe('DocumentDetailComponent', () => {
it('should show error if needed for get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const errorSpy = jest.spyOn(notificationService, 'showError')
const errorSpy = jest.spyOn(toastService, 'showError')
suggestionsSpy.mockImplementationOnce(() =>
throwError(() => new Error('failed to get suggestions'))
)

@ -63,7 +63,6 @@ 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,
@ -77,6 +76,7 @@ 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 notificationService: NotificationService,
private toastService: ToastService,
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 <object> tag
this.notificationService.showError(
this.toastService.showError(
$localize`Error retrieving metadata`,
error
)
@ -657,7 +657,7 @@ export class DocumentDetailComponent
},
error: (error) => {
this.suggestions = null
this.notificationService.showError(
this.toastService.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.notificationService.showInfo(
this.toastService.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.notificationService.showInfo(
this.toastService.showInfo(
$localize`Document "${this.document.title}" saved successfully.`
)
close && this.close()
} else {
this.error = error.error
this.notificationService.showError(
this.toastService.showError(
$localize`Error saving document "${this.document.title}"`,
error
)
@ -877,10 +877,7 @@ export class DocumentDetailComponent
error: (error) => {
this.networkActive = false
this.error = error.error
this.notificationService.showError(
$localize`Error saving document`,
error
)
this.toastService.showError($localize`Error saving document`, error)
},
})
}
@ -934,10 +931,7 @@ export class DocumentDetailComponent
this.close()
},
error: (error) => {
this.notificationService.showError(
$localize`Error deleting document`,
error
)
this.toastService.showError($localize`Error deleting document`, error)
modal.componentInstance.buttonsEnabled = true
this.subscribeModalDelete(modal)
},
@ -968,7 +962,7 @@ export class DocumentDetailComponent
.bulkEdit([this.document.id], 'reprocess', {})
.subscribe({
next: () => {
this.notificationService.showInfo(
this.toastService.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) {
@ -979,7 +973,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.notificationService.showError(
this.toastService.showError(
$localize`Error executing operation`,
error
)
@ -1026,7 +1020,7 @@ export class DocumentDetailComponent
},
error: (error) => {
this.downloading = false
this.notificationService.showError(
this.toastService.showError(
$localize`Error downloading document`,
error
)
@ -1335,7 +1329,7 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Split operation for "${this.document.title}" will begin in the background.`
)
modal.close()
@ -1344,7 +1338,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.notificationService.showError(
this.toastService.showError(
$localize`Error executing split operation`,
error
)
@ -1374,7 +1368,7 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.notificationService.show({
this.toastService.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),
@ -1386,7 +1380,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.notificationService.showError(
this.toastService.showError(
$localize`Error executing rotate operation`,
error
)
@ -1414,7 +1408,7 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.notificationService.showInfo(
this.toastService.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()
@ -1423,7 +1417,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.notificationService.showError(
this.toastService.showError(
$localize`Error executing delete pages operation`,
error
)

@ -16,7 +16,6 @@ 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'
@ -30,6 +29,7 @@ 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 notificationService: NotificationService
let toastService: ToastService
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)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notification on bulk edit error', () => {
it('should show a warning toast on bulk edit error', () => {
jest
.spyOn(documentService, 'bulkEdit')
.mockReturnValue(
@ -902,12 +902,12 @@ describe('BulkEditorComponent', () => {
.mockReturnValue(true)
component.showConfirmationDialogs = false
fixture.detectChanges()
const notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
component.setTags({
itemsToAdd: [{ id: 0 }],
itemsToRemove: [],
})
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
})
it('should support redo ocr', () => {
@ -1391,14 +1391,8 @@ describe('BulkEditorComponent', () => {
.spyOn(documentListViewService, 'selected', 'get')
.mockReturnValue(new Set([3, 4]))
fixture.detectChanges()
const notificationServiceShowInfoSpy = jest.spyOn(
notificationService,
'showInfo'
)
const notificationServiceShowErrorSpy = jest.spyOn(
notificationService,
'showError'
)
const toastServiceShowInfoSpy = jest.spyOn(toastService, 'showInfo')
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
component.customFields = [
@ -1416,11 +1410,11 @@ describe('BulkEditorComponent', () => {
expect(modal.componentInstance.documents).toEqual([3, 4])
modal.componentInstance.failed.emit()
expect(notificationServiceShowErrorSpy).toHaveBeenCalled()
expect(toastServiceShowErrorSpy).toHaveBeenCalled()
expect(listReloadSpy).not.toHaveBeenCalled()
modal.componentInstance.succeeded.emit()
expect(notificationServiceShowInfoSpy).toHaveBeenCalled()
expect(toastServiceShowInfoSpy).toHaveBeenCalled()
expect(listReloadSpy).toHaveBeenCalled()
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`

@ -23,7 +23,6 @@ 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,
@ -40,6 +39,7 @@ 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 notificationService: NotificationService,
private toastService: ToastService,
private storagePathService: StoragePathService,
private customFieldService: CustomFieldsService,
private permissionService: PermissionsService
@ -284,7 +284,7 @@ export class BulkEditorComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.notificationService.showError(
this.toastService.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.notificationService.showInfo(
this.toastService.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.notificationService.showInfo($localize`Custom fields updated.`)
this.toastService.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.notificationService.showError(
this.toastService.showError(
$localize`Error updating custom fields.`,
error
)

@ -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 notificationService: NotificationService
let toastService: ToastService
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)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, 'showInfo')
component.saveViewConfig()
expect(savedViewServicePatch).toHaveBeenCalledWith(modifiedView)
expect(notificationSpy).toHaveBeenCalledWith(
expect(toastSpy).toHaveBeenCalledWith(
`View "${view.name}" saved successfully.`
)
})
@ -427,12 +427,12 @@ describe('DocumentListComponent', () => {
},
],
})
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(savedViewService, 'patch')
.mockReturnValueOnce(throwError(() => new Error('Error saving view')))
component.saveViewConfig()
expect(notificationErrorSpy).toHaveBeenCalledWith(
expect(toastErrorSpy).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 notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, '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(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(modalCloseSpy).toHaveBeenCalled()
})

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private modalService: NgbModal,
private websocketStatusService: WebsocketStatusService,
public openDocumentsService: OpenDocumentsService,
@ -380,13 +380,13 @@ export class DocumentListComponent
.subscribe({
next: (view) => {
this.unmodifiedSavedView = view
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
)
this.unmodifiedFilterRules = this.list.filterRules
},
error: (err) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Failed to save view "${this.list.activeSavedViewTitle}".`,
err
)
@ -430,7 +430,7 @@ export class DocumentListComponent
.subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`View "${savedView.name}" created successfully.`
)
},

@ -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<DocumentNotesComponent>
let notesService: DocumentNotesService
let notificationService: NotificationService
let toastService: ToastService
beforeEach(async () => {
TestBed.configureTestingModule({
@ -103,7 +103,7 @@ describe('DocumentNotesComponent', () => {
}).compileComponents()
notesService = TestBed.inject(DocumentNotesService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationsSpy = jest.spyOn(notificationService, 'showError')
const toastsSpy = jest.spyOn(toastService, 'showError')
const addButton = fixture.debugElement.query(By.css('button'))
addButton.triggerEventHandler('click')
expect(addSpy).toHaveBeenCalledWith(12, note)
expect(notificationsSpy).toHaveBeenCalled()
expect(toastsSpy).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(notificationService, 'showError')
const toastsSpy = jest.spyOn(toastService, 'showError')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting note'))
)

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private usersService: UserService
) {
super()
@ -78,7 +78,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
},
error: (e) => {
this.networkActive = false
this.notificationService.showError($localize`Error saving note`, e)
this.toastService.showError($localize`Error saving note`, e)
},
})
}
@ -92,7 +92,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
},
error: (e) => {
this.networkActive = false
this.notificationService.showError($localize`Error deleting note`, e)
this.toastService.showError($localize`Error deleting note`, e)
},
})
}

@ -10,28 +10,24 @@ 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 { NotificationListComponent } from '../common/notification-list/notification-list.component'
import { ToastsComponent } from '../common/toasts/toasts.component'
import { FileDropComponent } from './file-drop.component'
describe('FileDropComponent', () => {
let component: FileDropComponent
let fixture: ComponentFixture<FileDropComponent>
let permissionsService: PermissionsService
let notificationService: NotificationService
let toastService: ToastService
let settingsService: SettingsService
let uploadDocumentsService: UploadDocumentsService
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NgxFileDropModule,
FileDropComponent,
NotificationListComponent,
],
imports: [NgxFileDropModule, FileDropComponent, ToastsComponent],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
@ -40,7 +36,7 @@ describe('FileDropComponent', () => {
permissionsService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
fixture = TestBed.createComponent(FileDropComponent)
@ -105,7 +101,7 @@ describe('FileDropComponent', () => {
fixture.detectChanges()
expect(dropzone.classes['hide']).toBeTruthy()
// drop
const notificationSpy = jest.spyOn(notificationService, 'show')
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(
UploadDocumentsService.prototype as any,
'uploadFile'
@ -139,7 +135,7 @@ describe('FileDropComponent', () => {
} as unknown as NgxFileDropEntry,
])
tick(3000)
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))

@ -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 notificationService: NotificationService,
private toastService: ToastService,
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.notificationService.showInfo($localize`Initiating upload...`, 3000)
this.toastService.showInfo($localize`Initiating upload...`, 3000)
}
@HostListener('window:blur', ['$event']) public onWindowBlur() {

@ -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<Correspo
constructor(
correspondentsService: CorrespondentService,
modalService: NgbModal,
notificationService: NotificationService,
toastService: ToastService,
documentListViewService: DocumentListViewService,
permissionsService: PermissionsService,
private datePipe: CustomDatePipe
@ -54,7 +54,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Correspo
correspondentsService,
modalService,
CorrespondentEditDialogComponent,
notificationService,
toastService,
documentListViewService,
permissionsService,
FILTER_HAS_CORRESPONDENT_ANY,

@ -21,10 +21,10 @@ 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 { 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 { PageHeaderComponent } from '../../common/page-header/page-header.component'
@ -48,7 +48,7 @@ describe('CustomFieldsComponent', () => {
let fixture: ComponentFixture<CustomFieldsComponent>
let customFieldsService: CustomFieldsService
let modalService: NgbModal
let notificationService: NotificationService
let toastService: ToastService
let listViewService: DocumentListViewService
let settingsService: SettingsService
@ -89,7 +89,7 @@ describe('CustomFieldsComponent', () => {
})
)
modalService = TestBed.inject(NgbModal)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private documentListViewService: DocumentListViewService,
private settingsService: SettingsService,
private documentService: DocumentService,
@ -86,9 +86,7 @@ export class CustomFieldsComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newField) => {
this.notificationService.showInfo(
$localize`Saved field "${newField.name}".`
)
this.toastService.showInfo($localize`Saved field "${newField.name}".`)
this.customFieldsService.clearCache()
this.settingsService.initializeDisplayFields()
this.documentService.reload()
@ -97,7 +95,7 @@ export class CustomFieldsComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.notificationService.showError($localize`Error saving field.`, e)
this.toastService.showError($localize`Error saving field.`, e)
})
}
@ -115,9 +113,7 @@ export class CustomFieldsComponent
this.customFieldsService.delete(field).subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
$localize`Deleted field "${field.name}"`
)
this.toastService.showInfo($localize`Deleted field "${field.name}"`)
this.customFieldsService.clearCache()
this.settingsService.initializeDisplayFields()
this.documentService.reload()
@ -125,7 +121,7 @@ export class CustomFieldsComponent
this.reload()
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error deleting field "${field.name}".`,
e
)

@ -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<DocumentT
constructor(
documentTypeService: DocumentTypeService,
modalService: NgbModal,
notificationService: NotificationService,
toastService: ToastService,
documentListViewService: DocumentListViewService,
permissionsService: PermissionsService
) {
@ -51,7 +51,7 @@ export class DocumentTypeListComponent extends ManagementListComponent<DocumentT
documentTypeService,
modalService,
DocumentTypeEditDialogComponent,
notificationService,
toastService,
documentListViewService,
permissionsService,
FILTER_HAS_DOCUMENT_TYPE_ANY,

@ -24,11 +24,11 @@ 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 { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
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'
@ -63,7 +63,7 @@ describe('MailComponent', () => {
let mailAccountService: MailAccountService
let mailRuleService: MailRuleService
let modalService: NgbModal
let notificationService: NotificationService
let toastService: ToastService
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)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(mailAccountService, 'listAll')
.mockImplementation(() =>
throwError(() => new Error('failed to load mail accounts'))
)
completeSetup(mailAccountService)
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
})
it('should show errors on load if load mailRules failure', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(mailRuleService, 'listAll')
.mockImplementation(() =>
throwError(() => new Error('failed to load mail rules'))
)
completeSetup(mailRuleService)
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailAccounts[0])
expect(notificationInfoSpy).toHaveBeenCalledWith(
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved account "${mailAccounts[0].name}".`
)
editDialog.cancel()
@ -203,37 +203,35 @@ describe('MailComponent', () => {
component.deleteMailAccount(mailAccounts[0] as MailAccount)
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(mailAccountService, 'delete')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(mailAccountService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting mail account'))
)
deleteDialog.confirm()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalledWith(
'Deleted mail account "account1"'
)
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account "account1"')
})
it('should support process mail account, show error if needed', () => {
completeSetup()
const processSpy = jest.spyOn(mailAccountService, 'processAccount')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
processSpy.mockReturnValueOnce(of(true))
component.processAccount(mailAccounts[0] as MailAccount)
expect(notificationInfoSpy).toHaveBeenCalledWith(
expect(toastInfoSpy).toHaveBeenCalledWith(
'Processing mail account "account1"'
)
})
@ -244,12 +242,12 @@ describe('MailComponent', () => {
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editMailRule(mailRules[0] as MailRule)
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailRules[0])
expect(notificationInfoSpy).toHaveBeenCalledWith(
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved rule "${mailRules[0].name}".`
)
editDialog.cancel()
@ -274,20 +272,18 @@ describe('MailComponent', () => {
component.deleteMailRule(mailRules[0] as MailRule)
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(mailRuleService, 'delete')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(mailRuleService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting mail rule "rule1"'))
)
deleteDialog.confirm()
expect(notificationErrorSpy).toBeCalled()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalledWith(
'Deleted mail rule "rule1"'
)
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule "rule1"')
})
it('should support edit permissions on mail rule objects', () => {
@ -307,8 +303,8 @@ describe('MailComponent', () => {
}
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const rulePatchSpy = jest.spyOn(mailRuleService, 'patch')
component.editPermissions(mailRules[0] as MailRule)
expect(modal).not.toBeUndefined()
@ -320,10 +316,10 @@ describe('MailComponent', () => {
)
dialog.confirmClicked.emit({ permissions: perms, merge: true })
expect(rulePatchSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule))
dialog.confirmClicked.emit({ permissions: perms, merge: true })
expect(notificationInfoSpy).toHaveBeenCalledWith('Permissions updated')
expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated')
modalService.dismissAll()
})
@ -360,15 +356,15 @@ describe('MailComponent', () => {
const toggleInput = fixture.debugElement.query(
By.css('input[type="checkbox"]')
)
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
// fail first
patchSpy.mockReturnValueOnce(
throwError(() => new Error('Error getting config'))
)
toggleInput.nativeElement.click()
expect(patchSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
// succeed second
patchSpy.mockReturnValueOnce(of(mailRules[0] as MailRule))
toggleInput.nativeElement.click()
@ -377,7 +373,7 @@ describe('MailComponent', () => {
)
toggleInput.nativeElement.click()
expect(patchSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
})
it('should show success message when oauth account is connected', () => {
@ -385,9 +381,9 @@ describe('MailComponent', () => {
jest
.spyOn(activatedRoute, 'queryParamMap', 'get')
.mockReturnValue(of(convertToParamMap(queryParams)))
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
completeSetup()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
})
it('should show error message when oauth account connect fails', () => {
@ -395,9 +391,9 @@ describe('MailComponent', () => {
jest
.spyOn(activatedRoute, 'queryParamMap', 'get')
.mockReturnValue(of(convertToParamMap(queryParams)))
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
completeSetup()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
})
it('should open account edit dialog if oauth account is connected', () => {

@ -11,7 +11,6 @@ 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,
@ -20,6 +19,7 @@ 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 notificationService: NotificationService,
private toastService: ToastService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private settingsService: SettingsService,
@ -104,7 +104,7 @@ export class MailComponent
this.showAccounts = true
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error retrieving mail accounts`,
e
)
@ -127,10 +127,7 @@ export class MailComponent
this.showRules = true
},
error: (e) => {
this.notificationService.showError(
$localize`Error retrieving mail rules`,
e
)
this.toastService.showError($localize`Error retrieving mail rules`, e)
},
})
@ -138,9 +135,7 @@ export class MailComponent
if (params.get('oauth_success')) {
const success = params.get('oauth_success') === '1'
if (success) {
this.notificationService.showInfo(
$localize`OAuth2 authentication success`
)
this.toastService.showInfo($localize`OAuth2 authentication success`)
this.oAuthAccountId = parseInt(params.get('account_id'))
if (this.mailAccounts.length > 0) {
this.editMailAccount(
@ -150,7 +145,7 @@ export class MailComponent
)
}
} else {
this.notificationService.showError(
this.toastService.showError(
$localize`OAuth2 authentication failed, see logs for details`
)
}
@ -174,7 +169,7 @@ export class MailComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newMailAccount) => {
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Saved account "${newMailAccount.name}".`
)
this.mailAccountService.clearCache()
@ -187,7 +182,7 @@ export class MailComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.notificationService.showError($localize`Error saving account.`, e)
this.toastService.showError($localize`Error saving account.`, e)
})
}
@ -205,7 +200,7 @@ export class MailComponent
this.mailAccountService.delete(account).subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Deleted mail account "${account.name}"`
)
this.mailAccountService.clearCache()
@ -216,7 +211,7 @@ export class MailComponent
})
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error deleting mail account "${account.name}".`,
e
)
@ -228,12 +223,12 @@ export class MailComponent
processAccount(account: MailAccount) {
this.mailAccountService.processAccount(account).subscribe({
next: () => {
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Processing mail account "${account.name}"`
)
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error processing mail account "${account.name}"`,
e
)
@ -252,9 +247,7 @@ export class MailComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newMailRule) => {
this.notificationService.showInfo(
$localize`Saved rule "${newMailRule.name}".`
)
this.toastService.showInfo($localize`Saved rule "${newMailRule.name}".`)
this.mailRuleService.clearCache()
this.mailRuleService
.listAll(null, null, { full_perms: true })
@ -265,7 +258,7 @@ export class MailComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.notificationService.showError($localize`Error saving rule.`, e)
this.toastService.showError($localize`Error saving rule.`, e)
})
}
@ -279,14 +272,14 @@ export class MailComponent
onMailRuleEnableToggled(rule: MailRule) {
this.mailRuleService.patch(rule).subscribe({
next: () => {
this.notificationService.showInfo(
this.toastService.showInfo(
rule.enabled
? $localize`Rule "${rule.name}" enabled.`
: $localize`Rule "${rule.name}" disabled.`
)
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error toggling rule "${rule.name}".`,
e
)
@ -308,7 +301,7 @@ export class MailComponent
this.mailRuleService.delete(rule).subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Deleted mail rule "${rule.name}"`
)
this.mailRuleService.clearCache()
@ -319,7 +312,7 @@ export class MailComponent
})
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error deleting mail rule "${rule.name}".`,
e
)
@ -344,11 +337,11 @@ export class MailComponent
object['set_permissions'] = permissions['set_permissions']
service.patch(object).subscribe({
next: () => {
this.notificationService.showInfo($localize`Permissions updated`)
this.toastService.showInfo($localize`Permissions updated`)
modal.close()
},
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error updating permissions`,
e
)

@ -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<ManagementListComponent<Tag>>
let tagService: TagService
let modalService: NgbModal
let notificationService: NotificationService
let toastService: ToastService
let documentListViewService: DocumentListViewService
let permissionsService: PermissionsService
@ -129,7 +129,7 @@ describe('ManagementListComponent', () => {
.spyOn(permissionsService, 'currentUserOwnsObject')
.mockReturnValue(true)
modalService = TestBed.inject(NgbModal)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
@ -293,22 +293,22 @@ describe('ManagementListComponent', () => {
bulkEditPermsSpy.mockReturnValueOnce(
throwError(() => new Error('error setting permissions'))
)
const errornotificationSpy = jest.spyOn(notificationService, 'showError')
const errorToastSpy = jest.spyOn(toastService, 'showError')
modal.componentInstance.confirmClicked.emit({
permissions: {},
merge: true,
})
expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(errornotificationSpy).toHaveBeenCalled()
expect(errorToastSpy).toHaveBeenCalled()
const successnotificationSpy = jest.spyOn(notificationService, 'showInfo')
const successToastSpy = jest.spyOn(toastService, 'showInfo')
bulkEditPermsSpy.mockReturnValueOnce(of('OK'))
modal.componentInstance.confirmClicked.emit({
permissions: {},
merge: true,
})
expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(successnotificationSpy).toHaveBeenCalled()
expect(successToastSpy).toHaveBeenCalled()
})
it('should support bulk delete objects', () => {
@ -327,19 +327,19 @@ describe('ManagementListComponent', () => {
bulkEditSpy.mockReturnValueOnce(
throwError(() => new Error('error setting permissions'))
)
const errornotificationSpy = jest.spyOn(notificationService, 'showError')
const errorToastSpy = jest.spyOn(toastService, 'showError')
modal.componentInstance.confirmClicked.emit(null)
expect(bulkEditSpy).toHaveBeenCalledWith(
Array.from(selected),
BulkEditObjectOperation.Delete
)
expect(errornotificationSpy).toHaveBeenCalled()
expect(errorToastSpy).toHaveBeenCalled()
const successnotificationSpy = jest.spyOn(notificationService, 'showInfo')
const successToastSpy = jest.spyOn(toastService, 'showInfo')
bulkEditSpy.mockReturnValueOnce(of('OK'))
modal.componentInstance.confirmClicked.emit(null)
expect(bulkEditSpy).toHaveBeenCalled()
expect(successnotificationSpy).toHaveBeenCalled()
expect(successToastSpy).toHaveBeenCalled()
})
it('should disallow bulk permissions or delete objects if no global perms', () => {

@ -27,7 +27,6 @@ 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,
@ -37,6 +36,7 @@ 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<T extends MatchingModel>
protected service: AbstractNameFilterService<T>,
private modalService: NgbModal,
private editDialogComponent: any,
private notificationService: NotificationService,
private toastService: ToastService,
private documentListViewService: DocumentListViewService,
private permissionsService: PermissionsService,
protected filterRuleType: number,
@ -173,12 +173,12 @@ export abstract class ManagementListComponent<T extends MatchingModel>
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Successfully created ${this.typeName}.`
)
})
activeModal.componentInstance.failed.subscribe((e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error occurred while creating ${this.typeName}.`,
e
)
@ -193,12 +193,12 @@ export abstract class ManagementListComponent<T extends MatchingModel>
activeModal.componentInstance.dialogMode = EditDialogMode.EDIT
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Successfully updated ${this.typeName} "${object.name}".`
)
})
activeModal.componentInstance.failed.subscribe((e) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error occurred while saving ${this.typeName}.`,
e
)
@ -234,7 +234,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
},
error: (error) => {
activeModal.componentInstance.buttonsEnabled = true
this.notificationService.showError(
this.toastService.showError(
$localize`Error while deleting element`,
error
)
@ -313,14 +313,14 @@ export abstract class ManagementListComponent<T extends MatchingModel>
.subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Permissions updated successfully`
)
this.reloadData()
},
error: (error) => {
modal.componentInstance.buttonsEnabled = true
this.notificationService.showError(
this.toastService.showError(
$localize`Error updating permissions`,
error
)
@ -349,14 +349,12 @@ export abstract class ManagementListComponent<T extends MatchingModel>
.subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
$localize`Objects deleted successfully`
)
this.toastService.showInfo($localize`Objects deleted successfully`)
this.reloadData()
},
error: (error) => {
modal.componentInstance.buttonsEnabled = true
this.notificationService.showError(
this.toastService.showError(
$localize`Error deleting objects`,
error
)

@ -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<SavedViewsComponent>
let savedViewService: SavedViewService
let notificationService: NotificationService
let toastService: ToastService
beforeEach(async () => {
TestBed.configureTestingModule({
@ -77,7 +77,7 @@ describe('SavedViewsComponent', () => {
}).compileComponents()
savedViewService = TestBed.inject(SavedViewService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
fixture = TestBed.createComponent(SavedViewsComponent)
component = fixture.componentInstance
@ -93,8 +93,8 @@ describe('SavedViewsComponent', () => {
})
it('should support save saved views, show error', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'show')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
notificationSpy.mockClear()
notificationErrorSpy.mockClear()
toastSpy.mockClear()
toastErrorSpy.mockClear()
savedViewPatchSpy.mockClear()
// succeed saved views
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
component.save()
expect(notificationErrorSpy).not.toHaveBeenCalled()
expect(toastErrorSpy).not.toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
})
@ -150,12 +150,12 @@ describe('SavedViewsComponent', () => {
})
it('should support delete saved view', () => {
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const deleteSpy = jest.spyOn(savedViewService, 'delete')
deleteSpy.mockReturnValue(of(true))
component.deleteSavedView(savedViews[0] as SavedView)
expect(deleteSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalledWith(
expect(toastSpy).toHaveBeenCalledWith(
`Saved view "${savedViews[0].name}" deleted.`
)
})

@ -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 notificationService: NotificationService
private toastService: ToastService
) {
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.notificationService.showInfo(
this.toastService.showInfo(
$localize`Saved view "${savedView.name}" deleted.`
)
this.savedViewService.clearCache()
@ -155,13 +155,11 @@ export class SavedViewsComponent
if (changed.length) {
this.savedViewService.patchMany(changed).subscribe({
next: () => {
this.notificationService.showInfo(
$localize`Views saved successfully.`
)
this.toastService.showInfo($localize`Views saved successfully.`)
this.store.next(this.savedViewsForm.value)
},
error: (error) => {
this.notificationService.showError(
this.toastService.showError(
$localize`Error while saving views.`,
error
)

@ -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<StoragePat
constructor(
directoryService: StoragePathService,
modalService: NgbModal,
notificationService: NotificationService,
toastService: ToastService,
documentListViewService: DocumentListViewService,
permissionsService: PermissionsService
) {
@ -53,7 +53,7 @@ export class StoragePathListComponent extends ManagementListComponent<StoragePat
directoryService,
modalService,
StoragePathEditDialogComponent,
notificationService,
toastService,
documentListViewService,
permissionsService,
FILTER_HAS_STORAGE_PATH_ANY,

@ -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 { TagService } from 'src/app/services/rest/tag.service'
import { ToastService } from 'src/app/services/toast.service'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-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 TagListComponent extends ManagementListComponent<Tag> {
constructor(
tagService: TagService,
modalService: NgbModal,
notificationService: NotificationService,
toastService: ToastService,
documentListViewService: DocumentListViewService,
permissionsService: PermissionsService
) {
@ -53,7 +53,7 @@ export class TagListComponent extends ManagementListComponent<Tag> {
tagService,
modalService,
TagEditDialogComponent,
notificationService,
toastService,
documentListViewService,
permissionsService,
FILTER_HAS_TAGS_ALL,

@ -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<WorkflowsComponent>
let workflowService: WorkflowService
let modalService: NgbModal
let notificationService: NotificationService
let toastService: ToastService
beforeEach(() => {
TestBed.configureTestingModule({
@ -116,7 +116,7 @@ describe('WorkflowsComponent', () => {
})
)
modalService = TestBed.inject(NgbModal)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(workflows[0])
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(workflows[0])
expect(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).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 notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const toastErrorSpy = jest.spyOn(toastService, '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(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
@ -267,21 +267,21 @@ describe('WorkflowsComponent', () => {
const toggleInput = fixture.debugElement.query(
By.css('input[type="checkbox"]')
)
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
// fail first
patchSpy.mockReturnValueOnce(
throwError(() => new Error('Error getting config'))
)
toggleInput.nativeElement.click()
expect(patchSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(toastErrorSpy).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(notificationInfoSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
})
})

@ -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 notificationService: NotificationService
private toastService: ToastService
) {
super()
}
@ -90,7 +90,7 @@ export class WorkflowsComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newWorkflow) => {
this.notificationService.showInfo(
this.toastService.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.notificationService.showError($localize`Error saving workflow.`, e)
this.toastService.showError($localize`Error saving workflow.`, e)
})
}
@ -142,14 +142,14 @@ export class WorkflowsComponent
this.workflowService.delete(workflow).subscribe({
next: () => {
modal.close()
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`Deleted workflow "${workflow.name}".`
)
this.workflowService.clearCache()
this.reload()
},
error: (e) => {
this.notificationService.showError(
this.toastService.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.notificationService.showInfo(
this.toastService.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.notificationService.showError(
this.toastService.showError(
$localize`Error toggling workflow "${workflow.name}".`,
e
)

@ -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 notificationService: NotificationService
let toastService: ToastService
beforeEach(() => {
TestBed.configureTestingModule({
@ -44,13 +44,13 @@ describe('PermissionsGuard', () => {
},
},
TourService,
NotificationService,
ToastService,
],
})
permissionsService = TestBed.inject(PermissionsService)
tourService = TestBed.inject(TourService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
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 notificationSpy = jest.spyOn(notificationService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
const canActivate = guard.canActivate(route.snapshot, routerState.snapshot)
expect(canActivate).toHaveProperty('root') // returns UrlTree
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
})
})

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private tourService: TourService
) {}
@ -32,7 +32,7 @@ export class PermissionsGuard {
) {
// Check if tour is running 1 = TourState.ON
if (this.tourService.getStatus() !== 1) {
this.notificationService.showError(
this.toastService.showError(
$localize`You don't have permissions to do that`
)
}

@ -1,109 +0,0 @@
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')
})
})

@ -1,87 +0,0 @@
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<Notification[]> = new Subject()
public showNotification: Subject<Notification> = 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)
}
}

@ -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 notificationService: NotificationService
let toastService: ToastService
const ui_settings: UiSettings = {
user: {
@ -105,7 +105,7 @@ describe('SettingsService', () => {
customFieldsService = TestBed.inject(CustomFieldsService)
permissionService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
notificationService = TestBed.inject(NotificationService)
toastService = TestBed.inject(ToastService)
// 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 notification on retrieve ui_settings error', fakeAsync(() => {
const notificationSpy = jest.spyOn(notificationService, 'showError')
it('should catch error and show toast on retrieve ui_settings error', fakeAsync(() => {
const toastSpy = jest.spyOn(toastService, 'showError')
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush(
@ -131,7 +131,7 @@ describe('SettingsService', () => {
{ status: 403, statusText: 'Forbidden' }
)
tick(500)
expect(notificationSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
}))
it('calls ui_settings api endpoint with POST on store', () => {

@ -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 notificationService: NotificationService,
private toastService: ToastService,
private permissionsService: PermissionsService,
private customFieldsService: CustomFieldsService
) {
@ -307,7 +307,7 @@ export class SettingsService {
first(),
catchError((error) => {
setTimeout(() => {
this.notificationService.showError('Error loading settings', error)
this.toastService.showError('Error loading settings', error)
}, 500)
return of({
settings: {
@ -601,7 +601,7 @@ export class SettingsService {
this.cookieService.get(this.getLanguageCookieName())
)
} catch (error) {
this.notificationService.showError(errorMessage)
this.toastService.showError(errorMessage)
console.log(error)
}
@ -610,10 +610,10 @@ export class SettingsService {
.subscribe({
next: () => {
this.updateAppearanceSettings()
this.notificationService.showInfo(successMessage)
this.toastService.showInfo(successMessage)
},
error: (e) => {
this.notificationService.showError(errorMessage)
this.toastService.showError(errorMessage)
console.log(e)
},
})
@ -633,7 +633,7 @@ export class SettingsService {
.pipe(first())
.subscribe({
error: (e) => {
this.notificationService.showError(
this.toastService.showError(
'Error migrating update checking setting'
)
console.log(e)
@ -663,7 +663,7 @@ export class SettingsService {
this.storeSettings()
.pipe(first())
.subscribe(() => {
this.notificationService.showInfo(
this.toastService.showInfo(
$localize`You can restart the tour from the settings page.`
)
})

@ -0,0 +1,109 @@
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')
})
})

@ -0,0 +1,87 @@
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<Toast[]> = new Subject()
public showToast: Subject<Toast> = 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)
}
}

@ -571,7 +571,7 @@ table.table {
}
.toast {
--bs-toast-max-width: var(--pngx-notification-max-width);
--bs-toast-max-width: var(--pngx-toast-max-width);
}
.alert-primary {

@ -24,11 +24,11 @@
--pngx-bg-alt2: var(--bs-gray-200); // #e9ecef
--pngx-bg-disabled: #f7f7f7;
--pngx-focus-alpha: 0.3;
--pngx-notification-max-width: 360px;
--pngx-toast-max-width: 360px;
--bs-info: var(--pngx-bg-alt2);
--bs-info-rgb: 233, 236, 239;
@media screen and (min-width: 1024px) {
--pngx-notification-max-width: 450px;
--pngx-toast-max-width: 450px;
}
}