Skip to content

Commit 666283a

Browse files
fix(plugin-approvals): the approval request page's highlights strip draws no Object chip; the Details tab's record card names the target object by its label (#24500)
Fixes #24375 Clause-②: no The served `sys_approval_request_detail` record page's highlights strip no longer draws an Object chip. It listed `object_name`, the target object's API name as stored, so a request about an announcement read `showcase_announcement` there. The page still names the target object, by its label: the Details tab's card for the `record_id` / `object_name` pointer pair heads the target record with its object's label. `object_name` leaves the object's `highlightFields` with the strip, so the Details tab does not draw it as a row. Responsibility: `plugin-approvals`' own declarations (the page's highlights `fields` and the object's `highlightFields`) | the platform path that already names the target by its label is the console's pointer-pair card in the Details tab, fed by the declared `referenceVia` pair | reached by every user who opens an approval request's served page, live today. Readings are against objectstack `fb9c22aa0` (this branch's base; `origin/main` `ce2e08ee3` is merged in at `5f21f062f` and touches nothing under `packages/plugins/plugin-approvals`) and objectui at the console pin `.objectui-sha` = `0707b4ae37`, by `git show` and `git grep`. The merged `main` moves the pin to `4016370af3c9`; between the two pins the files on the chain below differ only in two hunks that retire the `permission-facet-link` widget (`DetailSection.tsx`, `fields/src/index.tsx`), not the pointer pair. The page was not driven in a browser; the section below says why and what stands in for it. ## The measurement the route turns on: what the Details tab's card draws The card's two routes split on one question: does the Details tab's record card already name the target object by its label? Statically, at the pin, it does, in the state where the reader can read the target. - The Details tab is a bare `record:details` node. objectui's `record-details.tsx` draws the object's `highlightFields` minus every field the mounted strip registers and minus the H1 field (objectui#12433's declared fallback). On `main` that is `record_id` alone. - `record_id` declares `referenceVia: 'object_name'`. `DetailSection.tsx` resolves the pair with `resolveRecordPointer` and draws it with the renderer registered under `RECORD_POINTER_CARD_TYPE`, which is app-shell's `record-pointer-card-renderer.tsx`, mounting `RecordPreviewCard`. - `RecordPreviewCard`, readable state: it reads the target object's definition (`useMetadataItem('object', objectName)`) and heads the card with `objectLabel({ name, label: def.label })` from `useObjectLabel`, which resolves the object's translated label (`objects.NAME.label` in the reader's locale) and falls back to the definition's label. The record's title sits under it. objectui pins this in `RecordPreviewCard.test.tsx` ("heads it with the object label", `getByText('Invoice')`). - The other states name no object: `unreadable` (the reader may not read the target, or its definition is unavailable) draws "This record cannot be opened"; a request whose `cancel_reason` is `record_deleted` draws the tombstone, "Related record deleted"; `loading` draws a spinner. - The console reads the request through the approvals get (`routeApprovalRecordReads`; `GET /approvals/requests/ID` has no projection), so `object_name` stays in the row the card resolves whether or not any block lists it. Why not a browser reading: there is no console build in this container (`packages/console/dist` is absent), so a screen reading needs objectui's console built at the pin plus the showcase closure. The static chain above and objectui's own pin on the card's heading stand in for it. ## The route: no Object chip | | strip | Details tab (`highlightFields` minus strip) | |---|---|---| | `main` | `process_label`, `object_name`, `status`, `current_step_label`, `submitter_id`, `updated_at` | `record_id` (the card) | | strip only | `process_label`, `status`, … | `object_name`, `record_id`: an "Object: showcase_announcement" row appears in the tab | | this PR (strip and `highlightFields`) | `process_label`, `status`, `current_step_label`, `submitter_id`, `updated_at` | `record_id` (the card), unchanged | The page's own rule decides `highlightFields`: "The strip and the object's list change together, never the strip alone." The four list views' `columns` are not that list. The rule binds the strip to `highlightFields` because the Details tab reads `highlightFields`; no block on this page reads a list view's columns, so they stay as they are (see Acceptance notes). ### Four axes - **Real business need.** The reader is an approver on the request page. Measured: the same page's Details tab already heads the target record with its object's label, translated. No producer writes an object-label column today, and no reader other than this chip would read one. The approvals get's `object_label` already serves the label to the inbox, live. - **Long-term soundness.** No new column, writer, backfill or snapshot. The label route would store an `object_label` snapshot while the get already serves a LIVE `object_label` under that name (`enrichRows` sets it from the registered schema on every read): the approvals get would answer the live label and the generic data door the stored one, two answers for one field. A second name for the same fact would be a second spelling instead. - **Making AI mistakes harder.** The page lists only declared fields the get serves, and the pin now holds the strip and the object's list free of the three machine-name fields of this family (`process_name`, `current_step`, `object_name`). A stored snapshot beside a live label is a second source an author can read by mistake. - **Startup focus.** This route removes one entry from two lists (4 files, under 70 lines). The label route, measured against #23889's footprint for the same kind of column (`b74434c58`: object, writer, backfill, four generated bundles, two integration tests), adds a declared column, its writer in `openNodeRequest`, a backfill leg in `display-label-backfill.ts`, four bundle entries and their tests, for a fact the page already shows. ### A relabelled object The card reads the object's definition when the page draws, so a relabelled object shows its new label on every request, old ones included. Under the label route a stored snapshot would keep the old label on requests opened before the rename, while the approvals get's live `object_label` would show the new one. ### What this route gives up Where the card cannot read the target (the reader may not read it, or the platform deleted it), the page names no object at all. Today the strip names it there by its API name. The card allows this ("The strip names the target object by its label, or does not name it at all"). ## Other readers of the object's `highlightFields` PR #24279 listed them at its own pin; the halves this repository decides are re-checked here at `5f21f062f` (the list views' `columns`, the two lookups' `apiMethods`, no page or related list naming `sys_approval_request` as its `objectName`, and the eight objects that declare a `referenceVia` pair): - `ObjectView` / `ObjectGrid` default columns: only for a list view that declares no `columns`; all four request list views declare them. Not reached. - `InterfaceListPage`, `ObjectKanban` and `RelatedList` defaults: no interface page, kanban or related list of requests is declared here. Not reached. - `LookupField` preview columns (`deriveLookupColumns`): the two lookups to a request (`sys_approval_action.request_id`, `sys_approval_approver.request_id`) sit on objects whose `enable.apiMethods` is `['get', 'list']`, so no form opens that picker. Not reached. - `RecordPreviewCard` for a pointer pair that names a request row (an audit or activity row about a request): its strip loses the Object chip, the same change. Whether such rows exist is NOT MEASURED. - Backend readers (`@objectstack/lint` field references and semantic roles, `metadata-core`'s field-security fold, `metadata-protocol`'s build probes): no behaviour change for a list with one existing field fewer. objectui's served-page pin copies (`apps/console/src/services/approvalRequestsDataSource.servedPage*.test.tsx` and `record-details.highlightFieldsFallback-12433.test.tsx`) copy the strip by value, and still copy it as it stood before #24264 (`process_name` … `current_step`); objectstack-ai/objectui#12484 rewrites them. Nothing in this repository reads them, and none of them imports this package. ## Tests At `5f21f062f` (the final head), under `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`): - `pnpm turbo run build --filter='!@objectstack/docs' --concurrency=2`: exit 0, `76 successful, 76 total`. - `pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 src/sys-approval-request-page.test.ts`: `Tests 19 passed (19)` (18 before, plus the new case). - `pnpm --filter @objectstack/plugin-approvals test`: `Test Files 81 passed (81)`, `Tests 1112 passed (1112)`. - `pnpm --filter @objectstack/plugin-approvals typecheck`: exit 0, `check:test-typecheck: OK`. The same four passed at `770c72940`, before the merge. The pin `sys-approval-request-page.test.ts` gains one case: neither the strip nor the object's `highlightFields` lists `object_name`; the Details tab is a bare `record:details` node and the object's list minus the strip is exactly `record_id`, whose `referenceVia` is `object_name`; and, as a control, `object_name` stays a declared, required `text` field. The existing flow and step cases still hold `process_name` and `current_step` out of both lists, so the strip and the object's list carry none of the family's three machine-name fields. **Ablation (one run, then restored), at `770c72940`.** `node scripts/ablation-replace.mjs` put `object_name` back into the page's strip, in its old position. On-disk evidence: anchor x1 → x0, replacement x0 → x1, blob `a7d3ac80c4dc` → `2f28f5526bba`. The pin went red, `Tests 2 failed | 17 passed (19)`: the new case failed with `expected { strip: true, declared: false } to deeply equal { strip: false, declared: false }`, and the existing case failed on the strip no longer equalling the object's list minus `record_id`. The restore leg: blob after restore == HEAD (`a7d3ac80c4dc`), `git diff HEAD` empty, `git status` clean. The test imports the page from source, so no build sits between the mutation and the run. No file is left behind by the ablation. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `5f21f062f` derived 67 commands for this diff (the same 67 as at `770c72940`). All 67 ran locally at `5f21f062f`, after the full build above, each exit code captured before any pipe: **67 exited 0.** Among them, as the claim named them: - `pnpm check:page-declaration-shape`: `OK — 34 page entries across 3540 sources … all reach the kernel through a discoverable declaration`. - `pnpm check:i18n`: `check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys)`. `pnpm check:i18n-stale-fill`: `OK (11 bundle set(s) — no new stale fills, 0 baselined)`. - `pnpm check:nul-bytes`: `OK (scanned 12325 text file(s) … no raw ASCII control bytes)`. - `pnpm check:test-source-alias`: `OK — 76 packages with tests scanned`. - `node scripts/check-platform-object-tenancy-census.mjs` (and its `--self-test`): `✓ platform-object tenancy census matches the tree: 83 platform-namespace objects`. - `pnpm check:published-files`: `✓ … 73 publishable package(s)`. `pnpm check:dual-build-cjs-loads`: `✓ … 111 published require entry point(s) across 69 package(s) load`. `node scripts/pm/dispatch-gates.mjs --ran` reconciliation: `67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN`. On the first pass (at `770c72940`) three refused with exit 3, PREREQUISITE NOT MET, and passed after their prerequisite: `pnpm check:i18n` and `pnpm check:dual-build-cjs-loads` (no full build yet), and `node scripts/check-plugin-teardown-shape.mjs --self-test` (its positive control is pinned to commit `621a4876`, outside this shallow checkout; one `git fetch --depth=1` of that commit). The repo-wide scans (`pnpm lint` and the rest of the CI battery) and the vitest `repo` project are CI's; their PR check-runs are the reading. `check:dev-prereqs` and `check:skill-frame-freshness` were not run: the diff touches neither `packages/spec` nor the four-axis frame files. ## Acceptance notes - The four request list views (`all_requests`, `my_pending`, `submitted_by_me`, `completed`) still list `object_name`, so their Object column shows the API name. They are outside the page rule above and outside this card; reported to the seat. - The served page's H1 is unchanged by this PR. --- _Generated by [Claude Code](https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f72ce93 commit 666283a

4 files changed

Lines changed: 57 additions & 10 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix(plugin-approvals): the approval request page's highlights strip no longer shows the target object's API name
6+
7+
Clause-②: no
8+
9+
- **The strip draws no Object chip.** `sys_approval_request_detail` no longer lists `object_name` on its highlights strip, so a request about an announcement no longer shows `showcase_announcement` there. `object_name` holds the target object's API name, and `sys_approval_request` declares no field that holds that object's label.
10+
- **The page still names the target object, by its label.** The Details tab draws the `record_id` / `object_name` pointer pair as a card of the target record. The card heads the record with its object's label (`Announcement`), translated into the reader's locale and read from the object's definition when the page draws, so a relabelled object shows its new label. Where the reader may not read the target record, or the platform deleted it, the card says so and the page names no object.
11+
- **The object's `highlightFields` drops `object_name` too.** The page's Details tab is a bare `record:details` node, which draws the object's `highlightFields` minus the strip. A change to the strip alone would have moved `object_name` into that tab as a row showing the API name.
12+
- **Unchanged:** `object_name` stays a declared, required field: the approvals machinery queries on it, and it is the type half of the pointer pair. The request list views still show their Object column.
13+
14+
A patch: one entry fewer in one platform object's field list and in its page. No export, type, accepted value, served key or stored column changes.

‎packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* `record:approval_decision` node, `record:details`, `sys_approval_action`
1616
* as the related list, no discussion thread, and a highlights strip that
1717
* leaves `record_id` to the details grid (the seat's choice on the ui
18-
* seat's design input `6081889955`);
18+
* seat's design input `6081889955`) and names nothing by a machine name;
1919
* 3. the page passes what `os validate` judges a page by — the parse, the
2020
* component-type vocabulary, the props rows and the build rules — with a
2121
* control for each half that can go red;
@@ -183,6 +183,30 @@ describe('sys_approval_request detail page — the slot map', () => {
183183
expect(fields.current_step).toMatchObject({ type: 'text' });
184184
});
185185

186+
it('draws no Object chip: object_name, the target\'s API name, is in neither list, and the Details tab is the pointer pair\'s card', () => {
187+
// `object_name` holds the target object's API name (`showcase_announcement`)
188+
// and the object declares no field holding that object's label. The page
189+
// names the target object on the card the console draws for the
190+
// `record_id` / `object_name` pair in the Details tab, which heads the
191+
// record with its object's label. The object's list moves with the strip
192+
// for the reason the flow chip's does: dropped from the strip alone,
193+
// `object_name` would be drawn as a row in that tab.
194+
const strip = SLOTS.highlights.properties.fields as string[];
195+
const declared = SysApprovalRequest.highlightFields ?? [];
196+
const fields = SysApprovalRequest.fields as AnyRec;
197+
expect({ strip: strip.includes('object_name'), declared: declared.includes('object_name') })
198+
.toEqual({ strip: false, declared: false });
199+
// The Details tab's bare `record:details` node draws the object's list
200+
// minus the strip: the id half of the pair, whose type half names the
201+
// target object.
202+
expect(TAB_ITEMS[0].children).toEqual([{ type: 'record:details' }]);
203+
expect(declared.filter((f: string) => !strip.includes(f))).toEqual(['record_id']);
204+
expect(fields.record_id?.referenceVia).toBe('object_name');
205+
// Control: the type half stays a declared, required field. It is dropped
206+
// from the two lists, not from the object.
207+
expect(fields.object_name).toMatchObject({ type: 'text', required: true });
208+
});
209+
186210
it('tabs: Details is record:details, Timeline is sys_approval_action as a related list on request_id', () => {
187211
expect(TAB_ITEMS.map((t) => t.label?.en)).toEqual(['Details', 'Timeline']);
188212
expect(TAB_ITEMS[0].children).toEqual([{ type: 'record:details' }]);

‎packages/plugins/plugin-approvals/src/sys-approval-request.object.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ export const SysApprovalRequest = ObjectSchema.create({
6464
// [ADR-0085] The flow is named by its label, `process_label`, never by the
6565
// machine source `process_name` (`flow:NAME`), and the step by its label,
6666
// `current_step_label`, never by the node id `current_step`, as the request
67-
// lists name them. The record page's highlights strip is this list minus
68-
// `record_id`, and the page's bare `record:details` node draws this list
69-
// minus the strip, so the two move together (`sys-approval-request.page.ts`
70-
// says why).
71-
highlightFields: ['process_label', 'object_name', 'record_id', 'status', 'current_step_label', 'submitter_id', 'updated_at'],
67+
// lists name them. The target object is not listed: `object_name` holds its
68+
// API name, and the record page names it by its label on the card the
69+
// `record_id` / `object_name` pair draws. The record page's highlights strip
70+
// is this list minus `record_id`, and the page's bare `record:details` node
71+
// draws this list minus the strip, so the two move together
72+
// (`sys-approval-request.page.ts` says why).
73+
highlightFields: ['process_label', 'record_id', 'status', 'current_step_label', 'submitter_id', 'updated_at'],
7274

7375
// Curated built-in list views — render as segmented tabs in the console.
7476
// Filters use {current_user_id} substitution wired by the console.

‎packages/plugins/plugin-approvals/src/sys-approval-request.page.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,16 @@ import type { Page } from '@objectstack/spec/ui';
4949
* The approvals get serves it by the same chain: the stored label, else
5050
* the snapshot's `__nodeLabel`, else the humanized node id. A request with
5151
* no step at all serves neither field, and the strip draws no step chip.
52-
* - The Object chip is `object_name`, the target object's API name as
53-
* stored. The object declares no field that holds that object's label, so
54-
* the strip has none to list in its place.
52+
* - There is no Object chip. `object_name` is the target object's API name
53+
* as stored (`showcase_announcement`), and the object declares no field
54+
* that holds that object's label. The page names the target object in the
55+
* Details tab instead: the `record_id` / `object_name` pair's card heads the
56+
* target record with its object's label, translated and read from the
57+
* object's definition when the page draws, so a relabelled object reads its
58+
* new label. Where the card cannot read the target (the reader may not
59+
* read it, or the platform deleted it), the page names no object at all.
60+
* `object_name` leaves the object's `highlightFields` too, by the rule
61+
* above, so the Details tab does not draw it as a row.
5562
* - `tabs` is authored: Details (`record:details`, a bare node: see the
5663
* strip above) and Timeline — `sys_approval_action` as a
5764
* related list keyed on `request_id`, newest first. The `tabs` slot is
@@ -88,7 +95,7 @@ export const SysApprovalRequestDetailPage: Page = {
8895
highlights: {
8996
type: 'record:highlights',
9097
properties: {
91-
fields: ['process_label', 'object_name', 'status', 'current_step_label', 'submitter_id', 'updated_at'],
98+
fields: ['process_label', 'status', 'current_step_label', 'submitter_id', 'updated_at'],
9299
},
93100
},
94101

0 commit comments

Comments
 (0)