Form-share schema + editable shares — v3.31.43
Public form now renders the resource's actual fields; admin can edit label + password after creation.
The previous lesson left two rough edges on form sharing: the public form at /forms/[token] rendered a hardcoded name/email/phone/message shape regardless of the resource, and the admin had no way to edit an existing share. Both shipped fixed in v3.31.43. This lesson covers what changed, why it matters, and how the dispatcher now exposes the resource's real field schema to the public page.
The mental model — two halves of the same problem
Public form sharing in v3.31.20 always rendered the same form because the web page had no idea what the resource's real fields were. Generate a share for Category (which has name + image) and visitors still saw Name + Email + Phone + Message inputs. Name happened to line up; everything else was effectively unusable.
The fix is structural: the dispatcher service that knows which resources are reachable publicly now also knows their field shape. The public-info endpoint returns that shape; the web page renders inputs from it.
Before v3.31.43 After v3.31.43───────────────── ──────────────GET /api/public/forms/<token> GET /api/public/forms/<token>→ { resource_name, → { resource_name,has_password, has_password,label } label,fields: [ <-- new{ key, label,type, required }] }/forms/<token> renders /forms/<token> rendershardcoded name/email/ one input per fields[]phone/message entry, picking the rightHTML shape per type
How field types are inferred
The dispatcher reflects on the Go model struct and walks every field whose json tag isn't empty or -. Framework columns (id, created_at, updated_at, deleted_at, version, slug) are skipped — they're framework-managed, not user-input. For every remaining field the helper maps the reflected Go type onto an HTML input shape:
func publicTypeFor(fieldName string, t reflect.Type) string {for t.Kind() == reflect.Ptr {t = t.Elem()}typeName := t.String()if strings.Contains(typeName, "FileRef") || strings.Contains(typeName, "FileRefs") {return "file"}if strings.Contains(typeName, "time.Time") {return "datetime"}switch t.Kind() {case reflect.Bool:return "checkbox"case reflect.Int, reflect.Int8, reflect.Int16, ..., reflect.Float64:return "number"case reflect.String:lower := strings.ToLower(fieldName)switch {case lower == "email" || strings.HasSuffix(lower, "_email"):return "email"case lower == "phone" || lower == "tel":return "tel"case lower == "description" || lower == "notes" ||lower == "message" || lower == "body":return "textarea"}return "text"}return "text"}
Strings get a second pass on their name so a column called email gets type="email", notes renders a <textarea>, and so on. The heuristics are intentionally narrow — a one-line mapping per common shape — because a wrong guess is the kind of thing operators notice and report fast.
binding:"required" struct tag, the same tag the resource's admin handler uses for validation. Marking a field required is a single source of truth across admin form, public form, and API binding.File fields — why they render disabled
Anything whose Go type contains FileRef or FileRefs maps to type: "file" — but the public form intentionally doesn't render a real file input. Instead it shows an inline explainer:
if (field.type === "file") {return (<div className="space-y-1.5"><label className={labelClass}>{field.label}</label><div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 px-3 py-3 text-xs text-slate-500">File uploads aren't supported on public-share forms.{field.required? " The operator must collect this file through a different channel.": " You can leave this blank."}</div></div>);}
File uploads need the auth-gated /api/uploads endpoint plus a presigned URL flow — neither is available to anonymous visitors. Rendering a file input that secretly doesn't work would be a worse failure mode than not rendering one. The submit handler also strips file keys from the payload before POST, so the dispatcher's typed unmarshal never sees them.
Editing existing shares
The admin form-shares page (/system/form-shares) used to have Audit, Copy, Open, and Delete actions only. Want to add password protection to an existing link? Delete + recreate, then redistribute the new token to every recipient. v3.31.43 adds an Edit button that opens a small modal with three controls:
- Label — free-text operator-facing tag.
- Password mode — three pills: Keep current (no change), Set password (set or rotate the password), Remove password (clear the gate; disabled when the share has no password).
- New password — text input shown only when mode = "Set".
The backend handler at PATCH /api/admin/form-shares/:id already supported the full payload — passing password: "-" as the sentinel means "remove the existing hash." This release adds the missing UI.
Backward compatibility — pre-v3.31.43 projects
Two things changed in the dispatcher: a new PublicFields(resourceName) function with its own // grit:form-share:fields marker, and two new imports (reflect, strings) at the top of the file. Older projects scaffolded before v3.31.43 don't have any of these.
The generator handles both cases lazily:
- If the marker is missing,
grit generate resource Fooprints a one-line warning instead of failing. The new dispatch case still lands; the public form just falls back to no-fields. - The import-checker scans the file for
reflectandstringsand adds them lazily the first time they're needed, so the dispatcher compiles cleanly even when the marker patch happens by hand.
Want existing shares to render the right form right away? Either re-scaffold the project (then copy the rest of your custom code back over), or hand-edit services/form_share_dispatch.go to add the PublicFields function and the matching case for each resource. The full template is in the framework repo at internal/scaffold/api_form_share_files.go.
Quick check
Try it
In the project from the previous exercise, exercise the v3.31.43 changes end-to-end:
- Generate a second resource:
grit generate resource Category --fields=name:string!,image:file. Migrate. - In the admin, open
/system/form-sharesand create a share for Category. Copy the URL. - Open the URL in an incognito tab. Confirm you see aName input + the dashed "File uploads aren't supported" explainer for image — not the legacy contact form.
- Back in the admin, click Edit on the new share. Set a password, save. Refresh the public URL — you should now see a password input above Name.
- Submit the form without a password → 401. Submit with the password → 201. Edit the share again, Remove password, save. Submit clean → 201.
Paste the JSON response from GET /api/public/forms/<token> into notes.md so you can see the fields[] shape directly.
What's next
The form-share story is now end-to-end: typed dispatch, real field schema on the public side, and an Edit modal on the admin side. Next chapter shifts to the opposite question — what happens on the dashboard, where each resource now gets auto-generated preset widgets. The lesson on per-resource dashboard widgets in the web course covers that.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub