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) <noreply@anthropic.com>
This commit is contained in:
parent
f90d5b5d53
commit
bc0dc4febf
@ -71,13 +71,51 @@ export function ActivityForm({
|
|||||||
|
|
||||||
const fields = schema.data?.fields ?? [];
|
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 `_<n>`
|
||||||
|
* 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<string, unknown> | undefined) => {
|
||||||
|
if (!bag) return {};
|
||||||
|
const byId = new Map(fields.map((f) => [f.id, f]));
|
||||||
|
const bySlug = new Map<string, string>();
|
||||||
|
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<string, unknown> = {};
|
||||||
|
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
|
// Precedence, weakest first: the platform's own field defaults, then our
|
||||||
// computed draft, then the seeded identifiers (which also become read-only).
|
// computed draft, then the seeded identifiers (which also become read-only).
|
||||||
const initial = useMemo(
|
const initial = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...(schema.data?.field_defaults ?? {}),
|
...(schema.data?.field_defaults ?? {}),
|
||||||
...(prefill ?? {}),
|
...toFieldKeys(prefill),
|
||||||
...(seed ?? {}),
|
...toFieldKeys(seed),
|
||||||
}),
|
}),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[schema.data, JSON.stringify(prefill), JSON.stringify(seed)],
|
[schema.data, JSON.stringify(prefill), JSON.stringify(seed)],
|
||||||
@ -143,9 +181,19 @@ export function ActivityForm({
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
try {
|
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<string, unknown> = {};
|
||||||
|
for (const f of fields) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(current, f.id)) payload[f.id] = current[f.id];
|
||||||
|
}
|
||||||
|
|
||||||
const res = instanceId
|
const res = instanceId
|
||||||
? await client.performActivity(WORKFLOW, instanceId, activityUid, current)
|
? await client.performActivity(WORKFLOW, instanceId, activityUid, payload)
|
||||||
: await client.startInstance(WORKFLOW, activityUid, current);
|
: await client.startInstance(WORKFLOW, activityUid, payload);
|
||||||
reset();
|
reset();
|
||||||
onDone?.({ instance_id: res.instance_id });
|
onDone?.({ instance_id: res.instance_id });
|
||||||
onClose();
|
onClose();
|
||||||
@ -183,7 +231,7 @@ export function ActivityForm({
|
|||||||
key={f.id}
|
key={f.id}
|
||||||
field={f}
|
field={f}
|
||||||
value={current[f.id]}
|
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}
|
error={touched && missing.includes(f.id) ? 'Required' : null}
|
||||||
onChange={(v) => setValues((s) => ({ ...s, [f.id]: v }))}
|
onChange={(v) => setValues((s) => ({ ...s, [f.id]: v }))}
|
||||||
activityUid={activityUid}
|
activityUid={activityUid}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user