@peers-app/peers-ui 0.8.23 → 0.9.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/dist/screens/data-explorer/query-executor.js +22 -8
- package/dist/screens/settings/voice-settings.js +8 -3
- package/dist/tabs-layout/tabs-layout.js +26 -1
- package/package.json +3 -3
- package/src/screens/data-explorer/query-executor.tsx +35 -8
- package/src/screens/settings/voice-settings.tsx +7 -3
- package/src/tabs-layout/tabs-layout.tsx +52 -2
|
@@ -58,6 +58,7 @@ function QueryExecutor() {
|
|
|
58
58
|
const [results, setResults] = (0, react_1.useState)({});
|
|
59
59
|
const [errors, setErrors] = (0, react_1.useState)({});
|
|
60
60
|
const [loading, setLoading] = (0, react_1.useState)({});
|
|
61
|
+
const [writeEnabled, setWriteEnabled] = (0, react_1.useState)(false);
|
|
61
62
|
// Local state for query input to avoid locking up on fast typing
|
|
62
63
|
const [localQuery, setLocalQuery] = (0, react_1.useState)('');
|
|
63
64
|
const debounceTimeoutRef = (0, react_1.useRef)();
|
|
@@ -173,7 +174,7 @@ function QueryExecutor() {
|
|
|
173
174
|
setError('Data Explorer API not available');
|
|
174
175
|
return;
|
|
175
176
|
}
|
|
176
|
-
const queryResult = await api.executeQuery(query);
|
|
177
|
+
const queryResult = await api.executeQuery(query, writeEnabled);
|
|
177
178
|
setResult(queryResult);
|
|
178
179
|
}
|
|
179
180
|
catch (err) {
|
|
@@ -263,9 +264,12 @@ function QueryExecutor() {
|
|
|
263
264
|
react_1.default.createElement("i", { className: "bi bi-plus-lg" }))))),
|
|
264
265
|
react_1.default.createElement("div", { className: "card mb-4" },
|
|
265
266
|
react_1.default.createElement("div", { className: "card-body" },
|
|
266
|
-
react_1.default.createElement("
|
|
267
|
-
react_1.default.createElement("
|
|
268
|
-
|
|
267
|
+
react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center" },
|
|
268
|
+
react_1.default.createElement("label", { htmlFor: "query-input", className: "form-label mb-0" },
|
|
269
|
+
react_1.default.createElement("strong", null, "SQL Query")),
|
|
270
|
+
react_1.default.createElement("div", { className: "form-check form-switch" },
|
|
271
|
+
react_1.default.createElement("input", { className: "form-check-input", type: "checkbox", id: "enable-writes", checked: writeEnabled, onChange: (e) => setWriteEnabled(e.target.checked) }),
|
|
272
|
+
react_1.default.createElement("label", { className: "form-check-label small", htmlFor: "enable-writes" }, "Enable Writes"))),
|
|
269
273
|
react_1.default.createElement("textarea", { id: "query-input", className: "form-control font-monospace", rows: 6, value: query, onChange: (e) => setQuery(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: "Enter your SQL query here..." }),
|
|
270
274
|
react_1.default.createElement("div", { className: "mt-3 d-flex justify-content-between align-items-center" },
|
|
271
275
|
react_1.default.createElement("button", { className: "btn btn-primary", onClick: executeQuery, disabled: isLoading || !query.trim() }, isLoading ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
@@ -287,14 +291,24 @@ function QueryExecutor() {
|
|
|
287
291
|
react_1.default.createElement("div", { className: "card-body" },
|
|
288
292
|
react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center mb-3" },
|
|
289
293
|
react_1.default.createElement("h5", { className: "card-title mb-0" },
|
|
290
|
-
|
|
291
|
-
react_1.default.createElement("span", { className: "badge bg-
|
|
294
|
+
result.isWriteOperation ? 'Write Result' : 'Query Results',
|
|
295
|
+
result.isWriteOperation ? (react_1.default.createElement("span", { className: "badge bg-success ms-2" },
|
|
296
|
+
result.changesCount,
|
|
297
|
+
" row",
|
|
298
|
+
result.changesCount !== 1 ? 's' : '',
|
|
299
|
+
" affected")) : (react_1.default.createElement("span", { className: "badge bg-primary ms-2" },
|
|
292
300
|
result.rowCount,
|
|
293
|
-
" rows")),
|
|
301
|
+
" rows"))),
|
|
294
302
|
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-secondary", onClick: () => setResult(null) },
|
|
295
303
|
react_1.default.createElement("i", { className: "bi bi-x" }),
|
|
296
304
|
" Clear")),
|
|
297
|
-
result.
|
|
305
|
+
result.isWriteOperation ? (react_1.default.createElement("div", { className: "alert alert-success mb-0" },
|
|
306
|
+
react_1.default.createElement("i", { className: "bi bi-check-circle-fill me-2" }),
|
|
307
|
+
"Query executed successfully. ",
|
|
308
|
+
result.changesCount,
|
|
309
|
+
" row",
|
|
310
|
+
result.changesCount !== 1 ? 's' : '',
|
|
311
|
+
" affected.")) : result.rowCount === 0 ? (react_1.default.createElement("div", { className: "alert alert-info mb-0" }, "Query executed successfully but returned no rows.")) : (react_1.default.createElement("div", { className: "table-responsive" },
|
|
298
312
|
react_1.default.createElement("table", { className: "table table-sm table-hover table-bordered" },
|
|
299
313
|
react_1.default.createElement("thead", { className: "table-light" },
|
|
300
314
|
react_1.default.createElement("tr", null, result.columns.map((col, idx) => (react_1.default.createElement("th", { key: idx, className: "font-monospace small" }, col))))),
|
|
@@ -115,9 +115,14 @@ const VoiceSettings = () => {
|
|
|
115
115
|
};
|
|
116
116
|
setSettings(loadedSettings);
|
|
117
117
|
setLocalSettings(loadedSettings);
|
|
118
|
-
// Load audio devices
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
// Load audio devices (may not be available in React Native)
|
|
119
|
+
try {
|
|
120
|
+
const devices = await peers_sdk_1.rpcServerCalls.voiceGetAudioDevices();
|
|
121
|
+
setAudioDevices(devices || []);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// voiceGetAudioDevices not available (e.g. React Native) - leave empty
|
|
125
|
+
}
|
|
121
126
|
}
|
|
122
127
|
catch (err) {
|
|
123
128
|
console.error('Failed to load voice settings:', err);
|
|
@@ -50,6 +50,30 @@ const system_apps_1 = require("../system-apps");
|
|
|
50
50
|
const routes_loader_1 = require("../ui-router/routes-loader");
|
|
51
51
|
const ui_loader_1 = require("../ui-router/ui-loader");
|
|
52
52
|
const tabs_state_1 = require("./tabs-state");
|
|
53
|
+
class TabErrorBoundary extends react_1.Component {
|
|
54
|
+
constructor(props) {
|
|
55
|
+
super(props);
|
|
56
|
+
this.state = { hasError: false, error: null };
|
|
57
|
+
}
|
|
58
|
+
static getDerivedStateFromError(error) {
|
|
59
|
+
return { hasError: true, error };
|
|
60
|
+
}
|
|
61
|
+
componentDidCatch(error, errorInfo) {
|
|
62
|
+
console.error(`TabErrorBoundary caught error in tab "${this.props.tabTitle}":`, error, errorInfo);
|
|
63
|
+
}
|
|
64
|
+
render() {
|
|
65
|
+
if (this.state.hasError) {
|
|
66
|
+
return (react_1.default.createElement("div", { style: { padding: '24px', textAlign: 'center' } },
|
|
67
|
+
react_1.default.createElement("h4", { style: { marginBottom: '12px' } },
|
|
68
|
+
"Something went wrong in \"",
|
|
69
|
+
this.props.tabTitle,
|
|
70
|
+
"\""),
|
|
71
|
+
react_1.default.createElement("p", { style: { opacity: 0.7, marginBottom: '16px', fontFamily: 'monospace', fontSize: '13px' } }, this.state.error?.message || 'Unknown error'),
|
|
72
|
+
react_1.default.createElement("button", { className: "btn btn-outline-primary btn-sm", onClick: () => this.setState({ hasError: false, error: null }) }, "Try Again")));
|
|
73
|
+
}
|
|
74
|
+
return this.props.children;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
53
77
|
function TabsLayoutApp() {
|
|
54
78
|
const userId = (0, hooks_1.usePromise)(async () => {
|
|
55
79
|
const _userId = await peers_sdk_1.rpcServerCalls.getUserId();
|
|
@@ -166,7 +190,8 @@ function TabsLayoutInternal() {
|
|
|
166
190
|
msOverflowStyle: 'none'
|
|
167
191
|
} }, tabs.map(tab => (react_1.default.createElement(TabHeader, { key: tab.tabId, tab: tab, isActive: activeTab === tab.tabId, onSwitch: switchTab, onClose: closeTab, colorMode: _colorMode }))))))),
|
|
168
192
|
react_1.default.createElement("div", { className: "flex-grow-1 overflow-hidden" }, tabs.map(tab => (react_1.default.createElement("div", { key: tab.tabId, className: `h-100 ${activeTab === tab.tabId ? 'd-block' : 'd-none'}`, style: { display: activeTab === tab.tabId ? 'block' : 'none' } },
|
|
169
|
-
react_1.default.createElement(
|
|
193
|
+
react_1.default.createElement(TabErrorBoundary, { tabTitle: tab.title, tabId: tab.tabId },
|
|
194
|
+
react_1.default.createElement(TabContent, { tab: tab, isMobile: isMobile, isActive: activeTab === tab.tabId }))))))));
|
|
170
195
|
}
|
|
171
196
|
function MobileTabsHeader({ tabs, activeTab, onSwitch, onClose, colorMode }) {
|
|
172
197
|
const [showMenuDropdown, setShowMenuDropdown] = react_1.default.useState(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"test:coverage": "jest --coverage"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@peers-app/peers-sdk": "^0.
|
|
29
|
+
"@peers-app/peers-sdk": "^0.9.0",
|
|
30
30
|
"bootstrap": "^5.3.3",
|
|
31
31
|
"react": "^18.0.0",
|
|
32
32
|
"react-dom": "^18.0.0"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@babel/preset-react": "^7.24.1",
|
|
38
38
|
"@babel/preset-typescript": "^7.27.1",
|
|
39
39
|
"@electron/rebuild": "^3.6.0",
|
|
40
|
-
"@peers-app/peers-sdk": "0.
|
|
40
|
+
"@peers-app/peers-sdk": "0.9.0",
|
|
41
41
|
"@testing-library/dom": "^10.4.0",
|
|
42
42
|
"@testing-library/jest-dom": "^6.6.3",
|
|
43
43
|
"@testing-library/react": "^16.3.0",
|
|
@@ -9,6 +9,8 @@ interface ISqlQueryResult {
|
|
|
9
9
|
columns: string[];
|
|
10
10
|
rows: any[][];
|
|
11
11
|
rowCount: number;
|
|
12
|
+
isWriteOperation?: boolean;
|
|
13
|
+
changesCount?: number;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
interface IQueryTab {
|
|
@@ -38,6 +40,7 @@ export function QueryExecutor() {
|
|
|
38
40
|
const [results, setResults] = useState<{ [tabId: string]: ISqlQueryResult }>({});
|
|
39
41
|
const [errors, setErrors] = useState<{ [tabId: string]: string }>({});
|
|
40
42
|
const [loading, setLoading] = useState<{ [tabId: string]: boolean }>({});
|
|
43
|
+
const [writeEnabled, setWriteEnabled] = useState(false);
|
|
41
44
|
|
|
42
45
|
// Local state for query input to avoid locking up on fast typing
|
|
43
46
|
const [localQuery, setLocalQuery] = useState<string>('');
|
|
@@ -168,7 +171,7 @@ export function QueryExecutor() {
|
|
|
168
171
|
return;
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
const queryResult = await api.executeQuery(query);
|
|
174
|
+
const queryResult = await api.executeQuery(query, writeEnabled);
|
|
172
175
|
setResult(queryResult);
|
|
173
176
|
} catch (err: any) {
|
|
174
177
|
setError(err.message || 'Query execution failed');
|
|
@@ -312,10 +315,23 @@ export function QueryExecutor() {
|
|
|
312
315
|
{/* Query Input */}
|
|
313
316
|
<div className="card mb-4">
|
|
314
317
|
<div className="card-body">
|
|
315
|
-
<
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
<div className="d-flex justify-content-between align-items-center">
|
|
319
|
+
<label htmlFor="query-input" className="form-label mb-0">
|
|
320
|
+
<strong>SQL Query</strong>
|
|
321
|
+
</label>
|
|
322
|
+
<div className="form-check form-switch">
|
|
323
|
+
<input
|
|
324
|
+
className="form-check-input"
|
|
325
|
+
type="checkbox"
|
|
326
|
+
id="enable-writes"
|
|
327
|
+
checked={writeEnabled}
|
|
328
|
+
onChange={(e) => setWriteEnabled(e.target.checked)}
|
|
329
|
+
/>
|
|
330
|
+
<label className="form-check-label small" htmlFor="enable-writes">
|
|
331
|
+
Enable Writes
|
|
332
|
+
</label>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
319
335
|
<textarea
|
|
320
336
|
id="query-input"
|
|
321
337
|
className="form-control font-monospace"
|
|
@@ -371,8 +387,14 @@ export function QueryExecutor() {
|
|
|
371
387
|
<div className="card-body">
|
|
372
388
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
|
373
389
|
<h5 className="card-title mb-0">
|
|
374
|
-
Query Results
|
|
375
|
-
|
|
390
|
+
{result.isWriteOperation ? 'Write Result' : 'Query Results'}
|
|
391
|
+
{result.isWriteOperation ? (
|
|
392
|
+
<span className="badge bg-success ms-2">
|
|
393
|
+
{result.changesCount} row{result.changesCount !== 1 ? 's' : ''} affected
|
|
394
|
+
</span>
|
|
395
|
+
) : (
|
|
396
|
+
<span className="badge bg-primary ms-2">{result.rowCount} rows</span>
|
|
397
|
+
)}
|
|
376
398
|
</h5>
|
|
377
399
|
<button
|
|
378
400
|
className="btn btn-sm btn-outline-secondary"
|
|
@@ -382,7 +404,12 @@ export function QueryExecutor() {
|
|
|
382
404
|
</button>
|
|
383
405
|
</div>
|
|
384
406
|
|
|
385
|
-
{result.
|
|
407
|
+
{result.isWriteOperation ? (
|
|
408
|
+
<div className="alert alert-success mb-0">
|
|
409
|
+
<i className="bi bi-check-circle-fill me-2"></i>
|
|
410
|
+
Query executed successfully. {result.changesCount} row{result.changesCount !== 1 ? 's' : ''} affected.
|
|
411
|
+
</div>
|
|
412
|
+
) : result.rowCount === 0 ? (
|
|
386
413
|
<div className="alert alert-info mb-0">
|
|
387
414
|
Query executed successfully but returned no rows.
|
|
388
415
|
</div>
|
|
@@ -131,9 +131,13 @@ export const VoiceSettings: React.FC = () => {
|
|
|
131
131
|
setSettings(loadedSettings);
|
|
132
132
|
setLocalSettings(loadedSettings);
|
|
133
133
|
|
|
134
|
-
// Load audio devices
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
// Load audio devices (may not be available in React Native)
|
|
135
|
+
try {
|
|
136
|
+
const devices = await rpcServerCalls.voiceGetAudioDevices();
|
|
137
|
+
setAudioDevices(devices || []);
|
|
138
|
+
} catch {
|
|
139
|
+
// voiceGetAudioDevices not available (e.g. React Native) - leave empty
|
|
140
|
+
}
|
|
137
141
|
} catch (err) {
|
|
138
142
|
console.error('Failed to load voice settings:', err);
|
|
139
143
|
setError('Failed to load voice settings.');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getUserContext, hasShownWelcomeModal, IAppNav, rpcServerCalls, setDefaultClientUserContext, sleep, Subscription } from "@peers-app/peers-sdk";
|
|
2
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import React, { Component, useEffect, useState } from 'react';
|
|
3
3
|
import { openCommandPalette } from '../command-palette/command-palette';
|
|
4
4
|
import { CommandPaletteOverlay } from '../command-palette/command-palette-ui';
|
|
5
5
|
import { GroupSwitcher } from '../components/group-switcher';
|
|
@@ -14,6 +14,54 @@ import { allPackages, loadAllRoutes } from '../ui-router/routes-loader';
|
|
|
14
14
|
import { UIRouter } from '../ui-router/ui-loader';
|
|
15
15
|
import { activeTabId, activeTabs, goToTabPath, initializedTabs, recentlyUsedApps, TabState } from './tabs-state';
|
|
16
16
|
|
|
17
|
+
// Error boundary that wraps each tab's content so a crash in one tab
|
|
18
|
+
// doesn't take down the entire app.
|
|
19
|
+
interface TabErrorBoundaryProps {
|
|
20
|
+
tabTitle: string;
|
|
21
|
+
tabId: string;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TabErrorBoundaryState {
|
|
26
|
+
hasError: boolean;
|
|
27
|
+
error: Error | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class TabErrorBoundary extends Component<TabErrorBoundaryProps, TabErrorBoundaryState> {
|
|
31
|
+
constructor(props: TabErrorBoundaryProps) {
|
|
32
|
+
super(props);
|
|
33
|
+
this.state = { hasError: false, error: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static getDerivedStateFromError(error: Error): TabErrorBoundaryState {
|
|
37
|
+
return { hasError: true, error };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
41
|
+
console.error(`TabErrorBoundary caught error in tab "${this.props.tabTitle}":`, error, errorInfo);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
if (this.state.hasError) {
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ padding: '24px', textAlign: 'center' }}>
|
|
48
|
+
<h4 style={{ marginBottom: '12px' }}>Something went wrong in "{this.props.tabTitle}"</h4>
|
|
49
|
+
<p style={{ opacity: 0.7, marginBottom: '16px', fontFamily: 'monospace', fontSize: '13px' }}>
|
|
50
|
+
{this.state.error?.message || 'Unknown error'}
|
|
51
|
+
</p>
|
|
52
|
+
<button
|
|
53
|
+
className="btn btn-outline-primary btn-sm"
|
|
54
|
+
onClick={() => this.setState({ hasError: false, error: null })}
|
|
55
|
+
>
|
|
56
|
+
Try Again
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return this.props.children;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
17
65
|
export function TabsLayoutApp() {
|
|
18
66
|
const userId = usePromise(async () => {
|
|
19
67
|
const _userId = await rpcServerCalls.getUserId();
|
|
@@ -208,7 +256,9 @@ function TabsLayoutInternal() {
|
|
|
208
256
|
className={`h-100 ${activeTab === tab.tabId ? 'd-block' : 'd-none'}`}
|
|
209
257
|
style={{ display: activeTab === tab.tabId ? 'block' : 'none' }}
|
|
210
258
|
>
|
|
211
|
-
<
|
|
259
|
+
<TabErrorBoundary tabTitle={tab.title} tabId={tab.tabId}>
|
|
260
|
+
<TabContent tab={tab} isMobile={isMobile} isActive={activeTab === tab.tabId} />
|
|
261
|
+
</TabErrorBoundary>
|
|
212
262
|
</div>
|
|
213
263
|
))}
|
|
214
264
|
</div>
|