mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-20 23:08:14 +00:00
Update the frontend for the matching stuff
This commit is contained in:
7 files changed
+76
-50
No files matched your search
+5
-1
@@ -22,7 +22,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pngx-input-select [(ngModel)]="template.document_type" [items]="documentTypes()" bindLabel="name" bindValue="id" title="Document type" i18n-title></pngx-input-select>
|
||||
<pngx-input-select [(ngModel)]="template.matching_algorithm" [items]="matchingAlgorithms" title="Matching algorithm" i18n-title></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text [(ngModel)]="template.match" title="Matching pattern" i18n-title></pngx-input-text>
|
||||
<pngx-input-check [(ngModel)]="template.is_insensitive" title="Case insensitive" i18n-title></pngx-input-check>
|
||||
}
|
||||
|
||||
<div *pngxIfOwner="object">
|
||||
<hr />
|
||||
|
||||
+19
-10
@@ -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', () => {
|
||||
|
||||
+26
-13
@@ -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<ResizeHandle, string> = {
|
||||
nw: 'nw-resize',
|
||||
@@ -138,6 +147,7 @@ const RESIZE_CURSOR: Record<ResizeHandle, string> = {
|
||||
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<DocumentType[]>([])
|
||||
public readonly users = signal<User[]>([])
|
||||
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)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col d-none d-sm-flex" i18n>Document Type</div>
|
||||
<div class="col d-none d-sm-flex" i18n>Matching</div>
|
||||
<div class="col d-none d-sm-flex" i18n>Zones</div>
|
||||
<div class="col" i18n>Status</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<li class="list-group-item">
|
||||
<div class="row fade" [class.show]="show()">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editTemplate(template)" [disabled]="!userCanEdit(template)">{{template.name}}</button></div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex">{{getDocumentTypeName(template)}}</div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex">{{getMatching(template)}}</div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex"><code>{{template.zones.length}}</code></div>
|
||||
<div class="col d-flex align-items-center">
|
||||
<div class="form-check form-switch mb-0">
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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<OcrTemplate[]>([])
|
||||
private readonly documentTypes = signal<DocumentType[]>([])
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
Reference in new issue
Block a user