Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 4 additions & 28 deletions packages/core/src/lib/designFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,16 @@ import { dbSourceRefSchema, type DbSourceRef } from "../types/DataSource";
import type { LabelObject } from "../types/Group";
import { blockOverlaySchema, overlayText, type BlockOverlay } from "./zplOverlay/overlay";
import { reconstructLegacyBlockHeads, storedFormatPathOf } from "./zplHeadScan";
import { visitLeavesInPages, foldSerialLeaf, fixTlc39SlotsLeaf, bindSingleMarkerLeaf, safeUniqueNameById } from "./objectTree";
import { visitLeavesInPages, foldSerialLeaf, fixTlc39SlotsLeaf, bindSingleMarkerLeaf, migrateGs1DatabarLeaf, safeUniqueNameById } from "./objectTree";
import { sanitiseLoadedVariables } from "./loadedVariables";
import { insertReverseBackingBoxes, pageNeedsReverseBacking } from "./reverseBacking";
import { ok, err } from "./result";
import { schemaIssues } from "./schemaIssues";
import { getEntry } from "../registry";
import { propDomainIssue, truncateIntegerProps, type PropSpec } from "../types/propSpec";

/** Current design-file schema version. Bump when the persisted shape
* changes in a way an older app cannot read. Add a vN schema +
* migrator below and dispatch on `schemaVersion` in `parseDesignFile`.
* The persist middleware in `labelStore` has its own independent
* version for localStorage state; do not conflate. */
/** Bump when an older app could not read the persisted shape, with a hop in the migration chain
* of `parseDesignFile`. The persist middleware in `labelStore` versions localStorage on its own. */
export const CURRENT_DESIGN_SCHEMA_VERSION = 6;

