Better info for bulk tasks

This commit is contained in:
shamoon committed 2026-11-14 09:34:01 -08:00
1 parent 020a2c8fc0
commit 3a241e51d9
3 files changed
+142 -2

No files matched your search

@@ -180,6 +180,11 @@
@if (taskShowsSeparateTypeLabel(task)) {
<span class="me-2">{{ task.task_type_display }}</span>
}
@let documentCount = taskDocumentCount(task);
@if (documentCount !== null) {
<span class="me-2" i18n>{documentCount, plural, =1 {1 document} other {{{ documentCount }} documents}}</span>
<span class="me-2" aria-hidden="true">&middot;</span>
}
<span class="text-nowrap"><pngx-bs-icon class="me-1" width="1em" height="1em" name="box-arrow-in-right"></pngx-bs-icon>{{ task.trigger_source_display }}</span>
</div>
</td>
@@ -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
@@ -56,6 +56,27 @@ 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`,
@@ -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
}