Skip to content

Commit 75c2115

Browse files
committed
fix(zpl): correct ^MA type codes + ^JH g-slot wire format per spec
^MA type codes were H/R, spec is R (replacement) / C (cleaning). ^MA units default was M, spec default is I (inches). ^MA threshold/frequency caps are type-dependent per spec: R=150000m, C=2000m, enforced via superRefine. ^JH g-slot wire format is an indexed table (0..16 → 100M..900M), not raw <n>M with suffix as previously emitted. UI now exposes the discrete set via a select; the persisted meters value is constrained to the indexed set. A v6→v7 migration remaps maintenanceAlert/Message type 'H' → 'C' ('R' was already correct). The rehydrate fixpoint loop drops any residual schema violations top-key-by-top-key, bailing to {} if no present key can resolve the issue. The MaintenanceTab type-select clamps stale threshold/frequency to the new type's cap before patching, so a switch R→C with an out-of-spec value can't trip the schema and silently snap the dropdown back to R. JH_SLOT_F/G hoisted to PrinterProfile.ts so generator + parser share one definition; JH_SLOT_COUNT replaces the hard-coded 10.
1 parent a6da71d commit 75c2115

42 files changed

Lines changed: 1285 additions & 1029 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/components/PrinterSettings/ClockAndTimeTab.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export function ClockAndTimeTab() {
148148
min={CLOCK_TOLERANCE_RANGE.min}
149149
max={CLOCK_TOLERANCE_RANGE.max}
150150
value={profile.clockTolerance}
151+
// Required while clockMode='TOL': empty blur snaps the
152+
// draft back to the last committed value with no patch
153+
// sent, so the input stays in sync with the store.
154+
required
151155
onChange={(v) => patchPrinterProfile({ clockTolerance: v })}
152156
/>
153157
)}

