Skip to content

Lucas/forms fill out 299 - #309

Open
lkang172 wants to merge 7 commits into
mainfrom
lucas/forms-fill-out-299
Open

lkang172 wants to merge 7 commits into
mainfrom
lucas/forms-fill-out-299

Conversation

@lkang172

@lkang172 lkang172 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #299

Adds the form fill-out page, form submissions, and headshot uploads.

What's new:

  • lib/forms/submission.ts: buildSubmissionSchema(definition) builds a Zod schema from a form's questions. It's a pure function, so the page and the submit route both use it.
  • api/formSubmissions.ts: adds POST /forms/:formId/submissions and POST /forms/:formId/uploads, backed by FormSubmissionService. Both routes:
    • get the org from the host
    • return 404 when FormsEnabled is off
    • only accept published forms inside their open/close window
    • require membership when the form requires login
  • Headshot uploads are JPEG, PNG, or WebP up to 5 MB. The server checks the file's bytes against its type. Files are saved through IFileService without a media row, so they don't appear in the media library.
  • components/forms/: DynamicForm shows each question through a registry with one renderer per question type. Answers are checked on blur and on submit, focus jumps to the first invalid question, and help text and errors are linked to their fields with aria-describedby.
  • app/forms/[formId]: the page has a brand panel and no navbar, and handles these states: success, closed, not yet open, already submitted, not found. Admins can use ?preview=1. Logged-out visitors go to login and come back to the form.

Considerations

  • JunoFileService.upload used to throw. It now uploads from the server through a presigned URL, so the file we store is the one the server checked. This also makes profile picture uploads work under Juno.
  • BogTextInput has no number type, so number questions use a text input with a numeric keypad. A wrapper adds the id and ARIA attributes, since BogTextInput can't take them. components/bog/ is unchanged.
  • A form with status closed shows a "closed" message instead of a 404. Drafts return 404.
  • /forms/ in NO_NAVBAR_PAGES hides the navbar on every page under /forms/, including any admin pages [Forms] Build the question builder for the application question types #298 adds there.

Testing

  • Unit tests for the schema, the service, both routes (including a cross-org test), every renderer, and preview mode.
  • E2E tests for submitting the Visionaries application with a headshot, the closed state, the login redirect, and org A's form returning 404 on org B's subdomain.
  • One existing test, POST /api/media "creates media record…", also fails on main without this branch.

Checklist:

  • I have mentioned a ticket above
  • My changes meet the acceptance criteria of said ticket
  • I have self-reviewed my changes
  • I have requested a review from the appropriate team member(s)
  • I have fixed all merge conflicts
  • I have ensured CI checks pass

lkang172 and others added 4 commits October 6, 2026 11:35
Builds a Zod schema from a FormDefinition that checks required answers,
each type's value shape, short-text length, long-text word counts,
number ranges and whole numbers, email, and US phone numbers (stored as
digits). It rejects unknown question IDs and drops section headers. Also
adds getFormAvailability for the open/close window and the headshot
upload limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FormSubmissionService reads a form's definition for an org and writes a
submission with its answers in one transaction, after checking for a
duplicate and that each image answer's upload is this submitter's, for
this form and org.

The formSubmissions sub-app adds POST /forms/:formId/submissions and
POST /forms/:formId/uploads. Both resolve the org from the host and
return 404 when forms are off, accept only published forms inside their
window, and require membership when the form requires login. Uploads
take JPEG, PNG, or WebP up to 5 MB, check the file's bytes against its
type, and are stored through IFileService without a media row.

JunoFileService.upload now uploads through a presigned URL instead of
throwing, so FileService.upload works for both implementations, and
IFileService.upload takes only the file's location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DynamicForm renders a FormDefinition in order through FORM_RENDERERS,
typed per FormComponentType. Text, email, phone, and number questions
use BogTextInput, long text gets a textarea with a live word count, and
image questions upload on select with progress, replace, and remove.

Answers are checked with buildSubmissionSchema on blur and on submit.
Errors and help text are linked with aria-describedby, focus moves to
the first invalid question, and a submission can't be sent twice or
while an image is uploading. Preview mode hides the submit button and
turns off uploads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app/forms/[formId] resolves the org from the host and returns 404 when
forms are off, the form is a draft, or it's another org's. Forms that
require login send logged-out visitors to login and back, which the
login and signup pages now support with a same-site redirect param.

The page shows the org's logo on a brand panel beside the form, with no
navbar, and submits through the typed RPC client. It covers the success,
closed, not-yet-open, already-submitted, and not-found states, and
?preview=1 lets org admins view a draft or published form without
submitting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@netlify

netlify Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for servicestart ready!

