this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: customize post-submission completion state

+379 -29
+57 -1
components/form-builder.tsx
··· 63 63 id: string; 64 64 title: string; 65 65 description: string; 66 + completionTitle: string; 67 + completionMessage: string; 68 + completionLinkLabel: string | null; 69 + completionLinkUrl: string | null; 66 70 slug: string; 67 71 status: "DRAFT" | "PUBLISHED"; 68 72 updatedAt: string; ··· 245 249 const [metadataDraft, setMetadataDraft] = useState({ 246 250 title: initialForm.title, 247 251 description: initialForm.description, 252 + completionTitle: initialForm.completionTitle, 253 + completionMessage: initialForm.completionMessage, 254 + completionLinkLabel: initialForm.completionLinkLabel ?? "", 255 + completionLinkUrl: initialForm.completionLinkUrl ?? "", 248 256 slug: initialForm.slug, 249 257 }); 250 258 ··· 262 270 setMetadataDraft({ 263 271 title: form.title, 264 272 description: form.description, 273 + completionTitle: form.completionTitle, 274 + completionMessage: form.completionMessage, 275 + completionLinkLabel: form.completionLinkLabel ?? "", 276 + completionLinkUrl: form.completionLinkUrl ?? "", 265 277 slug: form.slug, 266 278 }); 267 - }, [form.title, form.description, form.slug]); 279 + }, [form.title, form.description, form.completionTitle, form.completionMessage, form.completionLinkLabel, form.completionLinkUrl, form.slug]); 268 280 269 281 useEffect(() => { 270 282 setBlockDraft(selectedBlock); ··· 555 567 <span className="font-medium text-[var(--ink)]">Share URL slug</span> 556 568 <Input value={metadataDraft.slug} onChange={(event) => setMetadataDraft((current) => ({ ...current, slug: event.target.value }))} /> 557 569 </label> 570 + </div> 571 + 572 + <div className="space-y-5 border-t border-black/8 pt-6"> 573 + <div> 574 + <p className="text-xs font-semibold uppercase tracking-[0.22em] text-[var(--accent)]">After submission</p> 575 + <p className="mt-2 text-sm leading-6 text-[var(--muted)]"> 576 + Customize what respondents see after they finish the form, including an optional next-step link. 577 + </p> 578 + </div> 579 + 580 + <div className="grid gap-5"> 581 + <label className="space-y-2 text-sm text-[var(--muted)]"> 582 + <span className="font-medium text-[var(--ink)]">Completion title</span> 583 + <Input 584 + value={metadataDraft.completionTitle} 585 + onChange={(event) => setMetadataDraft((current) => ({ ...current, completionTitle: event.target.value }))} 586 + /> 587 + </label> 588 + <label className="space-y-2 text-sm text-[var(--muted)]"> 589 + <span className="font-medium text-[var(--ink)]">Completion message</span> 590 + <Textarea 591 + value={metadataDraft.completionMessage} 592 + onChange={(event) => setMetadataDraft((current) => ({ ...current, completionMessage: event.target.value }))} 593 + /> 594 + </label> 595 + <div className="grid gap-5 lg:grid-cols-2"> 596 + <label className="space-y-2 text-sm text-[var(--muted)]"> 597 + <span className="font-medium text-[var(--ink)]">Follow-up link label</span> 598 + <Input 599 + value={metadataDraft.completionLinkLabel} 600 + placeholder="Read next steps" 601 + onChange={(event) => setMetadataDraft((current) => ({ ...current, completionLinkLabel: event.target.value }))} 602 + /> 603 + </label> 604 + <label className="space-y-2 text-sm text-[var(--muted)]"> 605 + <span className="font-medium text-[var(--ink)]">Follow-up link URL</span> 606 + <Input 607 + value={metadataDraft.completionLinkUrl} 608 + placeholder="https://example.com/next" 609 + onChange={(event) => setMetadataDraft((current) => ({ ...current, completionLinkUrl: event.target.value }))} 610 + /> 611 + </label> 612 + </div> 613 + </div> 558 614 </div> 559 615 560 616 <div className="grid gap-4 rounded-[20px] border border-black/8 bg-[var(--bg-strong)] p-5 lg:grid-cols-[1fr_auto_auto] lg:items-center">
+25 -5
components/public-form-runner.tsx
··· 2 2 3 3 import { AnimatePresence, motion } from "framer-motion"; 4 4 import { ArrowLeft, ArrowRight, Check, LoaderCircle } from "lucide-react"; 5 + import Link from "next/link"; 5 6 import { useMemo, useState } from "react"; 6 7 7 8 import { Button } from "@/components/ui/button"; ··· 29 30 id: string; 30 31 title: string; 31 32 description: string; 33 + completionTitle: string; 34 + completionMessage: string; 35 + completionLinkLabel: string | null; 36 + completionLinkUrl: string | null; 32 37 slug: string; 33 38 blocks: PublicBlock[]; 34 39 }; ··· 60 65 61 66 const currentBlock = form.blocks[step]; 62 67 const progress = useMemo(() => ((step + 1) / form.blocks.length) * 100, [form.blocks.length, step]); 68 + const completionTitle = form.completionTitle.trim() || "Thanks for taking the time."; 69 + const completionMessage = 70 + form.completionMessage.trim() || 71 + "Your response was submitted anonymously. The creator can review your answers, but they are not linked to a login or account."; 72 + const completionLinkLabel = form.completionLinkLabel?.trim() || null; 73 + const completionLinkUrl = form.completionLinkUrl?.trim() || null; 63 74 64 75 function setAnswer(blockId: string, value: string | string[]) { 65 76 setAnswers((current) => ({ ··· 142 153 return ( 143 154 <Card className="w-full max-w-3xl overflow-hidden bg-[#16120f] p-10 text-white shadow-[0_36px_120px_rgba(22,18,15,0.32)]"> 144 155 <p className="text-xs font-semibold uppercase tracking-[0.32em] text-[#b9dfb9]">Response received</p> 145 - <h2 className="mt-6 font-display text-5xl">Thanks for taking the time.</h2> 146 - <p className="mt-4 max-w-2xl text-base leading-8 text-white/72"> 147 - Your response was submitted anonymously. The creator can review your answers, but they are not linked to a 148 - login or account. 149 - </p> 156 + <h2 className="mt-6 font-display text-5xl">{completionTitle}</h2> 157 + <p className="mt-4 max-w-2xl text-base leading-8 text-white/72">{completionMessage}</p> 158 + {completionLinkLabel && completionLinkUrl ? ( 159 + <div className="mt-8"> 160 + <Link 161 + href={completionLinkUrl} 162 + target="_blank" 163 + rel="noreferrer" 164 + className="inline-flex items-center justify-center rounded-xl bg-[#9fd7a4] px-5 py-3 text-sm font-semibold !text-[#122012] transition hover:bg-[#b8e4bb]" 165 + > 166 + {completionLinkLabel} 167 + </Link> 168 + </div> 169 + ) : null} 150 170 </Card> 151 171 ); 152 172 }
+16
lib/forms.ts
··· 71 71 id: string; 72 72 title: string; 73 73 description: string; 74 + completionTitle: string; 75 + completionMessage: string; 76 + completionLinkLabel: string | null; 77 + completionLinkUrl: string | null; 74 78 slug: string; 75 79 status: FormStatus; 76 80 updatedAt: string; ··· 82 86 id: string; 83 87 title: string; 84 88 description: string; 89 + completionTitle: string; 90 + completionMessage: string; 91 + completionLinkLabel: string | null; 92 + completionLinkUrl: string | null; 85 93 slug: string; 86 94 blocks: SerializedBlock[]; 87 95 }; ··· 116 124 id: form.id, 117 125 title: form.title, 118 126 description: form.description, 127 + completionTitle: form.completionTitle, 128 + completionMessage: form.completionMessage, 129 + completionLinkLabel: form.completionLinkLabel, 130 + completionLinkUrl: form.completionLinkUrl, 119 131 slug: form.slug, 120 132 status: form.status, 121 133 updatedAt: form.updatedAt.toISOString(), ··· 129 141 id: form.id, 130 142 title: form.title, 131 143 description: form.description, 144 + completionTitle: form.completionTitle, 145 + completionMessage: form.completionMessage, 146 + completionLinkLabel: form.completionLinkLabel, 147 + completionLinkUrl: form.completionLinkUrl, 132 148 slug: form.slug, 133 149 blocks: form.blocks.map(serializeBlock), 134 150 };
+54 -10
lib/validators.ts
··· 1 1 import { FormBlockType } from "@prisma/client"; 2 2 import { z } from "zod"; 3 3 4 - export const formMetadataSchema = z.object({ 5 - title: z.string().trim().min(1).max(120), 6 - description: z.string().trim().max(600).default(""), 7 - slug: z 8 - .string() 9 - .trim() 10 - .min(2) 11 - .max(64) 12 - .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, numbers, and hyphens only."), 13 - }); 4 + export const formMetadataSchema = z 5 + .object({ 6 + title: z.string().trim().min(1).max(120), 7 + description: z.string().trim().max(600).default(""), 8 + completionTitle: z.string().trim().max(120).default(""), 9 + completionMessage: z.string().trim().max(600).default(""), 10 + completionLinkLabel: z.string().trim().max(80).default(""), 11 + completionLinkUrl: z.string().trim().max(2048).default(""), 12 + slug: z 13 + .string() 14 + .trim() 15 + .min(2) 16 + .max(64) 17 + .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, numbers, and hyphens only."), 18 + }) 19 + .superRefine((value, context) => { 20 + const hasLabel = Boolean(value.completionLinkLabel); 21 + const hasUrl = Boolean(value.completionLinkUrl); 22 + 23 + if (hasLabel !== hasUrl) { 24 + context.addIssue({ 25 + code: z.ZodIssueCode.custom, 26 + path: [hasLabel ? "completionLinkUrl" : "completionLinkLabel"], 27 + message: "Add both a follow-up link label and URL, or leave both empty.", 28 + }); 29 + } 30 + 31 + if (!hasUrl) { 32 + return; 33 + } 34 + 35 + try { 36 + const parsedUrl = new URL(value.completionLinkUrl); 37 + 38 + if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { 39 + context.addIssue({ 40 + code: z.ZodIssueCode.custom, 41 + path: ["completionLinkUrl"], 42 + message: "Use a full http:// or https:// URL for the follow-up link.", 43 + }); 44 + } 45 + } catch { 46 + context.addIssue({ 47 + code: z.ZodIssueCode.custom, 48 + path: ["completionLinkUrl"], 49 + message: "Use a valid follow-up link URL.", 50 + }); 51 + } 52 + }) 53 + .transform((value) => ({ 54 + ...value, 55 + completionLinkLabel: value.completionLinkLabel || null, 56 + completionLinkUrl: value.completionLinkUrl || null, 57 + })); 14 58 15 59 export const createBlockSchema = z.object({ 16 60 type: z.nativeEnum(FormBlockType),
+2
openspec/changes/archive/2026-04-08-customize-submission-message/.openspec.yaml
··· 1 + schema: spec-driven 2 + created: 2026-04-08
+93
openspec/changes/archive/2026-04-08-customize-submission-message/design.md
··· 1 + ## Context 2 + 3 + The public runner currently ends with a fixed completion state embedded in `components/public-form-runner.tsx`. Creators cannot tailor that final screen to explain what happens next or direct respondents to another destination. 4 + 5 + This change touches both creator-facing and respondent-facing flows. Form metadata already lives on the `Form` record and is edited through the builder settings panel, so the completion state should follow the same pattern rather than introducing a separate entity. 6 + 7 + ## Goals / Non-Goals 8 + 9 + **Goals:** 10 + - Let creators configure the completion text shown after a successful submission. 11 + - Let creators optionally provide a follow-up link that appears on the completion state. 12 + - Preserve a safe default completion state for older forms and forms with no custom content. 13 + - Keep the implementation aligned with the existing form metadata update flow. 14 + 15 + **Non-Goals:** 16 + - Rich text editing for completion content. 17 + - Per-response or conditional completion messages. 18 + - Multiple follow-up links or button styling customization. 19 + - Tracking follow-up link clicks. 20 + 21 + ## Decisions 22 + 23 + ### Store completion content directly on the `Form` model 24 + Add dedicated form-level fields for completion content instead of a JSON blob or separate table. This keeps the feature easy to validate, serialize, and expose through existing builder/public form payloads. 25 + 26 + Proposed fields: 27 + - `completionTitle`: short heading shown after submission 28 + - `completionMessage`: supporting copy shown below the heading 29 + - `completionLinkLabel`: optional CTA label 30 + - `completionLinkUrl`: optional CTA URL 31 + 32 + **Why this approach:** 33 + - Fits the existing form metadata model. 34 + - Avoids schema ambiguity and ad hoc JSON validation. 35 + - Makes default values and future migrations straightforward. 36 + 37 + **Alternatives considered:** 38 + - Store completion settings in a JSON column on `Form`: more flexible, but weaker typing and more validation complexity. 39 + - Hardcode only a single customizable message string: simpler, but too limiting for the requested “next action + link” use case. 40 + 41 + ### Extend existing metadata update validation and API 42 + The builder already updates title, description, and slug via the form metadata flow. Completion fields should be added to the same schema and persistence path. 43 + 44 + **Why this approach:** 45 + - One save action for all form-level settings. 46 + - No new endpoint or settings surface is required. 47 + - Keeps builder behavior predictable for creators. 48 + 49 + **Alternatives considered:** 50 + - Separate completion-settings endpoint: unnecessary complexity for a small metadata extension. 51 + 52 + ### Treat the follow-up link as optional and only render it when complete 53 + The runner should show the CTA only when both label and URL are present and valid enough to save. Partial CTA input should not render a broken link. 54 + 55 + **Why this approach:** 56 + - Prevents incomplete UI states. 57 + - Matches the user request without forcing every form to include a CTA. 58 + 59 + **Alternatives considered:** 60 + - Render when only a URL exists: creates unclear CTA text. 61 + - Require a link for all forms: too restrictive. 62 + 63 + ### Keep fallback completion copy for existing forms 64 + Older forms should continue to show a coherent completion state even before creators edit the new settings. 65 + 66 + **Why this approach:** 67 + - Avoids a migration that would require backfilling handcrafted content for every form. 68 + - Preserves the current experience while allowing progressive customization. 69 + 70 + **Alternatives considered:** 71 + - Make new fields required with no defaults: risks blank or broken completion screens for existing forms. 72 + 73 + ## Risks / Trade-offs 74 + 75 + - **More fields on `Form`** → Mitigation: use a small, explicit set of fields rather than a generalized content system. 76 + - **Invalid or unsafe link input** → Mitigation: validate URLs on save and render only complete CTA data. 77 + - **Long completion copy can overflow the current finish screen design** → Mitigation: keep inputs plain text, provide reasonable validation limits, and reuse the responsive card layout already used by the completion state. 78 + - **Migration introduces schema drift if not applied everywhere** → Mitigation: add a Prisma migration and ensure builder/public serializers read the new fields. 79 + 80 + ## Migration Plan 81 + 82 + 1. Add new completion fields to the Prisma `Form` model. 83 + 2. Generate and apply a migration with safe defaults or nullable link fields. 84 + 3. Extend form metadata validation, serialization, and update logic. 85 + 4. Add builder settings inputs for completion title, completion message, and optional link label/URL. 86 + 5. Update the public runner completion state to use saved form data with fallback defaults. 87 + 6. Verify existing forms still submit successfully and display a valid completion state. 88 + 89 + ## Open Questions 90 + 91 + - Should the CTA open in a new tab by default for external URLs? 92 + - Do we want URL validation to allow relative links, or only absolute `http/https` URLs? 93 + - Should the completion title/message have stricter length limits for better visual consistency?
+25
openspec/changes/archive/2026-04-08-customize-submission-message/proposal.md
··· 1 + ## Why 2 + 3 + Creators currently get a fixed completion state after respondents submit a form. That makes it hard to guide people toward a next step such as visiting a resource, booking time, joining a waitlist, or reading follow-up information. 4 + 5 + ## What Changes 6 + 7 + - Allow creators to configure the text shown after a successful form submission. 8 + - Allow creators to optionally add a follow-up link in the submission completion state. 9 + - Replace the current fixed completion copy in the public runner with the form-specific completion content. 10 + - Keep a sensible default completion message for forms that have not been customized. 11 + 12 + ## Capabilities 13 + 14 + ### New Capabilities 15 + - None. 16 + 17 + ### Modified Capabilities 18 + - `conversational-form-builder`: creators can edit post-submission completion content for an owned form. 19 + - `anonymous-form-runner`: respondents see the form's configured completion content and optional follow-up link after submission. 20 + 21 + ## Impact 22 + 23 + - Affected specs: `conversational-form-builder`, `anonymous-form-runner` 24 + - Affected code: form metadata model, builder settings UI, public runner completion state, form serialization and persistence 25 + - Affected APIs: form metadata update flow and any form payloads that expose completion content to the runner
+20
openspec/changes/archive/2026-04-08-customize-submission-message/specs/anonymous-form-runner/spec.md
··· 1 + ## MODIFIED Requirements 2 + 3 + ### Requirement: Form submission stores anonymous responses 4 + The system SHALL allow a respondent to submit a completed published form anonymously, SHALL store only the form response data needed for review, without using creator login credentials as part of submission identity, and SHALL show a completion state based on the form's configured completion content. 5 + 6 + #### Scenario: Respondent submits a completed form 7 + - **WHEN** a respondent completes the final step of a published form and submits it 8 + - **THEN** the system stores an anonymous response for that form and shows the form's completion state 9 + 10 + #### Scenario: Form has custom completion content 11 + - **WHEN** a respondent submits a published form whose creator configured custom completion text 12 + - **THEN** the system shows the saved completion heading and message after submission 13 + 14 + #### Scenario: Form has a configured follow-up link 15 + - **WHEN** a respondent submits a published form whose creator configured a follow-up link label and URL 16 + - **THEN** the system shows the follow-up link in the completion state after submission 17 + 18 + #### Scenario: Form has no custom completion content 19 + - **WHEN** a respondent submits a published form without customized completion settings 20 + - **THEN** the system shows the default completion content after submission
+16
openspec/changes/archive/2026-04-08-customize-submission-message/specs/conversational-form-builder/spec.md
··· 1 + ## ADDED Requirements 2 + 3 + ### Requirement: Creator can configure post-submission completion content 4 + The system SHALL allow an authenticated creator to edit the completion content for an owned form, including a completion heading, supporting message, and an optional follow-up link label and URL. 5 + 6 + #### Scenario: Creator saves custom completion text 7 + - **WHEN** an authenticated creator updates the completion heading or supporting message for an owned form 8 + - **THEN** the system saves the updated completion content as part of that form's settings 9 + 10 + #### Scenario: Creator saves a follow-up link 11 + - **WHEN** an authenticated creator provides both a follow-up link label and URL for an owned form 12 + - **THEN** the system saves the follow-up link so it can be shown after submission 13 + 14 + #### Scenario: Creator leaves follow-up link incomplete 15 + - **WHEN** an authenticated creator leaves the follow-up link label or URL empty 16 + - **THEN** the system does not save or expose a partial follow-up link for the form
+22
openspec/changes/archive/2026-04-08-customize-submission-message/tasks.md
··· 1 + ## 1. Data model and validation 2 + 3 + - [x] 1.1 Add completion title, message, link label, and link URL fields to the `Form` model and create a Prisma migration 4 + - [x] 1.2 Extend form metadata validation to accept completion content and reject incomplete or invalid follow-up link data 5 + - [x] 1.3 Update form serialization and update logic so builder and public form payloads include the new completion settings 6 + 7 + ## 2. Builder settings support 8 + 9 + - [x] 2.1 Add completion content inputs to the builder form settings panel 10 + - [x] 2.2 Populate the new builder inputs from existing form metadata and persist them through the existing save settings action 11 + - [x] 2.3 Verify builder behavior for default values, empty CTA fields, and validation feedback 12 + 13 + ## 3. Public runner completion state 14 + 15 + - [x] 3.1 Update the public form runner to render saved completion title and message after successful submission 16 + - [x] 3.2 Render the optional follow-up link only when both CTA label and URL are available 17 + - [x] 3.3 Preserve fallback completion copy for forms without customized completion settings 18 + 19 + ## 4. Verification 20 + 21 + - [x] 4.1 Test that existing forms still submit successfully and show a valid completion state 22 + - [x] 4.2 Run build and relevant checks to confirm the new metadata flow works end-to-end
+14 -2
openspec/specs/anonymous-form-runner/spec.md
··· 41 41 - **THEN** the system blocks advancement and indicates that an answer is required 42 42 43 43 ### Requirement: Form submission stores anonymous responses 44 - The system SHALL allow a respondent to submit a completed published form anonymously and SHALL store only the form response data needed for review, without using creator login credentials as part of submission identity. 44 + The system SHALL allow a respondent to submit a completed published form anonymously, SHALL store only the form response data needed for review, without using creator login credentials as part of submission identity, and SHALL show a completion state based on the form's configured completion content. 45 45 46 46 #### Scenario: Respondent submits a completed form 47 47 - **WHEN** a respondent completes the final step of a published form and submits it 48 - - **THEN** the system stores an anonymous response for that form and shows a completion state 48 + - **THEN** the system stores an anonymous response for that form and shows the form's completion state 49 + 50 + #### Scenario: Form has custom completion content 51 + - **WHEN** a respondent submits a published form whose creator configured custom completion text 52 + - **THEN** the system shows the saved completion heading and message after submission 53 + 54 + #### Scenario: Form has a configured follow-up link 55 + - **WHEN** a respondent submits a published form whose creator configured a follow-up link label and URL 56 + - **THEN** the system shows the follow-up link in the completion state after submission 57 + 58 + #### Scenario: Form has no custom completion content 59 + - **WHEN** a respondent submits a published form without customized completion settings 60 + - **THEN** the system shows the default completion content after submission
+15
openspec/specs/conversational-form-builder/spec.md
··· 69 69 #### Scenario: Creator reads helper copy in the builder 70 70 - **WHEN** an authenticated creator views builder headings, descriptions, or empty states 71 71 - **THEN** the system uses short task-oriented copy that explains the current editing context without persuasive or ornamental language 72 + 73 + ### Requirement: Creator can configure post-submission completion content 74 + The system SHALL allow an authenticated creator to edit the completion content for an owned form, including a completion heading, supporting message, and an optional follow-up link label and URL. 75 + 76 + #### Scenario: Creator saves custom completion text 77 + - **WHEN** an authenticated creator updates the completion heading or supporting message for an owned form 78 + - **THEN** the system saves the updated completion content as part of that form's settings 79 + 80 + #### Scenario: Creator saves a follow-up link 81 + - **WHEN** an authenticated creator provides both a follow-up link label and URL for an owned form 82 + - **THEN** the system saves the follow-up link so it can be shown after submission 83 + 84 + #### Scenario: Creator leaves follow-up link incomplete 85 + - **WHEN** an authenticated creator leaves the follow-up link label or URL empty 86 + - **THEN** the system does not save or expose a partial follow-up link for the form
+5
prisma/migrations/20260408185349_add_completion_content/migration.sql
··· 1 + -- AlterTable 2 + ALTER TABLE "Form" ADD COLUMN "completionLinkLabel" TEXT, 3 + ADD COLUMN "completionLinkUrl" TEXT, 4 + ADD COLUMN "completionMessage" TEXT NOT NULL DEFAULT 'Your response was submitted anonymously. The creator can review your answers, but they are not linked to a login or account.', 5 + ADD COLUMN "completionTitle" TEXT NOT NULL DEFAULT 'Thanks for taking the time.';
+15 -11
prisma/schema.prisma
··· 72 72 } 73 73 74 74 model Form { 75 - id String @id @default(cuid()) 76 - userId String 77 - title String @default("Untitled form") 78 - description String @default("") 79 - slug String @unique 80 - status FormStatus @default(DRAFT) 81 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 82 - blocks FormBlock[] 83 - responses Response[] 84 - createdAt DateTime @default(now()) 85 - updatedAt DateTime @updatedAt 75 + id String @id @default(cuid()) 76 + userId String 77 + title String @default("Untitled form") 78 + description String @default("") 79 + completionTitle String @default("Thanks for taking the time.") 80 + completionMessage String @default("Your response was submitted anonymously. The creator can review your answers, but they are not linked to a login or account.") 81 + completionLinkLabel String? 82 + completionLinkUrl String? 83 + slug String @unique 84 + status FormStatus @default(DRAFT) 85 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 86 + blocks FormBlock[] 87 + responses Response[] 88 + createdAt DateTime @default(now()) 89 + updatedAt DateTime @updatedAt 86 90 87 91 @@index([userId, updatedAt(sort: Desc)]) 88 92 }