Repository navigation
feat(app-shell): a module-internal record-preview card for any (object, id) pair (objectui#12029) - #12031
Merged
Conversation
…t, id) pair (objectui#12029) RecordPreviewCard resolves the target object's definition through useMetadataItem and reads the record through the given data source, then draws it with the record page's own rules: the ADR-0079 title ladder (getRecordDisplayName), the object label, and the highlight fields (deriveHighlightFields with detectStatusField, drawn by HeaderHighlight). Denied fields are removed before anything reads the row, and the $expand list is gated the way the record page gates its own. Four states: loading, readable, unreadable (one cause-free rendering for a missing record, a refused read, a failed read and a missing definition) and absent (the pair names no record; nothing is read). The strip runs in its own read-only InlineEditProvider so a host page's edit session cannot reach it. Not exported from the package entry and not registered as a component type. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ 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
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12029
Clause-②: no
What this adds
One module-internal component,
RecordPreviewCard, atpackages/app-shell/src/views/record-preview/RecordPreviewCard.tsx, with its tests beside it and apatchchangeset. It takesobjectName,recordIdand a data source, and renders a compact card for that pair. This is the A2 child of objectui#2763. Nothing re-exports it from the package entry and it registers no component type: registering it as a type page metadata can reference is B1's step, where it is first consumed.Premise check, on
main8f8f760fRecordCellshowsrecord_title || formatIdentity(record_id): a link when the readability probe allows one, plain text when it does not, and the tombstone or the cause-free label in the two dead-reference cases.RecordApprovalsPanelsits on the record page itself, so its target is the page's own record and it draws no target card.(object, id): none. Read:LookupField(its popover is a picker, its chip a label),RecordDetailViewinembeddedmode (the whole record page inside a drawer), plugin-dashboard'sRecordDetailDrawer(draws a row it already holds and reads nothing), the global-search hit cards (server hits, no definition read). What does exist is the record page header's three pieces:getRecordDisplayName(the title),deriveHighlightFieldswithdetectStatusField(which fields), andHeaderHighlight(how they are drawn). The card composes those three instead of adding a fourth rendering.useRecordReadabilitycannot be reused as code from app-shell. It lives inapps/console(pages/system/recordReadability.ts) and importsuseAdapterfrom@object-ui/app-shell, so importing it back would be a dependency cycle. The card keeps its RULE instead: an unreadable target says nothing about why, and it shows the inbox's own cause-free sentence, read by its existing keyapprovalsInbox.recordUnresolvable(no new pack key).ObjectStackAdapter.findOneresolvesnullon a 404, and the platform answers a by-id read of a record outside the viewer's row set with that same 404 on purpose. The objectui#8631 ruling forbids telling "deleted" from "not visible" (an existence oracle). So in this cardabsentmeans the REFERENCE is absent (no object name or no record id): nothing is read and the shared empty-value dash renders. A deletion the platform asserts (an approval'srecord_deleted) stays the caller's to render, as the inbox's tombstone does today.The four states, each pinned
role="status"card with the sharedcommon.loadingtextfindOnereturned the recordfindOneresolvednull, rejected (refused or failed) or threwEmptyValuedash, and zero readsRules reused, not restated
getRecordDisplayNamefrom@object-ui/core, the one ADR-0079 ladder the record page's H1 and breadcrumb read.deriveHighlightFields(def, detectStatusField(def)), the call the synthesized record page makes for its highlights strip: the object's declaredhighlightFieldsfirst, else the same heuristic.HeaderHighlight, the record page's own strip.withoutDeniedFieldsbefore the title and the fields read it, and the$expandlist is gated by the loaded policy the wayRecordDetailViewgates its own.InlineEditProvider. Without it, a card mounted on a record page would join that page's edit session: the host's draft would be laid over this record's values.Data path and cost
useMetadataItem('object', name), the console's existing hook. A cache hit when the provider already holds the object; otherwise one by-name read thatMetadataProviderde-duplicates in flight and caches. N cards of one object share one definition read, pinned against the realMetadataProvider: three cards of one object and one of another make exactly one by-name read per object.findOneper card, in the record page's request shape: no params, or$expandnaming only the reference fields the card shows. N cards for N ids are N record reads, not batched; a list surface that renders many cards (B2) should weigh that.useDataInvalidationre-reads in place, and the card keeps what it shows until the answer lands.Tests and reverse verification
All runs from the worktree root on HEAD
b3fe66cb0(the type-check ran on the same bytes just before that commit; nothing changed after it).pnpm exec vitest run packages/app-shell/src/views/record-preview/:Test Files 1 passed (1),Tests 13 passed (13). The pins: absent (four ways a pair can name nothing, zero reads), loading until the read answers, the title by the declarednameFieldover a record that also carriesname,$expandonly for a shown reference field, refresh in place on invalidation, one identical unreadable rendering for anullread, a refused read, a failed read, a throwing data source and a missing definition (the last with zero record reads), one definition read for N cards under the realMetadataProvider, a denied field neither drawn nor expanded, and a host edit session kept out.scripts/ablation-replace.mjsin wrap mode: each mutation's anchor had to hit once, the blob had to change, and the restore was proven by the blob hash equal to HEAD's and an emptygit diff HEAD. The test imports the module by relative path, so no build sits between a mutation and its run.return nullwhere the unreadable card is returned):Tests 1 failed | 12 passed, the unreadable pin. The first attempt was a no-op: its anchor was contained in its own replacement, so the tool refused it and ran nothing. It was redone with an anchor the replacement does not contain.String(row.name ?? '')in place ofgetRecordDisplayName):Tests 2 failed | 11 passed, the title pin and the N-cards pin.InlineEditProvideraround the strip:Tests 1 failed | 12 passed, the host-isolation pin.Gates
pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json)--listFilesOnlyon the test project lists both new filespnpm --filter @object-ui/app-shell builddist completeness: 1 package(s) completepnpm exec vitest runon the new test file and the 21 app-shell tests that enumerate files on diskTest Files 22 passed (22),Tests 174 passed (174)pnpm exec eslinton the two touched source filespnpm check:control-bytescheck-control-bytes: OKpnpm check:test-path-rootscheck-test-path-roots: OKpnpm check:changeset-claimsNo pending changeset names a file this change touches.pnpm check:pending-changeset-literalsNo test source names a pending changeset.node scripts/check-changeset-presence.mjs2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-major/check-changeset-fixedNo changeset declares a major bump./All workspace packages are in the changeset fixed group.pnpm check:i18n-keysEvery in-scope call-site key resolves against the en packpnpm check:i18n-drift/check:i18n-dead-keyspnpm check:new-line-citations0 new citation(s)pnpm check:phantom-deps/check:self-importEvery in-scope import is declared/No package names itselfpnpm check:published-dist/check:esm-specifiers/check:unreferenced-sourcesThe built entry declarations do not reach the module:
dist/index.d.tsanddist/index.jshave zero hits for it (control:RecordDetailViewhitsdist/index.d.ts), and no other emitted file imports it. No locale pack and no export changed, so the pack-parity andcheck:readme-exportsgates are not owed.Lint is a declared narrowing. The population is what eslint's own config applies to these files (
--print-configon the component lists 118 rules,react-hooks/exhaustive-depsamong them); the file count, 2, is read from--format json; and the root config enables no type-aware linting (noparserOptions.projectorprojectService), so this diff cannot move any untouched file's verdict. The repo-widepnpm lintis CI's.Acceptance notes
highlightFieldslist longer than four is capped at four byderiveHighlightFields' default. The record page's own path hands the declared list on uncapped. Same fields, same order; the card shows the first four.RecordDetailViewalso reads{ name }descriptors out of an object'shighlightFields, which the spec declares as an array of strings. The card reads only throughderiveHighlightFields, which keeps strings.Not measured
packages/app-shell/vitest run. Started under the shared lock with--maxWorkers=2and stopped by me after 23m27s (exit 143, no results printed) because two other agents were queued behind it. Replaced by a declared narrowing: the diff adds only new files that nothing imports, so an existing app-shell test can reach them only by enumerating files on disk. The 21 tests that do were found by grepping the package's tests forreaddirSync,globSync,readdir(,walk(,listSourceFiles,import.meta.glob,fs.promises,opendir,tinyglobbyandfdir, and all of them ran green above. Blind spot: a scan through a helper none of those words names. Repo-wide census tests in other packages are CI's.Dispatched from objectui#9800 (seat
domain:ui#3), sessionhttps://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8.Generated by Claude Code