Uh oh!
There was an error while loading. Please reload this page.
fix(console): nest internal /forms/:name in the console shell; internal submit lands on the created record (#4109) - #4279
Merged
Conversation
…al submit lands on the created record (#4109) A `type: 'form'` action navigates to `/forms/:name`, which was declared at the TOP level of the route tree — a sibling of the app-shell routes — so clicking a button inside an app dropped the user onto a bare form with no header, no navigation and no way back. The route is unchanged (deep links keep working; the missing chrome was the defect, not the navigation); it now renders inside the console's layout for app-independent authed pages, the same chrome /home and /organizations use. The public /f/:slug path stays chrome-less — an anonymous visitor has no console to be inside. The post-submit default was `{ kind: 'thank-you' }` for BOTH modes, so a signed-in operator who had just created a record got the anonymous confirmation with no link to it. `resolveSubmitBehavior` makes the default mode-aware: internal lands on the created record, public keeps thank-you. A declared `submitBehavior` still wins in both modes — the point of a default is that the corpus never has to opt out of a wrong one. The record id comes from the spec-declared `CreateDataResponse = { object, id, record }`; only that one declared key is read (`record.id` carries the same value, but reading both would be a second contract for one fact). No authorable surface changed: the internal default is deliberately not a new `submitBehavior.kind`, and nothing parses it out of metadata. Per the maintainer ruling of 2026-08-10 quoted verbatim on objectui#4109 (from objectstack#7245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 11, 2026 11:01
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 11, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #4109 — both deliverables of the ruling are implemented, but one half of deliverable 1 (the sidebar) is deliberately left open rather than faked; see "What is NOT in this PR". The card should stay open for that call.
The ruling this implements
Maintainer ruling of 2026-08-10, quoted verbatim on #4109 (from objectstack#7245) — reproduced untranslated because it is the spec:
Premise re-verified against
origin/mainBoth measured starting points on the card are still true at
4cb0562b5(the card cited a~line 233that has since drifted to 217):apps/console/src/App.tsx:217—/forms/:namewas a top-level route, a sibling of the app-shell routes.apps/console/src/components/FormPage.tsx:555—const behavior: SubmitBehavior = loaded?.form?.submitBehavior ?? { kind: 'thank-you' }, unconditional across both modes.1. In-shell (ruling point 1)
The route stays exactly where it is; only its element changes, to a new
InternalFormRoutethat wrapsFormPagein the console's layout for app-independent authed pages — the same chrome/homeand/organizationsalready use. The public/f/:slugpath is untouched and stays chrome-less.2. Mode-aware submit default (ruling points 2 and 3)
resolveSubmitBehavior(mode, declared)is a pure exported function: a declaredsubmitBehavioris returned as-is in both modes, and only the empty case differs — internal gets "land on the created record", public keepsthank-you.No authorable surface changed. The "land on the created record" behaviour is deliberately not a new
submitBehavior.kind: the spec's union (thank-you/redirect/continue/next-record) is a strict discriminated union and stays exactly as it is. Nothing parses the new internal default out of metadata — it is only what the renderer does when an author declared nothing, which is what lets the platform default differ from every authorable kind without widening the contract.The seam: what the submit call actually returns
The redirect needs the created record's id, so I verified the response contract rather than assuming it.
POST /api/v1/data/:objectanswers the spec-declaredCreateDataResponse = { object, id, record, droppedFields? }(@objectstack/spec,api/protocol.zod.ts), andpackages/rest's server returns it bare (res.status(201).json(result)).readCreatedRecordIdreads that one declared key.record.idcarries the same value and is deliberately NOT read as an alias — that would be the second de-facto contract AGENTS.md #0.1 forbids, and it is pinned by a negative test.What it does absorb is the transport envelope, which is not a metadata dialect but a platform fact: the runtime's http-dispatcher wraps every success as
{ success, data, meta }while the REST server does not. The platform already resolves this in exactly ONE rule, in@objectstack/client.unwrapResponse;FormPagehand-rollsfetchinstead of going through that client, so the same rule is applied at this call site. Mirroring it is not inventing a dialect — spelling a different one would be. Both shapes are pinned.A response naming no id, or a workspace where no app can host the record page, falls back to the confirmation panel rather than navigating to
record/undefined— the record really was created, so silence would be the worse answer.What is NOT in this PR, and why
The sidebar. The source card measured "no sidebar, navigation, or breadcrumb". This PR delivers the header/navigation; it does not mount
UnifiedSidebar, and that is a judgement I did not want to make silently:ConsoleLayout(the layout that owns the sidebar and breadcrumb) is app-SCOPED by construction — it takes anactiveAppName/activeAppand publishes them as the shell's current app./forms/:namenames no app, so mounting it here means inventing one, and on a cold deep-link that invention resolves to whichever app happens to be first. Wrapping someone's form in an arbitrary app's sidebar and breadcrumb — and writing that guess into shared navigation state on the way past — is worse than no sidebar.Doing it properly needs app-shell's own
resolveHostAppSegment(utils/appRoute.ts), which is written for exactly this case ("a framework-owned, app-independent page") but is not reachable:@object-ui/app-shellexports only its package root, and that root re-exports./utilsnowhere. Exporting it was out of scope here (a parallel agent holds that package). The same unavailability is whycreatedRecordPath.tsimplements only the first two steps of that resolver, documented as a subset with the divergence spelled out.?recordId=is still ignored by this route — filed as #4278, not fixed here.Tests
New:
createdRecordPath.test.ts(host-app policy),FormPage.submit.test.tsx(rendered submit behaviour),internalFormShell.test.tsx(the route table). Extended:FormPage.test.tswithresolveSubmitBehavior+readCreatedRecordId.internalFormShell.test.tsxrenders the realApp.tsxroute table rather than a transcription, for the reasonAppContent.systemHubRoutes.test.tsxdocuments at length: a hand-copied route list is free to agree with whatever the source does.Reverse verification (predicted, then measured)
resolveSubmitBehaviorback tothank-youfor both modesexpected { kind: 'thank-you' } to deeply equal { kind: 'created-record' }andexpected '/forms/showcase_task.edit' to be '/apps/…/record/task-42'FormPage mode="internal"Unable to find an element by: [data-testid="console-shell-layout"]Honest note on the other pins: the two precedence tests and the public-default test are guards, green before and after by design. They are here to stop a future "smarter default" from swallowing a declared behaviour, and to stop the new internal default leaking onto the anonymous path — a pin that only goes red with its own change would cover neither risk.
Commands
Lint delta on
FormPage.tsxis +2react-refresh/only-export-componentswarnings, one per new exported pure function — the same pattern the file already had 5 of. Warnings, not errors.Not browser-verified
Deliverable 3 (the showcase journey end-to-end in a browser) was not performed — no dev stack was booted in this environment. It is compensated with the route-level and component-level tests above, not claimed. Worth a browser pass on acceptance, particularly the cold-deep-link case where no app has been published to navigation context yet.
Generated by Claude Code