@peers-app/peers-ui 0.12.6 → 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.
- package/dist/components/group-switcher.js +3 -3
- package/dist/components/lazy-list.js +1 -1
- package/dist/globals.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/screens/packages/package-versions.js +76 -25
- package/dist/screens/settings/color-mode-dropdown.js +6 -0
- package/dist/screens/settings/settings-page.js +170 -2
- package/dist/tabs-layout/tabs-layout.js +10 -7
- package/dist/tabs-layout/tabs-state.js +4 -1
- package/package.json +3 -3
- package/src/components/group-switcher.tsx +9 -9
- package/src/components/lazy-list.tsx +1 -1
- package/src/globals.tsx +2 -2
- package/src/index.tsx +2 -1
- package/src/screens/packages/package-versions.tsx +186 -71
- package/src/screens/settings/color-mode-dropdown.tsx +7 -0
- package/src/screens/settings/settings-page.tsx +235 -2
- package/src/tabs-layout/tabs-layout.tsx +11 -10
- package/src/tabs-layout/tabs-state.ts +4 -1
|
@@ -110,17 +110,17 @@ function GroupSwitcher({ colorMode, isMobile = false }) {
|
|
|
110
110
|
maxHeight: '300px',
|
|
111
111
|
overflowY: 'auto'
|
|
112
112
|
} },
|
|
113
|
-
react_1.default.createElement("
|
|
113
|
+
react_1.default.createElement("button", { className: `dropdown-item d-flex align-items-center ${!currentGroup ? 'active' : ''}`, style: { cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left' }, onClick: () => handleGroupSelect(null) },
|
|
114
114
|
react_1.default.createElement("i", { className: "bi-person-fill me-2" }),
|
|
115
115
|
react_1.default.createElement("span", null, "Personal")),
|
|
116
116
|
allGroups.length > 0 && (react_1.default.createElement("div", { className: "dropdown-divider" })),
|
|
117
117
|
allGroups
|
|
118
118
|
.filter((g) => !g.disabled)
|
|
119
|
-
.map((group) => (react_1.default.createElement("
|
|
119
|
+
.map((group) => (react_1.default.createElement("button", { key: group.groupId, className: `dropdown-item d-flex align-items-center ${currentGroup?.groupId === group.groupId ? 'active' : ''}`, style: { cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left' }, onClick: () => handleGroupSelect(group) },
|
|
120
120
|
react_1.default.createElement("i", { className: `${getGroupIcon(group)} me-2` }),
|
|
121
121
|
react_1.default.createElement("span", { className: "text-truncate" }, group.name)))),
|
|
122
122
|
react_1.default.createElement("div", { className: "dropdown-divider" }),
|
|
123
|
-
react_1.default.createElement("
|
|
123
|
+
react_1.default.createElement("button", { className: "dropdown-item d-flex align-items-center", style: { cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left' }, onClick: handleCreateClick },
|
|
124
124
|
react_1.default.createElement("i", { className: "bi-plus-circle me-2" }),
|
|
125
125
|
react_1.default.createElement("span", null, "Create Group")))),
|
|
126
126
|
showDropdown && (react_1.default.createElement("div", { className: "position-fixed w-100 h-100", style: { top: 0, left: 0, zIndex: 999 }, onClick: () => setShowDropdown(false) }))),
|
|
@@ -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
|
|
75
|
+
}, [renderItems.length, allLoaded]);
|
|
76
76
|
(0, react_1.useEffect)(() => {
|
|
77
77
|
itemsObsAry([]);
|
|
78
78
|
setAllLoaded(false);
|
package/dist/globals.js
CHANGED
|
@@ -124,11 +124,11 @@ async function loadGlobals() {
|
|
|
124
124
|
await peers_sdk_1.rpcServerCalls.addOrUpdatePackage('updateAll');
|
|
125
125
|
}
|
|
126
126
|
await Promise.all([
|
|
127
|
-
(0, peers_sdk_1.Groups)().list().then(exports.groups),
|
|
127
|
+
(0, peers_sdk_1.Groups)().list().then(r => (0, exports.groups)(r)),
|
|
128
128
|
exports._mainContentPath.loadingPromise,
|
|
129
129
|
exports.openThreads.loadingPromise,
|
|
130
130
|
exports.threadViewOpen.loadingPromise,
|
|
131
|
-
(0, peers_sdk_1.sleep)(100),
|
|
131
|
+
(0, peers_sdk_1.sleep)(100),
|
|
132
132
|
]);
|
|
133
133
|
function updateWindowHash() {
|
|
134
134
|
const currentPath = window.location.hash.substring(1);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from "./hooks";
|
|
2
2
|
export * from "./screens/events/cron";
|
|
3
3
|
export * from "./tabs-layout/tabs-layout";
|
|
4
|
-
export { activeTabId, activeTabs, TabState } from "./tabs-layout/tabs-state";
|
|
4
|
+
export { activeTabId, activeTabs, TabState, updateActiveTabTitle, closeCurrentTab, goToTabPath } from "./tabs-layout/tabs-state";
|
|
5
|
+
export { mainContentPath } from "./globals";
|
|
5
6
|
export * from "./components/voice-indicator";
|
|
6
7
|
export * from "./components/chat-overlay";
|
|
7
8
|
export * from "./components/sortable-list";
|
package/dist/index.js
CHANGED
|
@@ -14,13 +14,18 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.activeTabs = exports.activeTabId = void 0;
|
|
17
|
+
exports.mainContentPath = exports.goToTabPath = exports.closeCurrentTab = exports.updateActiveTabTitle = exports.activeTabs = exports.activeTabId = void 0;
|
|
18
18
|
__exportStar(require("./hooks"), exports);
|
|
19
19
|
__exportStar(require("./screens/events/cron"), exports);
|
|
20
20
|
__exportStar(require("./tabs-layout/tabs-layout"), exports);
|
|
21
21
|
var tabs_state_1 = require("./tabs-layout/tabs-state");
|
|
22
22
|
Object.defineProperty(exports, "activeTabId", { enumerable: true, get: function () { return tabs_state_1.activeTabId; } });
|
|
23
23
|
Object.defineProperty(exports, "activeTabs", { enumerable: true, get: function () { return tabs_state_1.activeTabs; } });
|
|
24
|
+
Object.defineProperty(exports, "updateActiveTabTitle", { enumerable: true, get: function () { return tabs_state_1.updateActiveTabTitle; } });
|
|
25
|
+
Object.defineProperty(exports, "closeCurrentTab", { enumerable: true, get: function () { return tabs_state_1.closeCurrentTab; } });
|
|
26
|
+
Object.defineProperty(exports, "goToTabPath", { enumerable: true, get: function () { return tabs_state_1.goToTabPath; } });
|
|
27
|
+
var globals_1 = require("./globals");
|
|
28
|
+
Object.defineProperty(exports, "mainContentPath", { enumerable: true, get: function () { return globals_1.mainContentPath; } });
|
|
24
29
|
__exportStar(require("./components/voice-indicator"), exports);
|
|
25
30
|
__exportStar(require("./components/chat-overlay"), exports);
|
|
26
31
|
__exportStar(require("./components/sortable-list"), exports);
|
|
@@ -136,6 +136,28 @@ const PackageVersionsList = (props) => {
|
|
|
136
136
|
setDeleting(null);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
async function promoteVersion(pv, newTag) {
|
|
140
|
+
try {
|
|
141
|
+
const pvHash = (0, peers_sdk_1.computePackageVersionHash)(pv.version, newTag, pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
|
|
142
|
+
const updated = { ...pv, versionTag: newTag, packageVersionHash: pvHash };
|
|
143
|
+
await (0, peers_sdk_1.PackageVersions)().signAndSave(updated, { saveAsSnapshot: true });
|
|
144
|
+
refreshKey(refreshKey() + 1);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
alert(`Failed to promote version: ${err}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function updateVersion(pv, newSemver) {
|
|
151
|
+
try {
|
|
152
|
+
const pvHash = (0, peers_sdk_1.computePackageVersionHash)(newSemver, pv.versionTag || 'beta', pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
|
|
153
|
+
const updated = { ...pv, version: newSemver, packageVersionHash: pvHash };
|
|
154
|
+
await (0, peers_sdk_1.PackageVersions)().signAndSave(updated, { saveAsSnapshot: true });
|
|
155
|
+
refreshKey(refreshKey() + 1);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
alert(`Failed to update version: ${err}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
139
161
|
async function pinVersion() {
|
|
140
162
|
setPinning(true);
|
|
141
163
|
try {
|
|
@@ -168,30 +190,59 @@ const PackageVersionsList = (props) => {
|
|
|
168
190
|
sorted.length,
|
|
169
191
|
" version",
|
|
170
192
|
sorted.length !== 1 ? 's' : ''),
|
|
171
|
-
react_1.default.createElement("div", { className: "list-group mt-2" }, sorted.map(pv => {
|
|
172
|
-
const isActive = pv.packageVersionId === activeVersionId;
|
|
173
|
-
const creatorName = userMap.get(pv.createdBy);
|
|
174
|
-
return (react_1.default.createElement("div", { key: pv.packageVersionId, className: `list-group-item ${isActive ? 'list-group-item-success' : ''}` },
|
|
175
|
-
react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-between" },
|
|
176
|
-
react_1.default.createElement("div", null,
|
|
177
|
-
react_1.default.createElement("strong", { className: "me-2" },
|
|
178
|
-
"v",
|
|
179
|
-
pv.version),
|
|
180
|
-
pv.versionTag && (react_1.default.createElement("span", { className: `badge ${tagBadgeClass(pv.versionTag)} me-2` }, pv.versionTag)),
|
|
181
|
-
react_1.default.createElement("code", { className: "text-muted small me-2" }, pv.packageVersionHash?.substring(0, 8)),
|
|
182
|
-
react_1.default.createElement("small", { className: "text-muted me-2" }, formatDate(pv.createdAt)),
|
|
183
|
-
creatorName && (react_1.default.createElement("small", { className: "text-muted" },
|
|
184
|
-
"by ",
|
|
185
|
-
creatorName))),
|
|
186
|
-
react_1.default.createElement("div", { className: "d-flex gap-1" },
|
|
187
|
-
isActive ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
188
|
-
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-secondary", disabled: isPinned || pinning, onClick: () => pinVersion(), title: isPinned ? 'Already pinned' : 'Pin to this version (disable auto-updates)' },
|
|
189
|
-
pinning ? (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm" })) : (react_1.default.createElement("i", { className: "bi bi-pin-fill" })),
|
|
190
|
-
isPinned ? ' Pinned' : ' Pin'),
|
|
191
|
-
react_1.default.createElement("span", { className: "badge text-bg-success align-self-center" }, "active"))) : (react_1.default.createElement("button", { className: "btn btn-sm btn-outline-primary", disabled: activating === pv.packageVersionId, onClick: () => activateVersion(pv) },
|
|
192
|
-
activating === pv.packageVersionId ? (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-1" })) : (react_1.default.createElement("i", { className: "bi bi-check2-circle me-1" })),
|
|
193
|
-
"Activate")),
|
|
194
|
-
!isActive && (react_1.default.createElement("button", { className: "btn btn-sm btn-outline-danger", disabled: deleting === pv.packageVersionId, onClick: () => deleteVersion(pv) }, deleting === pv.packageVersionId ? (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm" })) : (react_1.default.createElement("i", { className: "bi bi-trash" }))))))));
|
|
195
|
-
}))));
|
|
193
|
+
react_1.default.createElement("div", { className: "list-group mt-2" }, sorted.map(pv => (react_1.default.createElement(VersionRow, { key: pv.packageVersionId, pv: pv, isActive: pv.packageVersionId === activeVersionId, isPinned: isPinned, pinning: pinning, activating: activating, deleting: deleting, userMap: userMap, onActivate: activateVersion, onDelete: deleteVersion, onPin: pinVersion, onPromote: promoteVersion, onUpdateVersion: updateVersion }))))));
|
|
196
194
|
};
|
|
197
195
|
exports.PackageVersionsList = PackageVersionsList;
|
|
196
|
+
const TAG_OPTIONS = ['beta', 'stable'];
|
|
197
|
+
function VersionRow(props) {
|
|
198
|
+
const { pv, isActive, isPinned, pinning, activating, deleting, userMap } = props;
|
|
199
|
+
const creatorName = userMap.get(pv.createdBy);
|
|
200
|
+
const [editingVersion, setEditingVersion] = (0, react_1.useState)(false);
|
|
201
|
+
const [versionDraft, setVersionDraft] = (0, react_1.useState)(pv.version);
|
|
202
|
+
function startEditing() {
|
|
203
|
+
setVersionDraft(pv.version);
|
|
204
|
+
setEditingVersion(true);
|
|
205
|
+
}
|
|
206
|
+
function commitVersion() {
|
|
207
|
+
const trimmed = versionDraft.trim();
|
|
208
|
+
if (trimmed && trimmed !== pv.version) {
|
|
209
|
+
props.onUpdateVersion(pv, trimmed);
|
|
210
|
+
}
|
|
211
|
+
setEditingVersion(false);
|
|
212
|
+
}
|
|
213
|
+
const currentTag = pv.versionTag || 'beta';
|
|
214
|
+
const promoteTargets = TAG_OPTIONS.filter(t => t !== currentTag);
|
|
215
|
+
return (react_1.default.createElement("div", { className: `list-group-item ${isActive ? 'list-group-item-success' : ''}` },
|
|
216
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-between" },
|
|
217
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center flex-wrap" },
|
|
218
|
+
editingVersion ? (react_1.default.createElement("input", { type: "text", className: "form-control form-control-sm me-2", style: { width: '7em' }, value: versionDraft, onChange: e => setVersionDraft(e.target.value), onBlur: commitVersion, onKeyDown: e => {
|
|
219
|
+
if (e.key === 'Enter')
|
|
220
|
+
commitVersion();
|
|
221
|
+
if (e.key === 'Escape')
|
|
222
|
+
setEditingVersion(false);
|
|
223
|
+
}, autoFocus: true })) : (react_1.default.createElement("strong", { className: "me-2", style: { cursor: 'pointer' }, onClick: startEditing, title: "Click to edit version" },
|
|
224
|
+
"v",
|
|
225
|
+
pv.version)),
|
|
226
|
+
pv.versionTag && (react_1.default.createElement("span", { className: `badge ${tagBadgeClass(pv.versionTag)} me-2` }, pv.versionTag)),
|
|
227
|
+
react_1.default.createElement("code", { className: "text-muted small me-2" }, pv.packageVersionHash?.substring(0, 8)),
|
|
228
|
+
react_1.default.createElement("small", { className: "text-muted me-2" }, formatDate(pv.createdAt)),
|
|
229
|
+
creatorName && (react_1.default.createElement("small", { className: "text-muted" },
|
|
230
|
+
"by ",
|
|
231
|
+
creatorName))),
|
|
232
|
+
react_1.default.createElement("div", { className: "d-flex gap-1 align-items-center" },
|
|
233
|
+
promoteTargets.length > 0 && (react_1.default.createElement("div", { className: "dropdown" },
|
|
234
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-info dropdown-toggle", "data-bs-toggle": "dropdown", title: "Change version tag" }, "Promote"),
|
|
235
|
+
react_1.default.createElement("ul", { className: "dropdown-menu dropdown-menu-end" }, promoteTargets.map(tag => (react_1.default.createElement("li", { key: tag },
|
|
236
|
+
react_1.default.createElement("button", { className: "dropdown-item", onClick: () => props.onPromote(pv, tag) },
|
|
237
|
+
currentTag,
|
|
238
|
+
" \u2192 ",
|
|
239
|
+
tag))))))),
|
|
240
|
+
isActive ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
241
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-secondary", disabled: isPinned || pinning, onClick: () => props.onPin(), title: isPinned ? 'Already pinned' : 'Pin to this version (disable auto-updates)' },
|
|
242
|
+
pinning ? (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm" })) : (react_1.default.createElement("i", { className: "bi bi-pin-fill" })),
|
|
243
|
+
isPinned ? ' Pinned' : ' Pin'),
|
|
244
|
+
react_1.default.createElement("span", { className: "badge text-bg-success align-self-center" }, "active"))) : (react_1.default.createElement("button", { className: "btn btn-sm btn-outline-primary", disabled: activating === pv.packageVersionId, onClick: () => props.onActivate(pv) },
|
|
245
|
+
activating === pv.packageVersionId ? (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-1" })) : (react_1.default.createElement("i", { className: "bi bi-check2-circle me-1" })),
|
|
246
|
+
"Activate")),
|
|
247
|
+
!isActive && (react_1.default.createElement("button", { className: "btn btn-sm btn-outline-danger", disabled: deleting === pv.packageVersionId, onClick: () => props.onDelete(pv) }, deleting === pv.packageVersionId ? (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm" })) : (react_1.default.createElement("i", { className: "bi bi-trash" }))))))));
|
|
248
|
+
}
|
|
@@ -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;
|
|
@@ -67,7 +67,9 @@ const SettingsPage = () => {
|
|
|
67
67
|
};
|
|
68
68
|
exports.SettingsPage = SettingsPage;
|
|
69
69
|
const UserSettingsTab = () => {
|
|
70
|
-
return (react_1.default.createElement(
|
|
70
|
+
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
71
|
+
react_1.default.createElement(ProfileSection, null),
|
|
72
|
+
react_1.default.createElement(LogoutSection, null)));
|
|
71
73
|
};
|
|
72
74
|
const AppearanceSettingsTab = () => {
|
|
73
75
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
@@ -81,7 +83,8 @@ const AdvancedSettingsTab = () => {
|
|
|
81
83
|
react_1.default.createElement(ReloadPackagesOnPageRefresh, null),
|
|
82
84
|
react_1.default.createElement(AutoUpdatePeersCore, null),
|
|
83
85
|
react_1.default.createElement(ResetDeviceSyncInfos, null),
|
|
84
|
-
react_1.default.createElement(DeleteLocalDatabase, null)
|
|
86
|
+
react_1.default.createElement(DeleteLocalDatabase, null),
|
|
87
|
+
react_1.default.createElement(ImportOldPeersData, null)));
|
|
85
88
|
};
|
|
86
89
|
const ProfileSection = () => {
|
|
87
90
|
const [deviceId] = (0, hooks_1.useObservable)(peers_sdk_1.thisDeviceId);
|
|
@@ -234,3 +237,168 @@ const DeleteLocalDatabase = () => {
|
|
|
234
237
|
}
|
|
235
238
|
} }, "Delete Local Database")));
|
|
236
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
|
+
};
|
|
390
|
+
const showLogoutInSettings = typeof window !== 'undefined'
|
|
391
|
+
&& !window.electronAPI
|
|
392
|
+
&& !window.ReactNativeWebView;
|
|
393
|
+
const LogoutSection = () => {
|
|
394
|
+
if (!showLogoutInSettings)
|
|
395
|
+
return null;
|
|
396
|
+
return (react_1.default.createElement("div", { className: "mt-4 pt-3 border-top" },
|
|
397
|
+
react_1.default.createElement("button", { className: "btn btn-outline-danger btn-sm", onClick: async () => {
|
|
398
|
+
const confirmed = confirm('Are you sure you want to logout? This will clear your credentials from this browser. ' +
|
|
399
|
+
'Make sure you have your User ID and Secret Key saved securely, or you will lose access to your account.');
|
|
400
|
+
if (!confirmed)
|
|
401
|
+
return;
|
|
402
|
+
await peers_sdk_1.rpcServerCalls.logout();
|
|
403
|
+
} }, "Logout")));
|
|
404
|
+
};
|
|
@@ -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 {
|
|
@@ -96,14 +97,13 @@ function TabsLayout(props) {
|
|
|
96
97
|
userContextInitialized = true;
|
|
97
98
|
}
|
|
98
99
|
const loaded = (0, hooks_1.usePromise)(async () => {
|
|
99
|
-
await (0, globals_1.loadGlobals)()
|
|
100
|
-
await (0, routes_loader_1.loadAllRoutes)()
|
|
101
|
-
// Wait for tab state and welcome modal state to load from database
|
|
100
|
+
await (0, globals_1.loadGlobals)();
|
|
101
|
+
await (0, routes_loader_1.loadAllRoutes)();
|
|
102
102
|
await Promise.all([
|
|
103
103
|
tabs_state_1.activeTabs.loadingPromise,
|
|
104
104
|
tabs_state_1.activeTabId.loadingPromise,
|
|
105
105
|
tabs_state_1.recentlyUsedApps.loadingPromise,
|
|
106
|
-
peers_sdk_1.hasShownWelcomeModal.loadingPromise
|
|
106
|
+
peers_sdk_1.hasShownWelcomeModal.loadingPromise,
|
|
107
107
|
]);
|
|
108
108
|
return true;
|
|
109
109
|
});
|
|
@@ -422,10 +422,13 @@ function AppSection({ title, iconClassName, apps, onOpenApp, isMobile }) {
|
|
|
422
422
|
function AppCard({ appItem, onOpenApp, isMobile }) {
|
|
423
423
|
const [_colorMode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
|
|
424
424
|
const isDark = _colorMode === 'dark';
|
|
425
|
-
return (react_1.default.createElement("
|
|
425
|
+
return (react_1.default.createElement("button", { className: "d-flex flex-column align-items-center text-center", style: {
|
|
426
426
|
cursor: 'pointer',
|
|
427
427
|
width: isMobile ? '80px' : '90px',
|
|
428
|
-
transition: 'all 0.15s ease'
|
|
428
|
+
transition: 'all 0.15s ease',
|
|
429
|
+
background: 'none',
|
|
430
|
+
border: 'none',
|
|
431
|
+
padding: 0,
|
|
429
432
|
}, title: appItem.name, onMouseEnter: (e) => {
|
|
430
433
|
e.currentTarget.style.transform = 'scale(1.05)';
|
|
431
434
|
}, onMouseLeave: (e) => {
|
|
@@ -442,7 +445,7 @@ function AppCard({ appItem, onOpenApp, isMobile }) {
|
|
|
442
445
|
} },
|
|
443
446
|
react_1.default.createElement("i", { className: appItem.iconClassName, style: {
|
|
444
447
|
fontSize: isMobile ? '28px' : '32px',
|
|
445
|
-
color: isDark ? '#0d6efd' : '#0d6efd'
|
|
448
|
+
color: isDark ? '#0d6efd' : '#0d6efd'
|
|
446
449
|
} })),
|
|
447
450
|
react_1.default.createElement("span", { className: isDark ? 'text-light' : 'text-dark', style: {
|
|
448
451
|
fontSize: isMobile ? '11px' : '12px',
|
|
@@ -164,7 +164,10 @@ function determineAppFromPath(path) {
|
|
|
164
164
|
const pkg = _allPackages.find(p => p.packageId === packageId);
|
|
165
165
|
if (pkg) {
|
|
166
166
|
const subPath = parts.slice(1).join('/');
|
|
167
|
-
const navItem = pkg.appNavs?.find(n =>
|
|
167
|
+
const navItem = pkg.appNavs?.find(n => {
|
|
168
|
+
const navPath = n.navigationPath || n.name.toLowerCase();
|
|
169
|
+
return subPath === navPath || subPath.startsWith(navPath + '/');
|
|
170
|
+
});
|
|
168
171
|
if (navItem) {
|
|
169
172
|
return {
|
|
170
173
|
navItem,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
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",
|
|
@@ -106,14 +106,14 @@ export function GroupSwitcher({ colorMode, isMobile = false }: GroupSwitcherProp
|
|
|
106
106
|
}}
|
|
107
107
|
>
|
|
108
108
|
{/* Personal Group */}
|
|
109
|
-
<
|
|
109
|
+
<button
|
|
110
110
|
className={`dropdown-item d-flex align-items-center ${!currentGroup ? 'active' : ''}`}
|
|
111
|
-
style={{ cursor: 'pointer' }}
|
|
111
|
+
style={{ cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left' }}
|
|
112
112
|
onClick={() => handleGroupSelect(null)}
|
|
113
113
|
>
|
|
114
114
|
<i className="bi-person-fill me-2" />
|
|
115
115
|
<span>Personal</span>
|
|
116
|
-
</
|
|
116
|
+
</button>
|
|
117
117
|
|
|
118
118
|
{/* Divider */}
|
|
119
119
|
{allGroups.length > 0 && (
|
|
@@ -124,27 +124,27 @@ export function GroupSwitcher({ colorMode, isMobile = false }: GroupSwitcherProp
|
|
|
124
124
|
{allGroups
|
|
125
125
|
.filter((g: IGroup) => !g.disabled)
|
|
126
126
|
.map((group: IGroup) => (
|
|
127
|
-
<
|
|
127
|
+
<button
|
|
128
128
|
key={group.groupId}
|
|
129
129
|
className={`dropdown-item d-flex align-items-center ${currentGroup?.groupId === group.groupId ? 'active' : ''}`}
|
|
130
|
-
style={{ cursor: 'pointer' }}
|
|
130
|
+
style={{ cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left' }}
|
|
131
131
|
onClick={() => handleGroupSelect(group)}
|
|
132
132
|
>
|
|
133
133
|
<i className={`${getGroupIcon(group)} me-2`} />
|
|
134
134
|
<span className="text-truncate">{group.name}</span>
|
|
135
|
-
</
|
|
135
|
+
</button>
|
|
136
136
|
))}
|
|
137
137
|
|
|
138
138
|
{/* Create New Group */}
|
|
139
139
|
<div className="dropdown-divider" />
|
|
140
|
-
<
|
|
140
|
+
<button
|
|
141
141
|
className="dropdown-item d-flex align-items-center"
|
|
142
|
-
style={{ cursor: 'pointer' }}
|
|
142
|
+
style={{ cursor: 'pointer', border: 'none', background: 'none', width: '100%', textAlign: 'left' }}
|
|
143
143
|
onClick={handleCreateClick}
|
|
144
144
|
>
|
|
145
145
|
<i className="bi-plus-circle me-2" />
|
|
146
146
|
<span>Create Group</span>
|
|
147
|
-
</
|
|
147
|
+
</button>
|
|
148
148
|
</div>
|
|
149
149
|
)}
|
|
150
150
|
|
|
@@ -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
|
|
50
|
+
}, [renderItems.length, allLoaded]);
|
|
51
51
|
|
|
52
52
|
useEffect(() => {
|
|
53
53
|
itemsObsAry([]);
|
package/src/globals.tsx
CHANGED
|
@@ -134,11 +134,11 @@ export async function loadGlobals() {
|
|
|
134
134
|
await rpcServerCalls.addOrUpdatePackage('updateAll');
|
|
135
135
|
}
|
|
136
136
|
await Promise.all([
|
|
137
|
-
Groups().list().then(groups),
|
|
137
|
+
Groups().list().then(r => groups(r)),
|
|
138
138
|
_mainContentPath.loadingPromise,
|
|
139
139
|
openThreads.loadingPromise,
|
|
140
140
|
threadViewOpen.loadingPromise,
|
|
141
|
-
sleep(100),
|
|
141
|
+
sleep(100),
|
|
142
142
|
]);
|
|
143
143
|
|
|
144
144
|
function updateWindowHash() {
|
package/src/index.tsx
CHANGED
|
@@ -2,7 +2,8 @@ export * from "./hooks";
|
|
|
2
2
|
export * from "./screens/events/cron";
|
|
3
3
|
|
|
4
4
|
export * from "./tabs-layout/tabs-layout";
|
|
5
|
-
export { activeTabId, activeTabs, TabState } from "./tabs-layout/tabs-state";
|
|
5
|
+
export { activeTabId, activeTabs, TabState, updateActiveTabTitle, closeCurrentTab, goToTabPath } from "./tabs-layout/tabs-state";
|
|
6
|
+
export { mainContentPath } from "./globals";
|
|
6
7
|
export * from "./components/voice-indicator";
|
|
7
8
|
export * from "./components/chat-overlay";
|
|
8
9
|
export * from "./components/sortable-list";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import { IDoc, IPackage, IPackageVersion, Packages, PackageVersions, Users } from "@peers-app/peers-sdk";
|
|
2
|
+
import { computePackageVersionHash, IDoc, IPackage, IPackageVersion, Packages, PackageVersions, Users } from "@peers-app/peers-sdk";
|
|
3
3
|
import { useObservable, useObservableState, usePromise } from "../../hooks";
|
|
4
4
|
|
|
5
5
|
function formatDate(iso: string): string {
|
|
@@ -89,6 +89,28 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
async function promoteVersion(pv: IPackageVersion, newTag: string) {
|
|
93
|
+
try {
|
|
94
|
+
const pvHash = computePackageVersionHash(pv.version, newTag, pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
|
|
95
|
+
const updated = { ...pv, versionTag: newTag, packageVersionHash: pvHash };
|
|
96
|
+
await PackageVersions().signAndSave(updated, { saveAsSnapshot: true });
|
|
97
|
+
refreshKey(refreshKey() + 1);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
alert(`Failed to promote version: ${err}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function updateVersion(pv: IPackageVersion, newSemver: string) {
|
|
104
|
+
try {
|
|
105
|
+
const pvHash = computePackageVersionHash(newSemver, pv.versionTag || 'beta', pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
|
|
106
|
+
const updated = { ...pv, version: newSemver, packageVersionHash: pvHash };
|
|
107
|
+
await PackageVersions().signAndSave(updated, { saveAsSnapshot: true });
|
|
108
|
+
refreshKey(refreshKey() + 1);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
alert(`Failed to update version: ${err}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
async function pinVersion() {
|
|
93
115
|
setPinning(true);
|
|
94
116
|
try {
|
|
@@ -124,81 +146,174 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
|
|
|
124
146
|
<div>
|
|
125
147
|
<small className="text-muted">{sorted.length} version{sorted.length !== 1 ? 's' : ''}</small>
|
|
126
148
|
<div className="list-group mt-2">
|
|
127
|
-
{sorted.map(pv =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
{sorted.map(pv => (
|
|
150
|
+
<VersionRow
|
|
151
|
+
key={pv.packageVersionId}
|
|
152
|
+
pv={pv}
|
|
153
|
+
isActive={pv.packageVersionId === activeVersionId}
|
|
154
|
+
isPinned={isPinned}
|
|
155
|
+
pinning={pinning}
|
|
156
|
+
activating={activating}
|
|
157
|
+
deleting={deleting}
|
|
158
|
+
userMap={userMap}
|
|
159
|
+
onActivate={activateVersion}
|
|
160
|
+
onDelete={deleteVersion}
|
|
161
|
+
onPin={pinVersion}
|
|
162
|
+
onPromote={promoteVersion}
|
|
163
|
+
onUpdateVersion={updateVersion}
|
|
164
|
+
/>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const TAG_OPTIONS = ['beta', 'stable'];
|
|
172
|
+
|
|
173
|
+
function VersionRow(props: {
|
|
174
|
+
pv: IPackageVersion;
|
|
175
|
+
isActive: boolean;
|
|
176
|
+
isPinned: boolean;
|
|
177
|
+
pinning: boolean;
|
|
178
|
+
activating: string | null;
|
|
179
|
+
deleting: string | null;
|
|
180
|
+
userMap: Map<string, string>;
|
|
181
|
+
onActivate: (pv: IPackageVersion) => void;
|
|
182
|
+
onDelete: (pv: IPackageVersion) => void;
|
|
183
|
+
onPin: () => void;
|
|
184
|
+
onPromote: (pv: IPackageVersion, newTag: string) => void;
|
|
185
|
+
onUpdateVersion: (pv: IPackageVersion, newSemver: string) => void;
|
|
186
|
+
}) {
|
|
187
|
+
const { pv, isActive, isPinned, pinning, activating, deleting, userMap } = props;
|
|
188
|
+
const creatorName = userMap.get(pv.createdBy);
|
|
189
|
+
const [editingVersion, setEditingVersion] = useState(false);
|
|
190
|
+
const [versionDraft, setVersionDraft] = useState(pv.version);
|
|
191
|
+
|
|
192
|
+
function startEditing() {
|
|
193
|
+
setVersionDraft(pv.version);
|
|
194
|
+
setEditingVersion(true);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function commitVersion() {
|
|
198
|
+
const trimmed = versionDraft.trim();
|
|
199
|
+
if (trimmed && trimmed !== pv.version) {
|
|
200
|
+
props.onUpdateVersion(pv, trimmed);
|
|
201
|
+
}
|
|
202
|
+
setEditingVersion(false);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const currentTag = pv.versionTag || 'beta';
|
|
206
|
+
const promoteTargets = TAG_OPTIONS.filter(t => t !== currentTag);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className={`list-group-item ${isActive ? 'list-group-item-success' : ''}`}>
|
|
210
|
+
<div className="d-flex align-items-center justify-content-between">
|
|
211
|
+
<div className="d-flex align-items-center flex-wrap">
|
|
212
|
+
{editingVersion ? (
|
|
213
|
+
<input
|
|
214
|
+
type="text"
|
|
215
|
+
className="form-control form-control-sm me-2"
|
|
216
|
+
style={{ width: '7em' }}
|
|
217
|
+
value={versionDraft}
|
|
218
|
+
onChange={e => setVersionDraft(e.target.value)}
|
|
219
|
+
onBlur={commitVersion}
|
|
220
|
+
onKeyDown={e => {
|
|
221
|
+
if (e.key === 'Enter') commitVersion();
|
|
222
|
+
if (e.key === 'Escape') setEditingVersion(false);
|
|
223
|
+
}}
|
|
224
|
+
autoFocus
|
|
225
|
+
/>
|
|
226
|
+
) : (
|
|
227
|
+
<strong
|
|
228
|
+
className="me-2"
|
|
229
|
+
style={{ cursor: 'pointer' }}
|
|
230
|
+
onClick={startEditing}
|
|
231
|
+
title="Click to edit version"
|
|
134
232
|
>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<i className="bi bi-pin-fill"></i>
|
|
165
|
-
)}
|
|
166
|
-
{isPinned ? ' Pinned' : ' Pin'}
|
|
167
|
-
</button>
|
|
168
|
-
<span className="badge text-bg-success align-self-center">active</span>
|
|
169
|
-
</>) : (
|
|
233
|
+
v{pv.version}
|
|
234
|
+
</strong>
|
|
235
|
+
)}
|
|
236
|
+
{pv.versionTag && (
|
|
237
|
+
<span className={`badge ${tagBadgeClass(pv.versionTag)} me-2`}>
|
|
238
|
+
{pv.versionTag}
|
|
239
|
+
</span>
|
|
240
|
+
)}
|
|
241
|
+
<code className="text-muted small me-2">
|
|
242
|
+
{pv.packageVersionHash?.substring(0, 8)}
|
|
243
|
+
</code>
|
|
244
|
+
<small className="text-muted me-2">{formatDate(pv.createdAt)}</small>
|
|
245
|
+
{creatorName && (
|
|
246
|
+
<small className="text-muted">by {creatorName}</small>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
<div className="d-flex gap-1 align-items-center">
|
|
250
|
+
{promoteTargets.length > 0 && (
|
|
251
|
+
<div className="dropdown">
|
|
252
|
+
<button
|
|
253
|
+
className="btn btn-sm btn-outline-info dropdown-toggle"
|
|
254
|
+
data-bs-toggle="dropdown"
|
|
255
|
+
title="Change version tag"
|
|
256
|
+
>
|
|
257
|
+
Promote
|
|
258
|
+
</button>
|
|
259
|
+
<ul className="dropdown-menu dropdown-menu-end">
|
|
260
|
+
{promoteTargets.map(tag => (
|
|
261
|
+
<li key={tag}>
|
|
170
262
|
<button
|
|
171
|
-
className="
|
|
172
|
-
|
|
173
|
-
onClick={() => activateVersion(pv)}
|
|
263
|
+
className="dropdown-item"
|
|
264
|
+
onClick={() => props.onPromote(pv, tag)}
|
|
174
265
|
>
|
|
175
|
-
{
|
|
176
|
-
<span className="spinner-border spinner-border-sm me-1" />
|
|
177
|
-
) : (
|
|
178
|
-
<i className="bi bi-check2-circle me-1"></i>
|
|
179
|
-
)}
|
|
180
|
-
Activate
|
|
266
|
+
{currentTag} → {tag}
|
|
181
267
|
</button>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
className="btn btn-sm btn-outline-danger"
|
|
186
|
-
disabled={deleting === pv.packageVersionId}
|
|
187
|
-
onClick={() => deleteVersion(pv)}
|
|
188
|
-
>
|
|
189
|
-
{deleting === pv.packageVersionId ? (
|
|
190
|
-
<span className="spinner-border spinner-border-sm" />
|
|
191
|
-
) : (
|
|
192
|
-
<i className="bi bi-trash"></i>
|
|
193
|
-
)}
|
|
194
|
-
</button>
|
|
195
|
-
)}
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
268
|
+
</li>
|
|
269
|
+
))}
|
|
270
|
+
</ul>
|
|
198
271
|
</div>
|
|
199
|
-
)
|
|
200
|
-
|
|
272
|
+
)}
|
|
273
|
+
{isActive ? (<>
|
|
274
|
+
<button
|
|
275
|
+
className="btn btn-sm btn-outline-secondary"
|
|
276
|
+
disabled={isPinned || pinning}
|
|
277
|
+
onClick={() => props.onPin()}
|
|
278
|
+
title={isPinned ? 'Already pinned' : 'Pin to this version (disable auto-updates)'}
|
|
279
|
+
>
|
|
280
|
+
{pinning ? (
|
|
281
|
+
<span className="spinner-border spinner-border-sm" />
|
|
282
|
+
) : (
|
|
283
|
+
<i className="bi bi-pin-fill"></i>
|
|
284
|
+
)}
|
|
285
|
+
{isPinned ? ' Pinned' : ' Pin'}
|
|
286
|
+
</button>
|
|
287
|
+
<span className="badge text-bg-success align-self-center">active</span>
|
|
288
|
+
</>) : (
|
|
289
|
+
<button
|
|
290
|
+
className="btn btn-sm btn-outline-primary"
|
|
291
|
+
disabled={activating === pv.packageVersionId}
|
|
292
|
+
onClick={() => props.onActivate(pv)}
|
|
293
|
+
>
|
|
294
|
+
{activating === pv.packageVersionId ? (
|
|
295
|
+
<span className="spinner-border spinner-border-sm me-1" />
|
|
296
|
+
) : (
|
|
297
|
+
<i className="bi bi-check2-circle me-1"></i>
|
|
298
|
+
)}
|
|
299
|
+
Activate
|
|
300
|
+
</button>
|
|
301
|
+
)}
|
|
302
|
+
{!isActive && (
|
|
303
|
+
<button
|
|
304
|
+
className="btn btn-sm btn-outline-danger"
|
|
305
|
+
disabled={deleting === pv.packageVersionId}
|
|
306
|
+
onClick={() => props.onDelete(pv)}
|
|
307
|
+
>
|
|
308
|
+
{deleting === pv.packageVersionId ? (
|
|
309
|
+
<span className="spinner-border spinner-border-sm" />
|
|
310
|
+
) : (
|
|
311
|
+
<i className="bi bi-trash"></i>
|
|
312
|
+
)}
|
|
313
|
+
</button>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
201
316
|
</div>
|
|
202
317
|
</div>
|
|
203
318
|
);
|
|
204
|
-
}
|
|
319
|
+
}
|
|
@@ -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';
|
|
@@ -36,7 +36,10 @@ export const SettingsPage: React.FC = () => {
|
|
|
36
36
|
|
|
37
37
|
const UserSettingsTab: React.FC = () => {
|
|
38
38
|
return (
|
|
39
|
-
|
|
39
|
+
<>
|
|
40
|
+
<ProfileSection />
|
|
41
|
+
<LogoutSection />
|
|
42
|
+
</>
|
|
40
43
|
);
|
|
41
44
|
};
|
|
42
45
|
|
|
@@ -58,6 +61,7 @@ const AdvancedSettingsTab: React.FC = () => {
|
|
|
58
61
|
<AutoUpdatePeersCore />
|
|
59
62
|
<ResetDeviceSyncInfos />
|
|
60
63
|
<DeleteLocalDatabase />
|
|
64
|
+
<ImportOldPeersData />
|
|
61
65
|
</>
|
|
62
66
|
);
|
|
63
67
|
};
|
|
@@ -319,4 +323,233 @@ const DeleteLocalDatabase: React.FC = () => {
|
|
|
319
323
|
</button>
|
|
320
324
|
</div>
|
|
321
325
|
);
|
|
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
|
+
|
|
532
|
+
const showLogoutInSettings = typeof window !== 'undefined'
|
|
533
|
+
&& !(window as any).electronAPI
|
|
534
|
+
&& !(window as any).ReactNativeWebView;
|
|
535
|
+
|
|
536
|
+
const LogoutSection: React.FC = () => {
|
|
537
|
+
if (!showLogoutInSettings) return null;
|
|
538
|
+
return (
|
|
539
|
+
<div className="mt-4 pt-3 border-top">
|
|
540
|
+
<button
|
|
541
|
+
className="btn btn-outline-danger btn-sm"
|
|
542
|
+
onClick={async () => {
|
|
543
|
+
const confirmed = confirm(
|
|
544
|
+
'Are you sure you want to logout? This will clear your credentials from this browser. ' +
|
|
545
|
+
'Make sure you have your User ID and Secret Key saved securely, or you will lose access to your account.'
|
|
546
|
+
);
|
|
547
|
+
if (!confirmed) return;
|
|
548
|
+
await rpcServerCalls.logout();
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
Logout
|
|
552
|
+
</button>
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
322
555
|
}
|
|
@@ -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} />;
|
|
@@ -87,14 +88,13 @@ export function TabsLayout(props: { userId: string }) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
const loaded = usePromise(async () => {
|
|
90
|
-
await loadGlobals()
|
|
91
|
-
await loadAllRoutes()
|
|
92
|
-
// Wait for tab state and welcome modal state to load from database
|
|
91
|
+
await loadGlobals();
|
|
92
|
+
await loadAllRoutes();
|
|
93
93
|
await Promise.all([
|
|
94
94
|
activeTabs.loadingPromise,
|
|
95
95
|
activeTabId.loadingPromise,
|
|
96
96
|
recentlyUsedApps.loadingPromise,
|
|
97
|
-
hasShownWelcomeModal.loadingPromise
|
|
97
|
+
hasShownWelcomeModal.loadingPromise,
|
|
98
98
|
]);
|
|
99
99
|
return true;
|
|
100
100
|
});
|
|
@@ -730,12 +730,15 @@ function AppCard({ appItem, onOpenApp, isMobile }: AppCardProps) {
|
|
|
730
730
|
const isDark = _colorMode === 'dark';
|
|
731
731
|
|
|
732
732
|
return (
|
|
733
|
-
<
|
|
733
|
+
<button
|
|
734
734
|
className="d-flex flex-column align-items-center text-center"
|
|
735
735
|
style={{
|
|
736
736
|
cursor: 'pointer',
|
|
737
737
|
width: isMobile ? '80px' : '90px',
|
|
738
|
-
transition: 'all 0.15s ease'
|
|
738
|
+
transition: 'all 0.15s ease',
|
|
739
|
+
background: 'none',
|
|
740
|
+
border: 'none',
|
|
741
|
+
padding: 0,
|
|
739
742
|
}}
|
|
740
743
|
title={appItem.name}
|
|
741
744
|
onMouseEnter={(e) => {
|
|
@@ -746,7 +749,6 @@ function AppCard({ appItem, onOpenApp, isMobile }: AppCardProps) {
|
|
|
746
749
|
}}
|
|
747
750
|
onClick={() => onOpenApp(appItem)}
|
|
748
751
|
>
|
|
749
|
-
{/* Icon Container */}
|
|
750
752
|
<div
|
|
751
753
|
className="d-flex align-items-center justify-content-center mb-2"
|
|
752
754
|
style={{
|
|
@@ -763,12 +765,11 @@ function AppCard({ appItem, onOpenApp, isMobile }: AppCardProps) {
|
|
|
763
765
|
className={appItem.iconClassName}
|
|
764
766
|
style={{
|
|
765
767
|
fontSize: isMobile ? '28px' : '32px',
|
|
766
|
-
color: isDark ? '#0d6efd' : '#0d6efd'
|
|
768
|
+
color: isDark ? '#0d6efd' : '#0d6efd'
|
|
767
769
|
}}
|
|
768
770
|
/>
|
|
769
771
|
</div>
|
|
770
772
|
|
|
771
|
-
{/* Title */}
|
|
772
773
|
<span
|
|
773
774
|
className={isDark ? 'text-light' : 'text-dark'}
|
|
774
775
|
style={{
|
|
@@ -783,6 +784,6 @@ function AppCard({ appItem, onOpenApp, isMobile }: AppCardProps) {
|
|
|
783
784
|
>
|
|
784
785
|
{appItem.displayName}
|
|
785
786
|
</span>
|
|
786
|
-
</
|
|
787
|
+
</button>
|
|
787
788
|
);
|
|
788
789
|
}
|
|
@@ -194,7 +194,10 @@ export function determineAppFromPath(path: string): AppInfo | undefined {
|
|
|
194
194
|
const pkg = _allPackages.find(p => p.packageId === packageId);
|
|
195
195
|
if (pkg) {
|
|
196
196
|
const subPath = parts.slice(1).join('/');
|
|
197
|
-
const navItem = pkg.appNavs?.find(n =>
|
|
197
|
+
const navItem = pkg.appNavs?.find(n => {
|
|
198
|
+
const navPath = n.navigationPath || n.name.toLowerCase();
|
|
199
|
+
return subPath === navPath || subPath.startsWith(navPath + '/');
|
|
200
|
+
});
|
|
198
201
|
if (navItem) {
|
|
199
202
|
return {
|
|
200
203
|
navItem,
|