From bc0dc4febf2eaa10a832aa8ce259cd8f23527e45 Mon Sep 17 00:00:00 2001 From: Yashas Date: Thu, 10 Sep 2026 17:31:52 +0530 Subject: [PATCH] fix: a form field's id is not the workflow field's name Prefill is computed from the record row, so it is keyed the way the data is keyed; the form's fields carry the deploy-assigned suffix. Untranslated, the draft landed under a key no control reads and then rode into the submit payload, where the workflow refuses the whole activity on an unknown key. Re-key prefill and seed onto the form's own field ids, and build the submit payload by walking fields rather than posting the whole value bag. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/ActivityForm.tsx | 58 ++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/src/components/ActivityForm.tsx b/src/components/ActivityForm.tsx index 53a93b8..f9d3539 100644 --- a/src/components/ActivityForm.tsx +++ b/src/components/ActivityForm.tsx @@ -71,13 +71,51 @@ export function ActivityForm({ const fields = schema.data?.fields ?? []; + /** + * Re-key a values bag onto the form's OWN field ids. + * + * A form field's id is not the workflow field's name. A mapped activity + * field shares its slug with the workflow-level field it maps to, and + * deploy resolves that collision by suffixing the activity copy — so the + * instance data says `cm_approved_amount` and this form's field is + * `cm_approved_amount_2`. Everything we compute from a record row + * (prefill.ts, and any seed) is keyed the way the DATA is keyed, so it has + * to be translated on the way in. + * + * Untranslated it fails twice over, and the first failure hides the second: + * the drafted value lands under a key no control reads, so the form opens + * blank; and it then rides along in the submit payload, where the workflow + * rejects it as an unknown field — `validation failed for activity + * hdfc-act-cm-approve: cm_approved_amount(unknown), …`. + * + * Match on the exact id first, then on the id with a trailing `_` + * removed. Never hardcode `_2`: the number is deploy-assigned and a slug + * used on two activities takes the next one (`firm_name_2` on Capture, + * `firm_name_3` on Upload Documents). + */ + const toFieldKeys = (bag: Record | undefined) => { + if (!bag) return {}; + const byId = new Map(fields.map((f) => [f.id, f])); + const bySlug = new Map(); + for (const f of fields) { + const slug = f.id.replace(/_\d+$/, ''); + if (slug !== f.id && !byId.has(slug) && !bySlug.has(slug)) bySlug.set(slug, f.id); + } + const out: Record = {}; + for (const [k, v] of Object.entries(bag)) { + const id = byId.has(k) ? k : bySlug.get(k); + if (id) out[id] = v; + } + return out; + }; + // Precedence, weakest first: the platform's own field defaults, then our // computed draft, then the seeded identifiers (which also become read-only). const initial = useMemo( () => ({ ...(schema.data?.field_defaults ?? {}), - ...(prefill ?? {}), - ...(seed ?? {}), + ...toFieldKeys(prefill), + ...toFieldKeys(seed), }), // eslint-disable-next-line react-hooks/exhaustive-deps [schema.data, JSON.stringify(prefill), JSON.stringify(seed)], @@ -143,9 +181,19 @@ export function ActivityForm({ setBusy(true); setErr(null); try { + // Submit ONLY what this form actually has a field for. The workflow + // validates every key in the payload and refuses the whole submission + // on an unknown one, so a stray key is not ignored — it is fatal. This + // is the backstop for the re-keying above: if a translation ever misses, + // the field simply arrives empty instead of failing the activity. + const payload: Record = {}; + for (const f of fields) { + if (Object.prototype.hasOwnProperty.call(current, f.id)) payload[f.id] = current[f.id]; + } + const res = instanceId - ? await client.performActivity(WORKFLOW, instanceId, activityUid, current) - : await client.startInstance(WORKFLOW, activityUid, current); + ? await client.performActivity(WORKFLOW, instanceId, activityUid, payload) + : await client.startInstance(WORKFLOW, activityUid, payload); reset(); onDone?.({ instance_id: res.instance_id }); onClose(); @@ -183,7 +231,7 @@ export function ActivityForm({ key={f.id} field={f} value={current[f.id]} - readOnly={seed ? Object.prototype.hasOwnProperty.call(seed, f.id) : false} + readOnly={Object.prototype.hasOwnProperty.call(toFieldKeys(seed), f.id)} error={touched && missing.includes(f.id) ? 'Required' : null} onChange={(v) => setValues((s) => ({ ...s, [f.id]: v }))} activityUid={activityUid}