From e89495dc02b8f71354e00ac67eeb7d6e55a73b37 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:32:48 -0700 Subject: [PATCH] Frontend use the bulk edit --- .../bulk-editor/bulk-editor.component.html | 2 +- .../bulk-editor/bulk-editor.component.spec.ts | 101 ++++-------------- .../bulk-editor/bulk-editor.component.ts | 47 +------- .../src/app/services/rest/document.service.ts | 6 ++ 4 files changed, 35 insertions(+), 121 deletions(-) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index fe8367345..afe325a7f 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -98,7 +98,7 @@ - diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts index 7ec590631..d3fcd5f08 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -1286,91 +1286,36 @@ describe('BulkEditorComponent', () => { ) // listAllFilteredIds }) - it('finishes bulk zone OCR despite skipped and failed documents and refreshes the list', () => { + it('should support run zone ocr', () => { + let modal: NgbModalRef + modalService.activeInstances.subscribe((m) => (modal = m[0])) + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest - .spyOn(documentListViewService, 'selected', 'get') - .mockReturnValue(new Set([3, 4, 5, 6])) - const modalSpy = jest.spyOn(modalService, 'open') - const refreshSpy = jest - .spyOn(component as any, 'handleOperationSuccess') - .mockImplementation(() => {}) - const errorSpy = jest.spyOn(toastService, 'showError') - - component.runZoneOcrSelected() - const modal = modalSpy.mock.results[0].value - modal.componentInstance.confirm() - const requests = [3, 4, 5, 6].map((id) => - httpTestingController.expectOne( - `${environment.apiBaseUrl}documents/${id}/run-zone-ocr/` - ) - ) - requests[1].flush({ - status: 'skipped', - reason: 'no_document_type', - results: [], - }) - requests[2].flush({ status: 'skipped', reason: 'no_template', results: [] }) - requests[3].flush( - { error: 'OCR failed' }, - { status: 500, statusText: 'Server Error' } - ) - - expect(requests[0].cancelled).toBe(false) - expect(refreshSpy).not.toHaveBeenCalled() - requests[0].flush({ status: 'completed', results: [] }) - - expect(refreshSpy).toHaveBeenCalledWith(modal, false) - expect(errorSpy).toHaveBeenCalledWith( - 'Zone OCR: 1 completed, 2 skipped, 1 failed. Failed documents: 6', - [{ document: 6, error: { error: 'OCR failed' } }] - ) - }) - - it.each([ - [403, 'Permission denied'], - [404, 'Document file not found'], - [500, 'OCR failed'], - ])('reports a %s OCR error as a failure, not a skip', (status, message) => { - jest - .spyOn(documentListViewService, 'selected', 'get') - .mockReturnValue(new Set([3])) - const modalSpy = jest.spyOn(modalService, 'open') - jest - .spyOn(component as any, 'handleOperationSuccess') - .mockImplementation(() => {}) - const errorSpy = jest.spyOn(toastService, 'showError') - - component.runZoneOcrSelected() - modalSpy.mock.results[0].value.componentInstance.confirm() - httpTestingController - .expectOne(`${environment.apiBaseUrl}documents/3/run-zone-ocr/`) - .flush({ error: message }, { status, statusText: 'Error' }) - - expect(errorSpy).toHaveBeenCalledWith( - 'Zone OCR: 0 completed, 0 skipped, 1 failed. Failed documents: 3', - [{ document: 3, error: { error: message } }] - ) - }) - - it('reports a successful bulk zone OCR run', () => { + .spyOn(documentListViewService, 'documents', 'get') + .mockReturnValue([{ id: 3 }, { id: 4 }]) jest .spyOn(documentListViewService, 'selected', 'get') .mockReturnValue(new Set([3, 4])) jest - .spyOn(documentService, 'runZoneOcr') - .mockReturnValue(of({ status: 'completed', results: [] })) - const modalSpy = jest.spyOn(modalService, 'open') - jest - .spyOn(component as any, 'handleOperationSuccess') - .mockImplementation(() => {}) - const infoSpy = jest.spyOn(toastService, 'showInfo') - + .spyOn(permissionsService, 'currentUserHasObjectPermissions') + .mockReturnValue(true) + fixture.detectChanges() component.runZoneOcrSelected() - modalSpy.mock.results[0].value.componentInstance.confirm() - - expect(infoSpy).toHaveBeenCalledWith( - 'Zone OCR: 2 completed, 0 skipped, 0 failed.' + expect(modal).not.toBeUndefined() + modal.componentInstance.confirm() + let req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/run_zone_ocr/` ) + req.flush(true) + expect(req.request.body).toEqual({ + documents: [3, 4], + }) + httpTestingController.match( + `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` + ) // list reload + httpTestingController.match( + `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id` + ) // listAllFilteredIds }) it('should support rotate', () => { diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 2cf6d3c17..5e78e5e60 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -21,12 +21,9 @@ import { import { saveAs } from 'file-saver' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { - catchError, first, - forkJoin, map, Observable, - of, Subject, switchMap, takeUntil, @@ -970,54 +967,20 @@ export class BulkEditorComponent } runZoneOcrSelected() { - const ids = Array.from(this.list.selected) - if (!ids.length) return const modal = this.modalService.open(ConfirmDialogComponent, { backdrop: 'static', }) - modal.componentInstance.title = $localize`Run Zone OCR` - modal.componentInstance.messageBold = $localize`Run zone OCR on ${this.getSelectionSize()} selected document(s)?` - modal.componentInstance.message = $localize`Each document's type template (if it has one) is applied, overwriting the mapped fields.` + modal.componentInstance.title = $localize`Run zone OCR confirm` + modal.componentInstance.messageBold = $localize`This operation will run zone OCR on ${this.getSelectionSize()} selected document(s).` + modal.componentInstance.message = $localize`Fields mapped by each document type's OCR template will be overwritten.` + modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.confirmClicked .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe(() => { - modal.componentInstance.buttonsEnabled.set(false) this.executeDocumentAction( modal, - forkJoin( - ids.map((id) => - this.documentService.runZoneOcr(id).pipe( - map(({ status }) => ({ id, status })), - catchError((error) => - of({ id, status: 'failed' as const, error }) - ) - ) - ) - ).pipe( - tap((results) => { - const completed = results.filter( - (r) => r.status === 'completed' - ).length - const skipped = results.filter( - (r) => r.status === 'skipped' - ).length - const failed = results.filter((r) => r.status === 'failed') - const summary = $localize`Zone OCR: ${completed} completed, ${skipped} skipped, ${failed.length} failed.` - if (failed.length) { - const failedIds = failed.map((r) => r.id).join(', ') - this.toastService.showError( - `${summary} ${$localize`Failed documents: ${failedIds}`}`, - failed.map((r) => ({ - document: r.id, - error: r.error.error ?? r.error.message, - })) - ) - } else { - this.toastService.showInfo(summary) - } - }) - ) + this.documentService.runZoneOcrDocuments(this.getSelectionQuery()) ) }) } diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 611df90c8..315d31bb9 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -361,6 +361,12 @@ export class DocumentService extends AbstractPaperlessService { }) } + runZoneOcrDocuments(selection: DocumentSelectionQuery) { + return this.http.post(this.getResourceUrl(null, 'run_zone_ocr'), { + ...selection, + }) + } + runZoneOcr(id: number): Observable { return this.http.post( this.getResourceUrl(id, 'run-zone-ocr'),