Very annoying refactor

This commit is contained in:
shamoon 2025-03-04 08:53:11 -08:00
parent 4488da6d3d
commit f28accb28f
No known key found for this signature in database
81 changed files with 1315 additions and 1151 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -14,14 +14,17 @@ import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { Subject } from 'rxjs'
import { routes } from './app-routing.module'
import { AppComponent } from './app.component'
import { ToastsComponent } from './components/common/toasts/toasts.component'
import { NotificationListComponent } from './components/common/notification-list/notification-list.component'
import { FileDropComponent } from './components/file-drop/file-drop.component'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { PermissionsGuard } from './guards/permissions.guard'
import { HotKeyService } from './services/hot-key.service'
import {
Notification,
NotificationService,
} from './services/notification.service'
import { PermissionsService } from './services/permissions.service'
import { SettingsService } from './services/settings.service'
import { Toast, ToastService } from './services/toast.service'
import {
FileStatus,
WebsocketStatusService,
@ -33,7 +36,7 @@ describe('AppComponent', () => {
let tourService: TourService
let websocketStatusService: WebsocketStatusService
let permissionsService: PermissionsService
let toastService: ToastService
let notificationService: NotificationService
let router: Router
let settingsService: SettingsService
let hotKeyService: HotKeyService
@ -46,7 +49,7 @@ describe('AppComponent', () => {
NgxFileDropModule,
NgbModalModule,
AppComponent,
ToastsComponent,
NotificationListComponent,
FileDropComponent,
NgxBootstrapIconsModule.pick(allIcons),
],
@ -62,7 +65,7 @@ describe('AppComponent', () => {
websocketStatusService = TestBed.inject(WebsocketStatusService)
permissionsService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
router = TestBed.inject(Router)
hotKeyService = TestBed.inject(HotKeyService)
fixture = TestBed.createComponent(AppComponent)
@ -82,12 +85,14 @@ describe('AppComponent', () => {
expect(document.body.classList).not.toContain('tour-active')
}))
it('should display toast on document consumed with link if user has access', () => {
it('should display notification on document consumed with link if user has access', () => {
const navigateSpy = jest.spyOn(router, 'navigate')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
let toast: Toast
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
const toastSpy = jest.spyOn(toastService, 'show')
let notification: Notification
notificationService
.getNotifications()
.subscribe((notifications) => (notification = notifications[0]))
const notificationSpy = jest.spyOn(notificationService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
@ -96,63 +101,65 @@ describe('AppComponent', () => {
const status = new FileStatus()
status.documentId = 1
fileStatusSubject.next(status)
expect(toastSpy).toHaveBeenCalled()
expect(toast.action).not.toBeUndefined()
toast.action()
expect(notificationSpy).toHaveBeenCalled()
expect(notification.action).not.toBeUndefined()
notification.action()
expect(navigateSpy).toHaveBeenCalledWith(['documents', status.documentId])
})
it('should display toast on document consumed without link if user does not have access', () => {
it('should display notification on document consumed without link if user does not have access', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
let toast: Toast
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
const toastSpy = jest.spyOn(toastService, 'show')
let notification: Notification
notificationService
.getNotifications()
.subscribe((notifications) => (notification = notifications[0]))
const notificationSpy = jest.spyOn(notificationService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled()
expect(toast.action).toBeUndefined()
expect(notificationSpy).toHaveBeenCalled()
expect(notification.action).toBeUndefined()
})
it('should display toast on document added', () => {
it('should display notification on document added', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'show')
const notificationSpy = jest.spyOn(notificationService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentDetected')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should suppress dashboard notifications if set', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest.spyOn(settingsService, 'get').mockReturnValue(true)
jest.spyOn(router, 'url', 'get').mockReturnValue('/dashboard')
const toastSpy = jest.spyOn(toastService, 'show')
const notificationSpy = jest.spyOn(notificationService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentDetected')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).not.toHaveBeenCalled()
expect(notificationSpy).not.toHaveBeenCalled()
})
it('should display toast on document failed', () => {
it('should display notification on document failed', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(websocketStatusService, 'onDocumentConsumptionFailed')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should support hotkeys', () => {

View File

@ -2,11 +2,12 @@ import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
import { Router, RouterOutlet } from '@angular/router'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { first, Subscription } from 'rxjs'
import { ToastsComponent } from './components/common/toasts/toasts.component'
import { NotificationListComponent } from './components/common/notification-list/notification-list.component'
import { FileDropComponent } from './components/file-drop/file-drop.component'
import { SETTINGS_KEYS } from './data/ui-settings'
import { ComponentRouterService } from './services/component-router.service'
import { HotKeyService } from './services/hot-key.service'
import { NotificationService } from './services/notification.service'
import {
PermissionAction,
PermissionsService,
@ -14,7 +15,6 @@ import {
} from './services/permissions.service'
import { SettingsService } from './services/settings.service'
import { TasksService } from './services/tasks.service'
import { ToastService } from './services/toast.service'
import { WebsocketStatusService } from './services/websocket-status.service'
@Component({
@ -23,7 +23,7 @@ import { WebsocketStatusService } from './services/websocket-status.service'
styleUrls: ['./app.component.scss'],
imports: [
FileDropComponent,
ToastsComponent,
NotificationListComponent,
TourNgBootstrapModule,
RouterOutlet,
],
@ -36,7 +36,7 @@ export class AppComponent implements OnInit, OnDestroy {
constructor(
private settings: SettingsService,
private websocketStatusService: WebsocketStatusService,
private toastService: ToastService,
private notificationService: NotificationService,
private router: Router,
private tasksService: TasksService,
public tourService: TourService,
@ -91,7 +91,7 @@ export class AppComponent implements OnInit, OnDestroy {
PermissionType.Document
)
) {
this.toastService.show({
this.notificationService.show({
content: $localize`Document ${status.filename} was added to Paperless-ngx.`,
delay: 10000,
actionName: $localize`Open document`,
@ -100,7 +100,7 @@ export class AppComponent implements OnInit, OnDestroy {
},
})
} else {
this.toastService.show({
this.notificationService.show({
content: $localize`Document ${status.filename} was added to Paperless-ngx.`,
delay: 10000,
})
@ -115,7 +115,7 @@ export class AppComponent implements OnInit, OnDestroy {
if (
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED)
) {
this.toastService.showError(
this.notificationService.showError(
$localize`Could not add ${status.filename}\: ${status.message}`
)
}
@ -130,7 +130,7 @@ export class AppComponent implements OnInit, OnDestroy {
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT
)
) {
this.toastService.show({
this.notificationService.show({
content: $localize`Document ${status.filename} is being processed by Paperless-ngx.`,
delay: 5000,
})

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
let settingService: SettingsService
beforeEach(async () => {
@ -51,7 +51,7 @@ describe('ConfigComponent', () => {
}).compileComponents()
configService = TestBed.inject(ConfigService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
settingService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(ConfigComponent)
component = fixture.componentInstance
@ -60,7 +60,7 @@ describe('ConfigComponent', () => {
it('should load config on init, show error if necessary', () => {
const getSpy = jest.spyOn(configService, 'getConfig')
const errorSpy = jest.spyOn(toastService, 'showError')
const errorSpy = jest.spyOn(notificationService, 'showError')
getSpy.mockReturnValueOnce(
throwError(() => new Error('Error getting config'))
)
@ -78,7 +78,7 @@ describe('ConfigComponent', () => {
it('should save config, show error if necessary', () => {
const saveSpy = jest.spyOn(configService, 'saveConfig')
const errorSpy = jest.spyOn(toastService, 'showError')
const errorSpy = jest.spyOn(notificationService, 'showError')
saveSpy.mockReturnValueOnce(
throwError(() => new Error('Error saving config'))
)
@ -112,7 +112,7 @@ describe('ConfigComponent', () => {
it('should upload file, show error if necessary', () => {
const uploadSpy = jest.spyOn(configService, 'uploadFile')
const errorSpy = jest.spyOn(toastService, 'showError')
const errorSpy = jest.spyOn(notificationService, 'showError')
uploadSpy.mockReturnValueOnce(
throwError(() => new Error('Error uploading file'))
)

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

View File

@ -29,12 +29,15 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import {
Notification,
NotificationService,
} from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CheckComponent } from '../../common/input/check/check.component'
@ -66,7 +69,7 @@ describe('SettingsComponent', () => {
let settingsService: SettingsService
let activatedRoute: ActivatedRoute
let viewportScroller: ViewportScroller
let toastService: ToastService
let notificationService: NotificationService
let userService: UserService
let permissionsService: PermissionsService
let groupService: GroupService
@ -115,7 +118,7 @@ describe('SettingsComponent', () => {
router = TestBed.inject(Router)
activatedRoute = TestBed.inject(ActivatedRoute)
viewportScroller = TestBed.inject(ViewportScroller)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = users[0]
userService = TestBed.inject(UserService)
@ -194,8 +197,8 @@ describe('SettingsComponent', () => {
it('should support save local settings updating appearance settings and calling API, show error', () => {
completeSetup()
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'show')
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
const appearanceSettingsSpy = jest.spyOn(
settingsService,
@ -209,7 +212,7 @@ describe('SettingsComponent', () => {
)
component.saveSettings()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(29)
@ -217,14 +220,14 @@ describe('SettingsComponent', () => {
// succeed
storeSpy.mockReturnValueOnce(of(true))
component.saveSettings()
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).toHaveBeenCalled()
})
it('should offer reload if settings changes require', () => {
completeSetup()
let toast: Toast
toastService.getToasts().subscribe((t) => (toast = t[0]))
let toast: Notification
notificationService.getNotifications().subscribe((t) => (toast = t[0]))
component.initialize(true) // reset
component.store.getValue()['displayLanguage'] = 'en-US'
component.store.getValue()['updateCheckingEnabled'] = false
@ -258,7 +261,7 @@ describe('SettingsComponent', () => {
})
it('should show errors on load if load users failure', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
jest
.spyOn(userService, 'listAll')
.mockImplementation(() =>
@ -266,11 +269,11 @@ describe('SettingsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
})
it('should show errors on load if load groups failure', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
jest
.spyOn(groupService, 'listAll')
.mockImplementation(() =>
@ -278,7 +281,7 @@ describe('SettingsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
})
it('should load system status on initialize, show errors if needed', () => {

View File

@ -43,6 +43,10 @@ import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import {
Notification,
NotificationService,
} from 'src/app/services/notification.service'
import {
PermissionAction,
PermissionType,
@ -55,7 +59,6 @@ import {
SettingsService,
} from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { CheckComponent } from '../../common/input/check/check.component'
import { ColorComponent } from '../../common/input/color/color.component'
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
@ -181,7 +184,7 @@ export class SettingsComponent
constructor(
private documentListViewService: DocumentListViewService,
private toastService: ToastService,
private notificationService: NotificationService,
private settings: SettingsService,
@Inject(LOCALE_ID) public currentLocale: string,
private viewportScroller: ViewportScroller,
@ -217,7 +220,10 @@ export class SettingsComponent
this.users = r.results
},
error: (e) => {
this.toastService.showError($localize`Error retrieving users`, e)
this.notificationService.showError(
$localize`Error retrieving users`,
e
)
},
})
}
@ -236,7 +242,10 @@ export class SettingsComponent
this.groups = r.results
},
error: (e) => {
this.toastService.showError($localize`Error retrieving groups`, e)
this.notificationService.showError(
$localize`Error retrieving groups`,
e
)
},
})
}
@ -531,7 +540,7 @@ export class SettingsComponent
this.store.next(this.settingsForm.value)
this.settings.updateAppearanceSettings()
this.settings.initializeDisplayFields()
let savedToast: Toast = {
let savedToast: Notification = {
content: $localize`Settings were saved successfully.`,
delay: 5000,
}
@ -543,10 +552,10 @@ export class SettingsComponent
}
}
this.toastService.show(savedToast)
this.notificationService.show(savedToast)
},
error: (error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`An error occurred while saving settings.`,
error
)

View File

@ -12,7 +12,7 @@ import {
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ToastService } from 'src/app/services/toast.service'
import { NotificationService } from 'src/app/services/notification.service'
import { TrashService } from 'src/app/services/trash.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
@ -38,7 +38,7 @@ describe('TrashComponent', () => {
let fixture: ComponentFixture<TrashComponent>
let trashService: TrashService
let modalService: NgbModal
let toastService: ToastService
let notificationService: NotificationService
let router: Router
beforeEach(async () => {
@ -60,7 +60,7 @@ describe('TrashComponent', () => {
fixture = TestBed.createComponent(TrashComponent)
trashService = TestBed.inject(TrashService)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
router = TestBed.inject(Router)
component = fixture.componentInstance
fixture.detectChanges()
@ -88,13 +88,13 @@ describe('TrashComponent', () => {
modalService.activeInstances.subscribe((instances) => {
modal = instances[0]
})
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
// fail first
trashSpy.mockReturnValue(throwError(() => 'Error'))
component.delete(documentsInTrash[0])
modal.componentInstance.confirmClicked.next()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
trashSpy.mockReturnValue(of('OK'))
component.delete(documentsInTrash[0])
@ -109,13 +109,13 @@ describe('TrashComponent', () => {
modalService.activeInstances.subscribe((instances) => {
modal = instances[instances.length - 1]
})
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
// fail first
trashSpy.mockReturnValue(throwError(() => 'Error'))
component.emptyTrash()
modal.componentInstance.confirmClicked.next()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
trashSpy.mockReturnValue(of('OK'))
component.emptyTrash()
@ -131,12 +131,12 @@ describe('TrashComponent', () => {
it('should support restore document, show error if needed', () => {
const restoreSpy = jest.spyOn(trashService, 'restoreDocuments')
const reloadSpy = jest.spyOn(component, 'reload')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
// fail first
restoreSpy.mockReturnValue(throwError(() => 'Error'))
component.restore(documentsInTrash[0])
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
restoreSpy.mockReturnValue(of('OK'))
@ -148,12 +148,12 @@ describe('TrashComponent', () => {
it('should support restore all documents, show error if needed', () => {
const restoreSpy = jest.spyOn(trashService, 'restoreDocuments')
const reloadSpy = jest.spyOn(component, 'reload')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
// fail first
restoreSpy.mockReturnValue(throwError(() => 'Error'))
component.restoreAll()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
restoreSpy.mockReturnValue(of('OK'))
@ -167,7 +167,7 @@ describe('TrashComponent', () => {
it('should offer link to restored document', () => {
let toasts
const navigateSpy = jest.spyOn(router, 'navigate')
toastService.getToasts().subscribe((allToasts) => {
notificationService.getNotifications().subscribe((allToasts) => {
toasts = [...allToasts]
})
jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK'))

View File

@ -10,8 +10,8 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, takeUntil, tap } from 'rxjs'
import { Document } from 'src/app/data/document'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { NotificationService } from 'src/app/services/notification.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
@ -44,7 +44,7 @@ export class TrashComponent
constructor(
private trashService: TrashService,
private toastService: ToastService,
private notificationService: NotificationService,
private modalService: NgbModal,
private settingsService: SettingsService,
private router: Router
@ -86,14 +86,14 @@ export class TrashComponent
modal.componentInstance.buttonsEnabled = false
this.trashService.emptyTrash([document.id]).subscribe({
next: () => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Document "${document.title}" deleted`
)
modal.close()
this.reload()
},
error: (err) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting document "${document.title}"`,
err
)
@ -121,13 +121,13 @@ export class TrashComponent
.emptyTrash(documents ? Array.from(documents) : null)
.subscribe({
next: () => {
this.toastService.showInfo($localize`Document(s) deleted`)
this.notificationService.showInfo($localize`Document(s) deleted`)
this.allToggled = false
modal.close()
this.reload()
},
error: (err) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting document(s)`,
err
)
@ -140,7 +140,7 @@ export class TrashComponent
restore(document: Document) {
this.trashService.restoreDocuments([document.id]).subscribe({
next: () => {
this.toastService.show({
this.notificationService.show({
content: $localize`Document "${document.title}" restored`,
delay: 5000,
actionName: $localize`Open document`,
@ -151,7 +151,7 @@ export class TrashComponent
this.reload()
},
error: (err) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error restoring document "${document.title}"`,
err
)
@ -164,12 +164,12 @@ export class TrashComponent
.restoreDocuments(documents ? Array.from(documents) : null)
.subscribe({
next: () => {
this.toastService.showInfo($localize`Document(s) restored`)
this.notificationService.showInfo($localize`Document(s) restored`)
this.allToggled = false
this.reload()
},
error: (err) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error restoring document(s)`,
err
)

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
let userService: UserService
let permissionsService: PermissionsService
let groupService: GroupService
@ -59,7 +59,7 @@ describe('UsersAndGroupsComponent', () => {
settingsService.currentUser = users[0]
userService = TestBed.inject(UserService)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
permissionsService = TestBed.inject(PermissionsService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
@ -104,13 +104,13 @@ describe('UsersAndGroupsComponent', () => {
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editUser(users[0])
const editDialog = modal.componentInstance as UserEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
settingsService.currentUser = users[1] // simulate logged in as different user
editDialog.succeeded.emit(users[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
expect(notificationInfoSpy).toHaveBeenCalledWith(
`Saved user "${users[0].username}".`
)
component.editUser()
@ -123,18 +123,18 @@ describe('UsersAndGroupsComponent', () => {
component.deleteUser(users[0])
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(userService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const listAllSpy = jest.spyOn(userService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting user'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user "user1"')
expect(notificationInfoSpy).toHaveBeenCalledWith('Deleted user "user1"')
})
it('should logout current user if password changed, after delay', fakeAsync(() => {
@ -163,12 +163,12 @@ describe('UsersAndGroupsComponent', () => {
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editGroup(groups[0])
const editDialog = modal.componentInstance as GroupEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
editDialog.succeeded.emit(groups[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
expect(notificationInfoSpy).toHaveBeenCalledWith(
`Saved group "${groups[0].name}".`
)
component.editGroup()
@ -181,18 +181,18 @@ describe('UsersAndGroupsComponent', () => {
component.deleteGroup(groups[0])
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(groupService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const listAllSpy = jest.spyOn(groupService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting group'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group "group1"')
expect(notificationInfoSpy).toHaveBeenCalledWith('Deleted group "group1"')
})
it('should get group name', () => {
@ -202,7 +202,7 @@ describe('UsersAndGroupsComponent', () => {
})
it('should show errors on load if load users failure', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
jest
.spyOn(userService, 'listAll')
.mockImplementation(() =>
@ -210,11 +210,11 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
})
it('should show errors on load if load groups failure', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
jest
.spyOn(groupService, 'listAll')
.mockImplementation(() =>
@ -222,6 +222,6 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(notificationErrorSpy).toBeCalled()
})
})

View File

@ -5,11 +5,11 @@ import { Subject, first, takeUntil } from 'rxjs'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
@ -39,7 +39,7 @@ export class UsersAndGroupsComponent
constructor(
private usersService: UserService,
private groupsService: GroupService,
private toastService: ToastService,
private notificationService: NotificationService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private settings: SettingsService
@ -56,7 +56,10 @@ export class UsersAndGroupsComponent
this.users = r.results
},
error: (e) => {
this.toastService.showError($localize`Error retrieving users`, e)
this.notificationService.showError(
$localize`Error retrieving users`,
e
)
},
})
@ -68,7 +71,10 @@ export class UsersAndGroupsComponent
this.groups = r.results
},
error: (e) => {
this.toastService.showError($localize`Error retrieving groups`, e)
this.notificationService.showError(
$localize`Error retrieving groups`,
e
)
},
})
}
@ -93,14 +99,14 @@ export class UsersAndGroupsComponent
newUser.id === this.settings.currentUser.id &&
(modal.componentInstance as UserEditDialogComponent).passwordIsSet
) {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
}, 2500)
} else {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Saved user "${newUser.username}".`
)
this.usersService.listAll().subscribe((r) => {
@ -111,7 +117,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving user.`, e)
this.notificationService.showError($localize`Error saving user.`, e)
})
}
@ -129,13 +135,15 @@ export class UsersAndGroupsComponent
this.usersService.delete(user).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted user "${user.username}"`)
this.notificationService.showInfo(
$localize`Deleted user "${user.username}"`
)
this.usersService.listAll().subscribe((r) => {
this.users = r.results
})
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting user "${user.username}".`,
e
)
@ -156,7 +164,9 @@ export class UsersAndGroupsComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newGroup) => {
this.toastService.showInfo($localize`Saved group "${newGroup.name}".`)
this.notificationService.showInfo(
$localize`Saved group "${newGroup.name}".`
)
this.groupsService.listAll().subscribe((r) => {
this.groups = r.results
})
@ -164,7 +174,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving group.`, e)
this.notificationService.showError($localize`Error saving group.`, e)
})
}
@ -182,13 +192,15 @@ export class UsersAndGroupsComponent
this.groupsService.delete(group).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted group "${group.name}"`)
this.notificationService.showInfo(
$localize`Deleted group "${group.name}"`
)
this.groupsService.listAll().subscribe((r) => {
this.groups = r.results
})
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting group "${group.name}".`,
e
)

View File

@ -30,7 +30,7 @@
</div>
</div>
<ul ngbNav class="order-sm-3">
<pngx-toasts-dropdown></pngx-toasts-dropdown>
<pngx-notifications-dropdown></pngx-notifications-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>

View File

@ -26,13 +26,13 @@ import {
DjangoMessageLevel,
DjangoMessagesService,
} from 'src/app/services/django-messages.service'
import { NotificationService } from 'src/app/services/notification.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
@ -86,7 +86,7 @@ describe('AppFrameComponent', () => {
let settingsService: SettingsService
let permissionsService: PermissionsService
let remoteVersionService: RemoteVersionService
let toastService: ToastService
let notificationService: NotificationService
let messagesService: DjangoMessagesService
let openDocumentsService: OpenDocumentsService
let router: Router
@ -126,7 +126,7 @@ describe('AppFrameComponent', () => {
PermissionsService,
RemoteVersionService,
IfPermissionsDirective,
ToastService,
NotificationService,
DjangoMessagesService,
OpenDocumentsService,
SearchService,
@ -157,7 +157,7 @@ describe('AppFrameComponent', () => {
const savedViewService = TestBed.inject(SavedViewService)
permissionsService = TestBed.inject(PermissionsService)
remoteVersionService = TestBed.inject(RemoteVersionService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
messagesService = TestBed.inject(DjangoMessagesService)
openDocumentsService = TestBed.inject(OpenDocumentsService)
modalService = TestBed.inject(NgbModal)
@ -216,7 +216,7 @@ describe('AppFrameComponent', () => {
it('should show error on toggle update checking if store settings fails', () => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
component.setUpdateChecking(true)
httpTestingController
@ -225,7 +225,7 @@ describe('AppFrameComponent', () => {
status: 500,
statusText: 'error',
})
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should support toggling slim sidebar and saving', fakeAsync(() => {
@ -245,7 +245,7 @@ describe('AppFrameComponent', () => {
it('should show error on toggle slim sidebar if store settings fails', () => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
component.toggleSlimSidebar()
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
@ -253,7 +253,7 @@ describe('AppFrameComponent', () => {
status: 500,
statusText: 'error',
})
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should support collapsible menu', () => {
@ -305,7 +305,7 @@ describe('AppFrameComponent', () => {
it('should update saved view sorting on drag + drop, show info', () => {
const settingsSpy = jest.spyOn(settingsService, 'updateSidebarViewsSort')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
SavedView[]
@ -315,7 +315,7 @@ describe('AppFrameComponent', () => {
saved_views[0],
saved_views[3],
])
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should update saved view sorting on drag + drop, show error', () => {
@ -326,14 +326,14 @@ describe('AppFrameComponent', () => {
fixture = TestBed.createComponent(AppFrameComponent)
component = fixture.componentInstance
fixture.detectChanges()
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
SavedView[]
>)
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should support edit profile', () => {
@ -345,9 +345,9 @@ describe('AppFrameComponent', () => {
})
})
it('should show toasts for django messages', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
it('should show notifications for django messages', () => {
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
jest.spyOn(messagesService, 'get').mockReturnValue([
{ level: DjangoMessageLevel.WARNING, message: 'Test warning' },
{ level: DjangoMessageLevel.ERROR, message: 'Test error' },
@ -356,7 +356,7 @@ describe('AppFrameComponent', () => {
{ level: DjangoMessageLevel.DEBUG, message: 'Test debug' },
])
component.ngOnInit()
expect(toastErrorSpy).toHaveBeenCalledTimes(2)
expect(toastInfoSpy).toHaveBeenCalledTimes(3)
expect(notificationErrorSpy).toHaveBeenCalledTimes(2)
expect(notificationInfoSpy).toHaveBeenCalledTimes(3)
})
})

View File

@ -29,6 +29,7 @@ import {
DjangoMessageLevel,
DjangoMessagesService,
} from 'src/app/services/django-messages.service'
import { NotificationService } from 'src/app/services/notification.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import {
PermissionAction,
@ -42,13 +43,12 @@ import {
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { GlobalSearchComponent } from './global-search/global-search.component'
import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component'
import { NotificationsDropdownComponent } from './notifications-dropdown/notifications-dropdown.component'
@Component({
selector: 'pngx-app-frame',
@ -58,7 +58,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo
GlobalSearchComponent,
DocumentTitlePipe,
IfPermissionsDirective,
ToastsDropdownComponent,
NotificationsDropdownComponent,
RouterModule,
NgClass,
NgbDropdownModule,
@ -89,7 +89,7 @@ export class AppFrameComponent
private remoteVersionService: RemoteVersionService,
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly toastService: ToastService,
private readonly notificationService: NotificationService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private djangoMessagesService: DjangoMessagesService
@ -123,12 +123,12 @@ export class AppFrameComponent
switch (message.level) {
case DjangoMessageLevel.ERROR:
case DjangoMessageLevel.WARNING:
this.toastService.showError(message.message)
this.notificationService.showError(message.message)
break
case DjangoMessageLevel.SUCCESS:
case DjangoMessageLevel.INFO:
case DjangoMessageLevel.DEBUG:
this.toastService.showInfo(message.message)
this.notificationService.showInfo(message.message)
break
}
})
@ -157,7 +157,7 @@ export class AppFrameComponent
.pipe(first())
.subscribe({
error: (error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`An error occurred while saving settings.`
)
console.warn(error)
@ -242,10 +242,13 @@ export class AppFrameComponent
this.settingsService.updateSidebarViewsSort(sidebarViews).subscribe({
next: () => {
this.toastService.showInfo($localize`Sidebar views updated`)
this.notificationService.showInfo($localize`Sidebar views updated`)
},
error: (e) => {
this.toastService.showError($localize`Error updating sidebar views`, e)
this.notificationService.showError(
$localize`Error updating sidebar views`,
e
)
},
})
}
@ -265,7 +268,7 @@ export class AppFrameComponent
.pipe(first())
.subscribe({
error: (error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`An error occurred while saving update checking settings.`
)
console.warn(error)

View File

@ -28,10 +28,10 @@ import {
} from 'src/app/data/filter-rule-type'
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NotificationService } from 'src/app/services/notification.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SearchService } from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@ -133,7 +133,7 @@ describe('GlobalSearchComponent', () => {
let modalService: NgbModal
let documentService: DocumentService
let documentListViewService: DocumentListViewService
let toastService: ToastService
let notificationService: NotificationService
let settingsService: SettingsService
beforeEach(async () => {
@ -157,7 +157,7 @@ describe('GlobalSearchComponent', () => {
modalService = TestBed.inject(NgbModal)
documentService = TestBed.inject(DocumentService)
documentListViewService = TestBed.inject(DocumentListViewService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
settingsService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(GlobalSearchComponent)
@ -397,16 +397,16 @@ describe('GlobalSearchComponent', () => {
})
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(true)
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
})
it('should support secondary action', () => {
@ -448,16 +448,16 @@ describe('GlobalSearchComponent', () => {
})
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(true)
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
})
it('should support reset', () => {

View File

@ -31,6 +31,7 @@ import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { HotKeyService } from 'src/app/services/hot-key.service'
import { NotificationService } from 'src/app/services/notification.service'
import {
PermissionAction,
PermissionsService,
@ -41,7 +42,6 @@ import {
SearchService,
} from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { paramsFromViewState } from 'src/app/utils/query-params'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@ -97,7 +97,7 @@ export class GlobalSearchComponent implements OnInit {
private documentService: DocumentService,
private documentListViewService: DocumentListViewService,
private permissionsService: PermissionsService,
private toastService: ToastService,
private notificationService: NotificationService,
private hotkeyService: HotKeyService,
private settingsService: SettingsService
) {
@ -206,10 +206,15 @@ export class GlobalSearchComponent implements OnInit {
modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
modalRef.componentInstance.object = object
modalRef.componentInstance.succeeded.subscribe(() => {
this.toastService.showInfo($localize`Successfully updated object.`)
this.notificationService.showInfo(
$localize`Successfully updated object.`
)
})
modalRef.componentInstance.failed.subscribe((e) => {
this.toastService.showError($localize`Error occurred saving object.`, e)
this.notificationService.showError(
$localize`Error occurred saving object.`,
e
)
})
}
}
@ -244,10 +249,15 @@ export class GlobalSearchComponent implements OnInit {
modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
modalRef.componentInstance.object = object
modalRef.componentInstance.succeeded.subscribe(() => {
this.toastService.showInfo($localize`Successfully updated object.`)
this.notificationService.showInfo(
$localize`Successfully updated object.`
)
})
modalRef.componentInstance.failed.subscribe((e) => {
this.toastService.showError($localize`Error occurred saving object.`, e)
this.notificationService.showError(
$localize`Error occurred saving object.`,
e
)
})
}
}

View File

@ -1,7 +1,7 @@
<li ngbDropdown class="nav-item" (openChange)="onOpenChange($event)">
@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>
@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>
}
<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)="toastService.clearToasts()"
[disabled]="toasts.length === 0"
(click)="notificationService.clearNotifications()"
[disabled]="notifications.length === 0"
i18n>Clear All</button>
</div>
</div>
@if (toasts.length === 0) {
@if (notifications.length === 0) {
<p class="text-center mb-0 small text-muted"><em i18n>No notifications</em></p>
}
<div class="scroll-list">
@for (toast of toasts; track toast.id) {
<pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (close)="toastService.closeToast(toast)"></pngx-toast>
@for (notification of notifications; track notification.id) {
<pngx-notification [autohide]="false" [notification]="notification" (hidden)="onHidden(notification)" (close)="notificationService.closeNotification(notification)"></pngx-notification>
}
</div>
</div>

View File

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

View File

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

View File

@ -0,0 +1,47 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import {
NgbDropdownModule,
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subscription } from 'rxjs'
import {
Notification,
NotificationService,
} from 'src/app/services/notification.service'
import { NotificationComponent } from '../../common/notification/notification.component'
@Component({
selector: 'pngx-notifications-dropdown',
templateUrl: './notifications-dropdown.component.html',
styleUrls: ['./notifications-dropdown.component.scss'],
imports: [
NotificationComponent,
NgbDropdownModule,
NgbProgressbarModule,
NgxBootstrapIconsModule,
],
})
export class NotificationsDropdownComponent implements OnInit, OnDestroy {
constructor(public notificationService: NotificationService) {}
private subscription: Subscription
public notifications: Notification[] = []
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
ngOnInit(): void {
this.subscription = this.notificationService
.getNotifications()
.subscribe((notifications) => {
this.notifications = [...notifications]
})
}
onOpenChange(open: boolean): void {
this.notificationService.suppressPopupNotifications = open
}
}

View File

@ -1,42 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import {
NgbDropdownModule,
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subscription } from 'rxjs'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { ToastComponent } from '../../common/toast/toast.component'
@Component({
selector: 'pngx-toasts-dropdown',
templateUrl: './toasts-dropdown.component.html',
styleUrls: ['./toasts-dropdown.component.scss'],
imports: [
ToastComponent,
NgbDropdownModule,
NgbProgressbarModule,
NgxBootstrapIconsModule,
],
})
export class ToastsDropdownComponent implements OnInit, OnDestroy {
constructor(public toastService: ToastService) {}
private subscription: Subscription
public toasts: Toast[] = []
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
ngOnInit(): void {
this.subscription = this.toastService.getToasts().subscribe((toasts) => {
this.toasts = [...toasts]
})
}
onOpenChange(open: boolean): void {
this.toastService.suppressPopupToasts = open
}
}

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
let modalService: NgbModal
let settingsService: SettingsService
@ -64,7 +64,7 @@ describe('CustomFieldsDropdownComponent', () => {
],
})
customFieldService = TestBed.inject(CustomFieldsService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
modalService = TestBed.inject(NgbModal)
jest.spyOn(customFieldService, 'listAll').mockReturnValue(
of({
@ -113,8 +113,8 @@ describe('CustomFieldsDropdownComponent', () => {
it('should support creating field, show error if necessary, then add', fakeAsync(() => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const getFieldsSpy = jest.spyOn(
CustomFieldsDropdownComponent.prototype as any,
'getFields'
@ -129,13 +129,13 @@ describe('CustomFieldsDropdownComponent', () => {
// fail first
editDialog.failed.emit({ error: 'error creating field' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(getFieldsSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
tick(100)
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(getFieldsSpy).toHaveBeenCalled()
expect(addFieldSpy).toHaveBeenCalled()
}))

View File

@ -14,13 +14,13 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first, takeUntil } from 'rxjs'
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
import { NotificationService } from 'src/app/services/notification.service'
import {
PermissionAction,
PermissionType,
PermissionsService,
} from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@ -78,7 +78,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
constructor(
private customFieldsService: CustomFieldsService,
private modalService: NgbModal,
private toastService: ToastService,
private notificationService: NotificationService,
private permissionsService: PermissionsService
) {
super()
@ -123,7 +123,9 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newField) => {
this.toastService.showInfo($localize`Saved field "${newField.name}".`)
this.notificationService.showInfo(
$localize`Saved field "${newField.name}".`
)
this.customFieldsService.clearCache()
this.getFields()
this.created.emit(newField)
@ -132,7 +134,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving field.`, e)
this.notificationService.showError($localize`Error saving field.`, e)
})
}

View File

@ -12,11 +12,11 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { PasswordComponent } from '../../input/password/password.component'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { SelectComponent } from '../../input/select/select.component'
@ -29,7 +29,7 @@ describe('UserEditDialogComponent', () => {
let component: UserEditDialogComponent
let settingsService: SettingsService
let permissionsService: PermissionsService
let toastService: ToastService
let notificationService: NotificationService
let fixture: ComponentFixture<UserEditDialogComponent>
beforeEach(async () => {
@ -75,7 +75,7 @@ describe('UserEditDialogComponent', () => {
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
permissionsService = TestBed.inject(PermissionsService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
component = fixture.componentInstance
fixture.detectChanges()
@ -133,22 +133,22 @@ describe('UserEditDialogComponent', () => {
component['service'] as UserService,
'deactivateTotp'
)
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
deactivateSpy.mockReturnValueOnce(throwError(() => new Error('error')))
component.deactivateTotp()
expect(deactivateSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
deactivateSpy.mockReturnValueOnce(of(false))
component.deactivateTotp()
expect(deactivateSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
deactivateSpy.mockReturnValueOnce(of(true))
component.deactivateTotp()
expect(deactivateSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
})
it('should check superuser status of current user', () => {

View File

@ -10,11 +10,11 @@ import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { PasswordComponent } from '../../input/password/password.component'
import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component'
@ -46,7 +46,7 @@ export class UserEditDialogComponent
activeModal: NgbActiveModal,
groupsService: GroupService,
settingsService: SettingsService,
private toastService: ToastService,
private notificationService: NotificationService,
private permissionsService: PermissionsService
) {
super(service, activeModal, service, settingsService)
@ -128,15 +128,20 @@ export class UserEditDialogComponent
next: (result) => {
this.totpLoading = false
if (result) {
this.toastService.showInfo($localize`Totp deactivated`)
this.notificationService.showInfo($localize`Totp deactivated`)
this.object.is_mfa_enabled = false
} else {
this.toastService.showError($localize`Totp deactivation failed`)
this.notificationService.showError(
$localize`Totp deactivation failed`
)
}
},
error: (e) => {
this.totpLoading = false
this.toastService.showError($localize`Totp deactivation failed`, e)
this.notificationService.showError(
$localize`Totp deactivation failed`,
e
)
},
})
}

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -34,7 +34,7 @@ describe('EmailDocumentDialogComponent', () => {
fixture = TestBed.createComponent(EmailDocumentDialogComponent)
documentService = TestBed.inject(DocumentService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
component = fixture.componentInstance
fixture.detectChanges()
})
@ -47,8 +47,8 @@ describe('EmailDocumentDialogComponent', () => {
})
it('should support sending document via email, showing error if needed', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSuccessSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationSuccessSpy = jest.spyOn(notificationService, 'showInfo')
component.emailAddress = 'hello@paperless-ngx.com'
component.emailSubject = 'Hello'
component.emailMessage = 'World'
@ -56,11 +56,11 @@ describe('EmailDocumentDialogComponent', () => {
.spyOn(documentService, 'emailDocument')
.mockReturnValue(throwError(() => new Error('Unable to email document')))
component.emailDocument()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
jest.spyOn(documentService, 'emailDocument').mockReturnValue(of(true))
component.emailDocument()
expect(toastSuccessSpy).toHaveBeenCalled()
expect(notificationSuccessSpy).toHaveBeenCalled()
})
it('should close the dialog', () => {

View File

@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { NotificationService } from 'src/app/services/notification.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ToastService } from 'src/app/services/toast.service'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
@ -40,7 +40,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
constructor(
private activeModal: NgbActiveModal,
private documentService: DocumentService,
private toastService: ToastService
private notificationService: NotificationService
) {
super()
this.loading = false
@ -62,11 +62,14 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
this.emailAddress = ''
this.emailSubject = ''
this.emailMessage = ''
this.toastService.showInfo($localize`Email sent`)
this.notificationService.showInfo($localize`Email sent`)
},
error: (e) => {
this.loading = false
this.toastService.showError($localize`Error emailing document`, e)
this.notificationService.showError(
$localize`Error emailing document`,
e
)
},
})
}

View File

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

View File

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

View File

@ -0,0 +1,84 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import {
Notification,
NotificationService,
} from 'src/app/services/notification.service'
import { NotificationListComponent } from './notification-list.component'
const notification = {
content: 'Error 2 content',
delay: 5000,
error: {
url: 'https://example.com',
status: 500,
statusText: 'Internal Server Error',
message: 'Internal server error 500 message',
error: { detail: 'Error 2 message details' },
},
}
describe('NotificationListComponent', () => {
let component: NotificationListComponent
let fixture: ComponentFixture<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])
})
})

View File

@ -0,0 +1,48 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import {
NgbAccordionModule,
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subscription } from 'rxjs'
import {
Notification,
NotificationService,
} from 'src/app/services/notification.service'
import { NotificationComponent } from '../notification/notification.component'
@Component({
selector: 'pngx-notification-list',
templateUrl: './notification-list.component.html',
styleUrls: ['./notification-list.component.scss'],
imports: [
NotificationComponent,
NgbAccordionModule,
NgbProgressbarModule,
NgxBootstrapIconsModule,
],
})
export class NotificationListComponent implements OnInit, OnDestroy {
constructor(public notificationService: NotificationService) {}
private subscription: Subscription
public notifications: Notification[] = [] // array to force change detection
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
ngOnInit(): void {
this.subscription = this.notificationService.showNotification.subscribe(
(notification) => {
this.notifications = notification ? [notification] : []
}
)
}
closeNotification() {
this.notificationService.closeNotification(this.notifications[0])
this.notifications = []
}
}

View File

@ -1,39 +1,39 @@
<ngb-toast
[autohide]="autohide"
[delay]="toast.delay"
[class]="toast.classname"
[delay]="notification.delay"
[class]="notification.classname"
[class.mb-2]="true"
(shown)="onShown(toast)"
(hidden)="hidden.emit(toast)">
(shown)="onShown(notification)"
(hidden)="hidden.emit(notification)">
@if (autohide) {
<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>
<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>
}
<div class="d-flex align-items-top">
@if (!toast.error) {
@if (!notification.error) {
<i-bs width="0.9em" height="0.9em" name="info-circle"></i-bs>
}
@if (toast.error) {
@if (notification.error) {
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
}
<div>
<p class="ms-2 mb-0">{{toast.content}}</p>
@if (toast.error) {
<p class="ms-2 mb-0">{{notification.content}}</p>
@if (notification.error) {
<details class="ms-2">
<div class="mt-2 ms-n4 me-n2 small">
@if (isDetailedError(toast.error)) {
@if (isDetailedError(notification.error)) {
<dl class="row mb-0">
<dt class="col-sm-3 fw-normal text-end">URL</dt>
<dd class="col-sm-9">{{ toast.error.url }}</dd>
<dd class="col-sm-9">{{ notification.error.url }}</dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
<dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd>
<dd class="col-sm-9">{{ notification.error.status }} <em>{{ notification.error.statusText }}</em></dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
<dd class="col-sm-9">{{ getErrorText(notification.error) }}</dd>
</dl>
}
<div class="row">
<div class="col offset-sm-3">
<button class="btn btn-sm btn-outline-secondary" (click)="copyError(toast.error)">
<button class="btn btn-sm btn-outline-secondary" (click)="copyError(notification.error)">
@if (!copied) {
<i-bs name="clipboard"></i-bs>&nbsp;
}
@ -47,10 +47,10 @@
</div>
</details>
}
@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>
@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>
}
</div>
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="close.emit(toast);"></button>
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="notification" aria-label="Close" (click)="close.emit(notification);"></button>
</div>
</ngb-toast>

View File

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

View File

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

View File

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

View File

@ -19,8 +19,8 @@ import {
TotpSettings,
} from 'src/app/data/user-profile'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { NotificationService } from 'src/app/services/notification.service'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
import { PasswordComponent } from '../input/password/password.component'
@ -86,7 +86,7 @@ export class ProfileEditDialogComponent
constructor(
private profileService: ProfileService,
public activeModal: NgbActiveModal,
private toastService: ToastService,
private notificationService: NotificationService,
private clipboard: Clipboard
) {
super()
@ -192,9 +192,11 @@ export class ProfileEditDialogComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.showInfo($localize`Profile updated successfully`)
this.notificationService.showInfo(
$localize`Profile updated successfully`
)
if (passwordChanged) {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
@ -204,7 +206,10 @@ export class ProfileEditDialogComponent
this.activeModal.close()
},
error: (error) => {
this.toastService.showError($localize`Error saving profile`, error)
this.notificationService.showError(
$localize`Error saving profile`,
error
)
this.networkActive = false
},
})
@ -220,7 +225,7 @@ export class ProfileEditDialogComponent
this.form.patchValue({ auth_token: token })
},
error: (error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error generating auth token`,
error
)
@ -245,7 +250,7 @@ export class ProfileEditDialogComponent
this.socialAccounts = this.socialAccounts.filter((a) => a.id != id)
},
error: (error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error disconnecting social account`,
error
)
@ -264,7 +269,7 @@ export class ProfileEditDialogComponent
this.totpSettings = totpSettings
},
error: (error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error fetching TOTP settings`,
error
)
@ -286,15 +291,20 @@ export class ProfileEditDialogComponent
this.recoveryCodes = activationResponse.recovery_codes
this.form.get('totp_code').enable()
if (activationResponse.success) {
this.toastService.showInfo($localize`TOTP activated successfully`)
this.notificationService.showInfo(
$localize`TOTP activated successfully`
)
} else {
this.toastService.showError($localize`Error activating TOTP`)
this.notificationService.showError($localize`Error activating TOTP`)
}
},
error: (error) => {
this.totpLoading = false
this.form.get('totp_code').enable()
this.toastService.showError($localize`Error activating TOTP`, error)
this.notificationService.showError(
$localize`Error activating TOTP`,
error
)
},
})
}
@ -310,14 +320,21 @@ export class ProfileEditDialogComponent
this.isTotpEnabled = !success
this.recoveryCodes = null
if (success) {
this.toastService.showInfo($localize`TOTP deactivated successfully`)
this.notificationService.showInfo(
$localize`TOTP deactivated successfully`
)
} else {
this.toastService.showError($localize`Error deactivating TOTP`)
this.notificationService.showError(
$localize`Error deactivating TOTP`
)
}
},
error: (error) => {
this.totpLoading = false
this.toastService.showError($localize`Error deactivating TOTP`, error)
this.notificationService.showError(
$localize`Error deactivating TOTP`,
error
)
},
})
}

View File

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

View File

@ -5,8 +5,8 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first } from 'rxjs'
import { FileVersion, ShareLink } from 'src/app/data/share-link'
import { NotificationService } from 'src/app/services/notification.service'
import { ShareLinkService } from 'src/app/services/rest/share-link.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
@Component({
@ -61,7 +61,7 @@ export class ShareLinksDialogComponent implements OnInit {
constructor(
private activeModal: NgbActiveModal,
private shareLinkService: ShareLinkService,
private toastService: ToastService,
private notificationService: NotificationService,
private clipboard: Clipboard
) {}
@ -81,7 +81,7 @@ export class ShareLinksDialogComponent implements OnInit {
this.shareLinks = results
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error retrieving links`,
10000,
e
@ -130,7 +130,11 @@ export class ShareLinksDialogComponent implements OnInit {
this.refresh()
},
error: (e) => {
this.toastService.showError($localize`Error deleting link`, 10000, e)
this.notificationService.showError(
$localize`Error deleting link`,
10000,
e
)
},
})
}
@ -158,7 +162,11 @@ export class ShareLinksDialogComponent implements OnInit {
},
error: (e) => {
this.loading = false
this.toastService.showError($localize`Error creating link`, 10000, e)
this.notificationService.showError(
$localize`Error creating link`,
10000,
e
)
},
})
}

View File

@ -16,9 +16,9 @@ import {
SystemStatus,
SystemStatusItemStatus,
} from 'src/app/data/system-status'
import { NotificationService } from 'src/app/services/notification.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
import { SystemStatusDialogComponent } from './system-status-dialog.component'
const status: SystemStatus = {
@ -61,7 +61,7 @@ describe('SystemStatusDialogComponent', () => {
let clipboard: Clipboard
let tasksService: TasksService
let systemStatusService: SystemStatusService
let toastService: ToastService
let notificationService: NotificationService
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -82,7 +82,7 @@ describe('SystemStatusDialogComponent', () => {
clipboard = TestBed.inject(Clipboard)
tasksService = TestBed.inject(TasksService)
systemStatusService = TestBed.inject(SystemStatusService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
fixture.detectChanges()
})
@ -116,9 +116,9 @@ describe('SystemStatusDialogComponent', () => {
expect(component.isRunning(PaperlessTaskName.SanityCheck)).toBeFalsy()
})
it('should support running tasks, refresh status and show toasts', () => {
const toastSpy = jest.spyOn(toastService, 'showInfo')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
it('should support running tasks, refresh status and show notifications', () => {
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const getStatusSpy = jest.spyOn(systemStatusService, 'get')
const runSpy = jest.spyOn(tasksService, 'run')
@ -126,7 +126,7 @@ describe('SystemStatusDialogComponent', () => {
runSpy.mockReturnValue(throwError(() => new Error('error')))
component.runTask(PaperlessTaskName.IndexOptimize)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
expect(toastErrorSpy).toHaveBeenCalledWith(
expect(notificationErrorSpy).toHaveBeenCalledWith(
`Failed to start task ${PaperlessTaskName.IndexOptimize}, see the logs for more details`,
expect.any(Error)
)
@ -138,7 +138,7 @@ describe('SystemStatusDialogComponent', () => {
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
expect(getStatusSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
expect(notificationSpy).toHaveBeenCalledWith(
`Task ${PaperlessTaskName.IndexOptimize} started`
)
})

View File

@ -14,10 +14,10 @@ import {
} from 'src/app/data/system-status'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
@Component({
selector: 'pngx-system-status-dialog',
@ -51,7 +51,7 @@ export class SystemStatusDialogComponent {
private clipboard: Clipboard,
private systemStatusService: SystemStatusService,
private tasksService: TasksService,
private toastService: ToastService,
private notificationService: NotificationService,
private permissionsService: PermissionsService
) {}
@ -79,7 +79,7 @@ export class SystemStatusDialogComponent {
public runTask(taskName: PaperlessTaskName) {
this.runningTasks.add(taskName)
this.toastService.showInfo(`Task ${taskName} started`)
this.notificationService.showInfo(`Task ${taskName} started`)
this.tasksService.run(taskName).subscribe({
next: () => {
this.runningTasks.delete(taskName)
@ -91,7 +91,7 @@ export class SystemStatusDialogComponent {
},
error: (err) => {
this.runningTasks.delete(taskName)
this.toastService.showError(
this.notificationService.showError(
`Failed to start task ${taskName}, see the logs for more details`,
err
)

View File

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

View File

@ -1,71 +0,0 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { ToastsComponent } from './toasts.component'
const toast = {
content: 'Error 2 content',
delay: 5000,
error: {
url: 'https://example.com',
status: 500,
statusText: 'Internal Server Error',
message: 'Internal server error 500 message',
error: { detail: 'Error 2 message details' },
},
}
describe('ToastsComponent', () => {
let component: ToastsComponent
let fixture: ComponentFixture<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])
})
})

View File

@ -1,43 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import {
NgbAccordionModule,
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subscription } from 'rxjs'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { ToastComponent } from '../toast/toast.component'
@Component({
selector: 'pngx-toasts',
templateUrl: './toasts.component.html',
styleUrls: ['./toasts.component.scss'],
imports: [
ToastComponent,
NgbAccordionModule,
NgbProgressbarModule,
NgxBootstrapIconsModule,
],
})
export class ToastsComponent implements OnInit, OnDestroy {
constructor(public toastService: ToastService) {}
private subscription: Subscription
public toasts: Toast[] = [] // array to force change detection
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
ngOnInit(): void {
this.subscription = this.toastService.showToast.subscribe((toast) => {
this.toasts = toast ? [toast] : []
})
}
closeToast() {
this.toastService.closeToast(this.toasts[0])
this.toasts = []
}
}

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
beforeEach(async () => {
TestBed.configureTestingModule({
@ -121,7 +121,7 @@ describe('DashboardComponent', () => {
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return [0, 2, 3]
})
tourService = TestBed.inject(TourService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
fixture = TestBed.createComponent(DashboardComponent)
component = fixture.componentInstance
@ -166,7 +166,7 @@ describe('DashboardComponent', () => {
it('should update saved view sorting on drag + drop, show info', () => {
const settingsSpy = jest.spyOn(settingsService, 'updateDashboardViewsSort')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
SavedView[]
@ -176,7 +176,7 @@ describe('DashboardComponent', () => {
saved_views[0],
saved_views[3],
])
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should update saved view sorting on drag + drop, show error', () => {
@ -187,13 +187,13 @@ describe('DashboardComponent', () => {
fixture = TestBed.createComponent(DashboardComponent)
component = fixture.componentInstance
fixture.detectChanges()
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
SavedView[]
>)
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
})

View File

@ -9,9 +9,9 @@ import { Component } from '@angular/core'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { NotificationService } from 'src/app/services/notification.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { LogoComponent } from '../common/logo/logo.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component'
@ -43,7 +43,7 @@ export class DashboardComponent extends ComponentWithPermissions {
public settingsService: SettingsService,
public savedViewService: SavedViewService,
private tourService: TourService,
private toastService: ToastService
private notificationService: NotificationService
) {
super()
@ -87,10 +87,13 @@ export class DashboardComponent extends ComponentWithPermissions {
.updateDashboardViewsSort(this.dashboardViews)
.subscribe({
next: () => {
this.toastService.showInfo($localize`Dashboard updated`)
this.notificationService.showInfo($localize`Dashboard updated`)
},
error: (e) => {
this.toastService.showError($localize`Error updating dashboard`, e)
this.notificationService.showError(
$localize`Error updating dashboard`,
e
)
},
})
}

View File

@ -48,6 +48,7 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
import { ComponentRouterService } from 'src/app/services/component-router.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NotificationService } from 'src/app/services/notification.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
@ -58,7 +59,6 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
@ -127,7 +127,7 @@ describe('DocumentDetailComponent', () => {
let documentService: DocumentService
let openDocumentsService: OpenDocumentsService
let modalService: NgbModal
let toastService: ToastService
let notificationService: NotificationService
let documentListViewService: DocumentListViewService
let settingsService: SettingsService
let customFieldsService: CustomFieldsService
@ -264,7 +264,7 @@ describe('DocumentDetailComponent', () => {
openDocumentsService = TestBed.inject(OpenDocumentsService)
documentService = TestBed.inject(DocumentService)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
documentListViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 1 }
@ -447,68 +447,68 @@ describe('DocumentDetailComponent', () => {
expect(navigateSpy).toHaveBeenCalledWith(['404'], { replaceUrl: true })
})
it('should support save, close and show success toast', () => {
it('should support save, close and show success notification', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
updateSpy.mockImplementation((o) => of(doc))
component.save(true)
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
expect(notificationSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
)
})
it('should support save without close and show success toast', () => {
it('should support save without close and show success notification', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
updateSpy.mockImplementation((o) => of(doc))
component.save()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
expect(notificationSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
)
})
it('should show toast error on save if error occurs', () => {
it('should show notification error on save if error occurs', () => {
currentUserHasObjectPermissions = true
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
const error = new Error('failed to save')
updateSpy.mockImplementation(() => throwError(() => error))
component.save()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
expect(notificationSpy).toHaveBeenCalledWith(
'Error saving document "Doc 3"',
error
)
})
it('should show error toast on save but close if user can no longer edit', () => {
it('should show error notification on save but close if user can no longer edit', () => {
currentUserHasObjectPermissions = false
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
updateSpy.mockImplementation(() =>
throwError(() => new Error('failed to save'))
)
component.save(true)
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
expect(notificationSpy).toHaveBeenCalledWith(
'Document "Doc 3" saved successfully.'
)
})
@ -531,19 +531,19 @@ describe('DocumentDetailComponent', () => {
expect
})
it('should show toast error on save & next if error occurs', () => {
it('should show notification error on save & next if error occurs', () => {
currentUserHasObjectPermissions = true
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
const error = new Error('failed to save')
updateSpy.mockImplementation(() => throwError(() => error))
component.saveEditNext()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Error saving document', error)
expect(notificationSpy).toHaveBeenCalledWith('Error saving document', error)
})
it('should show save button and save & close or save & next', () => {
@ -668,13 +668,13 @@ describe('DocumentDetailComponent', () => {
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const modalSpy = jest.spyOn(modalService, 'open')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
component.reprocess()
const modalCloseSpy = jest.spyOn(openModal, 'close')
openModal.componentInstance.confirmClicked.next()
expect(bulkEditSpy).toHaveBeenCalledWith([doc.id], 'reprocess', {})
expect(modalSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
expect(modalCloseSpy).toHaveBeenCalled()
})
@ -683,12 +683,12 @@ describe('DocumentDetailComponent', () => {
const bulkEditSpy = jest.spyOn(documentService, 'bulkEdit')
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
component.reprocess()
const modalCloseSpy = jest.spyOn(openModal, 'close')
bulkEditSpy.mockReturnValue(throwError(() => new Error('error occurred')))
openModal.componentInstance.confirmClicked.next()
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
expect(modalCloseSpy).not.toHaveBeenCalled()
})
@ -942,9 +942,12 @@ describe('DocumentDetailComponent', () => {
jest
.spyOn(documentService, 'getMetadata')
.mockReturnValue(throwError(() => error))
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
initNormally()
expect(toastSpy).toHaveBeenCalledWith('Error retrieving metadata', error)
expect(notificationSpy).toHaveBeenCalledWith(
'Error retrieving metadata',
error
)
})
it('should display custom fields', () => {
@ -1028,7 +1031,7 @@ describe('DocumentDetailComponent', () => {
it('should show error if needed for get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const errorSpy = jest.spyOn(toastService, 'showError')
const errorSpy = jest.spyOn(notificationService, 'showError')
suggestionsSpy.mockImplementationOnce(() =>
throwError(() => new Error('failed to get suggestions'))
)

View File

@ -63,6 +63,7 @@ import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { ComponentRouterService } from 'src/app/services/component-router.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { HotKeyService } from 'src/app/services/hot-key.service'
import { NotificationService } from 'src/app/services/notification.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import {
PermissionAction,
@ -76,7 +77,6 @@ import { DocumentService } from 'src/app/services/rest/document.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import * as UTIF from 'utif'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
@ -268,7 +268,7 @@ export class DocumentDetailComponent
private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService,
private documentTitlePipe: DocumentTitlePipe,
private toastService: ToastService,
private notificationService: NotificationService,
private settings: SettingsService,
private storagePathService: StoragePathService,
private permissionsService: PermissionsService,
@ -628,7 +628,7 @@ export class DocumentDetailComponent
},
error: (error) => {
this.metadata = {} // allow display to fallback to <object> tag
this.toastService.showError(
this.notificationService.showError(
$localize`Error retrieving metadata`,
error
)
@ -657,7 +657,7 @@ export class DocumentDetailComponent
},
error: (error) => {
this.suggestions = null
this.toastService.showError(
this.notificationService.showError(
$localize`Error retrieving suggestions.`,
error
)
@ -809,7 +809,7 @@ export class DocumentDetailComponent
this.store.next(newValues)
this.openDocumentService.setDirty(this.document, false)
this.openDocumentService.save()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Document "${newValues.title}" saved successfully.`
)
this.networkActive = false
@ -825,13 +825,13 @@ export class DocumentDetailComponent
error: (error) => {
this.networkActive = false
if (!this.userCanEdit) {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Document "${this.document.title}" saved successfully.`
)
close && this.close()
} else {
this.error = error.error
this.toastService.showError(
this.notificationService.showError(
$localize`Error saving document "${this.document.title}"`,
error
)
@ -877,7 +877,10 @@ export class DocumentDetailComponent
error: (error) => {
this.networkActive = false
this.error = error.error
this.toastService.showError($localize`Error saving document`, error)
this.notificationService.showError(
$localize`Error saving document`,
error
)
},
})
}
@ -931,7 +934,10 @@ export class DocumentDetailComponent
this.close()
},
error: (error) => {
this.toastService.showError($localize`Error deleting document`, error)
this.notificationService.showError(
$localize`Error deleting document`,
error
)
modal.componentInstance.buttonsEnabled = true
this.subscribeModalDelete(modal)
},
@ -962,7 +968,7 @@ export class DocumentDetailComponent
.bulkEdit([this.document.id], 'reprocess', {})
.subscribe({
next: () => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Reprocess operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.`
)
if (modal) {
@ -973,7 +979,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.toastService.showError(
this.notificationService.showError(
$localize`Error executing operation`,
error
)
@ -1020,7 +1026,7 @@ export class DocumentDetailComponent
},
error: (error) => {
this.downloading = false
this.toastService.showError(
this.notificationService.showError(
$localize`Error downloading document`,
error
)
@ -1329,7 +1335,7 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Split operation for "${this.document.title}" will begin in the background.`
)
modal.close()
@ -1338,7 +1344,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.toastService.showError(
this.notificationService.showError(
$localize`Error executing split operation`,
error
)
@ -1368,7 +1374,7 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.show({
this.notificationService.show({
content: $localize`Rotation of "${this.document.title}" will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
delay: 8000,
action: this.close.bind(this),
@ -1380,7 +1386,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.toastService.showError(
this.notificationService.showError(
$localize`Error executing rotate operation`,
error
)
@ -1408,7 +1414,7 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Delete pages operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.`
)
modal.close()
@ -1417,7 +1423,7 @@ export class DocumentDetailComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.toastService.showError(
this.notificationService.showError(
$localize`Error executing delete pages operation`,
error
)

View File

@ -16,6 +16,7 @@ import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag'
import { FilterPipe } from 'src/app/pipes/filter.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
@ -29,7 +30,6 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@ -64,7 +64,7 @@ describe('BulkEditorComponent', () => {
let permissionsService: PermissionsService
let documentListViewService: DocumentListViewService
let documentService: DocumentService
let toastService: ToastService
let notificationService: NotificationService
let modalService: NgbModal
let tagService: TagService
let correspondentsService: CorrespondentService
@ -160,7 +160,7 @@ describe('BulkEditorComponent', () => {
permissionsService = TestBed.inject(PermissionsService)
documentListViewService = TestBed.inject(DocumentListViewService)
documentService = TestBed.inject(DocumentService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
modalService = TestBed.inject(NgbModal)
tagService = TestBed.inject(TagService)
correspondentsService = TestBed.inject(CorrespondentService)
@ -884,7 +884,7 @@ describe('BulkEditorComponent', () => {
expect(button.nativeElement.disabled).toBeTruthy()
})
it('should show a warning toast on bulk edit error', () => {
it('should show a warning notification on bulk edit error', () => {
jest
.spyOn(documentService, 'bulkEdit')
.mockReturnValue(
@ -902,12 +902,12 @@ describe('BulkEditorComponent', () => {
.mockReturnValue(true)
component.showConfirmationDialogs = false
fixture.detectChanges()
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
component.setTags({
itemsToAdd: [{ id: 0 }],
itemsToRemove: [],
})
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
it('should support redo ocr', () => {
@ -1391,8 +1391,14 @@ describe('BulkEditorComponent', () => {
.spyOn(documentListViewService, 'selected', 'get')
.mockReturnValue(new Set([3, 4]))
fixture.detectChanges()
const toastServiceShowInfoSpy = jest.spyOn(toastService, 'showInfo')
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
const notificationServiceShowInfoSpy = jest.spyOn(
notificationService,
'showInfo'
)
const notificationServiceShowErrorSpy = jest.spyOn(
notificationService,
'showError'
)
const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
component.customFields = [
@ -1410,11 +1416,11 @@ describe('BulkEditorComponent', () => {
expect(modal.componentInstance.documents).toEqual([3, 4])
modal.componentInstance.failed.emit()
expect(toastServiceShowErrorSpy).toHaveBeenCalled()
expect(notificationServiceShowErrorSpy).toHaveBeenCalled()
expect(listReloadSpy).not.toHaveBeenCalled()
modal.componentInstance.succeeded.emit()
expect(toastServiceShowInfoSpy).toHaveBeenCalled()
expect(notificationServiceShowInfoSpy).toHaveBeenCalled()
expect(listReloadSpy).toHaveBeenCalled()
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`

View File

@ -23,6 +23,7 @@ import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NotificationService } from 'src/app/services/notification.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import {
PermissionAction,
@ -39,7 +40,6 @@ import {
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
@ -113,7 +113,7 @@ export class BulkEditorComponent
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private settings: SettingsService,
private toastService: ToastService,
private notificationService: NotificationService,
private storagePathService: StoragePathService,
private customFieldService: CustomFieldsService,
private permissionService: PermissionsService
@ -284,7 +284,7 @@ export class BulkEditorComponent
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.toastService.showError(
this.notificationService.showError(
$localize`Error executing bulk operation`,
error
)
@ -859,7 +859,7 @@ export class BulkEditorComponent
}
mergeDialog.buttonsEnabled = false
this.executeBulkOperation(modal, 'merge', args, mergeDialog.documentIDs)
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Merged document will be queued for consumption.`
)
})
@ -882,7 +882,7 @@ export class BulkEditorComponent
dialog.documents = Array.from(this.list.selected)
dialog.succeeded.subscribe((result) => {
this.toastService.showInfo($localize`Custom fields updated.`)
this.notificationService.showInfo($localize`Custom fields updated.`)
this.list.reload()
this.list.reduceSelectionToFilter()
this.list.selected.forEach((id) => {
@ -890,7 +890,7 @@ export class BulkEditorComponent
})
})
dialog.failed.subscribe((error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error updating custom fields.`,
error
)

View File

@ -39,11 +39,11 @@ import { FilterPipe } from 'src/app/pipes/filter.pipe'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { UsernamePipe } from 'src/app/pipes/username.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import {
FileStatus,
WebsocketStatusService,
@ -85,7 +85,7 @@ describe('DocumentListComponent', () => {
let savedViewService: SavedViewService
let router: Router
let activatedRoute: ActivatedRoute
let toastService: ToastService
let notificationService: NotificationService
let modalService: NgbModal
let settingsService: SettingsService
let permissionService: PermissionsService
@ -116,7 +116,7 @@ describe('DocumentListComponent', () => {
savedViewService = TestBed.inject(SavedViewService)
router = TestBed.inject(Router)
activatedRoute = TestBed.inject(ActivatedRoute)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
modalService = TestBed.inject(NgbModal)
settingsService = TestBed.inject(SettingsService)
permissionService = TestBed.inject(PermissionsService)
@ -405,11 +405,11 @@ describe('DocumentListComponent', () => {
delete modifiedView.name
const savedViewServicePatch = jest.spyOn(savedViewService, 'patch')
savedViewServicePatch.mockReturnValue(of(modifiedView))
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
component.saveViewConfig()
expect(savedViewServicePatch).toHaveBeenCalledWith(modifiedView)
expect(toastSpy).toHaveBeenCalledWith(
expect(notificationSpy).toHaveBeenCalledWith(
`View "${view.name}" saved successfully.`
)
})
@ -427,12 +427,12 @@ describe('DocumentListComponent', () => {
},
],
})
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
jest
.spyOn(savedViewService, 'patch')
.mockReturnValueOnce(throwError(() => new Error('Error saving view')))
component.saveViewConfig()
expect(toastErrorSpy).toHaveBeenCalledWith(
expect(notificationErrorSpy).toHaveBeenCalledWith(
'Failed to save view "Saved View 10".',
expect.any(Error)
)
@ -467,7 +467,7 @@ describe('DocumentListComponent', () => {
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const modalSpy = jest.spyOn(modalService, 'open')
const toastSpy = jest.spyOn(toastService, 'showInfo')
const notificationSpy = jest.spyOn(notificationService, 'showInfo')
const savedViewServiceCreate = jest.spyOn(savedViewService, 'create')
savedViewServiceCreate.mockReturnValueOnce(of(modifiedView))
component.saveViewConfigAs()
@ -480,7 +480,7 @@ describe('DocumentListComponent', () => {
})
expect(savedViewServiceCreate).toHaveBeenCalled()
expect(modalSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
expect(modalCloseSpy).toHaveBeenCalled()
})

View File

@ -45,11 +45,11 @@ import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
import { UsernamePipe } from 'src/app/pipes/username.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { HotKeyService } from 'src/app/services/hot-key.service'
import { NotificationService } from 'src/app/services/notification.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
import {
filterRulesDiffer,
@ -111,7 +111,7 @@ export class DocumentListComponent
public savedViewService: SavedViewService,
public route: ActivatedRoute,
private router: Router,
private toastService: ToastService,
private notificationService: NotificationService,
private modalService: NgbModal,
private websocketStatusService: WebsocketStatusService,
public openDocumentsService: OpenDocumentsService,
@ -380,13 +380,13 @@ export class DocumentListComponent
.subscribe({
next: (view) => {
this.unmodifiedSavedView = view
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
)
this.unmodifiedFilterRules = this.list.filterRules
},
error: (err) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Failed to save view "${this.list.activeSavedViewTitle}".`,
err
)
@ -430,7 +430,7 @@ export class DocumentListComponent
.subscribe({
next: () => {
modal.close()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`View "${savedView.name}" created successfully.`
)
},

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
beforeEach(async () => {
TestBed.configureTestingModule({
@ -103,7 +103,7 @@ describe('DocumentNotesComponent', () => {
}).compileComponents()
notesService = TestBed.inject(DocumentNotesService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
fixture = TestBed.createComponent(DocumentNotesComponent)
component = fixture.componentInstance
fixture.detectChanges()
@ -162,11 +162,11 @@ describe('DocumentNotesComponent', () => {
fixture.detectChanges()
const addSpy = jest.spyOn(notesService, 'addNote')
addSpy.mockReturnValueOnce(throwError(() => new Error('error saving note')))
const toastsSpy = jest.spyOn(toastService, 'showError')
const notificationsSpy = jest.spyOn(notificationService, 'showError')
const addButton = fixture.debugElement.query(By.css('button'))
addButton.triggerEventHandler('click')
expect(addSpy).toHaveBeenCalledWith(12, note)
expect(toastsSpy).toHaveBeenCalled()
expect(notificationsSpy).toHaveBeenCalled()
addSpy.mockReturnValueOnce(
of([...notes, { id: 31, note, user: { id: 1 } }])
@ -194,7 +194,7 @@ describe('DocumentNotesComponent', () => {
fixture.detectChanges()
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[1] // 0 is add button
const deleteSpy = jest.spyOn(notesService, 'deleteNote')
const toastsSpy = jest.spyOn(toastService, 'showError')
const toastsSpy = jest.spyOn(notificationService, 'showError')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting note'))
)

View File

@ -10,9 +10,9 @@ import { DocumentNote } from 'src/app/data/document-note'
import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { NotificationService } from 'src/app/services/notification.service'
import { DocumentNotesService } from 'src/app/services/rest/document-notes.service'
import { UserService } from 'src/app/services/rest/user.service'
import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
@Component({
@ -50,7 +50,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
constructor(
private notesService: DocumentNotesService,
private toastService: ToastService,
private notificationService: NotificationService,
private usersService: UserService
) {
super()
@ -78,7 +78,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
},
error: (e) => {
this.networkActive = false
this.toastService.showError($localize`Error saving note`, e)
this.notificationService.showError($localize`Error saving note`, e)
},
})
}
@ -92,7 +92,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
},
error: (e) => {
this.networkActive = false
this.toastService.showError($localize`Error deleting note`, e)
this.notificationService.showError($localize`Error deleting note`, e)
},
})
}

View File

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

View File

@ -4,13 +4,13 @@ import {
NgxFileDropEntry,
NgxFileDropModule,
} from 'ngx-file-drop'
import { NotificationService } from 'src/app/services/notification.service'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
@Component({
@ -26,7 +26,7 @@ export class FileDropComponent {
constructor(
private settings: SettingsService,
private toastService: ToastService,
private notificationService: NotificationService,
private uploadDocumentsService: UploadDocumentsService,
private permissionsService: PermissionsService
) {}
@ -90,7 +90,7 @@ export class FileDropComponent {
public dropped(files: NgxFileDropEntry[]) {
this.uploadDocumentsService.onNgxFileDrop(files)
if (files.length > 0)
this.toastService.showInfo($localize`Initiating upload...`, 3000)
this.notificationService.showInfo($localize`Initiating upload...`, 3000)
}
@HostListener('window:blur', ['$event']) public onWindowBlur() {

View File

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

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
let listViewService: DocumentListViewService
let settingsService: SettingsService
@ -89,7 +89,7 @@ describe('CustomFieldsComponent', () => {
})
)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
listViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 0, username: 'test' }
@ -104,8 +104,8 @@ describe('CustomFieldsComponent', () => {
it('should support create, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const createButton = fixture.debugElement.queryAll(By.css('button'))[1]
@ -116,12 +116,12 @@ describe('CustomFieldsComponent', () => {
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
jest.advanceTimersByTime(100)
})
@ -129,8 +129,8 @@ describe('CustomFieldsComponent', () => {
it('should support edit, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const editButton = fixture.debugElement.queryAll(By.css('button'))[2]
@ -142,19 +142,19 @@ describe('CustomFieldsComponent', () => {
// fail first
editDialog.failed.emit({ error: 'error editing item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
it('should support delete, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const deleteSpy = jest.spyOn(customFieldsService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
@ -167,7 +167,7 @@ describe('CustomFieldsComponent', () => {
// fail first
deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
editDialog.confirmClicked.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed

View File

@ -14,12 +14,12 @@ import {
import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
@ -48,7 +48,7 @@ export class CustomFieldsComponent
private customFieldsService: CustomFieldsService,
public permissionsService: PermissionsService,
private modalService: NgbModal,
private toastService: ToastService,
private notificationService: NotificationService,
private documentListViewService: DocumentListViewService,
private settingsService: SettingsService,
private documentService: DocumentService,
@ -86,7 +86,9 @@ export class CustomFieldsComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newField) => {
this.toastService.showInfo($localize`Saved field "${newField.name}".`)
this.notificationService.showInfo(
$localize`Saved field "${newField.name}".`
)
this.customFieldsService.clearCache()
this.settingsService.initializeDisplayFields()
this.documentService.reload()
@ -95,7 +97,7 @@ export class CustomFieldsComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving field.`, e)
this.notificationService.showError($localize`Error saving field.`, e)
})
}
@ -113,7 +115,9 @@ export class CustomFieldsComponent
this.customFieldsService.delete(field).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted field "${field.name}"`)
this.notificationService.showInfo(
$localize`Deleted field "${field.name}"`
)
this.customFieldsService.clearCache()
this.settingsService.initializeDisplayFields()
this.documentService.reload()
@ -121,7 +125,7 @@ export class CustomFieldsComponent
this.reload()
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting field "${field.name}".`,
e
)

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { NotificationService } from 'src/app/services/notification.service'
import {
PermissionAction,
PermissionsService,
@ -19,7 +20,6 @@ import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperle
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
@ -71,7 +71,7 @@ export class MailComponent
constructor(
public mailAccountService: MailAccountService,
public mailRuleService: MailRuleService,
private toastService: ToastService,
private notificationService: NotificationService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private settingsService: SettingsService,
@ -104,7 +104,7 @@ export class MailComponent
this.showAccounts = true
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error retrieving mail accounts`,
e
)
@ -127,7 +127,10 @@ export class MailComponent
this.showRules = true
},
error: (e) => {
this.toastService.showError($localize`Error retrieving mail rules`, e)
this.notificationService.showError(
$localize`Error retrieving mail rules`,
e
)
},
})
@ -135,7 +138,9 @@ export class MailComponent
if (params.get('oauth_success')) {
const success = params.get('oauth_success') === '1'
if (success) {
this.toastService.showInfo($localize`OAuth2 authentication success`)
this.notificationService.showInfo(
$localize`OAuth2 authentication success`
)
this.oAuthAccountId = parseInt(params.get('account_id'))
if (this.mailAccounts.length > 0) {
this.editMailAccount(
@ -145,7 +150,7 @@ export class MailComponent
)
}
} else {
this.toastService.showError(
this.notificationService.showError(
$localize`OAuth2 authentication failed, see logs for details`
)
}
@ -169,7 +174,7 @@ export class MailComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newMailAccount) => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Saved account "${newMailAccount.name}".`
)
this.mailAccountService.clearCache()
@ -182,7 +187,7 @@ export class MailComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving account.`, e)
this.notificationService.showError($localize`Error saving account.`, e)
})
}
@ -200,7 +205,7 @@ export class MailComponent
this.mailAccountService.delete(account).subscribe({
next: () => {
modal.close()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Deleted mail account "${account.name}"`
)
this.mailAccountService.clearCache()
@ -211,7 +216,7 @@ export class MailComponent
})
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting mail account "${account.name}".`,
e
)
@ -223,12 +228,12 @@ export class MailComponent
processAccount(account: MailAccount) {
this.mailAccountService.processAccount(account).subscribe({
next: () => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Processing mail account "${account.name}"`
)
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error processing mail account "${account.name}"`,
e
)
@ -247,7 +252,9 @@ export class MailComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newMailRule) => {
this.toastService.showInfo($localize`Saved rule "${newMailRule.name}".`)
this.notificationService.showInfo(
$localize`Saved rule "${newMailRule.name}".`
)
this.mailRuleService.clearCache()
this.mailRuleService
.listAll(null, null, { full_perms: true })
@ -258,7 +265,7 @@ export class MailComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving rule.`, e)
this.notificationService.showError($localize`Error saving rule.`, e)
})
}
@ -272,14 +279,14 @@ export class MailComponent
onMailRuleEnableToggled(rule: MailRule) {
this.mailRuleService.patch(rule).subscribe({
next: () => {
this.toastService.showInfo(
this.notificationService.showInfo(
rule.enabled
? $localize`Rule "${rule.name}" enabled.`
: $localize`Rule "${rule.name}" disabled.`
)
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error toggling rule "${rule.name}".`,
e
)
@ -301,7 +308,7 @@ export class MailComponent
this.mailRuleService.delete(rule).subscribe({
next: () => {
modal.close()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Deleted mail rule "${rule.name}"`
)
this.mailRuleService.clearCache()
@ -312,7 +319,7 @@ export class MailComponent
})
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting mail rule "${rule.name}".`,
e
)
@ -337,11 +344,11 @@ export class MailComponent
object['set_permissions'] = permissions['set_permissions']
service.patch(object).subscribe({
next: () => {
this.toastService.showInfo($localize`Permissions updated`)
this.notificationService.showInfo($localize`Permissions updated`)
modal.close()
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error updating permissions`,
e
)

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
let documentListViewService: DocumentListViewService
let permissionsService: PermissionsService
@ -129,7 +129,7 @@ describe('ManagementListComponent', () => {
.spyOn(permissionsService, 'currentUserOwnsObject')
.mockReturnValue(true)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
documentListViewService = TestBed.inject(DocumentListViewService)
fixture = TestBed.createComponent(TagListComponent)
component = fixture.componentInstance
@ -160,8 +160,8 @@ describe('ManagementListComponent', () => {
it('should support create, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData')
const createButton = fixture.debugElement.queryAll(By.css('button'))[3]
@ -172,20 +172,20 @@ describe('ManagementListComponent', () => {
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit()
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
it('should support edit, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reloadData')
const editButton = fixture.debugElement.queryAll(By.css('button'))[6]
@ -197,19 +197,19 @@ describe('ManagementListComponent', () => {
// fail first
editDialog.failed.emit({ error: 'error editing item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit()
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
it('should support delete, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const deleteSpy = jest.spyOn(tagService, 'delete')
const reloadSpy = jest.spyOn(component, 'reloadData')
@ -222,7 +222,7 @@ describe('ManagementListComponent', () => {
// fail first
deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
editDialog.confirmClicked.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
@ -293,22 +293,22 @@ describe('ManagementListComponent', () => {
bulkEditPermsSpy.mockReturnValueOnce(
throwError(() => new Error('error setting permissions'))
)
const errorToastSpy = jest.spyOn(toastService, 'showError')
const errornotificationSpy = jest.spyOn(notificationService, 'showError')
modal.componentInstance.confirmClicked.emit({
permissions: {},
merge: true,
})
expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(errorToastSpy).toHaveBeenCalled()
expect(errornotificationSpy).toHaveBeenCalled()
const successToastSpy = jest.spyOn(toastService, 'showInfo')
const successnotificationSpy = jest.spyOn(notificationService, 'showInfo')
bulkEditPermsSpy.mockReturnValueOnce(of('OK'))
modal.componentInstance.confirmClicked.emit({
permissions: {},
merge: true,
})
expect(bulkEditPermsSpy).toHaveBeenCalled()
expect(successToastSpy).toHaveBeenCalled()
expect(successnotificationSpy).toHaveBeenCalled()
})
it('should support bulk delete objects', () => {
@ -327,19 +327,19 @@ describe('ManagementListComponent', () => {
bulkEditSpy.mockReturnValueOnce(
throwError(() => new Error('error setting permissions'))
)
const errorToastSpy = jest.spyOn(toastService, 'showError')
const errornotificationSpy = jest.spyOn(notificationService, 'showError')
modal.componentInstance.confirmClicked.emit(null)
expect(bulkEditSpy).toHaveBeenCalledWith(
Array.from(selected),
BulkEditObjectOperation.Delete
)
expect(errorToastSpy).toHaveBeenCalled()
expect(errornotificationSpy).toHaveBeenCalled()
const successToastSpy = jest.spyOn(toastService, 'showInfo')
const successnotificationSpy = jest.spyOn(notificationService, 'showInfo')
bulkEditSpy.mockReturnValueOnce(of('OK'))
modal.componentInstance.confirmClicked.emit(null)
expect(bulkEditSpy).toHaveBeenCalled()
expect(successToastSpy).toHaveBeenCalled()
expect(successnotificationSpy).toHaveBeenCalled()
})
it('should disallow bulk permissions or delete objects if no global perms', () => {

View File

@ -27,6 +27,7 @@ import {
SortEvent,
} from 'src/app/directives/sortable.directive'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NotificationService } from 'src/app/services/notification.service'
import {
PermissionAction,
PermissionsService,
@ -36,7 +37,6 @@ import {
AbstractNameFilterService,
BulkEditObjectOperation,
} from 'src/app/services/rest/abstract-name-filter-service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
@ -63,7 +63,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
protected service: AbstractNameFilterService<T>,
private modalService: NgbModal,
private editDialogComponent: any,
private toastService: ToastService,
private notificationService: NotificationService,
private documentListViewService: DocumentListViewService,
private permissionsService: PermissionsService,
protected filterRuleType: number,
@ -173,12 +173,12 @@ export abstract class ManagementListComponent<T extends MatchingModel>
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Successfully created ${this.typeName}.`
)
})
activeModal.componentInstance.failed.subscribe((e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error occurred while creating ${this.typeName}.`,
e
)
@ -193,12 +193,12 @@ export abstract class ManagementListComponent<T extends MatchingModel>
activeModal.componentInstance.dialogMode = EditDialogMode.EDIT
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Successfully updated ${this.typeName} "${object.name}".`
)
})
activeModal.componentInstance.failed.subscribe((e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error occurred while saving ${this.typeName}.`,
e
)
@ -234,7 +234,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
},
error: (error) => {
activeModal.componentInstance.buttonsEnabled = true
this.toastService.showError(
this.notificationService.showError(
$localize`Error while deleting element`,
error
)
@ -313,14 +313,14 @@ export abstract class ManagementListComponent<T extends MatchingModel>
.subscribe({
next: () => {
modal.close()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Permissions updated successfully`
)
this.reloadData()
},
error: (error) => {
modal.componentInstance.buttonsEnabled = true
this.toastService.showError(
this.notificationService.showError(
$localize`Error updating permissions`,
error
)
@ -349,12 +349,14 @@ export abstract class ManagementListComponent<T extends MatchingModel>
.subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Objects deleted successfully`)
this.notificationService.showInfo(
$localize`Objects deleted successfully`
)
this.reloadData()
},
error: (error) => {
modal.componentInstance.buttonsEnabled = true
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting objects`,
error
)

View File

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

View File

@ -11,9 +11,9 @@ import { BehaviorSubject, Observable, takeUntil } from 'rxjs'
import { DisplayMode } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { NotificationService } from 'src/app/services/notification.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
import { NumberComponent } from '../../common/input/number/number.component'
@ -58,7 +58,7 @@ export class SavedViewsComponent
constructor(
private savedViewService: SavedViewService,
private settings: SettingsService,
private toastService: ToastService
private notificationService: NotificationService
) {
super()
this.settings.organizingSidebarSavedViews = true
@ -129,7 +129,7 @@ export class SavedViewsComponent
this.savedViewService.delete(savedView).subscribe(() => {
this.savedViewsGroup.removeControl(savedView.id.toString())
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Saved view "${savedView.name}" deleted.`
)
this.savedViewService.clearCache()
@ -155,11 +155,13 @@ export class SavedViewsComponent
if (changed.length) {
this.savedViewService.patchMany(changed).subscribe({
next: () => {
this.toastService.showInfo($localize`Views saved successfully.`)
this.notificationService.showInfo(
$localize`Views saved successfully.`
)
this.store.next(this.savedViewsForm.value)
},
error: (error) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error while saving views.`,
error
)

View File

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

View File

@ -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,
toastService: ToastService,
notificationService: NotificationService,
documentListViewService: DocumentListViewService,
permissionsService: PermissionsService
) {
@ -53,7 +53,7 @@ export class TagListComponent extends ManagementListComponent<Tag> {
tagService,
modalService,
TagEditDialogComponent,
toastService,
notificationService,
documentListViewService,
permissionsService,
FILTER_HAS_TAGS_ALL,

View File

@ -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 toastService: ToastService
let notificationService: NotificationService
beforeEach(() => {
TestBed.configureTestingModule({
@ -116,7 +116,7 @@ describe('WorkflowsComponent', () => {
})
)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
jest.useFakeTimers()
fixture = TestBed.createComponent(WorkflowsComponent)
component = fixture.componentInstance
@ -127,8 +127,8 @@ describe('WorkflowsComponent', () => {
it('should support create, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const createButton = fixture.debugElement.queryAll(By.css('button'))[1]
@ -139,20 +139,20 @@ describe('WorkflowsComponent', () => {
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(workflows[0])
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
it('should support edit, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const editButton = fixture.debugElement.queryAll(By.css('button'))[2]
@ -164,12 +164,12 @@ describe('WorkflowsComponent', () => {
// fail first
editDialog.failed.emit({ error: 'error editing item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(workflows[0])
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
@ -240,7 +240,7 @@ describe('WorkflowsComponent', () => {
it('should support delete, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const deleteSpy = jest.spyOn(workflowService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
@ -253,7 +253,7 @@ describe('WorkflowsComponent', () => {
// fail first
deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
editDialog.confirmClicked.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
@ -267,21 +267,21 @@ describe('WorkflowsComponent', () => {
const toggleInput = fixture.debugElement.query(
By.css('input[type="checkbox"]')
)
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const notificationErrorSpy = jest.spyOn(notificationService, 'showError')
const notificationInfoSpy = jest.spyOn(notificationService, 'showInfo')
// fail first
patchSpy.mockReturnValueOnce(
throwError(() => new Error('Error getting config'))
)
toggleInput.nativeElement.click()
expect(patchSpy).toHaveBeenCalled()
expect(toastErrorSpy).toHaveBeenCalled()
expect(notificationErrorSpy).toHaveBeenCalled()
// succeed second
patchSpy.mockReturnValueOnce(of(workflows[0]))
toggleInput.nativeElement.click()
patchSpy.mockReturnValueOnce(of({ ...workflows[0], enabled: false }))
toggleInput.nativeElement.click()
expect(patchSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalled()
expect(notificationInfoSpy).toHaveBeenCalled()
})
})

View File

@ -5,9 +5,9 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, takeUntil, tap } from 'rxjs'
import { Workflow } from 'src/app/data/workflow'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { NotificationService } from 'src/app/services/notification.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { WorkflowService } from 'src/app/services/rest/workflow.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import {
@ -40,7 +40,7 @@ export class WorkflowsComponent
private workflowService: WorkflowService,
public permissionsService: PermissionsService,
private modalService: NgbModal,
private toastService: ToastService
private notificationService: NotificationService
) {
super()
}
@ -90,7 +90,7 @@ export class WorkflowsComponent
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newWorkflow) => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Saved workflow "${newWorkflow.name}".`
)
this.workflowService.clearCache()
@ -99,7 +99,7 @@ export class WorkflowsComponent
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving workflow.`, e)
this.notificationService.showError($localize`Error saving workflow.`, e)
})
}
@ -142,14 +142,14 @@ export class WorkflowsComponent
this.workflowService.delete(workflow).subscribe({
next: () => {
modal.close()
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`Deleted workflow "${workflow.name}".`
)
this.workflowService.clearCache()
this.reload()
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error deleting workflow "${workflow.name}".`,
e
)
@ -161,7 +161,7 @@ export class WorkflowsComponent
toggleWorkflowEnabled(workflow: Workflow) {
this.workflowService.patch(workflow).subscribe({
next: () => {
this.toastService.showInfo(
this.notificationService.showInfo(
workflow.enabled
? $localize`Enabled workflow "${workflow.name}"`
: $localize`Disabled workflow "${workflow.name}"`
@ -170,7 +170,7 @@ export class WorkflowsComponent
this.reload()
},
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
$localize`Error toggling workflow "${workflow.name}".`,
e
)

View File

@ -1,12 +1,12 @@
import { TestBed } from '@angular/core/testing'
import { ActivatedRoute, RouterState } from '@angular/router'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { NotificationService } from '../services/notification.service'
import {
PermissionAction,
PermissionType,
PermissionsService,
} from '../services/permissions.service'
import { ToastService } from '../services/toast.service'
import { PermissionsGuard } from './permissions.guard'
describe('PermissionsGuard', () => {
@ -15,7 +15,7 @@ describe('PermissionsGuard', () => {
let route: ActivatedRoute
let routerState: RouterState
let tourService: TourService
let toastService: ToastService
let notificationService: NotificationService
beforeEach(() => {
TestBed.configureTestingModule({
@ -44,13 +44,13 @@ describe('PermissionsGuard', () => {
},
},
TourService,
ToastService,
NotificationService,
],
})
permissionsService = TestBed.inject(PermissionsService)
tourService = TestBed.inject(TourService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
guard = TestBed.inject(PermissionsGuard)
route = TestBed.inject(ActivatedRoute)
routerState = TestBed.inject(RouterState)
@ -88,11 +88,11 @@ describe('PermissionsGuard', () => {
})
jest.spyOn(tourService, 'getStatus').mockImplementation(() => 2)
const toastSpy = jest.spyOn(toastService, 'showError')
const notificationSpy = jest.spyOn(notificationService, 'showError')
const canActivate = guard.canActivate(route.snapshot, routerState.snapshot)
expect(canActivate).toHaveProperty('root') // returns UrlTree
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
})
})

View File

@ -6,15 +6,15 @@ import {
UrlTree,
} from '@angular/router'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { NotificationService } from '../services/notification.service'
import { PermissionsService } from '../services/permissions.service'
import { ToastService } from '../services/toast.service'
@Injectable()
export class PermissionsGuard {
constructor(
private permissionsService: PermissionsService,
private router: Router,
private toastService: ToastService,
private notificationService: NotificationService,
private tourService: TourService
) {}
@ -32,7 +32,7 @@ export class PermissionsGuard {
) {
// Check if tour is running 1 = TourState.ON
if (this.tourService.getStatus() !== 1) {
this.toastService.showError(
this.notificationService.showError(
$localize`You don't have permissions to do that`
)
}

View File

@ -0,0 +1,109 @@
import { TestBed } from '@angular/core/testing'
import { NotificationService } from './notification.service'
describe('NotificationService', () => {
let notificationService: NotificationService
beforeEach(() => {
TestBed.configureTestingModule({
providers: [NotificationService],
})
notificationService = TestBed.inject(NotificationService)
})
it('adds notification on show', () => {
const notification = {
title: 'Title',
content: 'content',
delay: 5000,
}
notificationService.show(notification)
notificationService.getNotifications().subscribe((notifications) => {
expect(notifications).toContainEqual(notification)
})
})
it('adds a unique id to notification on show', () => {
const notification = {
title: 'Title',
content: 'content',
delay: 5000,
}
notificationService.show(notification)
notificationService.getNotifications().subscribe((notifications) => {
expect(notifications[0].id).toBeDefined()
})
})
it('parses error string to object on show', () => {
const notification = {
title: 'Title',
content: 'content',
delay: 5000,
error: 'Error string',
}
notificationService.show(notification)
notificationService.getNotifications().subscribe((notifications) => {
expect(notifications[0].error).toEqual('Error string')
})
})
it('creates notifications with defaults on showInfo and showError', () => {
notificationService.showInfo('Info notification')
notificationService.showError('Error notification')
notificationService.getNotifications().subscribe((notifications) => {
expect(notifications).toContainEqual({
content: 'Info notification',
delay: 5000,
})
expect(notifications).toContainEqual({
content: 'Error notification',
delay: 10000,
})
})
})
it('removes notification on close', () => {
const notification = {
title: 'Title',
content: 'content',
delay: 5000,
}
notificationService.show(notification)
notificationService.closeNotification(notification)
notificationService.getNotifications().subscribe((notifications) => {
expect(notifications).toHaveLength(0)
})
})
it('clears all notifications on clear', () => {
notificationService.showInfo('Info notification')
notificationService.showError('Error notification')
notificationService.clearNotifications()
notificationService.getNotifications().subscribe((notifications) => {
expect(notifications).toHaveLength(0)
})
})
it('suppresses popup notifications if suppressPopupNotifications is true', (finish) => {
notificationService.showNotification.subscribe((notification) => {
expect(notification).not.toBeNull()
})
notificationService.showInfo('Info notification')
notificationService.showNotification.subscribe((notification) => {
expect(notification).toBeNull()
finish()
})
notificationService.suppressPopupNotifications = true
notificationService.showInfo('Info notification')
})
})

View File

@ -0,0 +1,87 @@
import { Injectable } from '@angular/core'
import { Subject } from 'rxjs'
import { v4 as uuidv4 } from 'uuid'
export interface Notification {
id?: string
content: string
delay: number
delayRemaining?: number
action?: any
actionName?: string
classname?: string
error?: any
}
@Injectable({
providedIn: 'root',
})
export class NotificationService {
constructor() {}
_suppressPopupNotifications: boolean
set suppressPopupNotifications(value: boolean) {
this._suppressPopupNotifications = value
this.showNotification.next(null)
}
private notifications: Notification[] = []
private notificationsSubject: Subject<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)
}
}

View File

@ -14,10 +14,10 @@ import { CustomFieldDataType } from '../data/custom-field'
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
import { SavedView } from '../data/saved-view'
import { SETTINGS_KEYS, UiSettings } from '../data/ui-settings'
import { NotificationService } from './notification.service'
import { PermissionsService } from './permissions.service'
import { CustomFieldsService } from './rest/custom-fields.service'
import { SettingsService } from './settings.service'
import { ToastService } from './toast.service'
const customFields = [
{
@ -41,7 +41,7 @@ describe('SettingsService', () => {
let customFieldsService: CustomFieldsService
let permissionService: PermissionsService
let subscription: Subscription
let toastService: ToastService
let notificationService: NotificationService
const ui_settings: UiSettings = {
user: {
@ -105,7 +105,7 @@ describe('SettingsService', () => {
customFieldsService = TestBed.inject(CustomFieldsService)
permissionService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
toastService = TestBed.inject(ToastService)
notificationService = TestBed.inject(NotificationService)
// Normally done in app initializer
settingsService.initializeSettings().subscribe()
})
@ -122,8 +122,8 @@ describe('SettingsService', () => {
expect(req.request.method).toEqual('GET')
})
it('should catch error and show toast on retrieve ui_settings error', fakeAsync(() => {
const toastSpy = jest.spyOn(toastService, 'showError')
it('should catch error and show notification on retrieve ui_settings error', fakeAsync(() => {
const notificationSpy = jest.spyOn(notificationService, 'showError')
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush(
@ -131,7 +131,7 @@ describe('SettingsService', () => {
{ status: 403, statusText: 'Forbidden' }
)
tick(500)
expect(toastSpy).toHaveBeenCalled()
expect(notificationSpy).toHaveBeenCalled()
}))
it('calls ui_settings api endpoint with POST on store', () => {

View File

@ -26,13 +26,13 @@ import {
UiSettings,
} from '../data/ui-settings'
import { User } from '../data/user'
import { NotificationService } from './notification.service'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from './permissions.service'
import { CustomFieldsService } from './rest/custom-fields.service'
import { ToastService } from './toast.service'
export interface LanguageOption {
code: string
@ -294,7 +294,7 @@ export class SettingsService {
private meta: Meta,
@Inject(LOCALE_ID) private localeId: string,
protected http: HttpClient,
private toastService: ToastService,
private notificationService: NotificationService,
private permissionsService: PermissionsService,
private customFieldsService: CustomFieldsService
) {
@ -307,7 +307,7 @@ export class SettingsService {
first(),
catchError((error) => {
setTimeout(() => {
this.toastService.showError('Error loading settings', error)
this.notificationService.showError('Error loading settings', error)
}, 500)
return of({
settings: {
@ -601,7 +601,7 @@ export class SettingsService {
this.cookieService.get(this.getLanguageCookieName())
)
} catch (error) {
this.toastService.showError(errorMessage)
this.notificationService.showError(errorMessage)
console.log(error)
}
@ -610,10 +610,10 @@ export class SettingsService {
.subscribe({
next: () => {
this.updateAppearanceSettings()
this.toastService.showInfo(successMessage)
this.notificationService.showInfo(successMessage)
},
error: (e) => {
this.toastService.showError(errorMessage)
this.notificationService.showError(errorMessage)
console.log(e)
},
})
@ -633,7 +633,7 @@ export class SettingsService {
.pipe(first())
.subscribe({
error: (e) => {
this.toastService.showError(
this.notificationService.showError(
'Error migrating update checking setting'
)
console.log(e)
@ -663,7 +663,7 @@ export class SettingsService {
this.storeSettings()
.pipe(first())
.subscribe(() => {
this.toastService.showInfo(
this.notificationService.showInfo(
$localize`You can restart the tour from the settings page.`
)
})

View File

@ -1,109 +0,0 @@
import { TestBed } from '@angular/core/testing'
import { ToastService } from './toast.service'
describe('ToastService', () => {
let toastService: ToastService
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ToastService],
})
toastService = TestBed.inject(ToastService)
})
it('adds toast on show', () => {
const toast = {
title: 'Title',
content: 'content',
delay: 5000,
}
toastService.show(toast)
toastService.getToasts().subscribe((toasts) => {
expect(toasts).toContainEqual(toast)
})
})
it('adds a unique id to toast on show', () => {
const toast = {
title: 'Title',
content: 'content',
delay: 5000,
}
toastService.show(toast)
toastService.getToasts().subscribe((toasts) => {
expect(toasts[0].id).toBeDefined()
})
})
it('parses error string to object on show', () => {
const toast = {
title: 'Title',
content: 'content',
delay: 5000,
error: 'Error string',
}
toastService.show(toast)
toastService.getToasts().subscribe((toasts) => {
expect(toasts[0].error).toEqual('Error string')
})
})
it('creates toasts with defaults on showInfo and showError', () => {
toastService.showInfo('Info toast')
toastService.showError('Error toast')
toastService.getToasts().subscribe((toasts) => {
expect(toasts).toContainEqual({
content: 'Info toast',
delay: 5000,
})
expect(toasts).toContainEqual({
content: 'Error toast',
delay: 10000,
})
})
})
it('removes toast on close', () => {
const toast = {
title: 'Title',
content: 'content',
delay: 5000,
}
toastService.show(toast)
toastService.closeToast(toast)
toastService.getToasts().subscribe((toasts) => {
expect(toasts).toHaveLength(0)
})
})
it('clears all toasts on clearToasts', () => {
toastService.showInfo('Info toast')
toastService.showError('Error toast')
toastService.clearToasts()
toastService.getToasts().subscribe((toasts) => {
expect(toasts).toHaveLength(0)
})
})
it('suppresses popup toasts if suppressPopupToasts is true', (finish) => {
toastService.showToast.subscribe((toast) => {
expect(toast).not.toBeNull()
})
toastService.showInfo('Info toast')
toastService.showToast.subscribe((toast) => {
expect(toast).toBeNull()
finish()
})
toastService.suppressPopupToasts = true
toastService.showInfo('Info toast')
})
})

View File

@ -1,87 +0,0 @@
import { Injectable } from '@angular/core'
import { Subject } from 'rxjs'
import { v4 as uuidv4 } from 'uuid'
export interface Toast {
id?: string
content: string
delay: number
delayRemaining?: number
action?: any
actionName?: string
classname?: string
error?: any
}
@Injectable({
providedIn: 'root',
})
export class ToastService {
constructor() {}
_suppressPopupToasts: boolean
set suppressPopupToasts(value: boolean) {
this._suppressPopupToasts = value
this.showToast.next(null)
}
private toasts: Toast[] = []
private toastsSubject: Subject<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)
}
}

View File

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

View File

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