From 3c4f5d89599810e92bcc6791966f077b64b0aab9 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Fri, 16 Oct 2026 13:45:51 -0800
Subject: [PATCH] Add to doc details
---
.../document-detail.component.html | 6 +++
.../document-detail.component.spec.ts | 21 ++++++++
.../document-detail.component.ts | 52 +++++++++++++++++++
3 files changed, 79 insertions(+)
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html
index 00267e76d7..630a97afbe 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.html
+++ b/src-ui/src/app/components/document-detail/document-detail.component.html
@@ -49,6 +49,12 @@
Reprocess
+ @if (manualWorkflows().length > 0) {
+
+ }
+
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
index 26a42336a2..4c766077a5 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
@@ -1030,6 +1030,27 @@ describe('DocumentDetailComponent', () => {
expect(modalCloseSpy).toHaveBeenCalled()
})
+ it('should run a workflow on the document, confirm and close modal', () => {
+ initNormally()
+ const bulkEditSpy = jest.spyOn(documentService, 'bulkEdit')
+ bulkEditSpy.mockReturnValue(of(true))
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const toastSpy = jest.spyOn(toastService, 'showInfo')
+ component.runWorkflow()
+ const modalCloseSpy = jest.spyOn(openModal, 'close')
+ openModal.componentInstance.workflowId = 5
+ openModal.componentInstance.force = true
+ openModal.componentInstance.confirmClicked.next()
+ expect(bulkEditSpy).toHaveBeenCalledWith(
+ { documents: [doc.id] },
+ 'run_workflow',
+ { workflow: 5, filter_documents: false }
+ )
+ expect(toastSpy).toHaveBeenCalled()
+ expect(modalCloseSpy).toHaveBeenCalled()
+ })
+
it('should pass remote OCR choice when reprocessing', () => {
initNormally()
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index 561fc6b681..6dbf5c6ac8 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -68,6 +68,8 @@ import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS, SuggestionSource } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user'
+import { Workflow } from 'src/app/data/workflow'
+import { WorkflowTriggerType } from 'src/app/data/workflow-trigger'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
@@ -93,6 +95,7 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
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 { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service'
@@ -104,6 +107,7 @@ import { DocumentDetailFieldID } from '../admin/settings/settings.component'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
import { ReprocessConfirmDialogComponent } from '../common/confirm-dialog/reprocess-confirm-dialog/reprocess-confirm-dialog.component'
+import { RunWorkflowConfirmDialogComponent } from '../common/confirm-dialog/run-workflow-confirm-dialog/run-workflow-confirm-dialog.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
@@ -233,6 +237,7 @@ export class DocumentDetailComponent
private componentRouterService = inject(ComponentRouterService)
private deviceDetectorService = inject(DeviceDetectorService)
private savedViewService = inject(SavedViewService)
+ private readonly workflowService = inject(WorkflowService)
private readonly websocketStatusService = inject(WebsocketStatusService)
private readonly useNativePdfViewerSetting = this.settings.getSignal(
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER
@@ -295,6 +300,7 @@ export class DocumentDetailComponent
readonly correspondents = signal(undefined)
readonly documentTypes = signal(undefined)
readonly storagePaths = signal(undefined)
+ readonly manualWorkflows = signal([])
documentForm: FormGroup = new FormGroup({
title: new FormControl(''),
@@ -836,6 +842,19 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe((result) => this.users.set(result.results))
}
+ if (
+ this.permissionsService.currentUserCan(
+ PermissionAction.View,
+ PermissionType.Workflow
+ )
+ ) {
+ this.workflowService
+ .list(1, 100000, null, null, {
+ trigger_type: WorkflowTriggerType.Manual,
+ })
+ .pipe(first(), takeUntil(this.unsubscribeNotifier))
+ .subscribe((result) => this.manualWorkflows.set(result.results))
+ }
this.getCustomFields()
@@ -1559,6 +1578,39 @@ export class DocumentDetailComponent
})
}
+ runWorkflow() {
+ const modal = this.modalService.open(RunWorkflowConfirmDialogComponent, {
+ backdrop: 'static',
+ })
+ modal.componentInstance.title = $localize`Run workflow`
+ modal.componentInstance.message = $localize`Run a workflow on "${this.document().title}".`
+ modal.componentInstance.btnCaption = $localize`Run`
+ modal.componentInstance.workflows = this.manualWorkflows()
+ modal.componentInstance.confirmClicked.subscribe(() => {
+ modal.componentInstance.buttonsEnabled.set(false)
+ this.documentsService
+ .bulkEdit({ documents: [this.document().id] }, 'run_workflow', {
+ workflow: modal.componentInstance.workflowId,
+ filter_documents: !modal.componentInstance.force,
+ })
+ .subscribe({
+ next: () => {
+ this.toastService.showInfo(
+ $localize`Workflow will run on "${this.document().title}" in the background.`
+ )
+ modal.close()
+ },
+ error: (error) => {
+ modal.componentInstance.buttonsEnabled.set(true)
+ this.toastService.showError(
+ $localize`Error executing operation`,
+ error
+ )
+ },
+ })
+ })
+ }
+
private getSelectedNonLatestVersionId(): number | null {
const versions = this.document()?.versions ?? []
if (!versions.length || !this.selectedVersionId()) {