‎src/components/PrinterSettings/IdentityTab.tsx‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ export function IdentityTab() {
4141
const loc = t.printerSettings.identity;
4242
const nameId = useId();
4343
const passwordId = useId();
44-
// Local mirror for the 4-digit input: progressive typing ("4", "47")
45-
// is shown immediately but only commits to the store once the regex
46-
// matches. Without this, partial values would always be filtered out
47-
// and the user could never finish entering a password. Re-sync on
48-
// store changes (import, reset) uses the adjust-state-during-render
49-
// pattern so the React compiler doesn't flag a useEffect → setState
50-
// cascade.
44+
// Progressive-commit draft: store only accepts a complete 4-digit
45+
// value, but the user has to type intermediate states. SafeStringInput
46+
// is fully controlled and would clobber the partial draft, so the
47+
// ^KP input stays hand-rolled. Adjust-state-during-render reseeds on
48+
// external changes (import, reset).
5149
const storedPassword = profile.setPassword ?? "";
5250
const [passwordDraft, setPasswordDraft] = useState(storedPassword);
5351
const [lastStoredPassword, setLastStoredPassword] = useState(storedPassword);
Lines changed: 153 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
1+
import { useId } from "react";
12
import { useT } from "../../lib/useT";
23
import { useLabelStore } from "../../store/labelStore";
34
import { inputCls, labelCls } from "../ui/formStyles";
45
import type { PrinterProfile } from "../../types/PrinterProfile";
56
import {
6-
HEAD_CLEANING_INTERVAL_RANGE,
7+
HEAD_CLEANING_INTERVAL_METERS,
78
MAINTENANCE_ALERT_DEFAULTS,
89
MAINTENANCE_ALERT_PRINT_VALUES,
910
MAINTENANCE_ALERT_TYPES,
1011
MAINTENANCE_ALERT_UNITS,
11-
MAINTENANCE_DISTANCE_MAX,
12+
MAINTENANCE_DISTANCE_MAX_BY_TYPE,
1213
MAINTENANCE_MESSAGE_MAX_LEN,
14+
isHeadCleaningIntervalMeters,
1315
isMaintenanceAlertPrint,
1416
isMaintenanceAlertType,
1517
isMaintenanceAlertUnit,
18+
type HeadCleaningIntervalMeters,
1619
type MaintenanceAlertType,
1720
type MaintenanceAlertUnit,
1821
} from "../../types/PrinterProfile";
1922
import {
2023
BoundedIntControl,
2124
SafeStringInput,
22-
ZplBoundedIntInput,
2325
ZplCommandLabel,
2426
ZplEnumSelect,
2527
ZplField,
@@ -30,14 +32,14 @@ type LocMaintenance = ReturnType<typeof useT>["printerSettings"]["maintenance"];
3032
type MaintenanceAlert = NonNullable<PrinterProfile["maintenanceAlert"]>;
3133

3234
const ALERT_TYPE_LABEL_KEYS = {
33-
H: 'alertTypeH',
3435
R: 'alertTypeR',
36+
C: 'alertTypeC',
3537
} as const satisfies Record<MaintenanceAlertType, keyof LocMaintenance>;
3638

3739
const ALERT_UNIT_LABEL_KEYS = {
38-
M: 'alertUnitsM',
39-
I: 'alertUnitsI',
4040
C: 'alertUnitsC',
41+
I: 'alertUnitsI',
42+
M: 'alertUnitsM',
4143
} as const satisfies Record<MaintenanceAlertUnit, keyof LocMaintenance>;
4244

4345
/** Maintenance tab (^MA/^MI/^MW/^JH). */
@@ -49,9 +51,7 @@ export function MaintenanceTab() {
4951
const alert = profile.maintenanceAlert;
5052
const message = profile.maintenanceMessage;
5153

52-
// Cascade between maintenanceAlert.type and maintenanceMessage.type
53-
// lives in patchPrinterProfile so UI call sites don't carry the
54-
// invariant; this helper only owns the per-field merge.
54+
// Type-cascade lives in patchPrinterProfile; this helper only owns the merge.
5555
const updateAlert = <K extends keyof MaintenanceAlert>(key: K, value: MaintenanceAlert[K]) => {
5656
if (!alert) return;
5757
patchPrinterProfile({ maintenanceAlert: { ...alert, [key]: value } });
@@ -61,109 +61,116 @@ export function MaintenanceTab() {
6161
<div className="flex flex-col gap-4">
6262
<ZplField>
6363
<ZplCommandLabel text={loc.maintenanceAlertHeading} command="^MA" />
64-
<div className="flex flex-col gap-2 pl-2 border-l border-border">
65-
<div className="grid grid-cols-2 gap-2">
66-
<div className="flex flex-col gap-1">
67-
<label className={labelCls}>{loc.alertType}</label>
64+
<ZplSubField label={loc.alertType}>
65+
{(id) => (
66+
<select
67+
id={id}
68+
className={inputCls}
69+
value={alert?.type ?? ""}
70+
onChange={(e) => {
71+
const raw = e.target.value;
72+
if (raw === "") {
73+
patchPrinterProfile({ maintenanceAlert: undefined });
74+
return;
75+
}
76+
if (!isMaintenanceAlertType(raw)) return;
77+
// First creation seeds defaults; later edits re-spread.
78+
// Threshold/frequency clamp to the new type's cap so a switch
79+
// R→C with stale 50000 doesn't trip the schema and snap back.
80+
const cap = MAINTENANCE_DISTANCE_MAX_BY_TYPE[raw];
81+
patchPrinterProfile({
82+
maintenanceAlert: alert
83+
? {
84+
...alert,
85+
type: raw,
86+
threshold: Math.min(alert.threshold, cap),
87+
frequency: Math.min(alert.frequency, cap),
88+
}
89+
: { ...MAINTENANCE_ALERT_DEFAULTS, type: raw },
90+
});
91+
}}
92+
>
93+
<option value="">{t.printerSettings.defaultOption}</option>
94+
{MAINTENANCE_ALERT_TYPES.map((v) => (
95+
<option key={v} value={v}>{loc[ALERT_TYPE_LABEL_KEYS[v]]}</option>
96+
))}
97+
</select>
98+
)}
99+
</ZplSubField>
100+
<div
101+
className={`grid grid-cols-4 items-end gap-2 pl-2 mt-1 border-l-2 ${
102+
alert ? "border-accent/60" : "border-border/40"
103+
}`}
104+
>
105+
<ZplSubField label={loc.alertPrint}>
106+
{(id) => (
68107
<select
108+
id={id}
69109
className={inputCls}
70-
value={alert?.type ?? ""}
110+
value={alert?.print ?? ""}
71111
onChange={(e) => {
72112
const raw = e.target.value;
73-
if (raw === "") {
74-
patchPrinterProfile({ maintenanceAlert: undefined });
75-
return;
76-
}
77-
if (!isMaintenanceAlertType(raw)) return;
78-
// First-time creation seeds defaults; subsequent edits
79-
// re-spread the alert. The cross-field type cascade
80-
// is enforced by patchPrinterProfile.
81-
patchPrinterProfile({
82-
maintenanceAlert: alert
83-
? { ...alert, type: raw }
84-
: { ...MAINTENANCE_ALERT_DEFAULTS, type: raw },
85-
});
113+
if (isMaintenanceAlertPrint(raw)) updateAlert("print", raw);
86114
}}
115+
disabled={!alert}
87116
>
88-
<option value="">{t.printerSettings.defaultOption}</option>
89-
{MAINTENANCE_ALERT_TYPES.map((v) => (
90-
<option key={v} value={v}>{loc[ALERT_TYPE_LABEL_KEYS[v]]}</option>
117+
{/* placeholder shown only while the alert is undefined */}
118+
<option value="" disabled hidden></option>
119+
{MAINTENANCE_ALERT_PRINT_VALUES.map((v) => (
120+
<option key={v} value={v}>{v === "Y" ? loc.alertPrintY : loc.alertPrintN}</option>
91121
))}
92122
</select>
93-
</div>
94-
<ZplSubField label={loc.alertPrint}>
95-
{(id) => (
96-
<select
97-
id={id}
98-
className={inputCls}
99-
value={alert?.print ?? MAINTENANCE_ALERT_DEFAULTS.print}
100-
onChange={(e) => {
101-
const raw = e.target.value;
102-
if (isMaintenanceAlertPrint(raw)) updateAlert("print", raw);
103-
}}
104-
disabled={!alert}
105-
>
106-
{MAINTENANCE_ALERT_PRINT_VALUES.map((v) => (
107-
<option key={v} value={v}>{v === "Y" ? loc.alertPrintY : loc.alertPrintN}</option>
108-
))}
109-
</select>
110-
)}
111-
</ZplSubField>
112-
</div>
113-
<div className="grid grid-cols-3 gap-2">
114-
<ZplSubField label={loc.alertThreshold}>
115-
{(id) => (
116-
<BoundedIntControl
117-
id={id}
118-
min={0}
119-
max={MAINTENANCE_DISTANCE_MAX}
120-
value={alert?.threshold}
121-
disabled={!alert}
122-
onChange={(threshold) => {
123-
// Drop undefined (mid-typing empty); blur clamps stale state.
124-
if (threshold !== undefined) updateAlert("threshold", threshold);
125-
}}
126-
/>
127-
)}
128-
</ZplSubField>
129-
<ZplSubField label={loc.alertFrequency}>
130-
{(id) => (
131-
<BoundedIntControl
132-
id={id}
133-
min={1}
134-
max={MAINTENANCE_DISTANCE_MAX}
135-
value={alert?.frequency}
136-
disabled={!alert}
137-
onChange={(frequency) => {
138-
// readBoundedInt lets sub-min positives through so a
139-
// user typing "1" toward "12" survives a min=2 field;
140-
// schema requires >= min, so gate the commit here.
141-
if (frequency !== undefined && frequency >= 1) {
142-
updateAlert("frequency", frequency);
143-
}
144-
}}
145-
/>
146-
)}
147-
</ZplSubField>
148-
<ZplSubField label={loc.alertUnits}>
149-
{(id) => (
150-
<select
151-
id={id}
152-
className={inputCls}
153-
value={alert?.units ?? MAINTENANCE_ALERT_DEFAULTS.units}
154-
onChange={(e) => {
155-
const raw = e.target.value;
156-
if (isMaintenanceAlertUnit(raw)) updateAlert("units", raw);
157-
}}
158-
disabled={!alert}
159-
>
160-
{MAINTENANCE_ALERT_UNITS.map((v) => (
161-
<option key={v} value={v}>{loc[ALERT_UNIT_LABEL_KEYS[v]]}</option>
162-
))}
163-
</select>
164-
)}
165-
</ZplSubField>
166-
</div>
123+
)}
124+
</ZplSubField>
125+
<ZplSubField label={loc.alertThreshold}>
126+
{(id) => (
127+
<BoundedIntControl
128+
id={id}
129+
min={0}
130+
max={MAINTENANCE_DISTANCE_MAX_BY_TYPE[alert?.type ?? MAINTENANCE_ALERT_DEFAULTS.type]}
131+
value={alert?.threshold}
132+
disabled={!alert}
133+
required={!!alert}
134+
onChange={(threshold) => {
135+
if (threshold !== undefined) updateAlert("threshold", threshold);
136+
}}
137+
/>
138+
)}
139+
</ZplSubField>
140+
<ZplSubField label={loc.alertFrequency}>
141+
{(id) => (
142+
<BoundedIntControl
143+
id={id}
144+
min={0}
145+
max={MAINTENANCE_DISTANCE_MAX_BY_TYPE[alert?.type ?? MAINTENANCE_ALERT_DEFAULTS.type]}
146+
value={alert?.frequency}
147+
disabled={!alert}
148+
required={!!alert}
149+
onChange={(frequency) => {
150+
if (frequency !== undefined) updateAlert("frequency", frequency);
151+
}}
152+
/>
153+
)}
154+
</ZplSubField>
155+
<ZplSubField label={loc.alertUnits}>
156+
{(id) => (
157+
<select
158+
id={id}
159+
className={inputCls}
160+
value={alert?.units ?? ""}
161+
onChange={(e) => {
162+
const raw = e.target.value;
163+
if (isMaintenanceAlertUnit(raw)) updateAlert("units", raw);
164+
}}
165+
disabled={!alert}
166+
>
167+
<option value="" disabled hidden></option>
168+
{MAINTENANCE_ALERT_UNITS.map((v) => (
169+
<option key={v} value={v}>{loc[ALERT_UNIT_LABEL_KEYS[v]]}</option>
170+
))}
171+
</select>
172+
)}
173+
</ZplSubField>
167174
</div>
168175
</ZplField>
169176

@@ -209,16 +216,14 @@ export function MaintenanceTab() {
209216
{loc.earlyWarningGateHint}
210217
</span>
211218

212-
<ZplBoundedIntInput
219+
<HeadCleaningIntervalSelect
213220
label={loc.headCleaningIntervalMeters}
214-
command="^JH"
215-
min={HEAD_CLEANING_INTERVAL_RANGE.min}
216-
max={HEAD_CLEANING_INTERVAL_RANGE.max}
221+
defaultLabel={t.printerSettings.defaultOption}
222+
unit={loc.alertUnitsM}
217223
value={profile.headCleaningIntervalMeters}
218224
onChange={(headCleaningIntervalMeters) =>
219225
patchPrinterProfile({ headCleaningIntervalMeters })
220226
}
221-
unit={loc.alertUnitsM}
222227
/>
223228
<span className={`${labelCls} normal-case tracking-normal text-muted/70 -mt-2`}>
224229
{loc.headCleaningIntervalHint}
@@ -238,3 +243,42 @@ export function MaintenanceTab() {
238243
);
239244
}
240245

246+
function HeadCleaningIntervalSelect({
247+
label,
248+
defaultLabel,
249+
unit,
250+
value,
251+
onChange,
252+
}: {
253+
label: string;
254+
defaultLabel: string;
255+
unit: string;
256+
value: HeadCleaningIntervalMeters | undefined;
257+
onChange: (next: HeadCleaningIntervalMeters | undefined) => void;
258+
}) {
259+
const id = useId();
260+
return (
261+
<ZplField>
262+
<ZplCommandLabel text={label} command="^JH" htmlFor={id} />
263+
<div className="flex items-center gap-2">
264+
<select
265+
id={id}
266+
className={inputCls}
267+
value={value ?? ""}
268+
onChange={(e) => {
269+
const raw = e.target.value;
270+
if (raw === "") { onChange(undefined); return; }
271+
const n = Number(raw);
272+
if (isHeadCleaningIntervalMeters(n)) onChange(n);
273+
}}
274+
>
275+
<option value="">{defaultLabel}</option>
276+
{HEAD_CLEANING_INTERVAL_METERS.map((m) => (
277+
<option key={m} value={m}>{m}</option>
278+
))}
279+
</select>
280+
<span className={labelCls}>{unit}</span>
281+
</div>
282+
</ZplField>
283+
);
284+
}

0 commit comments

Comments
 (0)