@hienlh/ppm 0.13.9 → 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/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-C1p-Q5XZ.js → audio-preview-DISP-2AE.js} +1 -1
- package/dist/web/assets/{chat-tab-BSJUkgxB.js → chat-tab-lp_mVSG-.js} +8 -8
- package/dist/web/assets/code-editor-BofKrbM8.js +8 -0
- package/dist/web/assets/{conflict-editor-Dcn3HuLD.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-viewer-P38Vxzkx.js +1 -0
- package/dist/web/assets/{diff-viewer-NMLD4V8q.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-DW2dBswj.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-Dqp1KSus.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-DYWhyaze.js → input-ozrR2DAV.js} +1 -1
- package/dist/web/assets/keybindings-store-COxqSoML.js +1 -0
- package/dist/web/assets/{markdown-renderer-DNIXdY0d.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-ChC1gaaZ.js → pdf-preview-DvgyxJX7.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-cjpNfVG5.js +1 -0
- package/dist/web/assets/{port-forwarding-tab-dLhH_g2l.js → port-forwarding-tab-CUkU6wac.js} +1 -1
- package/dist/web/assets/{postgres-viewer-De0pzd1C.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-CHPVrMtx.js → video-preview-BU7tibc4.js} +1 -1
- package/dist/web/assets/x-CG-_0yIW.js +1 -0
- package/dist/web/index.html +10 -11
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- 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-toolbar.tsx +105 -0
- package/src/web/components/database/glide-grid-types.ts +2 -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 +65 -30
- package/src/web/components/database/use-glide-columns.ts +24 -16
- 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/editor/code-editor.tsx +7 -5
- 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/dist/web/assets/ai-settings-section-CLNBWLS4.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-WMbLpD5Y.js +0 -1
- package/dist/web/assets/code-editor-rNw5_pXh.js +0 -8
- package/dist/web/assets/csv-parser-DO0dz4x_.js +0 -6
- package/dist/web/assets/data-grid-nZfSIop5.js +0 -5
- package/dist/web/assets/database-viewer-CNoq5Uxp.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-BdPTuzO3.js +0 -1
- package/dist/web/assets/index-CoMWx5VS.js +0 -27
- package/dist/web/assets/index-Dzb3OtrX.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-MHX_1JfR.js +0 -1
- package/dist/web/assets/keybindings-store-B7nlHmDh.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/radar-KQ55EAFF-UxsdRHvt.js +0 -1
- package/dist/web/assets/settings-tab-Mrs9uzCZ.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-BqtIjvil.js +0 -1
- package/dist/web/assets/terminal-tab-CeHEtoE2.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-OGGXhtlb.js +0 -1
- /package/dist/web/assets/{arrow-up-Dtrfv490.js → arrow-up-Rcw6_KKu.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
|
@@ -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,
|
|
@@ -2,6 +2,7 @@ import { useCallback, useRef } from "react";
|
|
|
2
2
|
import { GridCellKind, type GridCell, type EditableGridCell, type Item } from "@glideapps/glide-data-grid";
|
|
3
3
|
import type { GridColumnSchema } from "./glide-grid-types";
|
|
4
4
|
import { formatCellValue } from "./glide-grid-types";
|
|
5
|
+
import type { PendingEdit } from "./use-glide-pending-edits";
|
|
5
6
|
|
|
6
7
|
/** Map DB type string to Glide cell kind */
|
|
7
8
|
function dbTypeToKind(type: string): GridCellKind {
|
|
@@ -13,11 +14,24 @@ function dbTypeToKind(type: string): GridCellKind {
|
|
|
13
14
|
return GridCellKind.Text;
|
|
14
15
|
}
|
|
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
|
+
|
|
16
27
|
/** Truncate display string for canvas rendering performance */
|
|
17
28
|
function truncateDisplay(val: string, max = 200): string {
|
|
18
29
|
return val.length > max ? val.slice(0, max) + "…" : val;
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
/** Amber background for cells with pending unsaved edits */
|
|
33
|
+
const PENDING_THEME = { bgCell: "rgba(251, 191, 36, 0.15)" };
|
|
34
|
+
|
|
21
35
|
interface UseGlideCellContentResult {
|
|
22
36
|
getCellContent: (cell: Item) => GridCell;
|
|
23
37
|
onCellEdited: (cell: Item, newValue: EditableGridCell) => void;
|
|
@@ -26,15 +40,16 @@ interface UseGlideCellContentResult {
|
|
|
26
40
|
/**
|
|
27
41
|
* Provides getCellContent and onCellEdited callbacks for Glide Data Grid.
|
|
28
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.
|
|
29
44
|
*/
|
|
30
45
|
export function useGlideCellContent(
|
|
31
46
|
rows: Record<string, unknown>[],
|
|
32
47
|
columnOrder: string[],
|
|
33
48
|
schema: GridColumnSchema[],
|
|
34
49
|
pkCol: string | null,
|
|
35
|
-
|
|
50
|
+
addPendingEdit: (pkVal: unknown, col: string, newVal: unknown) => void,
|
|
51
|
+
pendingRef: React.RefObject<Map<string, PendingEdit>>,
|
|
36
52
|
): UseGlideCellContentResult {
|
|
37
|
-
// Use refs to avoid stale data in canvas callbacks
|
|
38
53
|
const rowsRef = useRef(rows);
|
|
39
54
|
rowsRef.current = rows;
|
|
40
55
|
const colOrderRef = useRef(columnOrder);
|
|
@@ -50,52 +65,72 @@ export function useGlideCellContent(
|
|
|
50
65
|
return { kind: GridCellKind.Text, data: "", displayData: "", allowOverlay: false };
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
const val = row[colName];
|
|
54
68
|
const colSchema = schemaMap.current.get(colName);
|
|
55
69
|
const kind = colSchema ? dbTypeToKind(colSchema.type) : GridCellKind.Text;
|
|
56
70
|
const isPk = colSchema?.pk ?? false;
|
|
57
71
|
|
|
58
|
-
//
|
|
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
|
|
59
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
|
+
}
|
|
60
107
|
return {
|
|
61
|
-
kind: GridCellKind.Text,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
allowOverlay: !isPk,
|
|
65
|
-
readonly: isPk,
|
|
66
|
-
themeOverride: { textDark: "#6b7280" },
|
|
108
|
+
kind: GridCellKind.Text, data: "", displayData: placeholder,
|
|
109
|
+
allowOverlay: !isPk, readonly: isPk,
|
|
110
|
+
themeOverride: hasPending ? PENDING_THEME : (isNewRow || placeholder !== "NULL" ? { textDark: "#9ca3af" } : { textDark: "#6b7280" }),
|
|
67
111
|
};
|
|
68
112
|
}
|
|
69
113
|
|
|
70
114
|
// Number cells
|
|
71
115
|
if (kind === GridCellKind.Number && typeof val === "number") {
|
|
72
116
|
return {
|
|
73
|
-
kind: GridCellKind.Number,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
allowOverlay: !isPk,
|
|
77
|
-
readonly: isPk,
|
|
117
|
+
kind: GridCellKind.Number, data: val, displayData: String(val),
|
|
118
|
+
allowOverlay: !isPk, readonly: isPk,
|
|
119
|
+
themeOverride: hasPending ? PENDING_THEME : undefined,
|
|
78
120
|
};
|
|
79
121
|
}
|
|
80
122
|
|
|
81
|
-
// Boolean cells
|
|
123
|
+
// Boolean cells
|
|
82
124
|
if (kind === GridCellKind.Boolean && typeof val === "boolean") {
|
|
83
|
-
return {
|
|
84
|
-
kind: GridCellKind.Boolean,
|
|
85
|
-
data: val,
|
|
86
|
-
readonly: isPk,
|
|
87
|
-
allowOverlay: false,
|
|
88
|
-
};
|
|
125
|
+
return { kind: GridCellKind.Boolean, data: val, readonly: isPk, allowOverlay: false };
|
|
89
126
|
}
|
|
90
127
|
|
|
91
|
-
//
|
|
128
|
+
// Text cell
|
|
92
129
|
const strVal = formatCellValue(val);
|
|
93
130
|
return {
|
|
94
|
-
kind: GridCellKind.Text,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
allowOverlay: !isPk,
|
|
98
|
-
readonly: isPk,
|
|
131
|
+
kind: GridCellKind.Text, data: strVal, displayData: truncateDisplay(strVal),
|
|
132
|
+
allowOverlay: !isPk, readonly: isPk,
|
|
133
|
+
themeOverride: hasPending ? PENDING_THEME : undefined,
|
|
99
134
|
};
|
|
100
135
|
}, []); // stable — reads from refs
|
|
101
136
|
|
|
@@ -117,8 +152,8 @@ export function useGlideCellContent(
|
|
|
117
152
|
return;
|
|
118
153
|
}
|
|
119
154
|
|
|
120
|
-
|
|
121
|
-
}, [pkCol,
|
|
155
|
+
addPendingEdit(pkVal, colName, parsed);
|
|
156
|
+
}, [pkCol, addPendingEdit]);
|
|
122
157
|
|
|
123
158
|
return { getCellContent, onCellEdited };
|
|
124
159
|
}
|
|
@@ -11,20 +11,36 @@ interface UseGlideColumnsResult {
|
|
|
11
11
|
columnOrder: string[];
|
|
12
12
|
}
|
|
13
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
|
+
|
|
14
30
|
/**
|
|
15
31
|
* Build Glide Data Grid column definitions from schema.
|
|
16
|
-
* Reorders columns: pinned first, then unpinned.
|
|
32
|
+
* Reorders columns: pinned first, then unpinned. Auto-sizes widths.
|
|
17
33
|
*/
|
|
18
34
|
export function useGlideColumns(
|
|
19
35
|
schema: GridColumnSchema[],
|
|
20
36
|
columnNames: string[],
|
|
21
37
|
pinnedCols: Set<string>,
|
|
22
38
|
colWidths: Map<string, number>,
|
|
39
|
+
rows: Record<string, unknown>[],
|
|
23
40
|
orderBy?: string | null,
|
|
24
41
|
orderDir?: "ASC" | "DESC",
|
|
25
42
|
): UseGlideColumnsResult {
|
|
26
43
|
return useMemo(() => {
|
|
27
|
-
// Reorder: pinned first, then rest (preserve original order within each group)
|
|
28
44
|
const pinned = columnNames.filter((c) => pinnedCols.has(c));
|
|
29
45
|
const unpinned = columnNames.filter((c) => !pinnedCols.has(c));
|
|
30
46
|
const ordered = [...pinned, ...unpinned];
|
|
@@ -35,27 +51,19 @@ export function useGlideColumns(
|
|
|
35
51
|
const col = schemaMap.get(name);
|
|
36
52
|
const isPk = col?.pk ?? false;
|
|
37
53
|
|
|
38
|
-
// Determine sort icon
|
|
39
54
|
let icon: string | undefined;
|
|
40
55
|
if (orderBy === name) {
|
|
41
56
|
icon = orderDir === "ASC" ? "sortAsc" : "sortDesc";
|
|
42
57
|
} else if (isPk) {
|
|
43
58
|
icon = "headerRowID";
|
|
59
|
+
} else if (col?.fk) {
|
|
60
|
+
icon = "headerFk";
|
|
44
61
|
}
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
id: name,
|
|
49
|
-
width: colWidths.get(name) ?? 150,
|
|
50
|
-
hasMenu: true,
|
|
51
|
-
icon,
|
|
52
|
-
};
|
|
63
|
+
const width = colWidths.get(name) ?? estimateColWidth(name, rows, col?.type ?? "text");
|
|
64
|
+
return { title: name, id: name, width, hasMenu: true, icon };
|
|
53
65
|
});
|
|
54
66
|
|
|
55
|
-
return {
|
|
56
|
-
|
|
57
|
-
freezeColumns: pinned.length,
|
|
58
|
-
columnOrder: ordered,
|
|
59
|
-
};
|
|
60
|
-
}, [schema, columnNames, pinnedCols, colWidths, orderBy, orderDir]);
|
|
67
|
+
return { columns, freezeColumns: pinned.length, columnOrder: ordered };
|
|
68
|
+
}, [schema, columnNames, pinnedCols, colWidths, rows, orderBy, orderDir]);
|
|
61
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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export interface PendingEdit {
|
|
4
|
+
pkVal: unknown;
|
|
5
|
+
col: string;
|
|
6
|
+
newVal: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UseGlidePendingEditsResult {
|
|
10
|
+
pendingEdits: Map<string, PendingEdit>;
|
|
11
|
+
pendingRef: React.RefObject<Map<string, PendingEdit>>;
|
|
12
|
+
addEdit: (pkVal: unknown, col: string, newVal: unknown) => void;
|
|
13
|
+
commitAll: () => Promise<void>;
|
|
14
|
+
discardAll: () => void;
|
|
15
|
+
hasPending: boolean;
|
|
16
|
+
pendingCount: number;
|
|
17
|
+
/** True after commitAll until cleared — used to clear edits on rows refresh */
|
|
18
|
+
committedRef: React.RefObject<boolean>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Tracks cell edits locally until the user explicitly saves.
|
|
23
|
+
* After commit, keeps pending values visible until rows refresh (avoids flash of stale data).
|
|
24
|
+
* Supports inline insert: edits with PK starting "__new_" are routed to onInsertRow.
|
|
25
|
+
*/
|
|
26
|
+
export function useGlidePendingEdits(
|
|
27
|
+
pkCol: string | null,
|
|
28
|
+
onCellUpdate: (pkCol: string, pkVal: unknown, col: string, val: unknown) => void,
|
|
29
|
+
onInsertRow?: (values: Record<string, unknown>) => Promise<void>,
|
|
30
|
+
): UseGlidePendingEditsResult {
|
|
31
|
+
const [pendingEdits, setPendingEdits] = useState<Map<string, PendingEdit>>(new Map());
|
|
32
|
+
const pendingRef = useRef(pendingEdits);
|
|
33
|
+
pendingRef.current = pendingEdits;
|
|
34
|
+
const committedRef = useRef(false);
|
|
35
|
+
|
|
36
|
+
const addEdit = useCallback((pkVal: unknown, col: string, newVal: unknown) => {
|
|
37
|
+
const key = `${pkVal}:${col}`;
|
|
38
|
+
setPendingEdits((prev) => new Map(prev).set(key, { pkVal, col, newVal }));
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const commitAll = useCallback(async () => {
|
|
42
|
+
if (!pkCol) return;
|
|
43
|
+
const newRows = new Map<string, Record<string, unknown>>();
|
|
44
|
+
for (const edit of pendingRef.current.values()) {
|
|
45
|
+
const pkStr = String(edit.pkVal);
|
|
46
|
+
if (pkStr.startsWith("__new_")) {
|
|
47
|
+
if (!newRows.has(pkStr)) newRows.set(pkStr, {});
|
|
48
|
+
newRows.get(pkStr)![edit.col] = edit.newVal;
|
|
49
|
+
} else {
|
|
50
|
+
onCellUpdate(pkCol, edit.pkVal, edit.col, edit.newVal);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (onInsertRow) {
|
|
54
|
+
for (const values of newRows.values()) {
|
|
55
|
+
await onInsertRow(values);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
committedRef.current = true;
|
|
59
|
+
// Don't clear — wait for rows prop to refresh so grid doesn't flash stale data
|
|
60
|
+
}, [pkCol, onCellUpdate, onInsertRow]);
|
|
61
|
+
|
|
62
|
+
const discardAll = useCallback(() => {
|
|
63
|
+
setPendingEdits(new Map());
|
|
64
|
+
committedRef.current = false;
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
pendingEdits, pendingRef, addEdit, commitAll, discardAll,
|
|
69
|
+
hasPending: pendingEdits.size > 0, pendingCount: pendingEdits.size,
|
|
70
|
+
committedRef,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useState, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
interface UseGlideRowPinningResult {
|
|
4
|
+
/** Rows reordered: unpinned first, pinned at end (frozen via freezeTrailingRows) */
|
|
5
|
+
effectiveRows: Record<string, unknown>[];
|
|
6
|
+
/** Number of pinned rows — pass to DataEditor's freezeTrailingRows */
|
|
7
|
+
pinnedCount: number;
|
|
8
|
+
pinnedPks: Set<string>;
|
|
9
|
+
setPinnedPks: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages row pinning state — pinned rows placed at the end of the array
|
|
14
|
+
* so they can be frozen at the bottom via Glide's freezeTrailingRows.
|
|
15
|
+
*/
|
|
16
|
+
export function useGlideRowPinning(
|
|
17
|
+
rows: Record<string, unknown>[],
|
|
18
|
+
pkCol: string | null,
|
|
19
|
+
): UseGlideRowPinningResult {
|
|
20
|
+
const [pinnedPks, setPinnedPks] = useState<Set<string>>(new Set());
|
|
21
|
+
|
|
22
|
+
const effectiveRows = useMemo(() => {
|
|
23
|
+
if (pinnedPks.size === 0 || !pkCol) return rows;
|
|
24
|
+
const normal: Record<string, unknown>[] = [];
|
|
25
|
+
const pinned: Record<string, unknown>[] = [];
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
if (pinnedPks.has(String(row[pkCol] ?? ""))) pinned.push(row); else normal.push(row);
|
|
28
|
+
}
|
|
29
|
+
return [...normal, ...pinned];
|
|
30
|
+
}, [rows, pinnedPks, pkCol]);
|
|
31
|
+
|
|
32
|
+
const pinnedCount = pinnedPks.size;
|
|
33
|
+
|
|
34
|
+
return { effectiveRows, pinnedCount, pinnedPks, setPinnedPks };
|
|
35
|
+
}
|
|
@@ -15,8 +15,9 @@ import { SaveAsDialog } from "./save-as-dialog";
|
|
|
15
15
|
import { EditorMobileToolbar } from "./editor-mobile-toolbar";
|
|
16
16
|
import { createSqlCompletionProvider, clearCompletionCache, type SchemaInfo } from "../database/sql-completion-provider";
|
|
17
17
|
import { useConnections, type Connection } from "../database/use-connections";
|
|
18
|
-
import {
|
|
19
|
-
import type {
|
|
18
|
+
import { GlideDataGrid } from "../database/glide-data-grid";
|
|
19
|
+
import type { GridColumnSchema } from "../database/glide-grid-types";
|
|
20
|
+
import type { DbQueryResult } from "../database/use-database";
|
|
20
21
|
|
|
21
22
|
const MarkdownRenderer = lazy(() =>
|
|
22
23
|
import("@/components/shared/markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
|
|
@@ -673,7 +674,7 @@ function SqlResultPanel({ result, error, loading, connName, onClose, onOpenInTab
|
|
|
673
674
|
: null
|
|
674
675
|
), [result]);
|
|
675
676
|
|
|
676
|
-
const querySchema = useMemo<
|
|
677
|
+
const querySchema = useMemo<GridColumnSchema[]>(() => (
|
|
677
678
|
(result?.columns ?? []).map((c) => ({ name: c, type: "text", nullable: true, pk: false, defaultValue: null }))
|
|
678
679
|
), [result?.columns]);
|
|
679
680
|
|
|
@@ -727,8 +728,9 @@ function SqlResultPanel({ result, error, loading, connName, onClose, onOpenInTab
|
|
|
727
728
|
</div>
|
|
728
729
|
)}
|
|
729
730
|
{tableData && (
|
|
730
|
-
<
|
|
731
|
-
tableData={tableData}
|
|
731
|
+
<GlideDataGrid
|
|
732
|
+
columns={tableData.columns} rows={tableData.rows} total={tableData.total} limit={tableData.limit}
|
|
733
|
+
schema={querySchema} loading={false}
|
|
732
734
|
page={1} onPageChange={NOOP} onCellUpdate={NOOP}
|
|
733
735
|
orderBy={null} orderDir="ASC" onToggleSort={NOOP}
|
|
734
736
|
connectionName={connName}
|
|
@@ -67,13 +67,13 @@ export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
|
|
|
67
67
|
const isActive = tab.id === panel.activeTabId;
|
|
68
68
|
if (!Component) {
|
|
69
69
|
return (
|
|
70
|
-
<div key={tab.id} className={isActive ? "
|
|
70
|
+
<div key={tab.id} className={isActive ? "absolute inset-0 flex items-center justify-center text-muted-foreground" : "hidden"}>
|
|
71
71
|
Unknown tab type: {tab.type}
|
|
72
72
|
</div>
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
return (
|
|
76
|
-
<div key={tab.id} className={isActive ?
|
|
76
|
+
<div key={tab.id} className="absolute inset-0" style={isActive ? undefined : { opacity: 0, pointerEvents: "none" }}>
|
|
77
77
|
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="size-6 animate-spin text-primary" /></div>}>
|
|
78
78
|
<Component metadata={tab.metadata} tabId={tab.id} />
|
|
79
79
|
</Suspense>
|