Skip to content

Commit 7afdc5c

Browse files
fix(rest): the /meta dashboard and view reads hand the translator the packaged base, so a published org overlay beats the packaged catalog (#20832)
Fixes #20730 Clause-②: no ## What this changes An org's published edit to a packaged dashboard or view is now what the `/meta` item and list reads serve, in every locale. Before, the reads served the bundle's translation of the string the package shipped. `packagedObjectBaseOf` in `packages/rest/src/meta-item-read-gate.ts` is now the one per-type packaged-base resolver that the triage asked for. It reads one table, `PACKAGED_BASE_ACCESSORS`: | type | protocol accessor | |:--|:--| | `object` | `getPackagedObjectBase(name)` (unchanged) | | `dashboard` | `getPackagedDashboardBase(name)` | | `view` | `getPackagedViewBase(name)`, by the served item's qualified `OBJECT.VIEW_KEY` registry name | `translateMetaDocument` (the item read) and `translateMetaList` (the list read) now resolve the protocol for every type in that table instead of for `object` only. Both `/meta` transports (the REST server's routes and the runtime's HTTP dispatcher) call these two functions. There is no second resolver; a future type with a packaged-base accessor adds its row to the table. The translation rule itself (ADR-0029 D9.2a, an explicit override beats a packaged default) is unchanged in `@objectstack/spec/system`. This PR only hands the translator the base it could not see. `packages/rest/src/rest-server.ts` is not touched: it delegates to these functions and already hands them its protocol. `packages/metadata-protocol` and `packages/spec` are read, not edited. ## Pins `packages/rest/src/meta-dashboard-view-i18n-explicit-override.test.ts` (52 tests) runs over the REAL `ObjectStackProtocolImplementation` and a REAL `SchemaRegistry`, through the `/meta` routes. The packaged items are registered the way the boot registers them, and the org overlay rows are seeded the way a published overlay stores them. - §1: the resolver asks each type for its own accessor. It asks nothing for `action`, `app`, `dataset`, `page`, `report`, `toString` or `constructor`. It answers `undefined` for an empty name, a missing accessor or a throwing accessor. - §2 dashboard: for the item read and the list read, for an admin and a member of the same org, in `en` and `zh-CN`: - after an overlay and a publish, the edited widget serves the edit; - the unedited widget stays translated; - after a reset, the shipped title is served, translated. - Control: a packaged dashboard whose catalog has no widget titles serves an overlay edit exactly as before. - §3 view: the same three cells for `showcase_task.in_progress`. The unedited view is `showcase_task.urgent`. One line in `scripts/engine-double-contract.pinned.json`, written by `check-engine-double-contract --write` for the new `findOne` double. The double calls `assertEngineFindOnePredicate`. ## Ablations (one-shot, at `87f1867ea`, source-resolved) Each ablation went through `scripts/ablation-replace.mjs`. The anchor hit once in every case, and each restore was proven by the blob hash matching HEAD and by an empty `git diff HEAD`. | mutation | result | |:--|:--| | resolver answers `object` only (blob `38a35644` to `4aeefa5c`) | 17 failed / 46 passed of 63. The failures are all 16 edit cells plus the §1 dispatch case. The unedited, reset and control cells and the object suite stay green. | | list read resolves the protocol for `object` only | 8 failed / 44 passed of 52. The failures are exactly the 8 list-read edit cells. | | item read resolves the protocol for `object` only | 8 failed / 44 passed of 52. The failures are exactly the 8 item-read edit cells. | ## Measured in a real boot Scratch probe, not committed: `bootStack(showcaseStack, { orgContext: true })`, the seeded admin plus a signed-up member of the same org, on tree `e830f24cb`. The probe ran three phases: pristine; overlaid (admin `PUT ?mode=draft` then `POST publish`, each answering 200, on `system_overview`, on the showcase control `showcase_ops_dashboard` and on `showcase_task.in_progress`); and reset (admin `DELETE`, answering 200). In every phase it read `/meta/dashboard/NAME`, `/meta/view/NAME`, `/meta/dashboard`, `/meta/view`, `/meta/app` and `/meta/object` for both callers in `en` and `zh-CN`. Before (the rest dist rebuilt from the ablated resolver; `ablation-dist-preflight` found the marker in 2 built files) compared with after (HEAD): **12 served leaf fields differ, all in the overlaid phase**: - `system_overview` `widget_total_users.title` on the item and list reads, admin and member, `en` and `zh-CN` (8). It went from `Total Users` / `用户总数` to `Total Users (edited-20730)`. - `showcase_task.in_progress` `label` on the item and list reads, admin and member, `zh-CN` (4). It went from `进行中` to `In Progress (edited-20730)`. An `en` reader was already served the edit before this change. - 0 diffs in the pristine and reset phases, on the control dashboard, on every other dashboard and view, and on the app and object lists. The restore leg rebuilt rest, and `ablation-dist-preflight --absent` found the marker absent from all 6 built files. The card cites 16 changed fields from the parent card's probe. That probe covered a different set of reads, so the two counts are not comparable. Here the dashboard alone accounts for 8. ## The rendered board, measured in a browser The console was built from objectui `db11afd4967c` (this repo's `.objectui-sha`) with `scripts/build-console.sh` and served by `pnpm dev -- --fresh` at `8ee4569fa`. The browser was headless Chromium from `/opt/pw-browsers/chromium`, signed in through the console's form as the seeded admin, with the browser locale set to `en` and to `zh-CN`. - After the overlay was published, the console's own `/api/v1/meta/dashboard` responses carried `widget_total_users.title` = `Total Users (edited-20730)` in both locales, and its `/api/v1/meta/view` responses carried `label` = `In Progress (edited-20730)`. - The drawn board at `/_console/apps/setup/dashboard/system_overview` still showed `Total Users` (`en`) and `用户总数` (`zh-CN`). - The view at `/_console/apps/showcase_app/showcase_task/view/in_progress`: - `en`: the tab showed the edit, and the breadcrumb showed `In Progress`; - `zh-CN`: both the tab and the breadcrumb showed `进行中`. - After the reset, both were served and drawn as shipped. So the server half is fixed, and the console still re-resolves these strings against the bundle in the browser. That half is objectui's. The measurement is handed to the seat to file, as the triage directed. ## Docs and changeset - `content/docs/ui/translations.mdx` gains "An edit beats the packaged catalog", which states the dashboard rule and the view rule side by side (which strings are compared, and how each is matched), the three consequences, and the object rule by reference. - `.changeset/20730-meta-packaged-base-dashboard-view.md`: a `patch` for `@objectstack/rest`. It says in words that the console still draws the packaged translation. ## Verification - `pnpm --filter @objectstack/rest test`: 233 files, 4524 passed, 77 skipped. `test:repo`: 1 file, 8 passed. `typecheck`: `tsc --noEmit` plus `check:test-typecheck` OK, and the new file is in the test program. All at `0a60a2f61`, after merging `origin/main`. - Readers of the edited module in other packages: - runtime `meta-list-projection-parity`: 658 passed; - runtime `meta-item-read-gate-parity` and `meta-list-read-gate-parity`: 118 passed; - http-conformance `hono-meta-list-read-gate`: 6 passed. - `dispatch-gates --commands` at `8ee4569fa` derived 97 families. All 97 were run and reconciled with `--ran`: 97 run, 0 NOT-MEASURED, 0 UNRUN. `check:engine-double-contract` failed at `0a60a2f61` (the new double was not yet in the ledger) and passed at `8ee4569fa` after the `--write`. The 20 changeset and doc families were re-run at `2b8df02e7` and all exited 0. - Lint, narrowed and proven at `2b8df02e7`: - Population: `eslint.config.mjs` matches `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, so 2 of the 5 changed paths are in it (the others are `.md`, `.mdx` and `.json`). - `eslint --no-inline-config --format json` on those 2 files: 2 files, 0 errors, 0 warnings. - The config enables no type-aware linting (0 hits for `parserOptions.project` or `projectService`), so this diff cannot move a verdict on an untouched file. ## Acceptance notes - **The name stays `packagedObjectBaseOf`** although it now answers dashboards and views: `RestServer.packagedObjectBase` in `rest-server.ts`, which PR #20683 holds, calls it by that name. A rename belongs to the next edit of that file. Noted, not filed. - **Docblocks outside this surface still say "object base only"**: `RestServer.metaItemTranslationSources` / `metaListTranslationSources` and the runtime's `metaTranslationSources` in `packages/runtime/src/domains/meta.ts`. They are comments only, and the behaviour is shared. Carrier for `rest-server.ts`: PR #20683. Carrier for the runtime file: none. Noted, not filed. - `action`, `app`, `dataset` and `page` have no packaged-base accessor. They are ADR-0126 tier B, where a packaged item answers `NOT_OVERRIDABLE` to a write, so no org overlay of them exists to protect. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b280546 commit 7afdc5c

5 files changed

Lines changed: 474 additions & 16 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest): an org's published edit to a packaged dashboard or view is what the `/meta` item and list reads serve, in every locale, instead of the packaged translation of the string it replaced
6+
7+
An organization may edit a packaged dashboard or view in place and publish the edit. The metadata protocol's reads returned the edit, and `?layers=true` reported it as effective, but `GET /api/v1/meta/dashboard/:name`, `GET /api/v1/meta/dashboard`, `GET /api/v1/meta/view/:name` and `GET /api/v1/meta/view` served the bundle's translation of the string the package shipped. For example, a widget retitled `Total Users (edited)` on the platform's `system_overview` dashboard was served as `Total Users` to an `en` reader and as `用户总数` to a `zh-CN` reader.
8+
9+
The translators in `@objectstack/spec/system` already let an edited string win over the bundle when they are handed the item as the package shipped it, and the metadata protocol already answers that item (`getPackagedDashboardBase`, `getPackagedViewBase`). The `/meta` reads handed it over for objects only. They now hand it over for dashboards and views too. The change is in the translation step that both `/meta` transports share, the REST server's routes and the runtime's HTTP dispatcher. A view is looked up by its full `<object>.<viewKey>` name.
10+
11+
What a reader sees now:
12+
13+
- An edited string is served as written, in every locale.
14+
- A widget or view the org left alone is still translated.
15+
- Resetting the overlay brings back the shipped string and its translation.
16+
- A dashboard or view with no org edit is served exactly as before.
17+
18+
This fixes what the metadata reads serve, not yet what the console draws. The console built from objectui `db11afd4967c`, this repository's pin when the change was made, looks a dashboard's widget titles and a view's label up in the bundle again in the browser, so it still draws the packaged translation over an edit the server now serves: measured, the `system_overview` board shows `Total Users` / `用户总数` and a `zh-CN` view tab shows `进行中`.
19+
20+
Nothing to migrate: no key, export or route changed.

