Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/12521-console-record-dialog-interim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@object-ui/app-shell': patch
---

**The console's New and Edit dialog on a field-heavy object opens bounded on desktop** (objectui#12521). For an object with twelve or more authorable fields, `@objectstack/spec`'s `deriveRecordFlowSurface` answers a full-screen modal for a create, edit or child task, and the console opened that dialog at 95% of the viewport. The console now gives that answer the bounded shape the spec is moving to (objectstack-ai/objectstack#23734), so `ModalForm`'s own default sizes the dialog: `xl` or `lg`, never `full`.

This is an interim with a pinned exit: the bound is one function in the console's record-dialog host, and a test fails, naming its deletion, once objectui installs a spec that derives the bound itself. On a phone the dialog is the full-screen sheet it was. No type, prop or export changes.
10 changes: 10 additions & 0 deletions .changeset/12521-dialog-default-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@object-ui/plugin-form': minor
---

**A record dialog with no authored layout opens bounded, and draws two fields per row wherever no `columns` is declared** (objectui#12521). Three defaults of `ModalForm` change. None reaches a setting the author declared.

- **Size.** A dialog with no `modalSize` derived its width from its inferred column count, and three or more columns derived `full` (95vw). It now derives `lg` or `xl` from the grid it renders and its field count, and never `full`. `inferModalSize` returns `'lg'` for one column and `'xl'` for two or more. An authored `modalSize`, `'full'` included, is used as written.
- **Flat grid.** A dialog whose form declares no `columns` and draws no sections inferred one to four columns from its field count. It now lays out at two columns from the container's `@md` tier up and one below it, so the phone sheet is unchanged. Every field takes one cell, so fields of one kind share one width. A wide kind (textarea, rich text, markdown, html, an inline grid) and a `span: 'full'` field take the whole row. The object's `fieldGroups` fallback lays out the same way.
- **Section density.** In a form that declares no `columns`, a section that declares none draws two fields per row from `@md`, whatever the other sections declare. Beside a `columns: 3` section it draws two per row on the 12-track grid of objectui#12548, where before it filled three; with no section declaring any, the grid is two wide. A declared count, `1` included, is drawn as written, and a form-level `columns` still sets an undeclared section's count. Until `@objectstack/spec` drops `FormSectionSchema.columns`' `default(1)` (objectstack-ai/objectstack#23740), the spec's parse writes `columns: 1` into every section that omits it, so a section from an authored stack arrives declared and draws one column, as before.
- **What does not move.** A declared form `columns`, a declared section `columns` and a field's `colSpan` / `span` render the same grid classes as before. `inferColumns`, and the page form, drawer and detail view that read it, are unchanged. A host that passes a `modalSize` of its own keeps it.
24 changes: 24 additions & 0 deletions content/docs/plugins/plugin-form.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,30 @@ resolve it that way, so the same metadata lays out identically in every host
so each keeps its own authored width). The grid is applied to the field container
inside the form, never wrapped around the `<form>`.

One default differs: a dialog. `ModalForm`, whether `ObjectForm` opens it with
`formType: 'modal'` or a host mounts it directly, draws two fields per row from
the container's `@md` tier up wherever nothing declares `columns`: the flat
field list, the object's `fieldGroups`, and every section that declares no
`columns` in a form that declares none. Such a section draws two per row beside
any other section's count; when the counts do not all fit one grid, the form
switches to a twelve-track grid so each section keeps its own count. A declared
count, `1` included, is drawn as written. Every field takes one cell; a wide
kind (textarea, rich text, markdown, html) and a `span: 'full'` field take the
row. Below `@md`, as on a phone, it is one column.

Until `@objectstack/spec` drops `FormSectionSchema.columns`' `default(1)`, the
spec's parse writes `columns: 1` into a section that omits it, so a section from
a `defineView` or `defineStack` stack reaches the dialog declared and draws one
column.

A dialog with no `modalSize` is bounded: `lg` for a short form or a single
column, `xl` otherwise, and never `full`. Declare `modalSize: 'full'` for the
95vw dialog. The console's New and Edit dialog on a field-heavy object (twelve
or more authorable fields) stays within that bound on desktop too: the
record-surface derivation still answers a full-screen modal for it, and the
console bounds that answer until the spec derives the bound itself. On a phone
the dialog is a full-screen sheet whatever its size.

### Tabbed field layout (`fieldTabs`)

