Frontend use the bulk edit

This commit is contained in:
shamoon committed 2026-10-20 10:32:48 -07:00
1 parent 8bb2f88015
commit e89495dc02
4 files changed
+35 -121

No files matched your search

@@ -98,7 +98,7 @@
<button ngbDropdownItem (click)="mergeSelectedAsVersions()" [disabled]="!userOwnsAll || !userCanEditAll || !userCanDelete || list.allSelected || list.selectedCount < 2">
<i-bs name="journal-bookmark-fill" class="me-1"></i-bs><ng-container i18n>Merge as versions</ng-container>
</button>
<button ngbDropdownItem (click)="runZoneOcrSelected()" [disabled]="!userCanEditAll || list.allSelected">
<button ngbDropdownItem (click)="runZoneOcrSelected()" [disabled]="!userCanEditAll">
<i-bs name="file-earmark-ruled" class="me-1"></i-bs><ng-container i18n>Run Zone OCR</ng-container>
</button>
</div>
@@ -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', () => {
@@ -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())
)
})
}
@@ -361,6 +361,12 @@ export class DocumentService extends AbstractPaperlessService<Document> {
})
}
runZoneOcrDocuments(selection: DocumentSelectionQuery) {
return this.http.post(this.getResourceUrl(null, 'run_zone_ocr'), {
...selection,
})
}
runZoneOcr(id: number): Observable<OcrZoneRunResponse> {
return this.http.post<OcrZoneRunResponse>(
this.getResourceUrl(id, 'run-zone-ocr'),