Two latent useState("") hydration bugs in product-detail-client.tsx
matching the U6 isHidden pattern. nameKh and categoryId
were hardcoded empty regardless of the loaded template's saved state — so re-opening a product
with a Khmer name or non-default category showed empty fields, and saving without re-entering
blanked them in the DB.
U6 fixed the same trap for isHidden on 2026-05-26 — useState(false)
ignored the persisted value. Two siblings stayed broken:
const [name, setName] = useState(template.name); - const [nameKh, setNameKh] = useState(""); - const [categoryId, setCategoryId] = useState(""); + const [nameKh, setNameKh] = useState(template.nameKh ?? ""); + const [categoryId, setCategoryId] = useState(template.categoryId ?? "");
updateProductAction already accepts + persists both fields, so the save path
was fine. The load path returned them as undefined because the two DAOs
(getTemplateWithLinesById + listTemplatesWithLines) didn't
SELECT them. The fix threads both columns through to the client.
lib/db/product_attribute.ts — TemplateRow gains
nameKh: string | null + categoryId: string | null;
listTemplatesWithLines + getTemplateWithLinesById both
SELECT T.nameKh + T.categoryId and propagate to the returned shape.lib/actions/product_attribute.ts —
createTemplateAction's returned row gains nameKh: null +
categoryId: null (new rows start empty; the form lets the admin fill them later).app/(authed)/products/[id]/product-detail-client.tsx —
hydration switched to useState(template.nameKh ?? "") /
useState(template.categoryId ?? ""). The legacy
useState("") form is explicitly asserted gone in Gate 1.No migration needed — both columns already exist on cafe.product_template
(name_kh varchar(200) and category_id uuid → cafe.product_categories.id);
see schema.ts:781-782. They were added by H4-X as bridge columns when
cafe.products became a view.
| ✓ | TemplateRow interface gains nameKh: string | null + categoryId: string | null |
| ✓ | listTemplatesWithLines selects both via T.nameKh / T.categoryId and returns them in the row map |
| ✓ | getTemplateWithLinesById selects + returns both fields |
| ✓ | createTemplateAction returns nameKh: null + categoryId: null on the new row |
| ✓ | product-detail-client.tsx hydrates via useState(template.nameKh ?? "") + useState(template.categoryId ?? ""); legacy useState("") patterns asserted absent |
| ✓ | DAO round-trip — seed category + product on local lumiere → updateProduct with Khmer name + categoryId → both getTemplateWithLinesById and listTemplatesWithLines return the saved values verbatim |
Typecheck: npx tsc --noEmit on nix-cafe → 0 errors after threading both fields through the action layer.
karouna-dev → CF auto-deploy on nix-cafe.test-u6-followup-prod.mjs on lumiere: seed throwaway category + product on prod Supabase, drive Settings UI through save → reload → assert both fields hydrate; restore.