From eae392469f3910cc0acf258b3852bd303d0138dd Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:41:55 -0700 Subject: [PATCH] Update the frontend for the matching stuff --- .../ocr-template-editor.component.html | 6 ++- .../ocr-template-editor.component.spec.ts | 29 +++++++++----- .../ocr-template-editor.component.ts | 39 ++++++++++++------- .../ocr-templates.component.html | 4 +- .../ocr-templates.component.spec.ts | 19 ++++----- .../ocr-templates/ocr-templates.component.ts | 23 ++++++----- src-ui/src/app/data/ocr-template.ts | 6 +-- 7 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html index e1231d4ef..2b7b159ec 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html @@ -22,7 +22,11 @@ - + + @if (patternRequired) { + + + }

diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts index 75e972384..f0e447975 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts @@ -9,6 +9,12 @@ import { of, Subject, throwError } from 'rxjs' import { PermissionsFormComponent } from 'src/app/components/common/input/permissions/permissions-form/permissions-form.component' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { Document } from 'src/app/data/document' +import { + MATCH_ANY, + MATCH_AUTO, + MATCH_LITERAL, + MATCH_NONE, +} from 'src/app/data/matching-model' import { OcrTemplate, OcrTemplateZone, @@ -20,7 +26,6 @@ import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { User } from 'src/app/data/user' import { PermissionsService } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' -import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentService } from 'src/app/services/rest/document.service' import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service' import { UserService } from 'src/app/services/rest/user.service' @@ -80,9 +85,6 @@ describe('OcrTemplateEditorComponent', () => { jest .spyOn(TestBed.inject(CustomFieldsService), 'listAll') .mockReturnValue(of({ count: 2, all: [1, 2], results: customFields })) - jest - .spyOn(TestBed.inject(DocumentTypeService), 'listAll') - .mockReturnValue(of({ count: 0, all: [], results: [] })) jest .spyOn(TestBed.inject(UserService), 'listAll') .mockReturnValue(of({ count: 0, all: [], results: [] })) @@ -90,7 +92,7 @@ describe('OcrTemplateEditorComponent', () => { documentService = TestBed.inject(DocumentService) jest .spyOn(documentService, 'get') - .mockReturnValue(of({ id: 5, page_count: 3, document_type: 2 })) + .mockReturnValue(of({ id: 5, page_count: 3 })) templateService = TestBed.inject(OcrTemplateService) modalService = TestBed.inject(NgbModal) activeModal.close.mockReset() @@ -132,7 +134,8 @@ describe('OcrTemplateEditorComponent', () => { ...component.template, id: 3, name: 'Invoices', - document_type: 1, + match: 'Northwind', + matching_algorithm: MATCH_LITERAL, sample_document: 5, zones: [zone()], combine_formats: { '1': '{Invoice number}' }, @@ -144,7 +147,7 @@ describe('OcrTemplateEditorComponent', () => { expect(component.previewDocId).toEqual(5) expect(component.previewPageCount()).toEqual(3) expect(component.pageImageUrl).toContain('document-page-image/5/0/') - expect(component.template.document_type).toEqual(1) + expect(component.template.match).toEqual('Northwind') expect(component.isDirty).toBeFalsy() component.template.zones[0].x = 500 @@ -153,9 +156,15 @@ describe('OcrTemplateEditorComponent', () => { expect(original.combine_formats['1']).toEqual('{Invoice number}') }) - it('should fill the document type from a new sample document', () => { - component.setSampleDocument([5]) - expect(component.template.document_type).toEqual(2) + it('should offer every matching algorithm except automatic', () => { + expect(component.matchingAlgorithms.map((a) => a.id)).not.toContain( + MATCH_AUTO + ) + expect(component.template.matching_algorithm).toEqual(MATCH_ANY) + expect(component.patternRequired).toBeTruthy() + + component.template.matching_algorithm = MATCH_NONE + expect(component.patternRequired).toBeFalsy() }) it('should initialize new template permissions from saved defaults', () => { diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts index e5c5099b3..c81d41202 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts @@ -24,6 +24,7 @@ import { Subject, takeUntil } from 'rxjs' import { CustomFieldEditDialogComponent } from 'src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { DocumentLinkComponent } from 'src/app/components/common/input/document-link/document-link.component' +import { CheckComponent } from 'src/app/components/common/input/check/check.component' import { NumberComponent } from 'src/app/components/common/input/number/number.component' import { PermissionsFormComponent, @@ -33,7 +34,12 @@ import { SelectComponent } from 'src/app/components/common/input/select/select.c import { SwitchComponent } from 'src/app/components/common/input/switch/switch.component' import { TextComponent } from 'src/app/components/common/input/text/text.component' import { CustomField } from 'src/app/data/custom-field' -import { DocumentType } from 'src/app/data/document-type' +import { + MATCH_ANY, + MATCH_AUTO, + MATCH_NONE, + MATCHING_ALGORITHMS, +} from 'src/app/data/matching-model' import { DEFAULT_OCR_ZONE_LANGUAGE, getZoneFieldKey, @@ -51,7 +57,6 @@ import { User } from 'src/app/data/user' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { PermissionsService } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' -import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentService } from 'src/app/services/rest/document.service' import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service' import { UserService } from 'src/app/services/rest/user.service' @@ -120,6 +125,10 @@ export const ZONE_LANGUAGE_OPTIONS = [ { id: 'nld', name: $localize`Dutch` }, ] +const TEMPLATE_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter( + (algorithm) => algorithm.id !== MATCH_AUTO +) + const NO_OVERLAY_INTERACTION: OverlayInteraction = { kind: 'idle' } const RESIZE_CURSOR: Record = { nw: 'nw-resize', @@ -138,6 +147,7 @@ const RESIZE_CURSOR: Record = { DocumentLinkComponent, NumberComponent, TextComponent, + CheckComponent, SelectComponent, SwitchComponent, CommonModule, @@ -155,7 +165,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { public readonly activeModal = inject(NgbActiveModal) private readonly templateService = inject(OcrTemplateService) private readonly customFieldsService = inject(CustomFieldsService) - private readonly documentTypeService = inject(DocumentTypeService) private readonly documentService = inject(DocumentService) private readonly toastService = inject(ToastService) private readonly modalService = inject(NgbModal) @@ -191,7 +200,9 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { template: OcrTemplate = { id: null, name: '', - document_type: null, + match: '', + matching_algorithm: MATCH_ANY, + is_insensitive: true, sample_document: null, source_width: 0, source_height: 0, @@ -209,7 +220,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { .filter((field) => OCR_SUPPORTED_FIELD_TYPES.includes(field.data_type)) .map((field) => ({ id: field.id, name: field.name })), ]) - public readonly documentTypes = signal([]) public readonly users = signal([]) permissionsForm: PermissionsFormObject = {} public readonly OcrZoneTransform = OcrZoneTransform @@ -244,6 +254,17 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { return ZONE_LANGUAGE_OPTIONS } + get matchingAlgorithms() { + return TEMPLATE_MATCHING_ALGORITHMS + } + + get patternRequired(): boolean { + return ( + this.template.matching_algorithm !== MATCH_AUTO && + this.template.matching_algorithm !== MATCH_NONE + ) + } + public get previewPageDisplay(): number { return this.previewPage + 1 } @@ -310,13 +331,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { this.customFields.set(r.results) }) - this.documentTypeService - .listAll() - .pipe(takeUntil(this.destroy$)) - .subscribe((r) => { - this.documentTypes.set(r.results) - }) - if (this.object) { this.template = { ...this.object, @@ -380,7 +394,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { next: (doc) => { if (this.previewDocId !== id) return this.previewPageCount.set(doc.page_count ?? null) - this.template.document_type ??= doc.document_type }, error: () => { if (this.previewDocId === id) this.pageFailed.set(true) diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html index 222fd2167..8d7615883 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html @@ -14,7 +14,7 @@
  • Name
    -
    Document Type
    +
    Matching
    Zones
    Status
    Actions
    @@ -32,7 +32,7 @@
  • -
    {{getDocumentTypeName(template)}}
    +
    {{getMatching(template)}}
    {{template.zones.length}}
    diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts index 203ada53f..5d04a58a8 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.spec.ts @@ -5,11 +5,11 @@ import { By } from '@angular/platform-browser' import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' +import { MATCH_LITERAL, MATCH_NONE } from 'src/app/data/matching-model' import { OcrTemplate } from 'src/app/data/ocr-template' import { User } from 'src/app/data/user' import { DirtyFormGuard } from 'src/app/guards/dirty-form.guard' import { PermissionsService } from 'src/app/services/permissions.service' -import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service' import { UserService } from 'src/app/services/rest/user.service' import { ToastService } from 'src/app/services/toast.service' @@ -21,7 +21,8 @@ const templates: OcrTemplate[] = [ { id: 1, name: 'Invoices', - document_type: 2, + match: 'Northwind', + matching_algorithm: MATCH_LITERAL, sample_document: null, source_width: 0, source_height: 0, @@ -64,11 +65,6 @@ describe('OcrTemplatesComponent', () => { jest .spyOn(ocrTemplateService, 'listAll') .mockReturnValue(of({ count: 1, all: [1], results: templates })) - jest - .spyOn(TestBed.inject(DocumentTypeService), 'listAll') - .mockReturnValue( - of({ count: 1, all: [2], results: [{ id: 2, name: 'Invoice' }] }) - ) modalService = TestBed.inject(NgbModal) modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) toastService = TestBed.inject(ToastService) @@ -79,10 +75,15 @@ describe('OcrTemplatesComponent', () => { afterEach(() => modalService.dismissAll()) - it('should show templates with their document type', () => { + it('should show templates with their matching', () => { const text = fixture.nativeElement.textContent expect(text).toContain('Invoices') - expect(text).toContain('Invoice') + expect(text).toContain('Exact match: Northwind') + + expect( + component.getMatching({ ...templates[0], matching_algorithm: MATCH_NONE }) + ).toEqual('None') + expect(component.getMatching({ ...templates[0], match: '' })).toEqual('-') }) it.each([ diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts index 06c646b93..f3684c3ab 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.ts @@ -3,7 +3,7 @@ import { FormsModule } from '@angular/forms' import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { firstValueFrom, takeUntil, tap } from 'rxjs' -import { DocumentType } from 'src/app/data/document-type' +import { MATCH_NONE, MATCHING_ALGORITHMS } from 'src/app/data/matching-model' import { OcrTemplate } from 'src/app/data/ocr-template' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { DirtyFormGuard } from 'src/app/guards/dirty-form.guard' @@ -12,7 +12,6 @@ import { PermissionType, PermissionsService, } from 'src/app/services/permissions.service' -import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service' import { ToastService } from 'src/app/services/toast.service' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' @@ -36,20 +35,14 @@ export class OcrTemplatesComponent implements OnInit { private readonly ocrTemplateService = inject(OcrTemplateService) - private readonly documentTypeService = inject(DocumentTypeService) public readonly permissionsService = inject(PermissionsService) private readonly modalService = inject(NgbModal) private readonly dirtyFormGuard = inject(DirtyFormGuard) private readonly toastService = inject(ToastService) public readonly templates = signal([]) - private readonly documentTypes = signal([]) ngOnInit() { - this.documentTypeService - .listAll() - .pipe(takeUntil(this.unsubscribeNotifier)) - .subscribe((r) => this.documentTypes.set(r.results)) this.reload() } @@ -67,11 +60,17 @@ export class OcrTemplatesComponent }) } - getDocumentTypeName(template: OcrTemplate): string { - return ( - this.documentTypes().find((dt) => dt.id === template.document_type) - ?.name ?? '' + getMatching(template: OcrTemplate): string { + if (template.matching_algorithm === MATCH_NONE) { + return $localize`None` + } + if (!template.match) { + return '-' + } + const algorithm = MATCHING_ALGORITHMS.find( + (a) => a.id === template.matching_algorithm ) + return `${algorithm?.shortName}: ${template.match}` } editTemplate(template?: OcrTemplate) { diff --git a/src-ui/src/app/data/ocr-template.ts b/src-ui/src/app/data/ocr-template.ts index 5baaafa5d..933bbc072 100644 --- a/src-ui/src/app/data/ocr-template.ts +++ b/src-ui/src/app/data/ocr-template.ts @@ -1,4 +1,5 @@ import { CustomFieldDataType } from './custom-field' +import { MatchingModel } from './matching-model' import { ObjectWithPermissions } from './object-with-permissions' export enum OcrZoneTarget { @@ -64,9 +65,8 @@ export interface OcrTemplateZone { zone_source_height?: number } -export interface OcrTemplate extends ObjectWithPermissions { +export interface OcrTemplate extends MatchingModel, ObjectWithPermissions { name: string - document_type: number sample_document: number | null source_width: number source_height: number @@ -109,6 +109,6 @@ export interface OcrZoneRunResult { export interface OcrZoneRunResponse { status: 'completed' | 'skipped' - reason?: 'no_document_type' | 'no_template' + reason?: 'no_template' results: OcrZoneRunResult[] }