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,318 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import type { ShortcutAction } from "../types";
|
|
3
|
+
import { useHotkey, formatShortcutDisplay } from "../stores/hooks";
|
|
4
|
+
import { useStore } from "../stores";
|
|
5
|
+
|
|
6
|
+
// Human-readable labels for each shortcut action
|
|
7
|
+
const SHORTCUT_LABELS: Record<ShortcutAction, string> = {
|
|
8
|
+
newConsole: "New Console",
|
|
9
|
+
closeInnerTab: "Close Tab",
|
|
10
|
+
nextInnerTab: "Next Tab",
|
|
11
|
+
prevInnerTab: "Previous Tab",
|
|
12
|
+
newConnectionTab: "New Connection Tab",
|
|
13
|
+
closeConnectionTab: "Close Connection Tab",
|
|
14
|
+
nextConnectionTab: "Next Connection",
|
|
15
|
+
prevConnectionTab: "Previous Connection",
|
|
16
|
+
runQuery: "Run Query",
|
|
17
|
+
closeModal: "Close Modal",
|
|
18
|
+
openTableSwitcher: "Switch Table",
|
|
19
|
+
openDatabaseSwitcher: "Switch Database",
|
|
20
|
+
deleteRows: "Delete Rows",
|
|
21
|
+
selectAll: "Select All",
|
|
22
|
+
refreshTable: "Refresh Table",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Default shortcuts (keep in sync with store.ts BROWSER_SHORTCUTS)
|
|
26
|
+
const DEFAULT_SHORTCUTS: Record<ShortcutAction, string> = {
|
|
27
|
+
newConsole: "alt+t",
|
|
28
|
+
closeInnerTab: "alt+w",
|
|
29
|
+
nextInnerTab: "alt+tab",
|
|
30
|
+
prevInnerTab: "alt+shift+tab",
|
|
31
|
+
newConnectionTab: "mod+alt+n",
|
|
32
|
+
closeConnectionTab: "mod+alt+w",
|
|
33
|
+
prevConnectionTab: "mod+alt+j",
|
|
34
|
+
nextConnectionTab: "mod+alt+k",
|
|
35
|
+
runQuery: "ctrl+enter",
|
|
36
|
+
closeModal: "escape",
|
|
37
|
+
openTableSwitcher: "mod+o",
|
|
38
|
+
openDatabaseSwitcher: "mod+p",
|
|
39
|
+
deleteRows: "delete",
|
|
40
|
+
selectAll: "mod+a",
|
|
41
|
+
refreshTable: "mod+r",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ALL_ACTIONS: ShortcutAction[] = [
|
|
45
|
+
"newConsole",
|
|
46
|
+
"closeInnerTab",
|
|
47
|
+
"nextInnerTab",
|
|
48
|
+
"prevInnerTab",
|
|
49
|
+
"newConnectionTab",
|
|
50
|
+
"closeConnectionTab",
|
|
51
|
+
"nextConnectionTab",
|
|
52
|
+
"prevConnectionTab",
|
|
53
|
+
"runQuery",
|
|
54
|
+
"closeModal",
|
|
55
|
+
"openTableSwitcher",
|
|
56
|
+
"openDatabaseSwitcher",
|
|
57
|
+
"deleteRows",
|
|
58
|
+
"selectAll",
|
|
59
|
+
"refreshTable",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
interface ShortcutSettingsModalProps {
|
|
63
|
+
onClose: () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ShortcutRowProps {
|
|
67
|
+
action: ShortcutAction;
|
|
68
|
+
currentShortcut: string;
|
|
69
|
+
isCustom: boolean;
|
|
70
|
+
onEdit: (action: ShortcutAction) => void;
|
|
71
|
+
onReset: (action: ShortcutAction) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ShortcutRow({
|
|
75
|
+
action,
|
|
76
|
+
currentShortcut,
|
|
77
|
+
isCustom,
|
|
78
|
+
onEdit,
|
|
79
|
+
onReset,
|
|
80
|
+
}: ShortcutRowProps) {
|
|
81
|
+
const defaultShortcut = DEFAULT_SHORTCUTS[action];
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex items-center justify-between py-2.5 border-b border-stone-100 dark:border-white/5 last:border-b-0">
|
|
85
|
+
<div className="flex-1">
|
|
86
|
+
<div className="text-[14px] text-primary font-medium">
|
|
87
|
+
{SHORTCUT_LABELS[action]}
|
|
88
|
+
</div>
|
|
89
|
+
{isCustom && (
|
|
90
|
+
<div className="text-[11px] text-tertiary mt-0.5">
|
|
91
|
+
Default: {formatShortcutDisplay(defaultShortcut)}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => onEdit(action)}
|
|
98
|
+
className={`px-3 py-1.5 text-[13px] font-mono rounded-md transition-colors ${
|
|
99
|
+
isCustom
|
|
100
|
+
? "bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-500/20"
|
|
101
|
+
: "bg-stone-100 dark:bg-white/5 text-secondary hover:bg-stone-200 dark:hover:bg-white/10"
|
|
102
|
+
}`}
|
|
103
|
+
>
|
|
104
|
+
{formatShortcutDisplay(currentShortcut)}
|
|
105
|
+
</button>
|
|
106
|
+
{isCustom && (
|
|
107
|
+
<button
|
|
108
|
+
onClick={() => onReset(action)}
|
|
109
|
+
className="p-1.5 text-tertiary hover:text-secondary transition-colors"
|
|
110
|
+
title="Reset to default"
|
|
111
|
+
>
|
|
112
|
+
<svg
|
|
113
|
+
className="w-4 h-4"
|
|
114
|
+
fill="none"
|
|
115
|
+
viewBox="0 0 24 24"
|
|
116
|
+
stroke="currentColor"
|
|
117
|
+
strokeWidth={2}
|
|
118
|
+
>
|
|
119
|
+
<path
|
|
120
|
+
strokeLinecap="round"
|
|
121
|
+
strokeLinejoin="round"
|
|
122
|
+
d="M6 18L18 6M6 6l12 12"
|
|
123
|
+
/>
|
|
124
|
+
</svg>
|
|
125
|
+
</button>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface RecordingOverlayProps {
|
|
133
|
+
action: ShortcutAction;
|
|
134
|
+
onCancel: () => void;
|
|
135
|
+
onSave: (keys: string) => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function RecordingOverlay({ action, onCancel, onSave }: RecordingOverlayProps) {
|
|
139
|
+
const [recordedKeys, setRecordedKeys] = useState<string | null>(null);
|
|
140
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
|
|
147
|
+
// Ignore lone modifier keys
|
|
148
|
+
if (["Control", "Alt", "Shift", "Meta"].includes(e.key)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build the shortcut string
|
|
153
|
+
const parts: string[] = [];
|
|
154
|
+
if (e.ctrlKey || e.metaKey) parts.push("ctrl");
|
|
155
|
+
if (e.altKey) parts.push("alt");
|
|
156
|
+
if (e.shiftKey) parts.push("shift");
|
|
157
|
+
|
|
158
|
+
// Handle special keys — use e.code for Alt+letter on macOS
|
|
159
|
+
let key = e.key.toLowerCase();
|
|
160
|
+
if (e.altKey && e.code.startsWith("Key")) {
|
|
161
|
+
key = e.code.slice(3).toLowerCase();
|
|
162
|
+
}
|
|
163
|
+
if (key === " ") key = "space";
|
|
164
|
+
if (key === "escape") {
|
|
165
|
+
// If pressing escape with no modifiers, cancel
|
|
166
|
+
if (parts.length === 0) {
|
|
167
|
+
onCancel();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
parts.push(key);
|
|
173
|
+
setRecordedKeys(parts.join("+"));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
177
|
+
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
|
178
|
+
}, [onCancel]);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div
|
|
182
|
+
ref={overlayRef}
|
|
183
|
+
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
184
|
+
>
|
|
185
|
+
<div className="bg-white dark:bg-[#1a1a1a] rounded-xl shadow-2xl p-6 w-full max-w-sm mx-4 border border-stone-200 dark:border-white/10">
|
|
186
|
+
<h3 className="text-[16px] font-semibold text-primary mb-2">
|
|
187
|
+
Set shortcut for "{SHORTCUT_LABELS[action]}"
|
|
188
|
+
</h3>
|
|
189
|
+
<p className="text-[13px] text-secondary mb-6">
|
|
190
|
+
Press the key combination you want to use, or Esc to cancel.
|
|
191
|
+
</p>
|
|
192
|
+
|
|
193
|
+
<div className="flex items-center justify-center py-8 mb-6 bg-stone-50 dark:bg-white/5 rounded-lg border border-stone-200 dark:border-white/10">
|
|
194
|
+
{recordedKeys ? (
|
|
195
|
+
<span className="text-[20px] font-mono text-primary">
|
|
196
|
+
{formatShortcutDisplay(recordedKeys)}
|
|
197
|
+
</span>
|
|
198
|
+
) : (
|
|
199
|
+
<span className="text-[14px] text-tertiary">
|
|
200
|
+
Waiting for input...
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="flex gap-3">
|
|
206
|
+
<button
|
|
207
|
+
onClick={onCancel}
|
|
208
|
+
className="flex-1 px-4 py-2.5 text-[14px] font-medium text-secondary bg-stone-100 dark:bg-white/5 hover:bg-stone-200 dark:hover:bg-white/10 rounded-lg transition-colors"
|
|
209
|
+
>
|
|
210
|
+
Cancel
|
|
211
|
+
</button>
|
|
212
|
+
<button
|
|
213
|
+
onClick={() => recordedKeys && onSave(recordedKeys)}
|
|
214
|
+
disabled={!recordedKeys}
|
|
215
|
+
className="flex-1 px-4 py-2.5 text-[14px] font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors"
|
|
216
|
+
>
|
|
217
|
+
Save
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function ShortcutSettingsModal({ onClose }: ShortcutSettingsModalProps) {
|
|
226
|
+
const shortcutOverrides = useStore((s) => s.shortcutOverrides);
|
|
227
|
+
const setShortcut = useStore((s) => s.setShortcut);
|
|
228
|
+
const resetShortcut = useStore((s) => s.resetShortcut);
|
|
229
|
+
const resetAllShortcuts = useStore((s) => s.resetAllShortcuts);
|
|
230
|
+
|
|
231
|
+
const [editingAction, setEditingAction] = useState<ShortcutAction | null>(
|
|
232
|
+
null,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Only use escape to close when not recording a shortcut
|
|
236
|
+
useHotkey("closeModal", () => {
|
|
237
|
+
if (!editingAction) {
|
|
238
|
+
onClose();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
function getCurrentShortcut(action: ShortcutAction): string {
|
|
243
|
+
return shortcutOverrides[action] ?? DEFAULT_SHORTCUTS[action];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isCustom(action: ShortcutAction): boolean {
|
|
247
|
+
return action in shortcutOverrides;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function handleSaveShortcut(keys: string) {
|
|
251
|
+
if (editingAction) {
|
|
252
|
+
setShortcut(editingAction, keys);
|
|
253
|
+
setEditingAction(null);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const hasAnyCustom = ALL_ACTIONS.some((action) => isCustom(action));
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<>
|
|
261
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
262
|
+
<div
|
|
263
|
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
264
|
+
onClick={onClose}
|
|
265
|
+
/>
|
|
266
|
+
<div className="relative bg-white dark:bg-[#1a1a1a] rounded-xl shadow-2xl w-full max-w-md mx-4 border border-stone-200 dark:border-white/10 max-h-[80vh] flex flex-col">
|
|
267
|
+
<div className="p-6 pb-0 shrink-0">
|
|
268
|
+
<div className="flex items-center justify-between mb-6">
|
|
269
|
+
<h2 className="text-[18px] font-semibold text-primary">
|
|
270
|
+
Keyboard Shortcuts
|
|
271
|
+
</h2>
|
|
272
|
+
{hasAnyCustom && (
|
|
273
|
+
<button
|
|
274
|
+
onClick={resetAllShortcuts}
|
|
275
|
+
className="text-[12px] text-tertiary hover:text-secondary transition-colors"
|
|
276
|
+
>
|
|
277
|
+
Reset All
|
|
278
|
+
</button>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="px-6 pb-4 overflow-y-auto min-h-0">
|
|
284
|
+
<div className="divide-y divide-stone-100 dark:divide-white/5">
|
|
285
|
+
{ALL_ACTIONS.map((action) => (
|
|
286
|
+
<ShortcutRow
|
|
287
|
+
key={action}
|
|
288
|
+
action={action}
|
|
289
|
+
currentShortcut={getCurrentShortcut(action)}
|
|
290
|
+
isCustom={isCustom(action)}
|
|
291
|
+
onEdit={setEditingAction}
|
|
292
|
+
onReset={resetShortcut}
|
|
293
|
+
/>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div className="p-6 pt-4 border-t border-stone-200 dark:border-white/10 shrink-0">
|
|
299
|
+
<button
|
|
300
|
+
onClick={onClose}
|
|
301
|
+
className="w-full px-4 py-2.5 text-[14px] font-medium text-secondary bg-stone-100 dark:bg-white/5 hover:bg-stone-200 dark:hover:bg-white/10 rounded-lg transition-colors"
|
|
302
|
+
>
|
|
303
|
+
Done
|
|
304
|
+
</button>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{editingAction && (
|
|
310
|
+
<RecordingOverlay
|
|
311
|
+
action={editingAction}
|
|
312
|
+
onCancel={() => setEditingAction(null)}
|
|
313
|
+
onSave={handleSaveShortcut}
|
|
314
|
+
/>
|
|
315
|
+
)}
|
|
316
|
+
</>
|
|
317
|
+
);
|
|
318
|
+
}
|