Skip to content

Commit bf9bf29

Browse files
authored
Merge pull request #62 from u8array/feat/import-report-clickable
refactor(import-report): findings-only summary, silent clean import
2 parents be2ffbb + 91016b1 commit bf9bf29

8 files changed

Lines changed: 272 additions & 232 deletions

File tree

‎src/components/Output/ImportReportModal.tsx‎

Lines changed: 0 additions & 107 deletions
This file was deleted.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describeFinding } from '../../lib/importReport';
2+
import type { ImportFinding, ImportFindingKind, ImportResult } from '../../lib/importReport';
3+
4+
export type { ImportResult };
5+
6+
/** Severity colour per finding kind. Tailwind classes only. The table
7+
* form over a switch keeps the kind→tone mapping in one expression
8+
* and lets the compiler check exhaustiveness via the Record type. */
9+
const TONE: Record<ImportFindingKind, string> = {
10+
partial: 'text-amber-400',
11+
browserLimit: 'text-amber-400',
12+
unknown: 'text-muted',
13+
};
14+
15+
function FindingRow({ finding, showPage }: { finding: ImportFinding; showPage: boolean }) {
16+
const { title, detail } = describeFinding(finding);
17+
return (
18+
<div className="flex items-start gap-2 px-3 py-2">
19+
{showPage && (
20+
<span className="font-mono text-[9px] uppercase tracking-wider text-muted shrink-0 mt-0.5">
21+
Page&nbsp;{finding.pageIndex + 1}
22+
</span>
23+
)}
24+
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
25+
<span className={`font-mono text-[10px] font-semibold ${TONE[finding.kind]}`}>
26+
{title}
27+
</span>
28+
<span className="font-mono text-[10px] text-muted truncate">
29+
{detail}
30+
</span>
31+
</div>
32+
</div>
33+
);
34+
}
35+
36+
export function ImportSummaryBody({ result }: { result: ImportResult }) {
37+
const { objectCount, report } = result;
38+
const { findings } = report;
39+
// Only show per-row page badges when the import actually spanned multiple
40+
// pages. Single-page reports don't need the badge clutter.
41+
const multiPage = findings.some((f) => f.pageIndex > 0);
42+
43+
return (
44+
<div className="flex flex-col gap-3 p-4 flex-1 min-h-0 overflow-y-auto">
45+
<p className="font-mono text-[10px] text-amber-400 leading-relaxed">
46+
Imported {objectCount} object{objectCount !== 1 ? 's' : ''} with{' '}
47+
{findings.length} issue{findings.length !== 1 ? 's' : ''}:
48+
</p>
49+
<div className="flex flex-col">
50+
{findings.map((f, i) => (
51+
<FindingRow
52+
key={`${f.pageIndex}-${f.kind}-${f.command}-${i}`}
53+
finding={f}
54+
showPage={multiPage}
55+
/>
56+
))}
57+
</div>
58+
</div>
59+
);
60+
}

‎src/components/Output/ZplImportModal.tsx‎

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { importZplText } from '../../lib/zplImportService';
44
import { readFileAsText } from '../../lib/readFile';
55
import { useLabelStore, type Page } from '../../store/labelStore';
66
import type { LabelConfig } from '../../types/ObjectType';
7-
import { formatReportAsText, type ImportResult } from '../../lib/importReport';
8-
import { ImportSummaryBody } from './ImportReportModal';
7+
import { formatReportAsText, type ImportReport, type ImportResult } from '../../lib/importReport';
8+
import { ImportSummaryBody } from './ImportSummary';
99
import { useT } from '../../lib/useT';
1010
import { DialogShell } from '../ui/DialogShell';
1111

