@hienlh/ppm 0.13.8 → 0.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/ai-settings-section-ysK_Eixc.js +1 -0
- package/dist/web/assets/architecture-PBZL5I3N-By4Nv3Gj.js +1 -0
- package/dist/web/assets/{audio-preview-DQbX8gfL.js → audio-preview-DISP-2AE.js} +1 -1
- package/dist/web/assets/{chat-tab-BJQT9kie.js → chat-tab-lp_mVSG-.js} +8 -8
- package/dist/web/assets/code-editor-BofKrbM8.js +8 -0
- package/dist/web/assets/{conflict-editor-BKwJLX0D.js → conflict-editor-kaAZUFD5.js} +1 -1
- package/dist/web/assets/csv-parser-Dly5nqE1.js +6 -0
- package/dist/web/assets/{csv-preview-D5lmgVEy.js → csv-preview-7TsYBQI6.js} +3 -3
- package/dist/web/assets/data-grid-overlay-editor-BjjuE4-G.js +1 -0
- package/dist/web/assets/data-grid-types-BTQHYBUh.js +1 -0
- package/dist/web/assets/database-DOWH9-Vv.js +1 -0
- package/dist/web/assets/database-viewer-P38Vxzkx.js +1 -0
- package/dist/web/assets/{diff-viewer-SAtaBwNI.js → diff-viewer-DlJfbgNJ.js} +1 -1
- package/dist/web/assets/dist-0kPgRaVx.js +1 -0
- package/dist/web/assets/{esm-nXReYVnB.js → esm-zjerHxpO.js} +1 -1
- package/dist/web/assets/{extension-webview-PiV4bKJ1.js → extension-webview-5s2MUx38.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-BLXEKVf1.js +1 -0
- package/dist/web/assets/glide-data-grid-D5D1N3L7.js +136 -0
- package/dist/web/assets/glide-data-grid-nthEL3fk.css +1 -0
- package/dist/web/assets/{image-preview-CbFFD9BS.js → image-preview-C9osjEPa.js} +1 -1
- package/dist/web/assets/index-COOnLKGB.css +2 -0
- package/dist/web/assets/index-CpcqiQOx.js +27 -0
- package/dist/web/assets/info-3K5VOQVL-CEkPcChg.js +1 -0
- package/dist/web/assets/{input-BMvRUOr7.js → input-ozrR2DAV.js} +1 -1
- package/dist/web/assets/keybindings-store-COxqSoML.js +1 -0
- package/dist/web/assets/{markdown-renderer-CHWA0KAo.js → markdown-renderer-k3EA9XmF.js} +3 -3
- package/dist/web/assets/number-overlay-editor-BoRxunFN.js +9 -0
- package/dist/web/assets/packet-RMMSAZCW-DECxYTOi.js +1 -0
- package/dist/web/assets/{pdf-preview-DQMdjqa2.js → pdf-preview-DvgyxJX7.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-cjpNfVG5.js +1 -0
- package/dist/web/assets/{port-forwarding-tab-9BpNC9_7.js → port-forwarding-tab-CUkU6wac.js} +1 -1
- package/dist/web/assets/{postgres-viewer-Bm5T51n6.js → postgres-viewer-C1w0tqQw.js} +3 -3
- package/dist/web/assets/radar-KQ55EAFF-Dnpi068b.js +1 -0
- package/dist/web/assets/{settings-store-BHBb62gq.js → settings-store-B-OmHo3J.js} +1 -1
- package/dist/web/assets/settings-tab-D0zKyVwg.js +1 -0
- package/dist/web/assets/sql-query-editor-46hLU7MI.js +3 -0
- package/dist/web/assets/sqlite-viewer-DrLi8P6y.js +1 -0
- package/dist/web/assets/terminal-tab-DqA3fEoQ.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-DRyb1eiw.js +1 -0
- package/dist/web/assets/{use-monaco-theme-CP-vyTF8.js → use-monaco-theme-DgzxiZS5.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-CMiurk2b.js → vendor-mermaid-CCmA_6Y0.js} +3 -3
- package/dist/web/assets/{video-preview-BLI_RruT.js → video-preview-BU7tibc4.js} +1 -1
- package/dist/web/assets/x-CG-_0yIW.js +1 -0
- package/dist/web/index.html +13 -14
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
- package/src/web/components/database/data-grid.tsx +18 -2
- package/src/web/components/database/database-viewer.tsx +19 -8
- package/src/web/components/database/export-button.tsx +38 -18
- package/src/web/components/database/glide-column-search.tsx +81 -0
- package/src/web/components/database/glide-context-menu.tsx +95 -0
- package/src/web/components/database/glide-data-grid.tsx +207 -0
- package/src/web/components/database/glide-data-preview-panel.tsx +113 -0
- package/src/web/components/database/glide-grid-pagination.tsx +34 -0
- package/src/web/components/database/glide-grid-theme.ts +82 -0
- package/src/web/components/database/glide-grid-toolbar.tsx +105 -0
- package/src/web/components/database/glide-grid-types.ts +81 -0
- package/src/web/components/database/glide-header-menu.tsx +111 -0
- package/src/web/components/database/glide-save-bar.tsx +33 -0
- package/src/web/components/database/sql-query-editor.tsx +14 -4
- package/src/web/components/database/use-database.ts +10 -2
- package/src/web/components/database/use-glide-cell-content.ts +159 -0
- package/src/web/components/database/use-glide-columns.ts +69 -0
- package/src/web/components/database/use-glide-grid-actions.ts +164 -0
- package/src/web/components/database/use-glide-pending-edits.ts +72 -0
- package/src/web/components/database/use-glide-row-pinning.ts +35 -0
- package/src/web/components/database/use-glide-selection.ts +48 -0
- package/src/web/components/editor/code-editor.tsx +126 -7
- package/src/web/components/layout/editor-panel.tsx +2 -2
- package/src/web/components/sqlite/sqlite-viewer.tsx +21 -12
- package/src/web/components/sqlite/use-sqlite.ts +1 -1
- package/src/web/hooks/use-terminal.ts +1 -1
- package/src/web/index.html +1 -0
- package/test.sql +1 -0
- package/dist/web/assets/ai-settings-section-CHgpQ_OP.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-WMbLpD5Y.js +0 -1
- package/dist/web/assets/code-editor-CeKTvfyz.js +0 -8
- package/dist/web/assets/csv-parser-DO0dz4x_.js +0 -6
- package/dist/web/assets/database-DCT0OjgQ.js +0 -1
- package/dist/web/assets/database-viewer-DixWWvjx.js +0 -5
- package/dist/web/assets/gitGraph-HDMCJU4V-BdPTuzO3.js +0 -1
- package/dist/web/assets/index-C1RBJe0a.css +0 -2
- package/dist/web/assets/index-ZFyltHwi.js +0 -27
- package/dist/web/assets/info-3K5VOQVL-MHX_1JfR.js +0 -1
- package/dist/web/assets/keybindings-store-D0C-Pq2o.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-CreFbf9A.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CnaHXUh8.js +0 -1
- package/dist/web/assets/plus-51UQ45rf.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-UxsdRHvt.js +0 -1
- package/dist/web/assets/settings-tab-BUstSDLR.js +0 -1
- package/dist/web/assets/sql-completion-provider-tCzZfqWs.js +0 -1
- package/dist/web/assets/sql-query-editor-CMQpaOjA.js +0 -3
- package/dist/web/assets/sqlite-viewer-C7rhO4bn.js +0 -1
- package/dist/web/assets/terminal-tab-Xtj6RN0d.js +0 -1
- package/dist/web/assets/trash-2-CJYoLw7Q.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-CBVPi4NV.js +0 -1
- package/dist/web/assets/x-BtqbfkR7.js +0 -1
- /package/dist/web/assets/{arrow-up-Dtrfv490.js → arrow-up-Rcw6_KKu.js} +0 -0
- /package/dist/web/assets/{chevron-right-BzAdxJRG.js → chevron-right-DnHIvvcy.js} +0 -0
- /package/dist/web/assets/{code-CuravVys.js → code-DGBecc50.js} +0 -0
- /package/dist/web/assets/{dist-D7KGU7Vl.js → dist-CaKCIxem.js} +0 -0
- /package/dist/web/assets/{dist-CGvx1c8C.js → dist-DGSkE2Ml.js} +0 -0
- /package/dist/web/assets/{katex-BFE6i_OH.js → katex-BuytEdO1.js} +0 -0
- /package/dist/web/assets/{lib-D_kRA9p6.js → lib-DQHnkzGy.js} +0 -0
- /package/dist/web/assets/{refresh-cw-CSFrDtiu.js → refresh-cw-LlbZDJpO.js} +0 -0
- /package/dist/web/assets/{scroll-area-BEllam7_.js → scroll-area-7H-Q_k8c.js} +0 -0
- /package/dist/web/assets/{sparkles-B0mRBy_j.js → sparkles-fWUT5Vzq.js} +0 -0
- /package/dist/web/assets/{table-Dq575bPF.js → table-tf7pRkME.js} +0 -0
- /package/dist/web/assets/{text-wrap-Cn6BNQfq.js → text-wrap-BV-R4Vvy.js} +0 -0
- /package/dist/web/assets/{use-blob-url-Hn6n1730.js → use-blob-url-e9uTXjv5.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-u3AZMvTx.js → vendor-xterm-CU2c3f0A.js} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { Filter, Pin, PinOff, ArrowUp, ArrowDown, X } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface HeaderMenuProps {
|
|
6
|
+
colName: string;
|
|
7
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
8
|
+
isPinned: boolean;
|
|
9
|
+
filterValue: string;
|
|
10
|
+
sortState: "asc" | "desc" | null;
|
|
11
|
+
onFilter: (value: string) => void;
|
|
12
|
+
onSort: () => void;
|
|
13
|
+
onClearSort?: () => void;
|
|
14
|
+
onTogglePin: () => void;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Header column dropdown menu — filter input, sort toggle, pin/unpin.
|
|
20
|
+
* Rendered via React portal into #portal div (required by Glide Data Grid).
|
|
21
|
+
*/
|
|
22
|
+
export function GlideHeaderMenu({
|
|
23
|
+
colName, bounds, isPinned, filterValue, sortState,
|
|
24
|
+
onFilter, onSort, onClearSort, onTogglePin, onClose,
|
|
25
|
+
}: HeaderMenuProps) {
|
|
26
|
+
const [localFilter, setLocalFilter] = useState(filterValue);
|
|
27
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
28
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
29
|
+
|
|
30
|
+
// Close on outside click
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const handler = (e: MouseEvent) => {
|
|
33
|
+
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
|
34
|
+
};
|
|
35
|
+
document.addEventListener("mousedown", handler);
|
|
36
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
37
|
+
}, [onClose]);
|
|
38
|
+
|
|
39
|
+
// Close on Escape
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
|
42
|
+
document.addEventListener("keydown", handler);
|
|
43
|
+
return () => document.removeEventListener("keydown", handler);
|
|
44
|
+
}, [onClose]);
|
|
45
|
+
|
|
46
|
+
// Debounced filter
|
|
47
|
+
const handleFilterChange = (val: string) => {
|
|
48
|
+
setLocalFilter(val);
|
|
49
|
+
clearTimeout(debounceRef.current);
|
|
50
|
+
debounceRef.current = setTimeout(() => onFilter(val), 300);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Clamp position to viewport
|
|
54
|
+
const menuWidth = 220;
|
|
55
|
+
const left = Math.min(bounds.x, window.innerWidth - menuWidth - 8);
|
|
56
|
+
const top = bounds.y + bounds.height + 2;
|
|
57
|
+
|
|
58
|
+
const portal = document.getElementById("portal");
|
|
59
|
+
if (!portal) return null;
|
|
60
|
+
|
|
61
|
+
return createPortal(
|
|
62
|
+
<div ref={ref} style={{ position: "fixed", left, top, zIndex: 10000 }}
|
|
63
|
+
className="w-[220px] bg-popover border border-border rounded-md shadow-lg text-xs overflow-hidden">
|
|
64
|
+
{/* Column name header */}
|
|
65
|
+
<div className="px-3 py-1.5 border-b border-border text-muted-foreground font-medium truncate">
|
|
66
|
+
{colName}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Filter input */}
|
|
70
|
+
<div className="px-2 py-1.5 border-b border-border">
|
|
71
|
+
<div className="flex items-center gap-1">
|
|
72
|
+
<Filter className="size-3 text-muted-foreground shrink-0" />
|
|
73
|
+
<input
|
|
74
|
+
autoFocus type="text" value={localFilter}
|
|
75
|
+
onChange={(e) => handleFilterChange(e.target.value)}
|
|
76
|
+
placeholder="Filter (ILIKE)…"
|
|
77
|
+
className="flex-1 bg-transparent outline-none text-foreground placeholder:text-muted-foreground text-xs"
|
|
78
|
+
/>
|
|
79
|
+
{localFilter && (
|
|
80
|
+
<button type="button" onClick={() => handleFilterChange("")}
|
|
81
|
+
className="text-muted-foreground hover:text-foreground">
|
|
82
|
+
<X className="size-3" />
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Actions */}
|
|
89
|
+
<div className="py-1">
|
|
90
|
+
<button type="button" onClick={() => { onSort(); onClose(); }}
|
|
91
|
+
className="w-full text-left px-3 py-1.5 hover:bg-muted flex items-center gap-2 text-foreground">
|
|
92
|
+
{sortState === "asc" ? <ArrowDown className="size-3" /> : <ArrowUp className="size-3" />}
|
|
93
|
+
{sortState === "asc" ? "Sort Descending" : "Sort Ascending"}
|
|
94
|
+
</button>
|
|
95
|
+
{sortState && onClearSort && (
|
|
96
|
+
<button type="button" onClick={() => { onClearSort(); onClose(); }}
|
|
97
|
+
className="w-full text-left px-3 py-1.5 hover:bg-muted flex items-center gap-2 text-foreground">
|
|
98
|
+
<X className="size-3" />
|
|
99
|
+
Clear Sort
|
|
100
|
+
</button>
|
|
101
|
+
)}
|
|
102
|
+
<button type="button" onClick={() => { onTogglePin(); onClose(); }}
|
|
103
|
+
className="w-full text-left px-3 py-1.5 hover:bg-muted flex items-center gap-2 text-foreground">
|
|
104
|
+
{isPinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
105
|
+
{isPinned ? "Unpin Column" : "Pin Column"}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>,
|
|
109
|
+
portal,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Save, Undo2 } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
interface SaveBarProps {
|
|
4
|
+
pendingCount: number;
|
|
5
|
+
onSave: () => void;
|
|
6
|
+
onDiscard: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Floating save bar shown when there are pending cell edits.
|
|
11
|
+
* Save with click or Mod+Enter. Discard with Escape.
|
|
12
|
+
*/
|
|
13
|
+
export function GlideSaveBar({ pendingCount, onSave, onDiscard }: SaveBarProps) {
|
|
14
|
+
if (pendingCount === 0) return null;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex items-center gap-2 px-3 py-1.5 border-t border-amber-400/50 bg-amber-50 dark:bg-amber-950/30 shrink-0 text-xs">
|
|
18
|
+
<span className="text-amber-700 dark:text-amber-300 font-medium">
|
|
19
|
+
{pendingCount} pending edit{pendingCount > 1 ? "s" : ""}
|
|
20
|
+
</span>
|
|
21
|
+
<div className="flex-1" />
|
|
22
|
+
<button type="button" onClick={onDiscard}
|
|
23
|
+
className="flex items-center gap-1 px-2 py-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
24
|
+
<Undo2 className="size-3" /> Discard
|
|
25
|
+
</button>
|
|
26
|
+
<button type="button" onClick={onSave}
|
|
27
|
+
className="flex items-center gap-1 px-2 py-0.5 rounded bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
|
|
28
|
+
<Save className="size-3" /> Save
|
|
29
|
+
<kbd className="ml-1 text-[9px] opacity-70">⌘↵</kbd>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -9,6 +9,8 @@ interface SqlQueryEditorProps {
|
|
|
9
9
|
loading: boolean;
|
|
10
10
|
defaultValue?: string;
|
|
11
11
|
schemaInfo?: SchemaInfo;
|
|
12
|
+
/** Unique key for caching query text in sessionStorage (e.g. connectionId) */
|
|
13
|
+
cacheKey?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/** Find the SQL statement surrounding the cursor line (split by ;) */
|
|
@@ -41,8 +43,13 @@ export function getStatementAtCursor(text: string, cursorLine: number): string {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/** Shared Monaco-based SQL query editor (editor only, no results) */
|
|
44
|
-
export function SqlQueryEditor({ onExecute, loading, defaultValue = "SELECT * FROM ", schemaInfo }: SqlQueryEditorProps) {
|
|
45
|
-
const
|
|
46
|
+
export function SqlQueryEditor({ onExecute, loading, defaultValue = "SELECT * FROM ", schemaInfo, cacheKey }: SqlQueryEditorProps) {
|
|
47
|
+
const storageKey = cacheKey ? `ppm:sql-query:${cacheKey}` : null;
|
|
48
|
+
const [query, setQuery] = useState(() => {
|
|
49
|
+
if (storageKey) { try { return sessionStorage.getItem(storageKey) ?? defaultValue; } catch { /* */ } }
|
|
50
|
+
return defaultValue;
|
|
51
|
+
});
|
|
52
|
+
const userEditedRef = useRef(false);
|
|
46
53
|
const editorRef = useRef<MonacoType.editor.IStandaloneCodeEditor | null>(null);
|
|
47
54
|
const monacoRef = useRef<typeof MonacoType | null>(null);
|
|
48
55
|
const disposableRef = useRef<MonacoType.IDisposable | null>(null);
|
|
@@ -88,7 +95,10 @@ export function SqlQueryEditor({ onExecute, loading, defaultValue = "SELECT * FR
|
|
|
88
95
|
}
|
|
89
96
|
}, [schemaInfo]);
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
// Sync from defaultValue only if user hasn't manually edited
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!userEditedRef.current) setQuery(defaultValue);
|
|
101
|
+
}, [defaultValue]);
|
|
92
102
|
|
|
93
103
|
return (
|
|
94
104
|
<div className="h-full overflow-hidden">
|
|
@@ -97,7 +107,7 @@ export function SqlQueryEditor({ onExecute, loading, defaultValue = "SELECT * FR
|
|
|
97
107
|
language="sql"
|
|
98
108
|
theme={monacoTheme}
|
|
99
109
|
value={query}
|
|
100
|
-
onChange={(v) =>
|
|
110
|
+
onChange={(v) => { const val = v ?? ""; setQuery(val); userEditedRef.current = true; if (storageKey) try { sessionStorage.setItem(storageKey, val); } catch {} }}
|
|
101
111
|
onMount={handleMount}
|
|
102
112
|
options={{
|
|
103
113
|
minimap: { enabled: false },
|
|
@@ -2,7 +2,7 @@ import { useState, useCallback } from "react";
|
|
|
2
2
|
import { api } from "@/lib/api-client";
|
|
3
3
|
|
|
4
4
|
export interface DbTableInfo { name: string; schema: string; rowCount: number }
|
|
5
|
-
export interface DbColumnInfo { name: string; type: string; nullable: boolean; pk: boolean; defaultValue: string | null }
|
|
5
|
+
export interface DbColumnInfo { name: string; type: string; nullable: boolean; pk: boolean; defaultValue: string | null; fk?: { table: string; column: string } | null }
|
|
6
6
|
export interface DbQueryResult { columns: string[]; rows: Record<string, unknown>[]; rowsAffected: number; changeType: "select" | "modify"; executionTimeMs?: number }
|
|
7
7
|
interface DbTableData { columns: string[]; rows: Record<string, unknown>[]; total: number; page: number; limit: number }
|
|
8
8
|
|
|
@@ -146,6 +146,14 @@ export function useDatabase(connectionId: number) {
|
|
|
146
146
|
fetchTableData(undefined, undefined, 1, newCol, newDir);
|
|
147
147
|
}, [orderBy, orderDir, fetchTableData]);
|
|
148
148
|
|
|
149
|
+
/** Clear sort entirely */
|
|
150
|
+
const clearSort = useCallback(() => {
|
|
151
|
+
setOrderBy(null);
|
|
152
|
+
setOrderDir("ASC");
|
|
153
|
+
setPageState(1);
|
|
154
|
+
fetchTableData(undefined, undefined, 1, null, "ASC");
|
|
155
|
+
}, [fetchTableData]);
|
|
156
|
+
|
|
149
157
|
/** Bulk delete rows */
|
|
150
158
|
const bulkDelete = useCallback(async (pkColumn: string, pkValues: unknown[]) => {
|
|
151
159
|
if (!selectedTable) return;
|
|
@@ -191,7 +199,7 @@ export function useDatabase(connectionId: number) {
|
|
|
191
199
|
return {
|
|
192
200
|
selectedTable, selectedSchema, selectTable, tableData, schema,
|
|
193
201
|
loading, error, page, setPage: changePage,
|
|
194
|
-
orderBy, orderDir, toggleSort,
|
|
202
|
+
orderBy, orderDir, toggleSort, clearSort,
|
|
195
203
|
queryResult, queryError, queryLoading, executeQuery,
|
|
196
204
|
updateCell, deleteRow, bulkDelete, insertRow,
|
|
197
205
|
refreshData: fetchTableData, queryAsTable,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { GridCellKind, type GridCell, type EditableGridCell, type Item } from "@glideapps/glide-data-grid";
|
|
3
|
+
import type { GridColumnSchema } from "./glide-grid-types";
|
|
4
|
+
import { formatCellValue } from "./glide-grid-types";
|
|
5
|
+
import type { PendingEdit } from "./use-glide-pending-edits";
|
|
6
|
+
|
|
7
|
+
/** Map DB type string to Glide cell kind */
|
|
8
|
+
function dbTypeToKind(type: string): GridCellKind {
|
|
9
|
+
const t = type.toLowerCase();
|
|
10
|
+
if (/^(int|serial|bigint|smallint|float|double|decimal|numeric|real|money)/.test(t)) {
|
|
11
|
+
return GridCellKind.Number;
|
|
12
|
+
}
|
|
13
|
+
if (/^bool/.test(t)) return GridCellKind.Boolean;
|
|
14
|
+
return GridCellKind.Text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Check if a PK column is auto-increment (serial, identity, nextval default) */
|
|
18
|
+
function isAutoIncrement(col: GridColumnSchema): boolean {
|
|
19
|
+
const t = col.type.toLowerCase();
|
|
20
|
+
if (/^(serial|bigserial|smallserial)/.test(t)) return true;
|
|
21
|
+
if (/^(int|bigint|smallint|integer)/.test(t) && col.defaultValue && /nextval|identity|auto_increment/i.test(col.defaultValue)) return true;
|
|
22
|
+
// SQLite: INTEGER PRIMARY KEY is auto-increment by default
|
|
23
|
+
if (t === "integer" && col.pk && !col.defaultValue) return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Truncate display string for canvas rendering performance */
|
|
28
|
+
function truncateDisplay(val: string, max = 200): string {
|
|
29
|
+
return val.length > max ? val.slice(0, max) + "…" : val;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Amber background for cells with pending unsaved edits */
|
|
33
|
+
const PENDING_THEME = { bgCell: "rgba(251, 191, 36, 0.15)" };
|
|
34
|
+
|
|
35
|
+
interface UseGlideCellContentResult {
|
|
36
|
+
getCellContent: (cell: Item) => GridCell;
|
|
37
|
+
onCellEdited: (cell: Item, newValue: EditableGridCell) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Provides getCellContent and onCellEdited callbacks for Glide Data Grid.
|
|
42
|
+
* Uses refs for rows/columnOrder to avoid stale closures in canvas render loop.
|
|
43
|
+
* Integrates with pending edits — shows pending values with amber highlight.
|
|
44
|
+
*/
|
|
45
|
+
export function useGlideCellContent(
|
|
46
|
+
rows: Record<string, unknown>[],
|
|
47
|
+
columnOrder: string[],
|
|
48
|
+
schema: GridColumnSchema[],
|
|
49
|
+
pkCol: string | null,
|
|
50
|
+
addPendingEdit: (pkVal: unknown, col: string, newVal: unknown) => void,
|
|
51
|
+
pendingRef: React.RefObject<Map<string, PendingEdit>>,
|
|
52
|
+
): UseGlideCellContentResult {
|
|
53
|
+
const rowsRef = useRef(rows);
|
|
54
|
+
rowsRef.current = rows;
|
|
55
|
+
const colOrderRef = useRef(columnOrder);
|
|
56
|
+
colOrderRef.current = columnOrder;
|
|
57
|
+
|
|
58
|
+
const schemaMap = useRef(new Map<string, GridColumnSchema>());
|
|
59
|
+
schemaMap.current = new Map(schema.map((s) => [s.name, s]));
|
|
60
|
+
|
|
61
|
+
const getCellContent = useCallback(([colIdx, rowIdx]: Item): GridCell => {
|
|
62
|
+
const colName = colOrderRef.current[colIdx];
|
|
63
|
+
const row = rowsRef.current[rowIdx];
|
|
64
|
+
if (!colName || !row) {
|
|
65
|
+
return { kind: GridCellKind.Text, data: "", displayData: "", allowOverlay: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const colSchema = schemaMap.current.get(colName);
|
|
69
|
+
const kind = colSchema ? dbTypeToKind(colSchema.type) : GridCellKind.Text;
|
|
70
|
+
const isPk = colSchema?.pk ?? false;
|
|
71
|
+
|
|
72
|
+
// Check for pending edit value
|
|
73
|
+
const pkVal = pkCol ? row[pkCol] : undefined;
|
|
74
|
+
const isNewRow = typeof pkVal === "string" && pkVal.startsWith("__new_");
|
|
75
|
+
const pendingKey = `${pkVal}:${colName}`;
|
|
76
|
+
const pending = pendingRef.current.get(pendingKey);
|
|
77
|
+
const val = pending !== undefined ? pending.newVal : row[colName];
|
|
78
|
+
const hasPending = pending !== undefined;
|
|
79
|
+
|
|
80
|
+
// New row PK column — auto-increment PKs show "NEW" (readonly), text PKs are editable
|
|
81
|
+
if (isPk && isNewRow) {
|
|
82
|
+
const isAuto = colSchema ? isAutoIncrement(colSchema) : false;
|
|
83
|
+
if (isAuto) {
|
|
84
|
+
return { kind: GridCellKind.Text, data: "", displayData: "AUTO", allowOverlay: false, readonly: true,
|
|
85
|
+
themeOverride: { textDark: "#6b7280" } };
|
|
86
|
+
}
|
|
87
|
+
// Editable PK (text, uuid, etc.) — show pending value or placeholder
|
|
88
|
+
const editedVal = pending !== undefined ? String(pending.newVal ?? "") : "";
|
|
89
|
+
return { kind: GridCellKind.Text, data: editedVal, displayData: editedVal || "Enter ID…",
|
|
90
|
+
allowOverlay: true, readonly: false,
|
|
91
|
+
themeOverride: editedVal ? (hasPending ? PENDING_THEME : undefined) : { textDark: "#9ca3af" } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// NULL values — for new rows show default/type hints
|
|
95
|
+
if (val == null) {
|
|
96
|
+
let placeholder = isNewRow ? "" : "NULL";
|
|
97
|
+
if (isNewRow && !hasPending && colSchema) {
|
|
98
|
+
const t = colSchema.type.toLowerCase();
|
|
99
|
+
if (colSchema.defaultValue) {
|
|
100
|
+
placeholder = colSchema.defaultValue;
|
|
101
|
+
} else if (/^(timestamp|datetime|date)/.test(t)) {
|
|
102
|
+
placeholder = "NOW()";
|
|
103
|
+
} else if (/^(uuid)/.test(t)) {
|
|
104
|
+
placeholder = "gen_random_uuid()";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
kind: GridCellKind.Text, data: "", displayData: placeholder,
|
|
109
|
+
allowOverlay: !isPk, readonly: isPk,
|
|
110
|
+
themeOverride: hasPending ? PENDING_THEME : (isNewRow || placeholder !== "NULL" ? { textDark: "#9ca3af" } : { textDark: "#6b7280" }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Number cells
|
|
115
|
+
if (kind === GridCellKind.Number && typeof val === "number") {
|
|
116
|
+
return {
|
|
117
|
+
kind: GridCellKind.Number, data: val, displayData: String(val),
|
|
118
|
+
allowOverlay: !isPk, readonly: isPk,
|
|
119
|
+
themeOverride: hasPending ? PENDING_THEME : undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Boolean cells
|
|
124
|
+
if (kind === GridCellKind.Boolean && typeof val === "boolean") {
|
|
125
|
+
return { kind: GridCellKind.Boolean, data: val, readonly: isPk, allowOverlay: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Text cell
|
|
129
|
+
const strVal = formatCellValue(val);
|
|
130
|
+
return {
|
|
131
|
+
kind: GridCellKind.Text, data: strVal, displayData: truncateDisplay(strVal),
|
|
132
|
+
allowOverlay: !isPk, readonly: isPk,
|
|
133
|
+
themeOverride: hasPending ? PENDING_THEME : undefined,
|
|
134
|
+
};
|
|
135
|
+
}, []); // stable — reads from refs
|
|
136
|
+
|
|
137
|
+
const onCellEdited = useCallback(([colIdx, rowIdx]: Item, newValue: EditableGridCell) => {
|
|
138
|
+
if (!pkCol) return;
|
|
139
|
+
const colName = colOrderRef.current[colIdx];
|
|
140
|
+
const row = rowsRef.current[rowIdx];
|
|
141
|
+
if (!colName || !row) return;
|
|
142
|
+
|
|
143
|
+
const pkVal = row[pkCol];
|
|
144
|
+
let parsed: unknown;
|
|
145
|
+
if (newValue.kind === GridCellKind.Text) {
|
|
146
|
+
parsed = newValue.data === "" ? null : newValue.data;
|
|
147
|
+
} else if (newValue.kind === GridCellKind.Number) {
|
|
148
|
+
parsed = newValue.data;
|
|
149
|
+
} else if (newValue.kind === GridCellKind.Boolean) {
|
|
150
|
+
parsed = newValue.data;
|
|
151
|
+
} else {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
addPendingEdit(pkVal, colName, parsed);
|
|
156
|
+
}, [pkCol, addPendingEdit]);
|
|
157
|
+
|
|
158
|
+
return { getCellContent, onCellEdited };
|
|
159
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { GridColumn } from "@glideapps/glide-data-grid";
|
|
3
|
+
import type { GridColumnSchema } from "./glide-grid-types";
|
|
4
|
+
|
|
5
|
+
interface UseGlideColumnsResult {
|
|
6
|
+
/** Ordered GridColumn definitions (pinned first) */
|
|
7
|
+
columns: GridColumn[];
|
|
8
|
+
/** Number of frozen columns from left */
|
|
9
|
+
freezeColumns: number;
|
|
10
|
+
/** Column name order matching GridColumn indices */
|
|
11
|
+
columnOrder: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Estimate column width from header name and sample row values */
|
|
15
|
+
function estimateColWidth(name: string, rows: Record<string, unknown>[], type: string): number {
|
|
16
|
+
const headerW = name.length * 9 + 40; // header text + sort icon + menu icon padding
|
|
17
|
+
let maxContentW = 0;
|
|
18
|
+
const sampleCount = Math.min(rows.length, 20);
|
|
19
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
20
|
+
const val = rows[i]?.[name];
|
|
21
|
+
if (val == null) continue;
|
|
22
|
+
const len = typeof val === "object" ? 12 : String(val).length;
|
|
23
|
+
maxContentW = Math.max(maxContentW, len * 8);
|
|
24
|
+
}
|
|
25
|
+
const isNumeric = /^(int|serial|bigint|smallint|float|double|decimal|numeric|real|money|bool)/.test(type.toLowerCase());
|
|
26
|
+
const minW = isNumeric ? 80 : 100;
|
|
27
|
+
return Math.max(minW, Math.min(Math.max(headerW, maxContentW) + 16, 400));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build Glide Data Grid column definitions from schema.
|
|
32
|
+
* Reorders columns: pinned first, then unpinned. Auto-sizes widths.
|
|
33
|
+
*/
|
|
34
|
+
export function useGlideColumns(
|
|
35
|
+
schema: GridColumnSchema[],
|
|
36
|
+
columnNames: string[],
|
|
37
|
+
pinnedCols: Set<string>,
|
|
38
|
+
colWidths: Map<string, number>,
|
|
39
|
+
rows: Record<string, unknown>[],
|
|
40
|
+
orderBy?: string | null,
|
|
41
|
+
orderDir?: "ASC" | "DESC",
|
|
42
|
+
): UseGlideColumnsResult {
|
|
43
|
+
return useMemo(() => {
|
|
44
|
+
const pinned = columnNames.filter((c) => pinnedCols.has(c));
|
|
45
|
+
const unpinned = columnNames.filter((c) => !pinnedCols.has(c));
|
|
46
|
+
const ordered = [...pinned, ...unpinned];
|
|
47
|
+
|
|
48
|
+
const schemaMap = new Map(schema.map((s) => [s.name, s]));
|
|
49
|
+
|
|
50
|
+
const columns: GridColumn[] = ordered.map((name) => {
|
|
51
|
+
const col = schemaMap.get(name);
|
|
52
|
+
const isPk = col?.pk ?? false;
|
|
53
|
+
|
|
54
|
+
let icon: string | undefined;
|
|
55
|
+
if (orderBy === name) {
|
|
56
|
+
icon = orderDir === "ASC" ? "sortAsc" : "sortDesc";
|
|
57
|
+
} else if (isPk) {
|
|
58
|
+
icon = "headerRowID";
|
|
59
|
+
} else if (col?.fk) {
|
|
60
|
+
icon = "headerFk";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const width = colWidths.get(name) ?? estimateColWidth(name, rows, col?.type ?? "text");
|
|
64
|
+
return { title: name, id: name, width, hasMenu: true, icon };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { columns, freezeColumns: pinned.length, columnOrder: ordered };
|
|
68
|
+
}, [schema, columnNames, pinnedCols, colWidths, rows, orderBy, orderDir]);
|
|
69
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import { useShallow } from "zustand/react/shallow";
|
|
3
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
4
|
+
import type { Item, GridSelection } from "@glideapps/glide-data-grid";
|
|
5
|
+
import type { GridColumnSchema } from "./glide-grid-types";
|
|
6
|
+
import { formatCellValue, detectLang, needsViewer } from "./glide-grid-types";
|
|
7
|
+
import type { PreviewData } from "./glide-data-preview-panel";
|
|
8
|
+
|
|
9
|
+
interface UseGlideGridActionsParams {
|
|
10
|
+
displayRows: Record<string, unknown>[];
|
|
11
|
+
columnOrder: string[];
|
|
12
|
+
schema: GridColumnSchema[];
|
|
13
|
+
pkCol: string | null;
|
|
14
|
+
connectionId?: number;
|
|
15
|
+
connectionName?: string;
|
|
16
|
+
selectedTable?: string | null;
|
|
17
|
+
selectedSchema?: string;
|
|
18
|
+
addEdit: (pkVal: unknown, col: string, newVal: unknown) => void;
|
|
19
|
+
/** Current grid selection — needed for document-level paste */
|
|
20
|
+
gridSelection?: GridSelection;
|
|
21
|
+
/** Container ref — paste only fires when focus is inside */
|
|
22
|
+
containerRef?: React.RefObject<HTMLElement | null>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts preview panel, paste handler, and FK navigation logic
|
|
27
|
+
* from the main GlideDataGrid component to keep it under 200 lines.
|
|
28
|
+
*/
|
|
29
|
+
export function useGlideGridActions(params: UseGlideGridActionsParams) {
|
|
30
|
+
const { displayRows, columnOrder, schema, pkCol, connectionId, connectionName, selectedTable, selectedSchema, addEdit, gridSelection, containerRef } = params;
|
|
31
|
+
const { openTab } = useTabStore(useShallow((s) => ({ openTab: s.openTab })));
|
|
32
|
+
const [previewData, setPreviewData] = useState<PreviewData | null>(null);
|
|
33
|
+
|
|
34
|
+
// Refs to avoid stale closures in canvas callbacks
|
|
35
|
+
const displayRowsRef = useRef(displayRows);
|
|
36
|
+
displayRowsRef.current = displayRows;
|
|
37
|
+
const columnOrderRef = useRef(columnOrder);
|
|
38
|
+
columnOrderRef.current = columnOrder;
|
|
39
|
+
|
|
40
|
+
// Preview panel — inline Monaco viewer for cell/row content
|
|
41
|
+
const openRowPreview = useCallback((rowIdx: number) => {
|
|
42
|
+
const row = displayRows[rowIdx]; if (!row) return;
|
|
43
|
+
const pk = pkCol ? String(row[pkCol] ?? "") : "";
|
|
44
|
+
const table = selectedTable ?? "";
|
|
45
|
+
const content = JSON.stringify(row, null, 2);
|
|
46
|
+
setPreviewData({ title: pk ? `Row #${pk}${table ? ` — ${table}` : ""}` : `Row — ${table}`,
|
|
47
|
+
content, language: "json", viewerKey: `${connectionId}:${table}:row:${pk}` });
|
|
48
|
+
}, [displayRows, pkCol, selectedTable, connectionId]);
|
|
49
|
+
|
|
50
|
+
const openCellPreview = useCallback((rowIdx: number, colIdx: number) => {
|
|
51
|
+
const row = displayRows[rowIdx]; if (!row) return;
|
|
52
|
+
const colName = columnOrder[colIdx]; if (!colName) return;
|
|
53
|
+
const val = formatCellValue(row[colName]);
|
|
54
|
+
const pk = pkCol ? String(row[pkCol] ?? rowIdx) : String(rowIdx);
|
|
55
|
+
const table = selectedTable ?? "";
|
|
56
|
+
setPreviewData({ title: `${colName} #${pk}${table ? ` — ${table}` : ""}`,
|
|
57
|
+
content: val, language: detectLang(val), viewerKey: `${connectionId}:${table}:${colName}:${pk}` });
|
|
58
|
+
}, [displayRows, columnOrder, pkCol, selectedTable, connectionId]);
|
|
59
|
+
|
|
60
|
+
const openPreviewInTab = useCallback(() => {
|
|
61
|
+
if (!previewData) return;
|
|
62
|
+
openTab({ type: "editor", title: previewData.title, projectId: null, closable: true,
|
|
63
|
+
metadata: { inlineContent: previewData.content, inlineLanguage: previewData.language, viewerKey: previewData.viewerKey } });
|
|
64
|
+
}, [openTab, previewData]);
|
|
65
|
+
|
|
66
|
+
// Custom paste handler — routes pasted TSV cells through addEdit
|
|
67
|
+
const handlePaste = useCallback((target: Item, values: readonly (readonly string[])[]) => {
|
|
68
|
+
if (!pkCol) return false;
|
|
69
|
+
const [startCol, startRow] = target;
|
|
70
|
+
for (let r = 0; r < values.length; r++) {
|
|
71
|
+
const row = displayRowsRef.current[startRow + r];
|
|
72
|
+
if (!row) continue;
|
|
73
|
+
const pk = row[pkCol];
|
|
74
|
+
for (let c = 0; c < values[r]!.length; c++) {
|
|
75
|
+
const colName = columnOrderRef.current[startCol + c];
|
|
76
|
+
if (!colName) continue;
|
|
77
|
+
const colDef = schema.find((s) => s.name === colName);
|
|
78
|
+
if (colDef?.pk) continue;
|
|
79
|
+
const raw = values[r]![c]!;
|
|
80
|
+
addEdit(pk, colName, raw === "" ? null : raw);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false; // we handled it
|
|
84
|
+
}, [pkCol, schema, addEdit]);
|
|
85
|
+
|
|
86
|
+
// Document-level paste listener — works even when Glide canvas doesn't have focus
|
|
87
|
+
const schemaRef = useRef(schema);
|
|
88
|
+
schemaRef.current = schema;
|
|
89
|
+
const gridSelRef = useRef(gridSelection);
|
|
90
|
+
gridSelRef.current = gridSelection;
|
|
91
|
+
const addEditRef = useRef(addEdit);
|
|
92
|
+
addEditRef.current = addEdit;
|
|
93
|
+
const pkColRef = useRef(pkCol);
|
|
94
|
+
pkColRef.current = pkCol;
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!containerRef) return;
|
|
98
|
+
const handler = (e: ClipboardEvent) => {
|
|
99
|
+
const container = containerRef.current;
|
|
100
|
+
if (!container || !container.contains(document.activeElement)) return;
|
|
101
|
+
// Skip if an input/textarea is focused (e.g. search bar)
|
|
102
|
+
const tag = (document.activeElement as HTMLElement)?.tagName;
|
|
103
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
104
|
+
const pk = pkColRef.current;
|
|
105
|
+
if (!pk) return;
|
|
106
|
+
const sel = gridSelRef.current?.current;
|
|
107
|
+
if (!sel) return; // need a selected cell as paste anchor
|
|
108
|
+
const text = e.clipboardData?.getData("text/plain");
|
|
109
|
+
if (!text) return;
|
|
110
|
+
const tsvRows = text.split(/\r?\n/).filter((r) => r.length > 0).map((r) => r.split("\t"));
|
|
111
|
+
if (tsvRows.length === 0) return;
|
|
112
|
+
const [startCol, startRow] = sel.cell;
|
|
113
|
+
for (let r = 0; r < tsvRows.length; r++) {
|
|
114
|
+
const row = displayRowsRef.current[startRow + r];
|
|
115
|
+
if (!row) continue;
|
|
116
|
+
const rowPk = row[pk];
|
|
117
|
+
for (let c = 0; c < tsvRows[r]!.length; c++) {
|
|
118
|
+
const colName = columnOrderRef.current[startCol + c];
|
|
119
|
+
if (!colName) continue;
|
|
120
|
+
const colDef = schemaRef.current.find((s) => s.name === colName);
|
|
121
|
+
if (colDef?.pk) continue;
|
|
122
|
+
const raw = tsvRows[r]![c]!;
|
|
123
|
+
addEditRef.current(rowPk, colName, raw === "" ? null : raw);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
};
|
|
128
|
+
document.addEventListener("paste", handler);
|
|
129
|
+
return () => document.removeEventListener("paste", handler);
|
|
130
|
+
}, [containerRef]);
|
|
131
|
+
|
|
132
|
+
// FK detection helpers for context menu
|
|
133
|
+
const getContextFk = useCallback((colName: string | null) => {
|
|
134
|
+
if (!colName) return null;
|
|
135
|
+
return schema.find((s) => s.name === colName)?.fk ?? null;
|
|
136
|
+
}, [schema]);
|
|
137
|
+
|
|
138
|
+
const isCellViewable = useCallback((row: Record<string, unknown> | null, colName: string | null) => {
|
|
139
|
+
return row && colName ? needsViewer(row[colName]) : false;
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
// FK navigation: open referenced table in new tab filtered by FK value
|
|
143
|
+
const openFkTable = useCallback((fk: { table: string; column: string }, cellValue: unknown) => {
|
|
144
|
+
if (cellValue == null || !connectionId) return;
|
|
145
|
+
openTab({
|
|
146
|
+
type: "database",
|
|
147
|
+
title: `${connectionName ?? "DB"} · ${fk.table}`,
|
|
148
|
+
projectId: null,
|
|
149
|
+
closable: true,
|
|
150
|
+
metadata: {
|
|
151
|
+
connectionId, connectionName,
|
|
152
|
+
tableName: fk.table,
|
|
153
|
+
schemaName: selectedSchema ?? "public",
|
|
154
|
+
initialSql: `SELECT * FROM "${fk.table}" WHERE "${fk.column}" = '${String(cellValue).replace(/'/g, "''")}'`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}, [connectionId, connectionName, selectedSchema, openTab]);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
previewData, setPreviewData,
|
|
161
|
+
openRowPreview, openCellPreview, openPreviewInTab,
|
|
162
|
+
handlePaste, getContextFk, isCellViewable, openFkTable,
|
|
163
|
+
};
|
|
164
|
+
}
|