@peers-app/peers-ui 0.13.0 → 0.13.1

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.
@@ -72,7 +72,7 @@ function LazyList(props) {
72
72
  if (renderItems.length < 50 && !allLoaded && !loading() && !isLoadingSync.current) {
73
73
  loadMore();
74
74
  }
75
- }, [renderItems.length, allLoaded, loading()]);
75
+ }, [renderItems.length, allLoaded]);
76
76
  (0, react_1.useEffect)(() => {
77
77
  itemsObsAry([]);
78
78
  setAllLoaded(false);
@@ -30,11 +30,17 @@ function applyColorMode(modePreference) {
30
30
  }
31
31
  document.documentElement.setAttribute('data-bs-theme', mode);
32
32
  if (mode === 'light') {
33
+ document.documentElement.style.backgroundColor = '';
33
34
  document.body.style.backgroundColor = 'initial';
34
35
  }
35
36
  else {
37
+ document.documentElement.style.backgroundColor = 'rgb(33, 37, 41)';
36
38
  document.body.style.backgroundColor = 'rgb(33, 37, 41)';
37
39
  }
40
+ const themeMeta = document.querySelector('meta[name="theme-color"]');
41
+ if (themeMeta) {
42
+ themeMeta.setAttribute('content', mode === 'light' ? '#ffffff' : '#212529');
43
+ }
38
44
  (0, exports.colorMode)(mode);
39
45
  exports.colorMode.notifySubscribers();
40
46
  return mode;
@@ -83,7 +83,8 @@ const AdvancedSettingsTab = () => {
83
83
  react_1.default.createElement(ReloadPackagesOnPageRefresh, null),
84
84
  react_1.default.createElement(AutoUpdatePeersCore, null),
85
85
  react_1.default.createElement(ResetDeviceSyncInfos, null),
86
- react_1.default.createElement(DeleteLocalDatabase, null)));
86
+ react_1.default.createElement(DeleteLocalDatabase, null),
87
+ react_1.default.createElement(ImportOldPeersData, null)));
87
88
  };
88
89
  const ProfileSection = () => {
89
90
  const [deviceId] = (0, hooks_1.useObservable)(peers_sdk_1.thisDeviceId);
@@ -236,6 +237,156 @@ const DeleteLocalDatabase = () => {
236
237
  }
237
238
  } }, "Delete Local Database")));
238
239
  };
