refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- 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
This commit is contained in:
@@ -0,0 +1,650 @@
|
||||
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: [] });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user