mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-08 21:23:44 -05: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:
@@ -230,7 +230,10 @@ export class ConsumerStatusService {
|
||||
|
||||
dismissCompleted() {
|
||||
this.consumerStatus = this.consumerStatus.filter(
|
||||
(status) => status.phase != FileStatusPhase.SUCCESS
|
||||
(status) =>
|
||||
![FileStatusPhase.SUCCESS, FileStatusPhase.FAILED].includes(
|
||||
status.phase
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
SETTINGS_KEYS,
|
||||
} from '../data/paperless-uisettings'
|
||||
import { SettingsService } from './settings.service'
|
||||
import { PaperlessSavedView } from '../data/paperless-saved-view'
|
||||
|
||||
describe('SettingsService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
@@ -277,4 +278,22 @@ describe('SettingsService', () => {
|
||||
)[0]
|
||||
expect(req.request.method).toEqual('POST')
|
||||
})
|
||||
|
||||
it('should update saved view sorting', () => {
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
.flush(ui_settings)
|
||||
const setSpy = jest.spyOn(settingsService, 'set')
|
||||
settingsService.updateDashboardViewsSort([
|
||||
{ id: 1 } as PaperlessSavedView,
|
||||
{ id: 4 } as PaperlessSavedView,
|
||||
])
|
||||
expect(setSpy).toHaveBeenCalledWith(
|
||||
SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER,
|
||||
[1, 4]
|
||||
)
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
.flush(ui_settings)
|
||||
})
|
||||
})
|
||||
|
@@ -26,6 +26,7 @@ import { PaperlessUser } from '../data/paperless-user'
|
||||
import { PermissionsService } from './permissions.service'
|
||||
import { SavedViewService } from './rest/saved-view.service'
|
||||
import { ToastService } from './toast.service'
|
||||
import { PaperlessSavedView } from '../data/paperless-saved-view'
|
||||
|
||||
export interface LanguageOption {
|
||||
code: string
|
||||
@@ -54,6 +55,9 @@ export class SettingsService {
|
||||
return this._renderer
|
||||
}
|
||||
|
||||
public globalDropzoneEnabled: boolean = true
|
||||
public globalDropzoneActive: boolean = false
|
||||
|
||||
constructor(
|
||||
rendererFactory: RendererFactory2,
|
||||
@Inject(DOCUMENT) private document,
|
||||
@@ -531,4 +535,13 @@ export class SettingsService {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateDashboardViewsSort(
|
||||
dashboardViews: PaperlessSavedView[]
|
||||
): Observable<any> {
|
||||
this.set(SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER, [
|
||||
...new Set(dashboardViews.map((v) => v.id)),
|
||||
])
|
||||
return this.storeSettings()
|
||||
}
|
||||
}
|
||||
|
@@ -5,12 +5,39 @@ import {
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { HttpEventType, HttpResponse } from '@angular/common/http'
|
||||
import { HttpEventType } from '@angular/common/http'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatusPhase,
|
||||
} from './consumer-status.service'
|
||||
|
||||
const files = [
|
||||
{
|
||||
lastModified: 1693349892540,
|
||||
lastModifiedDate: new Date(),
|
||||
name: 'file1.pdf',
|
||||
size: 386,
|
||||
type: 'application/pdf',
|
||||
},
|
||||
{
|
||||
lastModified: 1695618533892,
|
||||
lastModifiedDate: new Date(),
|
||||
name: 'file2.pdf',
|
||||
size: 358265,
|
||||
type: 'application/pdf',
|
||||
},
|
||||
]
|
||||
|
||||
const fileList = {
|
||||
item: (x) => {
|
||||
return new File(
|
||||
[new Blob(['testing'], { type: files[x].type })],
|
||||
files[x].name
|
||||
)
|
||||
},
|
||||
length: files.length,
|
||||
} as unknown as FileList
|
||||
|
||||
describe('UploadDocumentsService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let uploadDocumentsService: UploadDocumentsService
|
||||
@@ -32,66 +59,30 @@ describe('UploadDocumentsService', () => {
|
||||
})
|
||||
|
||||
it('calls post_document api endpoint on upload', () => {
|
||||
const fileEntry = {
|
||||
name: 'file.pdf',
|
||||
isDirectory: false,
|
||||
isFile: true,
|
||||
file: (callback) => {
|
||||
return callback(
|
||||
new File(
|
||||
[new Blob(['testing'], { type: 'application/pdf' })],
|
||||
'file.pdf'
|
||||
)
|
||||
)
|
||||
},
|
||||
}
|
||||
uploadDocumentsService.uploadFiles([
|
||||
{
|
||||
relativePath: 'path/to/file.pdf',
|
||||
fileEntry,
|
||||
},
|
||||
])
|
||||
const req = httpTestingController.expectOne(
|
||||
uploadDocumentsService.uploadFiles(fileList)
|
||||
const req = httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
expect(req.request.method).toEqual('POST')
|
||||
expect(req[0].request.method).toEqual('POST')
|
||||
|
||||
req.flush('123-456')
|
||||
req[0].flush('123-456')
|
||||
})
|
||||
|
||||
it('updates progress during upload and failure', () => {
|
||||
const fileEntry = {
|
||||
name: 'file.pdf',
|
||||
isDirectory: false,
|
||||
isFile: true,
|
||||
file: (callback) => {
|
||||
return callback(
|
||||
new File(
|
||||
[new Blob(['testing'], { type: 'application/pdf' })],
|
||||
'file.pdf'
|
||||
)
|
||||
)
|
||||
},
|
||||
}
|
||||
uploadDocumentsService.uploadFiles([
|
||||
{
|
||||
relativePath: 'path/to/file.pdf',
|
||||
fileEntry,
|
||||
},
|
||||
])
|
||||
uploadDocumentsService.uploadFiles(fileList)
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
2
|
||||
)
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toHaveLength(0)
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
const req = httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
req.event({
|
||||
req[0].event({
|
||||
type: HttpEventType.UploadProgress,
|
||||
loaded: 100,
|
||||
total: 300,
|
||||
@@ -103,6 +94,52 @@ describe('UploadDocumentsService', () => {
|
||||
})
|
||||
|
||||
it('updates progress on failure', () => {
|
||||
uploadDocumentsService.uploadFiles(fileList)
|
||||
|
||||
let req = httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(0)
|
||||
|
||||
req[0].flush(
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
statusText: 'failed',
|
||||
}
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(1)
|
||||
|
||||
uploadDocumentsService.uploadFiles(fileList)
|
||||
|
||||
req = httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
req[0].flush(
|
||||
{},
|
||||
{
|
||||
status: 500,
|
||||
statusText: 'failed',
|
||||
}
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('accepts files via drag and drop', () => {
|
||||
const uploadSpy = jest.spyOn(
|
||||
UploadDocumentsService.prototype as any,
|
||||
'uploadFile'
|
||||
)
|
||||
const fileEntry = {
|
||||
name: 'file.pdf',
|
||||
isDirectory: false,
|
||||
@@ -116,54 +153,16 @@ describe('UploadDocumentsService', () => {
|
||||
)
|
||||
},
|
||||
}
|
||||
uploadDocumentsService.uploadFiles([
|
||||
uploadDocumentsService.onNgxFileDrop([
|
||||
{
|
||||
relativePath: 'path/to/file.pdf',
|
||||
fileEntry,
|
||||
},
|
||||
])
|
||||
expect(uploadSpy).toHaveBeenCalled()
|
||||
|
||||
let req = httpTestingController.expectOne(
|
||||
let req = httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(0)
|
||||
|
||||
req.flush(
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
statusText: 'failed',
|
||||
}
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(1)
|
||||
|
||||
uploadDocumentsService.uploadFiles([
|
||||
{
|
||||
relativePath: 'path/to/file.pdf',
|
||||
fileEntry,
|
||||
},
|
||||
])
|
||||
|
||||
req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
req.flush(
|
||||
{},
|
||||
{
|
||||
status: 500,
|
||||
statusText: 'failed',
|
||||
}
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
@@ -19,56 +19,61 @@ export class UploadDocumentsService {
|
||||
private consumerStatusService: ConsumerStatusService
|
||||
) {}
|
||||
|
||||
uploadFiles(files: NgxFileDropEntry[]) {
|
||||
onNgxFileDrop(files: NgxFileDropEntry[]) {
|
||||
for (const droppedFile of files) {
|
||||
if (droppedFile.fileEntry.isFile) {
|
||||
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry
|
||||
fileEntry.file((file: File) => {
|
||||
let formData = new FormData()
|
||||
formData.append('document', file, file.name)
|
||||
let status = this.consumerStatusService.newFileUpload(file.name)
|
||||
|
||||
status.message = $localize`Connecting...`
|
||||
|
||||
this.uploadSubscriptions[file.name] = this.documentService
|
||||
.uploadDocument(formData)
|
||||
.subscribe({
|
||||
next: (event) => {
|
||||
if (event.type == HttpEventType.UploadProgress) {
|
||||
status.updateProgress(
|
||||
FileStatusPhase.UPLOADING,
|
||||
event.loaded,
|
||||
event.total
|
||||
)
|
||||
status.message = $localize`Uploading...`
|
||||
} else if (event.type == HttpEventType.Response) {
|
||||
status.taskId = event.body['task_id']
|
||||
status.message = $localize`Upload complete, waiting...`
|
||||
this.uploadSubscriptions[file.name]?.complete()
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
switch (error.status) {
|
||||
case 400: {
|
||||
this.consumerStatusService.fail(
|
||||
status,
|
||||
error.error.document
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
this.consumerStatusService.fail(
|
||||
status,
|
||||
$localize`HTTP error: ${error.status} ${error.statusText}`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
this.uploadSubscriptions[file.name]?.complete()
|
||||
},
|
||||
})
|
||||
})
|
||||
fileEntry.file((file: File) => this.uploadFile(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadFiles(files: FileList) {
|
||||
for (let index = 0; index < files.length; index++) {
|
||||
this.uploadFile(files.item(index))
|
||||
}
|
||||
}
|
||||
|
||||
private uploadFile(file: File) {
|
||||
let formData = new FormData()
|
||||
formData.append('document', file, file.name)
|
||||
let status = this.consumerStatusService.newFileUpload(file.name)
|
||||
|
||||
status.message = $localize`Connecting...`
|
||||
|
||||
this.uploadSubscriptions[file.name] = this.documentService
|
||||
.uploadDocument(formData)
|
||||
.subscribe({
|
||||
next: (event) => {
|
||||
if (event.type == HttpEventType.UploadProgress) {
|
||||
status.updateProgress(
|
||||
FileStatusPhase.UPLOADING,
|
||||
event.loaded,
|
||||
event.total
|
||||
)
|
||||
status.message = $localize`Uploading...`
|
||||
} else if (event.type == HttpEventType.Response) {
|
||||
status.taskId = event.body['task_id']
|
||||
status.message = $localize`Upload complete, waiting...`
|
||||
this.uploadSubscriptions[file.name]?.complete()
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
switch (error.status) {
|
||||
case 400: {
|
||||
this.consumerStatusService.fail(status, error.error.document)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
this.consumerStatusService.fail(
|
||||
status,
|
||||
$localize`HTTP error: ${error.status} ${error.statusText}`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
this.uploadSubscriptions[file.name]?.complete()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user