fix: show a notice when the form screen fails to load

This commit is contained in:
Bhanu Prakash Sai Potteri 2026-09-04 12:28:44 +05:30
parent fe6c257ace
commit b862f8215c
2 changed files with 32 additions and 7 deletions

View File

@ -25,7 +25,8 @@ async function parseResponse<T>(res: Response): Promise<T> {
if (!res.ok) {
let msg = res.statusText;
try { const e = await res.json(); if (e.error) msg = e.error; } catch {}
throw new Error(msg);
// Status rides along so callers can special-case it (403 -> no permission).
throw Object.assign(new Error(msg), { status: res.status });
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;

View File

@ -68,7 +68,7 @@ export default function FormModal({ activityId, instanceId, title, onClose, onSu
const [succeeded, setSucceeded] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data, isPending } = useQuery({
const { data, isPending, error: loadError } = useQuery({
queryKey: ["form-screen", activityId, instanceId],
queryFn: () => getFormScreen(activityId, instanceId) as Promise<any>,
});
@ -77,6 +77,15 @@ export default function FormModal({ activityId, instanceId, title, onClose, onSu
const gridConfig: GridItem[] = data?.grid_config ?? [];
const formTitle = data?.activity_name || title || "Form";
// A failed form-screen fetch used to fall through to an empty <form> with a
// live Submit button. 403 is the common one — the activity's RBAC gate — and
// its backend message ("not permitted") means nothing to the person reading it.
const loadErrorMessage = loadError
? (loadError as { status?: number }).status === 403
? "You don't have permission to open this form. Ask an admin for access to this activity."
: loadError.message || "This form could not be loaded."
: null;
// /upload and /ocr-extract resolve the field's deployed config and RBAC from
// these identifiers. The form screen echoes them; WORKFLOW_ID is the fallback
// for older view-service builds that don't.
@ -234,6 +243,17 @@ export default function FormModal({ activityId, instanceId, title, onClose, onSu
</div>
))}
</div>
) : loadErrorMessage ? (
<div
role="alert"
className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2.5 text-destructive"
>
<AlertCircle className="mt-0.5 size-4 shrink-0" aria-hidden />
<div>
<p className="text-sm font-medium">Form unavailable</p>
<p className="mt-0.5 text-sm">{loadErrorMessage}</p>
</div>
</div>
) : (
<form id="activity-form" onSubmit={handleSubmit} className="space-y-4">
{rows.map((row, ri) => (
@ -304,13 +324,16 @@ export default function FormModal({ activityId, instanceId, title, onClose, onSu
assume the content keeps its own padding — this dialog is p-0. */}
{!isPending && (
<DialogFooter className="mx-0 mb-0 items-center border-t border-border px-5 py-3 sm:justify-between">
<p className="text-xs text-muted-foreground">
<span className="text-destructive">*</span> Required
</p>
<div className="flex gap-2">
{!loadErrorMessage && (
<p className="text-xs text-muted-foreground">
<span className="text-destructive">*</span> Required
</p>
)}
<div className="flex gap-2 sm:ml-auto">
<Button type="button" variant="ghost" onClick={onClose} disabled={submitting}>
Cancel
{loadErrorMessage ? "Close" : "Cancel"}
</Button>
{!loadErrorMessage && (
<Button type="submit" form="activity-form" disabled={submitting || succeeded}>
{submitting ? (
<>
@ -329,6 +352,7 @@ export default function FormModal({ activityId, instanceId, title, onClose, onSu
</>
)}
</Button>
)}
</div>
</DialogFooter>
)}