Repository navigation
feat(plugin-detail,types,cli): record blocks honour the per-element dataSource binding (objectui#12294) - #12299
Conversation
…ataSource binding record:details, record:highlights and record:quick_actions now read their record from the node's `dataSource` binding when one is set: the gate resolves the binding as on every bound block, and a shared one-record scope reads the first row of the bound set under the caller's identity and provides it through RecordContextProvider. Unbound, the record page path is unchanged. The strict authoring face declares the binding on the three arms by reference to the spec's ElementDataSourceSchema, and `objectui validate` warns on a bound record block without `limit: 1`. Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
…locks; tests The gate, the one-record scope and the declaration seam move into one `recordBindingBlock` wrapper, so a record block takes the binding one way and cannot forget the seam. Tests pin each block on an app page with one, zero and two bound rows, a refused and a failed read, the record page path, the bound scope's isolation from the page's inline-edit session and highlight registry, the data-invalidation bus, the validate warning and the strict face. Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
The data-source guide's consumer table gains the three one-record blocks and a paragraph on their semantics; the plugin-detail README documents the binding. Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
… ledgers The repo-wide registry parity test gains member pins for record:details, record:highlights and record:quick_actions `dataSource`; the two per-block parity tests take the node-level spec keys the related list's already takes; the binding wrapper names its props type the renderer-props way. Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
… ledger Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
…ntrol Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
Seat ACCEPT: objectui PR #12299 at
|
Seat REWORK: objectui PR #12299 at
|
… key
The binding-doc ledger in objectname-binding-required-11605 read the three
one-record rows ("its record") as objectName blocks. Their binding lands on
the record scope, not on a schema key, so the "lands on no key" exception is
now exactly `element:*` plus record:details, record:highlights and
record:quick_actions, named one by one; record:related_list keeps objectName,
no `object-*` block can fall into it, and the doc says why.
Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z
Co-authored-by: Claude <noreply@anthropic.com>
✅ 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#12294 (body and all five comments: claim 6137033601, pointer 6137101274, ruling B 6137246293, dev reports 6137889031 and 6138204025), PR #12299 (body, 20-file list, net diff against ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong.
② Semver level
③ Boundary flagsEvery deviation in the latest
Noted, not blocking (the dev's acceptance notes, agreed): a bound Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Generated by Claude Code |
Refs #12294 (record:details, record:highlights, record:quick_actions; the record:related_list half is carved out, see the claim)
Clause-②: yes (widening)
What widens: the three record-block registrations publish the injected
dataSourceinput, and@object-ui/typesdeclaresdataSourceon their three strict-authoring-face arms (the spec'sElementDataSourceSchema, by reference), so a binding both the html tier andobjectui validaterefused before is now accepted on these three blocks.What this does
Maintainer ruling on objectstack-ai/cloud#2839 Q1, option D, as the card quotes it: close the real gap.
record:details,record:highlightsandrecord:quick_actionsnow honour the per-elementdataSourcebinding the spec already declares on every page component, so a productapppage composes a record's surfaces from blocks, with no record shell to switch off.recordBindingBlock(packages/plugin-detail/src/renderers/recordBinding.tsx) wraps each block body inElementDataSourceGate(the binding resolves exactly as on every bound block: namedview, AND-combinedfilter,sort, the family's configuration panels) and a one-record scope, and passes the result through theelementDataSourceBlockseam from@object-ui/core. The block bodies are unchanged except one guard inrecord:quick_actions(below).find(object, { $filter, $orderby, $top: 1, $expand }), with{current_user_id}-style tokens resolved (useResolvedFilter) and reference fields expanded only where the loaded field policy lets the caller read them, as the record page asks. The FIRST row is provided through the existingRecordContextProvider(object name, record id, readable row, object definition, adapter).classifyLoadErrorreportsforbidden), mount the block under a context that names the object and no record, so each block draws its own no-record state:record:detailsDetailView's "record not found",record:highlightsnothing,record:quick_actionsits empty bar. More than one row: the first by the binding'ssort(one row is ever asked for). A read that fails any other way is reported in the block's place with the family's data-source error panel, not drawn as an absent record.record:quick_actionsruns the object's declared actions against the bound row's id. Its one body change: a record context that names an object but no record offers no record action (the 0-row state); the record page always names its record.useDataInvalidation(object)from@object-ui/reactdata-invalidation.ts, the same busRecordDetailViewkeys its record read on. Object-scoped, because the binding is a query: a write to any row can change which row is first. A refetch replaces the row in place, and an equal answer keeps the row object it replaces (AGENTS.md [WIP] Enhance every detail of the designer #10). No second bus.BoundRecordScoperenders the body as it is: no provider, no element, no read. Bound and placed on a record page, the binding wins for that block ("Overrides the page-level object context for this block"), and the bound scope is kept out of the page record's inline-edit session (it mounts a read-onlyInlineEditProvider, so the page's Save bar can never write bound fields onto the page record) and out of the page's highlight-field registry (its ownHighlightFieldsProvider).objectui validatewarning. Measured:objectui validateruns the strict authoring face inpackages/cli(validateAuthoredDocument), notsdui-parser'svalidateTree. The warning lives there, once, in the success branch'sWarnings:block (packages/cli/src/utils/record-block-binding-hint.ts): a bound record block whose binding has nolimit: 1is named by path and type.record:related_listis never named.@object-ui/types. Measured at the merge base: the strict face refuseddataSourceon all three blocks (Undeclared key "dataSource" at (root) (type "record:details"), exit 1), so the warning above was unreachable and authors were told the one key that now works does not exist. Each arm now declares the binding by reference, asrecord:line_itemsdoes; the TypeScript authoring face (PublicBlockNode) follows from the arms.Zone 2 measurements (PM mechanism assumptions)
45ae6b8: the three bodies read the record only viauseRecordContext(); none wrapped the gate or the seam.composeElementDataSourcecomposes; neither reads a row. The card'sresolveElementDataSourceexists only asViewDataProvider.resolveElementDataSourcein@object-ui/core, which the render path never calls (ListViewBlock's docblock records it "had no caller outside its own test"). Used path:ElementDataSourceGate(useElementDataSource+composeElementDataSource) with the mapping{ filter: true, sort: true }, plus the new one-record scope. The scope lives in@object-ui/plugin-detail, not in@object-ui/reactor@object-ui/core: it needs@object-ui/permissionsfor the field-policy-gated$expandand readable row (@object-ui/reactdoes not depend on it), and the wholerecord:*family lives there. No new export from any package entry.check:element-data-source-declaration) discovers by file, with no roster to edit. The shared wrapper file consumes the gate and calls the seam from core, so the static census listsrecordBinding.tsx; that the three registrations declare the key is asserted per block inrecordBinding.appPage-12294.test.tsxand by the live-registry probeapps/console/src/__tests__/element-data-source-input-injection.test.tsx. Measured: all 309vi.mock('@object-ui/react', …)call sites onmainspread the actual module, so the hand-listed-mock hazard has no live population today; this change adds no@object-ui/reactexport anyway.objectui validatebullet above.dataSource,innerHTMLSHA-256 prefix at45ae6b8's block files and at this branch's: details22c708ac33a9ec77, highlights06f18891e29f52c7, quick_actionsc482317ba20672b7on both legs (scratch test, not committed; BASE files checked out over the committed implementation and restored from HEAD,git diff HEADempty after). The override direction is pinned inrecordBinding.recordPageAndBus-12294.test.tsx.Published surface
@object-ui/plugin-detail: therecord:details,record:highlightsandrecord:quick_actionsregistrations gain the injecteddataSourceinput (ELEMENT_DATA_SOURCE_INPUT, throughelementDataSourceBlock). No new export;recordBindingBlockis package-internal.@object-ui/types:RecordDetailsBlockSchema,RecordHighlightsBlockSchemaandRecordQuickActionsBlockSchemagain an optionaldataSourcemember, the spec'sElementDataSourceSchemaby reference;PublicBlockNodegains it on those three node types by derivation. No new export.@object-ui/cli:objectui validateprints one new advisory warning kind; exit codes unchanged..changeset/12294-record-blocks-element-datasource.md,minorfor the three packages.Tests
All at
4f59ed1, repo-rootpnpm exec vitest run, through the shared verify lock.packages/plugin-detail/src/renderers/__tests__/recordBinding.appPage-12294.test.tsx(each block through the real registration andSchemaRendereron anapppage: one row, zero rows, a refused read, two rows stored out of order, a failed read, an unresolvable view, the declared input;record:pathas the control),recordBinding.recordPageAndBus-12294.test.tsx(unbound reads the page record and reads nothing; bound overrides; bound details out of the page's inline-edit session; bound highlights does not trim the page's details; bus re-read after an adapter write and after a hand-announced write; no re-read for another object),packages/cli/src/__tests__/validate-record-block-binding-limit-12294.test.ts,packages/types/src/__tests__/record-block-data-source-arms-12294.test.ts.apps/console/src/__tests__/registry-inputs-spec-parity.test.ts(threeMEMBER_PINS),recordDetailsInputs.spec-parity/recordHighlightsInputs.spec-parity(node-level spec keys, asrecordRelatedListInputs.spec-parityalready takes them),public-block-responsive-styles-10872(DECLARES_DATA_SOURCE),zod-mirror-parity(exclusion prose).4f59ed1:Test Files 12 passed (12),Tests 642 passed (642).packages/plugin-detail/+packages/react/src/element-data-source/+packages/react/src/hooks/+packages/core/src/data-scope/: 312 files;packages/types/+packages/cli/+ plugin-detail tests: 654 files, all green after the ledger updates above. Every suite that namesRecordDetailView,PagePreviewor one of the three blocks outside those packages (202 files, enumerated bygit grep):157 passed(app-shell) and45 passed(the rest).@object-ui/plugin-detail,@object-ui/cli,@object-ui/types,@object-ui/consoletype-check, exit 0.Ablations (one-time,
ablation-replace.mjs, anchor hit once, blob changed, restored to HEAD withgit diff HEADempty)useDataInvalidation(…)replaced by0): the two bus pins red (re-reads the bound row after a write through the adapter…,…announced on the bus by hand), the other five green.a bound record:details does not join the page record's inline-edit sessionred.a bound record:highlights does not trim the page record's details bodyred.Gates
Exit 0 at
4f59ed1:check:element-data-source-declaration,check:component-surface-parity,check:sdui-registration-pins,check:vi-mock-specifiers,check:vi-mock-inherit,check:registry-bare-names,check-changeset-presence,check-changeset-no-major,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations,check:control-bytes,check:test-path-roots,check:eager-closure,check:doc-json,check:phantom-deps,check:unused-deps,check:unreferenced-sources. Also exit 0 on this branch:check:doc-snippets,check:doc-examples,check:readme-exports,check:doc-fences,check:doc-types,check:i18n-keys,check:spec-symbols,check:handler-key-reads,check:governed-queue-guard --test(NOT GOVERNED). The build-reading gates ran over a console-closure build of this branch.ESLint, proven narrowing: the population is
eslint.config.js's**/*.{ts,tsx}blocks;--format jsonover the 16 touched code files reports 16 files, 0 errors (70 warnings,no-explicit-anyandreact-refresh/only-export-components, the families these files already carry); the config sets noparserOptions(no type-aware linting) and no rule ineslint-rules/reads the disk, so this diff cannot move a verdict on an untouched file.pnpm lintis left to CI.Acceptance notes (observed, not filed)
RecordDetailViewandPagePrevieweach hand-roll "fetch a record and feedRecordContextProvider", and this change adds a third, by-query reader with a private copy ofRecordDetailView'sreadableRow(module-private in@object-ui/app-shell, which depends on this package, not the reverse).recordBinding.tsxis a natural home for the one internal core objectstack#4444 calls hygiene; neither host is refactored here.apppage a boundrecord:detailsis read-only: there is no record-level inline-edit host there, and the bound scope mounts a read-only session on purpose.record:quick_actions' 0-row bar says "no actions configured", the placeholder it already drew with no record context at all.#12294 remains openfor therecord:related_listhalf, as the claim states. Session:https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z.Generated by Claude Code