‎content/docs/ui/translations.mdx‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,33 @@ Resolved labels are served straight from the REST metadata endpoints (the
174174
locale is part of the ETag), so the Console and any SDUI client get translated
175175
metadata without doing lookups themselves.
176176

177+
### An edit beats the packaged catalog
178+
179+
A package's bundle translates the strings that package shipped, so it applies
180+
only to a string the served metadata still carries unchanged. Dashboards and
181+
views are the translated types an organization can edit in place: once an
182+
org's edit to a packaged dashboard or view is published, the metadata reads
183+
serve each edited string as written, in every locale. Each string is compared
184+
with the same string in the item as the package shipped it:
185+
186+
| Packaged type | Strings compared, and how each is matched |
187+
|:---|:---|
188+
| Dashboard | its `label` and `description`; each widget's `title`, `description` and sub-caption (`subCaption`), matched by widget `id`; each global filter's `label` and static option labels, matched by the filter key and the option `value` |
189+
| View | its `label` and `description`; each bulk action's `label`, `confirmText` and `confirmLabel`, and each of its params' `label`, `help` and `placeholder`, matched by `name` |
190+
191+
- **Only the edited strings move.** A widget the org left alone on an edited
192+
dashboard, or a view nobody edited, is still translated.
193+
- **A widget, global filter or bulk action the package never shipped counts as
194+
edited**: the org authored it, so its text is served as written.
195+
- **An edited string is served in the language it was written in, to every
196+
reader.** The bundle entry for that string no longer applies to it.
197+
- **Resetting the overlay restores the shipped string and its translation.**
198+
199+
Objects follow the same rule for their `label`, `pluralLabel` and
200+
`description`: a value that differs from the owning package's declaration (a
201+
label an [object extension](/docs/data-modeling/object-extensions) contributes,
202+
for one) is served as written.
203+
177204
## Organizing the files
178205

179206
How you lay out translation source files is an authoring convention — your

0 commit comments

Comments
 (0)