240
+ const IMPORT_TOOL_ID = '00mh0wlipkdbeaw8imptsk001';
241
+ const ImportOldPeersData = () => {
242
+ const fileInputRef = (0, react_1.useRef)(null);
243
+ const [filePath, setFilePath] = (0, react_1.useState)('');
244
+ const [fileName, setFileName] = (0, react_1.useState)('');
245
+ const [loading, setLoading] = (0, react_1.useState)(false);
246
+ const [result, setResult] = (0, react_1.useState)(null);
247
+ const [error, setError] = (0, react_1.useState)('');
248
+ const handleFileSelect = (e) => {
249
+ const file = e.target.files?.[0];
250
+ if (!file)
251
+ return;
252
+ // Electron exposes .path on File objects
253
+ const path = file.path;
254
+ if (path) {
255
+ setFilePath(path);
256
+ setFileName(file.name);
257
+ }
258
+ else {
259
+ setFileName(file.name);
260
+ setFilePath('');
261
+ setError('File path not available. This feature requires the Electron desktop app.');
262
+ }
263
+ setResult(null);
264
+ setError('');
265
+ };
266
+ const handleDryRun = async () => {
267
+ if (!filePath)
268
+ return;
269
+ setLoading(true);
270
+ setError('');
271
+ setResult(null);
272
+ try {
273
+ const response = await peers_sdk_1.rpcServerCalls.runTool(IMPORT_TOOL_ID, {
274
+ filePath,
275
+ dryRun: true,
276
+ });
277
+ if (response?.result) {
278
+ setResult(response.result);
279
+ }
280
+ else {
281
+ setError('Unexpected response from import tool');
282
+ }
283
+ }
284
+ catch (err) {
285
+ setError(err.message || 'Failed to run dry run');
286
+ }
287
+ finally {
288
+ setLoading(false);
289
+ }
290
+ };
291
+ return (react_1.default.createElement("div", { className: "mt-4 pt-3 border-top" },
292
+ react_1.default.createElement("h6", { className: "mb-2" }, "Import Old Peers Data"),
293
+ react_1.default.createElement("small", { className: "text-muted d-block mb-2" }, "Import tasks and log entries from an old peers JSON export file."),
294
+ react_1.default.createElement("div", { className: "d-flex align-items-center gap-2 mb-2" },
295
+ react_1.default.createElement("input", { ref: fileInputRef, type: "file", accept: ".json", className: "form-control form-control-sm", style: { maxWidth: 350 }, onChange: handleFileSelect }),
296
+ react_1.default.createElement("button", { className: "btn btn-outline-primary btn-sm", onClick: handleDryRun, disabled: !filePath || loading }, loading ? 'Analyzing...' : 'Dry Run')),
297
+ error && (react_1.default.createElement("div", { className: "alert alert-danger py-1 px-2 small mt-2" }, error)),
298
+ result && react_1.default.createElement(DryRunResults, { result: result })));
299
+ };
300
+ const DryRunResults = ({ result }) => {
301
+ const sm = result.statusMapping;
302
+ const um = result.userMapping;
303
+ return (react_1.default.createElement("div", { className: "mt-2" },
304
+ react_1.default.createElement("div", { className: "alert alert-info py-2 px-3 small" },
305
+ react_1.default.createElement("strong", null, "Summary:"),
306
+ " ",
307
+ result.totalTasks.toLocaleString(),
308
+ " tasks and ",
309
+ result.totalLogEntries.toLocaleString(),
310
+ " log entries across ",
311
+ result.groups.length,
312
+ " groups (",
313
+ result.totalRecords.toLocaleString(),
314
+ " total records in file)"),
315
+ result.warnings.length > 0 && (react_1.default.createElement("div", { className: "alert alert-warning py-2 px-3 small" },
316
+ react_1.default.createElement("strong", null, "Warnings:"),
317
+ react_1.default.createElement("ul", { className: "mb-0 ps-3" }, result.warnings.map((w, i) => react_1.default.createElement("li", { key: i }, w))))),
318
+ react_1.default.createElement("details", { className: "mb-2" },
319
+ react_1.default.createElement("summary", { className: "small fw-bold", style: { cursor: 'pointer' } }, "Status Mapping"),
320
+ react_1.default.createElement("table", { className: "table table-sm small mt-1" },
321
+ react_1.default.createElement("thead", null,
322
+ react_1.default.createElement("tr", null,
323
+ react_1.default.createElement("th", null, "Mapping"),
324
+ react_1.default.createElement("th", { className: "text-end" }, "Count"))),
325
+ react_1.default.createElement("tbody", null,
326
+ react_1.default.createElement("tr", null,
327
+ react_1.default.createElement("td", null, "In-Progress \u2192 Done"),
328
+ react_1.default.createElement("td", { className: "text-end" }, sm.inProgressToDone.toLocaleString())),
329
+ react_1.default.createElement("tr", null,
330
+ react_1.default.createElement("td", null, "In-Progress \u2192 In-Progress"),
331
+ react_1.default.createElement("td", { className: "text-end" }, sm.inProgressKeep.toLocaleString())),
332
+ react_1.default.createElement("tr", null,
333
+ react_1.default.createElement("td", null, "Queued \u2192 Done"),
334
+ react_1.default.createElement("td", { className: "text-end" }, sm.queuedToDone.toLocaleString())),
335
+ react_1.default.createElement("tr", null,
336
+ react_1.default.createElement("td", null, "Queued \u2192 Queued"),
337
+ react_1.default.createElement("td", { className: "text-end" }, sm.queuedKeep.toLocaleString())),
338
+ react_1.default.createElement("tr", null,
339
+ react_1.default.createElement("td", null, "Backlog \u2192 Done"),
340
+ react_1.default.createElement("td", { className: "text-end" }, sm.backlogToDone.toLocaleString())),
341
+ react_1.default.createElement("tr", null,
342
+ react_1.default.createElement("td", null, "Backlog \u2192 Backlog"),
343
+ react_1.default.createElement("td", { className: "text-end" }, sm.backlogKeep.toLocaleString())),
344
+ react_1.default.createElement("tr", null,
345
+ react_1.default.createElement("td", null, "No status \u2192 Done"),
346
+ react_1.default.createElement("td", { className: "text-end" }, sm.noStatusToDone.toLocaleString())),
347
+ react_1.default.createElement("tr", null,
348
+ react_1.default.createElement("td", null, "No status \u2192 Backlog"),
349
+ react_1.default.createElement("td", { className: "text-end" }, sm.noStatusToBacklog.toLocaleString()))))),
350
+ react_1.default.createElement("details", { className: "mb-2" },
351
+ react_1.default.createElement("summary", { className: "small fw-bold", style: { cursor: 'pointer' } }, "User Mapping"),
352
+ react_1.default.createElement("table", { className: "table table-sm small mt-1" },
353
+ react_1.default.createElement("thead", null,
354
+ react_1.default.createElement("tr", null,
355
+ react_1.default.createElement("th", null, "User"),
356
+ react_1.default.createElement("th", { className: "text-end" }, "Tasks"))),
357
+ react_1.default.createElement("tbody", null,
358
+ react_1.default.createElement("tr", null,
359
+ react_1.default.createElement("td", null, "Mark"),
360
+ react_1.default.createElement("td", { className: "text-end" }, um.mark.toLocaleString())),
361
+ react_1.default.createElement("tr", null,
362
+ react_1.default.createElement("td", null, "Blair"),
363
+ react_1.default.createElement("td", { className: "text-end" }, um.blair.toLocaleString())),
364
+ react_1.default.createElement("tr", null,
365
+ react_1.default.createElement("td", null, "Other \u2192 Mark"),
366
+ react_1.default.createElement("td", { className: "text-end" }, um.other.toLocaleString()))))),
367
+ react_1.default.createElement("details", { open: true, className: "mb-2" },
368
+ react_1.default.createElement("summary", { className: "small fw-bold", style: { cursor: 'pointer' } },
369
+ "Groups (",
370
+ result.groups.length,
371
+ ")"),
372
+ react_1.default.createElement("table", { className: "table table-sm small mt-1" },
373
+ react_1.default.createElement("thead", null,
374
+ react_1.default.createElement("tr", null,
375
+ react_1.default.createElement("th", null, "Group"),
376
+ react_1.default.createElement("th", null, "Route"),
377
+ react_1.default.createElement("th", { className: "text-end" }, "Tasks"),
378
+ react_1.default.createElement("th", { className: "text-end" }, "Active"),
379
+ react_1.default.createElement("th", { className: "text-end" }, "Done"),
380
+ react_1.default.createElement("th", { className: "text-end" }, "Logs"))),
381
+ react_1.default.createElement("tbody", null, result.groups.map(g => (react_1.default.createElement("tr", { key: g.groupId },
382
+ react_1.default.createElement("td", null, g.groupName),
383
+ react_1.default.createElement("td", null,
384
+ react_1.default.createElement("span", { className: `badge bg-${g.context === 'home' ? 'success' : 'secondary'}` }, g.context)),
385
+ react_1.default.createElement("td", { className: "text-end" }, g.taskCount.toLocaleString()),
386
+ react_1.default.createElement("td", { className: "text-end" }, g.activeCount.toLocaleString()),
387
+ react_1.default.createElement("td", { className: "text-end" }, g.doneCount.toLocaleString()),
388
+ react_1.default.createElement("td", { className: "text-end" }, g.logEntryCount.toLocaleString())))))))));
389
+ };
239
390
  const showLogoutInSettings = typeof window !== 'undefined'
