Skip to content

Commit 5bdb2bc

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-lwzcd3
2 parents a51a062 + 1708115 commit 5bdb2bc

130 files changed

Lines changed: 6649 additions & 582 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@object-ui/types': patch
4+
'@object-ui/i18n': patch
5+
'@object-ui/react': patch
6+
'@object-ui/components': patch
7+
'@object-ui/plugin-detail': patch
8+
'@object-ui/app-shell': patch
9+
---
10+
11+
feat(console): a greyed action says the reason its author declared
12+
13+
Clause-②: no
14+
Metadata-protocol: none
15+
16+
An action greyed out by its `disabled` predicate now shows its declared `disabledReason` as the button's hover title and accessible description, and as a menu item's second line. The console used to show "Not available for this record" for every such action. That sentence stays the default when the action declares no reason, or a blank one.
17+
18+
```ts
19+
defineAction({
20+
name: 'submit_for_approval',
21+
label: 'Submit for approval',
22+
objectName: 'clm_contract',
23+
disabled: "record.legal_review_status != 'approved'",
24+
disabledReason: 'An approved legal review is required before the contract enters approval.',
25+
});
26+
```
27+
28+
- **Where it shows.** The record header (inline and in the ⋯ overflow), the record section bar, the declared actions bar, and the four `action:*` renderers (`action:button`, `action:icon`, `action:group`, `action:menu`). On every one, only the declared `disabled` predicate evaluated true earns a reason. A button greyed while its own action runs, or by the host, still gives none.
29+
- **Translated like the other action copy.** `useActionTextLocalizer` (`@object-ui/react`) resolves the key when the action declares it, through the new `actionDisabledReason` resolver on `useObjectLabel` (`@object-ui/i18n`). The bundle's sentence at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an action with no `objectName`) comes first. After it comes the authored inline locale map, collapsed to the active language, then the authored string. A bundle entry never gives a reason to an action whose author wrote none. The four `action:*` renderers read the action as authored, so for them the inline locale map is the translation.
30+
- **Typed.** `UIActionSchema` (`@object-ui/types`) declares `disabledReason`, derived from the spec's `ActionSchema`.
31+
- **Liveness.** `disabledReason` is now `live` in `@objectstack/spec`'s ledger, with the console readers as evidence. The planned-property warning an author got for declaring it is gone. The key's description no longer says that nothing shows it.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/rest': minor
4+
'@objectstack/runtime': minor
5+
'@objectstack/cli': minor
6+
'@objectstack/lint': minor
7+
'@object-ui/app-shell': minor
8+
'@object-ui/i18n': minor
9+
---
10+
11+
feat(spec,rest,cli): a doc book's title and section titles are translatable — `books.<name>` in the translation bundle
12+
13+
Clause-②: yes (widening)
14+
Metadata-protocol: added
15+
16+
A package that ships its docs in two languages showed a half-translated manual: each doc's body followed the reader's locale (`doc.translations`, `<name>.<locale>.md`), but the book title and every sidebar section title stayed in the authoring language. The book's own inline `translations` map was retired in 17.0.0 because nothing read it, and the translation bundle had no key for a book.
17+
18+
- **`@objectstack/spec`:** `TranslationData` (and the registered `translation` item and the platform bundle, which share its groups) gains `books`, keyed by the book's `name`: `label`, `description`, and `groups.<key>.label` for each group by its stable `key` (strict: any other key is refused; `name` / `title` point at `label`, and `sections` / `chapters` / `toc` point at `groups`; the singular group spelling `book` points at `books`). A new translator, `translateBook`, overlays them and is registered in the metadata-document dispatch, so `GET /meta/book` (list and item) is served in the request's locale. A key the bundle does not carry keeps the authored string. The rejection for the retired inline `translations` map on a book now names the bundle keys to move the strings to.
19+
- **`@objectstack/rest`:** `GET /meta/book/:name/tree` translates the book it resolves through the same dispatch before rendering the spine, so the tree, the book list and the console's docs sidebar agree. `MetaBookTreeSources` gains `resolveI18nService()`, which both transports supply.
20+
- **`@objectstack/runtime`:** the dispatcher's book-tree route supplies the same i18n service, so it answers the localized tree `RestServer` answers.
21+
- **`@objectstack/cli`:** `os i18n extract` scaffolds `books.<name>.label` / `.description` and `books.<name>.groups.<key>.label` from the stack's `books[]`, and `os i18n check` / `os lint` coverage report an untranslated one as `i18n/missing-book`.
22+
- **`@objectstack/lint`:** `translation-target-unknown` resolves `books.<name>` against the declared books (a sibling package's included, and a package id for its implicit book) and `books.<name>.groups.<key>` against that book's group `key`s, so a mistyped book name or group key is an error instead of a translation nothing reads.
23+
- **`@object-ui/i18n`:** the console's language loader recognises `books` as a translation group, so a served document that carries only book translations is namespaced like any other.
24+
25+
Example (`zh-CN` bundle of an English-authored package):
26+
27+
```ts
28+
books: {
29+
crm_manual: {
30+
label: '使用手册',
31+
groups: { start: { label: '快速上手' }, pipeline: { label: '销售管道' } },
32+
},
33+
},
34+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime)!: the package draft doors refuse a query parameter they do not read, so `publish-drafts?dryRun=true` no longer publishes for real
6+
7+
Clause-②: yes (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition, stored row or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is the answer two HTTP doors give a request that carries a query parameter they never read: it is refused instead of ignored. The remedy is a request edit (send no query parameters), never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
11+
12+
**BREAKING** accept-set narrowing at two runtime dispatcher doors: `POST /api/v1/packages/:id/publish-drafts` and `POST /api/v1/packages/:id/discard-drafts`.
13+
14+
- **Before.** Both doors read nothing from the query string and ignored whatever was on it. `POST /packages/:id/publish-drafts?dryRun=true` answered `200 outcome: "published"` and the drafts went live; the same parameter on `discard-drafts` dropped every pending draft. A caller asking for a preview got the irreversible act, answered like a success.
15+
- **Now.** Any query parameter on either door is refused `400 VALIDATION_ERROR` before anything is published or discarded. The message is the one every closed-query door answers (`unknownQueryParamRefusalMessage`, `@objectstack/rest`): it names each parameter it did not understand and says the door takes none. The capability gate still answers first, so a caller without `manage_metadata` keeps its `403`.
16+
- **Unchanged.** A call with no query parameters, which is how the Console, `MetadataClient.publishPackageDrafts` and the client SDK's `packages.publishDrafts` / `packages.discardDrafts` call both doors. The body (`actor`, `backfill`) is read as before.
17+
- **Who sees it.** A script, tool or AI agent that adds a query parameter to either door now gets `400` instead of a publish or a discard. Neither door has a dry run; a preview of what a publish would do is not offered yet.
18+
19+
**The remedy.** Send the request without query parameters.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql)!: a `deleteBehavior: 'restrict'` reference refuses the delete of the record it protects even when the referencing row would be cascade-deleted by that same delete (#26718)
6+
7+
Clause-②: no (narrowing) — a by-id delete (`delete` by id, `DELETE /api/v1/data/:object/:id`, and the bulk `deleteManyData` door, which deletes by id row by row) refuses with 409 `DELETE_RESTRICTED` where it succeeded: when a row the delete would cascade-delete only through the record it restricts holds an authored `restrict` reference to that record. Nothing is widened.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: an authored deleteBehavior restrict is now honoured on a row that also cascades from the record it protects. The refusal is the existing DELETE_RESTRICTED, so there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the by-id delete (`@objectstack/objectql`), served by every delete door that deletes by id.
13+
14+
- **Before.** A child holding a `master_detail` and a `deleteBehavior: 'restrict'` lookup to the same parent row: deleting the parent answered success, the cascade deleted the child, and the restrict was never asked. The same held for a grandchild under that parent holding the restrict. The delete collects every record its cascade removes first, and judged a `restrict` only against rows outside that set; the child is in the set by its own master_detail.
15+
- **Now.** Before the first write, the delete judges every authored `restrict` (on a `lookup`, a `tree` or a `master_detail`) held by a row it would remove only because the record that restrict protects is removed. Such a row refuses the delete with the existing `409 DELETE_RESTRICTED`, naming the restricting object (`dependentObject`) and field, and nothing is deleted. The answer no longer depends on the order the two fields are declared in.
16+
- **Unchanged.** A restrict held by a row outside the delete's cascade still refuses, as before. A row the cascade reaches through another record is still exempt: deleting an account still deletes a contract and the contact the contract restricts when both are details of that account. A required lookup's `set_null`, which is refused as `restrict` because it cannot be cleared, still steps aside for a row the cascade deletes. A plain `master_detail` with no restrict still cascades, and `set_null` still clears.
17+
- **Who sees it.** An app with a detail that also restricts its own master (directly or from further down the cascade): deleting that master used to destroy the detail, and is now refused while the detail references it.
18+
19+
**The remedy.** Delete the restricting detail (or clear its restrict reference) first, then delete the parent. If the detail is meant to go with its parent, drop the `restrict` from the second reference.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: a list view's chart bound to a dataset over another object is refused (`list-view-chart-dataset-foreign-object`)
6+
7+
Clause-②: no (narrowing) — a list view whose `chart.dataset` names a dataset declared over another object than the one the list view lists is refused at publish and fails `os validate` / `os build`; it saved and published before. Nothing is widened.
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: a new author-time rule refuses one combination of two existing references. There is no mechanical rewrite to prescribe, because only the author knows whether the chart belongs on the dataset's object, on a dashboard, or needs a dataset over the list's own object. -->
11+
12+
**BREAKING** accept-set narrowing in `validateListViewFieldRefs` (`@objectstack/lint`), served by the runtime save door (`PUT /api/v1/meta/view`, `PUT /api/v1/meta/object`) and by `os validate` / `os build`.
13+
14+
- **Why.** A list view's chart draws the rows of its dataset's base `object`: the list hands the dataset query the dataset name, its dimensions and its measures and nothing else, so neither the list's object nor its filter reaches the query. A chart under one object's list bound to a dataset over another object plotted that other object's records, labelled as a view of the first.
15+
- **Before.** Such a list view saved and published `200` with no advisory: measured with a view item and a view container written into one package and bound to a dataset over another package's object. No rule compared the two objects on any list-view shape.
16+
- **Now.** It is reported once per list view, at `chart.dataset`, as `list-view-chart-dataset-foreign-object` (`error`). The verdict names the view, the dataset and both objects. The publish is refused `422 INVALID_METADATA`; a draft save is not gated, as for every author-time rule.
17+
- **A dataset that reaches the list's object through a relationship is refused too.** `include` lists to-one relationships from the dataset's base object, so it adds columns to group by and never a second set of rows: a dataset over opportunities that includes their account still counts opportunities.
18+
- **Not judged.** A dataset that resolves to nothing (`chart-dataset-unknown`), a dataset that declares no `object`, and a list view whose own binding does not resolve.
19+
- **Who moves.** No list view in `examples/**` or `packages/platform-objects` is refused: the two chart list views in the showcase app bind datasets over their own objects.
20+
21+
**The remedy.** Bind a dataset declared over the list view's own object, or move the chart to a list view of the dataset's object or to a dashboard widget naming the dataset. Which one is the author's call, so nothing rewrites it for you.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): the metadata save door stores an action as its `ActionSchema` parse, so a declared `requiresFeature` / `requiresMembershipReach` is lowered into `visible`
6+
7+
Clause-②: no — the save door refuses nothing it accepted before. It already judged the body by the same parse; what changes is only what it stores for an action that passed.
8+
9+
Metadata-protocol: none
10+
11+
`PUT /api/v1/meta/object/NAME` (draft and active) and a standalone `action` save judged an action by `ActionSchema`, then stored the request body as written. `ActionSchema`'s parse lowers `requiresMembershipReach` and `requiresFeature` into `visible`, and the console reads `visible` alone, so the stored action kept both gate keys raw, had no `visible`, and its button was offered to every user.
12+
13+
Now the door stores the parse it already ran:
14+
15+
- an object's own `actions` array is stored as the parsed array; every other key of the object body stays as written;
16+
- a standalone `action` item is stored as the parsed action.
17+
18+
A stored action therefore carries the lowered `visible` (and its schema defaults, and a CEL shorthand wrapped in `{ dialect, source }`), the same shape a code package's action has been stored in since #25981. The parse is idempotent over its own output, so a GET then PUT round trip stores the same bytes.
19+
20+
Rows already saved before this fix are not rewritten. Saving the object (or action) again stores it lowered.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
fix(spec)!: an object that declares one action name twice is refused at every door that parses it, and Studio never writes the pair
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: narrowed
10+
11+
**BREAKING** for an object whose `actions` array repeats a name.
12+
13+
- **Before.** The object save door accepted two entries of one object's `actions` under one name: `PUT /api/v1/meta/object/X?mode=draft` answered 200, the publish answered 200, and both were served. An action name is the key the action runs under on its object, so which declaration ran was undefined. `defineStack` and `os validate` already refused the pair inside a stack; the save door parses an object alone and did not.
14+
- **Now.** `ObjectSchema` refuses the pair, so every door that parses an object refuses it: `defineStack`, `os validate`, `os build` and the object save door (422 `INVALID_METADATA`). One issue per repeated name, at `actions[j]` of its second declaration, naming the name and every index: `Object 'X' declares the action 'Y' twice in actions: actions[i] and actions[j]. …`. Inside a stack the pair is now refused by the object parse (`STACK_SCHEMA_INVALID`) rather than by the cross-reference pass's duplicate action-key check, which keeps the pairs that span two collections (a bound standalone action beside an embedded one, two standalone actions).
15+
- **Not changed.** Distinct names, the same name on two different objects, and a standalone action bound to an object by `objectName` (judged by `defineStack` for one stack and reported by the runtime's bound-action collision warning across packages).
16+
- **Fix.** Rename all but one of the colliding entries (and every button, flow and caller that runs the renamed one by name), or delete the duplicate.
17+
- **Stored rows.** A stored object that repeats a name still loads, under a `[metadata_spec_invalid]` diagnostic carrying the refusal, and its next save is refused. `os migrate meta --stored` lists each such object under the TODO id `object-duplicate-action-name-refused`; nothing rewrites it, because which declaration keeps the name only the owner knows.
18+
- **Studio** (`@object-ui/app-shell`). New already derived a name no listed action held; renaming an action in the editor's Name field to a name another action of the object holds now writes the first free `<name>_N` instead of the duplicate.
19+
20+
<!-- adr-0087: registered object-duplicate-action-name-refused -->
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): Studio's action editor keeps a new field-value row, and stores a script action's type
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Two gaps in Studio's action editor (Data › an object › Actions) left a published action that does nothing:
11+
12+
- **Add field value** on an "Update fields on this record" action added no row, so `patch` stayed `{}` and the action could not be given a field to set. The row now appears at once and is held in the editor until it names a field; from then on it is written to `patch`. A row whose field is cleared stays on screen, held, and leaves `patch`. A row that names no field is never written.
13+
- **Run a script** left the action without `type`, while the Type control read Script. The console runs nothing for an action without `type`, so the button sent no request. Every edit that authors a script (the "What it does" switch, the body, its language, a field value) now stores `type: 'script'` on an action that has none, and the Type control shows only a type the action holds: "—" until then. An action that already has a `type` is written exactly as before.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): in Studio, selecting an element while the right panel is hidden unfolds the panel on its properties
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
With the right panel hidden from the top bar, selecting a flow node in Automations (or a block in Interfaces, or a field in Data) showed its configuration nowhere on a desktop-width screen: only a phone reopened the panel. A builder had to find the toggle again before they could edit the selected step.
11+
12+
What changes:
13+
14+
- A new selection now unfolds a hidden panel at every width, on the selected element's properties, the way `⌘.` unfolds it. The panel's shown state is remembered as when you show it from the toggle.
15+
- Hiding the panel again while the same element stays selected keeps it hidden. Only another selection unfolds it.
16+
17+
What to do: nothing. Nothing a flow, page or object declares changes.

0 commit comments

Comments
 (0)