The pipeline could be looked at but not driven. Adds the three pieces that make the machine walkable end to end from the UI. - ActivityForm renders whatever /view/form-screens returns — labels, types, select options, mandatory flags — and submits it back. No form is defined in this repo, so a field added in Studio appears here with no code change. - AddLead presents the three doors. Each is a separate INIT activity with its own permissions; the bank one lands further along because the bank already did CKYC. - Lead shows the file grouped in the order it was worked, ending with what the sourcing agent earns, plus the activities this state allows and who normally performs each. The "who normally performs this" label is presentational only. Nothing here enforces anything — the workflow refuses server-side and the form reports what it said, including a note when a 403 is the platform declining rather than the console misbehaving. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
114 lines
4.5 KiB
JavaScript
114 lines
4.5 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { useZino } from '../api/provider.jsx'
|
|
import './ActivityForm.css'
|
|
|
|
/**
|
|
* Renders whatever /view/form-screens returns for an activity — labels, types,
|
|
* select options and which fields are mandatory — and submits it straight back.
|
|
*
|
|
* Nothing about this form is defined in the frontend. Add a field to the
|
|
* activity in Studio, redeploy, and it appears here with no code change. That
|
|
* is the point: the workflow is the source of truth, and a hardcoded form would
|
|
* quietly drift from it.
|
|
*/
|
|
export default function ActivityForm({ activityUid, instanceId, onDone, onCancel }) {
|
|
const { client } = useZino()
|
|
const [schema, setSchema] = useState(null)
|
|
const [values, setValues] = useState({})
|
|
const [error, setError] = useState(null)
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
useEffect(() => {
|
|
let dead = false
|
|
setSchema(null); setError(null); setValues({})
|
|
client.formSchema(activityUid, instanceId)
|
|
.then((s) => { if (!dead) setSchema(s) })
|
|
.catch((e) => { if (!dead) setError(e) })
|
|
return () => { dead = true }
|
|
}, [client, activityUid, instanceId])
|
|
|
|
function set(id, v) { setValues((p) => ({ ...p, [id]: v })) }
|
|
|
|
async function submit(e) {
|
|
e.preventDefault()
|
|
setBusy(true); setError(null)
|
|
// Send only fields the activity defines. A submission is schema-validated
|
|
// and an unknown field is fatal, so empties are dropped rather than sent.
|
|
const payload = {}
|
|
for (const f of schema.fields) {
|
|
const v = values[f.id]
|
|
if (v === undefined || v === '' || (Array.isArray(v) && v.length === 0)) continue
|
|
payload[f.id] = f.data_type === 'number' ? Number(v) : v
|
|
}
|
|
try {
|
|
const res = instanceId
|
|
? await client.activity(instanceId, activityUid, payload)
|
|
: await client.start(activityUid, payload)
|
|
onDone?.(res)
|
|
} catch (err) {
|
|
setError(err)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
if (error && !schema) return <div className="af__err">Could not load the form — {error.status} {error.message}</div>
|
|
if (!schema) return <p className="af__loading">Loading the form…</p>
|
|
|
|
return (
|
|
<form className="af" onSubmit={submit}>
|
|
<div className="af__grid">
|
|
{schema.fields.map((f) => {
|
|
const opts = f.properties?.options || []
|
|
const v = values[f.id] ?? (f.data_type === 'multiselect' ? [] : '')
|
|
return (
|
|
<label key={f.uid} className={'af__field' + (f.data_type === 'longtext' ? ' af__field--wide' : '')}>
|
|
<span>{f.name}{f.mandatory ? <em className="af__req">required</em> : null}</span>
|
|
|
|
{f.data_type === 'select' ? (
|
|
<select value={v} onChange={(e) => set(f.id, e.target.value)}>
|
|
<option value="">—</option>
|
|
{opts.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
</select>
|
|
) : f.data_type === 'multiselect' ? (
|
|
<div className="af__multi">
|
|
{opts.map((o) => (
|
|
<button
|
|
key={o.value} type="button"
|
|
className={'af__chip' + (v.includes(o.value) ? ' is-on' : '')}
|
|
onClick={() => set(f.id, v.includes(o.value) ? v.filter((x) => x !== o.value) : [...v, o.value])}
|
|
>{o.label}</button>
|
|
))}
|
|
</div>
|
|
) : f.data_type === 'longtext' ? (
|
|
<textarea rows={3} value={v} onChange={(e) => set(f.id, e.target.value)} />
|
|
) : (
|
|
<input
|
|
type={f.data_type === 'number' ? 'number' : f.data_type === 'date' ? 'date' : 'text'}
|
|
value={v} onChange={(e) => set(f.id, e.target.value)}
|
|
/>
|
|
)}
|
|
</label>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{error ? (
|
|
<div className="af__err">
|
|
<strong>{error.status}</strong> {error.message}
|
|
{error.status === 403 || String(error.message).includes('permission denied') ? (
|
|
<p>This is the workflow refusing, not the console. The signed-in role does not hold this activity.</p>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="af__actions">
|
|
{onCancel ? <button type="button" className="af__ghost" onClick={onCancel}>Cancel</button> : null}
|
|
<button type="submit" className="af__submit" disabled={busy}>
|
|
{busy ? 'Submitting…' : 'Submit'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|