Skip to content

Commit 1ba4c33

Browse files
Collection-mode frontend review fixes: customs actually post, server-truth gating
pr-review findings: - The full form rendered custom questions but the submit allowlist stripped their answers before POST: optional customs were silent data loss, and a REQUIRED custom made the form unsubmittable (422 naming a field the user visibly filled). Custom keys join the allowlist. - The modal re-checks the fetched config's collection_mode (server truth) and self-heals stale draft records: a portal flipped to an auto mode mid-draft had already auto-finalized the submission, so prompting asked consent for something published and 409'd after burning the captcha. - Custom inputs in the modal use Radix TextField (visual/blur parity), carry ids for their labels, and cap at the backend lengths; the required-only scope of the abbreviated form is documented as a decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 964e0d3 commit 1ba4c33

2 files changed

Lines changed: 39 additions & 8 deletions

File tree

app/src/app/components/Forms/SubmissionForm.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,10 @@ export const SubmissionForm: React.FC<SubmissionFormProps> = ({
185185
const emailConfirmed =
186186
!requireEmailConfirm || (shown.includes('email') && emailConfirm === emailValue);
187187
if (captchaToken && formIsValid && emailConfirmed) {
188-
submitForm(portalId, shown);
188+
// Custom keys must be in the allowlist too, or the store filter
189+
// strips their answers before POST (silent loss for optional
190+
// customs; an unrecoverable 422 for required ones).
191+
submitForm(portalId, [...shown, ...(customFields ?? []).map(c => c.key)]);
189192
}
190193
}}
191194
ref={formRef}

app/src/app/components/MapPage/SubmitToPortalModal.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
'use client';
22
import {useEffect, useMemo, useState} from 'react';
3-
import {Blockquote, Button, Checkbox, Dialog, Flex, Text, TextArea} from '@radix-ui/themes';
3+
import {
4+
Blockquote,
5+
Button,
6+
Checkbox,
7+
Dialog,
8+
Flex,
9+
Text,
10+
TextArea,
11+
TextField,
12+
} from '@radix-ui/themes';
413
import {useMapStore} from '@/app/store/mapStore';
514
import {useFormState} from '@/app/store/formState';
615
import {useDraftSubmissionStore} from '@/app/store/draftSubmissionStore';
@@ -48,8 +57,16 @@ export const SubmitToPortalModal: React.FC = () => {
4857
setError('');
4958
if (!draft) return;
5059
getFormConfig(draft.portalId).then(response => {
51-
if (response.ok) setConfig(response.response);
52-
else setError('Could not load the portal form. Please try again later.');
60+
if (response.ok) {
61+
setConfig(response.response);
62+
// Self-heal stale/legacy records: the stored mode is a snapshot
63+
// from draft creation; the config is the server truth.
64+
if (draft.collectionMode !== response.response.collection_mode) {
65+
updateDraftSubmission(promptDocumentId!, {
66+
collectionMode: response.response.collection_mode,
67+
});
68+
}
69+
} else setError('Could not load the portal form. Please try again later.');
5370
});
5471
// eslint-disable-next-line react-hooks/exhaustive-deps
5572
}, [draft?.portalId, promptDocumentId]);
@@ -60,6 +77,12 @@ export const SubmitToPortalModal: React.FC = () => {
6077
if (!promptDocumentId || promptDocumentId !== currentDocumentId || !draft || draft.submitted) {
6178
return null;
6279
}
80+
// The config is server truth: a portal flipped to an auto mode after this
81+
// draft was created has ALREADY auto-finalized the submission — prompting
82+
// would ask consent for something published and 409 (burning a captcha).
83+
if (config && config.collection_mode !== 'prompt') {
84+
return null;
85+
}
6386

6487
const requiredFields = FIELD_ORDER.filter(name => config?.required_fields?.includes(name));
6588
const needsEmailConfirm = !!config?.require_email_confirm && requiredFields.includes('email');
@@ -160,23 +183,28 @@ export const SubmitToPortalModal: React.FC = () => {
160183
</Flex>
161184
);
162185
})}
186+
{/* Abbreviated by design: like the registry fields above, only
187+
REQUIRED custom questions are asked here — optional ones are
188+
full-form-only (SubmissionForm). */}
163189
{requiredCustoms.map(custom => (
164190
<Flex key={custom.key} direction="column" gap="1">
165-
<Text as="label" size="2" weight="medium">
191+
<Text as="label" size="2" weight="medium" htmlFor={custom.key}>
166192
{custom.label} *
167193
</Text>
168194
{custom.field_type === 'textarea' ? (
169195
<TextArea
196+
id={custom.key}
170197
value={values[custom.key] ?? ''}
171198
placeholder={custom.label}
199+
maxLength={5000}
172200
onChange={e => setValues(v => ({...v, [custom.key]: e.target.value}))}
173201
/>
174202
) : (
175-
<input
176-
className="rt-TextFieldInput rt-r-size-2 border border-slate-300 rounded p-2"
177-
type="text"
203+
<TextField.Root
204+
id={custom.key}
178205
value={values[custom.key] ?? ''}
179206
placeholder={custom.label}
207+
maxLength={255}
180208
onChange={e => setValues(v => ({...v, [custom.key]: e.target.value}))}
181209
/>
182210
)}

0 commit comments

Comments
 (0)