rozenite-sqlite 0.0.11 → 0.0.12
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/README.md +1 -1
- package/dist/assets/panel-BKWy6irM.js +58 -0
- package/dist/panel.html +1 -1
- package/dist/react-native.cjs +1 -1
- package/dist/react-native.d.ts +1 -1
- package/dist/react-native.js +59 -22
- package/dist/rozenite.json +1 -1
- package/package.json +2 -2
- package/rozenite.config.ts +1 -1
- package/src/components/DataTable.tsx +34 -37
- package/src/components/Dropdown.tsx +1 -4
- package/src/components/Placeholder.tsx +81 -0
- package/src/components/RowDetailPanel.tsx +81 -30
- package/src/constants.ts +4 -0
- package/src/hooks/useBridgeSync.ts +87 -16
- package/src/hooks/useExplorerState.ts +63 -7
- package/src/hooks/useRozeniteSQLite.ts +60 -1
- package/src/panel.tsx +6 -2
- package/dist/assets/panel-3I5ccfMq.js +0 -58
package/dist/panel.html
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
<script>
|
|
23
23
|
var __ROZENITE_PANEL__ = true;
|
|
24
24
|
</script>
|
|
25
|
-
<script type="module" crossorigin src="./assets/panel-
|
|
25
|
+
<script type="module" crossorigin src="./assets/panel-BKWy6irM.js"></script>
|
|
26
26
|
</head>
|
|
27
27
|
<body>
|
|
28
28
|
<div id="root"></div>
|
package/dist/react-native.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const S=require("react"),f=require("@rozenite/plugin-bridge"),d="rozenite-sqlite",s={GET_DB_LIST:"get-db-list",SEND_DB_LIST:"send-db-list",SQL_EXECUTE:"sql-execute",SQL_EXEC_RESULT:"sql-exec-result",SAVE_ROW:"save-row",DELETE_ROW:"delete-row",CLEAR_TABLE:"clear-table",MUTATION_RESULT:"mutation-result"};function l(n){return n==null?"NULL":typeof n=="number"?String(n):typeof n=="boolean"?n?"1":"0":`'${String(n).replace(/'/g,"''")}'`}function g(n){const e=f.useRozeniteDevToolsClient({pluginId:d}),T=S.useRef(n);S.useEffect(()=>{T.current=n}),S.useEffect(()=>{if(!e)return;const L=[e.onMessage(s.GET_DB_LIST,()=>{e.send(s.SEND_DB_LIST,T.current.databases)}),e.onMessage(s.SQL_EXECUTE,r=>{const{dbName:u,query:o}=r;T.current.sqlExecutor(u,o).then(t=>{e.send(s.SQL_EXEC_RESULT,t)},t=>{e.send(s.SQL_EXEC_RESULT,{error:t instanceof Error?t.message:String(t)})})}),e.onMessage(s.SAVE_ROW,r=>{const{dbName:u,table:o,row:t,primaryKey:E}=r,a=t[E],_=Object.keys(t).filter(c=>c!==E).map(c=>`"${c}" = ${l(t[c])}`).join(", "),R=`UPDATE "${o}" SET ${_} WHERE "${E}" = ${l(a)}`;T.current.sqlExecutor(u,R).then(()=>e.send(s.MUTATION_RESULT,{success:!0}),c=>e.send(s.MUTATION_RESULT,{success:!1,error:c instanceof Error?c.message:String(c)}))}),e.onMessage(s.DELETE_ROW,r=>{const{dbName:u,table:o,primaryKey:t,primaryKeyValue:E}=r,a=`DELETE FROM "${o}" WHERE "${t}" = ${l(E)}`;T.current.sqlExecutor(u,a).then(()=>e.send(s.MUTATION_RESULT,{success:!0}),i=>e.send(s.MUTATION_RESULT,{success:!1,error:i instanceof Error?i.message:String(i)}))}),e.onMessage(s.CLEAR_TABLE,r=>{const{dbName:u,table:o}=r,t=`DELETE FROM "${o}"`;T.current.sqlExecutor(u,t).then(()=>e.send(s.MUTATION_RESULT,{success:!0}),E=>e.send(s.MUTATION_RESULT,{success:!1,error:E instanceof Error?E.message:String(E)}))})];return()=>{L.forEach(r=>r.remove())}},[e])}exports.useRozeniteSQLite=g;
|
package/dist/react-native.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare interface RozeniteSQLiteConfig {
|
|
|
8
8
|
export declare type SQLExecutor = (dbName: string, query: string) => Promise<Record<string, unknown>[]>;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Connects your React Native app to the
|
|
11
|
+
* Connects your React Native app to the SQLighter devtools panel.
|
|
12
12
|
*
|
|
13
13
|
* Call this once somewhere near the root of your app (or in the component
|
|
14
14
|
* that holds the database instances). It handles all devtools communication —
|
package/dist/react-native.js
CHANGED
|
@@ -1,40 +1,77 @@
|
|
|
1
|
-
import { useRef as
|
|
2
|
-
import { useRozeniteDevToolsClient as
|
|
3
|
-
const
|
|
1
|
+
import { useRef as f, useEffect as L } from "react";
|
|
2
|
+
import { useRozeniteDevToolsClient as U } from "@rozenite/plugin-bridge";
|
|
3
|
+
const m = "rozenite-sqlite", s = {
|
|
4
4
|
GET_DB_LIST: "get-db-list",
|
|
5
5
|
SEND_DB_LIST: "send-db-list",
|
|
6
6
|
SQL_EXECUTE: "sql-execute",
|
|
7
|
-
SQL_EXEC_RESULT: "sql-exec-result"
|
|
7
|
+
SQL_EXEC_RESULT: "sql-exec-result",
|
|
8
|
+
SAVE_ROW: "save-row",
|
|
9
|
+
DELETE_ROW: "delete-row",
|
|
10
|
+
CLEAR_TABLE: "clear-table",
|
|
11
|
+
MUTATION_RESULT: "mutation-result"
|
|
8
12
|
};
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}),
|
|
13
|
+
function S(n) {
|
|
14
|
+
return n == null ? "NULL" : typeof n == "number" ? String(n) : typeof n == "boolean" ? n ? "1" : "0" : `'${String(n).replace(/'/g, "''")}'`;
|
|
15
|
+
}
|
|
16
|
+
function N(n) {
|
|
17
|
+
const e = U({ pluginId: m }), T = f(n);
|
|
18
|
+
L(() => {
|
|
19
|
+
T.current = n;
|
|
20
|
+
}), L(() => {
|
|
14
21
|
if (!e) return;
|
|
15
|
-
const
|
|
16
|
-
e.onMessage(
|
|
17
|
-
e.send(
|
|
22
|
+
const _ = [
|
|
23
|
+
e.onMessage(s.GET_DB_LIST, () => {
|
|
24
|
+
e.send(s.SEND_DB_LIST, T.current.databases);
|
|
18
25
|
}),
|
|
19
|
-
e.onMessage(
|
|
20
|
-
const { dbName:
|
|
21
|
-
|
|
22
|
-
(
|
|
23
|
-
e.send(
|
|
26
|
+
e.onMessage(s.SQL_EXECUTE, (r) => {
|
|
27
|
+
const { dbName: o, query: u } = r;
|
|
28
|
+
T.current.sqlExecutor(o, u).then(
|
|
29
|
+
(t) => {
|
|
30
|
+
e.send(s.SQL_EXEC_RESULT, t);
|
|
24
31
|
},
|
|
25
|
-
(
|
|
26
|
-
e.send(
|
|
27
|
-
error:
|
|
32
|
+
(t) => {
|
|
33
|
+
e.send(s.SQL_EXEC_RESULT, {
|
|
34
|
+
error: t instanceof Error ? t.message : String(t)
|
|
28
35
|
});
|
|
29
36
|
}
|
|
30
37
|
);
|
|
38
|
+
}),
|
|
39
|
+
e.onMessage(s.SAVE_ROW, (r) => {
|
|
40
|
+
const { dbName: o, table: u, row: t, primaryKey: E } = r, i = t[E], l = Object.keys(t).filter((c) => c !== E).map((c) => `"${c}" = ${S(t[c])}`).join(", "), R = `UPDATE "${u}" SET ${l} WHERE "${E}" = ${S(i)}`;
|
|
41
|
+
T.current.sqlExecutor(o, R).then(
|
|
42
|
+
() => e.send(s.MUTATION_RESULT, { success: !0 }),
|
|
43
|
+
(c) => e.send(s.MUTATION_RESULT, {
|
|
44
|
+
success: !1,
|
|
45
|
+
error: c instanceof Error ? c.message : String(c)
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}),
|
|
49
|
+
e.onMessage(s.DELETE_ROW, (r) => {
|
|
50
|
+
const { dbName: o, table: u, primaryKey: t, primaryKeyValue: E } = r, i = `DELETE FROM "${u}" WHERE "${t}" = ${S(E)}`;
|
|
51
|
+
T.current.sqlExecutor(o, i).then(
|
|
52
|
+
() => e.send(s.MUTATION_RESULT, { success: !0 }),
|
|
53
|
+
(a) => e.send(s.MUTATION_RESULT, {
|
|
54
|
+
success: !1,
|
|
55
|
+
error: a instanceof Error ? a.message : String(a)
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}),
|
|
59
|
+
e.onMessage(s.CLEAR_TABLE, (r) => {
|
|
60
|
+
const { dbName: o, table: u } = r, t = `DELETE FROM "${u}"`;
|
|
61
|
+
T.current.sqlExecutor(o, t).then(
|
|
62
|
+
() => e.send(s.MUTATION_RESULT, { success: !0 }),
|
|
63
|
+
(E) => e.send(s.MUTATION_RESULT, {
|
|
64
|
+
success: !1,
|
|
65
|
+
error: E instanceof Error ? E.message : String(E)
|
|
66
|
+
})
|
|
67
|
+
);
|
|
31
68
|
})
|
|
32
69
|
];
|
|
33
70
|
return () => {
|
|
34
|
-
|
|
71
|
+
_.forEach((r) => r.remove());
|
|
35
72
|
};
|
|
36
73
|
}, [e]);
|
|
37
74
|
}
|
|
38
75
|
export {
|
|
39
|
-
|
|
76
|
+
N as useRozeniteSQLite
|
|
40
77
|
};
|
package/dist/rozenite.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"rozenite-sqlite","version":"0.0.
|
|
1
|
+
{"name":"rozenite-sqlite","version":"0.0.12","description":"SQLighter - SQLite explorer for React Native devtools","panels":[{"name":"SQLighter","source":"/panel.html"}]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rozenite-sqlite",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "SQLite explorer for
|
|
3
|
+
"version": "0.0.12",
|
|
4
|
+
"description": "SQLighter - SQLite explorer for React Native devtools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react-native.js",
|
|
7
7
|
"module": "./dist/react-native.js",
|
package/rozenite.config.ts
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
ScrollView,
|
|
7
7
|
Pressable,
|
|
8
8
|
TextInput,
|
|
9
|
-
ActivityIndicator,
|
|
10
9
|
} from 'react-native';
|
|
11
10
|
import { C, type RowData } from '../theme';
|
|
12
11
|
import Pagination from './Pagination';
|
|
12
|
+
import { Placeholder } from './Placeholder';
|
|
13
13
|
import type { ExplorerStatus } from '../hooks/useExplorerState';
|
|
14
14
|
|
|
15
15
|
export interface DataTableProps {
|
|
@@ -20,13 +20,14 @@ export interface DataTableProps {
|
|
|
20
20
|
status: ExplorerStatus;
|
|
21
21
|
error: string | null;
|
|
22
22
|
onClearTable?: () => void;
|
|
23
|
+
onReconnect?: () => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const COL_WIDTH = 160;
|
|
26
27
|
const IDX_WIDTH = 44;
|
|
27
28
|
const HEADER_HEIGHT = 42;
|
|
28
29
|
|
|
29
|
-
export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status, error, onClearTable }: DataTableProps) {
|
|
30
|
+
export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status, error, onClearTable, onReconnect }: DataTableProps) {
|
|
30
31
|
const [sortCol, setSortCol] = useState<string | null>(null);
|
|
31
32
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
|
32
33
|
const [search, setSearch] = useState<Record<string, string>>({});
|
|
@@ -152,52 +153,56 @@ export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status
|
|
|
152
153
|
|
|
153
154
|
if (status === 'connecting') {
|
|
154
155
|
return (
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
<Placeholder
|
|
157
|
+
emoji="📡"
|
|
158
|
+
title="Waiting for app…"
|
|
159
|
+
subtitle="Make sure your app is running and useSQLighter is initialized"
|
|
160
|
+
buttonText={onReconnect ? "↻ Reconnect" : undefined}
|
|
161
|
+
onButtonPress={onReconnect}
|
|
162
|
+
/>
|
|
162
163
|
);
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
if (status === 'error') {
|
|
166
167
|
return (
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
<Placeholder
|
|
169
|
+
emoji="⚠️"
|
|
170
|
+
title="Query failed"
|
|
171
|
+
subtitle={error ?? undefined}
|
|
172
|
+
buttonText={onReconnect ? "↻ Try again" : undefined}
|
|
173
|
+
onButtonPress={onReconnect}
|
|
174
|
+
/>
|
|
172
175
|
);
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
if (status === 'loadingTables' || status === 'loadingData') {
|
|
176
179
|
return (
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
<Placeholder
|
|
181
|
+
loading
|
|
182
|
+
title="Loading…"
|
|
183
|
+
buttonText={onReconnect ? "↻ Refresh" : undefined}
|
|
184
|
+
onButtonPress={onReconnect}
|
|
185
|
+
/>
|
|
181
186
|
);
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
if (columns.length === 0) {
|
|
185
190
|
return (
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
<Placeholder
|
|
192
|
+
emoji="🗄️"
|
|
193
|
+
title="No data to display"
|
|
194
|
+
subtitle="Select a database and table above"
|
|
195
|
+
/>
|
|
191
196
|
);
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
if (rows.length === 0) {
|
|
195
200
|
return (
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
<Placeholder
|
|
202
|
+
emoji="📭"
|
|
203
|
+
title="Table is empty"
|
|
204
|
+
subtitle="This table has no rows yet"
|
|
205
|
+
/>
|
|
201
206
|
);
|
|
202
207
|
}
|
|
203
208
|
|
|
@@ -436,10 +441,7 @@ const s = StyleSheet.create({
|
|
|
436
441
|
paddingHorizontal: 10,
|
|
437
442
|
paddingVertical: 7,
|
|
438
443
|
zIndex: 100,
|
|
439
|
-
|
|
440
|
-
shadowOffset: { width: 0, height: 6 },
|
|
441
|
-
shadowOpacity: 0.5,
|
|
442
|
-
shadowRadius: 16,
|
|
444
|
+
boxShadow: '0 6px 16px rgba(0,0,0,0.5)',
|
|
443
445
|
},
|
|
444
446
|
filterInput: {
|
|
445
447
|
flex: 1,
|
|
@@ -476,11 +478,6 @@ const s = StyleSheet.create({
|
|
|
476
478
|
idxText: { fontSize: 11, color: C.textMuted, fontWeight: '600', textAlign: 'center' },
|
|
477
479
|
noResults: { paddingVertical: 32, alignItems: 'center' },
|
|
478
480
|
noResultsText: { fontSize: 13, color: C.textMuted, fontStyle: 'italic' },
|
|
479
|
-
placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 60 },
|
|
480
|
-
placeholderEmoji: { fontSize: 36, marginBottom: 16 },
|
|
481
|
-
placeholderTitle: { fontSize: 15, fontWeight: '600', color: C.textSecondary, marginBottom: 6 },
|
|
482
|
-
placeholderSub: { fontSize: 13, color: C.textMuted },
|
|
483
|
-
placeholderText: { fontSize: 14, color: C.textSecondary, marginTop: 14 },
|
|
484
481
|
resizeHandle: {
|
|
485
482
|
position: 'absolute',
|
|
486
483
|
right: 0,
|
|
@@ -117,10 +117,7 @@ const s = StyleSheet.create({
|
|
|
117
117
|
borderColor: C.border,
|
|
118
118
|
borderRadius: 8,
|
|
119
119
|
overflow: 'hidden',
|
|
120
|
-
|
|
121
|
-
shadowOffset: { width: 0, height: 8 },
|
|
122
|
-
shadowOpacity: 0.5,
|
|
123
|
-
shadowRadius: 20,
|
|
120
|
+
boxShadow: '0 8px 20px rgba(0,0,0,0.5)',
|
|
124
121
|
},
|
|
125
122
|
menuScroll: { maxHeight: 200 },
|
|
126
123
|
menuEmpty: { padding: 14, fontSize: 12, color: C.textMuted, textAlign: 'center' },
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, Pressable, ActivityIndicator } from 'react-native';
|
|
3
|
+
import { C } from '../theme';
|
|
4
|
+
|
|
5
|
+
export interface PlaceholderProps {
|
|
6
|
+
emoji?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
buttonText?: string;
|
|
11
|
+
onButtonPress?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Placeholder({
|
|
15
|
+
emoji,
|
|
16
|
+
title,
|
|
17
|
+
subtitle,
|
|
18
|
+
loading,
|
|
19
|
+
buttonText,
|
|
20
|
+
onButtonPress,
|
|
21
|
+
}: PlaceholderProps) {
|
|
22
|
+
return (
|
|
23
|
+
<View style={s.container}>
|
|
24
|
+
{loading ? (
|
|
25
|
+
<ActivityIndicator size="large" color={C.accent} />
|
|
26
|
+
) : emoji ? (
|
|
27
|
+
<Text style={s.emoji}>{emoji}</Text>
|
|
28
|
+
) : null}
|
|
29
|
+
{title && <Text style={s.title}>{title}</Text>}
|
|
30
|
+
{subtitle && <Text style={s.subtitle}>{subtitle}</Text>}
|
|
31
|
+
{buttonText && onButtonPress && (
|
|
32
|
+
<Pressable
|
|
33
|
+
style={({ pressed }) => [s.button, pressed && s.buttonPressed]}
|
|
34
|
+
onPress={onButtonPress}
|
|
35
|
+
>
|
|
36
|
+
<Text style={s.buttonText}>{buttonText}</Text>
|
|
37
|
+
</Pressable>
|
|
38
|
+
)}
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const s = StyleSheet.create({
|
|
44
|
+
container: {
|
|
45
|
+
flex: 1,
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
padding: 60,
|
|
49
|
+
},
|
|
50
|
+
emoji: {
|
|
51
|
+
fontSize: 36,
|
|
52
|
+
marginBottom: 16,
|
|
53
|
+
},
|
|
54
|
+
title: {
|
|
55
|
+
fontSize: 15,
|
|
56
|
+
fontWeight: '600',
|
|
57
|
+
color: C.textSecondary,
|
|
58
|
+
marginBottom: 6,
|
|
59
|
+
marginTop: 14,
|
|
60
|
+
},
|
|
61
|
+
subtitle: {
|
|
62
|
+
fontSize: 13,
|
|
63
|
+
color: C.textMuted,
|
|
64
|
+
textAlign: 'center',
|
|
65
|
+
},
|
|
66
|
+
button: {
|
|
67
|
+
marginTop: 20,
|
|
68
|
+
paddingHorizontal: 16,
|
|
69
|
+
paddingVertical: 10,
|
|
70
|
+
backgroundColor: C.accent,
|
|
71
|
+
borderRadius: 6,
|
|
72
|
+
},
|
|
73
|
+
buttonPressed: {
|
|
74
|
+
opacity: 0.7,
|
|
75
|
+
},
|
|
76
|
+
buttonText: {
|
|
77
|
+
fontSize: 13,
|
|
78
|
+
fontWeight: '600',
|
|
79
|
+
color: '#fff',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, ScrollView, Pressable, TextInput } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, ActivityIndicator } from 'react-native';
|
|
3
3
|
import { C, type RowData } from '../theme';
|
|
4
|
+
import type { MutationResult } from '../hooks/useBridgeSync';
|
|
4
5
|
|
|
5
6
|
function isJsonString(val: string): boolean {
|
|
6
7
|
const t = val.trim();
|
|
@@ -12,11 +13,14 @@ export interface RowDetailPanelProps {
|
|
|
12
13
|
row: RowData | null;
|
|
13
14
|
rowIndex: number | null;
|
|
14
15
|
onClose: () => void;
|
|
15
|
-
onSave: (updated: RowData) =>
|
|
16
|
-
onDelete: () =>
|
|
16
|
+
onSave: (updated: RowData) => Promise<MutationResult>;
|
|
17
|
+
onDelete: () => Promise<MutationResult>;
|
|
18
|
+
mutating?: boolean;
|
|
19
|
+
mutationError?: string | null;
|
|
20
|
+
onClearError?: () => void;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: RowDetailPanelProps) {
|
|
23
|
+
export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete, mutating, mutationError, onClearError }: RowDetailPanelProps) {
|
|
20
24
|
const [draft, setDraft] = useState<RowData>({});
|
|
21
25
|
const [dirty, setDirty] = useState(false);
|
|
22
26
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
@@ -45,9 +49,20 @@ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: Row
|
|
|
45
49
|
setDirty(true);
|
|
46
50
|
};
|
|
47
51
|
|
|
48
|
-
const handleSave = () => {
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
const handleSave = async () => {
|
|
53
|
+
onClearError?.();
|
|
54
|
+
const result = await onSave(draft);
|
|
55
|
+
if (result.success) {
|
|
56
|
+
setDirty(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleDelete = async () => {
|
|
61
|
+
onClearError?.();
|
|
62
|
+
const result = await onDelete();
|
|
63
|
+
if (result.success) {
|
|
64
|
+
setConfirmDelete(false);
|
|
65
|
+
}
|
|
51
66
|
};
|
|
52
67
|
|
|
53
68
|
const handleCancel = () => {
|
|
@@ -100,25 +115,41 @@ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: Row
|
|
|
100
115
|
</ScrollView>
|
|
101
116
|
|
|
102
117
|
<View style={s.primaryActions}>
|
|
103
|
-
|
|
104
|
-
style={
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
{mutationError && (
|
|
119
|
+
<View style={s.errorBox}>
|
|
120
|
+
<Text style={s.errorText}>{mutationError}</Text>
|
|
121
|
+
<Pressable onPress={onClearError} style={s.errorClose}>
|
|
122
|
+
<Text style={s.errorCloseText}>✕</Text>
|
|
123
|
+
</Pressable>
|
|
124
|
+
</View>
|
|
125
|
+
)}
|
|
126
|
+
<View style={s.primaryActionsRow}>
|
|
127
|
+
<Pressable
|
|
128
|
+
style={({ pressed }) => [s.btn, s.btnSave, (!dirty || mutating) && s.btnSaveDisabled, pressed && !dirty ? {} : pressed && s.btnPressed]}
|
|
129
|
+
onPress={dirty && !mutating ? handleSave : undefined}
|
|
130
|
+
disabled={!dirty || mutating}
|
|
131
|
+
>
|
|
132
|
+
{mutating ? (
|
|
133
|
+
<ActivityIndicator size="small" color={C.bg} />
|
|
134
|
+
) : (
|
|
135
|
+
<Text style={[s.btnSaveText, !dirty && s.btnSaveTextDisabled]}>Save</Text>
|
|
136
|
+
)}
|
|
137
|
+
</Pressable>
|
|
138
|
+
<Pressable
|
|
139
|
+
style={({ pressed }) => [s.btn, s.btnCancel, pressed && s.btnPressed]}
|
|
140
|
+
onPress={handleCancel}
|
|
141
|
+
>
|
|
142
|
+
<Text style={s.btnCancelText}>Cancel</Text>
|
|
143
|
+
</Pressable>
|
|
144
|
+
</View>
|
|
115
145
|
</View>
|
|
116
146
|
|
|
117
147
|
<View style={s.deleteZone}>
|
|
118
148
|
{!confirmDelete ? (
|
|
119
149
|
<Pressable
|
|
120
|
-
style={({ pressed }) => [s.btnDeleteSmall, pressed && s.btnPressed]}
|
|
121
|
-
onPress={() => setConfirmDelete(true)}
|
|
150
|
+
style={({ pressed }) => [s.btnDeleteSmall, mutating && s.btnSaveDisabled, pressed && s.btnPressed]}
|
|
151
|
+
onPress={() => !mutating && setConfirmDelete(true)}
|
|
152
|
+
disabled={mutating}
|
|
122
153
|
>
|
|
123
154
|
<Text style={s.btnDeleteSmallText}>⊘ Delete this row</Text>
|
|
124
155
|
</Pressable>
|
|
@@ -127,10 +158,15 @@ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: Row
|
|
|
127
158
|
<Text style={s.confirmLabel}>This action cannot be undone.</Text>
|
|
128
159
|
<View style={s.confirmRow}>
|
|
129
160
|
<Pressable
|
|
130
|
-
style={({ pressed }) => [s.btn, s.btnConfirm, { flex: 1 }, pressed && s.btnPressed]}
|
|
131
|
-
onPress={
|
|
161
|
+
style={({ pressed }) => [s.btn, s.btnConfirm, { flex: 1 }, mutating && s.btnSaveDisabled, pressed && s.btnPressed]}
|
|
162
|
+
onPress={!mutating ? handleDelete : undefined}
|
|
163
|
+
disabled={mutating}
|
|
132
164
|
>
|
|
133
|
-
|
|
165
|
+
{mutating ? (
|
|
166
|
+
<ActivityIndicator size="small" color="#fff" />
|
|
167
|
+
) : (
|
|
168
|
+
<Text style={s.btnConfirmText}>Yes, delete</Text>
|
|
169
|
+
)}
|
|
134
170
|
</Pressable>
|
|
135
171
|
<Pressable
|
|
136
172
|
style={({ pressed }) => [s.btn, s.btnCancelSmall, { flex: 1 }, pressed && s.btnPressed]}
|
|
@@ -153,11 +189,6 @@ const s = StyleSheet.create({
|
|
|
153
189
|
borderLeftWidth: 1,
|
|
154
190
|
borderLeftColor: C.border,
|
|
155
191
|
flexDirection: 'column',
|
|
156
|
-
shadowColor: '#000',
|
|
157
|
-
shadowOffset: { width: -8, height: 0 },
|
|
158
|
-
shadowOpacity: 0.45,
|
|
159
|
-
shadowRadius: 20,
|
|
160
|
-
elevation: 10,
|
|
161
192
|
boxShadow: '-8px 0 24px rgba(0,0,0,0.45)',
|
|
162
193
|
},
|
|
163
194
|
header: {
|
|
@@ -228,7 +259,7 @@ const s = StyleSheet.create({
|
|
|
228
259
|
paddingTop: 10,
|
|
229
260
|
},
|
|
230
261
|
primaryActions: {
|
|
231
|
-
flexDirection: '
|
|
262
|
+
flexDirection: 'column',
|
|
232
263
|
gap: 8,
|
|
233
264
|
paddingHorizontal: 16,
|
|
234
265
|
paddingTop: 14,
|
|
@@ -236,6 +267,10 @@ const s = StyleSheet.create({
|
|
|
236
267
|
borderTopWidth: 1,
|
|
237
268
|
borderTopColor: C.border,
|
|
238
269
|
},
|
|
270
|
+
primaryActionsRow: {
|
|
271
|
+
flexDirection: 'row',
|
|
272
|
+
gap: 8,
|
|
273
|
+
},
|
|
239
274
|
deleteZone: {
|
|
240
275
|
paddingHorizontal: 16,
|
|
241
276
|
paddingBottom: 14,
|
|
@@ -270,4 +305,20 @@ const s = StyleSheet.create({
|
|
|
270
305
|
btnConfirm: { backgroundColor: C.danger },
|
|
271
306
|
btnConfirmText: { fontSize: 12, fontWeight: '600', color: '#fff' },
|
|
272
307
|
btnCancelSmall: { backgroundColor: C.surface2, borderWidth: 1, borderColor: C.border },
|
|
308
|
+
errorBox: {
|
|
309
|
+
flexDirection: 'row',
|
|
310
|
+
alignItems: 'center',
|
|
311
|
+
justifyContent: 'space-between',
|
|
312
|
+
backgroundColor: 'rgba(239,68,68,0.15)',
|
|
313
|
+
borderWidth: 1,
|
|
314
|
+
borderColor: 'rgba(239,68,68,0.3)',
|
|
315
|
+
borderRadius: 6,
|
|
316
|
+
paddingHorizontal: 10,
|
|
317
|
+
paddingVertical: 8,
|
|
318
|
+
marginBottom: 8,
|
|
319
|
+
width: '100%',
|
|
320
|
+
},
|
|
321
|
+
errorText: { flex: 1, fontSize: 12, color: C.danger, fontWeight: '500' },
|
|
322
|
+
errorClose: { marginLeft: 8, padding: 4 },
|
|
323
|
+
errorCloseText: { fontSize: 12, color: C.danger },
|
|
273
324
|
});
|
package/src/constants.ts
CHANGED
|
@@ -5,6 +5,10 @@ export const EVENTS = {
|
|
|
5
5
|
SEND_DB_LIST: 'send-db-list',
|
|
6
6
|
SQL_EXECUTE: 'sql-execute',
|
|
7
7
|
SQL_EXEC_RESULT: 'sql-exec-result',
|
|
8
|
+
SAVE_ROW: 'save-row',
|
|
9
|
+
DELETE_ROW: 'delete-row',
|
|
10
|
+
CLEAR_TABLE: 'clear-table',
|
|
11
|
+
MUTATION_RESULT: 'mutation-result',
|
|
8
12
|
} as const;
|
|
9
13
|
|
|
10
14
|
export const QUERIES = {
|