dbdiff-app 0.1.0
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/README.md +73 -0
- package/bin/cli.js +83 -0
- package/bin/install-local.js +57 -0
- package/electron/generate-icon.mjs +54 -0
- package/electron/icon.icns +0 -0
- package/electron/icon.png +0 -0
- package/electron/icon.svg +21 -0
- package/electron/main.js +169 -0
- package/electron/patch-dev-plist.js +31 -0
- package/electron/preload.cjs +18 -0
- package/electron/wait-for-vite.js +43 -0
- package/index.html +13 -0
- package/package.json +91 -0
- package/public/favicon.svg +15 -0
- package/public/vite.svg +1 -0
- package/server/export.ts +57 -0
- package/server/index.ts +392 -0
- package/src/App.css +1 -0
- package/src/App.tsx +543 -0
- package/src/assets/react.svg +1 -0
- package/src/components/CommandPalette.tsx +243 -0
- package/src/components/ConnectedView.tsx +78 -0
- package/src/components/ConnectionPicker.tsx +381 -0
- package/src/components/ConsoleView.tsx +360 -0
- package/src/components/CsvExportModal.tsx +144 -0
- package/src/components/DataGrid/DataGrid.tsx +262 -0
- package/src/components/DataGrid/DataGridCell.tsx +73 -0
- package/src/components/DataGrid/DataGridHeader.tsx +89 -0
- package/src/components/DataGrid/index.ts +20 -0
- package/src/components/DataGrid/types.ts +63 -0
- package/src/components/DataGrid/useColumnResize.ts +153 -0
- package/src/components/DataGrid/useDataGridSelection.ts +340 -0
- package/src/components/DataGrid/utils.ts +184 -0
- package/src/components/DatabaseMenu.tsx +93 -0
- package/src/components/DatabaseSwitcher.tsx +208 -0
- package/src/components/DiffView.tsx +215 -0
- package/src/components/EditConnectionModal.tsx +417 -0
- package/src/components/ErrorBoundary.tsx +69 -0
- package/src/components/GlobalShortcuts.tsx +201 -0
- package/src/components/InnerTabBar.tsx +129 -0
- package/src/components/JsonTreeViewer.tsx +387 -0
- package/src/components/MemberAccessEditor.tsx +443 -0
- package/src/components/MembersModal.tsx +446 -0
- package/src/components/NewConnectionModal.tsx +274 -0
- package/src/components/Resizer.tsx +66 -0
- package/src/components/ScanSuccessModal.tsx +113 -0
- package/src/components/ShortcutSettingsModal.tsx +318 -0
- package/src/components/Sidebar.tsx +532 -0
- package/src/components/TabBar.tsx +188 -0
- package/src/components/TableView.tsx +2147 -0
- package/src/components/ThemeToggle.tsx +44 -0
- package/src/components/index.ts +17 -0
- package/src/constants.ts +12 -0
- package/src/electron.d.ts +12 -0
- package/src/index.css +44 -0
- package/src/main.tsx +13 -0
- package/src/stores/hooks.ts +1146 -0
- package/src/stores/index.ts +12 -0
- package/src/stores/store.ts +1514 -0
- package/src/stores/useCloudSync.ts +274 -0
- package/src/stores/useSyncDatabase.ts +422 -0
- package/src/types.ts +277 -0
- package/src/utils/csv.ts +27 -0
- package/src/vite-env.d.ts +2 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/tsconfig.server.json +14 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
3
|
+
import type { DataGridProps } from "./types";
|
|
4
|
+
import { getCellRangeInfo, ROW_HEIGHT, virtualToStoreIndex } from "./utils";
|
|
5
|
+
import { DataGridHeader } from "./DataGridHeader";
|
|
6
|
+
import { DataGridCell } from "./DataGridCell";
|
|
7
|
+
import { useColumnResize } from "./useColumnResize";
|
|
8
|
+
import { useDataGridSelection } from "./useDataGridSelection";
|
|
9
|
+
|
|
10
|
+
export function DataGrid({
|
|
11
|
+
columns,
|
|
12
|
+
rows,
|
|
13
|
+
extraRows,
|
|
14
|
+
sortColumns,
|
|
15
|
+
onSortChange,
|
|
16
|
+
selection: controlledSelection,
|
|
17
|
+
onSelectionChange,
|
|
18
|
+
renderCell,
|
|
19
|
+
onKeyDown,
|
|
20
|
+
className,
|
|
21
|
+
scrollRef: externalScrollRef,
|
|
22
|
+
tableRef: externalTableRef,
|
|
23
|
+
onHeaderContextMenu,
|
|
24
|
+
fkPreviewActiveColumns,
|
|
25
|
+
}: DataGridProps) {
|
|
26
|
+
const internalScrollRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
const internalTableRef = useRef<HTMLTableElement>(null);
|
|
28
|
+
const scrollRef = externalScrollRef ?? internalScrollRef;
|
|
29
|
+
const tableRef = externalTableRef ?? internalTableRef;
|
|
30
|
+
|
|
31
|
+
const totalVirtualRowCount = rows.length + (extraRows?.length ?? 0);
|
|
32
|
+
|
|
33
|
+
const virtualizer = useVirtualizer({
|
|
34
|
+
count: totalVirtualRowCount,
|
|
35
|
+
getScrollElement: () => scrollRef.current,
|
|
36
|
+
estimateSize: () => ROW_HEIGHT,
|
|
37
|
+
overscan: 15,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { columnWidths, resizingColumn, justResizedRef, handleResizeStart } =
|
|
41
|
+
useColumnResize({
|
|
42
|
+
columns,
|
|
43
|
+
scrollRef,
|
|
44
|
+
tableRef,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const scrollToIndex = useCallback(
|
|
48
|
+
(index: number) => {
|
|
49
|
+
virtualizer.scrollToIndex(index);
|
|
50
|
+
},
|
|
51
|
+
[virtualizer],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
selection,
|
|
56
|
+
handleCellClick,
|
|
57
|
+
handleCellMouseDown,
|
|
58
|
+
handleCellMouseEnter,
|
|
59
|
+
} = useDataGridSelection({
|
|
60
|
+
columns,
|
|
61
|
+
rows,
|
|
62
|
+
extraRows,
|
|
63
|
+
selection: controlledSelection,
|
|
64
|
+
onSelectionChange,
|
|
65
|
+
onKeyDown,
|
|
66
|
+
scrollToIndex,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const handleColumnClick = useCallback(
|
|
70
|
+
(columnName: string, e: React.MouseEvent) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
if (justResizedRef.current) return;
|
|
73
|
+
const addToExisting = e.ctrlKey || e.metaKey;
|
|
74
|
+
onSortChange?.(columnName, addToExisting);
|
|
75
|
+
},
|
|
76
|
+
[onSortChange, justResizedRef],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const columnNames = columns.map((c) => c.name);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
ref={scrollRef}
|
|
84
|
+
className={`flex-1 overflow-auto pb-8 pr-4 ${className ?? ""}`}
|
|
85
|
+
>
|
|
86
|
+
<table
|
|
87
|
+
ref={tableRef}
|
|
88
|
+
className={`w-full text-[13px] border-collapse ${
|
|
89
|
+
selection.isDragging || resizingColumn ? "select-none" : ""
|
|
90
|
+
}`}
|
|
91
|
+
style={{ tableLayout: "fixed" }}
|
|
92
|
+
>
|
|
93
|
+
<colgroup>
|
|
94
|
+
{columns.map((col) => (
|
|
95
|
+
<col
|
|
96
|
+
key={col.name}
|
|
97
|
+
style={{ width: columnWidths[col.name] ?? 150 }}
|
|
98
|
+
/>
|
|
99
|
+
))}
|
|
100
|
+
</colgroup>
|
|
101
|
+
<thead className="sticky top-0 z-10 bg-stone-100 dark:bg-neutral-900">
|
|
102
|
+
<tr>
|
|
103
|
+
{columns.map((col, i) => (
|
|
104
|
+
<DataGridHeader
|
|
105
|
+
key={i}
|
|
106
|
+
columnName={col.name}
|
|
107
|
+
sortColumns={sortColumns}
|
|
108
|
+
onClick={onSortChange ? handleColumnClick : undefined}
|
|
109
|
+
onResizeStart={handleResizeStart}
|
|
110
|
+
onContextMenu={onHeaderContextMenu}
|
|
111
|
+
fkPreviewActive={fkPreviewActiveColumns?.has(col.name)}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</tr>
|
|
115
|
+
</thead>
|
|
116
|
+
<tbody>
|
|
117
|
+
{(() => {
|
|
118
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
119
|
+
const paddingTop = virtualItems[0]?.start ?? 0;
|
|
120
|
+
const paddingBottom =
|
|
121
|
+
virtualItems.length > 0
|
|
122
|
+
? virtualizer.getTotalSize() -
|
|
123
|
+
virtualItems[virtualItems.length - 1].end
|
|
124
|
+
: 0;
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
{paddingTop > 0 && (
|
|
128
|
+
<tr>
|
|
129
|
+
<td
|
|
130
|
+
style={{ height: paddingTop, padding: 0 }}
|
|
131
|
+
colSpan={columns.length}
|
|
132
|
+
/>
|
|
133
|
+
</tr>
|
|
134
|
+
)}
|
|
135
|
+
{virtualItems.map((virtualItem) => {
|
|
136
|
+
const isExtraRow = virtualItem.index >= rows.length;
|
|
137
|
+
const storeIndex = virtualToStoreIndex(
|
|
138
|
+
virtualItem.index,
|
|
139
|
+
rows.length,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (isExtraRow) {
|
|
143
|
+
const extraRowIdx = virtualItem.index - rows.length;
|
|
144
|
+
const extraRow = extraRows?.[extraRowIdx];
|
|
145
|
+
if (!extraRow) return null;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<tr key={`extra-${extraRow.key}`}>
|
|
149
|
+
{columns.map((col, colIndex) => {
|
|
150
|
+
const value = extraRow.data[col.name] ?? null;
|
|
151
|
+
const rangeInfo = getCellRangeInfo(
|
|
152
|
+
{ rowIndex: storeIndex, columnName: col.name },
|
|
153
|
+
selection.selectedRange,
|
|
154
|
+
columnNames,
|
|
155
|
+
rows.length,
|
|
156
|
+
);
|
|
157
|
+
const isSelected =
|
|
158
|
+
selection.selectedCell?.rowIndex === storeIndex &&
|
|
159
|
+
selection.selectedCell?.columnName === col.name;
|
|
160
|
+
|
|
161
|
+
if (renderCell) {
|
|
162
|
+
return renderCell({
|
|
163
|
+
rowIndex: storeIndex,
|
|
164
|
+
columnIndex: colIndex,
|
|
165
|
+
columnName: col.name,
|
|
166
|
+
value,
|
|
167
|
+
isSelected,
|
|
168
|
+
isInRange: rangeInfo.isInRange,
|
|
169
|
+
rangeEdges: rangeInfo.edges,
|
|
170
|
+
onClick: handleCellClick,
|
|
171
|
+
onMouseDown: handleCellMouseDown,
|
|
172
|
+
onMouseEnter: handleCellMouseEnter,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<DataGridCell
|
|
178
|
+
key={colIndex}
|
|
179
|
+
rowIndex={storeIndex}
|
|
180
|
+
columnName={col.name}
|
|
181
|
+
value={value}
|
|
182
|
+
isSelected={isSelected}
|
|
183
|
+
isInRange={rangeInfo.isInRange}
|
|
184
|
+
rangeEdges={rangeInfo.edges}
|
|
185
|
+
onClick={handleCellClick}
|
|
186
|
+
onMouseDown={handleCellMouseDown}
|
|
187
|
+
onMouseEnter={handleCellMouseEnter}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
})}
|
|
191
|
+
</tr>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const row = rows[storeIndex];
|
|
196
|
+
if (!row) return null;
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<tr
|
|
200
|
+
key={storeIndex}
|
|
201
|
+
className="hover:bg-stone-50 dark:hover:bg-white/[0.02]"
|
|
202
|
+
>
|
|
203
|
+
{columns.map((col, colIndex) => {
|
|
204
|
+
const rangeInfo = getCellRangeInfo(
|
|
205
|
+
{ rowIndex: storeIndex, columnName: col.name },
|
|
206
|
+
selection.selectedRange,
|
|
207
|
+
columnNames,
|
|
208
|
+
rows.length,
|
|
209
|
+
);
|
|
210
|
+
const isSelected =
|
|
211
|
+
selection.selectedCell?.rowIndex === storeIndex &&
|
|
212
|
+
selection.selectedCell?.columnName === col.name;
|
|
213
|
+
|
|
214
|
+
if (renderCell) {
|
|
215
|
+
return renderCell({
|
|
216
|
+
rowIndex: storeIndex,
|
|
217
|
+
columnIndex: colIndex,
|
|
218
|
+
columnName: col.name,
|
|
219
|
+
value: row[col.name],
|
|
220
|
+
isSelected,
|
|
221
|
+
isInRange: rangeInfo.isInRange,
|
|
222
|
+
rangeEdges: rangeInfo.edges,
|
|
223
|
+
onClick: handleCellClick,
|
|
224
|
+
onMouseDown: handleCellMouseDown,
|
|
225
|
+
onMouseEnter: handleCellMouseEnter,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<DataGridCell
|
|
231
|
+
key={colIndex}
|
|
232
|
+
rowIndex={storeIndex}
|
|
233
|
+
columnName={col.name}
|
|
234
|
+
value={row[col.name]}
|
|
235
|
+
isSelected={isSelected}
|
|
236
|
+
isInRange={rangeInfo.isInRange}
|
|
237
|
+
rangeEdges={rangeInfo.edges}
|
|
238
|
+
onClick={handleCellClick}
|
|
239
|
+
onMouseDown={handleCellMouseDown}
|
|
240
|
+
onMouseEnter={handleCellMouseEnter}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
</tr>
|
|
245
|
+
);
|
|
246
|
+
})}
|
|
247
|
+
{paddingBottom > 0 && (
|
|
248
|
+
<tr>
|
|
249
|
+
<td
|
|
250
|
+
style={{ height: paddingBottom, padding: 0 }}
|
|
251
|
+
colSpan={columns.length}
|
|
252
|
+
/>
|
|
253
|
+
</tr>
|
|
254
|
+
)}
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
})()}
|
|
258
|
+
</tbody>
|
|
259
|
+
</table>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { RangeEdges } from "./types";
|
|
3
|
+
import { formatCellValue } from "./utils";
|
|
4
|
+
|
|
5
|
+
interface DataGridCellInternalProps {
|
|
6
|
+
rowIndex: number;
|
|
7
|
+
columnName: string;
|
|
8
|
+
value: unknown;
|
|
9
|
+
isSelected: boolean;
|
|
10
|
+
isInRange: boolean;
|
|
11
|
+
rangeEdges: RangeEdges | null;
|
|
12
|
+
onClick: (rowIndex: number, columnName: string, e: React.MouseEvent) => void;
|
|
13
|
+
onMouseDown: (
|
|
14
|
+
rowIndex: number,
|
|
15
|
+
columnName: string,
|
|
16
|
+
e: React.MouseEvent,
|
|
17
|
+
) => void;
|
|
18
|
+
onMouseEnter: (rowIndex: number, columnName: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DataGridCell = React.memo(function DataGridCell({
|
|
22
|
+
rowIndex,
|
|
23
|
+
columnName,
|
|
24
|
+
value,
|
|
25
|
+
isSelected,
|
|
26
|
+
isInRange,
|
|
27
|
+
rangeEdges,
|
|
28
|
+
onClick,
|
|
29
|
+
onMouseDown,
|
|
30
|
+
onMouseEnter,
|
|
31
|
+
}: DataGridCellInternalProps) {
|
|
32
|
+
let cellClassName =
|
|
33
|
+
"px-3 py-2 text-secondary border-b border-r border-stone-200 dark:border-white/[0.06] font-mono max-w-[300px] cursor-pointer";
|
|
34
|
+
|
|
35
|
+
if (isSelected) {
|
|
36
|
+
cellClassName += " bg-blue-100 dark:bg-blue-800/40";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isInRange && rangeEdges && !isSelected) {
|
|
40
|
+
cellClassName += " bg-blue-50 dark:bg-blue-900/20";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rangeBorderStyle: React.CSSProperties = {};
|
|
44
|
+
if (isInRange && rangeEdges) {
|
|
45
|
+
const borderColor = "rgb(59, 130, 246)";
|
|
46
|
+
const shadows: string[] = [];
|
|
47
|
+
if (rangeEdges.top) shadows.push(`inset 0 2px 0 0 ${borderColor}`);
|
|
48
|
+
if (rangeEdges.bottom) shadows.push(`inset 0 -2px 0 0 ${borderColor}`);
|
|
49
|
+
if (rangeEdges.left) shadows.push(`inset 2px 0 0 0 ${borderColor}`);
|
|
50
|
+
if (rangeEdges.right) shadows.push(`inset -2px 0 0 0 ${borderColor}`);
|
|
51
|
+
if (shadows.length > 0) {
|
|
52
|
+
rangeBorderStyle.boxShadow = shadows.join(", ");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<td
|
|
58
|
+
className={cellClassName}
|
|
59
|
+
style={rangeBorderStyle}
|
|
60
|
+
onClick={(e) => onClick(rowIndex, columnName, e)}
|
|
61
|
+
onMouseDown={(e) => onMouseDown(rowIndex, columnName, e)}
|
|
62
|
+
onMouseEnter={() => onMouseEnter(rowIndex, columnName)}
|
|
63
|
+
>
|
|
64
|
+
<div className="truncate">
|
|
65
|
+
{value === null ? (
|
|
66
|
+
<span className="text-tertiary italic">NULL</span>
|
|
67
|
+
) : (
|
|
68
|
+
formatCellValue(value)
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</td>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { SortColumn } from "../../types";
|
|
3
|
+
|
|
4
|
+
interface DataGridHeaderProps {
|
|
5
|
+
columnName: string;
|
|
6
|
+
sortColumns?: SortColumn[];
|
|
7
|
+
onClick?: (columnName: string, e: React.MouseEvent) => void;
|
|
8
|
+
onResizeStart: (columnName: string, clientX: number) => void;
|
|
9
|
+
onContextMenu?: (columnName: string, e: React.MouseEvent) => void;
|
|
10
|
+
fkPreviewActive?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DataGridHeader = React.memo(function DataGridHeader({
|
|
14
|
+
columnName,
|
|
15
|
+
sortColumns,
|
|
16
|
+
onClick,
|
|
17
|
+
onResizeStart,
|
|
18
|
+
onContextMenu,
|
|
19
|
+
fkPreviewActive,
|
|
20
|
+
}: DataGridHeaderProps) {
|
|
21
|
+
const sortIndex = sortColumns?.findIndex((s) => s.column === columnName);
|
|
22
|
+
const sortInfo =
|
|
23
|
+
sortIndex != null && sortIndex !== -1 ? sortColumns![sortIndex] : null;
|
|
24
|
+
const showPriority =
|
|
25
|
+
sortColumns != null && sortColumns.length > 1 && sortInfo;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<th
|
|
29
|
+
onClick={onClick ? (e) => onClick(columnName, e) : undefined}
|
|
30
|
+
onContextMenu={(e) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
onContextMenu?.(columnName, e);
|
|
33
|
+
}}
|
|
34
|
+
className={`text-left px-3 py-2 font-medium text-primary border-b border-r border-stone-200 dark:border-white/[0.06] ${onClick ? "cursor-pointer" : ""} hover:bg-stone-200 bg-stone-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 select-none transition-colors relative`}
|
|
35
|
+
>
|
|
36
|
+
<div className="flex items-center gap-1.5 overflow-hidden">
|
|
37
|
+
<span className="truncate">{columnName}</span>
|
|
38
|
+
{fkPreviewActive && (
|
|
39
|
+
<span
|
|
40
|
+
className="w-1.5 h-1.5 rounded-full bg-violet-500 dark:bg-violet-400 flex-shrink-0"
|
|
41
|
+
title="FK preview active"
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
{sortInfo && (
|
|
45
|
+
<span className="flex items-center gap-0.5 text-blue-600 dark:text-blue-400 flex-shrink-0">
|
|
46
|
+
{sortInfo.direction === "ASC" ? (
|
|
47
|
+
<svg
|
|
48
|
+
className="w-3.5 h-3.5"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
strokeWidth="2"
|
|
53
|
+
>
|
|
54
|
+
<polyline points="18 15 12 9 6 15" />
|
|
55
|
+
</svg>
|
|
56
|
+
) : (
|
|
57
|
+
<svg
|
|
58
|
+
className="w-3.5 h-3.5"
|
|
59
|
+
viewBox="0 0 24 24"
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="2"
|
|
63
|
+
>
|
|
64
|
+
<polyline points="6 9 12 15 18 9" />
|
|
65
|
+
</svg>
|
|
66
|
+
)}
|
|
67
|
+
{showPriority && (
|
|
68
|
+
<span className="text-[10px] font-bold">{sortIndex! + 1}</span>
|
|
69
|
+
)}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
{/* Resize handle */}
|
|
74
|
+
<div
|
|
75
|
+
onMouseDown={(e) => {
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
onResizeStart(columnName, e.clientX);
|
|
78
|
+
}}
|
|
79
|
+
className="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize bg-transparent hover:bg-blue-500/50 active:bg-blue-500/50 transition-colors"
|
|
80
|
+
style={{
|
|
81
|
+
marginLeft: -4,
|
|
82
|
+
marginRight: -4,
|
|
83
|
+
paddingLeft: 4,
|
|
84
|
+
paddingRight: 4,
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
</th>
|
|
88
|
+
);
|
|
89
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { DataGrid } from "./DataGrid";
|
|
2
|
+
export type {
|
|
3
|
+
DataGridColumn,
|
|
4
|
+
DataGridSelection,
|
|
5
|
+
DataGridCellProps,
|
|
6
|
+
DataGridProps,
|
|
7
|
+
ExtraRow,
|
|
8
|
+
RangeEdges,
|
|
9
|
+
} from "./types";
|
|
10
|
+
export {
|
|
11
|
+
ROW_HEIGHT,
|
|
12
|
+
storeToVirtualIndex,
|
|
13
|
+
virtualToStoreIndex,
|
|
14
|
+
getSelectedRowIndices,
|
|
15
|
+
getCellRangeInfo,
|
|
16
|
+
formatCellValue,
|
|
17
|
+
getInternalClipboardValue,
|
|
18
|
+
parseTSV,
|
|
19
|
+
isInternalRangeCopy,
|
|
20
|
+
} from "./utils";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CellPosition, CellRange, SortColumn } from "../../types";
|
|
2
|
+
|
|
3
|
+
export interface RangeEdges {
|
|
4
|
+
top: boolean;
|
|
5
|
+
bottom: boolean;
|
|
6
|
+
left: boolean;
|
|
7
|
+
right: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DataGridColumn {
|
|
11
|
+
name: string;
|
|
12
|
+
dataTypeID?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DataGridSelection {
|
|
16
|
+
selectedCell: CellPosition | null;
|
|
17
|
+
selectedRange: CellRange | null;
|
|
18
|
+
isDragging: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DataGridCellProps {
|
|
22
|
+
rowIndex: number;
|
|
23
|
+
columnIndex: number;
|
|
24
|
+
columnName: string;
|
|
25
|
+
value: unknown;
|
|
26
|
+
isSelected: boolean;
|
|
27
|
+
isInRange: boolean;
|
|
28
|
+
rangeEdges: RangeEdges | null;
|
|
29
|
+
onClick: (rowIndex: number, columnName: string, e: React.MouseEvent) => void;
|
|
30
|
+
onMouseDown: (
|
|
31
|
+
rowIndex: number,
|
|
32
|
+
columnName: string,
|
|
33
|
+
e: React.MouseEvent,
|
|
34
|
+
) => void;
|
|
35
|
+
onMouseEnter: (rowIndex: number, columnName: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ExtraRow {
|
|
39
|
+
key: string;
|
|
40
|
+
data: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DataGridProps {
|
|
44
|
+
columns: DataGridColumn[];
|
|
45
|
+
rows: Record<string, unknown>[];
|
|
46
|
+
extraRows?: ExtraRow[];
|
|
47
|
+
sortColumns?: SortColumn[];
|
|
48
|
+
onSortChange?: (column: string, addToExisting: boolean) => void;
|
|
49
|
+
selection?: DataGridSelection;
|
|
50
|
+
onSelectionChange?: (selection: DataGridSelection) => void;
|
|
51
|
+
renderCell?: (props: DataGridCellProps) => React.ReactNode;
|
|
52
|
+
/** Return true if the key was handled (prevents DataGrid's default handling) */
|
|
53
|
+
onKeyDown?: (e: KeyboardEvent) => boolean;
|
|
54
|
+
className?: string;
|
|
55
|
+
/** Ref to the scroll container element */
|
|
56
|
+
scrollRef?: React.RefObject<HTMLDivElement | null>;
|
|
57
|
+
/** Ref to the table element */
|
|
58
|
+
tableRef?: React.RefObject<HTMLTableElement | null>;
|
|
59
|
+
/** Called when user right-clicks a column header */
|
|
60
|
+
onHeaderContextMenu?: (columnName: string, e: React.MouseEvent) => void;
|
|
61
|
+
/** Set of column names that have active FK preview */
|
|
62
|
+
fkPreviewActiveColumns?: Set<string>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { DataGridColumn } from "./types";
|
|
3
|
+
|
|
4
|
+
interface UseColumnResizeOptions {
|
|
5
|
+
columns: DataGridColumn[];
|
|
6
|
+
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
7
|
+
tableRef: React.RefObject<HTMLTableElement | null>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useColumnResize({
|
|
11
|
+
columns,
|
|
12
|
+
scrollRef,
|
|
13
|
+
tableRef,
|
|
14
|
+
}: UseColumnResizeOptions) {
|
|
15
|
+
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
16
|
+
const [resizingColumn, setResizingColumn] = useState<{
|
|
17
|
+
name: string;
|
|
18
|
+
startX: number;
|
|
19
|
+
startWidth: number;
|
|
20
|
+
startScrollLeft: number;
|
|
21
|
+
} | null>(null);
|
|
22
|
+
const justResizedRef = useRef(false);
|
|
23
|
+
const lastClientXRef = useRef(0);
|
|
24
|
+
|
|
25
|
+
// Reset column widths when columns change
|
|
26
|
+
const fieldKey = useMemo(
|
|
27
|
+
() => columns.map((f) => f.name).join(","),
|
|
28
|
+
[columns],
|
|
29
|
+
);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (columns.length > 0) {
|
|
32
|
+
const widths: Record<string, number> = {};
|
|
33
|
+
for (const col of columns) {
|
|
34
|
+
widths[col.name] = 150;
|
|
35
|
+
}
|
|
36
|
+
setColumnWidths(widths);
|
|
37
|
+
}
|
|
38
|
+
}, [fieldKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
39
|
+
|
|
40
|
+
const handleResizeStart = useCallback(
|
|
41
|
+
(columnName: string, clientX: number) => {
|
|
42
|
+
lastClientXRef.current = clientX;
|
|
43
|
+
|
|
44
|
+
// Read actual rendered widths from the DOM so that all columns have
|
|
45
|
+
// accurate state before the drag begins. Without this, columns whose
|
|
46
|
+
// state width (150px) differs from their visual width (wider due to
|
|
47
|
+
// table-layout:fixed + w-full proportional distribution) would appear
|
|
48
|
+
// to compress when a sibling column is resized.
|
|
49
|
+
let startWidth = columnWidths[columnName] ?? 150;
|
|
50
|
+
if (tableRef.current) {
|
|
51
|
+
const ths = tableRef.current.querySelectorAll("thead > tr > th");
|
|
52
|
+
const actualWidths: Record<string, number> = {};
|
|
53
|
+
columns.forEach((col, i) => {
|
|
54
|
+
if (ths[i]) {
|
|
55
|
+
actualWidths[col.name] = Math.round(
|
|
56
|
+
ths[i].getBoundingClientRect().width,
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
actualWidths[col.name] = columnWidths[col.name] ?? 150;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
startWidth = actualWidths[columnName] ?? startWidth;
|
|
63
|
+
setColumnWidths(actualWidths);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setResizingColumn({
|
|
67
|
+
name: columnName,
|
|
68
|
+
startX: clientX,
|
|
69
|
+
startWidth,
|
|
70
|
+
startScrollLeft: scrollRef.current?.scrollLeft ?? 0,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
[columnWidths, scrollRef, columns, tableRef],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!resizingColumn) return;
|
|
78
|
+
|
|
79
|
+
const colIndex = columns.findIndex((f) => f.name === resizingColumn.name);
|
|
80
|
+
const colEl =
|
|
81
|
+
colIndex >= 0
|
|
82
|
+
? tableRef.current?.querySelector<HTMLElement>(
|
|
83
|
+
`colgroup > col:nth-child(${colIndex + 1})`,
|
|
84
|
+
)
|
|
85
|
+
: null;
|
|
86
|
+
|
|
87
|
+
const scrollEl = scrollRef.current;
|
|
88
|
+
const EDGE_ZONE = 40;
|
|
89
|
+
const SCROLL_SPEED = 12;
|
|
90
|
+
let rafId = 0;
|
|
91
|
+
let alive = true;
|
|
92
|
+
|
|
93
|
+
const computeWidth = () =>
|
|
94
|
+
Math.max(
|
|
95
|
+
50,
|
|
96
|
+
resizingColumn.startWidth +
|
|
97
|
+
(lastClientXRef.current - resizingColumn.startX) +
|
|
98
|
+
((scrollEl?.scrollLeft ?? 0) - resizingColumn.startScrollLeft),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const tick = () => {
|
|
102
|
+
if (!alive) return;
|
|
103
|
+
if (scrollEl) {
|
|
104
|
+
const rect = scrollEl.getBoundingClientRect();
|
|
105
|
+
const x = lastClientXRef.current;
|
|
106
|
+
if (x > rect.right - EDGE_ZONE) {
|
|
107
|
+
scrollEl.scrollLeft += SCROLL_SPEED;
|
|
108
|
+
} else if (x < rect.left + EDGE_ZONE) {
|
|
109
|
+
scrollEl.scrollLeft -= SCROLL_SPEED;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (colEl) {
|
|
113
|
+
colEl.style.width = `${computeWidth()}px`;
|
|
114
|
+
}
|
|
115
|
+
rafId = requestAnimationFrame(tick);
|
|
116
|
+
};
|
|
117
|
+
rafId = requestAnimationFrame(tick);
|
|
118
|
+
|
|
119
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
120
|
+
lastClientXRef.current = e.clientX;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleMouseUp = () => {
|
|
124
|
+
alive = false;
|
|
125
|
+
cancelAnimationFrame(rafId);
|
|
126
|
+
setColumnWidths((prev) => ({
|
|
127
|
+
...prev,
|
|
128
|
+
[resizingColumn.name]: computeWidth(),
|
|
129
|
+
}));
|
|
130
|
+
justResizedRef.current = true;
|
|
131
|
+
requestAnimationFrame(() => {
|
|
132
|
+
justResizedRef.current = false;
|
|
133
|
+
});
|
|
134
|
+
setResizingColumn(null);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
138
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
139
|
+
return () => {
|
|
140
|
+
alive = false;
|
|
141
|
+
cancelAnimationFrame(rafId);
|
|
142
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
143
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
144
|
+
};
|
|
145
|
+
}, [resizingColumn, columns, scrollRef, tableRef]);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
columnWidths,
|
|
149
|
+
resizingColumn,
|
|
150
|
+
justResizedRef,
|
|
151
|
+
handleResizeStart,
|
|
152
|
+
};
|
|
153
|
+
}
|