mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-20 23:08:14 +00:00
Pngx conventions
This commit is contained in:
5 files changed
+233
-278
No files matched your search
+2
-2
@@ -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', () => {
|
||||
|
||||
+3
-3
@@ -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
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user