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
11 changes: 11 additions & 0 deletions .changeset/22682-position-form-permission-sets-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@objectstack/spec": patch
---

`positionForm`: the `permissionSets` row now declares `widget: 'ref-multi:permission'` in place of `type: 'tags'`. ADR-0131 D4 resolves a reference to a declared item by machine name, registry-first, so the row names the permission-set registry (metadata type `permission`) for the designer to offer the declared sets and commit their names, instead of a free-text tag box where a name that does not exist can be typed.

Clause-②: no

- **The key.** `ref-multi:` followed by a metadata type is the list form of the forms' `ref:` pickers (`ref:object`, `ref:dataset`, `ref:component`, all single-valued). A distinct key, as `field-ref` / `field-multi` already are, because a single `ref:` picker writes one string and the widget registry declares one labelling per key.
- **Which renderer honours it.** A console whose metadata-admin widget registry has no `ref-multi:permission` entry renders this row as its announced fallback: a JSON editor with the note that no custom renderer is registered. The stored value is the same list of names either way.
- ⛔ Only this one form row changes. No schema, parse, export or accept-set change: `PositionSchema.permissionSets` stays a list of snake_case names, and `FormField.widget` stays a free string.
20 changes: 20 additions & 0 deletions .changeset/console-4997995022f8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@objectstack/console": minor
---

Console (objectui) refreshed to `4997995022f8`. Frontend changes in this range:

Derived from the changesets objectui declared over the range — 5 releasing of 5 changesets added across 7 non-merge commits; omitted: 2 commits carrying no changeset (they ship no package code).

