From b862f8215cc6444d229055992723a017b08e4c67 Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Fri, 4 Sep 2026 12:28:44 +0530 Subject: [PATCH] fix: show a notice when the form screen fails to load --- src/api/viewService.ts | 3 ++- src/components/FormModal.tsx | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/api/viewService.ts b/src/api/viewService.ts index 098624c..fcae6f0 100644 --- a/src/api/viewService.ts +++ b/src/api/viewService.ts @@ -25,7 +25,8 @@ async function parseResponse(res: Response): Promise { 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; diff --git a/src/components/FormModal.tsx b/src/components/FormModal.tsx index fbc2647..9a7bc17 100644 --- a/src/components/FormModal.tsx +++ b/src/components/FormModal.tsx @@ -68,7 +68,7 @@ export default function FormModal({ activityId, instanceId, title, onClose, onSu const [succeeded, setSucceeded] = useState(false); const [error, setError] = useState(null); - const { data, isPending } = useQuery({ + const { data, isPending, error: loadError } = useQuery({ queryKey: ["form-screen", activityId, instanceId], queryFn: () => getFormScreen(activityId, instanceId) as Promise, }); @@ -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
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 ))} + ) : loadErrorMessage ? ( +
+ +
+

Form unavailable

+

{loadErrorMessage}

+
+
) : ( {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 && ( -

- * Required -

-
+ {!loadErrorMessage && ( +

+ * Required +

+ )} +
+ {!loadErrorMessage && ( + )}
)}