feat(grid): show average for numeric selections

Closes #4929
This commit is contained in:
t8y2 committed 2026-08-24 22:53:11 +00:00
1 parent e2fa810f2e
commit b3e8709272
13 files changed
+133 -10

No files matched your search

@@ -253,7 +253,7 @@ import { allNullColumnIndexes } from "@/lib/dataGrid/dataGridColumnVisibility";
import { buildDataGridColumnLookupItems, dataGridColumnCommentFor, filterDataGridColumnLookupItems } from "@/lib/dataGrid/dataGridColumnLookup";
import { uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder";
import { dataGridColumnLayoutScopeKey, TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
import { summarizeSelection } from "@/lib/dataGrid/gridSelection";
import { createPendingSelectionSummary, formatSelectionAggregate, formatSelectionAverage, summarizeSelection } from "@/lib/dataGrid/gridSelection";
import { captureDataGridSelection, restoreDataGridSelection, type CaptureDataGridSelectionOptions, type PersistedDataGridSelection } from "@/lib/dataGrid/dataGridSelectionPersistence";
import { dataGridFrameCoversRow, dataGridSelectionEdgeMask, dataGridSelectionFrameKindAtCell, dataGridSelectionUsesOuterFrame, resolveDataGridSelectionFrames } from "@/lib/dataGrid/dataGridSelectionFrames";
import {
@@ -5554,12 +5554,7 @@ const multiRowCount = computed(() => {
const selectionSummary = computed(() => {
if (!hasCellSelection.value) return null;
if (isSelectingCells.value) {
return {
cellCount: selectedCellCount.value,
rowCount: multiRowCount.value,
numericCount: 0,
sum: 0,
};
return createPendingSelectionSummary(selectedCellCount.value, multiRowCount.value);
}
return summarizeSelection(selectedCells.value);
});
@@ -5567,8 +5562,11 @@ const selectionSummarySumText = computed(() => {
if (isSelectingCells.value) return "…";
const summary = selectionSummary.value;
if (!summary) return "0";
const sum = Object.is(summary.sum, -0) ? 0 : summary.sum;
return Number.isInteger(sum) ? String(sum) : sum.toLocaleString(undefined, { maximumFractionDigits: 12 });
return formatSelectionAggregate(summary.sum);
});
const selectionSummaryAverageText = computed(() => {
if (isSelectingCells.value) return "…";
return formatSelectionAverage(selectionSummary.value?.average);
});
const isMultiRow = computed(() => multiRowCount.value > 1);
@@ -13587,6 +13585,7 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
:pagination-enabled="paginationEnabled"
:selection-summary="selectionSummary"
:selection-summary-sum-text="selectionSummarySumText"
:selection-summary-average-text="selectionSummaryAverageText"
:loading="gridPaginationBusy"
:infinite-scroll-enabled="infiniteScrollEnabled"
:infinite-scroll-all-loaded="infiniteScrollAllLoaded"
@@ -16,6 +16,7 @@ const props = withDefaults(
paginationEnabled?: boolean;
selectionSummary: { cellCount: number; rowCount: number } | null;
selectionSummarySumText: string;
selectionSummaryAverageText: string;
loading: boolean;
infiniteScrollEnabled: boolean;
infiniteScrollAllLoaded: boolean;
@@ -86,6 +87,7 @@ function handlePageInputKeydown(event: KeyboardEvent) {
<div class="flex min-w-max items-center justify-end gap-1">
<div v-if="selectionSummary" class="flex shrink-0 items-center gap-3 tabular-nums">
<span class="shrink-0">{{ t("grid.selectionSum", { value: selectionSummarySumText }) }}</span>
<span class="shrink-0">{{ t("grid.selectionAverage", { value: selectionSummaryAverageText }) }}</span>
<div class="flex shrink-0 items-center gap-1">
<span class="shrink-0">{{ t("grid.selectionCells", { count: selectionSummary.cellCount }) }}</span>
<span class="shrink-0">{{ t("grid.rows", { count: selectionSummary.rowCount }) }}</span>
@@ -1,5 +1,6 @@
// @vitest-environment happy-dom
import { readFileSync } from "node:fs";
import { nextTick } from "vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { dispatch, findAll, findOne, hostText, mountComponent } from "./vueHostHarness";
@@ -98,6 +99,8 @@ import DataGridPagination from "@/components/grid/DataGridPagination.vue";
import DataGridQueryControls from "@/components/grid/DataGridQueryControls.vue";
import DataGridSearchBar from "@/components/grid/DataGridSearchBar.vue";
const dataGridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
function detail(patch: Partial<DataGridCellDetail> = {}): DataGridCellDetail {
return {
rowNumber: 1,
@@ -184,6 +187,42 @@ describe("DataGridSearchBar", () => {
});
describe("DataGridPagination", () => {
it("keeps drag summaries count-only and wires the derived average without materializing selected cells", () => {
const summaryStart = dataGridSource.indexOf("const selectionSummary = computed");
const pendingSummary = dataGridSource.indexOf("createPendingSelectionSummary(selectedCellCount.value, multiRowCount.value)", summaryStart);
const materializedSummary = dataGridSource.indexOf("summarizeSelection(selectedCells.value)", summaryStart);
expect(summaryStart).toBeGreaterThan(-1);
expect(pendingSummary).toBeGreaterThan(summaryStart);
expect(materializedSummary).toBeGreaterThan(pendingSummary);
expect(dataGridSource).toContain('const selectionSummaryAverageText = computed(() => {\n if (isSelectingCells.value) return "…";');
expect(dataGridSource).toContain(':selection-summary-average-text="selectionSummaryAverageText"');
});
it("shows average beside the existing selection summary values", () => {
const mounted = mountComponent(DataGridPagination, {
selectionSummary: { cellCount: 4, rowCount: 2 },
selectionSummarySumText: "10",
selectionSummaryAverageText: "2.5",
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
pageSize: 100,
customPageSizeInput: "",
pageSizeMenuItems: [],
exportMenuItems: [],
currentPage: 1,
canGoNextPage: false,
canJumpLastPage: false,
});
const summaryText = hostText(mounted.root);
expect(summaryText).toContain("grid.selectionSum");
expect(summaryText).toContain("grid.selectionAverage");
expect(summaryText).toContain("grid.selectionCells");
expect(summaryText).toContain("grid.rows");
});
it("enforces first/previous/next/last disabled boundaries", async () => {
const firstPage = vi.fn();
const previousPage = vi.fn();
@@ -192,6 +231,7 @@ describe("DataGridPagination", () => {
const mounted = mountComponent(DataGridPagination, {
selectionSummary: null,
selectionSummarySumText: "",
selectionSummaryAverageText: "",
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
@@ -236,6 +276,7 @@ describe("DataGridPagination", () => {
const mounted = mountComponent(DataGridPagination, {
selectionSummary: null,
selectionSummarySumText: "",
selectionSummaryAverageText: "",
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,
@@ -278,6 +319,7 @@ describe("DataGridPagination", () => {
paginationEnabled: false,
selectionSummary: null,
selectionSummarySumText: "",
selectionSummaryAverageText: "",
loading: false,
infiniteScrollEnabled: false,
infiniteScrollAllLoaded: false,