mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: compact toasts (#4545)
This commit is contained in:
@@ -80,9 +80,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
) {
|
||||
this.toastService.show({
|
||||
title: $localize`Document added`,
|
||||
content: $localize`Document ${status.filename} was added to Paperless-ngx.`,
|
||||
delay: 10000,
|
||||
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||
actionName: $localize`Open document`,
|
||||
action: () => {
|
||||
this.router.navigate(['documents', status.documentId])
|
||||
@@ -90,9 +89,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
} else {
|
||||
this.toastService.show({
|
||||
title: $localize`Document added`,
|
||||
content: $localize`Document ${status.filename} was added to Paperless-ngx.`,
|
||||
delay: 10000,
|
||||
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -121,9 +119,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
) {
|
||||
this.toastService.show({
|
||||
title: $localize`New document detected`,
|
||||
content: $localize`Document ${status.filename} is being processed by Paperless-ngx.`,
|
||||
delay: 5000,
|
||||
content: $localize`Document ${status.filename} is being processed by paperless.`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@@ -489,7 +489,6 @@ export class SettingsComponent
|
||||
this.documentListViewService.updatePageSize()
|
||||
this.settings.updateAppearanceSettings()
|
||||
let savedToast: Toast = {
|
||||
title: $localize`Settings saved`,
|
||||
content: $localize`Settings were saved successfully.`,
|
||||
delay: 5000,
|
||||
}
|
||||
|
@@ -1,30 +1,43 @@
|
||||
<ngb-toast
|
||||
*ngFor="let toast of toasts"
|
||||
[header]="toast.title" [autohide]="true" [delay]="toast.delay"
|
||||
[autohide]="true" [delay]="toast.delay"
|
||||
[class]="toast.classname"
|
||||
[class.mb-2]="true"
|
||||
(shown)="onShow(toast)"
|
||||
(hidden)="toastService.closeToast(toast)">
|
||||
<p>{{toast.content}}</p>
|
||||
<details *ngIf="toast.error">
|
||||
<div class="p-3">
|
||||
<dl class="row" *ngIf="isDetailedError(toast.error)">
|
||||
<dt class="col-sm-3 fw-normal text-end">URL</dt>
|
||||
<dd class="col-sm-9">{{ toast.error.url }}</dd>
|
||||
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
|
||||
<dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd>
|
||||
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
|
||||
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
|
||||
</dl>
|
||||
<div class="row">
|
||||
<div class="col offset-sm-3">
|
||||
<button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard" />
|
||||
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check" />
|
||||
</svg> <ng-container i18n>Copy Raw Error</ng-container>
|
||||
</button>
|
||||
<ngb-progressbar class="position-absolute h-100 w-100 top-90 start-0 bottom-0 end-0 pe-none" type="dark" [max]="toast.delay" [value]="toast.delayRemaining"></ngb-progressbar>
|
||||
<span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span>
|
||||
<div class="d-flex align-items-top">
|
||||
<svg class="sidebaricon-sm mt-1 me-2 flex-shrink-0" fill="currentColor">
|
||||
<use *ngIf="!toast.error" xlink:href="assets/bootstrap-icons.svg#info-circle"/>
|
||||
<use *ngIf="toast.error" xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="mb-0">{{toast.content}}</p>
|
||||
<details *ngIf="toast.error">
|
||||
<div class="mt-2">
|
||||
<dl class="row mb-0" *ngIf="isDetailedError(toast.error)">
|
||||
<dt class="col-sm-3 fw-normal text-end">URL</dt>
|
||||
<dd class="col-sm-9">{{ toast.error.url }}</dd>
|
||||
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
|
||||
<dd class="col-sm-9">{{ toast.error.status }} <em>{{ toast.error.statusText }}</em></dd>
|
||||
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
|
||||
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
|
||||
</dl>
|
||||
<div class="row">
|
||||
<div class="col offset-sm-3">
|
||||
<button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard" />
|
||||
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check" />
|
||||
</svg> <ng-container i18n>Copy Raw Error</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<p class="mb-0 mt-2" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
|
||||
</div>
|
||||
</details>
|
||||
<p class="mb-0" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
|
||||
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button>
|
||||
</div>
|
||||
</ngb-toast>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0.5em;
|
||||
margin: 0.3em;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,23 @@
|
||||
display: block; // this corrects an ng-bootstrap bug that prevented animations
|
||||
}
|
||||
|
||||
::ng-deep .toast.error .toast-header {
|
||||
background-color: hsla(350, 79%, 40%, 0.8); // bg-danger
|
||||
border-color: black;
|
||||
::ng-deep .toast-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::ng-deep .toast.error {
|
||||
border-color: hsla(350, 79%, 40%, 0.4); // bg-danger
|
||||
}
|
||||
|
||||
::ng-deep .toast.error .toast-body {
|
||||
background-color: hsla(350, 79%, 40%, 0.8); // bg-danger
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: var(--pngx-primary);
|
||||
opacity: .07;
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import {
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
flush,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ToastsComponent } from './toasts.component'
|
||||
@@ -14,18 +15,15 @@ import { Clipboard } from '@angular/cdk/clipboard'
|
||||
|
||||
const toasts = [
|
||||
{
|
||||
title: 'Title',
|
||||
content: 'content',
|
||||
content: 'foo bar',
|
||||
delay: 5000,
|
||||
},
|
||||
{
|
||||
title: 'Error 1',
|
||||
content: 'Error 1 content',
|
||||
delay: 5000,
|
||||
error: 'Error 1 string',
|
||||
},
|
||||
{
|
||||
title: 'Error 2',
|
||||
content: 'Error 2 content',
|
||||
delay: 5000,
|
||||
error: {
|
||||
@@ -75,8 +73,7 @@ describe('ToastsComponent', () => {
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
expect(component.toasts).toContainEqual({
|
||||
title: 'Title',
|
||||
content: 'content',
|
||||
content: 'foo bar',
|
||||
delay: 5000,
|
||||
})
|
||||
|
||||
@@ -89,13 +86,24 @@ describe('ToastsComponent', () => {
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain('Title')
|
||||
expect(fixture.nativeElement.textContent).toContain('foo bar')
|
||||
|
||||
component.ngOnDestroy()
|
||||
flush()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
|
||||
it('should countdown toast', fakeAsync(() => {
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
component.onShow(toasts[0])
|
||||
tick(5000)
|
||||
expect(component.toasts[0].delayRemaining).toEqual(0)
|
||||
component.ngOnDestroy()
|
||||
flush()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
|
||||
it('should show an error if given with toast', fakeAsync(() => {
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
@@ -134,6 +142,9 @@ describe('ToastsComponent', () => {
|
||||
'Error string no detail'
|
||||
)
|
||||
expect(component.getErrorText('Error string')).toEqual('')
|
||||
expect(
|
||||
component.getErrorText({ error: { message: 'foo error bar' } })
|
||||
).toContain('{"message":"foo error bar"}')
|
||||
expect(
|
||||
component.getErrorText({ error: new Array(205).join('a') })
|
||||
).toContain('...')
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Subscription, interval, take } from 'rxjs'
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
|
||||
@@ -20,6 +20,8 @@ export class ToastsComponent implements OnInit, OnDestroy {
|
||||
|
||||
public copied: boolean = false
|
||||
|
||||
public seconds: number = 0
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe()
|
||||
}
|
||||
@@ -37,6 +39,20 @@ export class ToastsComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
}
|
||||
|
||||
onShow(toast: Toast) {
|
||||
const refreshInterval = 150
|
||||
const delay = toast.delay - 500 // for fade animation
|
||||
|
||||
interval(refreshInterval)
|
||||
.pipe(take(delay / refreshInterval))
|
||||
.subscribe((count) => {
|
||||
toast.delayRemaining = Math.max(
|
||||
0,
|
||||
delay - refreshInterval * (count + 1)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public isDetailedError(error: any): boolean {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
|
@@ -2,12 +2,12 @@ import { Injectable } from '@angular/core'
|
||||
import { Subject } from 'rxjs'
|
||||
|
||||
export interface Toast {
|
||||
title: string
|
||||
|
||||
content: string
|
||||
|
||||
delay: number
|
||||
|
||||
delayRemaining?: number
|
||||
|
||||
action?: any
|
||||
|
||||
actionName?: string
|
||||
@@ -34,7 +34,6 @@ export class ToastService {
|
||||
|
||||
showError(content: string, error: any = null, delay: number = 10000) {
|
||||
this.show({
|
||||
title: $localize`Error`,
|
||||
content: content,
|
||||
delay: delay,
|
||||
classname: 'error',
|
||||
@@ -43,7 +42,7 @@ export class ToastService {
|
||||
}
|
||||
|
||||
showInfo(content: string, delay: number = 5000) {
|
||||
this.show({ title: $localize`Information`, content: content, delay: delay })
|
||||
this.show({ content: content, delay: delay })
|
||||
}
|
||||
|
||||
closeToast(toast: Toast) {
|
||||
|
@@ -249,12 +249,11 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.toast, .toast-header {
|
||||
background-color: hsla(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%), 0.8);
|
||||
.toast {
|
||||
background-color: hsla(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%), 0.9);
|
||||
}
|
||||
|
||||
.toast,
|
||||
.toast .toast-header,
|
||||
.toast .btn,
|
||||
.toast .btn-close {
|
||||
color: var(--pngx-primary-text-contrast);
|
||||
|
Reference in New Issue
Block a user