Floating upload widget status alerts

This commit is contained in:
shamoon 2023-09-23 17:10:15 -07:00
parent 9f4b8fcfed
commit 8c29e1e536
7 changed files with 79 additions and 43 deletions

View File

@ -99,10 +99,6 @@ main {
} }
} }
.col-slim {
padding-left: calc(50px + $grid-gutter-width) !important;
}
.sidebar-slim-toggler { .sidebar-slim-toggler {
display: block; display: block;
position: absolute; position: absolute;

View File

@ -3,7 +3,7 @@
</pngx-page-header> </pngx-page-header>
<div class="row"> <div class="row">
<div class="col col-md-9 mb-3"> <div class="col-12 col-md-8 col-lg-9 mb-4">
<div class="row row-cols-1 g-4" tourAnchor="tour.dashboard"> <div class="row row-cols-1 g-4" tourAnchor="tour.dashboard">
<div *ngIf="savedViewService.loading" class="col"> <div *ngIf="savedViewService.loading" class="col">
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
@ -23,7 +23,7 @@
</ng-container> </ng-container>
</div> </div>
</div> </div>
<div class="col"> <div class="col-12 col-md-4 col-lg-3">
<div class="row row-cols-1 g-4"> <div class="row row-cols-1 g-4">
<div class="col"> <div class="col">
<pngx-statistics-widget></pngx-statistics-widget> <pngx-statistics-widget></pngx-statistics-widget>

View File

@ -1,24 +1,28 @@
<pngx-widget-frame title="Upload new documents" i18n-title *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }"> <pngx-widget-frame title="Upload new documents" i18n-title *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }">
<div header-buttons> <div content tourAnchor="tour.upload-widget">
<a *ngIf="getStatusSuccess().length > 0" (click)="dismissCompleted()" [routerLink]="[]" > <form>
<span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
<path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
</svg>
</a>
</div>
<div content tourAnchor="tour.upload-widget" class="h-100">
<form class="h-100">
<ngx-file-drop dropZoneLabel="Drop documents anywhere or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)" <ngx-file-drop dropZoneLabel="Drop documents anywhere or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card h-100" (onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card h-100"
multiple="true" contentClassName="justify-content-center d-flex flex-column text-muted align-items-center py-5 px-2 h-100" [showBrowseBtn]=true multiple="true" contentClassName="justify-content-center d-flex flex-column text-muted align-items-center py-5 px-2 h-100" [showBrowseBtn]=true
browseBtnClassName="btn btn-sm btn-outline-primary mt-2" i18n-dropZoneLabel i18n-browseBtnLabel> browseBtnClassName="btn btn-sm btn-outline-primary mt-2" i18n-dropZoneLabel i18n-browseBtnLabel>
</ngx-file-drop> </ngx-file-drop>
</form> </form>
<p class="mt-3" *ngIf="getStatus().length > 0">{{getStatusSummary()}}</p> <div class="fixed-bottom p-2 p-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
<div *ngFor="let status of getStatus()"> <div class="row d-flex justify-content-end">
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> <div class="col-12 col-md-4 col-lg-3 d-flex px-4 justify-content-between align-items-center">
<p class="m-0 small text-muted" *ngIf="getStatus().length > 0">{{getStatusSummary()}}</p>
<a *ngIf="getStatusCompleted().length > 0" class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" >
<span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
<path d="M5.354 7.146l.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
</svg>
</a>
</div>
</div>
<div *ngFor="let status of getStatus()">
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
</div>
</div> </div>
<div *ngIf="getStatusHidden().length" class="alerts-hidden"> <div *ngIf="getStatusHidden().length" class="alerts-hidden">
<p *ngIf="!alertsExpanded" class="mt-3 mb-0 text-center"> <p *ngIf="!alertsExpanded" class="mt-3 mb-0 text-center">
@ -36,19 +40,23 @@
</pngx-widget-frame> </pngx-widget-frame>
<ng-template #consumerAlert let-status> <ng-template #consumerAlert let-status>
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)"> <div class="row d-flex justify-content-end">
<h6 class="alert-heading">{{status.filename}}</h6> <div class="col-12 col-md-4 col-lg-3">
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p> <ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar> <h6 class="alert-heading">{{status.filename}}</h6>
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
<div *ngIf="isFinished(status)"> <ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)"> <div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<small i18n>Open document</small> <div *ngIf="isFinished(status)">
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16"> <button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/> <small i18n>Open document</small>
</svg> <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
</button> <path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
</div> </svg>
</button>
</div>
</div>
</ngb-alert>
</div> </div>
</ngb-alert> </div>
</ng-template> </ng-template>