export type DesignFileError = "parse_error" | "invalid_schema" | "fn_slots_exhausted";
Expand Down Expand Up @@ -129,7 +126,7 @@ export function parseDesignFile(text: string): DesignFileResult {
// Valid JSON that is not an object (null, true, "x") must reject, not throw.
if (!json || typeof json !== "object") return err("invalid_schema");

migrateGs1databarModuleWidth(json);
visitLeavesInPages((json as { pages?: unknown }).pages, migrateGs1DatabarLeaf);
visitLeavesInPages((json as { pages?: unknown }).pages, fixTlc39SlotsLeaf);
migrateReverseTextBackground(json);
migrateSerialToTextMode(json);
Expand Down Expand Up @@ -184,27 +181,6 @@ function migrateReverseTextBackground(json: unknown): void {
j.schemaVersion = 2;
}

/** Rename gs1databar `props.moduleWidth` → `props.magnification` on
* legacy design files. Mirrors the persist-store v5→v6 hop in
* `labelStore.ts`. Idempotent. */
function migrateGs1databarModuleWidth(json: unknown): void {
if (!json || typeof json !== "object") return;
const pages = (json as { pages?: unknown }).pages;
visitLeavesInPages(pages, (leaf) => {
if (
leaf.type === "gs1databar" &&
leaf.props &&
typeof leaf.props === "object" &&
"moduleWidth" in leaf.props &&
!("magnification" in leaf.props)
) {
const props = leaf.props as Record<string, unknown>;
props.magnification = props.moduleWidth;
delete props.moduleWidth;
}
});
}

/** Convert the legacy standalone `serial` object type into a `text` field with
* a `serial` prop (the dissolved field-mode model). Keeps content/font/rotation
* and folds increment/zplMode into `props.serial`. Idempotent. */
Expand Down
30 changes: 22 additions & 8 deletions packages/core/src/lib/designOps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,36 @@ describe("applyDesignOps", () => {
const seen: { dpmm: number; variables: string[] }[] = [];
const input = doc();
input.pages[0]!.objects.push(rightBarcode("bc"));
const result = applyDesignOps(
input,
[{ op: "addVariable", variable: { name: "qty" } }, { op: "update", id: "bc", props: { content: "«qty»" } }],
(_o, ctx) => {
seen.push({ dpmm: ctx.label.dpmm, variables: ctx.variables.map((v) => v.name) });
return null;
},
);
input.variables.push(variable("qty", 2, "1"));
const result = applyDesignOps(input, [{ op: "update", id: "bc", props: { content: "«qty»" } }], (_o, ctx) => {
seen.push({ dpmm: ctx.label.dpmm, variables: ctx.variables.map((v) => v.name) });
return null;
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.doc.pages[0]?.objects[1]).toMatchObject({ dirty: true, props: { content: "«qty»" } });
expect([...result.edited]).toEqual([0]);
expect([...result.touched]).toEqual([]);
expect(result.doc.pages.every((p) => p.overlay === overlay)).toBe(true);
expect(seen.length).toBeGreaterThan(0);
expect(seen.every((s) => s.dpmm === 8 && s.variables.includes("qty"))).toBe(true);
});

it("measures under a variable the same batch adds, and that addition costs every capture", () => {
const seen: string[][] = [];
const input = doc();
input.pages[0]!.objects.push(rightBarcode("bc"));
const result = applyDesignOps(input, [{ op: "addVariable", variable: { name: "qty" } }, { op: "update", id: "bc", props: { content: "«qty»" } }], (_o, ctx) => {
seen.push(ctx.variables.map((v) => v.name));
return null;
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(seen.length).toBeGreaterThan(0);
expect(seen.every((names) => names.includes("qty"))).toBe(true);
expect([...result.touched]).toEqual([0, 1]);
});

it("renames a variable through every marker without costing the captures", () => {
const result = applyDesignOps(doc(), [{ op: "updateVariable", name: "lot", newName: "batch", comment: "c" }], noProbe);
expect(result.ok).toBe(true);
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/lib/objectTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ export function fixTlc39SlotsLeaf(leaf: UnknownNode): void {
}
}

/** Legacy gs1databar `moduleWidth` is the ^BR magnification. */
export function migrateGs1DatabarLeaf(leaf: UnknownNode): void {
if (leaf.type !== "gs1databar" || !leaf.props || typeof leaf.props !== "object") return;
const props = leaf.props as Record<string, unknown>;
if (!("moduleWidth" in props) || "magnification" in props) return;
props.magnification = props.moduleWidth;
delete props.moduleWidth;
}

/** Legacy standalone `serial` object type → `text` field with a `serial` prop. */
export function foldSerialLeaf(leaf: UnknownNode): void {
if (leaf.type !== "serial" || !leaf.props || typeof leaf.props !== "object") return;
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/lib/zplOverlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@

import { z } from "zod";

/** Key identifying a labelConfig/printerProfile field a config command sets.
* Refined to a union as config linkage lands (later stage); string for now. */
/** The labelConfig or printerProfile field a config command sets. */
export type ConfigFieldKey = string;

export type OverlaySegment =
Expand Down
8 changes: 8 additions & 0 deletions src/store/labelStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,14 @@ describe('setPageStoredFormatPath', () => {
});
});

describe('migrateLegacy: v5 gs1databar moduleWidth', () => {
it('becomes the magnification', () => {
const persisted = { pages: [{ objects: [{ id: 'g', type: 'gs1databar', x: 0, y: 0, rotation: 0, props: { moduleWidth: 4 } }] }] };
const migrated = migrateLegacy(persisted, 5) as { pages: { objects: { props: Record<string, unknown> }[] }[] };
expect(migrated.pages[0]?.objects[0]?.props).toEqual({ magnification: 4 });
});
});

describe('migrateLegacy: a main-era ^DF that only the overlay bytes carried', () => {
it('lands on the page and its head, so naming the page rewrites instead of doubling', () => {
const imported = importZplText(['^XA', '^DFE:OLD.ZPL', '^FO10,10^A0N,30,30^FDa^FS', '^XZ'].join('\n'), 8);
Expand Down
44 changes: 14 additions & 30 deletions src/store/labelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
visitLeavesInPages,
foldSerialLeaf,
fixTlc39SlotsLeaf,
migrateGs1DatabarLeaf,
bindSingleMarkerLeaf,
safeUniqueNameById,
} from '@zplab/core/lib/objectTree';
Expand Down Expand Up @@ -165,11 +166,9 @@ export function migrateLegacy(persistedState: unknown, version: number): unknown
s = { ...s, printerProfile: {} };
}

// v5→v6: gs1databar props.moduleWidth → props.magnification. The
// value was always semantically the ^BR magnification multiplier
// (1-10), not a dot quantity; the rename clarifies that.
// v5→v6: gs1databar props.moduleWidth → props.magnification.
if (version < 6) {
s = { ...s, pages: migrateGs1DatabarInPages(s.pages) };
visitLeavesInPages(s.pages, migrateGs1DatabarLeaf);
}

// v6→v7: ^MA/^MI type codes corrected to match spec (R/C instead of
Expand Down Expand Up @@ -230,17 +229,6 @@ export function migrateLegacy(persistedState: unknown, version: number): unknown
}
}

// v15→v16: TLC39 props move to the real ^BT slots (w2/h2, wideRatio);
// emit and canvas read the new shape unguarded.
if (version < 16 && Array.isArray(s.pages)) {
visitLeavesInPages(s.pages, fixTlc39SlotsLeaf);
}

// v17→v18: the same drive pin the .zpld lane applies at schema v6.
if (version < 18 && Array.isArray(s.pages)) {
visitLeavesInPages(s.pages, pinBareFontDriveLeaf);
}

// v9→v10: reverse text dropped its synthesized self-background ^GB for a
// spec-true ^FR knockout. Give every legacy reverse text a real black box
// behind it so the white-on-black look survives the model change.
Expand Down Expand Up @@ -353,6 +341,17 @@ export function migrateLegacy(persistedState: unknown, version: number): unknown
}
}

// v15→v16: TLC39 props move to the real ^BT slots (w2/h2, wideRatio);
// emit and canvas read the new shape unguarded.
if (version < 16 && Array.isArray(s.pages)) {
visitLeavesInPages(s.pages, fixTlc39SlotsLeaf);
}

// v17→v18: the same drive pin the .zpld lane applies at schema v6.
if (version < 18 && Array.isArray(s.pages)) {
visitLeavesInPages(s.pages, pinBareFontDriveLeaf);
}

// v18→v19: the print target moves from loose browser keys into the session. Persist writes the
// merged state back only after a migration, so the takeover has to run here and not at slice init.
if (version < 19 && !('printTarget' in s)) {
Expand Down Expand Up @@ -415,21 +414,6 @@ function migrateSingleBindInPages(
return pages;
}

function migrateGs1DatabarInPages(pages: unknown): unknown {
visitLeavesInPages(pages, (leaf) => {
if (
leaf.type !== 'gs1databar' ||
!leaf.props ||
typeof leaf.props !== 'object'
) return;
const props = leaf.props as Record<string, unknown>;
if (!('moduleWidth' in props) || 'magnification' in props) return;
props.magnification = props.moduleWidth;
delete props.moduleWidth;
});
return pages;
}

function migrateCirclesInPages(pages: unknown): unknown {
if (!Array.isArray(pages)) return pages;
return pages.map((page) => {
Expand Down
Loading