Skip to content

Commit 21b79c1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-s71sz8
Claude-Session: https://claude.ai/code/session_01ADHkmNiJfZmoXAbfcJSAfx Co-authored-by: Claude <noreply@anthropic.com>
2 parents 8090d1f + e78d705 commit 21b79c1

191 files changed

Lines changed: 13554 additions & 3604 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+
feat(i18n): the rest of the Studio inspectors and the package, form and editor strings move into the `studio` namespace
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Third slice of the move ruled on objectstack#25922** (6431032267, Q2 = A; kept by 6431234244). The remaining `engine.inspector.*` keys and every `engine.packages.*`, `engine.form.*` and `engine.edit.*` key (551 in `en`, 560 in `zh`) now live in `@object-ui/i18n/namespaces/studio` instead of `@object-ui/app-shell`'s private table, text unchanged. The nine `engine.packages.form.help.*` rows stay `zh`-only: their English is the producer's own help text, read through `tOptional`. Studio reads the same strings; a Chinese viewer sees these labels in English for the moment the `zh` catalogue takes to load, as with the earlier slices. No export changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
refactor(plugin-grid): `ObjectGrid`'s column generator, column post-processing and Actions column move into three sibling modules
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The last three blocks of the move-only split leave `ObjectGrid.tsx` verbatim. `generateColumns`, which turns the authored columns or the object's fields into column drafts, goes to the hook `useObjectGridGenerateColumns` (`objectGridGenerateColumns.tsx`). The column post-processing (field-type enrichment, the fold onto the declared column type, and the user's persisted widths and order) goes to `resolveObjectGridPersistedColumns` (`objectGridPersistedColumns.ts`). The Actions column and the pinned column sets go to `resolveObjectGridActionsColumn` (`objectGridActionsColumn.tsx`). `ObjectGrid` calls each where its block stood, so its hook order, its columns and its rendered output do not change. No name is renamed, and the package entry is unchanged.
11+
12+
What to do: nothing.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/core': patch
4+
---
5+
6+
fix(spec): `TranslationItemSchema.name` no longer tells authors to name a translation item after its locale tag
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The `name` describe said a translation item was "conventionally the locale code (`zh-CN`)", but the metadata save door's item-name grammar refuses `zh-CN` (uppercase and a hyphen), so an author or agent following the describe got a 400 `INVALID_REQUEST` on the first save. The describe now says a translation item is named in snake_case like every metadata item (e.g. `zh_cn`), with the BCP-47 tag in `locale`. The save door and the runtime sync's read-side name fallback for rows stored before `locale` became required are unchanged; the sync's skip warning now points authors at `locale` only.
12+
13+
`@objectstack/core`: the authored-translation sync's no-locale skip warning now tells authors to set `locale` to the item's BCP-47 tag (e.g. `zh-CN`), instead of suggesting they name the item after it.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
'@objectstack/platform-objects': minor
5+
---
6+
7+
feat(spec): a greyed action says why — `disabledReason` beside `disabled`
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: added
11+
12+
`ActionSchema` gains one optional key, `disabledReason`: the sentence a greyed action shows to say why it is unavailable, beside the `disabled` predicate that greys it. An action that does not declare it parses exactly as before, and the console keeps its default reason, "Not available for this record".
13+
14+
```ts
15+
defineAction({
16+
name: 'submit_for_approval',
17+
label: 'Submit for approval',
18+
objectName: 'clm_contract',
19+
disabled: "record.legal_review_status != 'approved'",
20+
disabledReason: 'An approved legal review is required before the contract enters approval.',
21+
});
22+
```
23+
24+
- **An `I18nLabel`.** A plain string or an inline locale map (`{ en, 'zh-CN' }`). A blank string is refused at parse: `''` or whitespace only. Omit the key to keep the default. The locale-map arm is the `InlineLocaleMapSchema` every `I18nLabel` takes, so a map is not checked for blank values. `disabledMessage`, `disabledTooltip`, `disabledHint` and `disabledText` are renamed onto it.
25+
- **Translated with the copy keys.** The slot is `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an action with no `objectName`), beside `confirmText` and `successMessage`. `translateAction` overlays it, and `os i18n extract` (`@objectstack/cli`) scaffolds it for an action that declares one and seeds nothing for one that does not.
26+
- **Display only.** It does not change what `disabled` evaluates to and is never read as a condition: `os validate` checks `disabled`, not `disabledReason`. It is not access control either. A call that reaches the server anyway is refused by the action's own rules (its body or hook, `requiredPermissions`), never by this text.
27+
- **Studio.** The action editor's Placement section gains a "Disabled Reason" row under "Disabled", translated in `@objectstack/platform-objects` for zh-CN, ja-JP and es-ES.
28+
29+
Nothing renders the key yet. The console's greyed-action surfaces (the record header, the record section bar, the declared actions bar and the four `action:*` renderers) read it in the follow-up change. Until then the liveness ledger marks it `planned` with `authorWarn`, so `os validate` warns an author who declares it that nothing shows it yet.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@object-ui/core': patch
3+
---
4+
5+
`SPEC_ACTION_KEYS` lists `disabledReason`, the key `@objectstack/spec` added to `ActionSchema` beside `disabled` (objectstack#26786). An action that carries it is no longer counted as having an unknown key, and the inventory pin is green again.
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/plugin-security': patch
3+
---
4+
5+
fix(plugin-security)!: security explain refuses a `userId` that no `sys_user` record has with `404 RECORD_NOT_FOUND`, naming the id, instead of explaining the baseline principal the resolver builds for any id
6+
7+
Clause-②: no (narrowing) — `POST` / `GET /security/explain` and the in-process `explain()` refuse a request naming another user whose id no `sys_user` record has, which they answered with a verdict before. 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: the explain request's userId keeps its shape, and an id that names nobody is now refused instead of explained. A verdict about nobody had no meaning to keep, so there is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** accept-set narrowing at the access-explanation door (`@objectstack/plugin-security`), served at `/security/explain` on both transports.
13+
14+
- **Before.** An administrator explaining another user by an id that no `sys_user` record has (a typo, a deleted user, an id from another deployment) was answered `200` with a full verdict, usually `allowed: true`: the authorization resolver gives any id the baseline position and permission sets. `GET /data/sys_user/ID` answered 404 for the same id.
15+
- **Now.** That request is refused `404 RECORD_NOT_FOUND`, and the message names the id. The id is read through the same `sys_user` existence read the on-behalf-of delegator path uses.
16+
- **Unchanged.** The order of checks: an anonymous caller is refused 401, and a caller who may not explain others gets the same 403 `PERMISSION_DENIED` whether the id exists or not, before any existence read. Explaining yourself (no `userId`, or your own) and explaining a user who exists answer as before.
17+
- **Who sees it.** A caller holding `manage_users` (or a system caller) who names an id that does not exist. An organization administrator or a delegated administrator naming such an id still gets the 403: no membership or scope covers a user who does not exist.
18+
19+
**The remedy.** None needed: pick the user from the directory, or check the id.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol)!: the Data API refuses an empty projection and a count flag that is neither true nor false, instead of reading them as absent (#26644)
6+
7+
Clause-②: no (narrowing) — `GET /data/:object`, `POST /data/:object/query` (both transports: `RestServer` and the runtime dispatcher) and `GET /data/:object/:id` refuse a projection that names no field, and the list doors refuse a `count` / `$count` other than true or false, where they answered 200 before. Nothing is widened and no export changes.
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: two query-parameter values the list normalizer read as the absent default are now refused 400 VALIDATION_ERROR naming the parameter. A caller drops the parameter or sends a well-formed value; there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the shared list normalizer (`findData`) and the by-id read (`getData`), served by every Data API door that uses them.
13+
14+
- **Before.** A blank or comma-only `$select` / `select` / `fields` (`?$select=`, `?$select=,`), and an empty array in a JSON body (`"fields": []`), became an empty projection, which every driver serves as no projection: all columns, under a 200. A `$count` / `count` other than `true` or `false` (`?$count=maybe`, `?$count=1`, a blank `?count=`) was left unlowered, so the `count=false` opt-out never matched and the count ran: a `total` was reported as if `$count=true` had been sent.
15+
- **Now.** Both are refused `400 VALIDATION_ERROR` before any read, with a message naming the parameter the caller wrote and the value (`Query parameter '$select' names no field; got ""`, `Query parameter '$count' must be true or false; got "maybe"`), as a blank `$top` already is. The accepted count values are the spec's `QueryTransportParamsSchema.count`: the boolean, or the strings `true` and `false`. `GET /data/:object/:id?select=` answers with the same code; that route's catch already reports every projection refusal (an unknown field included) as `404`, and the empty projection joins that answer.
16+
- **Unchanged.** An absent parameter keeps its default: every column, and the count runs. A named projection (`$select=a,b`, trailing commas and spaces trimmed as before), `$count=true`, `$count=false` and a JSON `null` answer exactly as before. `POST /data/:object/query` already refused a non-boolean `count` at its body schema (`400 VALIDATION_FAILED`), and still does.
17+
- **Who sees it.** A caller that builds `$select` from a list that can be empty, or sends a mistyped `$count`. It now learns that from a 400 instead of an answer that looked normal.
18+
19+
**The remedy.** Leave the parameter out to read every column or to keep the count, or send a projection naming at least one field and a count flag of `true` or `false`.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/formula': minor
3+
---
4+
5+
feat(formula)!: a template hole takes one formatter, and the `currency` formatter's argument is a literal ISO 4217 code — both refused at compile, never rendered as formatter syntax or in US dollars
6+
7+
Clause-②: no (narrowing) — the in-app template dialect's compile (and so `os validate` and the publish gate for every template slot) refuses a hole with a second formatter (`{{ x | default:0 | number:2 }}`) and a `{{ x | currency:ARG }}` hole whose `ARG` is not three capital letters, both of which it accepted before; `formatValue('currency', value, ARG)` now throws a `RangeError` for such an `ARG`: a path or another argument `Intl` refuses, and an empty one, used to fall back to US dollars, and a lowercase code (`eur`), which `Intl` formatted in that currency, is now refused too. No accept set is widened; the additions are exports only: `judgeHoleFormatter`, `mailHolePattern`, `parseMailHole`, `judgeMailHoleGrammar`, and the types `TemplateHoleGrammarRefusal`, `TemplateHoleGrammarRule`, `ParsedMailHole` and `MailHoleGrammarFinding`.
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 chained formatter and a currency argument that never rendered as written are now refused where the template is compiled. The refusal names the hole and the rule it breaks, so there is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** accept-set narrowing in the template dialect, and a new shared mail hole grammar.
13+
14+
- **Before, a chained hole.** `{{ record.n | default:0 | number:2 }}` compiled as `default` with the argument `0 | number:2`, and rendered that text.
15+
- **Before, a currency path.** `{{ record.amount | currency:currency_code }}` compiled; `Intl` refused the argument and the formatter caught that and used US dollars, so the amount printed in a currency nobody wrote.
16+
- **Now.** `templateEngine.compile` / `evaluate` answer a `parse` error naming the hole and the rule. `formatValue('currency', …)` throws a `RangeError` for an argument that is not a code, before it reads the value. A lowercase code (`currency:eur`), which formatted in euros before, is refused the same way. No argument still formats in US dollars.
17+
- **Shared grammar.** `judgeHoleFormatter` is the judgement both template dialects use. The mail template grammar is exported once, `mailHolePattern`, `parseMailHole` and `judgeMailHoleGrammar`, and is what `@objectstack/plugin-email`'s renderer, `judgeMailHtmlHoles` and `@objectstack/lint` read; `judgeMailHtmlHoles` no longer keeps a copy of it.
18+
- **Who sees it.** A template slot that chained formatters, or wrote a path or a lowercase code as the currency argument. No template shipped in this repository does.
19+
20+
**The remedy.** Keep one formatter per hole and decide anything more where the data is assembled. Write the currency code itself (`currency:EUR`), or format the amount where the data is assembled and pass the formatted text.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/service-messaging': patch
3+
---
4+
5+
fix(service-messaging): the email and inbox channels grade a `TEMPLATE_INVALID` template failure permanent, like the other template failures
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`@objectstack/plugin-email`'s `sendTemplate` and `renderTemplate` now fail with `TEMPLATE_INVALID` for a stored template whose hole the mail grammar refuses (a second formatter, or a `currency` argument that is not a literal ISO 4217 code). Retrying cannot succeed until the template is republished, so `classifyError` on both channels grades it `permanent`, as it already grades `TEMPLATE_NOT_FOUND`, `TEMPLATE_INACTIVE`, `MISSING_VARIABLES` and `TEMPLATE_UNSUPPORTED`: the delivery goes dead at once with the code on its row, instead of spending the whole retry schedule first. Every other failure keeps its grade.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/plugin-email': minor
3+
---
4+
5+
feat(plugin-email)!: a mail template hole that chains a second formatter, or whose `currency` argument is not a literal ISO 4217 code, fails the render with `TEMPLATE_INVALID` instead of printing formatter syntax or US dollars
6+
7+
Clause-②: no (narrowing) — `sendTemplate` and `renderTemplate` refuse a stored template whose `subject`, `body_text` or `body_html` writes a hole with a second formatter, or `{{ x | currency:ARG }}` with an `ARG` that is not three capital letters, both of which rendered 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. There is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** a render that printed a wrong currency now fails.
13+
14+
- **Before, a chained hole.** `{{ actual_amount | default:0 | number:2 }}` rendered `0 | number:2` for an empty value and `71100` otherwise, in every face.
15+
- **Before, a currency path.** `{{ planned_amount | currency:currency_code }}` with `currency_code = 'eur'` rendered `US$1,234.50` (zh-CN), `$1,234.50` (en-US) and `1 234,50 $US` (fr-FR), in every face.
16+
- **Now.** The render throws `TEMPLATE_INVALID: email template NAME (locale=LOCALE): …`, naming the hole and the rule it breaks (one formatter per hole, or the literal-only currency code); `sendTemplate` sends nothing. 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.
17+
- **The grammar.** The renderer parses holes with `@objectstack/formula`'s `parseMailHole`, the grammar the authoring rule and the placement judge read; it keeps no copy of its own.
18+
19+
**The remedy.** Republish the template with one formatter per hole and a literal currency code (`currency:EUR`), or format the amount in the caller and pass the formatted text as its own variable.

0 commit comments

Comments
 (0)