Name Link
🔨 Latest commit bd09f10
🔍 Latest deploy log https://app.netlify.com/projects/servicestart/deploys/6ac6ae585569990008c63d1f
😎 Deploy Preview https://deploy-preview-309--servicestart.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

<div className="flex size-32 shrink-0 items-center justify-center overflow-hidden rounded-full bg-grey-fill-weak text-grey-icon-strong">
{previewUrl ? (
<img
src={previewUrl}
@lkang172
lkang172 marked this pull request as ready for review October 7, 2026 19:49
@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Disclaimer: This comment was AI-generated and is not necessarily completely accurate. Please take code comments with a grain of salt.

RetriggerConfidence Score: 2/5

[High risk] Adds form submission API and fills out the form-filling feature.

Fix the answer overwrite and external redirect before merging.

Findings

  1. P1 Uploads erase newer answers ▶
  2. P1 Security Login links leave the site ▶
  3. P2 Unused images stay in storage ▶

Summary

Adds form pages, shared answer checks, response storage, headshot uploads, admin previews, and return-to-form links after login. Juno now supports server-side uploads.

  • Fix answer loss when an upload finishes.
  • Close the external redirect path in login and signup links.
  • Add cleanup for failed and unused image uploads.

Checklist areas needing improvement: frontend state updates; backend redirect checks and upload cleanup.
Review score: 70/100. Lint and runtime tests were not run.

Acknowledged choices from lkang172: hiding navigation on all /forms/ pages, including future admin pages; using text inputs for numbers because BogTextInput lacks a number type; and showing a closed message rather than a 404 for closed forms.

Reviews (1) · Last reviewed commit: "Add the form fill-out page" · Reviewed by Greptile

Comment thread components/forms/DynamicForm.tsx Outdated
Comment on lines +102 to +105
const next = { ...values };
if (value === undefined) delete next[id];
else next[id] = value;
setValues(next);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Uploads erase newer answers

handleChange replaces all answers using the values captured when an upload started. If someone edits another answer while their image uploads, ImageQuestion later calls that old callback and erases those edits. Two image uploads can also overwrite each other.

Merge each change into the latest state with a functional setValues update. Add a test that edits another field during an upload.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b0d8084: changes now merge into the latest answers, with tests for an edit during an upload and two uploads finishing out of order

Comment thread lib/utils.ts Outdated
Comment on lines +47 to +48
const path = new URLSearchParams(search).get("redirect");
return path && /^\/(?![/\\])/.test(path) ? path : "/";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Login links leave the site

getRedirectPath accepts ?redirect=%2F%09%2Fevil.com. The decoded tab passes this regex, but URL parsing removes it and turns the path into //evil.com. The login and signup pages pass this value directly to router.replace for already signed-in users. A crafted link can therefore send them to another site without going through the server’s login checks.

Reject control characters and check the parsed URL’s origin before returning a local path.

How this was verified: The decoded redirect passes the regex and reaches the signed-in navigation calls; removing the tab during URL parsing makes it an external URL.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b2d9e63: getRedirectPath now parses the path and only returns it if it stays on this site, with tests for the tab, newline and carriage-return cases

Comment thread api/formSubmissions.ts
Comment on lines +250 to +257
const id = await FormSubmissionService.createUpload({
organizationId,
formId: form.id,
uploadedBy: userId,
fileName,
contentType,
sizeBytes: file.size,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused images stay in storage

The upload route saves the image before createUpload records it, with no recovery if that insert fails. The headshot then has no database record, and retrying saves another copy. Replacing or removing an image also leaves its old upload behind.

Add a tracked cleanup path for failed and unused uploads, with a storage strategy that also works under Juno. This is a non-blocking improvement that avoids paying to store images no response uses.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd09f10: the upload row is now created before the file is stored and deleted if storing fails, so every stored file has a row. Cleanup of replaced or never-submitted uploads is a follow-up, as #299 says

An upload finishing called handleChange with the answers from when it
started, so edits made during the upload were lost, and two uploads
could overwrite each other. Changes now merge into the latest answers.
getRedirectPath accepted "/\t/evil.com": it passed the regex, but URL
parsing drops the tab and leaves "//evil.com". It now parses the path
and only returns it when it stays on this site.
The file used to be stored first, so a failed insert left it in storage
with no row to find it by. The row now goes in first and is deleted if
storing fails.
@lkang172
lkang172 requested a review from shaply October 7, 2026 20:47

This branch was successfully deployed

1 active deployment
Preview — bd09f10b Deployed Oct 7, 2026 by lkang172 via migrate-db #772
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Forms] Build the fill-out page, submissions, and headshot upload

2 participants