Very annoying refactor
This commit is contained in:
@@ -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()
|
||||
}))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@for (notification of notifications; track notification.id) {
|
||||
<pngx-notification [notification]="notification" [autohide]="true" (close)="closeNotification()"></pngx-notification>
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
@@ -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 = []
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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(
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
@for (toast of toasts; track toast.id) {
|
||||
<pngx-toast [toast]="toast" [autohide]="true" (close)="closeToast()"></pngx-toast>
|
||||
}
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
@@ -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 = []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user