mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-15 10:26:42 +00:00
Make the popups more helpful
This commit is contained in:
9 files changed
+142
-43
No files matched your search
@@ -33,6 +33,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { getDocumentOperationLabel } from 'src/app/utils/document-operation'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
@@ -56,27 +57,6 @@ const FILTER_TARGETS = [
|
||||
{ id: TaskFilterTargetID.TaskId, name: $localize`Task ID` },
|
||||
]
|
||||
|
||||
const OPERATION_LABELS: Record<string, string> = {
|
||||
set_correspondent: $localize`Set correspondent`,
|
||||
set_document_type: $localize`Set document type`,
|
||||
set_storage_path: $localize`Set storage path`,
|
||||
add_tag: $localize`Add tag`,
|
||||
remove_tag: $localize`Remove tag`,
|
||||
modify_tags: $localize`Update tags`,
|
||||
modify_custom_fields: $localize`Update custom fields`,
|
||||
set_permissions: $localize`Set permissions`,
|
||||
delete: $localize`Move to trash`,
|
||||
reprocess: $localize`Document reprocessing`,
|
||||
run_workflow: $localize`Run workflow`,
|
||||
rotate: $localize`Rotate documents`,
|
||||
merge: $localize`Merge documents`,
|
||||
merge_as_versions: $localize`Merge as versions`,
|
||||
split: $localize`Split documents`,
|
||||
delete_pages: $localize`Delete pages`,
|
||||
edit_pdf: $localize`Edit PDF`,
|
||||
remove_password: $localize`Remove password`,
|
||||
}
|
||||
|
||||
const SECTION_LABELS = {
|
||||
[TaskSection.All]: $localize`All`,
|
||||
[TaskSection.NeedsAttention]: $localize`Needs attention`,
|
||||
@@ -453,8 +433,7 @@ export class TasksComponent
|
||||
}
|
||||
|
||||
private taskOperationLabel(task: PaperlessTask): string | null {
|
||||
const label = OPERATION_LABELS[task.input_data?.operation]
|
||||
return typeof label === 'string' ? label : null
|
||||
return getDocumentOperationLabel(task.input_data?.operation)
|
||||
}
|
||||
|
||||
taskDocumentCount(task: PaperlessTask): number | null {
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('AppFrameComponent', () => {
|
||||
expect(success).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'task-bulk-task',
|
||||
content: 'Operation completed.',
|
||||
content: "Operation 'Set permissions' completed.",
|
||||
})
|
||||
)
|
||||
fixture.destroy()
|
||||
@@ -271,13 +271,13 @@ describe('AppFrameComponent', () => {
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content:
|
||||
'Operation failed. Some changes may already have been applied.',
|
||||
"Operation 'Set permissions' failed. Some changes may already have been applied.",
|
||||
error: 'Could not update',
|
||||
})
|
||||
)
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: 'Operation revoked.',
|
||||
content: "Operation 'Set permissions' revoked.",
|
||||
error: 'Task expired before execution.',
|
||||
})
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessTaskStatus } from 'src/app/data/paperless-task'
|
||||
import { Results } from 'src/app/data/results'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
@@ -39,6 +40,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { TaskNotificationService } from 'src/app/services/task-notification.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { TrashService } from 'src/app/services/trash.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
@@ -1557,7 +1559,17 @@ describe('BulkEditorComponent', () => {
|
||||
expect(documentListViewService.selected.size).toEqual(0)
|
||||
jest.advanceTimersByTime(400)
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: 'Bulk operation queued.' })
|
||||
expect.objectContaining({
|
||||
content: "Bulk operation 'Merge as versions' queued.",
|
||||
})
|
||||
)
|
||||
TestBed.inject(TaskNotificationService).complete({
|
||||
task_id: 'versions-1',
|
||||
operation: 'merge_as_versions',
|
||||
status: PaperlessTaskStatus.Success,
|
||||
})
|
||||
expect(toastSpy.mock.calls.at(-1)[0].content).toBe(
|
||||
"Bulk operation 'Merge as versions' completed."
|
||||
)
|
||||
jest.useRealTimers()
|
||||
})
|
||||
@@ -1650,13 +1662,22 @@ describe('BulkEditorComponent', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each([true, false])(
|
||||
'shows queued feedback and gates the task link on permission (%s)',
|
||||
(canViewTasks) => {
|
||||
it.each<[boolean, number]>([
|
||||
[true, 2],
|
||||
[false, 2],
|
||||
[true, 1],
|
||||
])(
|
||||
'names queued work and gates the task link (permission %s, documents %s)',
|
||||
(canViewTasks, count) => {
|
||||
jest.useFakeTimers()
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserCan')
|
||||
.mockReturnValue(canViewTasks)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(
|
||||
new Set(Array.from({ length: count }, (_, index) => index + 3))
|
||||
)
|
||||
const queued = jest.spyOn(toastService, 'show')
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((instances) => {
|
||||
@@ -1677,7 +1698,8 @@ describe('BulkEditorComponent', () => {
|
||||
expect(queued).not.toHaveBeenCalled()
|
||||
jest.advanceTimersByTime(400)
|
||||
const notification = queued.mock.calls.at(-1)[0]
|
||||
expect(notification.content).toBe('Bulk operation queued.')
|
||||
const prefix = count > 1 ? 'Bulk operation' : 'Operation'
|
||||
expect(notification.content).toBe(`${prefix} \'Set permissions\' queued.`)
|
||||
if (canViewTasks) {
|
||||
expect(notification.actionName).toBe('View task')
|
||||
notification.action()
|
||||
@@ -1691,6 +1713,14 @@ describe('BulkEditorComponent', () => {
|
||||
expect(reload).not.toHaveBeenCalled()
|
||||
expect(documentListViewService.selected.size).toBe(0)
|
||||
expect(documentListViewService.allSelected).toBe(false)
|
||||
TestBed.inject(TaskNotificationService).complete({
|
||||
task_id: 'queued-operation',
|
||||
operation: 'set_permissions',
|
||||
status: PaperlessTaskStatus.Success,
|
||||
})
|
||||
const completed = queued.mock.calls.at(-1)[0]
|
||||
expect(completed.content).toBe(`${prefix} \'Set permissions\' completed.`)
|
||||
expect(completed.action).toBeUndefined()
|
||||
jest.useRealTimers()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { TaskNotificationService } from 'src/app/services/task-notification.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { TrashService } from 'src/app/services/trash.service'
|
||||
import { getDocumentOperationLabel } from 'src/app/utils/document-operation'
|
||||
import { flattenTags } from 'src/app/utils/flatten-tags'
|
||||
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
|
||||
import { MergeAsVersionsConfirmDialogComponent } from '../../common/confirm-dialog/merge-as-versions-confirm-dialog/merge-as-versions-confirm-dialog.component'
|
||||
@@ -348,7 +349,8 @@ export class BulkEditorComponent
|
||||
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: ({ task_id }) => this.handleOperationQueued(modal, task_id),
|
||||
next: ({ task_id }) =>
|
||||
this.handleOperationQueued(modal, task_id, method),
|
||||
error: (error) => this.handleOperationError(modal, error),
|
||||
})
|
||||
}
|
||||
@@ -356,22 +358,33 @@ export class BulkEditorComponent
|
||||
private handleOperationQueued(
|
||||
modal: NgbModalRef,
|
||||
taskId: string,
|
||||
operation: string,
|
||||
onQueued?: (taskId: string) => void
|
||||
) {
|
||||
const bulk = this.getSelectionSize() > 1
|
||||
modal?.close()
|
||||
this.list.selectNone()
|
||||
if (onQueued) {
|
||||
onQueued(taskId)
|
||||
} else {
|
||||
const label = getDocumentOperationLabel(operation)
|
||||
this.taskNotifications.showQueued(
|
||||
taskId,
|
||||
$localize`Bulk operation queued.`
|
||||
bulk
|
||||
? $localize`Bulk operation '${label}' queued.`
|
||||
: $localize`Operation '${label}' queued.`,
|
||||
{
|
||||
content: bulk
|
||||
? $localize`Bulk operation '${label}' completed.`
|
||||
: $localize`Operation '${label}' completed.`,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private executeDocumentAction(
|
||||
modal: NgbModalRef,
|
||||
operation: string,
|
||||
request: Observable<BulkOperationResponse>,
|
||||
onQueued?: (taskId: string) => void
|
||||
) {
|
||||
@@ -380,7 +393,7 @@ export class BulkEditorComponent
|
||||
}
|
||||
request.pipe(first()).subscribe({
|
||||
next: ({ task_id }) => {
|
||||
this.handleOperationQueued(modal, task_id, onQueued)
|
||||
this.handleOperationQueued(modal, task_id, operation, onQueued)
|
||||
},
|
||||
error: (error) => this.handleOperationError(modal, error),
|
||||
})
|
||||
@@ -953,6 +966,7 @@ export class BulkEditorComponent
|
||||
const count = this.getSelectionSize()
|
||||
this.executeDocumentAction(
|
||||
modal,
|
||||
'delete',
|
||||
this.documentService.deleteDocuments(this.getSelectionQuery()),
|
||||
(taskId) => this.trashService.trackMovedToTrash(taskId, count, documents)
|
||||
)
|
||||
@@ -995,6 +1009,7 @@ export class BulkEditorComponent
|
||||
modal.componentInstance.buttonsEnabled.set(false)
|
||||
this.executeDocumentAction(
|
||||
modal,
|
||||
'reprocess',
|
||||
this.documentService.reprocessDocuments(
|
||||
this.getSelectionQuery(),
|
||||
modal.componentInstance.remoteOcr
|
||||
@@ -1035,6 +1050,7 @@ export class BulkEditorComponent
|
||||
rotateDialog.buttonsEnabled.set(false)
|
||||
this.executeDocumentAction(
|
||||
modal,
|
||||
'rotate',
|
||||
this.documentService.rotateDocuments(
|
||||
this.getSelectionQuery(),
|
||||
rotateDialog.degrees
|
||||
@@ -1068,6 +1084,7 @@ export class BulkEditorComponent
|
||||
mergeDialog.buttonsEnabled.set(false)
|
||||
this.executeDocumentAction(
|
||||
modal,
|
||||
'merge',
|
||||
this.documentService.mergeDocuments(mergeDialog.documentIDs(), args)
|
||||
)
|
||||
})
|
||||
@@ -1090,6 +1107,7 @@ export class BulkEditorComponent
|
||||
.subscribe(() => {
|
||||
this.executeDocumentAction(
|
||||
modal,
|
||||
'merge_as_versions',
|
||||
this.documentService.mergeDocumentsAsVersions(
|
||||
mergeDialog.documentIDs(),
|
||||
mergeDialog.rootDocumentID()
|
||||
@@ -1118,7 +1136,7 @@ export class BulkEditorComponent
|
||||
dialog.selection = this.getSelectionQuery()
|
||||
dialog.selectionCount = this.getSelectionSize()
|
||||
dialog.succeeded.subscribe(({ task_id }) =>
|
||||
this.handleOperationQueued(null, task_id)
|
||||
this.handleOperationQueued(null, task_id, 'modify_custom_fields')
|
||||
)
|
||||
dialog.failed.subscribe((error) => {
|
||||
this.toastService.showError(
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('TaskNotificationService', () => {
|
||||
service.complete(result())
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(popups.map((toast) => toast.content)).toEqual([
|
||||
'Operation completed.',
|
||||
"Operation 'Run workflow' completed.",
|
||||
])
|
||||
})
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('TaskNotificationService', () => {
|
||||
expect(notifications).toHaveLength(2)
|
||||
expect(notifications.map((toast) => toast.content)).toEqual([
|
||||
'Second queued',
|
||||
'Operation completed.',
|
||||
"Operation 'Run workflow' completed.",
|
||||
])
|
||||
})
|
||||
|
||||
@@ -145,4 +145,26 @@ describe('TaskNotificationService', () => {
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('identifies concurrent operations even when completion arrives before their queue responses', () => {
|
||||
service.complete({
|
||||
task_id: 'tags',
|
||||
operation: 'modify_tags',
|
||||
status: PaperlessTaskStatus.Success,
|
||||
})
|
||||
service.complete({
|
||||
task_id: 'correspondent',
|
||||
operation: 'set_correspondent',
|
||||
status: PaperlessTaskStatus.Success,
|
||||
})
|
||||
expect(notifications.map((toast) => toast.content)).toEqual([
|
||||
"Operation 'Set correspondent' completed.",
|
||||
"Operation 'Update tags' completed.",
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the generic fallback for unknown operation names', () => {
|
||||
service.complete({ ...result(), operation: 'future_operation' })
|
||||
expect(notifications[0].content).toBe('Operation completed.')
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import { inject, Injectable, OnDestroy } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { BulkOperationStatus } from '../data/bulk-operation'
|
||||
import { PaperlessTaskStatus } from '../data/paperless-task'
|
||||
import { getDocumentOperationLabel } from '../utils/document-operation'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
@@ -76,12 +77,23 @@ export class TaskNotificationService implements OnDestroy {
|
||||
success: Partial<Toast> = {}
|
||||
) {
|
||||
const succeeded = result.status === PaperlessTaskStatus.Success
|
||||
const operation = getDocumentOperationLabel(result.operation)
|
||||
let content: string
|
||||
if (succeeded) {
|
||||
content = operation
|
||||
? $localize`Operation '${operation}' completed.`
|
||||
: $localize`Operation completed.`
|
||||
} else if (result.status === PaperlessTaskStatus.Revoked) {
|
||||
content = operation
|
||||
? $localize`Operation '${operation}' revoked.`
|
||||
: $localize`Operation revoked.`
|
||||
} else {
|
||||
content = operation
|
||||
? $localize`Operation '${operation}' failed. Some changes may already have been applied.`
|
||||
: $localize`Operation failed. Some changes may already have been applied.`
|
||||
}
|
||||
this.show(result.task_id, {
|
||||
content: succeeded
|
||||
? $localize`Operation completed.`
|
||||
: result.status === PaperlessTaskStatus.Revoked
|
||||
? $localize`Operation revoked.`
|
||||
: $localize`Operation failed. Some changes may already have been applied.`,
|
||||
content,
|
||||
delay: succeeded ? 5000 : 10000,
|
||||
level: succeeded ? ToastLevel.Success : ToastLevel.Error,
|
||||
...(succeeded ? success : { error: result.error }),
|
||||
|
||||
@@ -24,7 +24,9 @@ export class TrashService {
|
||||
) {
|
||||
this.taskNotifications.showQueued(
|
||||
taskId,
|
||||
$localize`Bulk operation queued.`,
|
||||
count > 1
|
||||
? $localize`Bulk move to trash queued.`
|
||||
: $localize`Move to trash queued.`,
|
||||
this.movedToTrashToast(
|
||||
$localize`Moved ${count} document(s) to the trash`,
|
||||
documents
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getDocumentOperationLabel } from './document-operation'
|
||||
|
||||
describe('Document Operation Utils', () => {
|
||||
it('should retrieve the document operation label correctly for an existing operation, null otherwise', () => {
|
||||
const result = getDocumentOperationLabel('modify_custom_fields')
|
||||
expect(result).toBe('Update custom fields')
|
||||
|
||||
const resultNull = getDocumentOperationLabel('non_existing_operation')
|
||||
expect(resultNull).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
const OPERATION_LABELS: Record<string, string> = {
|
||||
set_correspondent: $localize`Set correspondent`,
|
||||
set_document_type: $localize`Set document type`,
|
||||
set_storage_path: $localize`Set storage path`,
|
||||
add_tag: $localize`Add tag`,
|
||||
remove_tag: $localize`Remove tag`,
|
||||
modify_tags: $localize`Update tags`,
|
||||
modify_custom_fields: $localize`Update custom fields`,
|
||||
set_permissions: $localize`Set permissions`,
|
||||
delete: $localize`Move to trash`,
|
||||
reprocess: $localize`Document reprocessing`,
|
||||
run_workflow: $localize`Run workflow`,
|
||||
rotate: $localize`Rotate documents`,
|
||||
merge: $localize`Merge documents`,
|
||||
merge_as_versions: $localize`Merge as versions`,
|
||||
split: $localize`Split documents`,
|
||||
delete_pages: $localize`Delete pages`,
|
||||
edit_pdf: $localize`Edit PDF`,
|
||||
remove_password: $localize`Remove password`,
|
||||
}
|
||||
|
||||
export function getDocumentOperationLabel(operation?: string): string | null {
|
||||
const label = operation ? OPERATION_LABELS[operation] : null
|
||||
return label ?? null
|
||||
}
|
||||
Reference in new issue
Block a user