From f0597a7c54feae376d36feb595c233a4d16640d6 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 20 Oct 2026 14:00:25 -0700
Subject: [PATCH] doc detail use it this way
---
.../zone-ocr-confirm-dialog.component.html | 30 +++++++++++
.../zone-ocr-confirm-dialog.component.spec.ts | 52 +++++++++++++++++++
.../zone-ocr-confirm-dialog.component.ts | 31 +++++++++++
.../document-detail.component.html | 4 +-
.../document-detail.component.spec.ts | 19 ++++++-
.../document-detail.component.ts | 31 +++++------
.../services/rest/document.service.spec.ts | 14 +++++
.../src/app/services/rest/document.service.ts | 7 ++-
8 files changed, 168 insertions(+), 20 deletions(-)
create mode 100644 src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.html
create mode 100644 src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.spec.ts
create mode 100644 src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.ts
diff --git a/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.html b/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.html
new file mode 100644
index 000000000..0f79d2929
--- /dev/null
+++ b/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.html
@@ -0,0 +1,30 @@
+
+
+ @if (messageBold) {
+
{{messageBold}}
+ }
+ @if (message) {
+
+ }
+
+
+
diff --git a/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.spec.ts b/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.spec.ts
new file mode 100644
index 000000000..dfe055523
--- /dev/null
+++ b/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.spec.ts
@@ -0,0 +1,52 @@
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
+import { provideHttpClientTesting } from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { of } from 'rxjs'
+import { MATCH_NONE } from 'src/app/data/matching-model'
+import { OcrTemplate } from 'src/app/data/ocr-template'
+import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service'
+import { ZoneOcrConfirmDialogComponent } from './zone-ocr-confirm-dialog.component'
+
+const templates: OcrTemplate[] = [
+ {
+ id: 1,
+ name: 'Invoices',
+ match: '',
+ matching_algorithm: MATCH_NONE,
+ sample_document: null,
+ source_width: 0,
+ source_height: 0,
+ enabled: true,
+ zones: [],
+ },
+]
+
+describe('ZoneOcrConfirmDialogComponent', () => {
+ let component: ZoneOcrConfirmDialogComponent
+ let fixture: ComponentFixture
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ NgbActiveModal,
+ provideHttpClient(withInterceptorsFromDi()),
+ provideHttpClientTesting(),
+ ],
+ imports: [ZoneOcrConfirmDialogComponent],
+ })
+ jest
+ .spyOn(TestBed.inject(OcrTemplateService), 'listAll')
+ .mockReturnValue(of({ count: 1, all: [1], results: templates }))
+
+ fixture = TestBed.createComponent(ZoneOcrConfirmDialogComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should offer templates and default to all matching templates', () => {
+ expect(component.templates()).toEqual(templates)
+ expect(component.template).toBeNull()
+ expect(fixture.nativeElement.textContent).toContain('OCR template')
+ })
+})
diff --git a/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.ts b/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.ts
new file mode 100644
index 000000000..939472b21
--- /dev/null
+++ b/src-ui/src/app/components/common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-confirm-dialog.component.ts
@@ -0,0 +1,31 @@
+import { Component, OnInit, inject, signal } from '@angular/core'
+import { FormsModule } from '@angular/forms'
+import { takeUntil } from 'rxjs'
+import { OcrTemplate } from 'src/app/data/ocr-template'
+import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service'
+import { SelectComponent } from '../../input/select/select.component'
+import { ConfirmDialogComponent } from '../confirm-dialog.component'
+
+@Component({
+ selector: 'pngx-zone-ocr-confirm-dialog',
+ templateUrl: './zone-ocr-confirm-dialog.component.html',
+ imports: [FormsModule, SelectComponent],
+})
+export class ZoneOcrConfirmDialogComponent
+ extends ConfirmDialogComponent
+ implements OnInit
+{
+ private readonly ocrTemplateService = inject(OcrTemplateService)
+
+ public readonly templates = signal([])
+
+ /** The template to run, or null for every template that matches. */
+ template: number | null = null
+
+ ngOnInit() {
+ this.ocrTemplateService
+ .listAll()
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe((r) => this.templates.set(r.results))
+ }
+}
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 5468950b7..37dcd7b6b 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
@@ -64,8 +64,8 @@
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 034af9df8..f918b5954 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
@@ -1916,9 +1916,17 @@ describe('DocumentDetailComponent', () => {
.mockImplementation(() => {})
const notifySpy = jest.spyOn(component.docChangeNotifier, 'next')
const toastSpy = jest.spyOn(toastService, 'showInfo')
+ const modalSpy = jest.spyOn(modalService, 'open')
component.runZoneOcr()
+ const dialog = modalSpy.mock.results[0].value.componentInstance
+ expect(dialog.message).toBe(
+ 'Fields filled by the template will be overwritten.'
+ )
+ dialog.template = 3
+ dialog.confirmClicked.emit()
+ expect(documentService.runZoneOcr).toHaveBeenCalledWith(doc.id, 3)
expect(toastSpy).toHaveBeenCalledWith(
'Filled 1 of 2 fields. No value found for: Due'
)
@@ -1936,9 +1944,12 @@ describe('DocumentDetailComponent', () => {
jest.spyOn(component as any, 'loadDocument').mockImplementation(() => {})
const runSpy = jest.spyOn(documentService, 'runZoneOcr')
const toastSpy = jest.spyOn(toastService, 'showInfo')
+ const modalSpy = jest.spyOn(modalService, 'open')
runSpy.mockReturnValueOnce(of({ status: 'completed', results: [] }))
component.runZoneOcr()
+ modalSpy.mock.results[0].value.componentInstance.confirmClicked.emit()
+ expect(runSpy).toHaveBeenCalledWith(doc.id, null)
expect(toastSpy).toHaveBeenLastCalledWith(
'Zone OCR found no fields to fill.'
)
@@ -1953,6 +1964,7 @@ describe('DocumentDetailComponent', () => {
})
)
component.runZoneOcr()
+ modalSpy.mock.results[1].value.componentInstance.confirmClicked.emit()
expect(toastSpy).toHaveBeenLastCalledWith('Filled 1 of 1 fields.')
})
@@ -1988,13 +2000,14 @@ describe('DocumentDetailComponent', () => {
component.runZoneOcr()
modalSpy.mock.results[0].value.componentInstance.confirmClicked.emit()
- expect(runSpy).toHaveBeenCalledWith(doc.id)
+ expect(runSpy).toHaveBeenCalledWith(doc.id, null)
expect(component.zoneOcrRunning()).toBe(true)
expect(component.networkActive()).toBe(true)
const saveSpy = jest.spyOn(documentService, 'patch')
component.save()
expect(saveSpy).not.toHaveBeenCalled()
component.runZoneOcr()
+ expect(modalSpy).toHaveBeenCalledTimes(1)
expect(runSpy).toHaveBeenCalledTimes(1)
pending.error(new Error('failed'))
expect(component.zoneOcrRunning()).toBe(false)
@@ -2008,8 +2021,10 @@ describe('DocumentDetailComponent', () => {
const pending = new Subject()
jest.spyOn(documentService, 'runZoneOcr').mockReturnValue(pending)
const loadSpy = jest.spyOn(component as any, 'loadDocument')
+ const modalSpy = jest.spyOn(modalService, 'open')
component.runZoneOcr()
+ modalSpy.mock.results[0].value.componentInstance.confirmClicked.emit()
component.documentId.set(99)
pending.next({ results: [] })
@@ -2048,8 +2063,10 @@ describe('DocumentDetailComponent', () => {
.mockReturnValue(throwError(() => new Error('nope')))
const loadSpy = jest.spyOn(component as any, 'loadDocument')
const errorSpy = jest.spyOn(toastService, 'showError')
+ const modalSpy = jest.spyOn(modalService, 'open')
component.runZoneOcr()
+ modalSpy.mock.results[0].value.componentInstance.confirmClicked.emit()
expect(errorSpy).toHaveBeenCalledWith(
'Error running zone OCR.',
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 518d517a6..6d3078c6a 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
@@ -105,6 +105,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 { ZoneOcrConfirmDialogComponent } from '../common/confirm-dialog/zone-ocr-confirm-dialog/zone-ocr-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'
@@ -1565,34 +1566,34 @@ export class DocumentDetailComponent
runZoneOcr() {
if (this.networkActive()) return
const documentId = this.documentId()
- if (!this.hasLocalEdits(this.document())) {
- this.executeZoneOcr(documentId)
- return
- }
-
- const modal = this.modalService.open(ConfirmDialogComponent, {
+ const modal = this.modalService.open(ZoneOcrConfirmDialogComponent, {
backdrop: 'static',
})
- modal.componentInstance.title = $localize`Unsaved Changes`
- modal.componentInstance.messageBold = $localize`You have unsaved changes.`
- modal.componentInstance.message = $localize`Running Zone OCR will discard them.`
- modal.componentInstance.btnClass = 'btn-warning'
- modal.componentInstance.btnCaption = $localize`Run Zone OCR`
- modal.componentInstance.confirmClicked
+ const dialog = modal.componentInstance as ZoneOcrConfirmDialogComponent
+ dialog.title = $localize`Run Zone OCR`
+ dialog.btnCaption = $localize`Run Zone OCR`
+ if (this.hasLocalEdits(this.document())) {
+ dialog.messageBold = $localize`You have unsaved changes.`
+ dialog.message = $localize`Running Zone OCR will discard them.`
+ dialog.btnClass = 'btn-warning'
+ } else {
+ dialog.message = $localize`Fields filled by the template will be overwritten.`
+ }
+ dialog.confirmClicked
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
modal.close()
if (this.documentId() === documentId && !this.networkActive()) {
- this.executeZoneOcr(documentId)
+ this.executeZoneOcr(documentId, dialog.template)
}
})
}
- private executeZoneOcr(documentId: number) {
+ private executeZoneOcr(documentId: number, templateId: number | null) {
this.zoneOcrRunning.set(true)
this.networkActive.set(true)
this.documentsService
- .runZoneOcr(documentId)
+ .runZoneOcr(documentId, templateId)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (response) => {
diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts
index 4250d8e84..2fa69e9b2 100644
--- a/src-ui/src/app/services/rest/document.service.spec.ts
+++ b/src-ui/src/app/services/rest/document.service.spec.ts
@@ -563,6 +563,20 @@ describe(`DocumentService`, () => {
req.flush('task-id')
})
+
+ it('should call appropriate api endpoint for running zone OCR', () => {
+ const url = `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/run-zone-ocr/`
+ subscription = service.runZoneOcr(documents[0].id).subscribe()
+ let req = httpTestingController.expectOne(url)
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({})
+ req.flush({ status: 'completed', results: [] })
+
+ subscription = service.runZoneOcr(documents[0].id, 3).subscribe()
+ req = httpTestingController.expectOne(url)
+ expect(req.request.body).toEqual({ template: 3 })
+ req.flush({ status: 'completed', results: [] })
+ })
})
it('should construct sort fields respecting permissions', () => {
diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts
index 315d31bb9..30520c2e6 100644
--- a/src-ui/src/app/services/rest/document.service.ts
+++ b/src-ui/src/app/services/rest/document.service.ts
@@ -367,10 +367,13 @@ export class DocumentService extends AbstractPaperlessService {
})
}
- runZoneOcr(id: number): Observable {
+ runZoneOcr(
+ id: number,
+ templateId: number | null = null
+ ): Observable {
return this.http.post(
this.getResourceUrl(id, 'run-zone-ocr'),
- {}
+ templateId ? { template: templateId } : {}
)
}