Make the zone list more useful, show colors

This commit is contained in:
shamoon committed 2026-10-19 22:43:12 -07:00
1 parent d2d434535b
commit 68eb4fe96d
6 files changed
+45 -70

No files matched your search

@@ -1,34 +1,25 @@
@if (zones.length === 0) {
<p class="text-muted" i18n>
No zones defined.
</p>
}
<div class="list-group">
@for (zone of zones; track $index; let i = $index) {
<div
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
[style.box-shadow]="selectedZoneIndex === i ? 'inset 3px 0 0 0 var(--bs-primary)' : null"
>
<div class="flex-grow-1" role="button" style="cursor: pointer;" (click)="zoneSelected.emit(i)">
<div>
<strong [class.text-primary]="selectedZoneIndex === i">
{{ zone.name }}
</strong>
<p class="text-muted" i18n>No zones yet. Use Draw to outline one on the sample document.</p>
} @else {
<div class="list-group">
@for (zone of zones; track $index; let i = $index) {
<div class="list-group-item list-group-item-action d-flex align-items-center gap-3 position-relative">
<span class="p-2 rounded flex-shrink-0" [style.background-color]="zoneColor(i)" aria-hidden="true"></span>
<div class="flex-grow-1 text-truncate">
<button type="button" class="btn btn-link p-0 fw-semibold text-reset text-decoration-none text-truncate mw-100 stretched-link" (click)="zoneSelected.emit(i)">{{ zone.name }}</button>
<div class="small text-muted text-truncate">{{ getZoneTargetName(zone) }}</div>
</div>
<div class="small text-muted">
{{ getZoneTargetName(zone) }} - {{ zone.width }}x{{ zone.height }}px
<ng-container i18n>p.</ng-container>{{ zonePage(zone) }}
</div>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="zoneSelected.emit(i)" title="Edit" i18n-title>
<i-bs name="pencil"></i-bs>
</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="zoneRemoved.emit(i)" title="Delete" i18n-title>
<small class="text-muted text-nowrap">
@if (zone.page === -1) {
<ng-container i18n>Last page</ng-container>
} @else {
<ng-container i18n>Page {{ zone.page ?? 1 }}</ng-container>
}
</small>
<button type="button" class="btn btn-sm btn-link text-danger position-relative z-2" (click)="zoneRemoved.emit(i)" title="Delete zone" i18n-title aria-label="Delete zone" i18n-aria-label>
<i-bs name="trash"></i-bs>
</button>
</div>
</div>
}
</div>
}
</div>
}
@@ -41,19 +41,19 @@ describe('OcrTemplateEditorZoneListComponent', () => {
it('shows empty state when no zones are defined', () => {
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('No zones defined')
expect(fixture.nativeElement.textContent).toContain('No zones yet')
})
it('renders zone target, size, and page', () => {
component.zones = [zone()]
it('renders zone name, target, and page', () => {
component.zones = [zone(), zone({ name: 'Zone 2', page: -1 })]
component.customFields = [{ id: 7, name: 'Invoice Number' } as CustomField]
fixture.detectChanges()
const text = fixture.nativeElement.textContent
expect(text).toContain('Zone 1')
expect(text).toContain('Invoice Number')
expect(text).toContain('30x40px')
expect(text).toContain('p.1')
expect(text).toContain('Page 1')
expect(text).toContain('Last page')
})
it('emits select and remove events', () => {
@@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { CustomField } from 'src/app/data/custom-field'
import { OCR_BUILTIN_TARGETS, OcrTemplateZone } from 'src/app/data/ocr-template'
import { getZonePage } from '../zone-geometry'
import { getZoneColor } from '../zone-geometry'
@Component({
selector: 'pngx-ocr-template-zone-list',
@@ -11,17 +11,12 @@ import { getZonePage } from '../zone-geometry'
})
export class OcrTemplateEditorZoneListComponent {
@Input() zones: OcrTemplateZone[] = []
@Input() selectedZoneIndex: number | null = null
@Input() previewPage = 0
@Input() previewPageCount: number | null = null
@Input() customFields: CustomField[] = []
@Output() zoneSelected = new EventEmitter<number>()
@Output() zoneRemoved = new EventEmitter<number>()
zonePage(zone: OcrTemplateZone): number {
return getZonePage(zone, this.previewPage, this.previewPageCount)
}
readonly zoneColor = getZoneColor
getZoneTargetName(zone: OcrTemplateZone): string {
const target = zone.target || 'custom_field'
@@ -187,9 +187,6 @@
} @else {
<pngx-ocr-template-zone-list
[zones]="template.zones"
[selectedZoneIndex]="selectedZoneIndex"
[previewPage]="previewPage"
[previewPageCount]="previewPageCount"
[customFields]="customFields"
(zoneSelected)="selectZone($event)"
(zoneRemoved)="removeZone($event)"
@@ -72,6 +72,7 @@ import {
DrawingRect,
findHandleAt,
findZoneAt,
getZoneColor,
getZoneDisplayRect,
getZonePage,
HANDLE_SIZE,
@@ -109,16 +110,6 @@ interface ResizeHandleMarker extends Point {
const CUSTOM_DATE_FORMAT_CHOICE = 'custom'
const NO_OVERLAY_INTERACTION: OverlayInteraction = { kind: 'idle' }
const ZONE_COLORS = [
'#4f8ff7',
'#ff6b6b',
'#51cf66',
'#ffd43b',
'#cc5de8',
'#ff922b',
'#20c997',
'#e599f7',
]
const RESIZE_CURSOR: Record<ResizeHandle, string> = {
nw: 'nw-resize',
ne: 'ne-resize',
@@ -738,7 +729,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
}
zoneColor(index: number): string {
return ZONE_COLORS[index % ZONE_COLORS.length]
return getZoneColor(index)
}
zoneFill(index: number): string {
@@ -960,11 +951,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
})
}
getCustomFieldName(id: number): string {
const cf = this.customFields.find((f) => f.id === id)
return cf ? cf.name : `Field #${id}`
}
/** Value bound to the field select: a built-in id string or a custom-field id. */
zoneFieldValue(zone: OcrTemplateZone): ZoneFieldSelection {
const target = zone.target || DEFAULT_OCR_ZONE_TARGET
@@ -1069,16 +1055,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
)
}
getZoneTargetName(zone: OcrTemplateZone): string {
const target = zone.target || DEFAULT_OCR_ZONE_TARGET
if (target === OCR_ZONE_TARGET.CustomField) {
return zone.custom_field
? this.getCustomFieldName(zone.custom_field)
: $localize`(no field)`
}
return this.builtinTargets.find((t) => t.id === target)?.name ?? target
}
getDocumentTypeName(id: number): string {
const dt = this.documentTypes.find((d) => d.id === id)
return dt ? dt.name : `Type #${id}`
@@ -36,6 +36,22 @@ export type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
export const HANDLE_SIZE = 8
export const MIN_ZONE_SIZE = 10
const ZONE_COLORS = [
'#4f8ff7',
'#ff6b6b',
'#51cf66',
'#ffd43b',
'#cc5de8',
'#ff922b',
'#20c997',
'#e599f7',
]
/** Outline color for a zone, shared by the preview overlay and the zone list. */
export function getZoneColor(index: number): string {
return ZONE_COLORS[index % ZONE_COLORS.length]
}
export function getZonePage(
zone: OcrTemplateZone,
previewPage: number,