240
391
  && !window.electronAPI
241
392
  && !window.ReactNativeWebView;
@@ -83,6 +83,7 @@ function TabsLayoutApp() {
83
83
  return;
84
84
  }
85
85
  if (!userId) {
86
+ document.getElementById('appLoadingDiv')?.remove();
86
87
  return react_1.default.createElement(setup_user_1.SetupUser, null);
87
88
  }
88
89
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
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.13.0",
29
+ "@peers-app/peers-sdk": "^0.13.1",
30
30
  "bootstrap": "^5.3.3",
31
31
  "react": "^18.0.0",
32
32
  "react-dom": "^18.0.0"
@@ -36,7 +36,7 @@
36
36
  "@babel/preset-env": "^7.24.5",
37
37
  "@babel/preset-react": "^7.24.1",
38
38
  "@babel/preset-typescript": "^7.27.1",
39
- "@peers-app/peers-sdk": "0.13.0",
39
+ "@peers-app/peers-sdk": "0.13.1",
40
40
  "@testing-library/dom": "^10.4.0",
41
41
  "@testing-library/jest-dom": "^6.6.3",
42
42
  "@testing-library/react": "^16.3.0",
@@ -47,7 +47,7 @@ export function LazyList<T>(props: IProps<T>) {
47
47
  if (renderItems.length < 50 && !allLoaded && !loading() && !isLoadingSync.current) {
48
48
  loadMore();
49
49
  }
50
- }, [renderItems.length, allLoaded, loading()]);
50
+ }, [renderItems.length, allLoaded]);
51
51
 
