mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-14 10:36:58 +01:00
Enhancement: dashboard improvements, drag-n-drop reorder dashboard views (#4252)
* Updated dashboard * Make entire screen dropzone on dashboard too * Floating upload widget status alerts * Visual tweaks: spacing, borders * Better empty view widget * Support drag + drop reorder of dashboard saved views * Update messages.xlf * Disable dashbaord dnd if global dnd active * Remove ngx-file-drop dep, rebuild file-drop & upload files widget * Revert custom file drop implementation * Try patch-package fix * Simplify dropzone transitions to make more reliable * Update messages.xlf * Update dashboard.spec.ts * Fix coverage
This commit is contained in:
parent
96176589ca
commit
6973691cce
45 changed files with 1715 additions and 534 deletions
|
|
@ -1,24 +1,26 @@
|
|||
<pngx-widget-frame title="Upload new documents" i18n-title *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }">
|
||||
<div header-buttons>
|
||||
<a *ngIf="getStatusSuccess().length > 0" (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 content tourAnchor="tour.upload-widget">
|
||||
<form>
|
||||
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
||||
multiple="true" contentClassName="justify-content-center d-flex align-items-center py-5 px-2" [showBrowseBtn]=true
|
||||
browseBtnClassName="btn btn-sm btn-outline-primary ms-2" i18n-dropZoneLabel i18n-browseBtnLabel>
|
||||
</ngx-file-drop>
|
||||
<form class="justify-content-center d-flex flex-column align-items-center py-3 px-2">
|
||||
<span class="text-muted" i18n>Drop documents anywhere or</span>
|
||||
<button class="btn btn-sm btn-outline-primary mt-3" (click)="fileUpload.click()" i18n>Browse files</button>
|
||||
<input type="file" class="visually-hidden" (change)="onFileSelected($event)" multiple #fileUpload>
|
||||
</form>
|
||||
<p class="mt-3" *ngIf="getStatus().length > 0">{{getStatusSummary()}}</p>
|
||||
<div *ngFor="let status of getStatus()">
|
||||
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
|
||||
<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 col-lg-4 col-xl-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 *ngIf="getStatusHidden().length" class="alerts-hidden">
|
||||
<p *ngIf="!alertsExpanded" class="mt-3 mb-0 text-center">
|
||||
|
|
@ -36,19 +38,23 @@
|
|||
</pngx-widget-frame>
|
||||
|
||||
<ng-template #consumerAlert let-status>
|
||||
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
|
||||
<h6 class="alert-heading">{{status.filename}}</h6>
|
||||
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
|
||||
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
|
||||
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<div *ngIf="isFinished(status)">
|
||||
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||
<small i18n>Open document</small>
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row d-flex justify-content-end">
|
||||
<div class="col col-lg-4 col-xl-3">
|
||||
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
|
||||
<h6 class="alert-heading">{{status.filename}}</h6>
|
||||
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
|
||||
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
|
||||
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<div *ngIf="isFinished(status)">
|
||||
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||
<small i18n>Open document</small>
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ngb-alert>
|
||||
</div>
|
||||
</ngb-alert>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
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 { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
|
|
@ -8,7 +13,6 @@ import {
|
|||
NgbAlert,
|
||||
NgbCollapse,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
|
|
@ -21,6 +25,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
|
|||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { UploadFileWidgetComponent } from './upload-file-widget.component'
|
||||
import { DndModule } from 'ngx-drag-drop'
|
||||
|
||||
describe('UploadFileWidgetComponent', () => {
|
||||
let component: UploadFileWidgetComponent
|
||||
|
|
@ -48,8 +53,8 @@ describe('UploadFileWidgetComponent', () => {
|
|||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgxFileDropModule,
|
||||
NgbAlertModule,
|
||||
DndModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
|
@ -61,13 +66,21 @@ describe('UploadFileWidgetComponent', () => {
|
|||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support drop files', () => {
|
||||
it('should support browse files', () => {
|
||||
const fileInput = fixture.debugElement.query(By.css('input'))
|
||||
const clickSpy = jest.spyOn(fileInput.nativeElement, 'click')
|
||||
fixture.debugElement
|
||||
.query(By.css('button'))
|
||||
.nativeElement.dispatchEvent(new Event('click'))
|
||||
expect(clickSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should upload files', () => {
|
||||
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
|
||||
component.dropped([])
|
||||
fixture.debugElement
|
||||
.query(By.css('input'))
|
||||
.nativeElement.dispatchEvent(new Event('change'))
|
||||
expect(uploadSpy).toHaveBeenCalled()
|
||||
// coverage
|
||||
component.fileLeave(null)
|
||||
component.fileOver(null)
|
||||
})
|
||||
|
||||
it('should generate stats summary', () => {
|
||||
|
|
@ -114,11 +127,15 @@ describe('UploadFileWidgetComponent', () => {
|
|||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow dismissing all alerts', () => {
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismissCompleted')
|
||||
it('should allow dismissing all alerts', fakeAsync(() => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
fixture.detectChanges()
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
|
||||
component.dismissCompleted()
|
||||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
tick(1000)
|
||||
fixture.detectChanges()
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(6)
|
||||
}))
|
||||
})
|
||||
|
||||
function mockConsumerStatuses(consumerStatusService) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { Component } from '@angular/core'
|
||||
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import { Component, QueryList, ViewChildren } from '@angular/core'
|
||||
import { NgbAlert } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
|
||||
|
||||
const MAX_ALERTS = 5
|
||||
|
|
@ -18,9 +20,12 @@ const MAX_ALERTS = 5
|
|||
export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
alertsExpanded = false
|
||||
|
||||
@ViewChildren(NgbAlert) alerts: QueryList<NgbAlert>
|
||||
|
||||
constructor(
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private uploadDocumentsService: UploadDocumentsService
|
||||
private uploadDocumentsService: UploadDocumentsService,
|
||||
public settingsService: SettingsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
|
@ -69,6 +74,10 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
|||
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS)
|
||||
}
|
||||
|
||||
getStatusCompleted() {
|
||||
return this.consumerStatusService.getConsumerStatusCompleted()
|
||||
}
|
||||
|
||||
getTotalUploadProgress() {
|
||||
let current = 0
|
||||
let max = 0
|
||||
|
|
@ -106,14 +115,16 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
|||
}
|
||||
|
||||
dismissCompleted() {
|
||||
this.consumerStatusService.dismissCompleted()
|
||||
this.alerts.forEach((a) => a.close())
|
||||
}
|
||||
|
||||
public fileOver(event) {}
|
||||
public onFileSelected(event: Event) {
|
||||
this.uploadDocumentsService.uploadFiles(
|
||||
(event.target as HTMLInputElement).files
|
||||
)
|
||||
}
|
||||
|
||||
public fileLeave(event) {}
|
||||
|
||||
public dropped(files: NgxFileDropEntry[]) {
|
||||
this.uploadDocumentsService.uploadFiles(files)
|
||||
get slimSidebarEnabled(): boolean {
|
||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue