Skip to content

Commit 4fd6eb2

Browse files
committed
chore(post-phase2): cleanup pass for support matrix, layers + dead code
Six small follow-ups from the Phase 2 reviews: 1. zplCommandSupport.ts: FN and FV were still marked 'unsupported' after Phase 1 merged Variables; flip both to 'supported'. Add DF, XF, XG, DY entries (Phase 2d batch export) which the matrix silently lacked, so the import report now reflects reality. 2. Move the `Page` interface out of the store and into types/Group alongside LabelObject. zplGenerator.ts no longer imports from store/, removing the only `lib -> store` edge in the Phase 2 surface. The single remaining consumer (ZplImportModal) imports from types/Group directly; no compatibility shim needed. 3. Drop the dead `getImportedFile` export from csvImport.ts and the corresponding `lastImportedFile` slot; `rememberImport` no longer takes a File argument (the only field-name consumer reads from `pending.parsed.file` directly). 4. Extract `R:LBL.ZPL` as `BATCH_TEMPLATE_PATH` constant in zplGenerator.ts so the path appears once instead of twice. The constant's JSDoc sits above it; the function's JSDoc is attached to the function (was orphaned in the first pass). 5. Add `selectBatchInputs` returning `{dataset, mapping} | null` and derive `selectCanBatchExport` from it. The hook's batch paths call `selectBatchInputs(getState())` once and consume the narrowed pair, instead of duplicating the predicate via redundant null guards. Boolean selector still drives the UI's menu-enable state. 6. canBatchExport lifts from useZplImportExport's hook-local derivation to the store-level selector, matching the existing selectPreviewLocksEditor / selectLabelaryNoticeRequired pattern.
1 parent 4f1a132 commit 4fd6eb2

9 files changed

Lines changed: 77 additions & 59 deletions

File tree

‎src/components/Output/ZplImportModal.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useRef, useState } from 'react';
22
import { XMarkIcon, ClipboardDocumentIcon, CheckIcon, FolderOpenIcon } from '@heroicons/react/16/solid';
33
import { importZplText } from '../../lib/zplImportService';
44
import { readFileAsText } from '../../lib/readFile';
5-
import { useLabelStore, type Page } from '../../store/labelStore';
5+
import { useLabelStore } from '../../store/labelStore';
6+
import type { Page } from '../../types/Group';
67
import type { LabelConfig } from '../../types/ObjectType';
78
import type { Variable } from '../../types/Variable';
89
import { formatReportAsText, type ImportReport, type ImportResult } from '../../lib/importReport';

