mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-21 07:18:13 +00:00
Make the zone list more useful, show colors
This commit is contained in:
6 files changed
+45
-70
No files matched your search
+20
-29
@@ -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>
|
||||
}
|
||||
+5
-5
@@ -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 +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'
|
||||
|
||||
-3
@@ -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)"
|
||||
|
||||
+2
-26
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user