@@ -35,7 +35,7 @@ export function ZplImportModal({ onClose }: Props) {
3535

3636
const applyImport = (labelConfig: Partial<LabelConfig>, importedPages: Page[]) => {
3737
if (appendMode && hasExistingContent) {
38-
// Keep the current label config — the user opted to keep the
38+
// Keep the current label config: the user opted to keep the
3939
// existing design's dimensions, so any imported ^PW/^LL is
4040
// intentionally discarded.
4141
appendPages(importedPages);
@@ -44,23 +44,41 @@ export function ZplImportModal({ onClose }: Props) {
4444
}
4545
};
4646

47-
const handleImport = () => {
48-
setError(null);
49-
if (!zpl.trim()) {
50-
setError('Please paste some ZPL code first.');
51-
return;
47+
// Feedback through state change: when the import has no findings the
48+
// changed canvas is confirmation enough. We only stop on the result
49+
// view when there is something the user could not otherwise see, i.e.
50+
// one or more findings to review.
51+
const finishImport = (totalObjects: number, report: ImportReport) => {
52+
if (report.findings.length === 0) {
53+
onClose();
54+
} else {
55+
setResult({ objectCount: totalObjects, report });
5256
}
57+
};
5358

54-
const { labelConfig, pages: importedPages, report } = importZplText(zpl, label.dpmm);
59+
// Shared post-source path: parse, gate on supported content, hand off
60+
// to the store + finishImport. The two entry points (paste textarea,
61+
// file picker) only differ in how they obtain the text and which
62+
// source-specific error they surface; everything past that point is
63+
// identical, so it lives here.
64+
const processImport = (text: string) => {
65+
const { labelConfig, pages: importedPages, report } = importZplText(text, label.dpmm);
5566
const totalObjects = importedPages.reduce((s, p) => s + p.objects.length, 0);
56-
5767
if (totalObjects === 0 && Object.keys(labelConfig).length === 0) {
5868
setError('No supported objects found in the ZPL code.');
5969
return;
6070
}
61-
6271
applyImport(labelConfig, importedPages);
63-
setResult({ objectCount: totalObjects, report });
72+
finishImport(totalObjects, report);
73+
};
74+
75+
const handleImport = () => {
76+
setError(null);
77+
if (!zpl.trim()) {
78+
setError('Please paste some ZPL code first.');
79+
return;
80+
}
81+
processImport(zpl);
6482
};
6583

6684
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -81,17 +99,7 @@ export function ZplImportModal({ onClose }: Props) {
8199
setError('The file appears to be empty.');
82100
return;
83101
}
84-
85-
const { labelConfig, pages: importedPages, report } = importZplText(text, label.dpmm);
86-
const totalObjects = importedPages.reduce((s, p) => s + p.objects.length, 0);
87-
88-
if (totalObjects === 0 && Object.keys(labelConfig).length === 0) {
89-
setError('No supported objects found in the ZPL code.');
90-
return;
91-
}
92-
93-
applyImport(labelConfig, importedPages);
94-
setResult({ objectCount: totalObjects, report });
102+
processImport(text);
95103
};
96104

97105
const handleCopy = () => {

‎src/lib/importReport.ts‎

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import type { ImportReport } from './zplParser';
1+
import type { ImportFinding, ImportFindingKind, ImportReport } from './zplParser';
22
import { ZPL_COMMAND_MAP } from './zplCommandSupport';
33

4+
// Re-export the parser-side domain types so UI code talks to the report
5+
// surface through this module exclusively instead of reaching into the
6+
// parser for type imports.
7+
export type { ImportFinding, ImportFindingKind, ImportReport };
8+
49
export interface ImportResult {
510
objectCount: number;
611
report: ImportReport;
@@ -13,25 +18,62 @@ export function partialLoss(cmd: string): string {
1318
return entry?.loss ?? 'imported with limitations';
1419
}
1520

21+
/**
22+
* Single source of truth for finding wording. Both the UI list and the
23+
* copy-as-text formatter feed off this so the user sees the same
24+
* description in both surfaces; without it, the two pathways drift
25+
* whenever someone tweaks a string in one place.
26+
*
27+
* `title` is the headline (kind + command code where useful);
28+
* `detail` is the secondary line (loss description for partial, raw
29+
* token for the others).
30+
*/
31+
export function describeFinding(f: ImportFinding): { title: string; detail: string } {
32+
if (f.kind === 'partial') {
33+
return {
34+
title: `Partially imported (${f.command})`,
35+
detail: partialLoss(f.command),
36+
};
37+
}
38+
if (f.kind === 'browserLimit') {
39+
return {
40+
title: 'Skipped: needs printer hardware',
41+
detail: f.command,
42+
};
43+
}
44+
return {
45+
title: 'Skipped: command not recognised',
46+
detail: f.command,
47+
};
48+
}
49+
50+
/** Compact "Page N: " prefix when a finding originates from a multi-page
51+
* import. Single-page reports omit it to stay terse. */
52+
function pagePrefix(f: ImportFinding, multiPage: boolean): string {
53+
return multiPage ? `Page ${f.pageIndex + 1}: ` : '';
54+
}
55+
1656
export function formatReportAsText(result: ImportResult): string {
1757
const { objectCount, report } = result;
58+
const findings = report.findings;
59+
const multiPage = findings.some((f) => f.pageIndex > 0);
60+
1861
const lines: string[] = [
1962
`ZPL Import Report`,
2063
`Objects imported: ${objectCount}`,
2164
'',
2265
];
23-
if (report.partial.length > 0) {
24-
const uniqueLosses = [...new Set(report.partial.map(partialLoss))];
25-
lines.push(`Partially imported (${report.partial.join(', ')}): ${uniqueLosses.join('; ')}`);
26-
}
27-
if (report.browserLimit.length > 0) {
28-
lines.push(`Skipped (printer-only): ${report.browserLimit.map((t) => t.split(',')[0]).join(', ')}`);
29-
}
30-
if (report.unknown.length > 0) {
31-
lines.push(`Skipped (unrecognised): ${report.unknown.map((t) => t.split(',')[0]).join(', ')}`);
66+
67+
if (findings.length === 0) {
68+
lines.push('All commands recognised. No design information was lost.');
69+
return lines.join('\n');
3270
}
33-
if (report.partial.length === 0 && report.browserLimit.length === 0 && report.unknown.length === 0) {
34-
lines.push('All commands recognised — no design information was lost.');
71+
72+
// One row per finding (per page-occurrence). Matches the UI list so the
73+
// copied text mirrors what the user sees in the modal.
74+
for (const f of findings) {
75+
const { title, detail } = describeFinding(f);
76+
lines.push(`${pagePrefix(f, multiPage)}${title}: ${detail}`);
3577
}
3678
return lines.join('\n');
3779
}

0 commit comments

Comments
 (0)