Floating upload widget status alerts
This commit is contained in:
parent
9f4b8fcfed
commit
8c29e1e536
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
<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>
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
</ngx-file-drop>
|
||||||
|
</form>
|
||||||
|
<div class="fixed-bottom p-2 p-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
|
||||||
|
<div class="row d-flex justify-content-end">
|
||||||
|
<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>
|
<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">
|
<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="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"/>
|
||||||
@ -8,18 +19,11 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div content tourAnchor="tour.upload-widget" class="h-100">
|
</div>
|
||||||
<form class="h-100">
|
|
||||||
<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"
|
|
||||||
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>
|
|
||||||
</ngx-file-drop>
|
|
||||||
</form>
|
|
||||||
<p class="mt-3" *ngIf="getStatus().length > 0">{{getStatusSummary()}}</p>
|
|
||||||
<div *ngFor="let status of getStatus()">
|
<div *ngFor="let status of getStatus()">
|
||||||
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
|
<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">
|
||||||
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
|
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
|
||||||
@ -36,6 +40,8 @@
|
|||||||
</pngx-widget-frame>
|
</pngx-widget-frame>
|
||||||
|
|
||||||
<ng-template #consumerAlert let-status>
|
<ng-template #consumerAlert let-status>
|
||||||
|
<div class="row d-flex justify-content-end">
|
||||||
|
<div class="col-12 col-md-4 col-lg-3">
|
||||||
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
|
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
|
||||||
<h6 class="alert-heading">{{status.filename}}</h6>
|
<h6 class="alert-heading">{{status.filename}}</h6>
|
||||||
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
|
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
|
||||||
@ -51,4 +57,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ngb-alert>
|
</ngb-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user