Repository navigation
docs(spec): record_related names its row placement inside a parent record - #20948
Conversation
…cord The ACTION_LOCATIONS docblock line, the two docs location tables and the platform-checklist variant now state the location's contract: a per-row action on each row of a related list shown inside a parent record, in that parent's context only, never on the object's own list views (unlike list_item). The docs rows state that the console does not place it on those rows yet. The checklist item bumps to revision 6 with a history entry. Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: Read-only inputs: card #20937 (body, ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…its child's `record_related` actions on each row (objectui#11270) (objectstack-ai#11289) Fixes objectstack-ai#11270 Clause-②: yes A related list inside a record now places its child object's `record_related` actions on each row. The authored `actions` channel on `record:related_list` accepts them too, and the Studio action designer previews that same row placement instead of a section-header button. This is the renderer half of the enforce answer on objectstack-ai/objectstack#20937 (triage `5919625056`): row placement, only inside a parent record. Implemented by the `domain:ui` seat 1 dispatch, session `https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ`, on claim `5920737104`. ## What changed - **Host bridge** (`packages/app-shell/src/views/RelatedRecordActionsBridge.tsx`, `deriveActions`). The row menu now reads `list_item`, plus `record_related` when a parent record is in scope (`parentRecordId` set). The check goes through `actionRendersAt`, the platform's one placement rule. An action that declares both locations renders once, in the child's declared order. The toolbar still reads `list_toolbar` alone. - **Authored channel** (`packages/plugin-detail/src/renderers/relatedListActions.ts`). An id whose action declares `record_related` is placed in each row's menu, in authored order. The refusal notice for an id that cannot be placed now names the three related-list locations: `list_toolbar`, `list_item` and `record_related`. - **Designer preview** (`packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx`, `PlacementPreview`). `record_related` no longer shares the `record_section` frame and its section-header button. It draws the row frame `list_item` already had: the same element, reused, under its own caption. No new frame family was added. `record_section` keeps its header frame. This takes in the cross-lane scope note `5920436535`. - Doc comments that described row actions as `list_item` only now name both row locations: `RelatedList.tsx`, `record-related-list.tsx`, and `RelatedRecordHandlers.rowActions` in `@object-ui/react`, which is a comment-only change. The guide `content/docs/guide/slotted-pages.md` explains how the two locations differ in scope. - Changeset `.changeset/11270-record-related-row-placement.md`: `@object-ui/app-shell` minor, `@object-ui/plugin-detail` minor, `@object-ui/react` patch (comment only). Unchanged: the `list_item` placement, the child object's own list view, and the row visibility gate (see measurement 3). ## Measurements (dispatch Zone 2) 1. **No surface read `record_related` (confirmed).** The four new pin files run against `e420df310f`: **11 failed, 6 passed**. The 6 that passed are the controls and the negative halves. This is commit `7c50fdac8a`, pushed before the implementation. 2. **How the bridge knows it is inside a record.** - `RelatedRecordActionsBridge` has one mount site, `RecordDetailView`. It wraps the record page body there, with `parentRecordId` set from the route's record id. - The list page publishes `listRecordActionsValue`, and its `resolve` answers `NO_RELATED_HANDLERS`. A related list under it therefore gets no row actions at all. - The child object's own list view is `ObjectView`. It composes `rowActionDefs` filtered by `list_item` alone. - The bridge's props document a mount with no parent ("Omit when there is no parent context (e.g. a standalone list)"). The rule is therefore explicit at the bridge rather than left to the mount site: `record_related` joins the row set only when `parentRecordId` is set. - The negative is pinned twice. The child's own list view does not carry a `record_related`-only action, with the `list_item` control carried by the same render. A bridge with no parent record does not place it either. 3. **The row gate: it is the second definition. Reported here and not widened.** - The related-list row menu path is: `RelatedList`, then the data table's `rowActionDefs`, then `planDataTableRowMenu` / `DataTableRowActionItem`. Both of those ask `isCustomRowActionVisible` in `packages/components/src/renderers/complex/data-table.tsx`. - That function has its own test (`pred == null` or `pred === ''`). It is not `hasDeclaredVisibilityGate`, which is core's `hasDeclaredPredicate`; that one also reads whitespace-only text and an empty envelope as undeclared. - `packages/plugin-grid/src/components/RowActionMenu.tsx` holds a second function under the same name. - `record_related` rows go through the same path as `list_item` rows, so this PR adds no gate. See Acceptance notes. 4. **The designer preview.** The `record_related` frame was the section-header frame shared with `record_section`. It now draws the `list_item` row frame, so no new frame family was needed. 5. **The refusal notice** names `list_toolbar`, `list_item` and `record_related`. It is pinned on the pure function and through the real renderer. ## Pins - `packages/app-shell/src/views/__tests__/RelatedRecordActionsBridge.recordRelated-11270.test.tsx`: the real bridge around the `{ type, properties }` node, rendered through the real `SchemaRenderer`, `@object-ui/plugin-detail`'s registration, the real `RelatedList` and the real data table. It covers: - a `record_related`-only action on the row, beside the `list_item` control; - an action declaring both locations renders once; - it runs against the clicked row, retargeted at the child object; - a bridge with no parent record places `list_item` only; - a named `record_related` id in the authored channel is accepted and nothing is refused; - the notice names the three locations. - `packages/app-shell/src/views/ObjectView.recordRelatedNotOnOwnList-11270.test.tsx`: the child's own list view carries `send_reminder` and `archive` (via its `list_item` half) and not `log_time`. - `packages/plugin-detail/src/renderers/__tests__/relatedListActions.recordRelated-11270.test.ts`: placement and notice on the pure functions. - `packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.recordRelatedRow-11270.test.tsx`: `record_related` draws rows and no section header. `record_section` keeps its header (the control). Each location draws its own frame. ## Verification (final head `b04aaba6d2`) - Red first on `e420df310f`: 11 failed / 6 passed. Green on the implementation: `Test Files 4 passed (4)`, `Tests 17 passed (17)`. - Ablation, run once with a revert trap, not a permanent test: - Mutation: `hasParentRecord ? ROW_LOCATIONS_IN_RECORD : ROW_LOCATIONS` became `ROW_LOCATIONS_IN_RECORD`, through `ablation-replace.mjs`. It landed: anchor 1 to 0, blob `c694d015fadb` to `727fa08500b1`. - Predicted and observed: 1 failed / 5 passed, and the failure is the parent-less case. - Restore proven: blob equals HEAD, and `git diff HEAD` is empty. - The first attempt was refused by the tool before running, because the replacement text already occurred in the anchor and its count could not rise. It was redone with a marker. - `pnpm exec vitest run packages/plugin-detail/`: `Test Files 228 passed | 1 skipped (229)`, `Tests 2270 passed | 8 skipped (2278)`. - `pnpm exec vitest run` over `packages/app-shell/src/views/metadata-admin/previews/`, `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`, and every suite `git grep -l` finds for `record_related`, `deriveActions` or `RelatedRecordActionsBridge`: `Test Files 105 passed (105)`, `Tests 1524 passed (1524)`. - Build: `turbo run build --filter='@object-ui/app-shell^...' --concurrency=2`, 28 of 28 tasks successful. - Type-check exit 0 for `@object-ui/plugin-detail`, `@object-ui/react` and `@object-ui/app-shell`. Each runs `tsc --noEmit && tsc -p tsconfig.test.json`, and `--listFilesOnly` confirms that all four new test files are in the test programs. - Repo checks, all exit 0: `check:action-forward-parity`, `check:component-surface-parity` (report-only), `check:handler-key-reads`, `check:new-line-citations` (`0 new citation(s)`), `changeset:check`, `check:control-bytes`, `node scripts/check-changeset-presence.mjs`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:doc-fences`, `check:doc-example-ids`, `check:doc-example-readers`, `check:action-ref-convention`, `check:i18n-dead-keys`, `check:unreferenced-sources`. - NOT MEASURED: - `check:doc-snippets` and `check:doc-examples` stopped with exit 2, precondition not met: seven packages outside this build's closure are unbuilt. The guide edit is prose only and changes no fenced block. - The repo-wide `pnpm lint` is left to CI. A narrowed `eslint --format json` over the 10 changed `.ts` / `.tsx` files reports 10 files and 0 errors. The warnings are the files' existing `no-explicit-any` / `react-refresh` / `set-state-in-effect` findings, and one `as any` in the new ObjectView test's data-source double, the same spelling its sibling suites use. `eslint.config.js` declares no type-aware parser project, so this diff cannot move a verdict on an untouched file. - Governed surface: `node scripts/check-governed-queue-guard.mjs --test` over the 12 changed paths answers NOT GOVERNED. ## Acceptance notes (observations, not filed) - **Row-gate second definition (measurement 3).** - `isCustomRowActionVisible` exists twice, in `data-table.tsx` and in plugin-grid's `RowActionMenu.tsx`. Each asks "is a gate declared?" with its own test instead of `hasDeclaredVisibilityGate`. - It differs in its reading of whitespace-only predicate text and of an empty envelope. - This is the same family as objectui#11244 (PR objectui#11275 moved the related-list toolbar onto the shared helper). - Not widened here, per the dispatch. Reach through a public door was not measured. - **Mobile.** Under the 768 px breakpoint a table-type related list renders as a card gallery with no row menu. Neither `list_item` nor `record_related` actions appear there. This predates this PR and is unchanged by it. - **`isRecordScopedAction`** (`packages/core/src/actions/serverActionHandler.ts`) lists `list_item`, `record_header`, `record_more` and `record_section`, but not `record_related`, although objectstack's runtime counts `record_related` as record-requiring. It only decides a toolbar launch with nothing selected, and a `record_related` action is never placed on the toolbar. - **Follow-on in objectstack**, not this repository: - PR objectstack-ai/objectstack#20948's "Declared, not yet placed" clause in the two docs location tables comes out at the objectstack `.objectui-sha` move that carries this merge. - The spec describe and docs wording follow on objectstack-ai/objectstack#20937. - **File surface.** The dispatch expected the bridge under `packages/plugin-detail/src/`. It lives in `packages/app-shell/src/views/RelatedRecordActionsBridge.tsx`, so `@object-ui/app-shell` carries both the bridge and the preview changes. --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20937
Clause-②: no
Pin measurement: the console at this repo's
.objectui-shaplaces norecord_relatedaction on related-list rowsRead at objectui
db11afd4967cd9d39381c5e21dc2deec9d706204, the pin onorigin/main31c39964fc, withgit grepagainst that commit (never objectuimain).git grep -w record_relatedanswers 12 lines in 7 files. The control leg is the same command forlist_iteminpackages/app-shell/src/views/RelatedRecordActionsBridge.tsx: 4 lines, so the read reaches the pinned tree.record_related.RelatedRecordActionsBridge.deriveActions(:190, called at:431and:442) places the child object'slist_itemactions on each row and itslist_toolbaractions in the header.record_relatedappears nowhere in that file.record:quick_actionspage-blocklocationoption (block-config.ts:536), andActionPreview.tsx:832-833(2). The preview drawsrecord_relatedas a button in a section header, the toolbar reading this card replaces.action-bar.tsx:30(docblock) anduseActionEngine.test.ts(2): the generic location filter. An author who places arecord:quick_actions/action:barnaminglocation: 'record_related'gets that bar where they put it. Nothing places the location by default.ROADMAP.md:1370: prose.record_related_list(containers.tsx:525,RelatedList.tsx:1632and one test), is a component type name, not this location.So the new words state the contract only, and they do not say the pinned console does it. The two docs rows describe where a button renders, so each says the console does not place it on those rows yet. They follow the docs corpus's own wording for this state ("declared, not yet applied" in
content/docs/ui/translations.mdx) and carry no tracker number.What changed
packages/spec/src/ui/action.zod.ts: therecord_relatedline of theACTION_LOCATIONSdocblock, in triage's words. It is now a per-row action on each row of a related list shown inside a parent record, in that parent's context only. Unlikelist_item(every row wherever the object is listed), it never surfaces on the object's own list views. JSDoc only: the enum, its accept set and theglobal_navrefusal are byte-identical.content/docs/ui/actions.mdxandcontent/docs/protocol/objectui/actions.mdx: therecord_relatedrow of each location table says the same, plus "Declared, not yet placed".docs/qa/platform-checklist/areas/records-forms.json, itemrecords-forms.action-location-matrix: the variant now reads "showcase_log_time on each row of the related-list section inside a record (the Tasks related list on a showcase_project record), and on no row of the showcase_task list view". One addition beyond the claimed line: the same item'srevisiongoes 5 → 6, with ahistoryentry. The checklist README's lifecycle rule for a changed item is "edit the fields, bumprevision, append ahistoryentry", because run records pin the revision they ran against. The entry also records that a run against the pinned console scores this variant as a missing placement, and that this is the correct verdict..changeset/20937-record-related-row-placement.md:@objectstack/specpatch, becausesrc/**/*.zod.tsships in the package'sfiles[].No generated file moves.
ActionLocationSchemahas no.describe(), and the docblock is not projected.check:generatedreports "All 15 generated artifacts are up to date", includingcheck:docs("227 generated files in sync").content/docs/references/ui/action.mdxlists only the allowed values forActionLocation.Verification, at
5e744bd8bfpnpm --filter @objectstack/spec build, thencheck:generated: all 15 up to date.pnpm --filter @objectstack/spec run typecheck: exit 0, includingcheck:scripts-typecheckandcheck:test-typecheck.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 584 test files passed, 17190 tests passed, 1 todo.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 100 commands. Each exit code was captured to a file before any pipe.--ranreconciled: "100 derived famil(ies) accounted for — 99 run, 1 NOT-MEASURED".PREREQUISITE NOT MET(lint, formula, client-react and objectql not built). They exited 0 afterturbo run buildof those four packages.pnpm check:platform-checklistexits 1 with 2 problems:coverage.json · picklist: UNCLASSIFIEDand an ABSENT SYMBOL inareas/identity-auth.json. Both are present at the base31c39964fc: the validator run in a detached worktree at that commit printed the same two problems. The edited item is not among them.pnpm check:dual-build-cjs-loads. It needs every workspace package built (86dist/directories absent), and this diff changes one JSDoc comment. CI'sLint & Repo Gatesruns it.check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity,check:meta-url-spellingandcheck:spec-changes.isPathIgnored/calculateConfigForFile): of the 5 touched files, onlyaction.zod.tsis linted, and the.mdx,.jsonand.mdfiles are outside it. ②--format jsonover all 5 lists 1 file: 0 errors, 0 warnings. ③eslint.config.mjsenables no type-aware linting (noparserOptions.projectorprojectService), so this diff cannot move any untouched file's verdict.The two nearby texts the dispatch asked to have measured (not changed here)
GLOBAL_NAV_RETIREDrefusal (action.zod.ts:609-622) says "Place the action on a location a renderer serves (…record_related…)". At the pin this is true only in the weak sense. The genericaction:bar/record:quick_actionsfilter renders arecord_relatedaction if an author places such a bar with that location. No surface places it by default. It becomes plainly true when the pin carries the renderer half. Changing it now would reword shipped refusal text twice, so I recommend leaving it.action.locationsrow inpackages/spec/liveness/action.jsonsays the "VALUE SET was audited per-member", which implies every remaining member is served. Forrecord_relatedat the pin that overstates: no default placement exists. It would take a note-only edit of that row: no status or count change, but apatchchangeset, becauselivenessships. Or leave it until the renderer half is at the pin.Acceptance notes
skills/objectstack-ui/rules/actions.md:11(the published skill) carries the same old wording: "Related-list section inside a record".skills/**is a Tier H surface and outside this card's surface. Carrier: none; it needs its own docs PR.packages/lint/src/validate-action-locations.ts:157: theaction-no-placementhint listsrecord_relatedamong the surfaces to add, which is the same weak-sense claim as (a).ActionPreview.tsx:832-833(at the pin) drawsrecord_relatedas a section-header button, the toolbar reading. It is a candidate for the renderer half's scope. That card's scope lists the host bridge, the authored channel, visibility and pins, not the Studio preview.origin/mainadvanced tof80e2a6dadafter the branch point. It touches none of these five paths orpackages/spec, so it was not merged in.Generated by Claude Code