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
43 changes: 43 additions & 0 deletions .changeset/8772-objectstack-17-4-0-line.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@object-ui/types': minor
'@object-ui/plugin-dashboard': minor
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-kanban': minor
'@object-ui/components': minor
'@object-ui/app-shell': minor
'@object-ui/console': minor
---

Take the `@objectstack/*` line to 17.4.0 and follow every contract it moved
(objectui#8772, with #7783 · #7663 · #8172 · #7845 · #8785).

**Breaking on one authored key, deliberately.** A dashboard node's
`refreshInterval` is now `refreshIntervalSeconds` — the value is unchanged
(seconds), and the spec refuses the old spelling by name with a message naming
the new one, so an existing document fails loudly at parse rather than silently
losing its auto-refresh. `os migrate meta --from 17` lists the mechanical edits.
The report component's own `refreshInterval` is a different key and is NOT
affected. (Scored `minor`, not `major`: this repo's fixed group tracks the
`@objectstack` major — AGENTS.md §版本号策略.)

Also authored-surface changes, all following a published spec move rather than a
local decision:

- `element:record_picker`'s `filter` input is now the `ViewFilterRule` array
form `[{ field, operator, value }, …]`. The MongoDB-style record form is
refused by the contract (objectstack#14406 converged the last record-form
`filter` in the map), so a JSX page writing the array form no longer draws a
false `type-mismatch` and one writing the record form is told.
- `object-kanban` now publishes `limit`, the row cap its renderer has always
lowered to `$top`. The spec declares it as of objectstack#16503, so the key
the docs teach is finally one the save gate stores.
- `object-gantt`'s ten extension keys (`timeSegments`, `interactions`,
`lockField`, …) are derived from the spec's `GanttConfigSchema` instead of
being re-declared locally. Same accept set on the flat face; the nested
`gantt` block narrows to the spec's, which now refuses an undeclared sub-key
by name where its `.passthrough()` window used to admit one.
- The console's preview-gallery samples — the worked examples an author copies —
move with two spec changes of their own: a job's `timeout` is now `timeoutMs`
(same unit-in-the-key-name ruling as the dashboard key above), and a flow's
end node writes `outcome: 'completed'`, the enum having narrowed to
`completed | refused`.
2 changes: 1 addition & 1 deletion .changeset/dashboard-declare-honoured-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@object-ui/plugin-dashboard': minor
---

`dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshInterval` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title.
`dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshIntervalSeconds` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title.
Original file line number Diff line number Diff line change
Expand Up @@ -2163,7 +2163,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
},
'element:record_picker.filter': {
file: 'packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts',
pins: 'The `object` arm is `FilterConditionSchema` — field keys plus `$and`/`$or`/`$not` — with the rule-ARRAY spelling every sibling `filter` uses rejected outright, and the description pinned to the renderer\'s own precedence read (`composed?.filter ?? props.filter`), objectui#3830.',
pins: 'The `array` arm is `ViewFilterRuleSchema[]` — the one filter orthography this map\'s array-declared `filter` doors share — with the MongoDB-style RECORD spelling (`{ status }`, `{ $and }`) refused by kind at `filter` and a bare tuple refused at `filter.0`, and the description pinned to the renderer\'s own precedence read (`composed?.filter ?? props.filter`). objectui#3830 declared the key; objectstack#14406 converged it off the record form and objectui#7663 moved this side onto the array arm.',
},
'element:record_picker.label': {
file: 'packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx',
Expand Down
20 changes: 15 additions & 5 deletions apps/console/src/preview-samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,12 @@ export const SAMPLES: Record<string, Record<string, unknown>> = {
{ id: 'review', type: 'screen', label: 'CSM review', config: { fields: [ { name: 'discount', label: 'Discount %', type: 'number', required: false }, { name: 'note', label: 'Note', type: 'text', required: true, visibleWhen: 'discount > 0' } ] } },
{ id: 'notify', type: 'script', label: 'Email the owner', config: { actionType: 'email', template: 'renewal_reminder', recipients: ['owner.email', 'csm@example.com'], variables: { contractId: '{contractId}' } } },
{ id: 'enrich', type: 'script', label: 'Score (code)', config: { script: "variables.score = 42;\nreturn variables;", outputVariables: ['score'] } },
{ id: 'end', type: 'end', label: 'End', config: { outcome: 'success' } },
// `completed`, not `success`: @objectstack/spec 17.4.0 narrowed the end
// node's outcome enum to `completed | refused` (objectui#8785). `refused`
// is not interchangeable — the spec additionally REQUIRES a `message`
// beside it, because a refusal with no text is the shape that contract
// exists to replace. This sample ends successfully, so it is `completed`.
{ id: 'end', type: 'end', label: 'End', config: { outcome: 'completed' } },
],
// `FlowEdgeSchema.id` is REQUIRED — the canvas already keys off it when
// present (splitting an edge, adding a back-edge), and an id-less edge
Expand Down Expand Up @@ -315,10 +320,15 @@ export const SAMPLES: Record<string, Record<string, unknown>> = {
schedule: { type: 'cron', expression: '0 2 * * *', timezone: 'UTC' },
handler: 'syncOrders',
retryPolicy: { maxRetries: 3 },
// `timeout`, not `timeoutMs` — the unit is already milliseconds. There is
// no `concurrency` key on a job; `JobSchema` is `.strict()` since spec
// 17.0.0, so both spellings are now rejected by name instead of dropped.
timeout: 600000,
// `timeoutMs`, not `timeout` — @objectstack/spec 17.4.0 RETIRED the bare
// spelling and put the unit in the key name, the same ruling that renamed
// `dashboard.refreshInterval` (ruling B on objectstack#14478): the sibling
// `retryPolicy.backoffMs` already spelled its own unit, so one surface was
// teaching two conventions. The VALUE is unchanged — still milliseconds.
// There is no `concurrency` key on a job either; `JobSchema` is `.strict()`
// since spec 17.0.0, so both retired spellings are rejected by name rather
// than dropped (objectui#8785).
timeoutMs: 600000,
},

agent: {
Expand Down
4 changes: 2 additions & 2 deletions content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ A widget-based dashboard with configurable grid layout and auto-refresh.
"type": "dashboard",
"columns": 4,
"gap": 6,
"refreshInterval": 30000,
"refreshIntervalSeconds": 30,
"widgets": [
{
"id": "revenue",
Expand Down Expand Up @@ -1050,7 +1050,7 @@ A widget-based dashboard with configurable grid layout and auto-refresh.
| `columns` | `number` | Number of grid columns. |
| `gap` | `number` | Gap between widgets (Tailwind spacing scale). |
| `widgets` | `DashboardWidgetSchema[]` | **Required.** Widget definitions with `id`, `title`, `colSpan`, `rowSpan`, and `body`. |
| `refreshInterval` | `number` | Auto-refresh interval in milliseconds. |
| `refreshIntervalSeconds` | `number` | Auto-refresh interval in **seconds** — the renderer multiplies by 1000. Renamed from `refreshInterval`, which this table documented as milliseconds and which it never was (objectui#7783). |

Each widget supports `colSpan` and `rowSpan` to control its size in the grid. The `body` can be any `SchemaNode`.

Expand Down
2 changes: 1 addition & 1 deletion content/docs/plugins/plugin-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ npm install @object-ui/plugin-dashboard
defaultRange?: string, // a spec date preset, or 'custom'
allowCustomRange?: boolean
},
refreshInterval?: number, // Auto-refresh period in seconds; runs only when the host wires onRefresh
refreshIntervalSeconds?: number, // Auto-refresh period in seconds; runs only when the host wires onRefresh
columns?: number, // Grid columns (default: 3)
gap?: number, // Gap between widgets
className?: string
Expand Down
5 changes: 3 additions & 2 deletions content/docs/plugins/plugin-kanban.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ const board: ObjectKanbanSchema = {
}
```

The `dataSource` binding sets it too — its own `limit`, or the
`pagination.pageSize` of the view it names (see
The `dataSource` binding sets it too, and the two do not rank the same way: the
binding's OWN `limit` wins outright, while the `pagination.pageSize` of a view it
names fills the cap only when the board leaves `limit` unset (see
[Data source](/docs/guide/data-source)).

<Callout type="info">
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=17.3.0
OBJECTSTACK_VERSION=17.4.0

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -268,23 +268,26 @@ describe('objectui#6041 · saveFields PUTs the relationship target as `reference
* to be a declared divergence.
*
* ⚠️ Read the instrument note before adding a spec assertion here. This repo's
* pin is `@objectstack/spec` **17.3.0** (`pnpm-lock.yaml`; the 17.2.0 this note
* used to name was superseded by objectui#7122). At 17.3.0 the #13632
* refinement refuses an absent and an EMPTY `reference` but spells its
* emptiness test as an equality against `''`, so `' '` still parses green.
* An assertion of the shape "the spec accepts `' '`" therefore measures the
* VERSION SKEW below, not a standing opinion of this repo's.
* pin is `@objectstack/spec` **17.4.0** (`pnpm-lock.yaml`; 17.3.0 before
* objectui#8772, 17.2.0 before objectui#7122).
*
* The divergence itself is retired (objectui#8621): objectstack#16920 applies
* that emptiness test to the trimmed value, so upstream now refuses the same
* shape under the same `custom` issue at the same `reference` path, and this
* writer's predicate mirrors the contract instead of exceeding it. The fix is
* an unreleased `minor` at the time of writing, which is why the spec half
* below still reads `success = true`: it is pinned against the INSTALLED spec,
* and it is the tripwire that reddens when the pin reaches the fix. ⛔ The
* writer's refusal is asserted separately and does not depend on the installed
* spec at all — that separation is what makes the bump a one-line edit here and
* no behaviour change at all in `MetadataService.ts`.
* ⭐ THE SKEW IS CLOSED, AND THE TRIPWIRE THAT SAID SO HAS FIRED. Until 17.3.0
* the #13632 refinement refused an absent and an EMPTY `reference` but spelled
* its emptiness test as an equality against `''`, so `' '` parsed green while
* this writer refused it — a real divergence, declared rather than hidden.
* objectstack#16920 (reported as objectstack#16126, retired here under
* objectui#8621) applies that test to the TRIMMED value, and it shipped in
* 17.4.0. Measured at this pin: `' '`, `''` and an absent target are all
* refused upstream under the same `custom` issue at the same `reference` path
* the writer names, so the writer's predicate now MIRRORS the contract rather
* than exceeding it.
*
* The row below was written as a tripwire against exactly this event and said
* what to do when it fired — "the reading becomes `false` and nothing else here
* moves" — which is what happened: the spec half flipped, and `MetadataService.ts`
* did not change at all. ⛔ The writer's refusal is still asserted separately and
* still does not depend on the installed spec; that separation is what kept this
* a prose-and-one-verdict edit.
*/
describe('objectui#7714 · the target states, and the retired divergence', () => {
const puttable = async (reference: unknown) => {
Expand Down Expand Up @@ -327,33 +330,45 @@ describe('objectui#7714 · the target states, and the retired divergence', () =>
expect(savedFields(puts)[0].reference).toBe(' account ');
});

it('the VERSION SKEW is real at THIS pin: 17.3.0 accepts `\' \'`, this writer refuses it', async () => {
it('the VERSION SKEW is CLOSED at THIS pin: 17.4.0 refuses `\' \'` too, exactly as this writer does', async () => {
// The writer half — true regardless of which spec is installed, and
// unchanged by objectstack#16920. This is the row that must never move.
const { refused } = await puttable(' ');
expect(refused).toBe(true);

// The spec half, stated honestly for the INSTALLED 17.3.0, where #13632's
// emptiness test is an equality against `''`. Discriminating on its own at
// this pin — an absent target IS refused here (asserted below), so a green
// `' '` isolates blankness rather than reflecting a schema that accepts
// everything. ⚠️ This is a TRIPWIRE, not an endorsement: objectstack#16920
// trims before the test, so this line reddens when the pin reaches it — at
// which point the reading becomes `false` and nothing else here moves.
expect(FieldSchema.safeParse({ type: 'lookup', label: 'L', reference: ' ' }).success).toBe(true);

// Falsifying control for the line above: the refinement IS attached and IS
// consulted at this pin — an absent target is refused, as `custom` at path
// `reference`. A passthrough schema would have made the green above
// meaningless.
// The spec half. This line read `.toBe(true)` while the pin was 17.3.0,
// where #13632's emptiness test was an equality against `''`; it was written
// as a TRIPWIRE for the day objectstack#16920 reached the pin, and
// objectui#8772 is that day. Asserted as an ENVELOPE rather than a bare
// `success === false`, because the two halves of the claim are separable: a
// blank target must be refused, and refused AS a blank target — the same
// `custom` issue at the same `reference` path the absent case draws, not an
// `invalid_type` or some unrelated failure that would satisfy a bare false.
const blank = FieldSchema.safeParse({ type: 'lookup', label: 'L', reference: ' ' });
expect(blank.success).toBe(false);
expect(blank.error!.issues.map((i) => i.code)).toContain('custom');
expect(blank.error!.issues.map((i) => i.path.join('.'))).toContain('reference');

// Non-vacuity for the refusal above: the schema has NOT simply become
// hostile to every `reference`. A real name still parses green, and a name
// that merely carries surrounding whitespace still does too — the trim is
// for the TEST only, which is the same reading the writer's own
// pass-it-through row above depends on.
expect(FieldSchema.safeParse({ type: 'lookup', label: 'L', reference: 'account' }).success).toBe(true);
expect(FieldSchema.safeParse({ type: 'lookup', label: 'L', reference: ' account ' }).success).toBe(true);

// The absent case, which the blank case above now matches exactly — the
// convergence stated as a measurement rather than asserted in prose.
const absent = FieldSchema.safeParse({ type: 'lookup', label: 'L' });
expect(absent.success).toBe(false);
expect(absent.error!.issues.map((i) => i.code)).toContain('custom');

// Second control, at value level rather than presence level: a non-string
// is refused as `invalid_type` by the base schema before the refinement
// runs. Blankness was simply not among the things the 17.3.0 test covered —
// the gap objectstack#16126 reported and objectstack#16920 closed.
// Value level rather than presence level: a non-string is refused as
// `invalid_type` by the base schema before the refinement runs — a
// different issue code from the blank case above, which is what makes
// "refused as a blank target" a distinguishable verdict at all. Blankness
// was simply not among the things the 17.3.0 test covered — the gap
// objectstack#16126 reported and objectstack#16920 closed.
const nonString = FieldSchema.safeParse({ type: 'lookup', label: 'L', reference: 42 });
expect(nonString.success).toBe(false);
expect(nonString.error!.issues.map((i) => i.code)).toContain('invalid_type');
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/views/metadata-admin/SchemaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1781,7 +1781,7 @@ function FieldControl({
// "pin it to today's default" in the saved metadata.
//
// ⚠️ Scope of the second half at the current spec pin: `columns` / `gap` /
// `refreshInterval` carry NO `default` in `DashboardSchema` — "(default 12)"
// `refreshIntervalSeconds` carry NO `default` in `DashboardSchema` — "(default 12)"
// exists only as prose in the description, and the 12 is applied by the
// renderer. Those three boxes therefore stay empty until the spec declares
// the defaults it documents; filed upstream rather than hard-coded here,
Expand Down
Loading
Loading