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 ?? [];
|
||||
|
||||
/**
|
||||
* 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
|
||||
// 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<string, unknown> = {};
|
||||
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}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user