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 @@ + + + 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 } : {} ) }