- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
650 lines
20 KiB
TypeScript
650 lines
20 KiB
TypeScript
import { create } from "zustand";
|
|
import type { QueryResult, TableInfo, ChangeItemType, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, IndexInfo, ConstraintInfo, ObjectType, RoleInfo } from "../lib/types";
|
|
import { getDatabases, getSchemas, getTables } from "../lib/commands";
|
|
import type { ObjectKind, DdlParams } from "../lib/objectCrud";
|
|
|
|
// ─── Local types ────────────────────────────────────────────────
|
|
|
|
export type QueueStatus = "pending" | "cancelled" | "committed" | "failed";
|
|
|
|
export type FilterOperator = "eq" | "neq" | "contains" | "starts" | "ends" | "gt" | "lt" | "null" | "notnull";
|
|
|
|
export interface FilterRule {
|
|
id: string;
|
|
column: string;
|
|
operator: FilterOperator;
|
|
value: string;
|
|
}
|
|
|
|
export interface SortRule {
|
|
id: string;
|
|
column: string;
|
|
order: "asc" | "desc";
|
|
}
|
|
|
|
export interface QueueItem {
|
|
id: string;
|
|
type: ChangeItemType;
|
|
sql: string;
|
|
schema?: string;
|
|
table?: string;
|
|
primaryKey?: Record<string, unknown>;
|
|
oldData?: Record<string, unknown> | null;
|
|
newData?: Record<string, unknown> | null;
|
|
columns?: string[];
|
|
rows?: unknown[][];
|
|
status: QueueStatus;
|
|
error?: string | null;
|
|
description?: string | null;
|
|
createdAt: number;
|
|
}
|
|
|
|
/** Payload carried by an "objectForm" tab — the kind + params for the
|
|
* object create/edit form and the SQL toggle it renders. */
|
|
export interface ViewerFormTabPayload {
|
|
kind: ObjectKind;
|
|
params: DdlParams;
|
|
title: string;
|
|
description: string;
|
|
mode: "create" | "edit";
|
|
}
|
|
|
|
export interface ViewerTab {
|
|
id: string;
|
|
schema: string;
|
|
table: string;
|
|
page: number;
|
|
pageSize: number;
|
|
loading: boolean;
|
|
error: string | null;
|
|
data: QueryResult | null;
|
|
columnFilter?: { column: string; value: string };
|
|
filterRules: FilterRule[];
|
|
sortRules: SortRule[];
|
|
hiddenColumns: string[];
|
|
smartSortApplied: boolean;
|
|
tabType: "table" | "query" | "object" | "objectForm";
|
|
query?: string;
|
|
objectType?: ObjectType | null;
|
|
objectItem?: unknown;
|
|
form?: ViewerFormTabPayload;
|
|
}
|
|
|
|
// ─── Auto-increment counters ───────────────────────────────────
|
|
|
|
let tabCounter = 0;
|
|
let changeCounter = 0;
|
|
|
|
const initialTab = (schema: string, table: string, defaultPageSize?: number): ViewerTab => ({
|
|
id: `tab-${++tabCounter}`,
|
|
schema,
|
|
table,
|
|
page: 1,
|
|
pageSize: defaultPageSize ?? 50,
|
|
loading: true,
|
|
error: null,
|
|
data: null,
|
|
filterRules: [],
|
|
sortRules: [],
|
|
hiddenColumns: [],
|
|
smartSortApplied: false,
|
|
tabType: "table",
|
|
query: undefined,
|
|
});
|
|
|
|
// ─── State interface ────────────────────────────────────────────
|
|
|
|
interface DbViewerState {
|
|
tabs: ViewerTab[];
|
|
activeTabId: string | null;
|
|
defaultPageSize: number;
|
|
changesQueue: QueueItem[];
|
|
changesPanelExpanded: boolean;
|
|
databases: string[];
|
|
schemas: string[];
|
|
tables: TableInfo[];
|
|
schemaTreeLoading: boolean;
|
|
currentDatabase: string | null;
|
|
currentSchema: string | null;
|
|
objectSearchOpen: boolean;
|
|
selectedObjectType: ObjectType | null;
|
|
/** View the DB viewer should switch to ("db-viewer" | "objects" | ...).
|
|
* Set by the search palette before closing; consumed (and cleared) by
|
|
* DbViewerScreen's navigation effect. */
|
|
requestedView: string | null;
|
|
functions: FunctionInfo[] | null;
|
|
triggers: TriggerInfo[] | null;
|
|
sequences: SequenceInfo[] | null;
|
|
enums: EnumInfo[] | null;
|
|
extensions: ExtensionInfo[] | null;
|
|
indexes: IndexInfo[] | null;
|
|
setSchemaTreeLoading: (loading: boolean) => void;
|
|
constraints: ConstraintInfo[] | null;
|
|
roles: RoleInfo[];
|
|
|
|
// Actions
|
|
openTab: (schema: string, table: string, forceNew?: boolean) => void;
|
|
openQueryTab: () => void;
|
|
openObjectTab: (objectType: ObjectType, schema: string, name: string, item?: unknown) => void;
|
|
openFormTab: (opts: {
|
|
kind: ObjectKind;
|
|
schema: string;
|
|
name: string;
|
|
title: string;
|
|
description: string;
|
|
mode: "create" | "edit";
|
|
params: DdlParams;
|
|
}) => void;
|
|
updateFormTabParams: (tabId: string, params: DdlParams) => void;
|
|
setDefaultPageSize: (size: number) => void;
|
|
closeTab: (tabId: string) => void;
|
|
reorderTab: (fromIndex: number, toIndex: number) => void;
|
|
closeTabsForTable: (schema: string, table: string) => void;
|
|
setActiveTab: (tabId: string) => void;
|
|
setPage: (tabId: string, page: number) => void;
|
|
setPageSize: (tabId: string, pageSize: number) => void;
|
|
setTabData: (tabId: string, data: QueryResult) => void;
|
|
setTabLoading: (tabId: string, loading: boolean) => void;
|
|
setTabError: (tabId: string, error: string) => void;
|
|
setColumnFilter: (tabId: string, column: string, value: string) => void;
|
|
clearColumnFilter: (tabId: string) => void;
|
|
setFilterRules: (tabId: string, rules: FilterRule[]) => void;
|
|
setSortRules: (tabId: string, rules: SortRule[]) => void;
|
|
setHiddenColumns: (tabId: string, columns: string[]) => void;
|
|
toggleHiddenColumn: (tabId: string, column: string) => void;
|
|
setSmartSortApplied: (tabId: string) => void;
|
|
addChange: (input: {
|
|
type: ChangeItemType;
|
|
sql?: string;
|
|
schema?: string;
|
|
table?: string;
|
|
primaryKey?: Record<string, unknown>;
|
|
oldData?: Record<string, unknown> | null;
|
|
newData?: Record<string, unknown> | null;
|
|
columns?: string[];
|
|
rows?: unknown[][];
|
|
description?: string | null;
|
|
}) => void;
|
|
cancelChange: (changeId: string) => void;
|
|
removeChange: (changeId: string) => void;
|
|
clearChanges: () => void;
|
|
markChangeCommitted: (changeId: string) => void;
|
|
markChangeFailed: (changeId: string, error: string) => void;
|
|
toggleChangesPanel: () => void;
|
|
setCurrentDatabase: (db: string | null) => void;
|
|
setCurrentSchema: (schema: string | null) => void;
|
|
setObjectSearchOpen: (open: boolean) => void;
|
|
setSelectedObjectType: (t: ObjectType | null) => void;
|
|
setRequestedView: (view: string | null) => void;
|
|
setFunctions: (functions: FunctionInfo[]) => void;
|
|
setTriggers: (triggers: TriggerInfo[]) => void;
|
|
setSequences: (sequences: SequenceInfo[]) => void;
|
|
setEnums: (enums: EnumInfo[]) => void;
|
|
setExtensions: (extensions: ExtensionInfo[]) => void;
|
|
setIndexes: (indexes: IndexInfo[]) => void;
|
|
setConstraints: (constraints: ConstraintInfo[]) => void;
|
|
setRoles: (roles: RoleInfo[]) => void;
|
|
stageCellEdit: (input: {
|
|
tabId: string;
|
|
schema: string;
|
|
table: string;
|
|
primaryKey: Record<string, unknown>;
|
|
oldData: Record<string, unknown>;
|
|
newData: Record<string, unknown>;
|
|
description?: string;
|
|
}) => void;
|
|
populate: (
|
|
databases: string[],
|
|
schemas: string[],
|
|
tables: TableInfo[],
|
|
) => void;
|
|
refreshTree: (connectionId: string, schema?: string) => Promise<void>;
|
|
reset: () => void;
|
|
}
|
|
|
|
// ─── Initial state ──────────────────────────────────────────────
|
|
|
|
const initialState = {
|
|
tabs: [] as ViewerTab[],
|
|
activeTabId: null as string | null,
|
|
defaultPageSize: 50,
|
|
changesQueue: [] as QueueItem[],
|
|
changesPanelExpanded: true,
|
|
databases: [] as string[],
|
|
schemas: [] as string[],
|
|
tables: [] as TableInfo[],
|
|
currentDatabase: null as string | null,
|
|
currentSchema: null as string | null,
|
|
objectSearchOpen: false,
|
|
selectedObjectType: null as ObjectType | null,
|
|
requestedView: null as string | null,
|
|
functions: null as FunctionInfo[] | null,
|
|
triggers: null as TriggerInfo[] | null,
|
|
sequences: null as SequenceInfo[] | null,
|
|
enums: null as EnumInfo[] | null,
|
|
extensions: null as ExtensionInfo[] | null,
|
|
indexes: null as IndexInfo[] | null,
|
|
constraints: null as ConstraintInfo[] | null,
|
|
roles: [] as RoleInfo[],
|
|
schemaTreeLoading: false,
|
|
};
|
|
|
|
// ─── Store ──────────────────────────────────────────────────────
|
|
|
|
export const useDbViewerStore = create<DbViewerState>((set, get) => ({
|
|
...initialState,
|
|
|
|
openTab: (schema, table, forceNew = false) => {
|
|
const { tabs } = get();
|
|
|
|
// Dedup: if not forceNew and an identical tab exists, just activate it
|
|
if (!forceNew) {
|
|
const existing = tabs.find(
|
|
(t) => t.schema === schema && t.table === table,
|
|
);
|
|
if (existing) {
|
|
set({ activeTabId: existing.id });
|
|
return;
|
|
}
|
|
}
|
|
|
|
const tab = initialTab(schema, table, get().defaultPageSize);
|
|
set({ tabs: [...tabs, tab], activeTabId: tab.id });
|
|
},
|
|
|
|
openQueryTab: () => {
|
|
const { tabs, currentSchema } = get();
|
|
const queryCount = tabs.filter((t) => t.tabType === "query").length;
|
|
const tab: ViewerTab = {
|
|
id: `tab-${++tabCounter}`,
|
|
schema: currentSchema ?? "public",
|
|
table: queryCount === 0 ? "Query" : `Query ${queryCount + 1}`,
|
|
page: 1,
|
|
pageSize: get().defaultPageSize,
|
|
loading: false,
|
|
error: null,
|
|
data: null,
|
|
filterRules: [],
|
|
sortRules: [],
|
|
hiddenColumns: [],
|
|
smartSortApplied: false,
|
|
tabType: "query",
|
|
query: "",
|
|
};
|
|
set({ tabs: [...tabs, tab], activeTabId: tab.id });
|
|
},
|
|
|
|
openObjectTab: (objectType, schema, name, item) => {
|
|
const { tabs } = get();
|
|
|
|
// Dedup on the object's identity (objectType + schema + name); an object
|
|
// tab is distinct from a table tab of the same name.
|
|
const existing = tabs.find(
|
|
(t) =>
|
|
t.tabType === "object" &&
|
|
t.objectType === objectType &&
|
|
t.schema === schema &&
|
|
t.table === name,
|
|
);
|
|
if (existing) {
|
|
set({ activeTabId: existing.id });
|
|
return;
|
|
}
|
|
|
|
const tab: ViewerTab = {
|
|
id: `tab-${++tabCounter}`,
|
|
schema,
|
|
table: name,
|
|
page: 1,
|
|
pageSize: get().defaultPageSize,
|
|
loading: false,
|
|
error: null,
|
|
data: null,
|
|
filterRules: [],
|
|
sortRules: [],
|
|
hiddenColumns: [],
|
|
smartSortApplied: false,
|
|
tabType: "object",
|
|
objectType,
|
|
objectItem: item,
|
|
};
|
|
set({ tabs: [...tabs, tab], activeTabId: tab.id });
|
|
},
|
|
|
|
openFormTab: ({ kind, schema, name, title, description, mode, params }) => {
|
|
const { tabs } = get();
|
|
|
|
// Resolve the effective schema before building the tab: an explicit schema
|
|
// wins, otherwise fall back to the viewer's current schema, then "public".
|
|
// The form payload (params) is left untouched — the tab schema is used for
|
|
// dedup/identity only.
|
|
const effSchema = schema || get().currentSchema || "public";
|
|
|
|
// Dedup key semantics:
|
|
// create -> `create:<kind>` (one create form per kind;
|
|
// schema/name excluded because the user may rename while typing)
|
|
// edit -> `edit:<kind>:<schema>:<name>`
|
|
// The key is derived from the stored form payload (params carries
|
|
// { schema, name } per the objectCrud contract), so no extra field is
|
|
// needed on ViewerTab.
|
|
const existing = tabs.find(
|
|
(t) =>
|
|
t.tabType === "objectForm" &&
|
|
t.form?.kind === kind &&
|
|
t.form?.mode === mode &&
|
|
(mode === "create" ||
|
|
(t.schema === schema && t.form?.params?.name === name)),
|
|
);
|
|
if (existing) {
|
|
// Re-clicking the same form focuses the tab and never clobbers its
|
|
// in-progress params.
|
|
set({ activeTabId: existing.id });
|
|
return;
|
|
}
|
|
|
|
const tab: ViewerTab = {
|
|
id: `tab-${++tabCounter}`,
|
|
schema: effSchema,
|
|
table: title,
|
|
page: 1,
|
|
pageSize: get().defaultPageSize,
|
|
loading: false,
|
|
error: null,
|
|
data: null,
|
|
filterRules: [],
|
|
sortRules: [],
|
|
hiddenColumns: [],
|
|
smartSortApplied: false,
|
|
tabType: "objectForm",
|
|
objectType: null,
|
|
form: { kind, params, title, description, mode },
|
|
};
|
|
set({ tabs: [...tabs, tab], activeTabId: tab.id });
|
|
},
|
|
|
|
updateFormTabParams: (tabId, params) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId && t.tabType === "objectForm" && t.form
|
|
? { ...t, form: { ...t.form, params } }
|
|
: t,
|
|
),
|
|
})),
|
|
|
|
setDefaultPageSize: (size) => set({ defaultPageSize: size }),
|
|
|
|
closeTab: (tabId) => {
|
|
const { tabs, activeTabId } = get();
|
|
const remaining = tabs.filter((t) => t.id !== tabId);
|
|
const newActiveId =
|
|
activeTabId === tabId
|
|
? remaining.length > 0
|
|
? remaining[remaining.length - 1].id
|
|
: null
|
|
: activeTabId;
|
|
set({ tabs: remaining, activeTabId: newActiveId });
|
|
},
|
|
|
|
// Move a tab to a new index (drag-to-reorder). The active tab is tracked by
|
|
// id, so it follows the moved tab automatically.
|
|
reorderTab: (fromIndex, toIndex) => {
|
|
const { tabs } = get();
|
|
if (
|
|
fromIndex < 0 ||
|
|
fromIndex >= tabs.length ||
|
|
toIndex < 0 ||
|
|
toIndex >= tabs.length ||
|
|
fromIndex === toIndex
|
|
) {
|
|
return;
|
|
}
|
|
const next = [...tabs];
|
|
const [moved] = next.splice(fromIndex, 1);
|
|
next.splice(toIndex, 0, moved);
|
|
set({ tabs: next });
|
|
},
|
|
|
|
closeTabsForTable: (schema, table) => {
|
|
const { tabs, activeTabId } = get();
|
|
const remaining = tabs.filter(
|
|
(t) => !(t.tabType === "table" && t.schema === schema && t.table === table),
|
|
);
|
|
if (remaining.length === tabs.length) return;
|
|
const newActiveId =
|
|
activeTabId !== null && !remaining.some((t) => t.id === activeTabId)
|
|
? remaining.length > 0
|
|
? remaining[remaining.length - 1].id
|
|
: null
|
|
: activeTabId;
|
|
set({ tabs: remaining, activeTabId: newActiveId });
|
|
},
|
|
|
|
setActiveTab: (tabId) => set({ activeTabId: tabId }),
|
|
|
|
setPage: (tabId, page) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId
|
|
? { ...t, page, loading: true, error: null }
|
|
: t,
|
|
),
|
|
})),
|
|
|
|
setPageSize: (tabId, pageSize) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId
|
|
? { ...t, pageSize, page: 1, loading: true, error: null }
|
|
: t,
|
|
),
|
|
})),
|
|
|
|
setTabData: (tabId, data) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, data, loading: false, error: null } : t,
|
|
),
|
|
})),
|
|
|
|
setTabLoading: (tabId, loading) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, loading } : t,
|
|
),
|
|
})),
|
|
|
|
setTabError: (tabId, error) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, error, loading: false } : t,
|
|
),
|
|
})),
|
|
|
|
setColumnFilter: (tabId, column, value) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, columnFilter: { column, value } } : t,
|
|
),
|
|
})),
|
|
|
|
clearColumnFilter: (tabId) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, columnFilter: undefined } : t,
|
|
),
|
|
})),
|
|
|
|
setFilterRules: (tabId, rules) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, filterRules: rules, page: 1, loading: true, error: null } : t,
|
|
),
|
|
})),
|
|
|
|
setSortRules: (tabId, rules) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, sortRules: rules, page: 1, loading: true, error: null } : t,
|
|
),
|
|
})),
|
|
|
|
setHiddenColumns: (tabId, columns) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, hiddenColumns: columns } : t,
|
|
),
|
|
})),
|
|
|
|
toggleHiddenColumn: (tabId, column) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) => {
|
|
if (t.id !== tabId) return t;
|
|
const exists = t.hiddenColumns.includes(column);
|
|
return {
|
|
...t,
|
|
hiddenColumns: exists
|
|
? t.hiddenColumns.filter((c) => c !== column)
|
|
: [...t.hiddenColumns, column],
|
|
};
|
|
}),
|
|
})),
|
|
|
|
setSmartSortApplied: (tabId) =>
|
|
set((state) => ({
|
|
tabs: state.tabs.map((t) =>
|
|
t.id === tabId ? { ...t, smartSortApplied: true } : t,
|
|
),
|
|
})),
|
|
|
|
addChange: (input) => {
|
|
const item: QueueItem = {
|
|
id: `ch-${++changeCounter}`,
|
|
type: input.type,
|
|
sql: input.sql ?? "",
|
|
schema: input.schema,
|
|
table: input.table,
|
|
primaryKey: input.primaryKey,
|
|
oldData: input.oldData ?? null,
|
|
newData: input.newData ?? null,
|
|
columns: input.columns,
|
|
rows: input.rows,
|
|
status: "pending",
|
|
description: input.description ?? null,
|
|
createdAt: Date.now(),
|
|
};
|
|
set((state) => ({ changesQueue: [...state.changesQueue, item] }));
|
|
},
|
|
|
|
cancelChange: (changeId) =>
|
|
set((state) => ({
|
|
changesQueue: state.changesQueue.map((c) =>
|
|
c.id === changeId ? { ...c, status: "cancelled" as const } : c,
|
|
),
|
|
})),
|
|
|
|
removeChange: (changeId) =>
|
|
set((state) => ({
|
|
changesQueue: state.changesQueue.filter((c) => c.id !== changeId),
|
|
})),
|
|
|
|
clearChanges: () => set({ changesQueue: [] }),
|
|
|
|
markChangeCommitted: (changeId) =>
|
|
set((state) => ({
|
|
changesQueue: state.changesQueue.map((c) =>
|
|
c.id === changeId ? { ...c, status: "committed" as const } : c,
|
|
),
|
|
})),
|
|
|
|
markChangeFailed: (changeId, error) =>
|
|
set((state) => ({
|
|
changesQueue: state.changesQueue.map((c) =>
|
|
c.id === changeId
|
|
? { ...c, status: "failed" as const, error }
|
|
: c,
|
|
),
|
|
})),
|
|
|
|
toggleChangesPanel: () =>
|
|
set((state) => ({ changesPanelExpanded: !state.changesPanelExpanded })),
|
|
|
|
setCurrentDatabase: (db) => set({ currentDatabase: db }),
|
|
setCurrentSchema: (schema) => set({ currentSchema: schema }),
|
|
setObjectSearchOpen: (open) => set({ objectSearchOpen: open }),
|
|
setSelectedObjectType: (t) => set({ selectedObjectType: t }),
|
|
setRequestedView: (view) => set({ requestedView: view }),
|
|
setFunctions: (functions) => set({ functions }),
|
|
setTriggers: (triggers) => set({ triggers }),
|
|
setSequences: (sequences) => set({ sequences }),
|
|
setEnums: (enums) => set({ enums }),
|
|
setExtensions: (extensions) => set({ extensions }),
|
|
setIndexes: (indexes) => set({ indexes }),
|
|
setConstraints: (constraints) => set({ constraints }),
|
|
setRoles: (roles) => set({ roles }),
|
|
setSchemaTreeLoading: (loading) => set({ schemaTreeLoading: loading }),
|
|
|
|
stageCellEdit: (input) => {
|
|
// Re-staging the same cell replaces the existing pending entry (keeps the
|
|
// original oldData so revert restores the DB value) instead of stacking
|
|
// a second queue item.
|
|
const colName = Object.keys(input.newData)[0];
|
|
const existing = get().changesQueue.find(
|
|
(c) =>
|
|
c.status === "pending" &&
|
|
c.type === "update" &&
|
|
c.schema === input.schema &&
|
|
c.table === input.table &&
|
|
c.primaryKey &&
|
|
JSON.stringify(c.primaryKey) === JSON.stringify(input.primaryKey) &&
|
|
Object.keys(c.newData ?? {})[0] === colName,
|
|
);
|
|
if (existing) {
|
|
set((state) => ({
|
|
changesQueue: state.changesQueue.map((c) =>
|
|
c.id === existing.id
|
|
? {
|
|
...c,
|
|
newData: input.newData,
|
|
description: input.description ?? c.description,
|
|
}
|
|
: c,
|
|
),
|
|
}));
|
|
return;
|
|
}
|
|
get().addChange({
|
|
type: "update",
|
|
schema: input.schema,
|
|
table: input.table,
|
|
primaryKey: input.primaryKey,
|
|
oldData: input.oldData,
|
|
newData: input.newData,
|
|
description: input.description ?? `Edit ${input.table}`,
|
|
});
|
|
},
|
|
|
|
populate: (databases, schemas, tables) =>
|
|
set({ databases, schemas, tables }),
|
|
|
|
// Best-effort re-fetch of the schema tree (databases/schemas/tables) so
|
|
// newly created/dropped objects show up without a manual refresh. A
|
|
// failure must never surface to the user.
|
|
refreshTree: async (connectionId, schema) => {
|
|
try {
|
|
const [dbs, scs, tbls] = await Promise.all([
|
|
getDatabases(connectionId),
|
|
getSchemas(connectionId),
|
|
getTables(connectionId, schema ?? get().currentSchema ?? undefined),
|
|
]);
|
|
get().populate(dbs, scs, tbls);
|
|
} catch {
|
|
// Best-effort refresh; a failure must not surface to the user.
|
|
}
|
|
},
|
|
|
|
reset: () => {
|
|
tabCounter = 0;
|
|
changeCounter = 0;
|
|
set({ ...initialState, tabs: [], changesQueue: [] });
|
|
},
|
|
})); |