Pngx conventions

This commit is contained in:
shamoon committed 2026-10-20 09:50:29 -07:00
1 parent 639827ec65
commit 123df2e826
5 files changed
+233 -278

No files matched your search

@@ -182,7 +182,7 @@ describe('OcrTemplateEditorComponent', () => {
expect(templates.update).toHaveBeenCalledWith(component.template)
expect(templates.create).not.toHaveBeenCalled()
expect(succeeded).toHaveBeenCalledWith(saved)
expect(activeModal.close).toHaveBeenCalledWith(saved)
expect(activeModal.close).toHaveBeenCalled()
})
it('searches across document metadata even when a template document type is set', () => {
@@ -527,7 +527,7 @@ describe('OcrTemplateEditorComponent', () => {
pending.next(saved)
pending.complete()
expect(succeeded).toHaveBeenCalledWith(saved)
expect(activeModal.close).toHaveBeenCalledWith(saved)
expect(activeModal.close).toHaveBeenCalled()
})
it('stops an active move and unlocks editing without losing changes after a failed save', () => {
@@ -153,6 +153,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
@Input() object: OcrTemplate
@Output() succeeded = new EventEmitter<OcrTemplate>()
@Output() failed = new EventEmitter()
@ViewChild('zoneOverlay') overlayRef: ElementRef<SVGSVGElement>
imageRef: ElementRef<HTMLImageElement>
@@ -856,13 +857,12 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
obs.pipe(takeUntil(this.destroy$)).subscribe({
next: (saved) => {
this.toastService.showInfo($localize`OCR template saved.`)
this.activeModal.close()
this.succeeded.emit(saved)
this.activeModal.close(saved)
},
error: (e) => {
this.saving.set(false)
this.toastService.showError($localize`Error saving OCR template.`, e)
this.failed.emit(e)
},
})
}
@@ -4,8 +4,8 @@
info="Define extraction zones on document types to automatically populate custom fields via OCR."
i18n-info
>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="createTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.OcrTemplate }">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Create Template</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.OcrTemplate }">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add Template</ng-container>
</button>
</pngx-page-header>
@@ -28,17 +28,17 @@
</li>
}
@for (t of templates(); track t.id) {
@for (template of templates(); track template.id) {
<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(t)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.OcrTemplate)">{{t.name}}</button></div>
<div class="col d-flex align-items-center d-none d-sm-flex">{{getDocumentTypeName(t)}}</div>
<div class="col d-flex align-items-center d-none d-sm-flex"><code>{{t.zones?.length || 0}}</code></div>
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.OcrTemplate)">{{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"><code>{{template.zones.length}}</code></div>
<div class="col d-flex align-items-center">
<div class="form-check form-switch mb-0">
<input type="checkbox" class="form-check-input cursor-pointer" [id]="t.id+'_enable'" [ngModel]="t.enabled" (ngModelChange)="toggleTemplate(t, $event)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.OcrTemplate }">
<label class="form-check-label cursor-pointer" [for]="t.id+'_enable'">
<code> @if(t.enabled) { <ng-container i18n>Enabled</ng-container> } @else { <span i18n class="text-muted">Disabled</span> }</code>
<input type="checkbox" class="form-check-input cursor-pointer" [id]="template.id+'_enable'" [(ngModel)]="template.enabled" (change)="toggleTemplateEnabled(template)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.OcrTemplate }">
<label class="form-check-label cursor-pointer" [for]="template.id+'_enable'">
<code> @if(template.enabled) { <ng-container i18n>Enabled</ng-container> } @else { <span i18n class="text-muted">Disabled</span> }</code>
</label>
</div>
</div>
@@ -46,21 +46,21 @@
<div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block">
<button type="button" class="btn btn-link" id="actionsMenuMobile{{t.id}}" (click)="$event.stopPropagation()" ngbDropdownToggle>
<button type="button" class="btn btn-link" id="actionsMenuMobile{{template.id}}" (click)="$event.stopPropagation()" ngbDropdownToggle>
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile{{t.id}}">
<button (click)="editTemplate(t)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.OcrTemplate }" ngbDropdownItem i18n>Edit</button>
<button (click)="deleteTemplate(t)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.OcrTemplate }" ngbDropdownItem i18n>Delete</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile{{template.id}}">
<button (click)="editTemplate(template)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.OcrTemplate }" ngbDropdownItem i18n>Edit</button>
<button (click)="deleteTemplate(template)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.OcrTemplate }" ngbDropdownItem i18n>Delete</button>
</div>
</div>
</div>
<div class="btn-toolbar d-none d-sm-flex gap-2" role="toolbar">
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.OcrTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(t)">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.OcrTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)">
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container>
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.OcrTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(t)">
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.OcrTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)">
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
</button>
</div>
@@ -1,8 +1,10 @@
import { EventEmitter, signal, WritableSignal } from '@angular/core'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbModal, NgbModalOptions } from '@ng-bootstrap/ng-bootstrap'
import { of, Subject } from 'rxjs'
import { DocumentType } from 'src/app/data/document-type'
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 { OcrTemplate } from 'src/app/data/ocr-template'
import { DirtyFormGuard } from 'src/app/guards/dirty-form.guard'
import { PermissionsService } from 'src/app/services/permissions.service'
@@ -13,211 +15,164 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
import { OcrTemplateEditorComponent } from './ocr-template-editor/ocr-template-editor.component'
import { OcrTemplatesComponent } from './ocr-templates.component'
describe('OcrTemplatesComponent', () => {
let fixture: ComponentFixture<OcrTemplatesComponent>
let component: OcrTemplatesComponent
let options: NgbModalOptions
const editor = {
object: undefined as OcrTemplate,
saving: signal(false),
isDirty: false,
succeeded: new EventEmitter<OcrTemplate>(),
}
const modal = { open: jest.fn() }
const dirtyFormGuard = { confirmChanges: jest.fn() }
const templates = { delete: jest.fn(), patch: jest.fn(), listAll: jest.fn() }
const documentTypes = { listAll: jest.fn() }
const toast = { showError: jest.fn() }
const templates: OcrTemplate[] = [
{
id: 1,
name: 'Invoices',
document_type: 2,
sample_document: null,
source_width: 0,
source_height: 0,
enabled: true,
zones: [],
},
]
beforeEach(async () => {
editor.object = undefined
editor.saving.set(false)
editor.isDirty = false
editor.succeeded = new EventEmitter<OcrTemplate>()
modal.open.mockReset().mockImplementation((_, modalOptions) => {
options = modalOptions
return { componentInstance: editor }
})
dirtyFormGuard.confirmChanges.mockReset().mockReturnValue(of(true))
templates.delete.mockReset()
templates.patch.mockReset()
templates.listAll.mockReset().mockReturnValue(of({ results: [] }))
documentTypes.listAll.mockReset().mockReturnValue(of({ results: [] }))
toast.showError.mockReset()
await TestBed.configureTestingModule({
imports: [OcrTemplatesComponent],
describe('OcrTemplatesComponent', () => {
let component: OcrTemplatesComponent
let fixture: ComponentFixture<OcrTemplatesComponent>
let ocrTemplateService: OcrTemplateService
let modalService: NgbModal
let toastService: ToastService
let modal: NgbModalRef
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NgxBootstrapIconsModule.pick(allIcons), OcrTemplatesComponent],
providers: [
{ provide: NgbModal, useValue: modal },
{ provide: DirtyFormGuard, useValue: dirtyFormGuard },
{ provide: PermissionsService, useValue: {} },
{ provide: OcrTemplateService, useValue: templates },
{ provide: DocumentTypeService, useValue: documentTypes },
{ provide: ToastService, useValue: toast },
{
provide: PermissionsService,
useValue: {
currentUserCan: () => true,
currentUserHasObjectPermissions: () => true,
currentUserOwnsObject: () => true,
},
},
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
})
.overrideComponent(OcrTemplatesComponent, {
set: {
template: `
@for (template of templates(); track template.id) {
<span class="name">{{ template.name }}</span>
<span class="document-type">{{ getDocumentTypeName(template) }}</span>
<input type="checkbox" [checked]="template.enabled" />
}
`,
imports: [],
},
})
.compileComponents()
ocrTemplateService = TestBed.inject(OcrTemplateService)
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)
fixture = TestBed.createComponent(OcrTemplatesComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('opens creation in a dialog and refreshes the list after saving', () => {
const reload = jest.spyOn(component, 'reload').mockImplementation(() => {})
component.createTemplate()
afterEach(() => modalService.dismissAll())
expect(modal.open).toHaveBeenCalledWith(
OcrTemplateEditorComponent,
expect.any(Object)
)
it('should show templates with their document type', () => {
const text = fixture.nativeElement.textContent
expect(text).toContain('Invoices')
expect(text).toContain('Invoice')
})
it('should support create, show notification on error / success', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSuccessSpy = jest.spyOn(toastService, 'showSuccess')
const reloadSpy = jest.spyOn(component, 'reload')
fixture.debugElement
.queryAll(By.css('pngx-page-header button'))
.find((button) => button.nativeElement.textContent.includes('Add'))
.triggerEventHandler('click')
const editor = modal.componentInstance as OcrTemplateEditorComponent
expect(editor).toBeInstanceOf(OcrTemplateEditorComponent)
expect(editor.object).toBeUndefined()
expect(options.backdrop).toBe('static')
expect(options.beforeDismiss()).toBe(true)
editor.succeeded.emit({ id: 3 } as OcrTemplate)
expect(reload).toHaveBeenCalledTimes(1)
editor.failed.emit({ error: 'error creating template' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
editor.succeeded.emit(templates[0])
expect(toastSuccessSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
it('passes the selected template to the same editor dialog', () => {
const template = { id: 3, name: 'Invoices' } as OcrTemplate
component.editTemplate(template)
expect(editor.object).toBe(template)
it('should pass the template to the editor', () => {
component.editTemplate(templates[0])
expect(modal.componentInstance.object).toBe(templates[0])
})
it('confirms dismissal only when dirty and never dismisses while saving', async () => {
component.createTemplate()
editor.isDirty = true
dirtyFormGuard.confirmChanges.mockReturnValue(of(false))
expect(await options.beforeDismiss()).toBe(false)
dirtyFormGuard.confirmChanges.mockReturnValue(of(true))
expect(await options.beforeDismiss()).toBe(true)
expect(dirtyFormGuard.confirmChanges).toHaveBeenCalledTimes(2)
it('should confirm closing the editor with unsaved changes', async () => {
const confirmSpy = jest
.spyOn(TestBed.inject(DirtyFormGuard), 'confirmChanges')
.mockReturnValue(of(false))
component.editTemplate()
const editor = modal.componentInstance as OcrTemplateEditorComponent
editor.ngOnInit()
editor.saving.set(true)
expect(options.beforeDismiss()).toBe(false)
expect(dirtyFormGuard.confirmChanges).toHaveBeenCalledTimes(2)
})
it('renders asynchronously loaded templates and type names without manual change detection', async () => {
const pendingTemplates = new Subject<{ results: OcrTemplate[] }>()
const pendingTypes = new Subject<{ results: DocumentType[] }>()
templates.listAll.mockReturnValue(pendingTemplates)
documentTypes.listAll.mockReturnValue(pendingTypes)
fixture.detectChanges()
pendingTemplates.next({
results: [{ id: 3, name: 'Invoices', document_type: 2 } as OcrTemplate],
})
modal.dismiss()
await fixture.whenStable()
expect(fixture.nativeElement.querySelector('.name').textContent).toBe(
'Invoices'
)
expect(confirmSpy).not.toHaveBeenCalled()
pendingTypes.next({ results: [{ id: 2, name: 'Invoice' } as DocumentType] })
component.editTemplate()
const dirtyEditor = modal.componentInstance as OcrTemplateEditorComponent
dirtyEditor.ngOnInit()
dirtyEditor.template.name = 'Changed'
modal.dismiss()
await fixture.whenStable()
expect(
fixture.nativeElement.querySelector('.document-type').textContent
).toBe('Invoice')
expect(confirmSpy).toHaveBeenCalled()
expect(modalService.hasOpenModals()).toBe(true)
dirtyEditor.saving.set(true)
confirmSpy.mockClear()
modal.dismiss()
await fixture.whenStable()
expect(confirmSpy).not.toHaveBeenCalled()
expect(modalService.hasOpenModals()).toBe(true)
})
it('renders toggle rollback after an asynchronous failure without manual change detection', async () => {
const template = { id: 3, name: 'Invoices', enabled: true } as OcrTemplate
templates.listAll.mockReturnValue(of({ results: [template] }))
const pending = new Subject<OcrTemplate>()
templates.patch.mockReturnValue(pending)
fixture.detectChanges()
it('should only send the enabled flag when toggling', () => {
const patchSpy = jest
.spyOn(ocrTemplateService, 'patch')
.mockReturnValueOnce(throwError(() => new Error('error toggling')))
.mockReturnValueOnce(of(templates[0]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSuccessSpy = jest.spyOn(toastService, 'showSuccess')
const reloadSpy = jest.spyOn(component, 'reload')
component.toggleTemplate(template, false)
await fixture.whenStable()
expect(fixture.nativeElement.querySelector('input').checked).toBe(false)
expect(templates.patch).toHaveBeenCalledWith({
...template,
enabled: false,
})
expect(template.enabled).toBe(true)
const toggle = fixture.debugElement.query(By.css('.form-switch input'))
toggle.nativeElement.click()
expect(patchSpy).toHaveBeenCalledWith({ id: 1, enabled: false })
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalledTimes(1)
const error = new Error('Update failed')
pending.error(error)
await fixture.whenStable()
expect(fixture.nativeElement.querySelector('input').checked).toBe(true)
expect(toast.showError).toHaveBeenCalledWith(expect.any(String), error)
component.toggleTemplateEnabled(templates[0])
expect(toastSuccessSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalledTimes(2)
})
describe('deletion', () => {
const template = { id: 3, name: 'Invoices' } as OcrTemplate
let dialog: {
confirmClicked: EventEmitter<void>
buttonsEnabled: WritableSignal<boolean>
messageBold?: string
}
let close: jest.Mock
let reload: jest.SpyInstance
it('should support delete, show notification on error / success', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSuccessSpy = jest.spyOn(toastService, 'showSuccess')
const deleteSpy = jest.spyOn(ocrTemplateService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
beforeEach(() => {
dialog = {
confirmClicked: new EventEmitter<void>(),
buttonsEnabled: signal(true),
}
close = jest.fn()
modal.open.mockReturnValue({ componentInstance: dialog, close })
reload = jest.spyOn(component, 'reload').mockImplementation(() => {})
})
component.deleteTemplate(templates[0])
const dialog = modal.componentInstance as ConfirmDialogComponent
it('keeps the confirmation open and disables buttons until deletion succeeds', () => {
const pending = new Subject<void>()
templates.delete.mockReturnValue(pending)
component.deleteTemplate(template)
deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
dialog.confirmClicked.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(dialog.buttonsEnabled()).toBe(true)
expect(reloadSpy).not.toHaveBeenCalled()
expect(modal.open).toHaveBeenCalledWith(ConfirmDialogComponent, {
backdrop: 'static',
})
expect(dialog.messageBold).toBe(template.name)
expect(templates.delete).not.toHaveBeenCalled()
dialog.confirmClicked.emit()
expect(templates.delete).toHaveBeenCalledWith(template)
expect(dialog.buttonsEnabled()).toBe(false)
expect(close).not.toHaveBeenCalled()
expect(reload).not.toHaveBeenCalled()
pending.next()
pending.complete()
expect(close).toHaveBeenCalledTimes(1)
expect(reload).toHaveBeenCalledTimes(1)
})
it('shows errors and allows retrying without closing the confirmation', () => {
const pending = new Subject<void>()
templates.delete
.mockReturnValueOnce(pending)
.mockReturnValue(of(undefined))
component.deleteTemplate(template)
dialog.confirmClicked.emit()
const error = new Error('Delete failed')
pending.error(error)
expect(toast.showError).toHaveBeenCalledWith(expect.any(String), error)
expect(dialog.buttonsEnabled()).toBe(true)
expect(close).not.toHaveBeenCalled()
expect(reload).not.toHaveBeenCalled()
dialog.confirmClicked.emit()
expect(templates.delete).toHaveBeenCalledTimes(2)
expect(close).toHaveBeenCalledTimes(1)
expect(reload).toHaveBeenCalledTimes(1)
})
deleteSpy.mockReturnValueOnce(of(true))
dialog.confirmClicked.emit()
expect(toastSuccessSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
})
@@ -2,7 +2,8 @@ import { Component, OnInit, inject, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, firstValueFrom, takeUntil, tap } from 'rxjs'
import { firstValueFrom, takeUntil, tap } from 'rxjs'
import { DocumentType } from 'src/app/data/document-type'
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'
@@ -30,36 +31,31 @@ export class OcrTemplatesComponent
extends LoadingComponentWithPermissions
implements OnInit
{
private readonly service = inject(OcrTemplateService)
private readonly documentTypeService = inject(DocumentTypeService)
private readonly modalService = inject(NgbModal)
private readonly dirtyFormGuard = inject(DirtyFormGuard)
private readonly toastService = inject(ToastService)
private ocrTemplateService = inject(OcrTemplateService)
private documentTypeService = inject(DocumentTypeService)
permissionsService = inject(PermissionsService)
private modalService = inject(NgbModal)
private dirtyFormGuard = inject(DirtyFormGuard)
private toastService = inject(ToastService)
readonly templates = signal<OcrTemplate[]>([])
private readonly documentTypeNames = signal(new Map<number, string>())
private readonly documentTypes = signal<DocumentType[]>([])
ngOnInit() {
this.documentTypeService
.listAll()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((r) => {
this.documentTypeNames.set(
new Map(r.results.map((dt) => [dt.id, dt.name]))
)
})
.subscribe((r) => this.documentTypes.set(r.results))
this.reload()
}
reload() {
this.loading.set(true)
this.service
this.ocrTemplateService
.listAll()
.pipe(
takeUntil(this.unsubscribeNotifier),
tap((r) => this.templates.set(r.results)),
delay(100)
tap((r) => this.templates.set(r.results))
)
.subscribe(() => {
this.show.set(true)
@@ -67,18 +63,14 @@ export class OcrTemplatesComponent
})
}
getDocumentTypeName(t: OcrTemplate): string {
getDocumentTypeName(template: OcrTemplate): string {
return (
this.documentTypeNames().get(t.document_type) ??
`${t.document_type ?? ''}`
this.documentTypes().find((dt) => dt.id === template.document_type)
?.name ?? ''
)
}
createTemplate() {
this.editTemplate()
}
editTemplate(t?: OcrTemplate) {
editTemplate(template?: OcrTemplate) {
const modal = this.modalService.open(OcrTemplateEditorComponent, {
backdrop: 'static',
keyboard: false,
@@ -89,71 +81,79 @@ export class OcrTemplatesComponent
beforeDismiss: () => {
const editor: OcrTemplateEditorComponent = modal.componentInstance
if (editor.saving()) return false
return editor.isDirty
? firstValueFrom(
this.dirtyFormGuard.confirmChanges(undefined, $localize`Close`)
)
: true
if (!editor.isDirty) return true
return firstValueFrom(
this.dirtyFormGuard.confirmChanges(undefined, $localize`Close`)
)
},
})
modal.componentInstance.object = t
modal.componentInstance.object = template
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => this.reload())
.subscribe((saved: OcrTemplate) => {
this.toastService.showSuccess(
$localize`Saved OCR template "${saved.name}".`
)
this.ocrTemplateService.clearCache()
this.reload()
})
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving OCR template.`, e)
})
}
toggleTemplate(t: OcrTemplate, enabled: boolean) {
this.templates.update((templates) =>
templates.map((template) =>
template.id === t.id ? { ...template, enabled } : template
)
)
this.service.patch({ ...t, enabled }).subscribe({
error: (error) => {
this.templates.update((templates) =>
templates.map((template) =>
template.id === t.id
? { ...template, enabled: t.enabled }
: template
toggleTemplateEnabled(template: OcrTemplate) {
this.ocrTemplateService
.patch({ id: template.id, enabled: template.enabled } as OcrTemplate)
.subscribe({
next: () => {
this.toastService.showSuccess(
template.enabled
? $localize`Enabled OCR template "${template.name}"`
: $localize`Disabled OCR template "${template.name}"`
)
)
this.toastService.showError(
$localize`Error updating OCR template.`,
error
)
},
})
this.reload()
},
error: (e) => {
this.toastService.showError(
$localize`Error toggling OCR template "${template.name}".`,
e
)
this.reload()
},
})
}
deleteTemplate(t: OcrTemplate) {
deleteTemplate(template: OcrTemplate) {
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.title = $localize`Delete OCR Template`
modal.componentInstance.messageBold = t.name
modal.componentInstance.message = $localize`Do you really want to delete this OCR template?`
modal.componentInstance.title = $localize`Confirm delete OCR template`
modal.componentInstance.messageBold = $localize`This operation will permanently delete this OCR template.`
modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Delete`
modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
modal.componentInstance.buttonsEnabled.set(false)
this.service
.delete(t)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
modal.close()
this.reload()
},
error: (error) => {
modal.componentInstance.buttonsEnabled.set(true)
this.toastService.showError(
$localize`Error deleting OCR template.`,
error
)
},
})
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled.set(false)
this.ocrTemplateService.delete(template).subscribe({
next: () => {
modal.close()
this.toastService.showSuccess(
$localize`Deleted OCR template "${template.name}".`
)
this.ocrTemplateService.clearCache()
this.reload()
},
error: (e) => {
modal.componentInstance.buttonsEnabled.set(true)
this.toastService.showError(
$localize`Error deleting OCR template "${template.name}".`,
e
)
},
})
})
}
}