Found while implementing objectui#8459 (RelatedList's two emptiness spellings). Filed unassigned, ⛔ not claimed. Out of that card's fence: it is a defect in the shared authority itself, on a different surface (the record page body), not in the related-list grid.
What
hasCellValue — THE definition of emptiness for the record page (objectui#8376, widened by objectui#8394, extracted to packages/plugin-detail/src/emptiness.ts by PR #8457) — opens with:
if (value !== null && typeof value === 'object') return true;
typeof [] === 'object', so an empty array is a VALUE. Nothing further is asked of it.
Why that is wrong here, measured
The object half exists for a good reason, and its docblock states it: an object value is handed to a TYPE-AWARE renderer that knows how to draw it — coordinates, a postal address, select badges. But every example the docblock reasons about is a populated object; [] is never considered. And a type-aware renderer has nothing to draw for it. SelectCellRenderer tests value == null || value === '', which [] passes, and then maps it over zero options.
Measured on origin/main (a407bd6), rendering a real DetailSection with tags: [] on a select field:
FULL TEXT: "SNameWidgetTags"
has em-dash anywhere: false
count of em-dash: 0
TAGS ROW HTML:
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Tags</div>
The label renders; the value area is empty. No No value affordance, no em-dash — the visually blank cell the em-dash exists to prevent.
The three consequences are the same three objectui#8376 names
Because all three of DetailSection's reads share this one function, the empty array reproduces the full objectui#8376 triple:
- the row paints a blank cell (measured above);
- it escapes
emptyCount, so "Show N empty fields" reads one too low and revealing the empty rows does not reveal this one;
canCopy is hasCellValue(value), so the row offers to copy an empty array next to a value it did not draw.
shouldAutoHideEmpty needs only filledCount > 0, so a section whose only non-null value is [] can suppress the all-empty skeleton by itself — the objectui#8376 escalation, unchanged.
PR #8457 does not fix it
The extracted emptiness.ts in the merge queue carries the identical predicate (if (value !== null && typeof value === 'object') return true;), so this survives the extraction verbatim. Its docblock's reader list (DetailSection, HeaderHighlight, DetailView summary chips, HistoryTimeline, RecordMetaFooter) is the blast radius: each of those bands asks the same question and gets the same wrong answer for [].
There is already a measured counter-example in the repo
RelatedList.isValueEmpty — the related-list grid's own predicate, and the convergence target chosen in objectui#8459 — draws the finer line and has for some time:
(Array.isArray(v) && v.length === 0)
objectui#8459 measured both directions end-to-end: an all-[] column is correctly pruned there, and a surviving [] cell draws the placeholder. That card deliberately did not delegate to hasCellValue for exactly this reason, and pins the difference in RelatedList.emptinessAgreement-8459.test.tsx (A SURVIVING EMPTY-ARRAY CELL DRAWS THE EM-DASH), which is red for the hasCellValue shape.
Suggested shape (not settled — measurement should decide)
Narrow the object half from "every non-null object is a value" to "every non-empty object is a value": keep objects and populated arrays as values, and let [] fall through to EMPTY. {} deserves its own measurement rather than being assumed — JsonCellRenderer may well draw {} visibly, in which case it is a value and only the array clause moves.
⚠️ Whatever the shape, the pin must assert the rendered outcome and carry non-regression axes that stay green for populated objects and arrays — a predicate that answers EMPTY for every object would pass a naive absence-only pin.
Dedup
Searched objectstack-ai/objectui via MCP search_issues (REST is 403 from the dev container): RelatedList EmptyValue placeholder em-dash aria-label inconsistent hand-rolled span and RelatedList two emptiness spellings disagree pruneEmpty makeCell. Both returned objectui#8459 as a control hit, so the readings are real. Nothing covers the empty-array hole in the authority: objectui#8376, objectui#8394 and objectui#8350 are all about whitespace-only strings.
Found while implementing objectui#8459 (
RelatedList's two emptiness spellings). Filed unassigned, ⛔ not claimed. Out of that card's fence: it is a defect in the shared authority itself, on a different surface (the record page body), not in the related-list grid.What
hasCellValue— THE definition of emptiness for the record page (objectui#8376, widened by objectui#8394, extracted topackages/plugin-detail/src/emptiness.tsby PR #8457) — opens with:typeof [] === 'object', so an empty array is a VALUE. Nothing further is asked of it.Why that is wrong here, measured
The object half exists for a good reason, and its docblock states it: an object value is handed to a TYPE-AWARE renderer that knows how to draw it — coordinates, a postal address, select badges. But every example the docblock reasons about is a populated object;
[]is never considered. And a type-aware renderer has nothing to draw for it.SelectCellRenderertestsvalue == null || value === '', which[]passes, and then maps it over zero options.Measured on
origin/main(a407bd6), rendering a realDetailSectionwithtags: []on aselectfield:The label renders; the value area is empty. No
No valueaffordance, no em-dash — the visually blank cell the em-dash exists to prevent.The three consequences are the same three objectui#8376 names
Because all three of
DetailSection's reads share this one function, the empty array reproduces the full objectui#8376 triple:emptyCount, so "Show N empty fields" reads one too low and revealing the empty rows does not reveal this one;canCopyishasCellValue(value), so the row offers to copy an empty array next to a value it did not draw.shouldAutoHideEmptyneeds onlyfilledCount > 0, so a section whose only non-null value is[]can suppress the all-empty skeleton by itself — the objectui#8376 escalation, unchanged.PR #8457 does not fix it
The extracted
emptiness.tsin the merge queue carries the identical predicate (if (value !== null && typeof value === 'object') return true;), so this survives the extraction verbatim. Its docblock's reader list (DetailSection,HeaderHighlight,DetailViewsummary chips,HistoryTimeline,RecordMetaFooter) is the blast radius: each of those bands asks the same question and gets the same wrong answer for[].There is already a measured counter-example in the repo
RelatedList.isValueEmpty— the related-list grid's own predicate, and the convergence target chosen in objectui#8459 — draws the finer line and has for some time:objectui#8459 measured both directions end-to-end: an all-
[]column is correctly pruned there, and a surviving[]cell draws the placeholder. That card deliberately did not delegate tohasCellValuefor exactly this reason, and pins the difference inRelatedList.emptinessAgreement-8459.test.tsx(A SURVIVING EMPTY-ARRAY CELL DRAWS THE EM-DASH), which is red for thehasCellValueshape.Suggested shape (not settled — measurement should decide)
Narrow the object half from "every non-null object is a value" to "every non-empty object is a value": keep objects and populated arrays as values, and let
[]fall through to EMPTY.{}deserves its own measurement rather than being assumed —JsonCellRenderermay well draw{}visibly, in which case it is a value and only the array clause moves.Dedup
Searched
objectstack-ai/objectuivia MCPsearch_issues(REST is 403 from the dev container):RelatedList EmptyValue placeholder em-dash aria-label inconsistent hand-rolled spanandRelatedList two emptiness spellings disagree pruneEmpty makeCell. Both returned objectui#8459 as a control hit, so the readings are real. Nothing covers the empty-array hole in the authority: objectui#8376, objectui#8394 and objectui#8350 are all about whitespace-only strings.