- **minor** — A Studio form field that declares the `ref-multi:permission` widget now renders a picker of the declared permission sets instead of the JSON editor fallback (objectui#12126). (objectui `499799502`)
- **minor** — The Marketplace's install answers now say what the runtime served (objectui#12098). (objectui `107139379`)
- **minor** — A paged grid's column footer reads the whole matched set, or says it reads the page (objectui#12081 item 3). (objectui `69bca0af6`)
- **patch** — A boolean field whose `defaultValue` is a CEL expression or a runtime token now gets that default on create (objectui#12121). (objectui `c9fc10b1e`)
- **patch** — More write affordances read the affordance-to-grant map, so a caller without the grant is no longer offered a write the server then refuses (objectui#12082, leg A of the write-cen… (objectui `752504520`)

**In this console build, declared nowhere** — objectui merged 2 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387.

- _(no changeset)_ docs: stop teaching keys the strict authoring face refuses (objectui#12131, phase 1) (#12134) (objectui `66eb5604e`)
- _(no changeset)_ fix(examples): repair the class (i) keys the strict authoring face refuses (objectui#5250 slice C) (#12128) (objectui `802f0bc41`)

objectui range: `0df67f237c5a...4997995022f8`
18 changes: 18 additions & 0 deletions .changeset/objectui-pin-citations-4997995022f8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/spec': patch
---

The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `4997995022f8`.

Clause-②: no

Every anchor was mapped through the objectui diff `0df67f237c5a..4997995022f8`: 80 paths over 7 commits, none deleted and one renamed (a schema-catalog example, which no record cites). Three files a record cites changed on the hop, plus the `en` / `zh` / `de` packs (whose `calendar.configRequired` strings did not change): `ObjectGrid.tsx` (objectui#12081 item 3 and objectui#12082), `ObjectGantt.tsx` (objectui#12082) and `form.tsx` (objectui#12121). Every cited line in them moved or held with its text byte-identical, and each record that cites one is re-pointed with a hop sentence that says by how much. The records that cite only byte-identical files gain a hop sentence that says so.

Two readings sit beside a cited read, and each hop sentence says what it found:

- `ObjectGrid.tsx`: a bulk `update` def is now withheld unless the edit verdict allows it (objectui#12082), which narrows the declared defs and adds no implicit one, so the `sys_api_key` reading holds. The selection block still hashes to `c88443302d40`.
- `ObjectGantt.tsx`: the chart's in-chart write handlers are withheld from a caller without the grant (objectui#12082). No `schema.*` read moved; the renderer reads the same 49 names at both pins.

The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that reproduces every `0df67f237c5a` number. The corpus is now 8441 tracked files. Every token an entry counts as zero still reads zero (96 tokens; `Span` and `SpanSchema` read 517 and 57 at both pins, as the tracing entry records). The controls moved with the corpus, for example `objectstack` from 18248 to 18271 and `timeout` from 1705 to 1704.

No key, default, enum member or export moves.
2 changes: 1 addition & 1 deletion .objectui-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0df67f237c5ab46cbc65252c5f6bd05f2be44c48
4997995022f8e65946f6362b7434685539cc72b7
4 changes: 2 additions & 2 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Column footer summary configuration
| **required** | `boolean` | optional | Required override |
| **hidden** | `boolean` | optional | Hidden override |
| **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. |
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `0df67f237c5a`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `4997995022f8`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
| **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) |
| **language** | `string` | optional | Code editor language (for type=code) |
| **keyField** | `{ field?: string; label?: string \| Record<string, string>; placeholder?: string \| Record<string, string>; helpText?: string \| Record<string, string>; … }` | optional | Key column config for record-typed fields |
Expand Down Expand Up @@ -343,7 +343,7 @@ Form-view select option — the object-field option shape minus the per-option `
| **required** | `boolean` | optional | Required override |
| **hidden** | `boolean` | optional | Hidden override |
| **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. |
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `0df67f237c5a`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `4997995022f8`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
| **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) |
| **language** | `string` | optional | Code editor language (for type=code) |
| **keyField** | `{ field?: string; label?: string \| Record<string, string>; placeholder?: string \| Record<string, string>; helpText?: string \| Record<string, string>; … }` | optional | Key column config for record-typed fields |
Expand Down
6 changes: 3 additions & 3 deletions packages/sdui-parser/objectui-lockstep.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
],
"objectui": {
"repo": "https://github.com/objectstack-ai/objectui.git",
"rev": "0df67f237c5ab46cbc65252c5f6bd05f2be44c48",
"revDate": "2026-10-11T07:26:10+00:00",
"rev": "4997995022f8e65946f6362b7434685539cc72b7",
"revDate": "2026-10-11T10:56:33+00:00",
"source": "packages/sdui-parser/src",
"files": [
"packages/sdui-parser/src/body-dialect.ts",
Expand All @@ -21,7 +21,7 @@
"packages/sdui-parser/src/validate.ts"
]
},
"recordedAgainstPin": "0df67f237c5ab46cbc65252c5f6bd05f2be44c48",
"recordedAgainstPin": "4997995022f8e65946f6362b7434685539cc72b7",
"grammarRegion": {
"file": "packages/sdui-parser/src/parse.ts",
"delimiter": "/* ---------------------- the JS literal subset (#6614) ---------------------- */",
Expand Down
19 changes: 18 additions & 1 deletion packages/spec/src/data/api-methods-batch-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,24 @@ const SINGLE_RECORD_WRITE_ONLY: Record<string, string> = {
// `revoked` on ONE key. The multi-select surface this rule protects does not
// exist for API keys, and the shape a future one would take does not need
// `bulk` either — both read off the console build this release pins
// (`.objectui-sha` = `0df67f237`, re-read there 2026-10-11:
// (`.objectui-sha` = `499799502`, re-read there 2026-10-11:
// in `packages/plugin-grid`, on the hop off `0df67f237`, `ObjectGrid.tsx`
// changed again (89 insertions, 44 deletions: objectui#12081 item 3's
// whole-set footer and objectui#12082 leg A), none of it inside the
// selection block, which only MOVED by +84: `ObjectGrid.tsx:4981-5008` here
// (`4897-4924` at `0df67f237`), still hashing to
// `c88443302d40c2db739ddb235470bafa29056e2e` (hash-object of the block),
// re-READ with the same reading below. Two hunks sit beside that reading and
// were re-READ as well: objectui#12081 item 3 moved the `bulkFanoutParams`
// and `resolvedTotalMatching` declarations, their code byte-identical, above
// the footer's hook; and objectui#12082 now also withholds a bulk `update`
// def unless the edit verdict (`rowEditVerdict`) allows it, which narrows the
// declared defs and adds no implicit one. The `delete` verdict line MOVED
// `:1786` -> `:1787` and the promoted def's params `:5097-5116` ->
// `:5165-5184` (called at `:5132` -> `:5200`), each byte-identical.
// `hooks/useBulkExecutor.ts` is byte-identical to `0df67f237` and its
// `:298-303` still hashes to `01083348330f10a201cdf1078b4c21c236402b6a`. At
// `0df67f237`, re-read there 2026-10-11:
// in `packages/plugin-grid`, on the hop off `20c6d351a`, `ObjectGrid.tsx`
// changed again (82 insertions, 32 deletions: objectui#12082's
// affordance-to-grant map, objectui#12103's per-column write grant and
Expand Down
8 changes: 7 additions & 1 deletion packages/spec/src/identity/position.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ export const positionForm = defineForm({
{ field: 'description', colSpan: 2 },
// [ADR-0131 D3/D4] The sets a holder of this position holds, by name —
// what the authorization resolver reads (the junction rows grant nothing).
{ field: 'permissionSets', type: 'tags', colSpan: 2, helpText: 'Permission sets this position distributes, by name (snake_case).' },
// D4 resolves a reference registry-first, so the row binds the
// permission-set registry (metadata type `permission`) rather than a
// free-text tag box: `ref-multi:<type>` is the list form of the
// `ref:<type>` pickers (`ref:object`, `ref:dataset`), offering the
// declared names and committing each pick's name. An explicit widget
// outranks type inference, so the row declares no `type`.
{ field: 'permissionSets', widget: 'ref-multi:permission', colSpan: 2, helpText: 'Permission sets this position distributes, by name (snake_case).' },
// #19331 — `delegatable` is declared by PositionSchema and was offered by
// no control, so the only door to an ADR-0091 D3 self-service delegation
// was the Source tab's free-text JSON. A boolean node takes the boolean
Expand Down
42 changes: 42 additions & 0 deletions packages/spec/src/identity/position.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, it, expect } from 'vitest';
import { PositionSchema, type Position } from './position.zod';
import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas';
import { PermissionSetSchema } from '../security/permission.zod';
import { METADATA_FORM_REGISTRY } from '../system/metadata-form-registry';

describe('PositionSchema', () => {
describe('Basic Properties', () => {
Expand Down Expand Up @@ -285,3 +288,42 @@ describe('unknown keys are rejected, not stripped', () => {
expect(parsed._lock).toBe('full');
});
});

// [ADR-0131 D4] "References to declared items are by machine name, resolved
// registry-first." The position form is the one authoring surface for the
// position → permission-set binding, so its `permissionSets` row names the
// permission-set registry picker rather than a free-text tag box. The form is
// read through the registry `/meta/types` serves, so a parse that dropped
// `widget` would turn this red too.
describe('the position form picks `permissionSets` from the permission-set registry', () => {
type Row = { field?: string; widget?: string; type?: string };
const rows = (): Row[] =>
((METADATA_FORM_REGISTRY.position?.sections ?? []) as Array<{ fields?: Row[] }>)
.flatMap((s) => s.fields ?? []);
const rowFor = (key: string) => rows().filter((r) => r.field === key);

it('CONTROLS: the row probe finds a known row and reports a known absence', () => {
expect(rowFor('name'), 'lit control: the position form has always offered `name`').toHaveLength(1);
expect(rowFor('noSuchKeyAtAll'), 'dark control: a key the form does not offer').toHaveLength(0);
});

it('offers `permissionSets` once, as the `ref-multi:permission` picker', () => {
const [row, ...extra] = rowFor('permissionSets');
expect(extra, 'one row only').toEqual([]);
expect(row?.widget).toBe('ref-multi:permission');
});

it('the registry type the row names is the permission-set catalog', () => {
const [row] = rowFor('permissionSets');
const registryType = row?.widget?.split(':')[1];
expect(registryType).toBe('permission');
expect(getMetadataTypeSchema(registryType!)).toBe(PermissionSetSchema);
});

it('the multi-value key sits on a list: `permissionSets` refuses a bare name', () => {
const result = PositionSchema.safeParse({ name: 'auditor', label: 'Auditor', permissionSets: 'read_all' });
expect(result.success).toBe(false);
const issue = result.error!.issues.find((i) => i.path.join('.') === 'permissionSets');
expect(issue?.code).toBe('invalid_type');
});
});
Loading
Loading