feat(structure): persist local column order
This commit is contained in:
13 files changed
+278
-9
No files matched your search
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadTableDataGridColumnOrder, notifyTableDataGridColumnOrderChanged, removeTableDataGridColumnOrder, saveTableDataGridColumnOrder, TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: (key: string) => data.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => data.set(key, value),
|
||||
removeItem: (key: string) => data.delete(key),
|
||||
});
|
||||
}
|
||||
|
||||
describe("table data grid column order storage", () => {
|
||||
beforeEach(installLocalStorage);
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("stores an order independently from the current result column signature", () => {
|
||||
const scopeKey = tableDataGridColumnOrderScopeKey({
|
||||
connectionId: "sqlserver-1",
|
||||
database: "sales",
|
||||
schema: "core",
|
||||
tableName: "products",
|
||||
});
|
||||
const order = ["name\u00000", "id\u00000"];
|
||||
|
||||
saveTableDataGridColumnOrder(scopeKey, order);
|
||||
|
||||
expect(loadTableDataGridColumnOrder(scopeKey)).toEqual(order);
|
||||
});
|
||||
|
||||
it("isolates tables and removes a saved order", () => {
|
||||
const products = tableDataGridColumnOrderScopeKey({ connectionId: "sqlserver-1", database: "sales", schema: "core", tableName: "products" });
|
||||
const orders = tableDataGridColumnOrderScopeKey({ connectionId: "sqlserver-1", database: "sales", schema: "core", tableName: "orders" });
|
||||
saveTableDataGridColumnOrder(products, ["name\u00000", "id\u00000"]);
|
||||
|
||||
expect(loadTableDataGridColumnOrder(orders)).toEqual([]);
|
||||
removeTableDataGridColumnOrder(products);
|
||||
expect(loadTableDataGridColumnOrder(products)).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes a missing schema to the database namespace", () => {
|
||||
const withoutSchema = tableDataGridColumnOrderScopeKey({ connectionId: "sqlite-1", database: "main", tableName: "products" });
|
||||
const explicitMainSchema = tableDataGridColumnOrderScopeKey({ connectionId: "sqlite-1", database: "main", schema: "main", tableName: "products" });
|
||||
|
||||
expect(withoutSchema).toBe(explicitMainSchema);
|
||||
});
|
||||
|
||||
it("notifies other open views when a table order changes", () => {
|
||||
const dispatchEvent = vi.fn();
|
||||
vi.stubGlobal("window", { dispatchEvent });
|
||||
|
||||
notifyTableDataGridColumnOrderChanged("table-scope");
|
||||
|
||||
expect(dispatchEvent).toHaveBeenCalledOnce();
|
||||
const event = dispatchEvent.mock.calls[0]?.[0] as CustomEvent;
|
||||
expect(event.type).toBe(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT);
|
||||
expect(event.detail).toEqual({ scopeKey: "table-scope" });
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
import { getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities";
|
||||
|
||||
describe("tableStructureCapabilities", () => {
|
||||
it("uses table rebuilds only for native SQLite connections", () => {
|
||||
@@ -27,4 +27,39 @@ describe("tableStructureCapabilities", () => {
|
||||
expect(getTableStructureCapabilities("mysql", "mysql").alterStrategy).toBe("direct");
|
||||
expect(getTableStructureCapabilities("postgres", "postgres").alterStrategy).toBe("direct");
|
||||
});
|
||||
|
||||
it("uses local-only column reordering for editable databases without physical reorder support", () => {
|
||||
for (const databaseType of ["sqlserver", "postgres", "sqlite", "oracle", "dameng", "duckdb", "informix"] as const) {
|
||||
expect(supportsLocalTableColumnReorder(databaseType, databaseType)).toBe(true);
|
||||
}
|
||||
|
||||
for (const databaseType of ["mysql", "gbase", "clickhouse"] as const) {
|
||||
expect(supportsLocalTableColumnReorder(databaseType, databaseType)).toBe(false);
|
||||
}
|
||||
expect(supportsLocalTableColumnReorder("influxdb", "influxdb")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat local-only reordering as a database structure change", () => {
|
||||
expect(isPhysicalTableColumnOrderChange("sqlserver", "sqlserver", 0, 2)).toBe(false);
|
||||
expect(isPhysicalTableColumnOrderChange("postgres", "postgres", 0, 2)).toBe(false);
|
||||
expect(isPhysicalTableColumnOrderChange("mysql", "mysql", 0, 2)).toBe(true);
|
||||
});
|
||||
|
||||
it("detects local order changes including newly added columns", () => {
|
||||
const first = { original: {}, originalPosition: 0 };
|
||||
const second = { original: {}, originalPosition: 1 };
|
||||
const added = {};
|
||||
|
||||
expect(hasLocalTableColumnOrderChange([first, second, added])).toBe(false);
|
||||
expect(hasLocalTableColumnOrderChange([first, added, second])).toBe(true);
|
||||
expect(hasLocalTableColumnOrderChange([second, first, added])).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores dropped columns when comparing local order", () => {
|
||||
const first = { original: {}, originalPosition: 0 };
|
||||
const dropped = { original: {}, originalPosition: 1, markedForDrop: true };
|
||||
const third = { original: {}, originalPosition: 2 };
|
||||
|
||||
expect(hasLocalTableColumnOrderChange([first, dropped, third])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,15 @@
|
||||
import { safeLocalStorageGet, safeLocalStorageRemove, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
|
||||
const STORAGE_PREFIX = "dbx-data-grid-column-layout:";
|
||||
const TABLE_STORAGE_PREFIX = "dbx-data-grid-table-column-order:";
|
||||
const STORAGE_VERSION = 1;
|
||||
|
||||
export const TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT = "dbx:table-data-grid-column-order-changed";
|
||||
|
||||
export interface TableDataGridColumnOrderChangedDetail {
|
||||
scopeKey: string;
|
||||
}
|
||||
|
||||
export interface DataGridColumnLayoutScope {
|
||||
connectionId?: string;
|
||||
database?: string;
|
||||
@@ -21,6 +28,18 @@ interface StoredDataGridColumnLayout {
|
||||
order: string[];
|
||||
}
|
||||
|
||||
export interface TableDataGridColumnOrderScope {
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
interface StoredTableDataGridColumnOrder {
|
||||
version: number;
|
||||
order: string[];
|
||||
}
|
||||
|
||||
function normalizeSql(sql?: string): string {
|
||||
return (sql ?? "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
@@ -56,3 +75,37 @@ export function saveDataGridColumnOrder(scopeKey: string, columnKeys: readonly s
|
||||
export function removeDataGridColumnOrder(scopeKey: string) {
|
||||
safeLocalStorageRemove(`${STORAGE_PREFIX}${scopeKey}`);
|
||||
}
|
||||
|
||||
export function tableDataGridColumnOrderScopeKey(scope: TableDataGridColumnOrderScope): string {
|
||||
const namespace = scope.schema?.trim() || scope.database;
|
||||
return [scope.connectionId, scope.database, namespace, scope.tableName].join("\u0001");
|
||||
}
|
||||
|
||||
export function loadTableDataGridColumnOrder(scopeKey: string): string[] {
|
||||
const raw = safeLocalStorageGet(`${TABLE_STORAGE_PREFIX}${scopeKey}`);
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<StoredTableDataGridColumnOrder>;
|
||||
if (parsed.version !== STORAGE_VERSION || !Array.isArray(parsed.order)) return [];
|
||||
return parsed.order.filter((key): key is string => typeof key === "string");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function saveTableDataGridColumnOrder(scopeKey: string, order: readonly string[]) {
|
||||
const payload: StoredTableDataGridColumnOrder = {
|
||||
version: STORAGE_VERSION,
|
||||
order: [...order],
|
||||
};
|
||||
safeLocalStorageSet(`${TABLE_STORAGE_PREFIX}${scopeKey}`, JSON.stringify(payload));
|
||||
}
|
||||
|
||||
export function removeTableDataGridColumnOrder(scopeKey: string) {
|
||||
safeLocalStorageRemove(`${TABLE_STORAGE_PREFIX}${scopeKey}`);
|
||||
}
|
||||
|
||||
export function notifyTableDataGridColumnOrderChanged(scopeKey: string) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.dispatchEvent(new CustomEvent<TableDataGridColumnOrderChangedDetail>(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, { detail: { scopeKey } }));
|
||||
}
|
||||
@@ -339,6 +339,23 @@ export function canEditTableStructure(dbType?: DatabaseType): boolean {
|
||||
return caps.createTable || caps.addColumn || caps.alterExistingColumn || caps.createIndex || caps.dropIndex;
|
||||
}
|
||||
|
||||
export function supportsLocalTableColumnReorder(dbType?: DatabaseType, connectionDbType?: DatabaseType): boolean {
|
||||
const caps = getTableStructureCapabilities(dbType, connectionDbType);
|
||||
return canEditTableStructure(dbType) && !caps.reorderColumn;
|
||||
}
|
||||
|
||||
export function isPhysicalTableColumnOrderChange(dbType: DatabaseType | undefined, connectionDbType: DatabaseType | undefined, originalPosition: number | undefined, currentPosition: number): boolean {
|
||||
return getTableStructureCapabilities(dbType, connectionDbType).reorderColumn && originalPosition !== currentPosition;
|
||||
}
|
||||
|
||||
export function hasLocalTableColumnOrderChange(columns: readonly { originalPosition?: number; original?: unknown; markedForDrop?: boolean }[]): boolean {
|
||||
const activeColumns = columns.filter((column) => !column.markedForDrop);
|
||||
// Databases without physical reorder support keep existing columns in ordinal order
|
||||
// and append newly added columns, so compare against that post-save layout.
|
||||
const databaseOrder = [...activeColumns.filter((column) => column.original).sort((left, right) => (left.originalPosition ?? Number.MAX_SAFE_INTEGER) - (right.originalPosition ?? Number.MAX_SAFE_INTEGER)), ...activeColumns.filter((column) => !column.original)];
|
||||
return activeColumns.some((column, index) => column !== databaseOrder[index]);
|
||||
}
|
||||
|
||||
export function canAddTableStructureColumn(dbType: DatabaseType | undefined, isCreateMode: boolean): boolean {
|
||||
const caps = getTableStructureCapabilities(dbType);
|
||||
return isCreateMode ? caps.createTable : caps.addColumn;
|
||||
|
||||
Reference in new issue
Block a user