diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 35f20db265..1e1da18753 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -288,7 +288,7 @@ import { import { normalizeResultPageSize, resultPageSizeMenuOptions } from "@/lib/dataGrid/paginationPageSize"; import { dataGridPageSizeSettingsPatch, preferredDataGridPageSize, resolveDataGridPageSizePreference, type DataGridPageSizePreference } from "@/lib/dataGrid/dataGridPageSizePreference"; import { continuousQueryResultMaxRows, effectiveQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; -import { allNullColumnIndexes } from "@/lib/dataGrid/dataGridColumnVisibility"; +import { allNullColumnIndexes, identicalValueColumnIndexes } 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"; @@ -2744,6 +2744,24 @@ function showAllColumns() { void nextTick(scheduleColumnLayoutRefresh); } +function getComparisonRows(): ReadonlyArray> { + // 比较行必须取全宽 data:identicalValueColumnIndexes 的候选索引是原始列索引, + // 不能用经过可见列投影(visibleRowData 重排)后的行。 + const affected = affectedRowIds(); + if (affected.length > 1) { + const affectedSet = new Set(affected); + return displayItems.value.filter((item) => affectedSet.has(item.id) && !item.isDraft).map((item) => item.data); + } + const displayRows = displayItems.value.filter((item) => !item.isDraft).map((item) => item.data); + return displayRows.length > 0 ? displayRows : props.result.rows; +} + +function hideIdenticalColumns() { + const identical = identicalValueColumnIndexes(getComparisonRows(), visibleColumnIndexes.value); + if (identical.length === 0) return; + hideColumns(identical); +} + // --- 表头拖拽进 SQL 编辑器:目标导向模式切换的控制器 --- // 按选择顺序(Set 插入序)取列名;被拖列未选中时仅拖该列。 function columnReferenceDragNames(draggedVisibleColIdx: number): string[] | null { @@ -12597,6 +12615,11 @@ const gridContextMenuItems = computed(() => { } } + const comparisonRows = getComparisonRows(); + const identicalColumnIndexes = identicalValueColumnIndexes(comparisonRows, visibleColumnIndexes.value); + const canHideIdenticalColumns = identicalColumnIndexes.length > 0; + const identicalColumnCount = identicalColumnIndexes.length; + return createDataGridContextMenuItems( createDataGridColumnContextMenuItems({ headerColumn: !!contextHeaderColumn.value, @@ -12612,6 +12635,7 @@ const gridContextMenuItems = computed(() => { selectedColumnCount, visibleColumnCount: visibleColumnCount.value, hiddenColumnCount: hiddenColumnCount.value, + canHideIdenticalColumns, labels: { copyName: selectedColumnNamesForCopy.value.length > 1 @@ -12634,6 +12658,7 @@ const gridContextMenuItems = computed(() => { unfreezeColumns: t("grid.unfreezeColumns", { count: frozenColumnCount.value }), hideColumn: t("grid.hideColumn"), hideSelectedColumns: t("grid.hideSelectedColumns", { count: selectedColumnCount }), + hideIdenticalColumns: identicalColumnCount > 0 ? t("grid.hideIdenticalColumnsCount", { count: identicalColumnCount }) : t("grid.hideIdenticalColumns"), showAllColumnsMenu: t("grid.showAllColumnsMenu"), }, icons: { @@ -12679,6 +12704,7 @@ const gridContextMenuItems = computed(() => { }, hideColumn: hideContextColumn, hideSelectedColumns, + hideIdenticalColumns, showAllColumnsMenu: showAllColumns, }, filterSubmenu: filterSubmenu(), diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 8fc866673e..e03bd0edda 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -2135,6 +2135,8 @@ export default withEnglishFallback({ unfreezeColumns: "Sütunların sabitlənməsini ləğv et ({count})", hideColumn: "Bu sütunu gizlət", hideSelectedColumns: "Seçilmiş sütunları gizlət ({count})", + hideIdenticalColumns: "Eyni dəyərlərə malik sütunları gizlət", + hideIdenticalColumnsCount: "Eyni dəyərlərə malik sütunları gizlət ({count})", showAllColumnsMenu: "Bütün sütunları göstər", pasted: "Yerləşdirildi!", batchAppendPasteNotEditable: "Bu nəticəni redaktə etmək mümkün deyil.", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index fd7bcefe59..019a30b854 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2548,6 +2548,8 @@ export default { unfreezeColumns: "Unfreeze Columns ({count})", hideColumn: "Hide This Column", hideSelectedColumns: "Hide Selected Columns ({count})", + hideIdenticalColumns: "Hide Columns with Identical Values", + hideIdenticalColumnsCount: "Hide Columns with Identical Values ({count})", showAllColumnsMenu: "Show All Columns", pasted: "Pasted!", batchAppendPasteNotEditable: "This result cannot be edited.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index eae07d676c..c4d95c6557 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2388,6 +2388,8 @@ export default withEnglishFallback({ unfreezeColumns: "Desinmovilizar columnas ({count})", hideColumn: "Ocultar esta columna", hideSelectedColumns: "Ocultar columnas seleccionadas ({count})", + hideIdenticalColumns: "Ocultar columnas con valores idénticos", + hideIdenticalColumnsCount: "Ocultar columnas con valores idénticos ({count})", showAllColumnsMenu: "Mostrar todas las columnas", pasted: "¡Pegado!", search: "Buscar...", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index 7562212410..b72b151834 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -2407,6 +2407,8 @@ export default withEnglishFallback({ unfreezeColumns: "Batalkan Pembekuan Kolom ({count})", hideColumn: "Sembunyikan Kolom Ini", hideSelectedColumns: "Sembunyikan Kolom Terpilih ({count})", + hideIdenticalColumns: "Sembunyikan Kolom dengan Nilai yang Sama", + hideIdenticalColumnsCount: "Sembunyikan Kolom dengan Nilai yang Sama ({count})", showAllColumnsMenu: "Tampilkan Semua Kolom", pasted: "Ditempel!", batchAppendPasteNotEditable: "Hasil ini tidak dapat diedit.", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index d40deb3fb9..756b0576a4 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2282,6 +2282,8 @@ export default withEnglishFallback({ unfreezeColumns: "Sblocca colonne ({count})", hideColumn: "Nascondi questa colonna", hideSelectedColumns: "Nascondi colonne selezionate ({count})", + hideIdenticalColumns: "Nascondi colonne con valori identici", + hideIdenticalColumnsCount: "Nascondi colonne con valori identici ({count})", showAllColumnsMenu: "Mostra tutte le colonne", pasted: "Incollato!", search: "Cerca...", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index ad6a3749c7..52b88cbb68 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2289,6 +2289,8 @@ export default withEnglishFallback({ unfreezeColumns: "列の固定を解除 ({count})", hideColumn: "この列を非表示", hideSelectedColumns: "選択した列を非表示 ({count})", + hideIdenticalColumns: "同一値の列を非表示", + hideIdenticalColumnsCount: "同一値の列を非表示({count} 列)", showAllColumnsMenu: "すべての列を表示", pasted: "貼り付けました!", search: "検索...", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 0aac038b48..a2bd13ed78 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -2326,6 +2326,8 @@ export default withEnglishFallback({ unfreezeColumns: "컬럼 고정 해제 ({count})", hideColumn: "이 컬럼 숨기기", hideSelectedColumns: "선택한 컬럼 숨기기 ({count})", + hideIdenticalColumns: "동일한 값의 컬럼 숨기기", + hideIdenticalColumnsCount: "동일한 값의 컬럼 숨기기 ({count}개)", showAllColumnsMenu: "모든 컬럼 표시", pasted: "붙여넣었습니다!", replaceText: "바꿀 내용", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 8c9ae5c0c5..91d142d12c 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2284,6 +2284,8 @@ export default withEnglishFallback({ unfreezeColumns: "Descongelar colunas ({count})", hideColumn: "Ocultar esta coluna", hideSelectedColumns: "Ocultar colunas selecionadas ({count})", + hideIdenticalColumns: "Ocultar colunas com valores idênticos", + hideIdenticalColumnsCount: "Ocultar colunas com valores idênticos ({count})", showAllColumnsMenu: "Mostrar todas as colunas", pasted: "Colado!", search: "Pesquisar...", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index a2908b2a8a..472cbf574e 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -2442,6 +2442,8 @@ export default withEnglishFallback({ unfreezeColumns: "Открепить столбцы ({count})", hideColumn: "Скрыть этот столбец", hideSelectedColumns: "Скрыть выбранные столбцы ({count})", + hideIdenticalColumns: "Скрыть столбцы с одинаковыми значениями", + hideIdenticalColumnsCount: "Скрыть столбцы с одинаковыми значениями ({count})", showAllColumnsMenu: "Показать все столбцы", pasted: "Вставлено!", batchAppendPasteNotEditable: "Этот результат нельзя редактировать.", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 4d3cf134c0..8dd78a5675 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -2128,6 +2128,8 @@ export default withEnglishFallback({ unfreezeColumns: "Sütun Dondurmayı Kaldır ({count})", hideColumn: "Bu Sütunu Gizle", hideSelectedColumns: "Seçili Sütunları Gizle ({count})", + hideIdenticalColumns: "Aynı Değerlere Sahip Sütunları Gizle", + hideIdenticalColumnsCount: "Aynı Değerlere Sahip Sütunları Gizle ({count})", showAllColumnsMenu: "Tüm Sütunları Göster", pasted: "Yapıştırıldı!", batchAppendPasteNotEditable: "Bu sonuç düzenlenemez.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index f5db8b647d..cea4e03d94 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2483,6 +2483,8 @@ export default withEnglishFallback({ unfreezeColumns: "取消冻结({count} 列)", hideColumn: "隐藏此列", hideSelectedColumns: "隐藏选中列({count} 列)", + hideIdenticalColumns: "隐藏相同数值列", + hideIdenticalColumnsCount: "隐藏相同数值列({count} 列)", showAllColumnsMenu: "显示全部列", pasted: "已粘贴!", batchAppendPasteNotEditable: "当前结果不可编辑。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 95075b15f5..fe22c78aa4 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2284,6 +2284,8 @@ export default withEnglishFallback({ unfreezeColumns: "取消凍結({count} 欄)", hideColumn: "隱藏此欄", hideSelectedColumns: "隱藏選取欄({count} 欄)", + hideIdenticalColumns: "隱藏相同數值欄", + hideIdenticalColumnsCount: "隱藏相同數值欄({count} 欄)", showAllColumnsMenu: "顯示全部欄", pasted: "已貼上!", search: "搜尋……", diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridContextMenu.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridContextMenu.spec.ts index 7e978e088e..940e88ee8c 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridContextMenu.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridContextMenu.spec.ts @@ -139,7 +139,7 @@ describe("dataGridContextMenu", () => { expect(rowItems.find((item) => item.label === "delete")?.variant).toBe("destructive"); }); - function columnMenuItems(overrides: { hasColumnSelection?: boolean; selectedColumnCount?: number; visibleColumnCount?: number; hiddenColumnCount?: number }) { + function columnMenuItems(overrides: { headerColumn?: boolean; contextColumn?: boolean; contextVisibleColIdx?: number; hasColumnSelection?: boolean; selectedColumnCount?: number; visibleColumnCount?: number; hiddenColumnCount?: number; canHideIdenticalColumns?: boolean }) { const action = vi.fn(); const filter = createDataGridFilterSubmenu({ label: "filter", @@ -149,13 +149,13 @@ describe("dataGridContextMenu", () => { clear: action, }); return createDataGridColumnContextMenuItems({ - headerColumn: true, - contextColumn: false, + headerColumn: overrides.headerColumn ?? true, + contextColumn: overrides.contextColumn ?? false, canCopyAlterSql: false, canFilter: false, hasSort: false, sortMode: "database", - contextVisibleColIdx: 0, + contextVisibleColIdx: overrides.contextVisibleColIdx !== undefined ? overrides.contextVisibleColIdx : 0, hasColumnSelection: false, selectedColumnCount: 1, visibleColumnCount: 3, @@ -175,10 +175,11 @@ describe("dataGridContextMenu", () => { unfreezeColumns: "unfreeze", hideColumn: "hide column", hideSelectedColumns: "hide selected (3)", + hideIdenticalColumns: "hide identical", showAllColumnsMenu: "show all columns", }, icons: { copy: icon, columnDetails: icon, database: icon, ascending: icon, descending: icon, clearSort: icon }, - actions: { copyName: action, copyNames: action, details: action, copyAlterSql: action, sort: action, freezeToColumn: action, freezeSelectedColumns: action, unfreezeColumns: action, hideColumn: action, hideSelectedColumns: action, showAllColumnsMenu: action }, + actions: { copyName: action, copyNames: action, details: action, copyAlterSql: action, sort: action, freezeToColumn: action, freezeSelectedColumns: action, unfreezeColumns: action, hideColumn: action, hideSelectedColumns: action, hideIdenticalColumns: action, showAllColumnsMenu: action }, filterSubmenu: filter, ...overrides, }); @@ -221,4 +222,30 @@ describe("dataGridContextMenu", () => { expect(hideColumn).toBeDefined(); expect(hideColumn?.disabled).toBe(false); }); + + it("offers hide identical columns on a header and disables it when no identical columns exist", () => { + const items = columnMenuItems({ canHideIdenticalColumns: false }); + const hideIdentical = items.find((item) => item.label === "hide identical"); + + expect(hideIdentical).toBeDefined(); + expect(hideIdentical?.disabled).toBe(true); + }); + + it("enables hide identical columns when identical columns exist", () => { + const items = columnMenuItems({ canHideIdenticalColumns: true }); + const hideIdentical = items.find((item) => item.label === "hide identical"); + + expect(hideIdentical).toBeDefined(); + expect(hideIdentical?.disabled).toBe(false); + }); + + it("offers hide identical columns on a cell context menu and shows recovery entry if hidden columns exist", () => { + const items = columnMenuItems({ headerColumn: false, contextColumn: true, contextVisibleColIdx: undefined, canHideIdenticalColumns: true, hiddenColumnCount: 2 }); + const hideIdentical = items.find((item) => item.label === "hide identical"); + const showAll = items.find((item) => item.label === "show all columns"); + + expect(hideIdentical).toBeDefined(); + expect(hideIdentical?.disabled).toBe(false); + expect(showAll).toBeDefined(); + }); }); diff --git a/apps/desktop/src/lib/dataGrid/dataGridColumnVisibility.ts b/apps/desktop/src/lib/dataGrid/dataGridColumnVisibility.ts index 948cba37ac..a302ae2596 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridColumnVisibility.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridColumnVisibility.ts @@ -149,3 +149,49 @@ export function removeAutoHiddenColumnIndexes(hiddenIndexes: ReadonlySet } return next; } + +export function areCellValuesEqual(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (typeof a === "number" && typeof b === "number" && Number.isNaN(a) && Number.isNaN(b)) return true; + if (a === null || b === null || a === undefined || b === undefined) return false; + if (typeof a !== "object" || typeof b !== "object") return false; + if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); + if (a instanceof Uint8Array && b instanceof Uint8Array) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; + } + try { + return JSON.stringify(a) === JSON.stringify(b); + } catch { + return false; + } +} + +export function identicalValueColumnIndexes(rows: ReadonlyArray>, candidateIndexes: readonly number[]): number[] { + if (rows.length < 2) return []; + const candidates = [...candidateIndexes]; + let count = candidates.length; + if (count === 0) return []; + + const firstRow = rows[0]; + if (!firstRow) return []; + + for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) { + const row = rows[rowIndex]; + if (!row) continue; + let write = 0; + for (let read = 0; read < count; read++) { + const col = candidates[read]!; + if (areCellValuesEqual(row[col], firstRow[col])) { + candidates[write++] = col; + } + } + count = write; + if (count === 0) return []; + } + candidates.length = count; + return candidates; +} diff --git a/apps/desktop/src/lib/dataGrid/dataGridContextMenu.ts b/apps/desktop/src/lib/dataGrid/dataGridContextMenu.ts index 29823f3fb7..cbaddacebb 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridContextMenu.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridContextMenu.ts @@ -87,11 +87,12 @@ export function createDataGridColumnContextMenuItems(options: { visibleColumnCount?: number; /** 当前被隐藏的列数量,用于决定是否显示「显示全部列」。 */ hiddenColumnCount?: number; + canHideIdenticalColumns?: boolean; labels: Record< "copyName" | "copyNames" | "details" | "copyAlterSql" | "databaseAscending" | "databaseDescending" | "localAscending" | "localDescending" | "clearSort" | "freezeToColumn" | "freezeSelectedColumns" | "unfreezeColumns" | "hideColumn" | "hideSelectedColumns" | "showAllColumnsMenu", string > & - Partial>; + Partial>; icons: Pick; actions: { copyName: () => void; @@ -106,6 +107,7 @@ export function createDataGridColumnContextMenuItems(options: { unfreezeColumns: () => void; hideColumn: () => void; hideSelectedColumns: () => void; + hideIdenticalColumns?: () => void; showAllColumnsMenu: () => void; }; filterSubmenu: DataGridContextMenuItem; @@ -141,6 +143,13 @@ export function createDataGridColumnContextMenuItems(options: { if (options.hasColumnSelection && selectedColumnCount > 1) { items.push({ label: options.labels.hideSelectedColumns, action: options.actions.hideSelectedColumns, disabled: visibleColumnCount > 0 && selectedColumnCount >= visibleColumnCount }); } + if (options.labels.hideIdenticalColumns && options.actions.hideIdenticalColumns) { + items.push({ + label: options.labels.hideIdenticalColumns, + action: options.actions.hideIdenticalColumns, + disabled: options.canHideIdenticalColumns === false, + }); + } if ((options.hiddenColumnCount ?? 0) > 0) { items.push({ label: options.labels.showAllColumnsMenu, action: options.actions.showAllColumnsMenu }); } @@ -153,6 +162,16 @@ export function createDataGridColumnContextMenuItems(options: { } if (options.labels.freezeCurrentColumn && options.actions.freezeCurrentColumn) items.push({ label: options.labels.freezeCurrentColumn, action: options.actions.freezeCurrentColumn }); items.push({ label: options.labels.freezeToColumn, action: options.actions.freezeToColumn }); + } else if (options.labels.hideIdenticalColumns && options.actions.hideIdenticalColumns) { + items.push({ label: "", separator: true }); + items.push({ + label: options.labels.hideIdenticalColumns, + action: options.actions.hideIdenticalColumns, + disabled: options.canHideIdenticalColumns === false, + }); + if ((options.hiddenColumnCount ?? 0) > 0) { + items.push({ label: options.labels.showAllColumnsMenu, action: options.actions.showAllColumnsMenu }); + } } return items; } diff --git a/packages/app-tests/dataGridColumnVisibility.test.ts b/packages/app-tests/dataGridColumnVisibility.test.ts index 336d9a1080..55993efca5 100644 --- a/packages/app-tests/dataGridColumnVisibility.test.ts +++ b/packages/app-tests/dataGridColumnVisibility.test.ts @@ -3,11 +3,13 @@ import { test } from "vitest"; import { buildDataGridColumnLookupItems, filterDataGridColumnLookupItems } from "../../apps/desktop/src/lib/dataGrid/dataGridColumnLookup.ts"; import { allNullColumnIndexes, + areCellValuesEqual, filterColumnVisibilityOptions, hiddenColumnIndexesAfterHiding, hiddenColumnIndexesForKeys, hiddenColumnIndexesWithAllNullColumns, hiddenColumnKeysForIndexes, + identicalValueColumnIndexes, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, removeAutoHiddenColumnIndexes, @@ -212,3 +214,69 @@ test("keeps one all-null column visible when every column is NULL", () => { assert.deepEqual([...result.hiddenIndexes].sort(), [1, 2]); assert.deepEqual([...result.autoHiddenIndexes].sort(), [1, 2]); }); + +test("compares cell values for equality correctly", () => { + assert.equal(areCellValuesEqual(1, 1), true); + assert.equal(areCellValuesEqual(1, 2), false); + assert.equal(areCellValuesEqual("a", "a"), true); + assert.equal(areCellValuesEqual("a", "b"), false); + assert.equal(areCellValuesEqual(null, null), true); + assert.equal(areCellValuesEqual(undefined, undefined), true); + assert.equal(areCellValuesEqual(null, undefined), false); + assert.equal(areCellValuesEqual(null, ""), false); + assert.equal(areCellValuesEqual(Number.NaN, Number.NaN), true); + assert.equal(areCellValuesEqual(new Date("2026-01-01"), new Date("2026-01-01")), true); + assert.equal(areCellValuesEqual(new Date("2026-01-01"), new Date("2026-01-02")), false); + assert.equal(areCellValuesEqual({ a: 1 }, { a: 1 }), true); + assert.equal(areCellValuesEqual({ a: 1 }, { a: 2 }), false); + assert.equal(areCellValuesEqual([1, 2], [1, 2]), true); + assert.equal(areCellValuesEqual([1, 2], [1, 3]), false); + assert.equal(areCellValuesEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2])), true); + assert.equal(areCellValuesEqual(new Uint8Array([1, 2]), new Uint8Array([1, 3])), false); +}); + +test("finds columns where all rows have identical values", () => { + const rows = [ + [1, "status_ok", "same", 100], + [2, "status_ok", "diff", 100], + [3, "status_ok", "diff2", 100], + ]; + + const identical = identicalValueColumnIndexes(rows, [0, 1, 2, 3]); + assert.deepEqual(identical, [1, 3]); +}); + +test("returns empty array when fewer than 2 rows exist", () => { + assert.deepEqual(identicalValueColumnIndexes([], [0, 1]), []); + assert.deepEqual(identicalValueColumnIndexes([[1, "a"]], [0, 1]), []); +}); + +test("returns empty array when candidate indexes is empty", () => { + assert.deepEqual(identicalValueColumnIndexes([[1, 2], [1, 2]], []), []); +}); + +test("returns empty array when no columns have identical values", () => { + const rows = [ + [1, "a"], + [2, "b"], + ]; + assert.deepEqual(identicalValueColumnIndexes(rows, [0, 1]), []); +}); + +test("detects identical values by original index when middle column is hidden", () => { + // 行保持全宽、候选是原始列索引(模拟第 1 列被隐藏后只剩 [0, 2, 3] 可见) + const rows = [ + [1, "hidden_a", "same", 100], + [2, "hidden_b", "same", 101], + [3, "hidden_c", "same", 102], + ]; + assert.deepEqual(identicalValueColumnIndexes(rows, [0, 2, 3]), [2]); +}); + +test("identifies identical columns with null and json objects", () => { + const rows = [ + [null, { k: 1 }, "x"], + [null, { k: 1 }, "y"], + ]; + assert.deepEqual(identicalValueColumnIndexes(rows, [0, 1, 2]), [0, 1]); +}); diff --git a/packages/app-tests/dataGridContextMenu.test.ts b/packages/app-tests/dataGridContextMenu.test.ts index 2c12c1d3ca..ead1111198 100644 --- a/packages/app-tests/dataGridContextMenu.test.ts +++ b/packages/app-tests/dataGridContextMenu.test.ts @@ -14,6 +14,13 @@ test("set NULL applies a real null value only to editable selections", () => { assert.doesNotMatch(handler, /fillSelectionWithValue\(["'](?:NULL)?["']\)/); }); +test("hide identical columns compares full-width rows, not visibility-projected ones", () => { + const fn = dataGridSource.match(/function getComparisonRows\(\)[^{]*\{[^]*?\n\}/)?.[0] ?? ""; + assert.ok(fn, "getComparisonRows not found"); + assert.match(fn, /\bdisplayItems\.value/); + assert.doesNotMatch(fn, /visibleDisplayItems/); +}); + test("editable cell selections expose generation after bulk edit", () => { const icon = {}; const action = () => {};