From 3a241e51d9ed1d9a5e4b1187ed032fb3298ee125 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 14 Nov 2026 08:52:22 -0800 Subject: [PATCH] Better info for bulk tasks --- .../admin/tasks/tasks.component.html | 5 ++ .../admin/tasks/tasks.component.spec.ts | 90 +++++++++++++++++++ .../components/admin/tasks/tasks.component.ts | 49 +++++++++- 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.html b/src-ui/src/app/components/admin/tasks/tasks.component.html index 7424668d21..cd2089c88b 100644 --- a/src-ui/src/app/components/admin/tasks/tasks.component.html +++ b/src-ui/src/app/components/admin/tasks/tasks.component.html @@ -180,6 +180,11 @@ @if (taskShowsSeparateTypeLabel(task)) { {{ task.task_type_display }} } + @let documentCount = taskDocumentCount(task); + @if (documentCount !== null) { + {documentCount, plural, =1 {1 document} other {{{ documentCount }} documents}} + + } {{ task.trigger_source_display }} diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts b/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts index 887e690982..fd4ede26c5 100644 --- a/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts +++ b/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts @@ -895,6 +895,96 @@ describe('TasksComponent', () => { expect(sanityTaskRow.nativeElement.textContent).toContain('System') }) + it.each([ + ['modify_tags', 12, 'Update tags', '12 documents'], + ['run_workflow', 1, 'Run workflow', '1 document'], + ['delete', 3, 'Move to trash', '3 documents'], + ])( + 'shows the operation, document count and outcome for %s', + (operation, count, label, summary) => { + const task: PaperlessTask = { + ...tasks[4], + task_type: + operation === 'delete' + ? PaperlessTaskType.BulkDelete + : PaperlessTaskType.BulkUpdate, + task_type_display: + operation === 'delete' ? 'Bulk Delete' : 'Bulk Update', + trigger_source: PaperlessTaskTriggerSource.WebUI, + trigger_source_display: 'Web UI', + input_data: { operation, document_count: count }, + result_data: { operation, document_count: count }, + } + component.setSection(TaskSection.Completed) + flushTaskLists(25, 1, [task]) + fixture.detectChanges() + + const name = fixture.nativeElement.querySelector('tbody .name-col') + expect(name.textContent).toContain(label) + expect(name.textContent).toContain(summary) + expect(name.textContent).toContain('Web UI') + expect(name.textContent).not.toContain(task.task_type_display) + expect( + name.closest('tr').querySelector('.results-col').textContent + ).toContain('Completed') + } + ) + + it('shows operation context while work is pending without reporting completion', () => { + const task = { + ...tasks[2], + task_type: PaperlessTaskType.BulkUpdate, + task_type_display: 'Bulk Update', + input_data: { operation: 'reprocess', document_count: 1 }, + } + component.setSection(TaskSection.InProgress) + flushTaskLists(25, 1, [task]) + fixture.detectChanges() + + const name = fixture.nativeElement.querySelector('tbody .name-col') + expect(name.textContent).toContain('Document reprocessing') + expect(name.textContent).toContain('1 document') + expect(component.taskResultMessage(task)).toBeNull() + }) + + it.each([{}, { operation: 'future_operation' }])( + 'preserves the fallback for legacy or unknown operation data: %j', + (input_data) => { + const task = { + ...tasks[4], + task_type: PaperlessTaskType.BulkUpdate, + task_type_display: 'Bulk Update', + input_data, + result_data: {}, + } + expect(component.taskDisplayName(task)).toBe('Bulk Update') + expect(component.taskDocumentCount(task)).toBeNull() + expect(component.taskResultMessage(task)).toBeNull() + } + ) + + it.each([-1, 1.5, '2', null, undefined])( + 'does not display an invalid document count: %s', + (document_count) => { + expect( + component.taskDocumentCount({ + ...tasks[4], + input_data: { operation: 'modify_tags', document_count }, + }) + ).toBeNull() + } + ) + + it('retains error details for failed operation tasks', () => { + expect( + component.taskResultMessage({ + ...tasks[0], + input_data: { operation: 'modify_tags', document_count: 12 }, + result_data: { error_message: 'Insufficient permissions' }, + }) + ).toBe('Insufficient permissions') + }) + it('should filter tasks by result', () => { component.setSection(TaskSection.NeedsAttention) component.filterTargetID = 1 diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.ts b/src-ui/src/app/components/admin/tasks/tasks.component.ts index ad8793ef85..568ce5daef 100644 --- a/src-ui/src/app/components/admin/tasks/tasks.component.ts +++ b/src-ui/src/app/components/admin/tasks/tasks.component.ts @@ -56,6 +56,27 @@ const FILTER_TARGETS = [ { id: TaskFilterTargetID.TaskId, name: $localize`Task ID` }, ] +const OPERATION_LABELS: Record = { + 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`, @@ -414,11 +435,28 @@ export class TasksComponent } taskDisplayName(task: PaperlessTask): string { - return task.input_data?.filename?.toString() || task.task_type_display + return ( + task.input_data?.filename?.toString() || + this.taskOperationLabel(task) || + task.task_type_display + ) } taskShowsSeparateTypeLabel(task: PaperlessTask): boolean { - return this.taskDisplayName(task) !== task.task_type_display + return ( + !this.taskOperationLabel(task) && + this.taskDisplayName(task) !== task.task_type_display + ) + } + + private taskOperationLabel(task: PaperlessTask): string | null { + const label = OPERATION_LABELS[task.input_data?.operation] + return typeof label === 'string' ? label : null + } + + taskDocumentCount(task: PaperlessTask): number | null { + const count = task.input_data?.document_count + return Number.isInteger(count) && count >= 0 ? count : null } taskResultMessage(task: PaperlessTask): string | null { @@ -446,6 +484,13 @@ export class TasksComponent return errorMessage } + if ( + task.status === PaperlessTaskStatus.Success && + this.taskOperationLabel(task) + ) { + return $localize`Completed` + } + return null }