feat(structure): persist local column order
This commit is contained in:
parent
c3ec7fceef
commit
0650428d1c
|
|
@ -159,7 +159,19 @@ import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE, normalizeResultPageSize, re
|
|||
import { allNullColumnIndexes, filterColumnVisibilityOptions, hiddenColumnIndexesWithAllNullColumns, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, removeAutoHiddenColumnIndexes, visibleColumnIndexesForFilter } from "@/lib/dataGrid/dataGridColumnVisibility";
|
||||
import { buildDataGridColumnLookupItems, filterDataGridColumnLookupItems } from "@/lib/dataGrid/dataGridColumnLookup";
|
||||
import { columnOrderKeysForIndexes, isDefaultColumnOrder, moveVisibleColumnIndex, orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder";
|
||||
import { dataGridColumnLayoutScopeKey, loadDataGridColumnOrder, removeDataGridColumnOrder, saveDataGridColumnOrder } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
|
||||
import {
|
||||
dataGridColumnLayoutScopeKey,
|
||||
loadDataGridColumnOrder,
|
||||
loadTableDataGridColumnOrder,
|
||||
notifyTableDataGridColumnOrderChanged,
|
||||
removeDataGridColumnOrder,
|
||||
removeTableDataGridColumnOrder,
|
||||
saveDataGridColumnOrder,
|
||||
saveTableDataGridColumnOrder,
|
||||
TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT,
|
||||
tableDataGridColumnOrderScopeKey,
|
||||
type TableDataGridColumnOrderChangedDetail,
|
||||
} from "@/lib/dataGrid/dataGridColumnLayoutStorage";
|
||||
import { parseClipboardTable, summarizeSelection } from "@/lib/dataGrid/gridSelection";
|
||||
import { columnHeaderCanvasPointerDisabled, columnHeaderClickShouldBeSuppressed, columnHeaderPreviewOffsetForColumn, columnHeaderTooltipDisabled } from "@/lib/dataGrid/dataGridColumnHeaderInteraction";
|
||||
|
||||
|
|
@ -2554,6 +2566,15 @@ const columnLayoutScopeKey = computed(() =>
|
|||
sourceColumns: props.sourceColumns,
|
||||
}),
|
||||
);
|
||||
const tableColumnOrderScopeKey = computed(() => {
|
||||
if (props.context !== "table-data" || !props.connectionId || !props.database || !props.tableMeta?.tableName) return "";
|
||||
return tableDataGridColumnOrderScopeKey({
|
||||
connectionId: props.connectionId,
|
||||
database: props.database,
|
||||
schema: props.tableMeta.schema,
|
||||
tableName: props.tableMeta.tableName,
|
||||
});
|
||||
});
|
||||
const persistedColumnOrderKeys = ref<string[]>([]);
|
||||
const displayableColumnIndexes = computed(() =>
|
||||
props.result.columns
|
||||
|
|
@ -2633,20 +2654,42 @@ function invertColumnVisibility() {
|
|||
hiddenColumnIndexes.value = invertedHiddenColumnIndexes(displayableColumnIndexes.value, hiddenColumnIndexes.value);
|
||||
}
|
||||
function loadColumnOrder() {
|
||||
persistedColumnOrderKeys.value = loadDataGridColumnOrder(columnLayoutScopeKey.value, columnOrderKeys.value);
|
||||
const tableOrder = tableColumnOrderScopeKey.value ? loadTableDataGridColumnOrder(tableColumnOrderScopeKey.value) : [];
|
||||
persistedColumnOrderKeys.value = tableOrder.length ? tableOrder : loadDataGridColumnOrder(columnLayoutScopeKey.value, columnOrderKeys.value);
|
||||
}
|
||||
function onTableDataGridColumnOrderChanged(event: Event) {
|
||||
if (!(event instanceof CustomEvent)) return;
|
||||
const detail = event.detail as TableDataGridColumnOrderChangedDetail | undefined;
|
||||
if (!detail || detail.scopeKey !== tableColumnOrderScopeKey.value) return;
|
||||
loadColumnOrder();
|
||||
nextTick(refreshGridScrollerMetrics);
|
||||
}
|
||||
function persistColumnOrder(indexes: number[]) {
|
||||
const tableScopeKey = tableColumnOrderScopeKey.value;
|
||||
if (isDefaultColumnOrder(displayableColumnIndexes.value, indexes)) {
|
||||
removeDataGridColumnOrder(columnLayoutScopeKey.value);
|
||||
if (tableScopeKey) {
|
||||
removeTableDataGridColumnOrder(tableScopeKey);
|
||||
notifyTableDataGridColumnOrderChanged(tableScopeKey);
|
||||
}
|
||||
persistedColumnOrderKeys.value = [];
|
||||
return;
|
||||
}
|
||||
const keys = columnOrderKeysForIndexes(indexes, columnOrderKeys.value);
|
||||
persistedColumnOrderKeys.value = keys;
|
||||
saveDataGridColumnOrder(columnLayoutScopeKey.value, columnOrderKeys.value, keys);
|
||||
if (tableScopeKey) {
|
||||
saveTableDataGridColumnOrder(tableScopeKey, keys);
|
||||
notifyTableDataGridColumnOrderChanged(tableScopeKey);
|
||||
}
|
||||
}
|
||||
function resetColumnOrder() {
|
||||
removeDataGridColumnOrder(columnLayoutScopeKey.value);
|
||||
const tableScopeKey = tableColumnOrderScopeKey.value;
|
||||
if (tableScopeKey) {
|
||||
removeTableDataGridColumnOrder(tableScopeKey);
|
||||
notifyTableDataGridColumnOrderChanged(tableScopeKey);
|
||||
}
|
||||
persistedColumnOrderKeys.value = [];
|
||||
nextTick(refreshGridScrollerMetrics);
|
||||
}
|
||||
|
|
@ -2678,7 +2721,7 @@ watch(allNullColumnIndexesForResult, () => {
|
|||
autoHiddenNullColumnIndexes.value = new Set();
|
||||
hideAllNullColumns();
|
||||
});
|
||||
watch(() => columnLayoutScopeKey.value, loadColumnOrder, { immediate: true });
|
||||
watch([columnLayoutScopeKey, tableColumnOrderScopeKey], loadColumnOrder, { immediate: true });
|
||||
const firstVisibleColumnIndex = computed(() => visibleColumnIndexes.value[0] ?? 0);
|
||||
function actualColumnIndex(visibleColumnIndex: number): number {
|
||||
return visibleColumnIndexes.value[visibleColumnIndex] ?? visibleColumnIndex;
|
||||
|
|
@ -6266,6 +6309,7 @@ onMounted(() => {
|
|||
window.visualViewport?.addEventListener("resize", resizeFocusedConditionInputs);
|
||||
window.addEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh);
|
||||
window.addEventListener("dbx:ui-scale-applied", resizeFocusedConditionInputs);
|
||||
window.addEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onTableDataGridColumnOrderChanged);
|
||||
document.addEventListener("pointerdown", onConditionSuggestionDocumentPointerDown, true);
|
||||
});
|
||||
onDeactivated(pauseCanvasGridWork);
|
||||
|
|
@ -6292,6 +6336,7 @@ onUnmounted(() => {
|
|||
window.visualViewport?.removeEventListener("resize", resizeFocusedConditionInputs);
|
||||
window.removeEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh);
|
||||
window.removeEventListener("dbx:ui-scale-applied", resizeFocusedConditionInputs);
|
||||
window.removeEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onTableDataGridColumnOrderChanged);
|
||||
document.removeEventListener("pointerdown", onConditionSuggestionDocumentPointerDown, true);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStor
|
|||
import { type BuildTableStructureChangeSqlOptions, type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql";
|
||||
import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates";
|
||||
import { getTableMetadataCapabilities, firstStructureMetadataTab, isStructureMetadataTabSupported } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
import { canAddTableStructureColumn, getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities";
|
||||
import { orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder";
|
||||
import { loadTableDataGridColumnOrder, notifyTableDataGridColumnOrderChanged, removeTableDataGridColumnOrder, saveTableDataGridColumnOrder, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
|
||||
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import type { TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget, TableStructureEditorViewport } from "@/types/database";
|
||||
import {
|
||||
|
|
@ -191,7 +193,7 @@ function columnChanged(column: EditableStructureColumn, index: number): boolean
|
|||
if (!column.original || column.markedForDrop) return true;
|
||||
const original = column.original;
|
||||
return (
|
||||
column.originalPosition !== index ||
|
||||
isPhysicalTableColumnOrderChange(databaseType.value, connection.value?.db_type, column.originalPosition, index) ||
|
||||
column.name !== original.name ||
|
||||
column.dataType !== original.data_type ||
|
||||
column.isNullable !== original.is_nullable ||
|
||||
|
|
@ -1152,6 +1154,7 @@ function resetState() {
|
|||
highlightedIndexId.value = null;
|
||||
appliedInitialTargetSearchKey = "";
|
||||
appliedInitialTargetScrollKey = "";
|
||||
localColumnOrderNoticeShown.value = false;
|
||||
}
|
||||
|
||||
async function reloadStructureFromDatabase() {
|
||||
|
|
@ -1216,7 +1219,7 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL
|
|||
// Load live charset/collation metadata from the MySQL server so the column
|
||||
// editor shows the correct options for the server version.
|
||||
void loadCharsetMetadata();
|
||||
columns.value = createColumnDrafts(nextColumns, databaseType.value);
|
||||
columns.value = applyStoredLocalColumnOrder(createColumnDrafts(nextColumns, databaseType.value));
|
||||
}
|
||||
|
||||
const nextTableComment = await tableCommentPromise;
|
||||
|
|
@ -1325,6 +1328,7 @@ type ColumnDragState = {
|
|||
};
|
||||
|
||||
const columnDragState = ref<ColumnDragState | null>(null);
|
||||
const localColumnOrderNoticeShown = ref(false);
|
||||
let columnDragPreviousBodyUserSelect = "";
|
||||
let columnDragPreviousBodyCursor = "";
|
||||
let columnDragTracking = false;
|
||||
|
|
@ -1350,7 +1354,53 @@ function canDropColumnAt(sourceIndex: number, insertionIndex: number): boolean {
|
|||
return crossedColumns.every((column) => !column.original);
|
||||
}
|
||||
|
||||
const canShowColumnDragControls = computed(() => isCreateMode.value || structureCapabilities.value.reorderColumn);
|
||||
const usesLocalTableColumnOrder = computed(() => !isCreateMode.value && supportsLocalTableColumnReorder(databaseType.value, connection.value?.db_type));
|
||||
const canShowColumnDragControls = computed(() => isCreateMode.value || structureCapabilities.value.reorderColumn || usesLocalTableColumnOrder.value);
|
||||
|
||||
function localTableColumnOrderScopeKey(): string {
|
||||
return tableDataGridColumnOrderScopeKey({
|
||||
connectionId: props.connectionId,
|
||||
database: props.database,
|
||||
schema: props.schema,
|
||||
tableName: props.tableName,
|
||||
});
|
||||
}
|
||||
|
||||
function localColumnOrderKeys(items: readonly EditableStructureColumn[]): string[] {
|
||||
return uniqueDataGridColumnOrderKeys(items.map((column) => column.name));
|
||||
}
|
||||
|
||||
const hasLocalColumnOrderChange = computed(() => {
|
||||
if (!usesLocalTableColumnOrder.value) return false;
|
||||
return hasLocalTableColumnOrderChange(columns.value);
|
||||
});
|
||||
|
||||
function applyStoredLocalColumnOrder(items: EditableStructureColumn[]): EditableStructureColumn[] {
|
||||
if (!usesLocalTableColumnOrder.value) return items;
|
||||
const orderedKeys = loadTableDataGridColumnOrder(localTableColumnOrderScopeKey());
|
||||
if (!orderedKeys.length) return items;
|
||||
const columnKeys = uniqueDataGridColumnOrderKeys(items.map((column) => column.name));
|
||||
const indexes = orderedColumnIndexes({
|
||||
availableIndexes: items.map((_, index) => index),
|
||||
columnKeys,
|
||||
orderedKeys,
|
||||
});
|
||||
return indexes.map((index) => items[index]).filter((column): column is EditableStructureColumn => !!column);
|
||||
}
|
||||
|
||||
function persistLocalColumnOrder(showNotice = true) {
|
||||
if (!usesLocalTableColumnOrder.value) return;
|
||||
const scopeKey = localTableColumnOrderScopeKey();
|
||||
if (hasLocalColumnOrderChange.value) {
|
||||
saveTableDataGridColumnOrder(scopeKey, localColumnOrderKeys(columns.value));
|
||||
} else {
|
||||
removeTableDataGridColumnOrder(scopeKey);
|
||||
}
|
||||
notifyTableDataGridColumnOrderChanged(scopeKey);
|
||||
if (!showNotice || localColumnOrderNoticeShown.value) return;
|
||||
localColumnOrderNoticeShown.value = true;
|
||||
toast(t("structureEditor.localColumnOrderNotice"), 4000);
|
||||
}
|
||||
|
||||
function isSqlServerIdentityChecked(column: EditableStructureColumn): boolean {
|
||||
return !!column.extra.autoIncrement || !!column.extra.identity;
|
||||
|
|
@ -1510,6 +1560,7 @@ function moveColumnTo(index: number, insertionIndex: number) {
|
|||
const adjustedInsertionIndex = insertionIndex > index ? insertionIndex - 1 : insertionIndex;
|
||||
nextColumns.splice(adjustedInsertionIndex, 0, column);
|
||||
columns.value = nextColumns;
|
||||
persistLocalColumnOrder();
|
||||
}
|
||||
|
||||
function onColumnDragPointerDown(index: number, event: PointerEvent) {
|
||||
|
|
@ -2097,6 +2148,8 @@ async function applyChanges() {
|
|||
emit("saved", tableComment.value !== originalTableComment.value);
|
||||
emit("close");
|
||||
} else {
|
||||
// Refresh persisted keys after successful renames/additions before metadata reloads.
|
||||
persistLocalColumnOrder(false);
|
||||
saving.value = false;
|
||||
postSaveRefreshing.value = true;
|
||||
skipNextRefreshVersion = true;
|
||||
|
|
@ -2788,7 +2841,7 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
|||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="[structureActionButtonClass, canDragColumn(index) ? 'cursor-grab active:cursor-grabbing' : 'cursor-not-allowed']"
|
||||
:class="[structureActionButtonClass, canDragColumn(index) ? 'cursor-grab active:cursor-grabbing' : 'cursor-not-allowed', hasLocalColumnOrderChange ? 'border-primary/30 bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary' : '']"
|
||||
:disabled="!canDragColumn(index)"
|
||||
:title="t('structureEditor.dragColumn')"
|
||||
:aria-label="t('structureEditor.dragColumn')"
|
||||
|
|
|
|||
|
|
@ -1892,6 +1892,7 @@ export default {
|
|||
moveColumnUp: "Move column up",
|
||||
moveColumnDown: "Move column down",
|
||||
dragColumn: "Drag to reorder column",
|
||||
localColumnOrderNotice: "Column order is saved locally and does not change the database table structure.",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
emptyReadonly: "No records",
|
||||
|
|
|
|||
|
|
@ -1817,6 +1817,7 @@ export default withEnglishFallback({
|
|||
moveColumnUp: "Mover columna arriba",
|
||||
moveColumnDown: "Mover columna abajo",
|
||||
dragColumn: "Arrastra para reordenar columnas",
|
||||
localColumnOrderNotice: "El orden de las columnas se guarda solo localmente y no modifica la estructura de la base de datos.",
|
||||
yes: "Sí",
|
||||
no: "No",
|
||||
emptyReadonly: "Sin registros",
|
||||
|
|
|
|||
|
|
@ -1815,6 +1815,7 @@ export default withEnglishFallback({
|
|||
moveColumnUp: "Sposta colonna su",
|
||||
moveColumnDown: "Sposta colonna giù",
|
||||
dragColumn: "Trascina per riordinare la colonna",
|
||||
localColumnOrderNotice: "L'ordine delle colonne viene salvato solo localmente e non modifica la struttura del database.",
|
||||
yes: "Sì",
|
||||
no: "No",
|
||||
emptyReadonly: "Nessun record",
|
||||
|
|
|
|||
|
|
@ -1850,6 +1850,7 @@ export default withEnglishFallback({
|
|||
moveColumnUp: "列を上に移動",
|
||||
moveColumnDown: "列を下に移動",
|
||||
dragColumn: "ドラッグして列の順序を変更",
|
||||
localColumnOrderNotice: "列の順序はローカルにのみ保存され、データベースのテーブル構造は変更されません。",
|
||||
yes: "はい",
|
||||
no: "いいえ",
|
||||
emptyReadonly: "レコードなし",
|
||||
|
|
|
|||
|
|
@ -1817,6 +1817,7 @@ export default withEnglishFallback({
|
|||
moveColumnUp: "Mover coluna para cima",
|
||||
moveColumnDown: "Mover coluna para baixo",
|
||||
dragColumn: "Arraste para reordenar colunas",
|
||||
localColumnOrderNotice: "A ordem das colunas é salva apenas localmente e não altera a estrutura do banco de dados.",
|
||||
yes: "Sim",
|
||||
no: "Não",
|
||||
emptyReadonly: "Nenhum registro",
|
||||
|
|
|
|||
|
|
@ -1891,6 +1891,7 @@ export default withEnglishFallback({
|
|||
moveColumnUp: "上移字段",
|
||||
moveColumnDown: "下移字段",
|
||||
dragColumn: "拖拽调整字段顺序",
|
||||
localColumnOrderNotice: "字段顺序仅保存在本地,不会修改数据库表结构。",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
emptyReadonly: "暂无记录",
|
||||
|
|
|
|||
|
|
@ -1721,6 +1721,7 @@ export default withEnglishFallback({
|
|||
moveColumnUp: "上移欄位",
|
||||
moveColumnDown: "下移欄位",
|
||||
dragColumn: "拖曳調整欄位順序",
|
||||
localColumnOrderNotice: "欄位順序僅儲存在本機,不會修改資料庫資料表結構。",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
emptyReadonly: "暫無記錄",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue