Skip to content

Commit 829aec1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-g48t33
2 parents 12b1047 + 1287b2f commit 829aec1

403 files changed

Lines changed: 20048 additions & 3183 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
docs(i18n): the `studio` namespace is the designer strings' only home
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Last slice of the move ruled on objectstack#25922** (6431032267, Q2 = A; kept by 6431234244). `@object-ui/app-shell`'s private designer table, empty since the previous slice, is deleted, and its `t()` / `tOptional()` read `@object-ui/i18n/namespaces/studio` alone. Every string reads the same as before in every language. The README and the namespace's doc comments now say so, and point at `src/__tests__/studio-namespace.test.ts` as the namespace's one parity check, since the paired-table gate `check:i18n-designer-parity` is retired with the table. No export or string changes.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@object-ui/components': minor
4+
'@object-ui/types': minor
5+
'@object-ui/plugin-detail': minor
6+
---
7+
8+
feat(spec)!: the action blocks spell the destructive variant `danger`, the word `ActionSchema.variant` uses
9+
10+
An action's variant was spelled in two dialects. `ActionSchema.variant`, the declaration the action runner dispatches, takes `primary | secondary | danger | ghost | link`. The four action blocks (`action:button`, `action:icon`, `action:group`, `action:menu`) and the group/menu members took the Button primitive's `default | destructive | outline | secondary | ghost | link` (plus `primary` on the button, the icon and the members) and refused `danger`. So one destructive action was `danger` as a declaration and `destructive` as a block, and each block renderer translated between the two.
11+
12+
The block rows now take `danger` in place of `destructive`: `default | danger | outline | secondary | ghost | link`, plus `primary` where it was accepted before. `danger` draws the destructive button (a red item in a dropdown or `action:menu`) and asks a `confirmText` question through the destructive confirm button. `destructive` is refused at `variant` with the rename. Other components that draw the Button primitive directly (`element:button`, toasts, alerts, badges and the like) keep `destructive`, the primitive's own word, and are not touched.
13+
14+
The four block renderers forward `danger` to the runner as it is and draw it as the primitive's `destructive`. They no longer read `destructive`: stored pages are rewritten by the conversion below, so no renderer keeps a bridge for the old word. `UIActionSchema.variant` (`@object-ui/types`) follows the rows by reference. The record detail views' Delete entry in the `⋯` menu is spelled `danger` too.
15+
16+
This replaces the variant sentences of the #26665 entry in this release: the action blocks no longer translate `destructive` to `danger` at their forward to the runner, and `danger` is the spelling they accept (`UIActionSchema.variant` included). That entry's confirm-dialog behaviour is unchanged: a destructive block with a `confirmText` asks through the destructive confirm button, now when it is declared `variant: 'danger'`.
17+
18+
Stored rows and older built artifacts: the ADR-0087 D2 conversion `page-action-block-variant-destructive-to-danger` rewrites `variant: 'destructive'` to `'danger'` on the four blocks' `properties.variant` and on each `action:group` / `action:menu` member's `variant`, at every position a page nests a component (regions, slots, nested containers). It renames an `action:bar`'s own `variant` and the `variant` of each `actions` / `systemActions` member as well. The bar has no props row, so nothing refuses `destructive` on it, but it draws each member through the block renderers, which no longer read that word. A stored `page` row is rewritten when it is rehydrated, so it keeps drawing the destructive button and confirming through the destructive confirm button. The conversion is retired from the load path, so a source that still writes `destructive` on a block is refused, not rewritten. The D3 entry `ui-page-action-block-variant-destructive-refused` carries the judgment for `os migrate meta`.
19+
20+
Clause-②: no (narrowing) — the action block rows refuse `destructive`, a value they used to accept. `danger` takes its place on the same rows.
21+
22+
Metadata-protocol: narrowed (`variant: 'destructive'` is refused on the action blocks and their members; write `danger`)
23+
24+
<!-- adr-0087: registered page-action-block-variant-destructive-to-danger, ui-page-action-block-variant-destructive-refused -->
25+
26+
**BREAKING** for an author who wrote `variant: 'destructive'` on an action block.
27+
28+
**`destructive` → `danger`.**
29+
30+
- **Before.** `{ type: 'action:button', properties: { label: 'Purge', actionType: 'script', variant: 'destructive' } }` parsed; `variant: 'danger'` was refused.
31+
- **Now.** `destructive` is refused at `variant` (or at `actions.N.variant` on a group or menu member) with a message naming the rename.
32+
- **Fix.** Write `variant: 'danger'` on the block or member. Nothing else changes: it draws the same destructive button. `os migrate meta --from 17 --write` applies the rename to your sources.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
'@object-ui/app-shell': patch
5+
'@object-ui/i18n': patch
6+
---
7+
8+
feat(app-shell): a key-only record control, and the Studio hook form offers `refusals` with it
9+
10+
Clause-②: no
11+
Metadata-protocol: none
12+
13+
A hook built in Studio can now declare its refusal catalogue. The hook form shows a **Refusals** row beside the body: add a code, write the message the user reads and an optional 4xx status, rename or remove a code. The saved body is exactly `refusals: { CODE: { message, status? } }`, the shape `HookSchema` accepts, so a hook made in Studio no longer has to be edited as code to refuse in translatable text.
14+
15+
- **`@object-ui/app-shell`.** The metadata form's `record` editor gains a key-only face. A `type: 'record'` row that declares `widget: 'key-only-record'` edits a map of closed entries (add, rename, remove, reorder, edit each entry's fields) and never writes the key into the entry. A key is checked against the record's served key rule (`propertyNames.pattern`, derived from the spec), so a code like `clean_version` is refused before it reaches the draft. A rename moves the entry in place once the new key is valid. A record row with no such widget behaves as before: the key is still mirrored into each entry.
16+
- **`@objectstack/spec`.** `hookForm` offers `refusals` with that control. `HookSchema` is unchanged.
17+
- **`@objectstack/platform-objects`.** The generated metadata-form bundles carry the three new hook-form rows, translated in zh-CN, ja-JP and es-ES.
18+
- **`@object-ui/i18n`.** One new Studio string, `engine.form.key` (en, zh), which names the key column when the form declares no key label.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/components': minor
3+
'@object-ui/plugin-form': minor
4+
'@object-ui/console': minor
5+
---
6+
7+
fix(ui): a form view's `layout: 'horizontal'` draws each label beside its field, a tabbed form keeps it, and the form page draws `type: 'tabbed'` as tabs (#26843)
8+
9+
Clause-②: no
10+
Metadata-protocol: semantic
11+
12+
A form view's `layout: 'horizontal'` reached every console form but drew nothing different, and the form page ignored `type: 'tabbed'`:
13+
14+
- **`layout: 'horizontal'`** now places each field's label in a fixed column beside its control from the `sm` breakpoint up; below `sm` (a phone) the field stays stacked, label above control, as `vertical` draws it. The description and validation message stay under the control. `vertical`, and a form view with no `layout`, draw exactly as before. Before, the form renderer read the key only to align its action row.
15+
- **A tabbed form** (`type: 'tabbed'`) now keeps an authored `layout: 'horizontal'` on every tab. It used to draw its fields `vertical` whatever the form view said.
16+
- **The form page** (`/forms/:name`, and the public `/f/:slug`) now draws a `type: 'tabbed'` form view as one tab per section: `defaultTab` opens first, `tabPosition` places the strip, and a section hidden by its `visibleWhen` draws no tab. A submit carries every section's fields, the closed tabs' included, and a submit refused for a missing field opens the tab that holds it. A tabbed form view with a single section draws it stacked, as before. The page also draws `layout: 'horizontal'` rows. Before, it drew every section stacked and every label above its field.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: an action's `newTabUrl` is a path on this backend and must start with `/`; an absolute URL is refused at parse
6+
7+
`newTabUrl` is the zero-roundtrip target of the pre-opened-tab flow (`opensInNewTab: true`): on click the console drives the pre-opened tab to the backend origin + `newTabUrl`, with `{recordId}` substituted, and posts nothing. Its JSDoc called it a path template, while its `.describe()` and the action form's help text called it a URL template, and nothing refused an absolute value. So `newTabUrl: 'https://example.com/export/{recordId}'` parsed clean and opened a tab at `https://BACKEND/https://example.com/export/…`. `ActionSchema.newTabUrl` now refuses any value that does not start with `/`, and the JSDoc, `.describe()` and the form help (all four form-translation locales) say the same thing: a path on this backend.
8+
9+
Clause-②: no (narrowing) — `ActionSchema.newTabUrl` (every action door: `defineAction`, the `action` metadata type, an object's `actions[]`) refuses a value that does not start with `/`. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (an action's `newTabUrl` may no longer be an absolute URL or a relative path without a leading `/`)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: newTabUrl keeps its name, shape and meaning, and now refuses the values that never opened the page they named. No backend path can be derived from an external URL, so there is no mechanical rewrite; a stored action row holding one loads unchanged, is counted invalid with a [metadata_spec_invalid] boot line and still registered, is served with _diagnostics locating newTabUrl, and its next save is refused until the author rewrites it. -->
14+
15+
**BREAKING** accept-set narrowing on `ActionSchema.newTabUrl` (`@objectstack/spec`).
16+
17+
- **Before.** `newTabUrl: 'https://example.com/export/{recordId}'` (or `sso-open/{recordId}`) parsed, and the console opened the backend origin + that text: a broken tab.
18+
- **Now.** The parse refuses it at `newTabUrl` with: `` `newTabUrl` is a path on this backend and must start with `/` `` and names the external-page remedy.
19+
- **Unchanged.** A path template such as `/sso-open/{recordId}` or `/api/v1/invoice/{recordId}/pdf` parses byte-identically. No renderer changes: the console already prefixes the backend origin, so the declared meaning and the behaviour now agree.
20+
- **A row stored before this release.** Nothing converts it. It loads unchanged and is still registered, counted `invalid` with a `[metadata_spec_invalid]` boot line; the metadata read serves it with `_diagnostics` locating `newTabUrl`; it keeps opening the same broken tab; and its next save is refused until the value is a path.
21+
- **Who sees it.** An author who wrote an absolute URL in `newTabUrl`. No example, template, fixture or platform object in this repository does.
22+
23+
**The remedy.** For an endpoint on this backend, write its path: `newTabUrl: '/sso-open/{recordId}'`. For an external page, drop `opensInNewTab` and `newTabUrl` and declare a `type: 'url'` action instead: `target: 'https://example.com/export/${ctx.recordId}'`, `openIn: 'new-tab'`.
24+
25+
Also in `@objectstack/spec`, prose only, no accept-set change:
26+
27+
- The `DashboardWidget` docblock and `requiresObject`'s `.describe()` say that, outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 `PERMISSION_DENIED`) is left out of the dashboard for that viewer, and that `requiresObject` stays an install gate, never a viewer gate.
28+
- Three shipped citations of `content/docs/concepts/north-star.mdx` "§8" / "§6.3" (sections that page never had; the page is now `concepts/architecture-overview`) are corrected: `expression.zod.ts` cites ADR-0032 ("one CEL language"), `environment-artifact.zod.ts` cites the architecture overview's "Environment Artifact" section, and `hook-body.zod.ts` drops a pointer to a heading that does not exist. The generated references follow.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the action form's `newTabUrl` help text says it is a path on this backend starting with `/`, in all four locales (en, zh-CN, ja-JP, es-ES), and names the `type: 'url'` action for an external page — matching the spec, which now refuses an absolute `newTabUrl`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/formula': minor
3+
---
4+
5+
feat(formula)!: the mail hole grammar refuses a hole whose formatter the whitelist does not name, by name, listing the known formatters
6+
7+
Clause-②: no (narrowing) — `judgeHoleFormatter` gains a third rule, `known-formatter`: a hole that names a formatter `TEMPLATE_FORMATTERS` does not hold (`{{ amount | curency:EUR }}`, `{{ due | dat:long }}`) is refused, so `parseMailHole` carries the refusal and `judgeMailHoleGrammar` reports the hole, where both accepted it before. The in-app dialect, which already refused an unknown formatter in its own parse, now also refuses an inherited object key it used to call as a formatter (`{{ x | toString }}`, which printed `[object Object]`). No accept set is widened and no export is added or removed; `TemplateHoleGrammarRule` gains the member `'known-formatter'`.
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 and no export is removed: a hole whose formatter never formatted anything is now refused where the template is judged. A conversion cannot know which formatter the author meant, and the refusal names the hole and lists the known formatters, so there is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** accept-set narrowing in the shared hole grammar.
13+
14+
- **Before.** `parseMailHole(' amount | curency:EUR ')` answered a hole with no refusal, and the mail renderer printed the raw value for it (`1234.5`, an ISO timestamp), because `formatValue` knows no such formatter.
15+
- **Now.** The hole carries a `known-formatter` refusal whose sentence names the hole and lists `TEMPLATE_FORMATTERS`. Text after the `|` that is not a single word (`{{ x | two words }}`) still names no formatter and is left as written. A chained hole is still refused by the one-formatter rule first.
16+
- **In-app.** `templateEngine.compile` keeps its own sentence for an unknown formatter. It now also refuses `toString`, `constructor` and the other inherited object keys, which it used to accept and run as formatters.
17+
- **Who sees it.** A template whose hole misspells a formatter or names one that does not exist. No template shipped in this repository does.
18+
19+
**The remedy.** Correct the formatter's name to one the refusal lists, or format the value in the caller that supplies the data and pass the finished text as its own variable.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
feat(lint)!: `email-template-hole-grammar` refuses an email template hole whose formatter the whitelist does not name, naming the hole and listing the known formatters
6+
7+
Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door (`422 INVALID_METADATA`) refuse an `email_template` whose `subject`, `bodyText` or `bodyHtml` writes a hole naming a formatter `TEMPLATE_FORMATTERS` does not hold (`{{ amount | curency:EUR }}`, `{{ due | dat:long }}`), which they accepted before. No accept set is widened, and no export or rule id is added or removed.
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 hole whose formatter never formatted anything (the renderer printed the raw value) is now refused where it is authored. A conversion cannot know which formatter was meant, and the refusal names the hole and lists the known formatters, so there is no key for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at every authoring door for `email_template`.
13+
14+
- **Before.** `{{ amount | curency:EUR }}` passed every gate, and the email went out with `1234.5` where money was meant. The in-app template dialect already refused such a hole; mail did not.
15+
- **Now.** The rule refuses it at `error`, naming the hole, the face and the template, and listing the known formatters. The judgement is `@objectstack/formula`'s `judgeMailHoleGrammar`, the grammar the mail renderer parses with.
16+
- **Who sees it.** A template whose hole misspells a formatter or names one that does not exist. No template shipped in this repository does.
17+
18+
**The remedy.** Correct the formatter's name to one the finding lists, or format the value in the caller that supplies the data and pass the finished text as its own variable.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/plugin-email': minor
3+
---
4+
5+
feat(plugin-email)!: a mail template hole whose formatter the whitelist does not name fails the render with `TEMPLATE_INVALID` instead of printing the raw value
6+
7+
Clause-②: no (narrowing) — `sendTemplate` and `renderTemplate` refuse a stored template whose `subject`, `body_text` or `body_html` writes a hole naming a formatter `TEMPLATE_FORMATTERS` does not hold (`{{ amount | curency:EUR }}`), which rendered the raw value before. No accept set is widened, and the package adds no export.
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 and no export changes: the renderer refuses a hole the authoring rule refuses, for a template that reached it without being judged. A conversion cannot know which formatter was meant, so there is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** a render that printed a raw value where a formatted one was meant now fails.
13+
14+
- **Before.** `{{ amount | curency:EUR }}` with `amount = 1234.5` rendered `1234.5` in every face, and `{{ due | dat:long }}` rendered the stored timestamp as written: the renderer's fallback for a formatter it did not know was the raw value.
15+
- **Now.** The render throws `TEMPLATE_INVALID: email template NAME (locale=LOCALE): the hole … names the formatter 'curency', which is not a template formatter …`, listing the known formatters; `sendTemplate` sends nothing, and the messaging channels grade the failure permanent as they grade every `TEMPLATE_INVALID`. The authoring doors refuse the same hole first (`email-template-hole-grammar`), so this is reached only by a template stored before that rule, until its next publish.
16+
- **The raw fallback is gone.** `renderTemplate` and `renderPlainTextTemplate` throw `TEMPLATE_INVALID` for such a hole too, when handed a body directly.
17+
18+
**The remedy.** Republish the template with the formatter's name corrected to one the refusal lists.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `email_template` face descriptions and the `IEmailService` `TEMPLATE_INVALID` TSDoc name a formatter the template engine does not know among the refused holes
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **The face descriptions.** `subject`, `bodyHtml` and `bodyText` now say that a hole's formatter is one the template engine knows, and that an unknown name (a misspelled `curency`) is refused, naming the known formatters. The `currency` sentence is unchanged. The refusal itself ships in `@objectstack/formula`, `@objectstack/lint` and `@objectstack/plugin-email`; the spec only states the grammar, so no accept set of any schema moves.
11+
- **The TSDoc.** The `TEMPLATE_INVALID` error of `sendTemplate` and `renderTemplate` now names all three holes `email-template-hole-grammar` refuses: a second formatter, a formatter the whitelist does not name, and a `currency` argument that is neither a literal ISO 4217 code nor a marked `@path`.
12+
13+
No type, schema shape or export changes.

0 commit comments

Comments
 (0)