@rozenite/sqlite-plugin 1.12.0 → 2.0.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/CHANGELOG.md +23 -0
- package/dist/devtools/assets/panel-BarPLQ4-.css +1 -0
- package/dist/devtools/assets/{panel-DjRs5NTl.js → panel-CsA6Ejak.js} +45 -44
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/index.d.ts +2 -2
- package/dist/rozenite.json +1 -1
- package/package.json +9 -13
- package/src/react-native/useSqliteAgentTools.ts +4 -1
- package/src/ui/__tests__/sqlite-drop-mutations.test.ts +135 -0
- package/src/ui/globals.css +12 -0
- package/src/ui/panel.tsx +214 -0
- package/src/ui/sqlite-drop-modal.tsx +219 -0
- package/src/ui/sqlite-drop-mutations.ts +60 -0
- package/dist/devtools/assets/panel-CIU0JBOs.css +0 -1
- package/postcss.config.js +0 -6
package/dist/devtools/panel.html
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
<script>
|
|
23
23
|
var __ROZENITE_PANEL__ = true;
|
|
24
24
|
</script>
|
|
25
|
-
<script type="module" crossorigin src="../devtools/assets/panel-
|
|
26
|
-
<link rel="stylesheet" crossorigin href="../devtools/assets/panel-
|
|
25
|
+
<script type="module" crossorigin src="../devtools/assets/panel-CsA6Ejak.js"></script>
|
|
26
|
+
<link rel="stylesheet" crossorigin href="../devtools/assets/panel-BarPLQ4-.css">
|
|
27
27
|
</head>
|
|
28
28
|
<body>
|
|
29
29
|
<div id="root"></div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RozeniteDevToolsRequestClient } from '@rozenite/plugin-bridge';
|
|
2
2
|
|
|
3
3
|
export declare let classifySqlStatement: typeof classifySqlStatement_2;
|
|
4
4
|
|
|
@@ -203,6 +203,6 @@ declare const statementReturnsRows_2: (statementType: SqliteStatementType) => st
|
|
|
203
203
|
|
|
204
204
|
export declare let useRozeniteSqlitePlugin: typeof useRozeniteSqlitePlugin_2;
|
|
205
205
|
|
|
206
|
-
declare const useRozeniteSqlitePlugin_2: ({ adapters, }: RozeniteSqlitePluginOptions) =>
|
|
206
|
+
declare const useRozeniteSqlitePlugin_2: ({ adapters, }: RozeniteSqlitePluginOptions) => RozeniteDevToolsRequestClient<SqliteEventMap> | null;
|
|
207
207
|
|
|
208
208
|
export { }
|
package/dist/rozenite.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@rozenite/sqlite-plugin","version":"
|
|
1
|
+
{"name":"@rozenite/sqlite-plugin","version":"2.0.0","description":"SQLite inspector for Rozenite.","panels":[{"name":"SQLite","source":"/devtools/panel.html"}]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozenite/sqlite-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "SQLite inspector for Rozenite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react-native/index.cjs",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"url": "https://github.com/callstackincubator/rozenite.git"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@rozenite/agent-bridge": "
|
|
19
|
-
"@rozenite/plugin-bridge": "
|
|
18
|
+
"@rozenite/agent-bridge": "2.0.0",
|
|
19
|
+
"@rozenite/plugin-bridge": "2.0.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@dnd-kit/core": "^6.3.1",
|
|
@@ -31,26 +31,22 @@
|
|
|
31
31
|
"@heroui/react": "^3.0.1",
|
|
32
32
|
"@heroui/styles": "^3.0.1",
|
|
33
33
|
"@lezer/highlight": "^1.2.3",
|
|
34
|
-
"@tailwindcss/postcss": "^4.2.2",
|
|
35
34
|
"@tanstack/react-table": "^8.21.3",
|
|
36
35
|
"@tanstack/react-virtual": "^3.0.0",
|
|
37
|
-
"@types/react": "~19.2.
|
|
38
|
-
"autoprefixer": "^10.4.21",
|
|
36
|
+
"@types/react": "~19.2.3",
|
|
39
37
|
"expo-sqlite": "^55.0.11",
|
|
40
38
|
"lucide-react": "^0.263.1",
|
|
41
|
-
"
|
|
42
|
-
"react": "19.2.
|
|
43
|
-
"react-dom": "19.2.0",
|
|
39
|
+
"react": "19.2.3",
|
|
40
|
+
"react-dom": "19.2.3",
|
|
44
41
|
"react-json-tree": "^0.20.0",
|
|
45
|
-
"react-native": "0.
|
|
42
|
+
"react-native": "0.86.0",
|
|
46
43
|
"react-native-web": "^0.21.2",
|
|
47
44
|
"sql-formatter": "^15.7.2",
|
|
48
|
-
"tailwindcss": "^4.2.2",
|
|
49
45
|
"typescript": "~5.9.3",
|
|
50
46
|
"vite": "^7.3.1",
|
|
51
47
|
"vitest": "^3.2.4",
|
|
52
|
-
"@rozenite/vite-plugin": "
|
|
53
|
-
"rozenite": "
|
|
48
|
+
"@rozenite/vite-plugin": "2.0.0",
|
|
49
|
+
"rozenite": "2.0.0"
|
|
54
50
|
},
|
|
55
51
|
"peerDependencies": {
|
|
56
52
|
"expo-sqlite": "*",
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useRozenitePluginAgentTool,
|
|
4
|
+
type AgentTool,
|
|
5
|
+
} from '@rozenite/agent-bridge';
|
|
3
6
|
import { formatSqliteError } from '../shared/bridge-values';
|
|
4
7
|
import { normalizeSingleStatementSql, splitSqlStatements } from '../shared/sql';
|
|
5
8
|
import type { SqliteExecuteStatementsError } from '../shared/types';
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildDropAllEntitiesSql,
|
|
4
|
+
buildDropEntitySql,
|
|
5
|
+
buildSetForeignKeysSql,
|
|
6
|
+
isForeignKeysEnabled,
|
|
7
|
+
orderEntitiesForDrop,
|
|
8
|
+
SQLITE_READ_FOREIGN_KEYS_SQL,
|
|
9
|
+
type SqliteDropTarget,
|
|
10
|
+
} from '../sqlite-drop-mutations';
|
|
11
|
+
|
|
12
|
+
describe('buildDropEntitySql', () => {
|
|
13
|
+
it('builds a DROP TABLE statement for a table entity', () => {
|
|
14
|
+
expect(
|
|
15
|
+
buildDropEntitySql({
|
|
16
|
+
schemaName: 'main',
|
|
17
|
+
name: 'projects',
|
|
18
|
+
type: 'table',
|
|
19
|
+
}),
|
|
20
|
+
).toBe('DROP TABLE "main"."projects"');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('builds a DROP VIEW statement for a view entity', () => {
|
|
24
|
+
expect(
|
|
25
|
+
buildDropEntitySql({
|
|
26
|
+
schemaName: 'main',
|
|
27
|
+
name: 'active_projects',
|
|
28
|
+
type: 'view',
|
|
29
|
+
}),
|
|
30
|
+
).toBe('DROP VIEW "main"."active_projects"');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('quotes identifiers containing double quotes by doubling them', () => {
|
|
34
|
+
expect(
|
|
35
|
+
buildDropEntitySql({
|
|
36
|
+
schemaName: 'main',
|
|
37
|
+
name: 'weird"table',
|
|
38
|
+
type: 'table',
|
|
39
|
+
}),
|
|
40
|
+
).toBe('DROP TABLE "main"."weird""table"');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('quotes identifiers containing dots so they are not read as separators', () => {
|
|
44
|
+
expect(
|
|
45
|
+
buildDropEntitySql({
|
|
46
|
+
schemaName: 'main',
|
|
47
|
+
name: 'schema.like.name',
|
|
48
|
+
type: 'table',
|
|
49
|
+
}),
|
|
50
|
+
).toBe('DROP TABLE "main"."schema.like.name"');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('quotes schema names containing double quotes and dots as well', () => {
|
|
54
|
+
expect(
|
|
55
|
+
buildDropEntitySql({
|
|
56
|
+
schemaName: 'weird"schema.name',
|
|
57
|
+
name: 'projects',
|
|
58
|
+
type: 'view',
|
|
59
|
+
}),
|
|
60
|
+
).toBe('DROP VIEW "weird""schema.name"."projects"');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('orderEntitiesForDrop', () => {
|
|
65
|
+
it('moves all views before all tables while preserving relative order', () => {
|
|
66
|
+
const entities: SqliteDropTarget[] = [
|
|
67
|
+
{ schemaName: 'main', name: 'a_table', type: 'table' },
|
|
68
|
+
{ schemaName: 'main', name: 'b_view', type: 'view' },
|
|
69
|
+
{ schemaName: 'main', name: 'c_table', type: 'table' },
|
|
70
|
+
{ schemaName: 'main', name: 'd_view', type: 'view' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
expect(orderEntitiesForDrop(entities).map((entity) => entity.name)).toEqual(
|
|
74
|
+
['b_view', 'd_view', 'a_table', 'c_table'],
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns an empty array for an empty database', () => {
|
|
79
|
+
expect(orderEntitiesForDrop([])).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('buildDropAllEntitiesSql', () => {
|
|
84
|
+
it('emits DROP VIEW statements before DROP TABLE statements', () => {
|
|
85
|
+
const entities: SqliteDropTarget[] = [
|
|
86
|
+
{ schemaName: 'main', name: 'projects', type: 'table' },
|
|
87
|
+
{ schemaName: 'main', name: 'active_projects', type: 'view' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
expect(buildDropAllEntitiesSql(entities)).toBe(
|
|
91
|
+
'DROP VIEW "main"."active_projects";\nDROP TABLE "main"."projects";',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('quotes every identifier in the generated script', () => {
|
|
96
|
+
const entities: SqliteDropTarget[] = [
|
|
97
|
+
{ schemaName: 'main', name: 'weird"table', type: 'table' },
|
|
98
|
+
{ schemaName: 'main', name: 'schema.view', type: 'view' },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
expect(buildDropAllEntitiesSql(entities)).toBe(
|
|
102
|
+
'DROP VIEW "main"."schema.view";\nDROP TABLE "main"."weird""table";',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns an empty string for an empty database', () => {
|
|
107
|
+
expect(buildDropAllEntitiesSql([])).toBe('');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('foreign key pragma helpers', () => {
|
|
112
|
+
it('exposes the read-only pragma statement', () => {
|
|
113
|
+
expect(SQLITE_READ_FOREIGN_KEYS_SQL).toBe('PRAGMA foreign_keys');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('builds a statement enabling foreign keys', () => {
|
|
117
|
+
expect(buildSetForeignKeysSql(true)).toBe('PRAGMA foreign_keys = ON');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('builds a statement disabling foreign keys', () => {
|
|
121
|
+
expect(buildSetForeignKeysSql(false)).toBe('PRAGMA foreign_keys = OFF');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('treats a foreign_keys value of 1 as enabled', () => {
|
|
125
|
+
expect(isForeignKeysEnabled([{ foreign_keys: 1 }])).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('treats a foreign_keys value of 0 as disabled', () => {
|
|
129
|
+
expect(isForeignKeysEnabled([{ foreign_keys: 0 }])).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('treats an empty result set as disabled', () => {
|
|
133
|
+
expect(isForeignKeysEnabled([])).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
package/src/ui/globals.css
CHANGED
|
@@ -399,6 +399,18 @@
|
|
|
399
399
|
flex-shrink: 0;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
.sqlite-button-danger {
|
|
403
|
+
border-color: rgba(255, 100, 100, 0.35);
|
|
404
|
+
background: rgba(255, 90, 90, 0.14);
|
|
405
|
+
color: rgba(255, 214, 214, 0.95);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.sqlite-button-danger:hover:not(:disabled) {
|
|
409
|
+
border-color: rgba(255, 120, 120, 0.55);
|
|
410
|
+
background: rgba(255, 90, 90, 0.22);
|
|
411
|
+
color: white;
|
|
412
|
+
}
|
|
413
|
+
|
|
402
414
|
.sqlite-badge,
|
|
403
415
|
.sqlite-chip,
|
|
404
416
|
.sqlite-count-pill {
|
package/src/ui/panel.tsx
CHANGED
|
@@ -65,6 +65,17 @@ import {
|
|
|
65
65
|
} from './sqlite-introspection';
|
|
66
66
|
import { QueryResultTable } from './query-result-table';
|
|
67
67
|
import { SqliteRowDeleteModal } from './sqlite-row-delete-modal';
|
|
68
|
+
import {
|
|
69
|
+
SqliteDropModal,
|
|
70
|
+
type SqliteDropModalTarget,
|
|
71
|
+
} from './sqlite-drop-modal';
|
|
72
|
+
import {
|
|
73
|
+
buildDropAllEntitiesSql,
|
|
74
|
+
buildDropEntitySql,
|
|
75
|
+
buildSetForeignKeysSql,
|
|
76
|
+
isForeignKeysEnabled,
|
|
77
|
+
SQLITE_READ_FOREIGN_KEYS_SQL,
|
|
78
|
+
} from './sqlite-drop-mutations';
|
|
68
79
|
import { SqliteDataTable } from './sqlite-data-table';
|
|
69
80
|
import { SqliteRowEditModal } from './sqlite-row-edit-modal';
|
|
70
81
|
import { SqlEditor, type SqlEditorHandle } from './sql-editor';
|
|
@@ -148,6 +159,15 @@ type ActiveRowMutationState = {
|
|
|
148
159
|
rowIndex: number;
|
|
149
160
|
} | null;
|
|
150
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The drop dialog operates on the target captured when it opened, so that a
|
|
164
|
+
* mid-dialog explorer reload cannot repoint it at a different object.
|
|
165
|
+
*/
|
|
166
|
+
type ActiveDropState = {
|
|
167
|
+
databaseId: string;
|
|
168
|
+
target: SqliteDropModalTarget;
|
|
169
|
+
} | null;
|
|
170
|
+
|
|
151
171
|
const DEFAULT_QUERY =
|
|
152
172
|
'SELECT name, type FROM sqlite_schema ORDER BY type, name';
|
|
153
173
|
const DEFAULT_QUERY_LIMIT = 100;
|
|
@@ -164,6 +184,14 @@ const DEFAULT_EXPLORER_STATE: ExplorerState = {
|
|
|
164
184
|
loaded: false,
|
|
165
185
|
};
|
|
166
186
|
|
|
187
|
+
const DEFAULT_DROP_TARGET: SqliteDropModalTarget = {
|
|
188
|
+
kind: 'entity',
|
|
189
|
+
entityType: 'table',
|
|
190
|
+
qualifiedName: '',
|
|
191
|
+
confirmationValue: '',
|
|
192
|
+
sql: '',
|
|
193
|
+
};
|
|
194
|
+
|
|
167
195
|
const joinClassNames = (
|
|
168
196
|
...classNames: Array<string | false | null | undefined>
|
|
169
197
|
) => classNames.filter(Boolean).join(' ');
|
|
@@ -315,6 +343,7 @@ const iconButtonClassName =
|
|
|
315
343
|
'sqlite-icon-button inline-flex h-10 w-10 items-center justify-center rounded-xl';
|
|
316
344
|
const primaryIconButtonClassName = `${iconButtonClassName} sqlite-button-primary`;
|
|
317
345
|
const secondaryIconButtonClassName = `${iconButtonClassName} sqlite-button-secondary`;
|
|
346
|
+
const dangerIconButtonClassName = `${iconButtonClassName} sqlite-button-danger`;
|
|
318
347
|
const ghostIconButtonClassName = `${iconButtonClassName} sqlite-button-ghost`;
|
|
319
348
|
|
|
320
349
|
const renderEmptyState = (
|
|
@@ -418,6 +447,7 @@ export default function SqlitePanel() {
|
|
|
418
447
|
>({});
|
|
419
448
|
const [editingRow, setEditingRow] = useState<ActiveRowMutationState>(null);
|
|
420
449
|
const [deletingRow, setDeletingRow] = useState<ActiveRowMutationState>(null);
|
|
450
|
+
const [activeDrop, setActiveDrop] = useState<ActiveDropState>(null);
|
|
421
451
|
|
|
422
452
|
const [objectSearch, setObjectSearch] = useState('');
|
|
423
453
|
const [dataSearch, setDataSearch] = useState('');
|
|
@@ -1202,6 +1232,149 @@ export default function SqlitePanel() {
|
|
|
1202
1232
|
selectedEntity,
|
|
1203
1233
|
]);
|
|
1204
1234
|
|
|
1235
|
+
const handleOpenDropEntity = useCallback(() => {
|
|
1236
|
+
if (!selectedDatabaseId || !selectedEntity) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
setActiveDrop({
|
|
1241
|
+
databaseId: selectedDatabaseId,
|
|
1242
|
+
target: {
|
|
1243
|
+
kind: 'entity',
|
|
1244
|
+
entityType: selectedEntity.type,
|
|
1245
|
+
qualifiedName: `${selectedEntity.schemaName}.${selectedEntity.name}`,
|
|
1246
|
+
confirmationValue: selectedEntity.name,
|
|
1247
|
+
sql: `${buildDropEntitySql(selectedEntity)};`,
|
|
1248
|
+
},
|
|
1249
|
+
});
|
|
1250
|
+
}, [selectedDatabaseId, selectedEntity]);
|
|
1251
|
+
|
|
1252
|
+
const handleOpenDropAllEntities = useCallback(() => {
|
|
1253
|
+
if (!selectedDatabaseId || !selectedDatabase) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const tableCount = entities.filter(
|
|
1258
|
+
(entity) => entity.type === 'table',
|
|
1259
|
+
).length;
|
|
1260
|
+
const viewCount = entities.filter(
|
|
1261
|
+
(entity) => entity.type === 'view',
|
|
1262
|
+
).length;
|
|
1263
|
+
|
|
1264
|
+
setActiveDrop({
|
|
1265
|
+
databaseId: selectedDatabaseId,
|
|
1266
|
+
target: {
|
|
1267
|
+
kind: 'database',
|
|
1268
|
+
databaseName: selectedDatabase.name,
|
|
1269
|
+
confirmationValue: selectedDatabase.name,
|
|
1270
|
+
tableCount,
|
|
1271
|
+
viewCount,
|
|
1272
|
+
sql: buildDropAllEntitiesSql(entities),
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
}, [entities, selectedDatabase, selectedDatabaseId]);
|
|
1276
|
+
|
|
1277
|
+
const dropSelectedEntity = useCallback(
|
|
1278
|
+
async (databaseId: string, sql: string) => {
|
|
1279
|
+
await requestQuery({ databaseId, sql });
|
|
1280
|
+
},
|
|
1281
|
+
[requestQuery],
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
const dropAllEntities = useCallback(
|
|
1285
|
+
async (databaseId: string, sql: string) => {
|
|
1286
|
+
// Dropping a parent table before its children fails while foreign key
|
|
1287
|
+
// enforcement is on, so turn it off for the sweep. The pragma is
|
|
1288
|
+
// per-connection and that connection belongs to the running app, so the
|
|
1289
|
+
// original value has to be restored on every path out of here.
|
|
1290
|
+
const foreignKeysResult = await requestQuery({
|
|
1291
|
+
databaseId,
|
|
1292
|
+
sql: SQLITE_READ_FOREIGN_KEYS_SQL,
|
|
1293
|
+
});
|
|
1294
|
+
const foreignKeysWereEnabled = isForeignKeysEnabled(
|
|
1295
|
+
foreignKeysResult.rows,
|
|
1296
|
+
);
|
|
1297
|
+
|
|
1298
|
+
if (foreignKeysWereEnabled) {
|
|
1299
|
+
await requestQuery({
|
|
1300
|
+
databaseId,
|
|
1301
|
+
sql: buildSetForeignKeysSql(false),
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
let dropError: unknown = null;
|
|
1306
|
+
|
|
1307
|
+
try {
|
|
1308
|
+
const scriptResult = await requestScriptExecution({
|
|
1309
|
+
databaseId,
|
|
1310
|
+
sql,
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
if (scriptResult.failedStatementIndex != null) {
|
|
1314
|
+
const failedStatement = scriptResult.statements.find(
|
|
1315
|
+
(statement) =>
|
|
1316
|
+
statement.index === scriptResult.failedStatementIndex,
|
|
1317
|
+
);
|
|
1318
|
+
throw new Error(failedStatement?.error ?? 'Script execution failed.');
|
|
1319
|
+
}
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
dropError = error;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (foreignKeysWereEnabled) {
|
|
1325
|
+
try {
|
|
1326
|
+
await requestQuery({
|
|
1327
|
+
databaseId,
|
|
1328
|
+
sql: buildSetForeignKeysSql(true),
|
|
1329
|
+
});
|
|
1330
|
+
} catch (restoreError) {
|
|
1331
|
+
// The drop error, if any, takes priority: it's the reason the user
|
|
1332
|
+
// is looking at this modal. Only surface the restore failure when
|
|
1333
|
+
// the drop itself succeeded, since that's a new, silent problem.
|
|
1334
|
+
if (!dropError) {
|
|
1335
|
+
throw restoreError instanceof Error
|
|
1336
|
+
? restoreError
|
|
1337
|
+
: new Error(String(restoreError));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (dropError) {
|
|
1343
|
+
throw dropError instanceof Error
|
|
1344
|
+
? dropError
|
|
1345
|
+
: new Error(String(dropError));
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
[requestQuery, requestScriptExecution],
|
|
1349
|
+
);
|
|
1350
|
+
|
|
1351
|
+
const handleConfirmDrop = useCallback(async () => {
|
|
1352
|
+
if (!activeDrop) {
|
|
1353
|
+
throw new Error('The drop target is no longer available.');
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Execute the target captured when the dialog opened rather than the
|
|
1357
|
+
// current selection: an app reload can fire `sqlite:ready` mid-dialog,
|
|
1358
|
+
// which reloads the explorer and may move the selection elsewhere. The
|
|
1359
|
+
// user confirmed by typing this object's name, so this is the object that
|
|
1360
|
+
// has to be dropped.
|
|
1361
|
+
const { databaseId, target } = activeDrop;
|
|
1362
|
+
|
|
1363
|
+
if (!target.sql) {
|
|
1364
|
+
throw new Error('There is nothing to drop.');
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (target.kind === 'entity') {
|
|
1368
|
+
await dropSelectedEntity(databaseId, target.sql);
|
|
1369
|
+
} else {
|
|
1370
|
+
await dropAllEntities(databaseId, target.sql);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
setActiveDrop(null);
|
|
1374
|
+
setSelectedEntityKey(null);
|
|
1375
|
+
await refreshExplorerData();
|
|
1376
|
+
}, [activeDrop, dropAllEntities, dropSelectedEntity, refreshExplorerData]);
|
|
1377
|
+
|
|
1205
1378
|
const getActiveStatement = useCallback(() => {
|
|
1206
1379
|
const cursorPosition =
|
|
1207
1380
|
editorRef.current?.getSelection().start ?? querySelection.start;
|
|
@@ -2287,6 +2460,17 @@ export default function SqlitePanel() {
|
|
|
2287
2460
|
)}
|
|
2288
2461
|
/>
|
|
2289
2462
|
</button>
|
|
2463
|
+
<button
|
|
2464
|
+
type="button"
|
|
2465
|
+
className={dangerIconButtonClassName}
|
|
2466
|
+
onClick={handleOpenDropEntity}
|
|
2467
|
+
aria-label={
|
|
2468
|
+
selectedEntity.type === 'view' ? 'Drop view' : 'Drop table'
|
|
2469
|
+
}
|
|
2470
|
+
title={selectedEntity.type === 'view' ? 'Drop view' : 'Drop table'}
|
|
2471
|
+
>
|
|
2472
|
+
<Trash2 aria-hidden="true" className="h-4 w-4" />
|
|
2473
|
+
</button>
|
|
2290
2474
|
</div>
|
|
2291
2475
|
</header>
|
|
2292
2476
|
|
|
@@ -2455,6 +2639,29 @@ export default function SqlitePanel() {
|
|
|
2455
2639
|
)}
|
|
2456
2640
|
/>
|
|
2457
2641
|
</button>
|
|
2642
|
+
<button
|
|
2643
|
+
type="button"
|
|
2644
|
+
className={dangerIconButtonClassName}
|
|
2645
|
+
aria-label={
|
|
2646
|
+
selectedDatabase
|
|
2647
|
+
? `Drop all objects in ${selectedDatabase.name}`
|
|
2648
|
+
: 'Drop all objects'
|
|
2649
|
+
}
|
|
2650
|
+
title={
|
|
2651
|
+
selectedDatabase
|
|
2652
|
+
? `Drop all objects in ${selectedDatabase.name}`
|
|
2653
|
+
: 'Drop all objects'
|
|
2654
|
+
}
|
|
2655
|
+
onClick={handleOpenDropAllEntities}
|
|
2656
|
+
disabled={
|
|
2657
|
+
!selectedDatabaseId ||
|
|
2658
|
+
entities.length === 0 ||
|
|
2659
|
+
databaseLoading ||
|
|
2660
|
+
entityLoading
|
|
2661
|
+
}
|
|
2662
|
+
>
|
|
2663
|
+
<Trash2 aria-hidden="true" className="h-4 w-4" />
|
|
2664
|
+
</button>
|
|
2458
2665
|
</div>
|
|
2459
2666
|
</header>
|
|
2460
2667
|
|
|
@@ -2809,6 +3016,13 @@ export default function SqlitePanel() {
|
|
|
2809
3016
|
onClose={() => setDeletingRow(null)}
|
|
2810
3017
|
onDelete={handleDeleteRow}
|
|
2811
3018
|
/>
|
|
3019
|
+
|
|
3020
|
+
<SqliteDropModal
|
|
3021
|
+
isOpen={!!activeDrop}
|
|
3022
|
+
target={activeDrop?.target ?? DEFAULT_DROP_TARGET}
|
|
3023
|
+
onClose={() => setActiveDrop(null)}
|
|
3024
|
+
onConfirm={handleConfirmDrop}
|
|
3025
|
+
/>
|
|
2812
3026
|
</div>
|
|
2813
3027
|
</div>
|
|
2814
3028
|
);
|