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 1dca9d4c9..a16df8cc8 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
@@ -10,7 +10,7 @@
-
+
-
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 99a56ad2b..62803f0aa 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
@@ -48,6 +48,7 @@ describe('OcrTemplateEditorComponent', () => {
documents.list.mockReset().mockReturnValue(of({ results: [] }))
documents.get.mockReset().mockReturnValue(of({ id: 1, page_count: 3 }))
templates.testZone.mockReset()
+ templates.create.mockReset()
await TestBed.configureTestingModule({
imports: [OcrTemplateEditorComponent],
providers: [
@@ -69,7 +70,11 @@ describe('OcrTemplateEditorComponent', () => {
{ provide: NgbModal, useValue: modal },
{
provide: ToastService,
- useValue: { showSuccess: jest.fn(), showError: jest.fn() },
+ useValue: {
+ showInfo: jest.fn(),
+ showSuccess: jest.fn(),
+ showError: jest.fn(),
+ },
},
],
})
@@ -319,6 +324,51 @@ describe('OcrTemplateEditorComponent', () => {
expect(component.isDirty).toBe(true)
})
+ it('cancels drawing and prevents new gestures or duplicate saves while saving', () => {
+ prepareOverlay()
+ const pending = new Subject()
+ templates.create.mockReturnValue(pending)
+ component.onOverlayPointerDown(pointer(500, 500))
+
+ component.save()
+ expect(component.saving).toBe(true)
+ expect(component.drawingRect()).toBeNull()
+ component.save()
+ expect(templates.create).toHaveBeenCalledTimes(1)
+
+ component.onOverlayPointerDown(pointer(500, 500))
+ component.onOverlayPointerUp(pointer(700, 700))
+ expect(component.template.zones).toHaveLength(0)
+
+ pending.next({ ...component.template, id: 3 })
+ pending.complete()
+ expect(component.saving).toBe(false)
+ component.onOverlayPointerDown(pointer(500, 500))
+ component.onOverlayPointerUp(pointer(700, 700))
+ expect(component.template.zones).toHaveLength(1)
+ })
+
+ it('stops an active move and unlocks editing without losing changes after a failed save', () => {
+ fixture.detectChanges()
+ prepareOverlay()
+ component.template.zones = [zone()]
+ const pending = new Subject()
+ templates.create.mockReturnValue(pending)
+ component.onOverlayPointerDown(pointer(150, 150))
+
+ component.save()
+ component.onOverlayPointerMove(pointer(250, 250))
+ component.onOverlayPointerUp(pointer(250, 250))
+ expect(component.template.zones[0].x).toBe(100)
+
+ pending.error(new Error('Save failed'))
+ expect(component.saving).toBe(false)
+ expect(component.isDirty).toBe(true)
+ component.onOverlayPointerDown(pointer(150, 150))
+ component.onOverlayPointerUp(pointer(250, 250))
+ expect(component.template.zones[0].x).toBe(200)
+ })
+
it('hides results when extraction settings or geometry change', () => {
const selected = zone()
component.template.zones = [selected]
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 c7c55e9ea..d0cc44de0 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
@@ -509,6 +509,7 @@ export class OcrTemplateEditorComponent
}
onOverlayPointerDown(event: PointerEvent) {
+ if (this.saving) return
if (event.button !== 0 || this.overlayInteraction.kind !== 'idle') return
const point = this.svgPointFromEvent(event)
if (!this.imageLoaded || !point) return
@@ -903,6 +904,8 @@ export class OcrTemplateEditorComponent
}
save() {
+ if (this.saving) return
+ this.stopOverlayInteraction()
this.saving = true
this.pruneCombineFormats()
this.template.sample_document = this.previewDocId