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
22 changes: 22 additions & 0 deletions .changeset/12626-spec-next-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@object-ui/app-shell": minor
"@object-ui/types": minor
"@object-ui/core": minor
"@object-ui/plugin-calendar": minor
"@object-ui/plugin-designer": minor
"@object-ui/plugin-detail": minor
"@object-ui/plugin-kanban": minor
"@object-ui/console": minor
"@object-ui/runner": minor
---

Resolve `@objectstack/*` `18.0.0-next.2` (objectui#12626).

The console and the runner bundle `18.0.0-next.2`. `@object-ui/types` and `@object-ui/app-shell` now declare `@objectstack/spec` `^18.0.0-next.2`; the other packages keep `^18.0.0-next.1`, which already admits it.

**The record dialog opens bounded on desktop for every object.** A create, edit or child record dialog on an object with many fields used to open as a 95vw task canvas. `@objectstack/spec` `18.0.0-next.2` derives a bounded modal for it on desktop and keeps the full-screen sheet on a phone, and the console now takes that answer directly.

**Accept sets that narrow with it.** `@object-ui/types` validates a page through the spec's own page checks, so its faces (`safeValidateSchema` and `StrictAnyComponentSchema`) now refuse what `18.0.0-next.2` refuses. Each was accepted before:

- On a `kind: 'full'` page, a region whose `name` the page's `template` does not place (with the default template: anything but `header`, `sidebar`, `main`, `aside` and `footer`), and a second region for a slot another region already fills. The renderer stacked such a region under `main` at full width. Rename it to a slot the template places, or choose the `dashboard` template, which lays every other region out as a grid cell.
- `isDefault: true` on a page whose `type` is its page kind (`record`, `home` or `utility`) unless it is a `record` page bound to an `object`. Delete the key; an app's landing page is its first navigation item.
Original file line number Diff line number Diff line change
Expand Up @@ -2966,11 +2966,11 @@ const MEMBER_PINS: Record<string, MemberPin> = {
},
'object-grid.data': {
file: 'packages/plugin-grid/src/__tests__/gridDataInputContract.test.ts',
pins: 'The `object` arm is `ViewDataSchema` discriminated on `provider`: each of the three providers parses and the retired `api` one is refused (spec 18.0.0-next.1, objectui#12370), none of them is an array, the declaration is one shape across both registered tags so the alias cannot drift, and it is pinned at compile time too (objectui#5090).',
pins: 'The `object` arm is `ViewDataSchema` discriminated on `provider`: each of the three providers parses and the retired `api` one is refused (spec 18.0.0-next.2, objectui#12370), none of them is an array, the declaration is one shape across both registered tags so the alias cannot drift, and it is pinned at compile time too (objectui#5090).',
},
'object-grid.conditionalFormatting': {
file: 'packages/plugin-grid/src/__tests__/gridRowDecorationMembers-8071.test.tsx',
pins: 'The members read inside ONE formatting rule, asserted on the `style` attribute of the `<tr>` the grid paints. THREE alternative predicate members with a PRECEDENCE between them — one rule carrying `condition`, `expression` and the native `field`/`operator`/`value` triple, each naming a DIFFERENT row, is decided by `condition`; drop it and `expression` decides; drop that and the triple does — so a renderer reading any two of them as equivalent is red in a specific direction rather than merely unpinned. FOUR style members, and the sharp one is a RENAME: `backgroundColor` and `borderColor` keep their authored names on the way to the DOM while `textColor` is read as the CSS `color`, so keeping the authored spelling hands React a key it silently drops — same rules, same background, the text colour gone with nothing thrown. `style` is pinned as the BASE the three colour members override rather than replace (a non-colour member of it survives alongside the override). Two rule-list facts complete it: FIRST-MATCH-WINS with no merge (the second matching rule\'s own `textColor` never lands) and a rule carrying NO predicate member is SKIPPED rather than read as always-true, with the rule after it still deciding. ⚠️ Since objectui#11533 the AUTHORED member is ONE dialect, `{ condition, style }`: `expression`, the native triple and the three top-level colour members are refused by name on `ObjectGridSchema` and the list view (`packages/types/src/__tests__/grid-list-view-conditional-formatting-11533.test.ts`), so the rows that write them pin STORED-rule reads, which the resolver keeps as compatibility arms; a last block pins the two facts side by side — the stored native rule refused at authoring and painted at render, with its respelling painting the same row. The registration is an `array` arm; the installed spec row (18.0.0-next.1, re-measured by objectui#12030, objectui#12370) takes the list view\'s own `{ condition, style }` rule and refuses the native members and any other key, so the spec fixes the AUTHORED shape and the read site is the rest of the member contract, the stored-rule compatibility arms included (objectui#8071 slice 12).',
pins: 'The members read inside ONE formatting rule, asserted on the `style` attribute of the `<tr>` the grid paints. THREE alternative predicate members with a PRECEDENCE between them — one rule carrying `condition`, `expression` and the native `field`/`operator`/`value` triple, each naming a DIFFERENT row, is decided by `condition`; drop it and `expression` decides; drop that and the triple does — so a renderer reading any two of them as equivalent is red in a specific direction rather than merely unpinned. FOUR style members, and the sharp one is a RENAME: `backgroundColor` and `borderColor` keep their authored names on the way to the DOM while `textColor` is read as the CSS `color`, so keeping the authored spelling hands React a key it silently drops — same rules, same background, the text colour gone with nothing thrown. `style` is pinned as the BASE the three colour members override rather than replace (a non-colour member of it survives alongside the override). Two rule-list facts complete it: FIRST-MATCH-WINS with no merge (the second matching rule\'s own `textColor` never lands) and a rule carrying NO predicate member is SKIPPED rather than read as always-true, with the rule after it still deciding. ⚠️ Since objectui#11533 the AUTHORED member is ONE dialect, `{ condition, style }`: `expression`, the native triple and the three top-level colour members are refused by name on `ObjectGridSchema` and the list view (`packages/types/src/__tests__/grid-list-view-conditional-formatting-11533.test.ts`), so the rows that write them pin STORED-rule reads, which the resolver keeps as compatibility arms; a last block pins the two facts side by side — the stored native rule refused at authoring and painted at render, with its respelling painting the same row. The registration is an `array` arm; the installed spec row (18.0.0-next.2, re-measured by objectui#12030, objectui#12370, objectui#12626) takes the list view\'s own `{ condition, style }` rule and refuses the native members and any other key, so the spec fixes the AUTHORED shape and the read site is the rest of the member contract, the stored-rule compatibility arms included (objectui#8071 slice 12).',
},
'object-grid.operations': {
file: 'packages/plugin-grid/src/__tests__/gridOperationsMembers-8071.test.tsx',
Expand Down Expand Up @@ -3046,7 +3046,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
},
'object-kanban.conditionalFormatting': {
file: 'packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx',
pins: 'Members are card STYLE RULES in ONE dialect since objectui#11522, the spec list view\'s `{ condition, style }`, and it reaches the sink: a rule colours the matching card and only it, a second rule aimed at the other card does the same, and the whole `style` map arrives, with the sibling card in the same render as the live non-matching control so a green cannot come from two unstyled cards agreeing. The retired native `{ field, operator, value }` and flat-colour dialects are refused by name on the types side (`packages/types/src/__tests__/kanban-conditional-formatting.test.ts`); these rows are their respellings, painting the same cards. ⛔ NOT an identity pin and not a wire pin: `ObjectKanban.tsx` never names this key at all (measured zero, against nine for `cardFields` in the same file) — it rides the `{ ...schema }` spread into `KanbanRenderer`, which forwards it to `KanbanImpl`\'s `getCardStyles`. That makes the pin load-bearing in a way the others are not: an edit replacing that spread with an explicit key list drops the key silently and nothing else in the repo would notice. The installed spec row (18.0.0-next.1, re-measured by objectui#12030, objectui#12370) takes the list view\'s own `{ condition, style }` rule, so the spec fixes the authored shape and the read site is the rest of the member contract (objectui#8313).',
pins: 'Members are card STYLE RULES in ONE dialect since objectui#11522, the spec list view\'s `{ condition, style }`, and it reaches the sink: a rule colours the matching card and only it, a second rule aimed at the other card does the same, and the whole `style` map arrives, with the sibling card in the same render as the live non-matching control so a green cannot come from two unstyled cards agreeing. The retired native `{ field, operator, value }` and flat-colour dialects are refused by name on the types side (`packages/types/src/__tests__/kanban-conditional-formatting.test.ts`); these rows are their respellings, painting the same cards. ⛔ NOT an identity pin and not a wire pin: `ObjectKanban.tsx` never names this key at all (measured zero, against nine for `cardFields` in the same file) — it rides the `{ ...schema }` spread into `KanbanRenderer`, which forwards it to `KanbanImpl`\'s `getCardStyles`. That makes the pin load-bearing in a way the others are not: an edit replacing that spread with an explicit key list drops the key silently and nothing else in the repo would notice. The installed spec row (18.0.0-next.2, re-measured by objectui#12030, objectui#12370, objectui#12626) takes the list view\'s own `{ condition, style }` rule, so the spec fixes the authored shape and the read site is the rest of the member contract (objectui#8313).',
},
'object-kanban.data': {
file: 'packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx',
Expand Down
2 changes: 1 addition & 1 deletion e2e/live/ci/backend.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#
# This file declares exactly ONE @objectstack pin, and that is the whole
# design: the commit is derived from it, see below.
OBJECTSTACK_VERSION=18.0.0-next.1
OBJECTSTACK_VERSION=18.0.0-next.2

# There is deliberately no OBJECTSTACK_REF key here. The objectstack-ai/objectstack
# commit the showcase app metadata is sparse-checked-out from is DERIVED at
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"@object-ui/types": "workspace:*",
"@objectstack/formula": "^18.0.0-next.1",
"@objectstack/lint": "^18.0.0-next.1",
"@objectstack/spec": "^18.0.0-next.1",
"@objectstack/spec": "^18.0.0-next.2",
"@sentry/react": "^10.70.0",
"jsonc-parser": "^3.3.1",
"lucide-react": "^1.43.0",
Expand Down
52 changes: 11 additions & 41 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, type RecordFlowSurface } from '@object-ui/plugin-view';
import { deriveRecordSurface, deriveRecordFlowSurface } 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,36 +102,6 @@ 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 @@ -1220,20 +1190,20 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
...(!editingRecord && (formLinkValues ?? overlayCreateValues)
? { initialValues: formLinkValues ?? overlayCreateValues }
: {}),
// objectstack-ai/objectstack#2604 D1: create/edit follow the flow-surface derivation —
// 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(
// objectstack-ai/objectstack#2604 D1: create/edit follow the flow-surface derivation.
// This host calls it with no `viewport`, so it gets the desktop
// answer: a bounded modal (`size: 'auto'`, ModalForm's bounded
// default when modalSize is unset) for field-heavy and light
// objects alike (objectstack-ai/objectstack#23734, objectui#12592);
// a phone gets ModalForm's full-screen sheet below `sm`. Derived
// default only — anything spread later (form view layout) would
// win. Child tasks size to the CHILD object's def.
...(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