52
52
  useEffect(() => {
53
53
  itemsObsAry([]);
@@ -28,10 +28,17 @@ function applyColorMode(modePreference?: ColorModePreference): ColorMode {
28
28
  }
29
29
  document.documentElement.setAttribute('data-bs-theme', mode);
30
30
  if (mode === 'light') {
31
+ document.documentElement.style.backgroundColor = '';
31
32
  document.body.style.backgroundColor = 'initial';
32
33
  } else {
34
+ document.documentElement.style.backgroundColor = 'rgb(33, 37, 41)';
33
35
  document.body.style.backgroundColor = 'rgb(33, 37, 41)';
34
36
  }
37
+
38
+ const themeMeta = document.querySelector('meta[name="theme-color"]');
39
+ if (themeMeta) {
40
+ themeMeta.setAttribute('content', mode === 'light' ? '#ffffff' : '#212529');
41
+ }
35
42
  colorMode(mode);
36
43
  colorMode.notifySubscribers();
37
44
 
@@ -1,5 +1,5 @@
1
1
  import { autoUpdatePeersCore, Devices, getUserContext, IDevice, packagesRootDir, reloadPackagesOnPageRefresh, rpcServerCalls, thisDeviceId, TrustLevel, Users } from "@peers-app/peers-sdk";
2
- import React, { useEffect } from 'react';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
3
  import { Input } from '../../components/input';
4
4
  import { Tooltip } from '../../components/tooltip';
5
5
  import { Tabs } from '../../components/tabs';
@@ -61,6 +61,7 @@ const AdvancedSettingsTab: React.FC = () => {
61
61
  <AutoUpdatePeersCore />
62
62
  <ResetDeviceSyncInfos />
63
63
  <DeleteLocalDatabase />
64
+ <ImportOldPeersData />
64
65
  </>
65
66
  );
66
67
  };
@@ -324,6 +325,210 @@ const DeleteLocalDatabase: React.FC = () => {
324
325
  );
325
326
  }
326
327
 
