@@ -216,6 +216,7 @@ The form builder provides a drag-and-drop interface with:
216216| Date Picker | Date selection | ` type: "string", format: "date-time" ` |
217217| Phone | Phone number input | ` type: "string", fieldType: "phone" ` |
218218| URL | Website URL input | ` type: "string", format: "uri" ` |
219+ | File Upload | Standalone upload using your callback | ` type: "string", fieldType: "file" ` |
219220
220221### Field Properties
221222
@@ -231,6 +232,73 @@ Each field can be configured with:
231232| Min/Max | Minimum and maximum values (numbers) or length (strings) |
232233| Options | For select, radio, and checkbox groups |
233234| Default Value | Pre-filled value for the field |
235+ | Visibility | One ` showIf ` condition against another field |
236+
237+ ### Conditional fields and steps
238+
239+ In a field's edit dialog, use ** Visibility** to choose a source field, operator,
240+ and comparison value. For a multi-step form, use the active step's visibility
241+ editor below its tabs. Step conditions can use fields from earlier steps.
242+ Choose ** Always show** to remove a condition.
243+
244+ ` showIf ` is stored on a property or step as ` { field, operator, value } ` .
245+ ` field ` names a sibling field (a top-level field for step rules).
246+
247+ | Operator | Comparison |
248+ | ----------| ------------|
249+ | ` equals ` | Strict equality; ` 1 ` differs from ` "1" ` |
250+ | ` notEquals ` | Strict inequality |
251+ | ` in ` | Source value is an element of the ` value ` array |
252+ | ` isTruthy ` | JavaScript truthiness; no comparison value is needed |
253+
254+ The editor accepts text, numbers, and booleans. Quote a number or boolean to
255+ compare it as text. For ` in ` , enter one comparison value per line.
256+
257+ ``` ts
258+ import { z } from " zod"
259+ import { zodToFormSchema } from " @btst/plugin-form-builder/client"
260+
261+ const schema = z .object ({
262+ business: z .boolean (),
263+ company: z .string ().min (1 ).meta ({
264+ showIf: { field: " business" , operator: " isTruthy" },
265+ }),
266+ })
267+ const jsonSchema = zodToFormSchema (schema )
268+ ```
269+
270+ Both ` FormRenderer ` and ` SteppedAutoForm ` hide non-matching fields and skip hidden
271+ steps. The shared ` formSchemaToZod ` validator bypasses their required constraints
272+ and removes stale values, including nested object and array-item fields, before
273+ persistence. Visible fields retain their normal validation. Submission hooks
274+ receive validated data; any replacement data is validated again before storage.
275+
276+ ### Standalone file uploads
277+
278+ Drag ** File Upload** from the palette. Configure an upload callback on
279+ ` FormRenderer ` , or set ` overrides.formBuilder.onUpload ` on ` StackProvider ` :
280+
281+ ``` tsx
282+ <FormRenderer
283+ slug = " application"
284+ onUpload = { async (file ) => {
285+ const body = new FormData ()
286+ body .append (" file" , file )
287+ const response = await fetch (" /api/files" , { method: " POST" , body })
288+ if (! response .ok ) throw new Error (" Upload failed" )
289+ const result = await response .json ()
290+ return result .url
291+ }}
292+ />
293+ ```
294+
295+ The callback receives a ` File ` and returns a non-empty URL string, including
296+ relative URLs. Only that string is submitted. Your endpoint owns file validation,
297+ storage, access control, and cleanup; the Media plugin is optional. The renderer
298+ disables submission during upload and displays upload failures with a retry path.
299+ Without a callback, the file control is disabled with a configuration message.
300+ For standalone AutoForm, use ` fieldConfig.attachment.onUpload ` . Builder previews
301+ can use ` fieldComponents.file ` to provide an uploader.
234302
235303## Public Form Rendering
236304
@@ -276,6 +344,7 @@ export default function FormDemoPage({ params }: { params: { slug: string } }) {
276344| ` slug ` | ` string ` | Form slug to fetch and render |
277345| ` onSuccess ` | ` (submission) => void ` | Callback after successful submission (submission.form has success info) |
278346| ` onError ` | ` (error) => void ` | Callback when submission fails |
347+ | ` onUpload ` | ` (file: File) => Promise<string> ` | Standalone file uploader; overrides the provider callback |
279348| ` LoadingComponent ` | ` ComponentType ` | Custom loading state |
280349| ` ErrorComponent ` | ` ComponentType<{ error: Error }> ` | Custom error state |
281350| ` submitButtonText ` | ` string ` | Custom submit button text |
@@ -306,6 +375,7 @@ Access form data in your frontend using the provided hooks:
306375| ` useSubmissions(formId) ` | List non-sensitive submission metadata and authoritative form facts | ` { form, submissions, total, isLoading } ` |
307376| ` useSubmission(formId, submissionId) ` | Get one record-authorized submission with its data, IP address, and user agent | ` { submission, isLoading, error } ` |
308377| ` useDeleteSubmission(formId) ` | Delete submission | React Query mutation |
378+ | ` useExportSubmissions() ` | Export all submissions for a form ID | Mutation returning ` { csv, filename } ` |
309379
310380` useSubmissions ` returns ` form ` as the intentionally minimal
311381` SubmissionListFormContext ` : ` { id, name, createdBy? } ` . Submission access does
@@ -387,6 +457,10 @@ export const authorization = defineAuthorization({
387457 identity ?.role === " admin" || identity ?.id === facts .ownerId ,
388458 ),
389459 forms .submission .create .allow (),
460+ forms .submission .export .when (({ identity , facts }) =>
461+ identity !== null &&
462+ (identity .role === " admin" || identity .id === facts .ownerId ),
463+ ),
390464 forms .submission .read .when (({ identity , facts }) =>
391465 identity ?.role === " admin" || identity ?.id === facts .ownerId ,
392466 ),
@@ -468,6 +542,30 @@ transactional database adapter. The CLI supports Form Builder with Prisma,
468542Drizzle, and Kysely and rejects its current memory and MongoDB scaffold
469543configurations.
470544
545+ ## CSV export
546+
547+ The submissions page offers ** Export CSV** when ` submission.export ` is granted.
548+ This is a separate permission for all payloads of the selected form; permission
549+ to read one submission or collection metadata does not grant export access.
550+ Facts include the server-resolved ` formId ` , ` formExists ` , and ` ownerId ` .
551+
552+ The operation exports every submission, independent of admin pagination. Columns
553+ contain ` id ` , ` submittedAt ` , and ` data.<field> ` values. Deleted fields remain in
554+ the export when older submissions contain them. Nested values are JSON strings;
555+ cells are quoted and formula-like values are prefixed with an apostrophe for
556+ spreadsheet safety. Source reads are paged and CSV rows are serialized without
557+ building a full cell matrix. An export supports up to 10,000 submissions,
558+ 1,000 distinct data columns, one million data cells, and 10 MiB each of stored
559+ JSON and CSV output. Larger exports fail with HTTP 413 and
560+ ` FORM_EXPORT_TOO_LARGE ` ; they require an application-managed streaming or
561+ offline export. No partial CSV is downloaded when a limit is exceeded.
562+
563+ ``` ts
564+ const result = await stack .forRequest (request ).operations .formBuilder
565+ .exportSubmissions ({ formId })
566+ // { csv: string, filename: string }
567+ ```
568+
471569## Backend Hooks
472570
473571Authorization runs before lifecycle hooks. Use hooks for domain behavior such
@@ -516,6 +614,7 @@ The Form Builder plugin exposes these REST endpoints:
516614| ` /form-builder/forms/:id ` | DELETE | Delete a form |
517615| ` /form-builder/forms/:slug/submit ` | POST | Submit form data |
518616| ` /form-builder/forms/:formId/submissions ` | GET | List non-sensitive submission metadata for a form |
617+ | ` /form-builder/forms/:formId/submissions/export ` | POST | Export all submissions as ` { csv, filename } ` ; requires ` submission.export ` |
519618| ` /form-builder/forms/:formId/submissions/:subId ` | GET | Get a record-authorized submission and its contents |
520619| ` /form-builder/forms/:formId/submissions/:subId ` | DELETE | Delete a submission |
521620
@@ -555,20 +654,17 @@ The Form Builder supports multi-step forms through JSON Schema's `allOf` structu
555654``` json
556655{
557656 "type" : " object" ,
558- "allOf" : [
559- {
560- "title" : " Step 1: Personal Info" ,
561- "properties" : {
562- "name" : { "type" : " string" , "label" : " Full Name" },
563- "email" : { "type" : " string" , "format" : " email" }
564- }
565- },
657+ "properties" : {
658+ "business" : { "type" : " boolean" , "stepGroup" : 0 },
659+ "company" : { "type" : " string" , "stepGroup" : 1 }
660+ },
661+ "required" : [" business" , " company" ],
662+ "steps" : [
663+ { "id" : " personal" , "title" : " Personal Info" },
566664 {
567- "title" : " Step 2: Details" ,
568- "properties" : {
569- "company" : { "type" : " string" },
570- "message" : { "type" : " string" , "fieldType" : " textarea" }
571- }
665+ "id" : " company" ,
666+ "title" : " Company Details" ,
667+ "showIf" : { "field" : " business" , "operator" : " isTruthy" }
572668 }
573669 ]
574670}
0 commit comments