‎src/hooks/useCsvImportActions.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function useCsvImportActions() {
137137
function applyImport(p: ParsedImport, opts: { keepMapping: boolean }): void {
138138
const { loadCsv, setCsvMapping, openCsvMappingModal, csvMapping } =
139139
useLabelStore.getState();
140-
rememberImport(p.file, p.bytes, p.text);
140+
rememberImport(p.bytes, p.text);
141141
const effectiveMapping: CsvMapping | null = opts.keepMapping ? csvMapping : null;
142142
if (!opts.keepMapping) setCsvMapping(null);
143143
loadCsv(p.result);

‎src/hooks/useZplImportExport.ts‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { useState } from "react";
2-
import { useLabelStore, useCurrentObjects } from "../store/labelStore";
2+
import {
3+
selectBatchInputs,
4+
selectCanBatchExport,
5+
useCurrentObjects,
6+
useLabelStore,
7+
} from "../store/labelStore";
38
import { generateMultiPageZPL, generateBatchZpl } from "../lib/zplGenerator";
49
import { printLabel } from "../lib/printPreview";
510
import { triggerDownload } from "../lib/triggerDownload";
@@ -17,23 +22,17 @@ export function useZplImportExport() {
1722
const [showZebraPrint, setShowZebraPrint] = useState(false);
1823
const [printError, setPrintError] = useState<string | null>(null);
1924

20-
// Batch export is only meaningful with both a dataset and at least one
21-
// mapped variable. Without either, the output would be identical to a
22-
// single label and surfacing the action just clutters the menu.
23-
const canBatchExport =
24-
csvDataset !== null &&
25-
csvDataset.rows.length > 0 &&
26-
csvMapping !== null &&
27-
Object.keys(csvMapping.bindings).length > 0;
25+
const canBatchExport = useLabelStore(selectCanBatchExport);
2826

2927
const handleDownload = () => {
3028
const zpl = generateMultiPageZPL(label, pages, variables);
3129
triggerDownload(new Blob([zpl], { type: "text/plain" }), "label.zpl");
3230
};
3331

3432
const handleExportBatch = () => {
35-
if (!canBatchExport) return;
36-
const zpl = generateBatchZpl(label, objects, variables, csvDataset, csvMapping);
33+
const batch = selectBatchInputs(useLabelStore.getState());
34+
if (!batch) return;
35+
const zpl = generateBatchZpl(label, objects, variables, batch.dataset, batch.mapping);
3736
triggerDownload(new Blob([zpl], { type: "text/plain" }), "label-batch.zpl");
3837
};
3938

@@ -53,10 +52,12 @@ export function useZplImportExport() {
5352
// ZPL surfaced to direct-print: batch form when a CSV is in play (so
5453
// sending to the printer produces N labels), otherwise the same
5554
// template the editor displays.
56-
const currentZpl = () =>
57-
canBatchExport
58-
? generateBatchZpl(label, objects, variables, csvDataset, csvMapping)
55+
const currentZpl = () => {
56+
const batch = selectBatchInputs(useLabelStore.getState());
57+
return batch
58+
? generateBatchZpl(label, objects, variables, batch.dataset, batch.mapping)
5959
: generateMultiPageZPL(label, pages, variables);
60+
};
6061

6162
return {
6263
showZplImport,

‎src/lib/csvImport.test.ts‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,28 +138,25 @@ describe('encoding cache', () => {
138138
});
139139

140140
it('rememberImport + decodeImportedText round-trips UTF-8', () => {
141-
const file = new File([''], 't.csv', { type: 'text/csv' });
142141
const bytes = new TextEncoder().encode('sku,qty\n');
143-
rememberImport(file, bytes, new TextDecoder('utf-8').decode(bytes));
142+
rememberImport(bytes, new TextDecoder('utf-8').decode(bytes));
144143
expect(getImportedBytes()).toBe(bytes);
145144
expect(decodeImportedText('utf-8')).toBe('sku,qty\n');
146145
forgetImport();
147146
});
148147

149148
it('decodeImportedText with windows-1252 turns 0xE4 into ä', () => {
150-
const file = new File([''], 't.csv', { type: 'text/csv' });
151149
// 0xE4 in windows-1252 (ANSI) is "ä". The same byte in UTF-8 is
152150
// a continuation byte (invalid as a standalone), so the two
153151
// decodings of the same bytes should differ.
154152
const bytes = new Uint8Array([0x73, 0xE4, 0x6F]); // s ä o (CP1252)
155-
rememberImport(file, bytes, new TextDecoder('utf-8').decode(bytes));
153+
rememberImport(bytes, new TextDecoder('utf-8').decode(bytes));
156154
expect(decodeImportedText('windows-1252')).toBe('säo');
157155
forgetImport();
158156
});
159157

160158
it('forgetImport clears the cache', () => {
161-
const file = new File([''], 't.csv', { type: 'text/csv' });
162-
rememberImport(file, new Uint8Array([1, 2, 3]), 'abc');
159+
rememberImport(new Uint8Array([1, 2, 3]), 'abc');
163160
forgetImport();
164161
expect(getImportedBytes()).toBeNull();
165162
expect(decodeImportedText('utf-8')).toBeNull();

‎src/lib/csvImport.ts‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,26 +154,19 @@ export function parseCsvText(
154154
* mapping modal needs synchronous re-parse on every option-change
155155
* keystroke. Mirrors the previewCache pattern in labelStore.ts.
156156
*/
157-
let lastImportedFile: File | null = null;
158157
let lastImportedBytes: Uint8Array | null = null;
159158
let lastImportedText: string | null = null;
160159

161-
export function rememberImport(file: File, bytes: Uint8Array, text: string): void {
162-
lastImportedFile = file;
160+
export function rememberImport(bytes: Uint8Array, text: string): void {
163161
lastImportedBytes = bytes;
164162
lastImportedText = text;
165163
}
166164

167165
export function forgetImport(): void {
168-
lastImportedFile = null;
169166
lastImportedBytes = null;
170167
lastImportedText = null;
171168
}
172169

173-
export function getImportedFile(): File | null {
174-
return lastImportedFile;
175-
}
176-
177170
export function getImportedText(): string | null {
178171
return lastImportedText;
179172
}

‎src/lib/zplCommandSupport.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ export const ZPL_COMMANDS: readonly ZplCommandInfo[] = [
5353
{ cmd: 'FC', status: 'unsupported', description: 'Field clock — inserts date/time into field data' },
5454
{ cmd: 'FE', status: 'unsupported', description: 'Field concatenation — appends data to the current field' },
5555
{ cmd: 'FM', status: 'unsupported', description: 'Multiple field origin locations' },
56-
{ cmd: 'FN', status: 'unsupported', description: 'Field number — variable field placeholder for recall/merge' },
56+
{ cmd: 'FN', status: 'supported', description: 'Field number — variable field placeholder, lands in the Variables tab on import and emits as ^FN{slot} on export' },
5757
{ cmd: 'FP', status: 'unsupported', description: 'Field parameter — sets character-by-character text direction' },
58-
{ cmd: 'FV', status: 'unsupported', description: 'Field variable — supplies data for a ^FN field at print time' },
58+
{ cmd: 'FV', status: 'supported', description: 'Field variable — supplies data for a ^FN field at print time; populates the bound Variable\'s default on import' },
5959

6060
// ── Fonts & text ──────────────────────────────────────────────────────────
6161
{ cmd: 'A0', status: 'supported', description: 'Scalable/bitmap font 0 — primary designer font' },
@@ -171,6 +171,13 @@ export const ZPL_COMMANDS: readonly ZplCommandInfo[] = [
171171
description: 'Download graphic to printer storage (~DG)',
172172
loss: 'Stores data on the physical printer; not relevant for canvas label design',
173173
},
174+
175+
// ── Templates & batch merge (round-trip on parse; emit as ^DFR/^XFR
176+
// for CSV-driven batch printing) ──────────────────────────────────────────
177+
{ cmd: 'DF', status: 'supported', description: 'Download format — used as ^DFR:LBL.ZPL to store the design template once, recalled per CSV row during batch export' },
178+
{ cmd: 'XF', status: 'supported', description: 'Recall format — pulled per row in batch export, paired with ^FN overrides' },
179+
{ cmd: 'XG', status: 'supported', description: 'Recall graphic — used together with ~DY for printer-resident images' },
180+
{ cmd: 'DY', status: 'supported', description: 'Download object (~DY) — embeds custom fonts and graphics so the printer can resolve ^A aliases and ^XG recalls' },
174181
] as const;
175182

176183
/** O(1) lookup map: command code → info */

‎src/lib/zplGenerator.ts‎

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { mmToDots } from './coordinates';
22
import { ObjectRegistry } from '../registry';
33
import { fdField, stripZplCommandChars } from '../registry/zplHelpers';
44
import type { CustomFontMapping, LabelConfig } from '../types/ObjectType';
5-
import type { Page } from '../store/labelStore';
65
import type { Variable } from '../types/Variable';
7-
import { isGroup, type LabelObject } from '../types/Group';
6+
import { isGroup, type LabelObject, type Page } from '../types/Group';
87
import { getFontBytes } from './fontCache';
98
import type { ImageProps } from '../registry/image';
109
import { formatStoragePath } from './storagePath';
@@ -91,30 +90,23 @@ export function generateMultiPageZPL(
9190
return pages.map((p) => generateZPL(label, p.objects, variables)).join('\n');
9291
}
9392

93+
/** Drive + filename used to stash the batch template on the printer.
94+
* R: is RAM (volatile, dropped on power cycle), which matches a
95+
* single-run batch scope. 8.3 filename avoids stomping on
96+
* user-managed stored forms. */
97+
const BATCH_TEMPLATE_PATH = 'R:LBL.ZPL';
98+
9499
/**
95100
* Batch-print form for a single page-design driven by a CSV dataset.
96-
* Emits two parts:
97-
*
98-
* 1. The template once, wrapped in `^DFR:LBL.ZPL` so the printer
99-
* stores it under R:LBL.ZPL. The body keeps its `^FN` slots,
100-
* so the printer treats it as a reusable form file.
101-
* 2. One small `^XA^XFR:LBL.ZPL^FN…^FD…^FS…^XZ` block per CSV row
102-
* that recalls the stored format and supplies per-row values
103-
* via `^FN` overrides.
104-
*
105-
* This is the idiomatic ZPL data-merge pattern: the printer parses
106-
* the heavy template once and applies cheap field overrides per
107-
* label, so a 10k-row batch isn't 10k full copies of the design.
101+
* Emits the template once (wrapped in `^DF{path}` so the printer
102+
* stores it under `BATCH_TEMPLATE_PATH`) and then one small recall
103+
* block per CSV row: `^XA^XF{path}^FN..^FD..^FS..^XZ`. This is the
104+
* idiomatic ZPL data-merge pattern, so a 10k-row batch isn't 10k
105+
* full copies of the design.
108106
*
109107
* Variables with no binding (or whose bound header is missing from
110108
* the current dataset) emit no override; the printer falls back to
111-
* the variable's `^FD` default that's baked into the stored template.
112-
*
113-
* The drive choice is R: (volatile RAM) so the stored format gets
114-
* dropped on printer power-cycle — a single print run is the
115-
* intended scope, not permanent provisioning. Filename `LBL.ZPL`
116-
* keeps the printer's 8.3 limit and avoids stomping on user
117-
* filenames that follow design conventions.
109+
* the variable's `^FD` default baked into the stored template.
118110
*/
119111
export function generateBatchZpl(
120112
label: LabelConfig,
@@ -132,7 +124,10 @@ export function generateBatchZpl(
132124
// darkness) emit before ^XA, and a start-anchored regex would silently
133125
// skip the inject — recall blocks below would then reference a form
134126
// file the printer never stored.
135-
const templateStored = baseZpl.replace(/\^XA\n/, '^XA\n^DFR:LBL.ZPL\n');
127+
const templateStored = baseZpl.replace(
128+
/\^XA\n/,
129+
`^XA\n^DF${BATCH_TEMPLATE_PATH}\n`,
130+
);
136131

137132
// Pre-compute (variable.fnNumber, columnIdx) per mapped variable so
138133
// the per-row loop is a tight zip. Variables with no binding or whose
@@ -149,7 +144,7 @@ export function generateBatchZpl(
149144
}
150145

151146
const recallBlocks = csvDataset.rows.map((row) => {
152-
const lines: string[] = ['^XA', '^XFR:LBL.ZPL'];
147+
const lines: string[] = ['^XA', `^XF${BATCH_TEMPLATE_PATH}`];
153148
for (const { fn, colIdx } of overrides) {
154149
const value = row[colIdx] ?? '';
155150
// Route through `fdField` so values containing `^`/`~` get the

‎src/store/labelStore.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
stripVariableIdFromObjects,
1616
type GroupObject,
1717
type LabelObject,
18+
type Page,
1819
} from '../types/Group';
1920
import { locales } from '../locales';
2021
import type { LocaleCode } from '../locales';
@@ -46,10 +47,6 @@ export interface CsvDataset {
4647
export type { ObjectChanges };
4748
export type { Variable, VariableInput };
4849

49-
export interface Page {
50-
objects: LabelObject[];
51-
}
52-
5350
/** Meta fields that remain editable on a locked object so the user can
5451
* release the lock or annotate without unlocking first. Everything else
5552
* (position, props, rotation, positionType) is blocked. */
@@ -348,6 +345,25 @@ export const selectLabelaryNoticeRequired = (s: LabelState): boolean =>
348345
export const selectPreviewLocksEditor = (s: LabelState): boolean =>
349346
s.previewMode.status === 'loading' || s.previewMode.status === 'active';
350347

348+
/** The dataset + mapping pair that batch emit needs, or null when
349+
* batch emit would produce nothing different from a single label.
350+
* Requires a loaded CSV with rows and at least one mapped Variable.
351+
* Co-narrows the two store fields so callers don't repeat the
352+
* null-checks for TS. */
353+
export const selectBatchInputs = (
354+
s: LabelState,
355+
): { dataset: CsvDataset; mapping: CsvMapping } | null => {
356+
const { csvDataset, csvMapping } = s;
357+
if (!csvDataset || csvDataset.rows.length === 0) return null;
358+
if (!csvMapping || Object.keys(csvMapping.bindings).length === 0) return null;
359+
return { dataset: csvDataset, mapping: csvMapping };
360+
};
361+
362+
/** Boolean form of {@link selectBatchInputs} — feeds the File menu
363+
* enable state without triggering re-renders on the inner refs. */
364+
export const selectCanBatchExport = (s: LabelState): boolean =>
365+
selectBatchInputs(s) !== null;
366+
351367
function updateCurrentObjects(
352368
state: PageState,
353369
fn: (objects: LabelObject[]) => LabelObject[]

‎src/types/Group.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export type GroupObject = LabelObjectBase & {
2424
* consumers — keeping it here breaks the registry ↔ types cycle. */
2525
export type LabelObject = LeafObject | GroupObject;
2626

27+
/** One page of label objects. The store holds a list of these; consumers
28+
* that emit ZPL per-page (zplGenerator, design-file save) take the
29+
* `Page` shape directly so the lib layer doesn't have to depend on
30+
* the store. */
31+
export interface Page {
32+
objects: LabelObject[];
33+
}
34+
2735
export function isGroup(obj: LabelObject): obj is GroupObject {
2836
return obj.type === 'group';
2937
}

0 commit comments

Comments
 (0)