@peers-app/peers-ui 0.8.19 → 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.
@@ -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("label", { htmlFor: "query-input", className: "form-label" },
267
- react_1.default.createElement("strong", null, "SQL Query"),
268
- react_1.default.createElement("span", { className: "text-muted small ms-2" }, "(Read-only: SELECT and PRAGMA only)")),
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
- "Query Results",
291
- react_1.default.createElement("span", { className: "badge bg-primary ms-2" },
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.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" },
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
- const devices = await peers_sdk_1.rpcServerCalls.voiceGetAudioDevices();
120
- setAudioDevices(devices);
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(TabContent, { tab: tab, isMobile: isMobile, isActive: activeTab === tab.tabId })))))));
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.8.19",
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.8.19",
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.8.19",
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
- <label htmlFor="query-input" className="form-label">
316
- <strong>SQL Query</strong>
317
- <span className="text-muted small ms-2">(Read-only: SELECT and PRAGMA only)</span>
318
- </label>
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
- <span className="badge bg-primary ms-2">{result.rowCount} rows</span>
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.rowCount === 0 ? (
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
- const devices = await rpcServerCalls.voiceGetAudioDevices();
136
- setAudioDevices(devices);
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
- <TabContent tab={tab} isMobile={isMobile} isActive={activeTab === tab.tabId} />
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>