You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Dashboard table widget: right-align numeric columns, sortable headers, null bucket last, row hover — it currently reads as a raw dump #5827
The dashboard table widget reads as a raw dump: numeric columns left-aligned, unsortable, no row affordances, null dimension bucket floats to the top
Measured on objectui origin/main (313cc3c, includes #5814), rendering HotCRM's executive_dashboard → "按行业统计客户" (type: 'table', dataset-backed, dimensions: ['industry'], values: ['annual_revenue_sum', 'account_count']). All numbers below are computed-style/DOM readings, not impressions.
What renders today
defect
reading
Numeric columns left-aligned
th and td for both measures: text-align: left/start. font-variant-numeric: tabular-nums is already applied — and wasted, since digits only line up when right-aligned. With column widths 321/310/201px and 4px/8px cell padding, the value sits ~250px of dead space away from the next column's label.
Not sortable
No sort affordance on any header (sortable: false — no button/icon in any th). Rows arrive alphabetically by dimension label, so for a "who are our biggest industries" table, Technology (30,000,000, the largest) renders last.
Null bucket floats to the top
The account with no industry renders as row 1: — / 0 / 1. An empty-dimension bucket sorting first reads as a data bug to the viewer; it belongs at the end (keep the — label, sort nulls last).
No row affordances
Both sampled tbody tr backgrounds are rgba(0,0,0,0) — no zebra, no hover; 9 thin rows spanning 832px with hairline separators only.
No totals row
The app's own list views render aggregate footers (合计, 平均值); the dashboard table stops at the rows.
Suggested contract (renderer-level, zero metadata changes required)
Right-align numeric columns — header and cells — detected from the resolved value type/format the renderer already has (it applies tabular-nums today, so it knows which columns are numeric).
Sortable headers with the usual affordance; default remains the authored/dataset order, click cycles per column. Nulls sort last in either direction.
Null dimension bucket: keep — as the label, pin it after non-null rows in the default order.
Row hover (and/or subtle zebra) consistent with the console's table idiom elsewhere.
(Optional, flag if taken) totals row when every non-dimension column is numeric — matches the list-view footer idiom; skip if it fights the widget's height budget.
Known-adjacent, NOT this card: measure header labels render untranslated ("Annual Revenue" beside 行业) — that is the dataset-label i18n gap already filed as objectstack#10292; do not solve it here.
Filed from the HotCRM exemplar walkthrough with the maintainer; maintainer instruction verbatim (未译): 「按行业统计客户 二维表的显示效果不好看,需要优化objectui。」
Claim: direct-dispatch (hotcrm seat acting cross-repo under maintainer instruction)
Session: session_61f28fb9-8e31-4a6f-b4d8-10f749a092d2
Branch: claude/issue-5827-dashboard-table-polish
Worktree: objectui-issue-5827
File surface: the dashboard table widget renderer under packages/plugin-dashboard/src/ + its tests (exact file re-derived by the dev; stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (UI rendering behaviour; no contract accept/reject change — Clause-②: no)
Serial constraints cleared: #5814 (DashboardRenderer.tsx) merged at cc5de5c — base on a ref including it. No open PR touches plugin-dashboard (REST pulls listing checked at claim time).
{"issue":5827,"status":"done","branch":"claude/issue-5827-dashboard-table-polish","pr":"https://github.com/objectstack-ai/objectui/pull/5834","premise_still_valid":true,"summary":"Items 1-4 landed in the flat branch of `if (isTable)` in packages/plugin-dashboard/src/DatasetWidget.tsx (the card did not name the file). Blast radius is nil outside the dashboard: this markup is hand-written there and shared with no other surface - not data-table, ObjectDataTable, PivotTable, list views or the report renderer; DatasetWidget's only importers are DashboardRenderer and DashboardGridLayout in the same package. One PM mechanism assumption is FALSE: the renderer does NOT already know which columns are numeric. It applied `tabular-nums` to every cell including dimension cells, and the column descriptor is no help either - service-analytics' dataset-executor stamps `type:'number'` on every measure column it appends without consulting the aggregate, so the declared type is a constant across measures. Numeric-ness is therefore inferred defensively: a MEASURE (or a measure's comparison column) whose every non-null value is a number; a min()/max() over a text field stays left-aligned, and a digit-keyed DIMENSION stays left-aligned because a grouping axis is not a quantity. The client-side-sort assumption HOLDS - one queryDataset call, no pagination, so the rows on screen are the whole result set (options.limit truncates server-side, and a sort then orders exactly the set the unsorted table showed). Sorting reuses @object-ui/core's getSortValue/compareSortValues (the console's one client-side sort primitive) and data-table.tsx's own indicator, with aria-sort on the th and a real keyboard-reachable button as the control; cycle is asc->desc->the dataset's own order, blanks last in BOTH directions (compareSortValues puts them last ascending and expects callers to negate, which would float them to the top, so the blank arm is settled before the direction is applied). The sharp edge: rows are reordered as (row, incoming index) PAIRS, because openDrill(i) indexes the server's parallel drillRawRows - a bare-row sort would have kept every length and every click working while filtering by another row's bucket. Item 5 (totals row) NOT taken and flagged: beyond the height budget, a correct totals row cannot be computed client-side (this file carries the no-re-aggregation line - avg/min/max cannot be recombined) and the server's marginal totals are requested only for the cross-tab, so it would mean changing the query every dashboard table issues. objectstack#10292 was left alone as instructed.","tests":"All from the repo root in a dedicated worktree; union re-run AFTER the final commit, at db437ef57. `pnpm --filter '@object-ui/plugin-dashboard^...' build` exit 0 (dependency closure, 'built in 6.19s'). `pnpm --filter @object-ui/plugin-dashboard type-check` exit 0, script name echoed ('tsc --noEmit && tsc -p tsconfig.test.json'), no diagnostics. `pnpm --filter @object-ui/plugin-dashboard lint` exit 0, verdict line '369 problems (0 errors, 369 warnings)' - all pre-existing no-explicit-any/react-hooks warnings, none in the new code. `pnpm exec vitest run packages/plugin-dashboard/` exit 0, 'Test Files 76 passed (76)' / 'Tests 693 passed (693)'; baseline before the change was 75/682, so the delta is exactly the new file (+1 file, +11 tests) and nothing pre-existing moved. check-changeset-presence.mjs exit 0 ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major.mjs exit 0 ('No changeset declares a major bump.'), check-control-bytes.mjs exit 0 ('OK (scanned 4852 tracked text file(s); skipped 85 binary)'). Every exit code captured by redirecting to a file first, never after a pipe. REVERSE VERIFICATION - direction stated before the run: reverting DatasetWidget.tsx to origin/main turns the new file red. Observed 10 of 11 red, 1 green: 'keeps the drillable row on its stronger accent fill' is the acceptance-boundary case (that row already had hover:bg-accent/40 + cursor-pointer on main) and is supposed to stay green. Mutation confirmed ON DISK by anchored greps, not by an editor exit code: injected markers dataset-table-sort / numericColumns / compareSortValues each counted 0, and the DELETED origin/main cell spelling 'className=\"px-2 py-1 whitespace-nowrap tabular-nums\"' counted 1. Restore leg confirmed the same way (1 / 3 / 0) plus an empty `git status --porcelain` for the file, and re-ran green at 11/11. No rebuild is involved: the suite imports the component by relative source path (../DatasetWidget), not through a package exports->dist hop, so there is no stale-dist failure mode here. The ablation script carried `trap ... EXIT INT TERM`, so a kill mid-run could not leave the tree mutated. NOT DONE: no browser verification. The evidence is component tests over the DOM the widget produces, not a locally built console rendering HotCRM's executive_dashboard - stating that plainly as the dispatch asks. DECLARED NARROWING: the shared verification lock is inoperable on this host - `scripts/pm/os-verify-lock.sh --status` prints 'CANNOT OPERATE THIS LOCK ... a stock macOS does not ship flock' - so every command above ran directly rather than through the entry point; and repo-wide check:* scanning is left to CI, which runs the farm regardless.","open_questions":[{"question":"First click on a NUMERIC header sorts ascending, following the console's DataTable idiom. The card's motivating complaint is that the largest industry renders last, which one descending-first click would answer directly. Should numeric columns start descending?","options":["A - keep asc-first uniformly (as shipped): matches data-table.tsx, one rule for every column, two clicks to reach biggest-first","B - numeric columns start descending: one click to 'who are our biggest industries', but the first-click direction now depends on column type"],"recommendation":"A as shipped. Deviating from the repo's own sortable-table idiom is a product decision, not an implementation one, and the card did not pin a first direction. If the maintainer wants B it is a two-line change to cycleSort."},{"question":"Item 5 (totals row) was declined. A correct one needs the SERVER's marginal totals - the file's own no-client-re-aggregation line forbids deriving it here, and totalsGroupings is currently requested only for the cross-tab. Worth a follow-up?","options":["A - leave it: the height budget is already tight (9 rows over 832px) and every dashboard table would pay for an extra grouping in its query","B - file a follow-up to request the [] grouping for flat tables and render a tfoot"],"recommendation":"A for now. It is a query-shape change affecting every dataset table in every dashboard, which is more than a rendering-polish card should decide; happy to file B if the PM wants it queued."}],"out_of_scope_findings":["filed as #5836: the dashboard cross-tab branch (pivot with >=2 dimensions) did not get #5827's row hover, and whether its row axis should be sortable by a cell column is an unsettled design question - labelled `finding`, unassigned, no pm:queue"]}
PM review — ACCEPT. PR #5834. Both shipped judgement calls accepted as recommended; merge on CI green.
My mechanism assumption was false, and the falsification improved the fix
I dispatched "the renderer already knows which columns are numeric (it applies tabular-nums)". Measured: it applied tabular-nums to every cell, dimensions included, and the column descriptor is no help — the dataset executor stamps type:'number' on every measure column regardless of aggregate. So numeric-ness is inferred defensively (measure column + all non-null values numeric), a text-min() stays left-aligned, and a digit-keyed dimension stays left-aligned because a grouping axis is not a quantity. That last distinction is exactly what a naive "looks like numbers ⇒ right-align" would have gotten wrong.
The sharp edge that justified the review
Rows are reordered as (row, incoming-index) pairs, never bare rows — because openDrill(i) indexes the server's parallel drillRawRows array. A bare-row sort would have kept every test green and every click working while drill-filtering by another row's bucket, silently. The diff carries the reasoning in place, and the blanks-last arm is settled before direction is applied (naive negation of compareSortValues would float blanks to the top on descending). Verified both in the patch text.
Sorting reuses @object-ui/core's getSortValue/compareSortValues and data-table's indicator — the console's one client-sort idiom, not a re-implementation — with aria-sort and a keyboard-reachable control.
Blast radius measured, not asserted
The table markup is hand-written in DatasetWidget.tsx's flat branch and shared with no other surface (not data-table, ObjectDataTable, PivotTable, list views, or the report renderer); DatasetWidget's only importers are the two dashboard renderers in the same package. Baseline suite 75/682 → 76/693: the delta is exactly the new test file, nothing pre-existing moved. Reverse verification: revert to origin/main → 10/11 red with the single green being the acceptance-boundary case that should stay green (the drillable row already had hover on main) — a full-red result would actually have been suspicious; this is the honest shape.
The two open questions — decided as recommended, on the record
Asc-first on numeric headers (A). Matching the console's own DataTable idiom beats optimizing one click for one widget; biggest-first is two clicks. If the maintainer prefers descending-first for numeric columns it is a two-line cycleSort change — flagged to them directly.
No totals row (A). The file's own no-client-re-aggregation rule forbids deriving one (avg/min/max cannot be recombined), and the server's marginal totals are only requested for the cross-tab — adding them is a query-shape change for every dashboard table, beyond a rendering-polish card. Left unfiled per recommendation; the maintainer can ask for it as a follow-up.
#5836 verified: open, finding, unassigned — the cross-tab sibling gap correctly parked for triage rather than smuggled into this diff.
One check consciously deferred to the PM
No browser verification (declared plainly — component tests over the produced DOM only). The PM is doing the visual half: a local console build of this branch overlaid onto the HotCRM exemplar, with the maintainer watching. Results land on this thread if they diverge from the tests' claims.
Activity
os-zhuang commentedon Aug 23, 2026
Claim: direct-dispatch (hotcrm seat acting cross-repo under maintainer instruction)
Session:
session_61f28fb9-8e31-4a6f-b4d8-10f749a092d2Branch:
claude/issue-5827-dashboard-table-polishWorktree:
objectui-issue-5827File surface: the dashboard
tablewidget renderer underpackages/plugin-dashboard/src/+ its tests (exact file re-derived by the dev; stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(UI rendering behaviour; no contract accept/reject change — Clause-②: no)Serial constraints cleared: #5814 (DashboardRenderer.tsx) merged at cc5de5c — base on a ref including it. No open PR touches plugin-dashboard (REST pulls listing checked at claim time).
Authorization — maintainer direct-dispatch, verbatim (未译): 「按行业统计客户 二维表的显示效果不好看,需要优化objectui。」 (in-session, 2026-08-23)
fix(dashboard): make the dataset `table` widget read as a table, not …
os-zhuang commentedon Aug 23, 2026
{"issue":5827,"status":"done","branch":"claude/issue-5827-dashboard-table-polish","pr":"https://github.com/objectstack-ai/objectui/pull/5834","premise_still_valid":true,"summary":"Items 1-4 landed in the flat branch of `if (isTable)` in packages/plugin-dashboard/src/DatasetWidget.tsx (the card did not name the file). Blast radius is nil outside the dashboard: this markup is hand-written there and shared with no other surface - not data-table, ObjectDataTable, PivotTable, list views or the report renderer; DatasetWidget's only importers are DashboardRenderer and DashboardGridLayout in the same package. One PM mechanism assumption is FALSE: the renderer does NOT already know which columns are numeric. It applied `tabular-nums` to every cell including dimension cells, and the column descriptor is no help either - service-analytics' dataset-executor stamps `type:'number'` on every measure column it appends without consulting the aggregate, so the declared type is a constant across measures. Numeric-ness is therefore inferred defensively: a MEASURE (or a measure's comparison column) whose every non-null value is a number; a min()/max() over a text field stays left-aligned, and a digit-keyed DIMENSION stays left-aligned because a grouping axis is not a quantity. The client-side-sort assumption HOLDS - one queryDataset call, no pagination, so the rows on screen are the whole result set (options.limit truncates server-side, and a sort then orders exactly the set the unsorted table showed). Sorting reuses @object-ui/core's getSortValue/compareSortValues (the console's one client-side sort primitive) and data-table.tsx's own indicator, with aria-sort on the th and a real keyboard-reachable button as the control; cycle is asc->desc->the dataset's own order, blanks last in BOTH directions (compareSortValues puts them last ascending and expects callers to negate, which would float them to the top, so the blank arm is settled before the direction is applied). The sharp edge: rows are reordered as (row, incoming index) PAIRS, because openDrill(i) indexes the server's parallel drillRawRows - a bare-row sort would have kept every length and every click working while filtering by another row's bucket. Item 5 (totals row) NOT taken and flagged: beyond the height budget, a correct totals row cannot be computed client-side (this file carries the no-re-aggregation line - avg/min/max cannot be recombined) and the server's marginal totals are requested only for the cross-tab, so it would mean changing the query every dashboard table issues. objectstack#10292 was left alone as instructed.","tests":"All from the repo root in a dedicated worktree; union re-run AFTER the final commit, at db437ef57. `pnpm --filter '@object-ui/plugin-dashboard^...' build` exit 0 (dependency closure, 'built in 6.19s'). `pnpm --filter @object-ui/plugin-dashboard type-check` exit 0, script name echoed ('tsc --noEmit && tsc -p tsconfig.test.json'), no diagnostics. `pnpm --filter @object-ui/plugin-dashboard lint` exit 0, verdict line '369 problems (0 errors, 369 warnings)' - all pre-existing no-explicit-any/react-hooks warnings, none in the new code. `pnpm exec vitest run packages/plugin-dashboard/` exit 0, 'Test Files 76 passed (76)' / 'Tests 693 passed (693)'; baseline before the change was 75/682, so the delta is exactly the new file (+1 file, +11 tests) and nothing pre-existing moved. check-changeset-presence.mjs exit 0 ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major.mjs exit 0 ('No changeset declares a major bump.'), check-control-bytes.mjs exit 0 ('OK (scanned 4852 tracked text file(s); skipped 85 binary)'). Every exit code captured by redirecting to a file first, never after a pipe. REVERSE VERIFICATION - direction stated before the run: reverting DatasetWidget.tsx to origin/main turns the new file red. Observed 10 of 11 red, 1 green: 'keeps the drillable row on its stronger accent fill' is the acceptance-boundary case (that row already had hover:bg-accent/40 + cursor-pointer on main) and is supposed to stay green. Mutation confirmed ON DISK by anchored greps, not by an editor exit code: injected markers dataset-table-sort / numericColumns / compareSortValues each counted 0, and the DELETED origin/main cell spelling 'className=\"px-2 py-1 whitespace-nowrap tabular-nums\"' counted 1. Restore leg confirmed the same way (1 / 3 / 0) plus an empty `git status --porcelain` for the file, and re-ran green at 11/11. No rebuild is involved: the suite imports the component by relative source path (../DatasetWidget), not through a package exports->dist hop, so there is no stale-dist failure mode here. The ablation script carried `trap ... EXIT INT TERM`, so a kill mid-run could not leave the tree mutated. NOT DONE: no browser verification. The evidence is component tests over the DOM the widget produces, not a locally built console rendering HotCRM's executive_dashboard - stating that plainly as the dispatch asks. DECLARED NARROWING: the shared verification lock is inoperable on this host - `scripts/pm/os-verify-lock.sh --status` prints 'CANNOT OPERATE THIS LOCK ... a stock macOS does not ship flock' - so every command above ran directly rather than through the entry point; and repo-wide check:* scanning is left to CI, which runs the farm regardless.","open_questions":[{"question":"First click on a NUMERIC header sorts ascending, following the console's DataTable idiom. The card's motivating complaint is that the largest industry renders last, which one descending-first click would answer directly. Should numeric columns start descending?","options":["A - keep asc-first uniformly (as shipped): matches data-table.tsx, one rule for every column, two clicks to reach biggest-first","B - numeric columns start descending: one click to 'who are our biggest industries', but the first-click direction now depends on column type"],"recommendation":"A as shipped. Deviating from the repo's own sortable-table idiom is a product decision, not an implementation one, and the card did not pin a first direction. If the maintainer wants B it is a two-line change to cycleSort."},{"question":"Item 5 (totals row) was declined. A correct one needs the SERVER's marginal totals - the file's own no-client-re-aggregation line forbids deriving it here, and totalsGroupings is currently requested only for the cross-tab. Worth a follow-up?","options":["A - leave it: the height budget is already tight (9 rows over 832px) and every dashboard table would pay for an extra grouping in its query","B - file a follow-up to request the [] grouping for flat tables and render a tfoot"],"recommendation":"A for now. It is a query-shape change affecting every dataset table in every dashboard, which is more than a rendering-polish card should decide; happy to file B if the PM wants it queued."}],"out_of_scope_findings":["filed as #5836: the dashboard cross-tab branch (pivot with >=2 dimensions) did not get #5827's row hover, and whether its row axis should be sortable by a cell column is an unsettled design question - labelled `finding`, unassigned, no pm:queue"]}os-zhuang commentedon Aug 23, 2026
PM review — ACCEPT. PR #5834. Both shipped judgement calls accepted as recommended; merge on CI green.
My mechanism assumption was false, and the falsification improved the fix
I dispatched "the renderer already knows which columns are numeric (it applies
tabular-nums)". Measured: it appliedtabular-numsto every cell, dimensions included, and the column descriptor is no help — the dataset executor stampstype:'number'on every measure column regardless of aggregate. So numeric-ness is inferred defensively (measure column + all non-null values numeric), a text-min()stays left-aligned, and a digit-keyed dimension stays left-aligned because a grouping axis is not a quantity. That last distinction is exactly what a naive "looks like numbers ⇒ right-align" would have gotten wrong.The sharp edge that justified the review
Rows are reordered as (row, incoming-index) pairs, never bare rows — because
openDrill(i)indexes the server's paralleldrillRawRowsarray. A bare-row sort would have kept every test green and every click working while drill-filtering by another row's bucket, silently. The diff carries the reasoning in place, and the blanks-last arm is settled before direction is applied (naive negation ofcompareSortValueswould float blanks to the top on descending). Verified both in the patch text.Sorting reuses
@object-ui/core'sgetSortValue/compareSortValuesand data-table's indicator — the console's one client-sort idiom, not a re-implementation — witharia-sortand a keyboard-reachable control.Blast radius measured, not asserted
The table markup is hand-written in
DatasetWidget.tsx's flat branch and shared with no other surface (not data-table, ObjectDataTable, PivotTable, list views, or the report renderer); DatasetWidget's only importers are the two dashboard renderers in the same package. Baseline suite 75/682 → 76/693: the delta is exactly the new test file, nothing pre-existing moved. Reverse verification: revert to origin/main → 10/11 red with the single green being the acceptance-boundary case that should stay green (the drillable row already had hover on main) — a full-red result would actually have been suspicious; this is the honest shape.The two open questions — decided as recommended, on the record
cycleSortchange — flagged to them directly.#5836 verified: open,
finding, unassigned — the cross-tab sibling gap correctly parked for triage rather than smuggled into this diff.One check consciously deferred to the PM
No browser verification (declared plainly — component tests over the produced DOM only). The PM is doing the visual half: a local console build of this branch overlaid onto the HotCRM exemplar, with the maintainer watching. Results land on this thread if they diverge from the tests' claims.
fix(dashboard): make the dataset `table` widget read as a table, not …