fix(plugin-kanban): each column header totals the view's summarizeField (objectui#11629) - #11653
Conversation
…ld (objectui#11629) The spec declares `summarizeField` on the view-level KanbanConfig and ListView's kanban branch relays it onto the object-kanban node, but the board never read it. ObjectKanban now hands the field, its label and its cell renderer to the column headers through a package-private context; KanbanImpl sums the field over each lane's cards and paints the total beside the count on the flat header and the swimlane title row. Absent / null / empty count as 0, an empty lane totals 0, a non-numeric value suppresses that lane's total (never NaN), a no-scale sum rounds to the widest input, a windowed fetch marks the total `+` like the count, and an FLS-denied or undeclared field gets no total. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…9 pin without explicit any Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…s apart from the count badge (objectui#11629) The glyph is aria-hidden; the field label stays the total's tooltip and screen-reader name, so no user-facing string is added. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…geset Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…jectui#11629) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…nban-column-sum Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL 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
|
…ld to ListView (objectui#11629) `kanbanViewOptions` is the only kanban config `ListView` receives on the console object page when the stored row carries no `options.kanban` bag. It relayed the lane, the title and the card fields and dropped `summarizeField`, so the key never reached `ListView`'s projection or the board's column headers, and the showcase task board showed counts with no totals. The key is now relayed when the view declares it, and stays absent when it does not. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#11629) The changeset gains `@object-ui/app-shell` and replaces its note that the object page did not forward the key. The plugin page lists column totals among the board's features. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…nban-column-sum Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL 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
|
ACCEPT: PR objectui#11653, head
|
Fixes #11629
Clause-②: no
Session:
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL. This is the dispatched os-dev run for thedomain:uiseat 1 claim on this card. The first round resumed from branch head7c30a25after a container restart. Patch round 2 adds the producer half under the seat's surface extension, comment5991348386on the card.A kanban view's
summarizeFieldnow shows as a per-column total on the board. The key is declared in@objectstack/spec'sKanbanConfigas "Field to sum at top of column". Two halves were missing, and this PR carries both:kanbanViewOptionsis the only kanban configListViewreceives when the stored view row carries nooptions.kanbanbag. It relayedgroupByField,titleFieldandcardFields, and droppedsummarizeField. So on the card's own repro route (/apps/com.example.showcase/showcase_task/view/showcase_task.board) the key never reachedListView's projection or the generated board. It is now relayed when the view declares it.object-kanbanboard never read the key. Each column header now paints the total beside the count.What changed
packages/app-shell/src/views/ObjectView.tsx,kanbanViewOptionsonly:summarizeFieldwhen the view'skanbanblock declares it;packages/plugin-kanban/src:KanbanColumnSummary.ts(new, not re-exported fromindex.tsx):renderTotal, which writes a total through the field's own cell renderer;sumLaneField, the sum of one lane's cards.ObjectKanban.tsx:summarizeFieldoff the node, typed onGateBoundKanbanSchema(the module-local read type that already types the gate-writtensort);KanbanImpl.tsx:LaneTotalpaints aΣglyph and the total beside the count, on the flat header and on the swimlane layout's column-title row;aria-hidden;Docs and changeset:
content/docs/plugins/plugin-kanban.mdx: one Features line for column totals..changeset/11629-kanban-column-sum.md:@object-ui/plugin-kanbanand@object-ui/app-shell, bothpatch. The note that the object page did not forward the key is replaced by one sentence for the relay.Behaviour:
nullor empty value counts as 0, and an empty column totals 0.NaN.getCellRenderer(resolveCellRendererType(def))call the card fields make. A currency field totals as currency, and anumberfield keeps its declaredscale.numberfield with noscaleis rounded to its widest input, so0.1 + 0.2reads0.3. This is ruling A′ of [finding] an undeclarednumberfield prints two different decimal widths on two faces of the same column — and ⛔number ⇒ 0cannot be the fix, because a declared 0 also turns grouping OFF objectstack#19628, the same reading the grid footer applies.summarizeFieldrenders exactly as before.What the total covers
The board has no per-column pager. It makes one query, capped by
$top(the authoredlimit, elseDEFAULT_KANBAN_FETCH_BATCH_SIZE), and groups the rows that come back.+the count carries (6+), so it never reads as the group's full total.hasMore: false. So the totals there are complete.Evidence (final head
7e2cb00)Gates
Run from the repo root. Each exit code was captured before any pipe.
pnpm exec vitest run packages/app-shell/: exit 0,Test Files 1005 passed | 1 skipped (1006),Tests 9979 passed | 9 skipped (9988).pnpm exec vitest run packages/plugin-kanban/: exit 0,Test Files 66 passed (66),Tests 454 passed (454).pnpm exec turbo run type-check --filter=@object-ui/app-shell --concurrency=2: exit 0, 30/30 tasks (closure via^build). The new pin is in thetsconfig.test.jsonprogram. This and the app-shell lint ran at91d32af; the merge that made7e2cb00changed no app-shell file.pnpm exec turbo run build --filter='@object-ui/plugin-kanban...' --concurrency=2andpnpm --filter @object-ui/plugin-kanban type-check: exit 0 (13/13 tasks), then exit 0.pnpm --filter @object-ui/app-shell lintandpnpm --filter @object-ui/plugin-kanban lint: exit 0 with 0 errors, and no warning on any line this branch adds.check:new-line-citations(0 new),check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check:phantom-deps,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:handler-key-reads,check:metadata-write-doors,check:icon-record-names,check:i18n-keys,check:unreferenced-sources,check:doc-types,check:doc-fences,check:doc-example-ids,check:doc-example-readers,check-changeset-presence(2 released packages, 1 changeset) andcheck-changeset-no-major.check:doc-snippetsandcheck:doc-examplesneed a scoped build first; without it they report PRECONDITION NOT MET (exit 2), which means they did not run. After the build their own message names (turbo run buildovercheck-doc-snippet-types.mjs --build-filter, exit 0, 35/35 tasks),check:doc-snippetsexits 0 (777 of 777 blocks judged, 0 failed) andcheck:doc-examplesexits 0 (every covered@examplecompiles or fails as its ledger row declares).Pins
ObjectView.kanbanSummarizeField-11629.test.ts(app-shell, new):kanbanViewOptionscarriessummarizeFieldwhen the view declares it, also on the detector path where the view names no lane. It omits the key when the view's kanban block lacks it, and when the view has no kanban block; the omission arms carry a control that the bag is not empty.kanbanViewOptionsreaders stay green (6 files, 75 tests together with the new pin):ObjectView.kanbanLane-8193,ObjectView.kanbanGroupByRetired-8213(its refused-key ratchet is unchanged, becausesummarizeFieldis a declared spec key),ObjectView.relayRungCensus-7559,ObjectView.storedOptionsBag-10380andplugin-list'sListView.strayGroupByRefused-8365.columnSummarizeField-11629.test.tsx(plugin-kanban) has 12 rows: mixed values and nulls with an empty column; the label as tooltip and screen-reader name; the NO SUMMARIZE FIELD control; a currency total that reads like the card's cell;0.1 + 0.2reads0.3; a non-numeric lane; WINDOWED6+with an UNSATURATED control; the swimlane title row; DENIED with a READABLE control; an undeclared field.Ablations
Each ablation went through
ablation-replace: the anchor hit once, the blob moved, and the restore was proven by blob == HEAD and an emptygit diff HEAD. The expected direction was written down before each run, and every run went red as expected.kanbanViewOptionsLaneTotalrender removed+removedsumLaneFieldremovedThe two relay ablations ran in round 2 at
91d32af. The five plugin-kanban ablations ran in round 1 at7c30a25; plugin-kanban has not changed since.Live, on the branch head with no mutation
Setup:
ba575886showcase, on a--freshdatabase.main27991556.GET /api/v1/data/showcase_task: backlog 30, todo 36, in_progress 100, in_review 54, done 24. Fromshowcase_project: planned 200000, active 750000, on_hold 90000, completed 50000.selectshowcase_task.board(the card's repro)Estimate (h); noNaNestimate_hoursshowcase_project.by_status(currencybudget)cancelledcolumn, matching the cards'budgetcellsbudgetsummarizeField)budget, yet no total is drawnRound 1 also measured the card's route before the relay: counts only, and no
estimate_hoursin the board'sselect. The showcase "Task Board" interface page resolves the task object's default view, which also declaressummarizeField, and it shows the same totals. So the interface-page reach is now measured live, not only read from the code.Clause-② evidence
Measured on the built
dist/of plugin-kanban, over the declaration files reachable from theexportsentry:summarizeField,KanbanColumnSummary,sumLaneField,useKanbanColumnSummaryandLaneTotaleach occur 0 times. Controls:ObjectKanbanComponentPropsoccurs 6 times andKanbanRenderer11 times.dist/index.jsexports the same ten names, andindex.tsxis untouched.In app-shell, the relay adds a key to the object
kanbanViewOptionsreturns. It adds no export, no prop, no type member and no i18n key.kanbanViewOptionswas already exported, for its pin test. Across the PR, the only type-level addition is one optional member on plugin-kanban's non-exported read typeGateBoundKanbanSchema.Acceptance notes
+on a sum. It is a lower bound only when the values are non-negative. If a field holds negative values and the window is saturated,N+is not a bound; it says only that more rows exist than the window holds. The count uses the same marker, by design.widestFractionDigits, the metric tile hasownFractionDigits, and this PR addsfractionDigitsOf. Sharing one function needs a new export, which this claim's Clause-② fence rules out. Carrier: none.packages/plugin-kanban/README.mddoes not mentionsummarizeField. The surface extension covered one line in the docs page only.check:unreferenced-sourcescovers onlypackages/components, so it did not judge the new module. The module is imported byObjectKanban.tsxandKanbanImpl.tsx, and it is bundled intodist/index.js.ObjectView.tsxin the fallback-tab andpersistViewPatchregion. This PR touches onlykanbanViewOptionsand its doc comment.check:eager-closureandcheck:sdui-registration-pins(both need a console build), and the repo-widepnpm lint.Generated by Claude Code