A sectioned form stays **one** form: declare the tabs on it and the renderer
Expand Down
49 changes: 40 additions & 9 deletions packages/app-shell/src/console/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { useTrackRouteAsRecent } from '../hooks/useTrackRouteAsRecent.js';
import { useHomePath } from '../hooks/useHomePath.js';
import { useSignedInUserLocale } from '../hooks/useUserLocale.js';
import { resolveRecordFormTarget, resolveFormViewLayout, resolveNavigateCreateUrl, resolveNavigateEditUrl, resolvePostCreateTarget, recordFormCreateValues, readRecordFormCreateValues, RECORD_FORM_CREATE_VALUES_STATE } from '../utils/recordFormNavigation.js';
import { deriveRecordSurface, deriveRecordFlowSurface } from '@object-ui/plugin-view';
import { deriveRecordSurface, deriveRecordFlowSurface, type RecordFlowSurface } from '@object-ui/plugin-view';
import { RECORD_FORM_PARAM, RECORD_FORM_OBJECT_PARAM, RECORD_FORM_LINK_PARAM } from '../urlParams.js';
import { appRouteSegment, matchAppBySegment } from '../utils/appRoute.js';
import { resolveHref, type NavTemplateContext } from '@object-ui/layout';
Expand Down Expand Up @@ -102,6 +102,36 @@ interface AppContentProps {
extraRoutesNoApp?: ReactNode;
}

/**
* INTERIM — objectui#12521, ruled in comment 6205068311 (the maintainer's
* 「可以,12521 走快速方案」), the early outcome of letter B (6203721639).
*
* `deriveRecordFlowSurface` answers `size: 'full'` for a field-heavy create /
* edit / child-create / child-edit flow (objectstack-ai/objectstack#2604 D1),
* and the record dialog below spreads that into `modalSize: 'full'`: a 95vw
* task canvas on desktop. Letter B moves the bound into the protocol — on
* desktop a field-heavy task flow derives a bounded modal, `size: 'auto'` —
* through objectstack-ai/objectstack#23734. This gives the installed
* derivation's answer that shape early, so `ModalForm`'s own bounded default
* (`lg` / `xl`, never `full`) sizes the dialog until that spec is installed.
*
* Desktop only, with no posture check of its own: this host calls the
* derivation with no `viewport`, so the answer it receives is the desktop
* one, and the phone presentation is `ModalForm`'s full-screen sheet below
* `sm`, which no `modalSize` reaches (every size class is `sm:`-prefixed).
* Only the derived default changes: a `modalSize` handed to `ModalForm` is
* used as written.
*
* EXIT: once objectui installs an `@objectstack/spec` carrying
* objectstack-ai/objectstack#23734, delete this function and call
* `deriveRecordFlowSurface` directly at its one call site. The pin
* `AppContent.recordDialogInterimBound-12521.test.tsx` fails on that bump and
* names this deletion.
*/
function interimBoundedTaskFlowSurface(surface: RecordFlowSurface): RecordFlowSurface {
return surface.size === 'full' ? { ...surface, size: 'auto' } : surface;
}

/**
* Bridges the global chat's "Review N change(s)" affordance (ADR-0033 Phase B)
* to the metadata designer. The chat publishes a review target on `assistantBus`;
Expand Down Expand Up @@ -1191,18 +1221,19 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
? { initialValues: formLinkValues ?? overlayCreateValues }
: {}),
// objectstack-ai/objectstack#2604 D1: create/edit follow the flow-surface derivation —
// field-heavy → full-screen modal (the same big canvas the
// detail page gets, with overlay return semantics); light
// objects keep the existing auto-sized modal (ModalForm
// infers from columns when modalSize is unset). Derived
// default only — anything spread later (form view layout)
// would win. Child tasks size to the CHILD object's def.
...(deriveRecordFlowSurface(
// field-heavy → full-screen modal, which the objectui#12521
// interim bounds on desktop until the spec derives it
// (`interimBoundedTaskFlowSurface`); light objects keep the
// auto-sized modal (ModalForm's bounded default when
// modalSize is unset). Derived default only — anything spread
// later (form view layout) would win. Child tasks size to the
// CHILD object's def.
...(interimBoundedTaskFlowSurface(deriveRecordFlowSurface(
formObjectDef,
isChildFormTask
? (editingRecord ? 'child-edit' : 'child-create')
: (editingRecord ? 'edit' : 'create'),
).size === 'full'
)).size === 'full'
? { modalSize: 'full' as const }
: {}),
// Honor the object's DEFAULT FORM VIEW: curated sections (field
Expand Down
Loading
Loading