View File

@ -1,5 +1,10 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { HttpClientTestingModule } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { import {
@ -114,11 +119,15 @@ describe('UploadFileWidgetComponent', () => {
expect(dismissSpy).toHaveBeenCalled() expect(dismissSpy).toHaveBeenCalled()
}) })
it('should allow dismissing all alerts', () => { it('should allow dismissing all alerts', fakeAsync(() => {
const dismissSpy = jest.spyOn(consumerStatusService, 'dismissCompleted') mockConsumerStatuses(consumerStatusService)
fixture.detectChanges()
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
component.dismissCompleted() component.dismissCompleted()
expect(dismissSpy).toHaveBeenCalled() tick(1000)
}) fixture.detectChanges()
expect(dismissSpy).toHaveBeenCalledTimes(6)
}))
}) })
function mockConsumerStatuses(consumerStatusService) { function mockConsumerStatuses(consumerStatusService) {

View File

@ -1,11 +1,14 @@
import { Component } from '@angular/core' import { Component, QueryList, ViewChildren } from '@angular/core'
import { NgbAlert } from '@ng-bootstrap/ng-bootstrap'
import { NgxFileDropEntry } from 'ngx-file-drop' import { NgxFileDropEntry } from 'ngx-file-drop'
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component' import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { import {
ConsumerStatusService, ConsumerStatusService,
FileStatus, FileStatus,
FileStatusPhase, FileStatusPhase,
} from 'src/app/services/consumer-status.service' } from 'src/app/services/consumer-status.service'
import { SettingsService } from 'src/app/services/settings.service'
import { UploadDocumentsService } from 'src/app/services/upload-documents.service' import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
const MAX_ALERTS = 5 const MAX_ALERTS = 5
@ -18,9 +21,12 @@ const MAX_ALERTS = 5
export class UploadFileWidgetComponent extends ComponentWithPermissions { export class UploadFileWidgetComponent extends ComponentWithPermissions {
alertsExpanded = false alertsExpanded = false
@ViewChildren(NgbAlert) alerts: QueryList<NgbAlert>
constructor( constructor(
private consumerStatusService: ConsumerStatusService, private consumerStatusService: ConsumerStatusService,
private uploadDocumentsService: UploadDocumentsService private uploadDocumentsService: UploadDocumentsService,
public settingsService: SettingsService
) { ) {
super() super()
} }
@ -69,6 +75,10 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS) return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS)
} }
getStatusCompleted() {
return this.consumerStatusService.getConsumerStatusCompleted()
}
getTotalUploadProgress() { getTotalUploadProgress() {
let current = 0 let current = 0
let max = 0 let max = 0
@ -106,7 +116,7 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
} }
dismissCompleted() { dismissCompleted() {
this.consumerStatusService.dismissCompleted() this.alerts.forEach((a) => a.close())
} }
public fileOver(event) {} public fileOver(event) {}
@ -116,4 +126,8 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
public dropped(files: NgxFileDropEntry[]) { public dropped(files: NgxFileDropEntry[]) {
this.uploadDocumentsService.uploadFiles(files) this.uploadDocumentsService.uploadFiles(files)
} }
get slimSidebarEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
}
} }

View File

@ -230,7 +230,10 @@ export class ConsumerStatusService {
dismissCompleted() { dismissCompleted() {
this.consumerStatus = this.consumerStatus.filter( this.consumerStatus = this.consumerStatus.filter(
(status) => status.phase != FileStatusPhase.SUCCESS (status) =>
![FileStatusPhase.SUCCESS, FileStatusPhase.FAILED].includes(
status.phase
)
) )
} }

View File

@ -16,6 +16,12 @@ body {
transition: background-color 0.3s ease, border-color 0.3s ease; transition: background-color 0.3s ease, border-color 0.3s ease;
} }
@media(min-width: 768px) {
.col-slim {
padding-left: calc(50px + $grid-gutter-width) !important;
}
}
svg.logo { svg.logo {
.leaf { .leaf {
fill: var(--bs-primary) !important; fill: var(--bs-primary) !important;