328
+ const IMPORT_TOOL_ID = '00mh0wlipkdbeaw8imptsk001';
329
+
330
+ interface IDryRunResult {
331
+ totalRecords: number;
332
+ totalTasks: number;
333
+ totalLogEntries: number;
334
+ groups: {
335
+ groupId: string;
336
+ groupName: string;
337
+ context: 'personal' | 'home';
338
+ taskCount: number;
339
+ activeCount: number;
340
+ doneCount: number;
341
+ rootTaskCount: number;
342
+ subtaskCount: number;
343
+ logEntryCount: number;
344
+ }[];
345
+ statusMapping: {
346
+ inProgressToDone: number;
347
+ inProgressKeep: number;
348
+ queuedToDone: number;
349
+ queuedKeep: number;
350
+ backlogToDone: number;
351
+ backlogKeep: number;
352
+ noStatusToDone: number;
353
+ noStatusToBacklog: number;
354
+ };
355
+ userMapping: {
356
+ mark: number;
357
+ blair: number;
358
+ other: number;
359
+ };
360
+ warnings: string[];
361
+ }
362
+
363
+ const ImportOldPeersData: React.FC = () => {
364
+ const fileInputRef = useRef<HTMLInputElement>(null);
365
+ const [filePath, setFilePath] = useState<string>('');
366
+ const [fileName, setFileName] = useState<string>('');
367
+ const [loading, setLoading] = useState(false);
368
+ const [result, setResult] = useState<IDryRunResult | null>(null);
369
+ const [error, setError] = useState<string>('');
370
+
371
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
372
+ const file = e.target.files?.[0];
373
+ if (!file) return;
374
+ // Electron exposes .path on File objects
375
+ const path = (file as any).path as string | undefined;
376
+ if (path) {
377
+ setFilePath(path);
378
+ setFileName(file.name);
379
+ } else {
380
+ setFileName(file.name);
381
+ setFilePath('');
382
+ setError('File path not available. This feature requires the Electron desktop app.');
383
+ }
384
+ setResult(null);
385
+ setError('');
386
+ };
387
+
388
+ const handleDryRun = async () => {
389
+ if (!filePath) return;
390
+ setLoading(true);
391
+ setError('');
392
+ setResult(null);
393
+ try {
394
+ const response = await rpcServerCalls.runTool(IMPORT_TOOL_ID, {
395
+ filePath,
396
+ dryRun: true,
397
+ });
398
+ if (response?.result) {
399
+ setResult(response.result);
400
+ } else {
401
+ setError('Unexpected response from import tool');
402
+ }
403
+ } catch (err) {
404
+ setError((err as Error).message || 'Failed to run dry run');
405
+ } finally {
406
+ setLoading(false);
407
+ }
408
+ };
409
+
410
+ return (
411
+ <div className="mt-4 pt-3 border-top">
412
+ <h6 className="mb-2">Import Old Peers Data</h6>
413
+ <small className="text-muted d-block mb-2">
414
+ Import tasks and log entries from an old peers JSON export file.
415
+ </small>
416
+
417
+ <div className="d-flex align-items-center gap-2 mb-2">
418
+ <input
419
+ ref={fileInputRef}
420
+ type="file"
421
+ accept=".json"
422
+ className="form-control form-control-sm"
423
+ style={{ maxWidth: 350 }}
424
+ onChange={handleFileSelect}
425
+ />
426
+ <button
427
+ className="btn btn-outline-primary btn-sm"
428
+ onClick={handleDryRun}
429
+ disabled={!filePath || loading}
430
+ >
431
+ {loading ? 'Analyzing...' : 'Dry Run'}
432
+ </button>
433
+ </div>
434
+
435
+ {error && (
436
+ <div className="alert alert-danger py-1 px-2 small mt-2">{error}</div>
437
+ )}
438
+
439
+ {result && <DryRunResults result={result} />}
440
+ </div>
441
+ );
442
+ };
443
+
444
+ const DryRunResults: React.FC<{ result: IDryRunResult }> = ({ result }) => {
445
+ const sm = result.statusMapping;
446
+ const um = result.userMapping;
447
+
448
+ return (
449
+ <div className="mt-2">
450
+ <div className="alert alert-info py-2 px-3 small">
451
+ <strong>Summary:</strong> {result.totalTasks.toLocaleString()} tasks
452
+ and {result.totalLogEntries.toLocaleString()} log entries
453
+ across {result.groups.length} groups
454
+ ({result.totalRecords.toLocaleString()} total records in file)
455
+ </div>
456
+
457
+ {result.warnings.length > 0 && (
458
+ <div className="alert alert-warning py-2 px-3 small">
459
+ <strong>Warnings:</strong>
460
+ <ul className="mb-0 ps-3">
461
+ {result.warnings.map((w, i) => <li key={i}>{w}</li>)}
462
+ </ul>
463
+ </div>
464
+ )}
465
+
466
+ <details className="mb-2">
467
+ <summary className="small fw-bold" style={{ cursor: 'pointer' }}>
468
+ Status Mapping
469
+ </summary>
470
+ <table className="table table-sm small mt-1">
471
+ <thead><tr><th>Mapping</th><th className="text-end">Count</th></tr></thead>
472
+ <tbody>
473
+ <tr><td>In-Progress → Done</td><td className="text-end">{sm.inProgressToDone.toLocaleString()}</td></tr>
474
+ <tr><td>In-Progress → In-Progress</td><td className="text-end">{sm.inProgressKeep.toLocaleString()}</td></tr>
475
+ <tr><td>Queued → Done</td><td className="text-end">{sm.queuedToDone.toLocaleString()}</td></tr>
476
+ <tr><td>Queued → Queued</td><td className="text-end">{sm.queuedKeep.toLocaleString()}</td></tr>
477
+ <tr><td>Backlog → Done</td><td className="text-end">{sm.backlogToDone.toLocaleString()}</td></tr>
478
+ <tr><td>Backlog → Backlog</td><td className="text-end">{sm.backlogKeep.toLocaleString()}</td></tr>
479
+ <tr><td>No status → Done</td><td className="text-end">{sm.noStatusToDone.toLocaleString()}</td></tr>
480
+ <tr><td>No status → Backlog</td><td className="text-end">{sm.noStatusToBacklog.toLocaleString()}</td></tr>
481
+ </tbody>
482
+ </table>
483
+ </details>
484
+
485
+ <details className="mb-2">
486
+ <summary className="small fw-bold" style={{ cursor: 'pointer' }}>
487
+ User Mapping
488
+ </summary>
489
+ <table className="table table-sm small mt-1">
490
+ <thead><tr><th>User</th><th className="text-end">Tasks</th></tr></thead>
491
+ <tbody>
492
+ <tr><td>Mark</td><td className="text-end">{um.mark.toLocaleString()}</td></tr>
493
+ <tr><td>Blair</td><td className="text-end">{um.blair.toLocaleString()}</td></tr>
494
+ <tr><td>Other → Mark</td><td className="text-end">{um.other.toLocaleString()}</td></tr>
495
+ </tbody>
496
+ </table>
497
+ </details>
498
+
499
+ <details open className="mb-2">
500
+ <summary className="small fw-bold" style={{ cursor: 'pointer' }}>
501
+ Groups ({result.groups.length})
502
+ </summary>
503
+ <table className="table table-sm small mt-1">
504
+ <thead>
505
+ <tr>
506
+ <th>Group</th>
507
+ <th>Route</th>
508
+ <th className="text-end">Tasks</th>
509
+ <th className="text-end">Active</th>
510
+ <th className="text-end">Done</th>
511
+ <th className="text-end">Logs</th>
512
+ </tr>
513
+ </thead>
514
+ <tbody>
515
+ {result.groups.map(g => (
516
+ <tr key={g.groupId}>
517
+ <td>{g.groupName}</td>
518
+ <td><span className={`badge bg-${g.context === 'home' ? 'success' : 'secondary'}`}>{g.context}</span></td>
519
+ <td className="text-end">{g.taskCount.toLocaleString()}</td>
520
+ <td className="text-end">{g.activeCount.toLocaleString()}</td>
521
+ <td className="text-end">{g.doneCount.toLocaleString()}</td>
522
+ <td className="text-end">{g.logEntryCount.toLocaleString()}</td>
523
+ </tr>
524
+ ))}
525
+ </tbody>
526
+ </table>
527
+ </details>
528
+ </div>
529
+ );
530
+ };
531
+
327
532
  const showLogoutInSettings = typeof window !== 'undefined'
328
533
  && !(window as any).electronAPI
329
534
  && !(window as any).ReactNativeWebView;
@@ -73,6 +73,7 @@ export function TabsLayoutApp() {
73
73
  }
74
74
 
75
75
  if (!userId) {
76
+ document.getElementById('appLoadingDiv')?.remove();
76
77
  return <SetupUser />;
77
78
  } else {
78
79
  return <TabsLayout userId={userId} />;