|
1 | | -import { useId } from "react"; |
| 1 | +import { useId, useState } from "react"; |
2 | 2 | import { useT } from "../../lib/useT"; |
3 | 3 | import { useLabelStore } from "../../store/labelStore"; |
4 | 4 | import { inputCls, labelCls } from "../ui/formStyles"; |
5 | | -import { PRINTER_NAME_MAX_LEN } from "../../types/PrinterProfile"; |
| 5 | +import { |
| 6 | + CONFIG_UPDATE_VALUES, |
| 7 | + type ConfigUpdateAction, |
| 8 | + PRINTER_NAME_MAX_LEN, |
| 9 | + PRINTER_PASSWORD_REGEX, |
| 10 | + isConfigUpdateAction, |
| 11 | +} from "../../types/PrinterProfile"; |
6 | 12 | import { |
7 | 13 | ZplCommandLabel, |
| 14 | + ZplEnumSelect, |
8 | 15 | ZplField, |
9 | 16 | ZplSubField, |
10 | 17 | } from "./zplFieldPrimitives"; |
11 | 18 |
|
12 | | -/** Tab 5 of the Printer Settings Modal — printer identity setup. |
13 | | - * Emits ^KN (printer name + description) on the Setup-Script |
14 | | - * channel; both fields fold into a single `^KNname,description` |
15 | | - * command at emit time. The tab stays intentionally focused on |
16 | | - * "what is this printer called" — ^SL (clock-formatting Mode + |
17 | | - * Language) lives in Tab 3 Clock & Time per its semantic home. |
18 | | - * See memory:project_ticket_sl_clock_formatting for the ^SL |
19 | | - * follow-up. */ |
| 19 | +type LocIdentity = ReturnType<typeof useT>["printerSettings"]["identity"]; |
| 20 | + |
| 21 | +/** Explicit value → locale-key map; `satisfies` forces every |
| 22 | + * ConfigUpdateAction to have an entry and rejects typo keys at |
| 23 | + * compile time. Matches the CLOCK_FORMAT_LABEL_KEYS pattern. */ |
| 24 | +const CONFIG_UPDATE_LABEL_KEYS = { |
| 25 | + S: 'configurationUpdateOptionS', |
| 26 | + R: 'configurationUpdateOptionR', |
| 27 | + N: 'configurationUpdateOptionN', |
| 28 | + F: 'configurationUpdateOptionF', |
| 29 | +} as const satisfies Record<ConfigUpdateAction, keyof LocIdentity>; |
| 30 | + |
| 31 | +/** Tab 5 of the Printer Settings Modal: printer identity + EEPROM |
| 32 | + * commit setup. Emits ^KN (name + description), ^KP (4-digit panel |
| 33 | + * password) and ^JU (configuration update action) on the Setup- |
| 34 | + * Script channel. ^SL lives in Tab 3 Clock & Time per its semantic |
| 35 | + * home. See memory:project_ticket_sl_clock_formatting. */ |
20 | 36 | export function IdentityTab() { |
21 | 37 | const t = useT(); |
22 | 38 | const profile = useLabelStore((s) => s.printerProfile); |
23 | 39 | const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile); |
24 | 40 | const loc = t.printerSettings.identity; |
25 | 41 | const nameId = useId(); |
| 42 | + const passwordId = useId(); |
| 43 | + // Local mirror for the 4-digit input: progressive typing ("4", "47") |
| 44 | + // is shown immediately but only commits to the store once the regex |
| 45 | + // matches. Without this, partial values would always be filtered out |
| 46 | + // and the user could never finish entering a password. Re-sync on |
| 47 | + // store changes (import, reset) uses the adjust-state-during-render |
| 48 | + // pattern so the React compiler doesn't flag a useEffect → setState |
| 49 | + // cascade. |
| 50 | + const storedPassword = profile.setPassword ?? ""; |
| 51 | + const [passwordDraft, setPasswordDraft] = useState(storedPassword); |
| 52 | + const [lastStoredPassword, setLastStoredPassword] = useState(storedPassword); |
| 53 | + if (lastStoredPassword !== storedPassword) { |
| 54 | + setLastStoredPassword(storedPassword); |
| 55 | + setPasswordDraft(storedPassword); |
| 56 | + } |
26 | 57 |
|
27 | 58 | return ( |
28 | 59 | <div className="flex flex-col gap-4"> |
@@ -61,6 +92,43 @@ export function IdentityTab() { |
61 | 92 | )} |
62 | 93 | </ZplSubField> |
63 | 94 | </ZplField> |
| 95 | + |
| 96 | + <ZplField> |
| 97 | + <ZplCommandLabel text={loc.setPassword} command="^KP" htmlFor={passwordId} /> |
| 98 | + <input |
| 99 | + id={passwordId} |
| 100 | + type="text" |
| 101 | + inputMode="numeric" |
| 102 | + pattern="\d{4}" |
| 103 | + maxLength={4} |
| 104 | + className={inputCls} |
| 105 | + value={passwordDraft} |
| 106 | + onChange={(e) => { |
| 107 | + const raw = e.target.value; |
| 108 | + if (!/^\d{0,4}$/.test(raw)) return; |
| 109 | + setPasswordDraft(raw); |
| 110 | + if (raw === "") { |
| 111 | + patchPrinterProfile({ setPassword: undefined }); |
| 112 | + } else if (PRINTER_PASSWORD_REGEX.test(raw)) { |
| 113 | + patchPrinterProfile({ setPassword: raw }); |
| 114 | + } |
| 115 | + }} |
| 116 | + /> |
| 117 | + <span className={`${labelCls} normal-case tracking-normal text-muted/70`}> |
| 118 | + {loc.setPasswordHint} |
| 119 | + </span> |
| 120 | + </ZplField> |
| 121 | + |
| 122 | + <ZplEnumSelect |
| 123 | + label={loc.configurationUpdate} |
| 124 | + command="^JU" |
| 125 | + values={CONFIG_UPDATE_VALUES} |
| 126 | + isValid={isConfigUpdateAction} |
| 127 | + value={profile.configurationUpdate} |
| 128 | + onChange={(configurationUpdate) => patchPrinterProfile({ configurationUpdate })} |
| 129 | + defaultLabel={loc.configurationUpdateUnset} |
| 130 | + optionLabel={(v) => loc[CONFIG_UPDATE_LABEL_KEYS[v]]} |
| 131 | + /> |
64 | 132 | </div> |
65 | 133 | ); |
66 | 134 | } |
0 commit comments