Repository navigation
fix(plugin-kanban): decline to draw a card badge with no label - #8733
Merged
Merged
Conversation
`ObjectKanban`'s explicit-card-field loop opens with `if (raw == null || raw === '') continue;` — which an empty array passes — and then forks on `isPicklist`, true for any field carrying declared `options`. That fork never calls `getCellRenderer`, so the shared `@object-ui/fields` renderers' rule that an empty array is not a cell value cannot reach it: it resolves a label and a colour itself and pushes a plain object onto `cardBadges`. With nothing to resolve the label came out as the empty string, and the card drew a fully styled, fully coloured pill with no children in it — on the most commonly authored shape of the three, because a picklist that declares its options is the normal case. Both `cardBadges.push` sites now decline a badge whose resolved label is empty. That is narrower than teaching the kanban what an "empty value" is: at the push site the only question left is whether there is a label to draw, and asking exactly that also closes the half that is independent of arrays — any value resolving to an empty label drew the same pill, the empty array being merely the shape that is easy to hit. The comparison is against the empty string rather than falsiness, so a legitimately authored '0' label keeps rendering. The legacy semantic-field heuristic that runs when a board authors no `cardFields` resolves its label the same way and had the same defect; it is repaired in the same shape, skipping the push rather than breaking so an unlabelled field cannot consume one of the two badge slots. Pinned at the DOM, not at `cardBadges`: counts taken from rendered nodes scoped per card, with the counting selector proved to match exactly one node per badge. Every populated badge is pinned byte-for-byte against markup captured before the repair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
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
|
os-justin
marked this pull request as ready for review
September 9, 2026 01:46
os-justin
enabled auto-merge
September 9, 2026 01:46
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 #8489
What was wrong
ObjectKanban's explicit-card-field loop opens withif (raw == null || raw === '') continue;— which an empty array passes — and four lines later forks onisPicklist, true for any field carrying declaredoptions. That fork never callsgetCellRenderer, so objectui#8481's landed rule for the shared@object-ui/fieldsrenderers ("an empty array is not a cell value") cannot reach it: the branch resolves a label and a colour itself and pushes a plain object ontocardBadges.With nothing to resolve, the label came out as the empty string and the card drew a fully styled, fully coloured pill with no children in it — on the most commonly authored of the three shapes, because a picklist that declares its options is the normal case.
Addresses re-located by content on
5ece49bbc(the card's line numbers were from7cf6f38fb): the guard is still at line 604, theisPicklistfork at 605-609, and the badge push at 629. They match the card exactly — nothing had moved.The repair
Both
cardBadges.pushsites now decline a badge whose resolved label is empty. That is deliberately narrower than teaching the kanban what an "empty value" is — at the push site the only question left is is there a label to draw? — and asking exactly that closes the half of the defect that has nothing to do with arrays: any value resolving to an empty label drew the same pill, the empty array being merely the shape that is easy to hit.The comparison is against the empty string, not falsiness, so a legitimately authored
'0'label keeps rendering.Second push site, repaired in the same shape (declared)
cardBadges.pushoccurs twice in this file. Besides the explicitcardFieldsloop, the legacy semantic-field heuristic — the branch that runs when a board authors nocardFieldsat all — resolves its label the same way (the file's own comment says so) and had the identical defect, reproduced below. It is repaired identically, skipping the push rather than breaking so an unlabelled field cannot consume one of the two badge slots. Same file, same defect class, same gate family, and it is pinned at the DOM alongside the dispatched branch; a repair on one alone leaves the pill drawable from the other.Evidence
All measurements at
6e3b9ffe5. DOM shapes are spelled in words below on purpose — tag-shaped fragments are eaten from GitHub bodies even inside backticks.Leg 1 — reproduced before repairing
packages/plugin-kanban/src/ObjectKanban.emptyBadgeLabel.test.tsx, run on the unrepaired tree: 4 failed, 5 passed. Each of the four failures printed the pill that should not exist — a lone division element whose class list is the badge primitive's own base (inline-flex, items-center, whitespace-nowrap, rounded-full, border, px-2.5, py-0.5) plus the indigo colour set, with no children and no text at all between its opening and closing tags. Reproduced for both shapes, on both branches.Leg 2 — repaired, and populated badges byte-identical
After the repair the same file is 9 passed, and the whole package is 35 files / 232 tests passed. Byte-identity is not asserted by construction:
POPULATED_BADGE_HTMLandLEGACY_POPULATED_BADGE_HTMLin the test file are the outer markup of the four populated pills captured from a run before the repair, and the suite compares the post-repair markup against those captured strings.Leg 3 — non-regression, derived from the two plausible wrong fixes
Both mutate a read site (the new guard), never a pin, from a committed tree; each proves the mutation on disk in both directions (anchor counts plus
git hash-objectagainst the HEAD blob) with a line-total gate, and restores by state (git diff HEADclean and the blob back toef675ce63). Per-test classification comes from vitest's JSON reporter.Leg 3a is reported as negative, not green. The mutation landed (blob moved, anchors flipped, line-total held) and nothing reddened, because at this site
!labelandlabel === ''are behaviourally equivalent:opt?.label || String(raw)lets no falsy label through, so the only falsy label that can reach the push is the empty string. The plausible wrong fix therefore is not wrong here — a real measurement, and a correction to the expectation the card set.=== ''is kept anyway as the intent-revealing spelling, and the'0'pin still asserts the user-facing guarantee (it is observed red under leg 4a).Leg 3b is the one that matters: it is exactly the fix that special-cases the empty array, and the fixture is built so that it passes both array pins and reddens both non-array pins. A fix pinned only on the array would have read as complete.
Leg 4 — caricatures, both directions
'0'pinEvery one of the nine pins was observed red under leg 3 or leg 4. No leg was void: every mutation moved the blob, flipped its anchors and held its line-total gate, and every restore was verified by state rather than by an exit code.
Assertions are at the DOM, not at
cardBadgesCounts are taken from rendered nodes, scoped to one card through the accessible-name handle
SortableCardalready gives every card (list-item role plus an aria-label of the card title).queryByTextwith an empty string cannot express "no pill", and it throws on multiple matches as well as none, so the pins count nodes at a selector instead — and the selector is proved, not assumed, to match exactly one node per badge: the first case in each group asserts the populated card's badge text list has exactly the expected entries. The selector uses the badge primitive's own base classes (inline-flex and whitespace-nowrap together with rounded-full), which the two avatar nodes that make a bare rounded-full count double elsewhere in this repo do not carry.Gates run locally
vitest run packages/plugin-kanban/pnpm --filter @object-ui/plugin-kanban type-checkeslint .(whole repo, 4594 files)check-changeset-presence.mjscheck-changeset-no-major.mjscheck-control-bytes.mjscheck-governed-queue-guard.mjs --testThe type-check genuinely covers the new test file: it is absent from the main program and present in
tsconfig.test.json's, verified with--listFiles, andtype-checkruns both.Notes
labelis the empty string cannot on its own produce an empty label, becauseopt?.label || String(raw)falls back to the stringified value; the i18n hop cannot introduce one either, sinceuseObjectLabel'sresolve()rejects a translation equal to the empty string and returns its fallback. The empty label always arrives throughString(raw). The card's central finding — the empty pill, and the second half being independent of arrays — is unaffected and confirmed by rendering.KanbanImplmirroring itscolumnsprop into state) is not folded in and the fixture did not trip over it.@object-ui/fields' renderers are untouched — objectui#8481 owns those.🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code