Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit bdfe0ae

Browse files
Merge branch 'main' into claude/issue-10021-legend-swatch-custom-property
2 parents b63440f + 7a564e0 commit bdfe0ae

54 files changed

Lines changed: 1954 additions & 397 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@object-ui/plugin-kanban': minor
3+
---
4+
5+
fix(plugin-kanban): a kanban lane matches records by its `id` only, never by its `title`
6+
7+
⚠️ Behaviour change (narrowing), declared `minor` under this repo's fixed-group
8+
versioning. Executes maintainer ruling A on objectui#10069.
9+
10+
A kanban board decides which lane a record belongs to by comparing the record's
11+
stored `groupBy` value with each lane's `id` — the picklist option value. Until
12+
now `bucketCardsIntoColumns` also lowercased every lane `title` into that lookup,
13+
so the title was a second, undeclared bucketing key. That had three
14+
user-visible effects, and all three are gone:
15+
16+
- renaming a lane in authored `columns` moved records between lanes;
17+
- relabelling a picklist option moved records too, because lanes derived from
18+
the `groupBy` field's options take `title: opt.label`;
19+
- membership depended on the viewer's locale, because lane titles are
20+
translated: a record storing the English label bucketed into its lane in one
21+
locale and into "Uncategorized" in another.
22+
23+
The lane `title` is now presentation only. The id comparison keeps its existing
24+
case folding (`IN_PROGRESS` still matches a lane `in_progress`).
25+
26+
A record whose value matches no lane id still surfaces in the trailing
27+
"Uncategorized" lane (#2792), and the board now also names it on the console:
28+
one `console.warn` per distinct raw value per bucketing pass, giving the
29+
`groupBy` field, the raw value, the number of records carrying it and the
30+
available lane ids. An empty group value is not warned about.
31+
32+
**Migration.** Records whose group field stores an option's LABEL (for example
33+
`'In Progress'`) instead of its VALUE (`'in_progress'`) used to reach their lane
34+
through the retired title match; they now appear under "Uncategorized", and the
35+
console warning names each such value. Fix the data, not the board: update each
36+
record to store the option value. A drag from "Uncategorized" into the right lane
37+
writes the lane id and so repairs one record at a time; for a bulk repair, find
38+
the affected records with a filter that selects values outside the field's
39+
option values, e.g. `['and', ['status', 'nin', ['in_progress', 'done']],
40+
['status', '!=', null], ['status', '!=', '']]`, and update each to the value
41+
whose option label it stores. ⛔ Do not add the label as a lane `id` to make the
42+
records reappear: that re-teaches the label as a second value for the same state.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@object-ui/plugin-detail': minor
3+
---
4+
5+
feat(plugin-detail): row caps on `record:activity`, `record:history`, `record:chatter` and `record:discussion` admit only a positive integer number, and a refused one warns
6+
7+
`@objectstack/spec` declares `limit` on `RecordActivityProps` (which is also
8+
`RecordChatterProps.feed`) and on `RecordHistoryProps` as
9+
`z.number().int().positive()`, and refuses a string, a boolean or an array
10+
outright. The renderers used to run `Number(value)` before their integer check,
11+
so they also read `'5'` as 5, `' 5 '` as 5, `'0x10'` as 16, `true` as 1 and `[7]`
12+
as 7: a second accepted set, wider than the contract (objectui#10145, ruled
13+
STOP).
14+
15+
Both resolvers (`normalizeLimit`, shared by activity, chatter and discussion,
16+
and `normalizeHistoryLimit`) now admit only a value whose type is `number` and
17+
which is a positive integer. Every other authored value falls back to the
18+
block's default (20 for activity, chatter and discussion; 50 for history). The
19+
renderer then logs one `console.warn` that names the block and spells the raw
20+
value with its type (for example `declared limit: "5" (string)`). The warning
21+
also fires for the fractional, zero, negative, `NaN` and infinite values these
22+
blocks already refused silently. That matches the "always warn" ruling on
23+
objectui#10097 and the three objectui#9925 read points. An absent `limit` is not
24+
a refusal and stays silent.
25+
26+
**Migration.** Stored metadata that spells a row cap as a string (for example
27+
`limit: '5'`, or `feed: { limit: '5' }` on a chatter or discussion block) now
28+
drops to the block's default and warns in the console, where it used to be read
29+
as 5. Write the number instead (`limit: 5`). Publishing such metadata was
30+
already refused by the spec, so this affects only bodies stored before that
31+
check, or written around it.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
feat(types): `slider` and `tooltip` single-or-list keys follow their read sites (objectui#10280, objectui#7759 group B)
6+
7+
Three keys that `@objectstack/spec` does not declare, where the zod mirror accepted a
8+
spelling the TypeScript declaration refused. Each now matches what its renderer reads.
9+
10+
- **`SliderSchema.defaultValue` is now `number | number[]` (wider).** The `slider`
11+
renderer turns a single number into a one-item list, so both spellings render. The
12+
mirror already accepted both, and the TypeScript declaration now does too. Code that
13+
writes the key keeps compiling. Code that reads it must now handle a single number; the
14+
in-repo renderer already does.
15+
- **BREAKING (authoring): `SliderSchema.value` is retired on both faces.** No renderer
16+
reads it: the `slider` renderer reads `defaultValue`, `max`, `min` and `step`, and the
17+
form-control DOM whitelist drops `value` from the props it passes on. An authored `value`
18+
rendered nothing. The mirror now refuses it by name with a message that points at
19+
`defaultValue`, and the TypeScript member is `value?: never`. To migrate, write the
20+
initial position as `defaultValue`.
21+
- **BREAKING (authoring): `TooltipSchema.content` no longer accepts a list.** The renderer
22+
puts `content` straight into a React child position without rendering it through
23+
`renderChildren`, so a list of nodes failed to render with "Objects are not valid as a
24+
React child". The mirror now accepts `string | SchemaNode`, the same as the declaration,
25+
which has not changed. To migrate, put a list under `children`, which the renderer does
26+
render as a list.
27+
28+
Marked `minor` under this repo's version-alignment rule (`major` is reserved for
29+
following an `@objectstack` major); the breaking authoring changes are named above.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@object-ui/plugin-designer': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(plugin-designer): the Navigation Designer has an entry for the spec's `doc` navigation item type
7+
8+
objectstack#19789 added a `doc` member to the spec's navigation item union (an
9+
item that targets a book and/or a doc). `NAV_TYPE_META` in `NavigationDesigner`
10+
is keyed by that spec-derived union, so objectui stopped compiling against
11+
`@objectstack/spec` built from objectstack `main`, and every row reads its badge,
12+
colour and icon from that map. The map now has a `doc` entry (a `BookOpen` icon,
13+
its own colour and the `appDesigner.navTypeDoc` label key). The key has an
14+
English fallback in the designer's defaults and a translation in all ten locale
15+
packs.
16+
17+
The map is typed `Record<NavigationItemType | 'doc', ...>` so it compiles both
18+
against the pinned `@objectstack/spec`, which predates `doc`, and against
19+
objectstack `main`. The `| 'doc'` goes away at the pin bump that ships `doc`.
20+
`doc` is not added to the quick-add buttons: an empty `doc` item fails the spec's
21+
book-or-doc requirement, and authoring one is objectui#10188.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
**BREAKING** — `NamedListView` (one entry of `ObjectViewSchema.listViews`) retires
6+
sixteen members on its TypeScript authoring face (objectui#7924). Each is now a
7+
`?: never` tombstone: a TypeScript author who writes one gets a compile error at that
8+
key, and the member's docblock names the key to write instead.
9+
10+
**The eight legacy `show*` spellings** (bucket ② of the director-seat ruling on
11+
objectui#7924, letter **A**). None is a protocol key on a view; the protocol's
12+
`userActions` and `appearance` blocks are the canonical home, and `NamedListView`
13+
already declares both (objectui#8980), so objectui is not narrower than the protocol
14+
on any of the toggles.
15+
16+
| retired | author instead |
17+
| --- | --- |
18+
| `showSearch` | `userActions.search` |
19+
| `showSort` | `userActions.sort` |
20+
| `showFilters` | `userActions.filter` |
21+
| `showHideFields` | `userActions.hideFields` |
22+
| `showGroup` | `userActions.group` |
23+
| `showColor` | `userActions.rowColor` |
24+
| `showDensity` | `userActions.rowHeight` |
25+
| `showDescription` | `appearance.showDescription` |
26+
27+
```ts
28+
// before
29+
const all: NamedListView = { label: 'All', showSearch: false, showDescription: true };
30+
// after
31+
const all: NamedListView = { label: 'All', userActions: { search: false }, appearance: { showDescription: true } };
32+
```
33+
34+
**Eight members the protocol declares nowhere and no renderer acts on** (bucket ③):
35+
`addDeleteRecordsInline`, `addRecordViaForm`, `clickIntoRecordDetails`,
36+
`collapseAllByDefault`, `color`, `fieldTextColor`, `prefixField`, `wrapHeaders`. Two
37+
tombstones point at a likely intent, with no alias mapping behind either: `color` at the
38+
row-colour configuration `rowColor: { field }`, and `addRecordViaForm` at the
39+
`userActions.addRecordForm` toggle.
40+
41+
**Two bucket-③ members are NOT retired, because each is read:** `allowExport` gates
42+
`ListView`'s export control when the Console relays it off the active named view, and
43+
`densityMode` is folded onto `rowHeight` by `normalizeListViewSchema`. They stay
44+
declared as they were.
45+
46+
**Only the authoring face changes.** Stored view documents that still carry a legacy
47+
spelling keep loading: `normalizeListViewSchema` (`@object-ui/core`) still folds the eight
48+
`show*` flags onto `userActions` / `appearance` at runtime, and hosts that read a stored
49+
view through a loose type are unaffected.
50+
51+
**ADR-0087 disposition:** the eight `show*` spellings are a **D2** conversion. Stored
52+
documents are accepted and converted at load by the existing runtime fold, and the TS
53+
face refuses the old spelling at authoring time. The eight bucket-③ members have **no
54+
conversion**, because the protocol has no equivalent to convert them to. The refusal is
55+
loud and by name on the TS face, and a stored document carrying one stays as inert as
56+
it was before this change.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
feat(types): `AppComponentSchema`, `DashboardComponentSchema` and `PageNodeSchema` take the spec by reference, like their zod mirrors
6+
7+
The three published zod mirrors have long taken `@objectstack/spec`'s `App` / `Dashboard` /
8+
`Page` surface by reference (`BaseSchema.extend(SpecXFields.shape)`), while their hand-written
9+
TypeScript twins restated only the members the renderers read. So the published validator
10+
admitted keys the published type did not declare: the package-lock envelope (`_lock`,
11+
`_lockReason`, `_lockSource`, `_provenance`, `_packageId`, `_packageVersion`, `_lockDocsUrl`,
12+
written by the packaging pipeline), `protection`, the app's `isDefault` / `_unpublished` /
13+
`defaultAgent` / `contextSelectors`, and the page's `source` / `interfaceConfig` / `requires`.
14+
The spec's retirement tombstones reached the type only as `any`, through `BaseSchema`'s index
15+
signature.
16+
17+
Each twin now extends `Omit` over the spec's exported input type, and that `Omit` reads the same
18+
`as const` exclusion array the mirror's `specFieldsExcept` call reads. The two faces therefore
19+
project one spec surface, and a later spec bump moves both of them together:
20+
21+
- **Admitted keys are declared** with the spec's own member types. A reader of a served document
22+
can read `app.protection`, `page.requires` or the envelope without a cast.
23+
- **Tombstones refuse at compile time.** `version`, `homePageId`, `objects`, `apis`, `sharing`,
24+
`embed`, `mobileNavigation` and `aria` on an app, and `aria`, `refreshInterval` and
25+
`performance` on a dashboard, are now optional members typed `undefined`. Authoring a value is
26+
a TypeScript error, which is the verdict the validator already gave at parse. ⚠️ This is the
27+
one breaking edge: code that wrote one of these keys used to compile against the index
28+
signature and was refused at parse. It is now refused by `tsc`, before it runs.
29+
- `DashboardComponentSchema.header` and `PageNodeSchema.slots` keep their own hand-written types.
30+
Both are withheld from the spec projection on the type side only, because neither type is
31+
assignable to the spec's member. The drift is already recorded in the parity ledgers.
32+
`PageNodeSchema.assignedProfiles` is withheld the same way, but for forward compatibility:
33+
objectstack `main` retires the key, and the hand-written `string[]` member (unchanged) would
34+
otherwise stop compiling at the next spec bump. Its retirement is tracked separately.
35+
36+
Why this is a minor bump: the published type surface gains members, and one authoring
37+
spelling moves from a parse-time refusal to a compile-time refusal. This repo marks breaking
38+
semantics `minor` rather than `major`.
39+
40+
This change affects types only. It changes no runtime code and narrows no mirror.

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2424,7 +2424,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
24242424
},
24252425
'object-kanban.columns': {
24262426
file: 'packages/plugin-kanban/src/__tests__/objectKanbanColumnMembers-8071.test.tsx',
2427-
pins: 'The SWIMLANE element\'s six members, each at its OWN sink and none assumed to behave like its neighbour, driven through the real renderer on a real adapter. `id` decides which records land in the lane (`groups[col.id]`) and names the heading, with an unmatched record swept into the trailing lane rather than dropped (objectui#2792) as the control. ⭐ `title` is read TWICE with two unrelated meanings — as the lane\'s accessible name, and as a BUCKETING ALIAS (`labelToColumnId[String(col.title).toLowerCase()] = col.id`), so a record whose stored group value is the lane\'s TITLE lands in that lane as surely as one carrying its id: nothing on the authoring surface says so, and renaming a lane therefore MOVES RECORDS. `cards` is a UNION and not a replacement — static lane cards survive the fetch, come first, and are not de-duplicated against it. `limit` is displayed beside the count and flags the lane, and ⛔ never truncates: the over-limit row asserts every card still renders, so a pin watching only the badge could not stay green under a renderer that dropped the overflow. `className` reaches that lane\'s container and no other. `collapsed` withholds that lane\'s cards while its neighbour keeps them — the member-set row only, because `columnCollapsedHonoured-9628.test.tsx` owns that member whole. The set itself is asserted as a WHITELIST: an undeclared lane member reaches no sink, with a `className` marker on the same lane in the same render as the lit control. ⚠️ The DECLARATION half is a different file and deliberately not duplicated here: `packages/types/src/__tests__/object-kanban-columns-declared-8913.test.ts` (objectui#8913/#8989) parses lane bags against both published faces, and every assertion in it is a `safeParse` — it cannot say what the board DOES with a member it admitted, which is objectui#8068\'s criterion. Read end to end before being cited. New file (objectui#8071 slice 16).',
2427+
pins: 'The SWIMLANE element\'s six members, each at its OWN sink and none assumed to behave like its neighbour, driven through the real renderer on a real adapter. `id` decides which records land in the lane (`groups[col.id]`) and names the heading, with an unmatched record swept into the trailing lane rather than dropped (objectui#2792) as the control. ⭐ `title` is the lane\'s accessible name and ⛔ NOT a bucketing key: it used to be a second, undeclared BUCKETING ALIAS, so renaming a lane moved records; objectui#10069 (ruling A) retired it and FLIPPED that row, so a record whose stored group value is the lane\'s TITLE is now swept into the trailing lane with a console warn. `cards` is a UNION and not a replacement — static lane cards survive the fetch, come first, and are not de-duplicated against it. `limit` is displayed beside the count and flags the lane, and ⛔ never truncates: the over-limit row asserts every card still renders, so a pin watching only the badge could not stay green under a renderer that dropped the overflow. `className` reaches that lane\'s container and no other. `collapsed` withholds that lane\'s cards while its neighbour keeps them — the member-set row only, because `columnCollapsedHonoured-9628.test.tsx` owns that member whole. The set itself is asserted as a WHITELIST: an undeclared lane member reaches no sink, with a `className` marker on the same lane in the same render as the lit control. ⚠️ The DECLARATION half is a different file and deliberately not duplicated here: `packages/types/src/__tests__/object-kanban-columns-declared-8913.test.ts` (objectui#8913/#8989) parses lane bags against both published faces, and every assertion in it is a `safeParse` — it cannot say what the board DOES with a member it admitted, which is objectui#8068\'s criterion. Read end to end before being cited. New file (objectui#8071 slice 16).',
24282428
},
24292429
'object-kanban.conditionalFormatting': {
24302430
file: 'packages/plugin-kanban/src/__tests__/ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx',

‎content/docs/api/schema-reference.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -935,7 +935,7 @@ A drag-and-drop Kanban board. The `object-kanban` type key validates the shape t
935935
|----------|------|-------------|
936936
| `objectName` | `string` | Object to fetch records from. |
937937
| `groupBy` | `string` | Field whose values become the lanes (maps to column ids). **Optional** since objectui#8990, matching `@objectstack/spec`. A board that omits it draws whatever lanes `columns` declares — and holds **no cards**, because records are only distributed once a lane key exists. |
938-
| `columns` | `string[] \| KanbanLane[]` | Swimlane definitions — an array of `{ id, title }` lanes (one per `groupBy` value), **or** an array of bare value strings; never a mix. **Not** a field projection (that is `cardFields`). A lane's `cards` is optional: an object-bound board buckets records into the lane by `groupBy`, and only a static board writes a lane's cards itself. |
938+
| `columns` | `string[] \| KanbanLane[]` | Swimlane definitions — an array of `{ id, title }` lanes (one per `groupBy` value), **or** an array of bare value strings; never a mix. **Not** a field projection (that is `cardFields`). A lane's `cards` is optional: an object-bound board buckets records into the lane by `groupBy`, and only a static board writes a lane's cards itself. A record lands in a lane when its stored `groupBy` value equals the lane **`id`** (the option value, compared case-insensitively); the lane `title` is display only and never decides membership (objectui#10069). A record matching no lane id is shown in a trailing *Uncategorized* lane and named in a console warning. |
939939
| `titleField` | `string` | Field used as the card title. |
940940
| `cardFields` | `string[]` | Fields rendered on each card. |
941941
| `filter` | `any[]` | Query filter, forwarded verbatim as `$filter`. |

0 commit comments

Comments
 (0)