… the Studio Records grid and console lists draw every row (objectui#12480) (#12502)
Fixes #12480
Clause-②: no
## Root cause
The rows are not missing: they are drawn, below the fold of the grid's
scroll box. One row is very tall.
- `data-table` keeps a cell to one line with `truncate` unless the
column is authored `wrap: true`. `truncate` is nowrap + overflow hidden
+ ellipsis. It bounds inline content only. It never bounds the cell
body's height.
- The three rich-content cell renderers (`markdown`, `html`, `richtext`,
the `RICH_TEXT_FIELD_TYPES` table in `@object-ui/fields`) draw a block
document: paragraphs, headings, lists. Blocks stack under nowrap.
- HotCRM's `crm_account` has a `description` markdown field. Its seed
gives Acme Corporation a long multi-paragraph description. The Studio
Records grid shows every non-system field as a column (37 here), so the
Description column is in the grid, off-screen to the right.
- Result: Acme's row grows to the document's height (456 px in
Chromium). Its visible cells are vertically centred in that row, which
reads as "a tall empty band, then the row". The rows after it sit below
the scroll box's bottom edge, and the footer still counts them.
- The committed screenshot fits this exactly: row 9 starts at y 586, and
a 456 px row centred there puts its text at y 814. The screenshot shows
it at y 815.
The trigger is the content, not the row count. The 8-row object in the
card did not reproduce it because it has no long rich-content value. An
8-row object with one does the same thing (table below).
PR #12479 is not the cause: the screenshot's commit `cf430cd` is an
ancestor of #12479's merge commit (`git merge-base --is-ancestor` exit
0).
## The fix
`ObjectGrid` (`packages/plugin-grid/src/ObjectGrid.tsx`). Every column
passes through `applyColumnChrome`. That function now also runs a new
step, `withOneLineRichText`. For a column whose field resolves to a
rich-content renderer and which is not authored `wrap: true`, it wraps
the renderer's output in a `line-clamp-1` box. The first line of the
document shows; the rest is cut.
- The renderer itself is unchanged, so its sanitising pipeline stays the
trust boundary.
- A column authored `wrap: true` still shows the whole document, as
authored.
- The record-detail drawer resolves its renderers separately, so it
still shows the full document.
Why not in `data-table`: it does not know field types. A generic height
clamp there would also clip legitimate one-line widgets that are taller
than one text line. Measured on the same harness: thumbnails 32 px,
select badges 22 px, against a line height of 16 to 19.5 px.
Measured per type on the harness: only `markdown`, `html` and `richtext`
exceed one line. Before the fix, at the first row: html 176 px and
richtext 104 px. textarea, json, address, multi-select, file, image and
code stay within 16–32 px. So the clamp covers the whole class.
## Browser readings (Chromium `/opt/pw-browsers/chromium`, 1280 x 900)
Scratch harness in `apps/console` (never committed, deleted after). It
uses the real `ObjectView` (plugin-view) with `renderStudioGridList` as
its `renderListView` slot, inside a `min-h-0 flex-1 overflow-auto` body
like the Data pillar's `data-records-body`. A mock data source supplies
`crm_account`-shaped fields, plus a `description` markdown field
carrying the Acme description on the last row. The "console" rows mount
`ListView` (plugin-list) directly with its default toolbar, which is the
body the console's object list page renders. No backend ran.
| case | rows | before: tall row / rows inside the box | after |
|---|---|---|---|
| Studio, plain text only (control) | 8 / 9 / 10 | none, all 41 px, 8/8,
9/9, 10/10 visible | not re-measured in the browser; the jsdom CONTROL
pin holds it |
| Studio, one long markdown description | 8 | last row 456 px, 7 of 8
visible | all 41 px, 8 of 8 |
| Studio, one long markdown description | 9 | last row 456 px, 8 of 9
visible | all 41 px, 9 of 9 |
| Studio, one long markdown description | 10 | last row 456 px, 9 of 10
visible, table 873 px in a 642 px box | all 41 px, 10 of 10, table 458
px |
| console list body, same data | 8 / 9 / 10 | last row 456 px; 7/8, 8/9,
9/10 visible | all 36 px, 8/8, 9/9, 10/10 |
Triage's second question: **yes, console list pages show the same
thing.** They render through the same `ObjectGrid`, and this fix covers
both.
## Tests (head `256cd24d4`)
- New:
`packages/plugin-grid/src/__tests__/ObjectGrid.richTextOneLine-12480.test.tsx`
(jsdom does no layout, so these read the class contract and the row
count).
- 10 rows with a markdown, an html and a richtext column: 10 body rows;
30 clamps (`line-clamp-1`), one per rich cell; each document is drawn
inside its clamp; the text column is not clamped.
- `wrap: true` on the markdown column: no clamp, and the list is drawn.
- CONTROL: an 8-row object with no rich-content column: 8 rows, no
clamp, the cell still `truncate`s.
- Ablation (fix committed in `d4005d878`, mutated with objectstack's
`scripts/ablation-replace.mjs --hold` under an EXIT trap):
- The guard line in `withOneLineRichText` was replaced by an
unconditional `return col`. On disk: anchor count 1 to 0, marker count 0
to 1.
- Result: the pin went red (1 failed / 2 passed), and the browser's
console rows above went back to the 456 px row.
- Restored with `git checkout HEAD`. `git diff HEAD` is 0 bytes, and the
file's blob equals the HEAD blob (`887bcfcb3989`).
- No dist step: the test imports `../ObjectGrid` by source path.
- `vitest run packages/plugin-grid/`: 216 files / 2015 tests passed (on
`d4005d878`). The new pin plus `ObjectGrid.columnWrapForward.test.tsx`:
8 / 8 passed on `256cd24d4`.
- `pnpm --filter @object-ui/plugin-grid type-check` (both `tsc`
projects; the test project includes `src/**/*.test.tsx`): exit 0, after
`pnpm --filter '@object-ui/plugin-grid^...' build` exit 0.
- No public export, prop or schema key changed, so the import-side
packages owe no tests (declared, CI runs them).
## Gates
- `check:phantom-deps`, `self-import`, `unreferenced-sources`,
`doc-example-readers`, `handler-key-reads`, `side-effects-array`,
`element-data-source-declaration`, `spec-symbols`,
`action-forward-parity`, `designer-field-key-parity`,
`icon-record-names`, `i18n-keys`, `i18n-drift`, `i18n-designer-parity`,
`control-bytes` and `new-line-citations`: all exit 0.
- `node scripts/check-changeset-fixed.mjs`,
`check-changeset-presence.mjs` and `check-changeset-no-major.mjs`: exit
0.
- `check:spec-floors`: exit 1, its "no-artifact" prerequisite line (it
wants a whole-workspace build). NOT MEASURED here. It is the known red
on main (objectui#12334).
- Lint, narrowed to the 2 changed files (`eslint --no-inline-config
--format json`). The test file has 0 errors. `ObjectGrid.tsx` has 1
error and 219 warnings; linted the same way, the base blob has 1 error
and 215 warnings. The one error is the same pre-existing
`react-hooks/static-components` far below the edit. The 4 new warnings
are `no-explicit-any` on the new step's `col` / `value` / `row` / schema
reads, the same spelling as its neighbour `withSortability`. The
repo-wide `pnpm lint` is CI's.
## Acceptance notes
- **Content, not count.** The card's "8-row case is fine" is true only
for an object with no long rich-content value. The pin's CONTROL keeps
that case unchanged.
- **`wrap: true` keeps the full document.** A rich-content column
authored `wrap: true` still grows its row to the whole document. That is
the author's choice, and this change keeps it.
- **Not measured.**
- The phone card layout (below 768 px). `ObjectGrid` resolves its own
renderers there, so this change does not reach it.
- Kanban and gallery cards, which draw the same renderers.
- A dotted lookup-path column (no field definition here, so not
clamped).
- **Merge.** `origin/main` was merged once more right before opening
(`256cd24d4`, a merge commit). PR #12481's region of
`StudioDesignSurface.tsx` is untouched: this diff does not edit that
file.
Changeset: `.changeset/12480-studio-grid-missing-rows.md`
(`@object-ui/plugin-grid` patch).
The dev run's session is `session_01XVp19E1MYUTFM3dnkwz8gB` (seat
`domain:ui#2`).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB)_
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #12454
Clause-②: no
Nothing on
@object-ui/app-shell's package entry changes.The Automations pillar takes the redesign of epic objectui#12446 (decisions 1, 3 and 5), on top of the layout skeleton (objectui#12459,
cf430cd).What changed
studio-design/flowGrouping.tsis a pure function: the served flow definitions and the scope's object index in, ordered groups out. A flow is grouped under the first answer from these steps, in order: (1) its Start node's object (config.objectName, orconfig.timeRelative.objectfor a time-relative sweep); (2) the first data node of its own graph, in graph order, on one of the package's objects; (3) the first record write anywhere in it, nested regions included. A flow with none of these goes in a trailing "No object" group. Groups are sorted by label, show a count and can be folded. A search filters within the groups.FlowMetaPills, renders them here and in the designer's info bar, so both read the same trigger and run status (objectui#11779). The Enabled switch is 36 × 20 px. Its title begins with "Off by default · review before enabling", and a visually hidden line carries the same sentence. The full-width band has been gone since the skeleton.The metadata-admin designer (FlowPreview / FlowCanvas)
The Studio look is scoped by a provider.
FlowPreviewChromeProvider, exported fromFlowPreview.tsx, carries the open section and the problem-count reporter. Under it, the preview draws no info bar and no frame. The designer does not provide it, so it keeps its info bar, its meta pills and its side panels unchanged.The canvas restyle in
FlowCanvas.tsx(sidebar surface, 20 px dots, zoom control bottom right) applies to both surfaces. I checked it with objectui'sverifyskill: the console preview gallery,preview-gallery.html?only=flow, run headless with Chromium on this branch. It showed 14 node cards, the info bar's Trigger and Status pills, Runs, Problems and Debug, the Problems panel opening beside the canvas, the zoom control, Add node, and no page errors.Pins kept, and one reversed as the card asks
StudioDesignSurface.flowCanvasChip-11665): unchanged from origin/main and green. The header shows the flow's label, not the API name. The API name is on the label's tooltip. The canvas hint stays in the header row as a visually hidden line, which also describes the Canvas tab.flowStatusTruth-11779), objectui#11785 (authorRefusal-11785), objectui#11786 (heldIncomplete-11786), objectui#11795 (narrowRow-11795), objectui#12303 (the switch's read-only description and wrapper): all unchanged and green.AutomationsPillar.railSearch-11794is updated: it reads rows by test id, so a group header is never counted as a row, and it pins the one-line label with the whole name in the row's title.Deviation list (against
docs/design/studio-redesign/automations.html)Label:captions (Trigger: screen · Status: On · Run as: user · v1) instead ofTrigger · screen, and adds Status. Seven pinned tests read the header'sStatus:pill by that text: flowStatusTruth-11779, narrowRow-11795, automationsReadOnly-11124, automationsReadOnlySelect-11546, autosaveInFlight-11204, flowRegistration-12349 and packageLessFlows-11553. Status also carries objectui#11779's rule that the header agrees with the rail.StudioRailGroupis the skeleton's shared component, andStudioRail.tsxtakes additive edits only.GET_RECORDkind word, 240 px width. The card markup isNodeCardinmetadata-admin/previews/flow-canvas-parts.tsx, outside this claim's file surface. The width is the layout engine'sNODE_W, and the card keeps layout as the engine's.inspectors/_shared.tsx(InspectorShell) andinspectors/FlowObjectListField.tsx, outside this claim's file surface. Only the double padding is fixed (the aside's body padding gives way to the inspector's own).objectstack devserves it. That package is read-only (code-provided), so the switch is disabled and no Add node shows. The artboard draws a writable package. The artboard's PNG also renders the rail wider than 240 px, so labels truncate sooner here. The row's title gives the whole label.Screenshots (1280 px)
docs/design/studio-redesign/after/12454-automations-light.png,docs/design/studio-redesign/after/12454-automations-dark.png. Taken from the local console (Vite on this branch) against HotCRM served byobjectstack dev(hotcrm63db83e), on "Enroll Members in Campaign" with "Campaign Open?" selected.docs/design/studio-redesign/after/skeleton-automations-light.pngand-dark.png(cf430cd).Grouping measured on HotCRM, re-read
Measured on hotcrm
63db83e, fromGET /api/v1/meta/flow?package=app.objectstack.hotcrmon a freshobjectstack dev. The package now serves 26 flows, not the card's 23. The three new ones each fall under an object that already had a group, so the ten groups stand:flowGrouping.test.tspins this over those definitions, trimmed to the keys the grouping reads and inlined in the test with the hotcrm commit named.Every HotCRM screen flow names its record on its Start node, so no action metadata is read: the "action that references the flow" step has no instance to serve, and it is not built. Forecast Snapshot first queries
sys_user, a platform object outside the package. It therefore groups under the record it writes,crm_forecast, as the card measured.Test evidence (HEAD
1de2c1d)Every test in
studio-design/that reaches the pillar, the rail,FlowStatusDotor the flow previews, plusflowGrouping.test.tsand the newAutomationsPillar.tabs-12454.test.tsx:pnpm exec vitest run --maxWorkers=2over 30 files givesTest Files 30 passed (30),Tests 273 passed (273).Every metadata-admin flow preview and canvas test, plus the other files that name the pillar or the previews: 28 files give
Test Files 28 passed (28),Tests 409 passed (409).pnpm --filter @object-ui/app-shell run type-check: exit 0.pnpm --filter @object-ui/app-shell run build: exit 0, dist completeness 1094 files.eslint --no-inline-configon the eight touched source files: exit 0, no errors.Ablation: one mutation per pinned behaviour, applied through
ablation-replace(the anchor hit once, the mutation was verified on disk, and the file was restored to HEAD's blob each time). All ten legs went red:Local gates:
node scripts/check-changeset-presence.mjs,pnpm check:i18n-keys,pnpm check:i18n-drift,pnpm check:i18n-designer-parity,pnpm check:new-line-citationsandnode scripts/check-changeset-no-major.mjsall exit 0. The full farm is CI's.Acceptance notes
AutomationsPillar.railSearch-11794.test.tsx(updated; the card namesAutomationsPillar.*as stay-green-or-update) and the newAutomationsPillar.tabs-12454.test.tsx, which pins acceptance 3. No other test pinned it.flow-canvas-parts.tsx,_shared.tsxandFlowObjectListField.tsxwere not touched (deviations 6 and 7). Restyling the node cards and the aside needs them added to a claim.NodeCardinflow-canvas-parts.tsx, notFlowCanvas.tsx.InspectorShellin_shared.tsx, and the outcomes areFlowObjectListField.StudioRailGroupand thefooterslot sufficed, andStudioRail.tsxis untouched.StudioDesignSurface.tsx, React Compiler'spreserve-manual-memoizationwarning now also reportsdoCreateFlow. The component already reported two of these atcf430cd, so the compiler was already skipping it. Removing this branch's line fromdoCreateFlowdoes not clear the new warning, and I have not found its cause.FlowPreview.tsxloses oneno-explicit-any.Session:
https://claude.ai/code/session_01SJn1GEkrbbBQkhGC26wEDwGenerated by Claude Code
flowGroupingcollates in the locale the pillar hands in (the locale is now required and the machine-locale fallback is removed); the censusmachineLocaleCensus-9909is green at2c87994. Patch round 2 removes the process line from the changeset text.