Repository navigation
feat(plugin-detail): record:related_list reads its declared actions; host-bridge actions stay the default (objectui#11163) - #11263
Conversation
…, host-bridge actions stay the default (objectui#11163) The maintainer ruled ENFORCE on objectstack-ai/objectstack#20665: `record:related_list.actions` ("Action IDs available for related records") was declared, published and parsed, and no renderer read it. The renderer now reads it. Absent, the host bridge's actions render exactly as before and no lookup is made. Authored, the authored list is what renders, in authored order: each id resolves against the related object's registered actions (useMetadataItem + resolveDeclaredActionIds, the lookup record:quick_actions.actionNames uses) and is placed by its own locations (list_toolbar -> header button, list_item -> row menu), run by the host's executor. `[]` draws no actions. An id that does not resolve, or whose action declares no list location, is named in a role="status" notice on the list instead of being dropped. The parity ledger's NO_READ_SITE_TO_PIN exemption becomes a member pin; MEMBER_PIN_EXEMPTION_CEILING 5 -> 4 (the four remaining units are the objectui#11111 bookings owed to objectui#11168). Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#11163) The slotted-pages guide gains a section on naming a related list's actions: the host default, the authored list replacing it in authored order, placement by each action's own locations, `[]`, the built-in affordances the key does not govern, and the refusal notice. Changeset: @object-ui/plugin-detail minor, since a related list that already authors `actions` now shows exactly those actions. Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card objectui#11163 (body, claim ① Derived judgmentsAccept set — unchanged, right. The spec key is untouched (the ruling keeps it declared). The registration's Public surface —
Loud refusal — author-visible, never a silently dropped button. Right. Parity ledger — right, and the 5 → 4 account verified against main. At merge base Measured, not changed — confirmed. Docs ( Check-runs on the head, at review time: 32 success (Type Check, Lint, Build & E2E, Test (dist pins), the five Changeset gates, every doc gate, Line Citation Gate, Governed Surface Queue Guard, Spec-free gates), 3 skipped (coverage ×2, dependabot), and 7 ② Semver level
③ Boundary flagsDev deviations, each answered:
Out-of-scope findings, judged:
Also noted, not blocking: the registration's Implemented-by: VERDICT: PASS |
Fixes #11163
Clause-②: yes
What this does
record:related_listnow reads its declaredactionskey and renders the actions it names. The key isRecordRelatedListProps.actions, described as "Action IDs available for related records". This carries out the maintainer's ENFORCE ruling on objectstack-ai/objectstack#20665 (batch #247, 「业务上需要支持,ENFORCE」). Until now the key was declared, published and parsed, but no renderer read it.The composition rule (stated here, pinned in the new test file)
actionslist_toolbaractions as header buttons and itslist_itemactions in each row's menu, in registration order. No metadata lookup is made for the key.actionsand is placed by that action's ownlocations:list_toolbargoes to the header,list_itemgoes to the row menu. The host still runs it.[]role="status"notice on the list. The entries that do resolve still render.The built-in New / Edit / Delete / View affordances are not action ids, because the runtime ships no built-in action names. They stay governed by the child object's
userActionsand the user's permissions, whateveractionssays.actions: []keeps New, and the pin asserts it.Measurements taken before building
These cover the card's caveat and the dispatch's mechanism assumptions.
ae0b9d3905the renderer never touchedschema.actions, and the parity ledger exemptedrecord:related_list.actionsasNO_READ_SITE_TO_PIN.Tests 10 failed | 2 passed (12).expected [ 'invite', 'export_csv' ] to deeply equal [ 'export_csv', 'invite' ]. The bridge default was drawn and the authored order was ignored.useMetadataItem('object', childObject)plusresolveDeclaredActionIdsfrom@object-ui/types.record:quick_actions.actionNames,page:header.actionsandrecord:alertresolve through, over the same MetadataProvider the bridge'sobjectscome from. There is no second registry.page:header's transition tolerance, andrecord:alertmade the same choice for its CTA.RelatedRecordActionsValue.resolvetakes{ objectName, relationshipField, parentId }.RelatedRecordActionsBridge.deriveActionskeys on the child object'slist_item/list_toolbarlocations, so two lists of one child object get one set.actionsis therefore the per-list channel, not a duplicate of the bridge's. The bridge is not modified.inputs.record:related_listalready declares{ name: 'actions', type: 'array', of: 'string' }, sopackages/plugin-detail/src/index.tsxis untouched.RelatedToolbarButton'svisibletruthiness gate). Authored toolbar actions do pass through that button. No pin fixture declaresvisible, so the defect does not affect any pin here. It stays with that card.Where the refusal is, and why there
The refusal is drawn in the block, where the lookup answers. It is an inline
role="status"notice above the list, naming each refused entry and the reason. There are three reasons:list_toolbarnorlist_item. Naming an action does not bypass location filtering: the platform's action docs make a list view's selection bar the only surface where naming an action places it;Three properties keep the notice truthful:
This is the smallest loud refusal the measurement supports. An authoring-time refusal would belong to objectstack's lint (see Acceptance notes).
Running an authored action, and the no-host case
An authored action is handed to the bridge's own executor,
onToolbarActionoronRowAction. That executor already runs the child object's actions against the clicked row through the page's ActionRunner. An authored surface is passed down only together with that executor, so no dead button is ever drawn. With no host (the Studio designer, a standalone embed) the list stays read-only, as it always has for every affordance. A bad id is still refused there.Parity ledger
record:related_list.actionsmoves fromMEMBER_PIN_EXEMPTIONStoMEMBER_PINS, pointing at the new pin file.NO_READ_SITE_TO_PINis deleted together with its one entry. A note is left where it stood, the wayAWAITING_A_PINwas retired.MEMBER_PIN_EXEMPTION_CEILINGgoes 5 → 4, not 1 → 0 as the card was written. The objectui#11111 decision 3 = B bookings (fourelement:*units owed to objectui#11168) landed between the card and this change. Those entries are untouched and move only with that card. The ceiling's docblock records this.Files
packages/plugin-detail/src/renderers/record-related-list.tsx: readsschema.actions, looks it up, composes it with the host, and draws the notice.packages/plugin-detail/src/renderers/relatedListActions.ts(new): the pure resolve-and-place rule and the notice text. It is a separate module so the renderer file keeps exporting components only.packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.authoredActions-11163.test.tsx(new): the pins, 12 cases, through the realSchemaRenderer, this block's registration, the realRelatedListand the real data table.apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: the exemption becomes a member pin, and the ceiling goes 5 → 4.content/docs/guide/slotted-pages.md: a section on naming a related list's actions. This is outside the claim's file list and is added under AGENTS.md commandment Add automated testing infrastructure and CI/CD workflows #2 (docs-driven). No package README is touched, because PR objectui#11246 ownsplugin-detail's..changeset/11163-related-list-actions.md:@object-ui/plugin-detailminor. A page that already authorsactionson a related list now shows exactly those actions.Tests
The runs below are at head
95a8cd520e, which is this branch after mergingmainataf9e9572c7(PR objectui#11246 landed while this was in flight). Heavy runs went through the shared verification lock. Each result below is the gate's own verdict line.pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' buildgaveVERDICT command-exit 0.pnpm --filter @object-ui/plugin-detail type-check(its output echoes the script nametype-checkfor@object-ui/plugin-detail@17.6.0) gaveVERDICT command-exit 0.tsc -p tsconfig.test.json --listFileslists the new test file, the new module and the renderer.pnpm exec vitest run packages/plugin-detail/src/__tests__/:Test Files 144 passed | 1 skipped (145),Tests 1296 passed | 8 skipped (1304).pnpm exec vitest runover the other 83packages/plugin-detailtest files:Test Files 83 passed (83),Tests 962 passed (962).plugin-detailthatgit grep -l 'record:related_list'finds, 25 files includingregistry-inputs-spec-parity.test.ts:Test Files 25 passed (25),Tests 1020 passed (1020).ablation-replace.mjs): removing the in-flight guard inrecord-related-list.tsxmade exactlyCONTROL: no refusal is drawn while the lookup is still in flightfail (Tests 1 failed | 11 passed (12)). The file was restored to blob35704b30774d, equal to HEAD, andgit diff HEADwas empty.check:action-forward-paritycheck:component-surface-parity(report-only, norecord:related_listrow)check:handler-key-readscheck:new-line-citations(0 new citation(s))check:control-bytescheck:doc-fencesdocs:check-linksnode scripts/check-changeset-presence.mjsnode scripts/markdown-test-inputs.mjs --auditcheck:doc-typescheck:doc-example-idscheck:docs-route-closurecheck:doc-example-readerscheck:changeset-claimscheck:pending-changeset-literalscheck:test-path-rootscheck:unreferenced-sourcescheck:spec-symbolscheck:phantom-depscheck:element-data-source-declarationcheck:action-ref-conventioncheck:self-importeslint --no-inline-config): 0 errors. The 17 warnings all sit on lines this diff does not add.check:doc-snippetsandcheck:doc-examplesexited 2 (PRECONDITION NOT MET, 34 packages unbuilt). They read TypeScript fences and@exampleblocks, and this diff adds ajsonfence.check:sdui-registration-pinsexited 2 (no console build). No registration changes here.apps/consoletype-check and repo-widepnpm lintare left to CI.Acceptance notes
Observations, not filed:
action-name-undefinedlint (objectstack#20105) deliberately does not walkrecord:related_list.actions; its header says "actionsis declared separately onrecord:related_list". Now that the key renders, a misspelled id is refused only at runtime, by the notice. Extending that walk would add the authoring-time refusal. Carrier: the seat that settles spec(ui):record:related_list.actionsis declared ("Action IDs available for related records") and read by no renderer — ENFORCE or RETIRE under ruling A′ ④ (the remainder of objectui#8071) objectstack#20665 and flips its liveness row.record_relatedlocation. The spec'sACTION_LOCATIONSdescribes it as "actions on a related list section inside a record", and the platform's action docs list it as "Related-list sections". This list, like the bridge before it, places actions bylist_item/list_toolbar. An action that declares onlyrecord_relatedand is named inactionsis refused by a notice that names the two list locations. This was not measured through a public door. Carrier: none.onRowAction/onToolbarActiononly when its own derived list for that surface is non-empty. Authored placement uses the same location rule over the same registry, so a placeable authored action implies a non-empty derived list whenever the bridge'sobjectsand the MetadataProvider agree. If they ever disagree, the authored surface is withheld rather than drawn dead. Carrier: none.actionsdescription is still the spec's describe text verbatim and does not state the composition rule. The docs guide now does. Carrier: none.The session that produced this change is
https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ.Generated by Claude Code