@peers-app/peers-ui 0.9.4 → 0.10.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/screens/packages/package-details.js +11 -2
- package/dist/screens/packages/package-info.d.ts +1 -0
- package/dist/screens/packages/package-info.js +120 -1
- package/dist/screens/packages/package-list.js +15 -0
- package/dist/screens/packages/package-versions.d.ts +5 -0
- package/dist/screens/packages/package-versions.js +197 -0
- package/dist/system-apps/index.js +0 -3
- package/dist/ui-router/routes-loader.d.ts +4 -7
- package/dist/ui-router/routes-loader.js +14 -7
- package/dist/ui-router/ui-loader.js +17 -11
- package/package.json +3 -3
- package/src/screens/packages/package-details.tsx +13 -1
- package/src/screens/packages/package-info.tsx +162 -9
- package/src/screens/packages/package-list.tsx +25 -2
- package/src/screens/packages/package-versions.tsx +204 -0
- package/src/system-apps/index.ts +0 -3
- package/src/ui-router/routes-loader.ts +16 -8
- package/src/ui-router/ui-loader.tsx +15 -13
|
@@ -10,11 +10,13 @@ const loading_indicator_1 = require("../../components/loading-indicator");
|
|
|
10
10
|
const save_button_1 = require("../../components/save-button");
|
|
11
11
|
const tabs_1 = require("../../components/tabs");
|
|
12
12
|
const package_info_1 = require("./package-info");
|
|
13
|
+
const package_versions_1 = require("./package-versions");
|
|
13
14
|
const hooks_1 = require("../../hooks");
|
|
14
15
|
const input_1 = require("../../components/input");
|
|
15
16
|
const tabs_state_1 = require("../../tabs-layout/tabs-state");
|
|
16
17
|
const PackageDetails = (props) => {
|
|
17
18
|
const refresh = (0, hooks_1.useObservableState)(Date.now());
|
|
19
|
+
const saveDeviceTagRef = react_1.default.useRef(null);
|
|
18
20
|
const pkg = (0, hooks_1.usePromise)(async () => {
|
|
19
21
|
const pkg = await (0, peers_sdk_1.Packages)().get(props.packageId);
|
|
20
22
|
if (!pkg) {
|
|
@@ -62,13 +64,20 @@ const PackageDetails = (props) => {
|
|
|
62
64
|
react_1.default.createElement("h4", null,
|
|
63
65
|
react_1.default.createElement(input_1.Input, { key: pkg.packageId, className: 'border border-0', style: { width: '100%', outline: 'none', backgroundColor: 'transparent' }, value: pkg.qs.name }))),
|
|
64
66
|
react_1.default.createElement("div", null,
|
|
65
|
-
react_1.default.createElement(save_button_1.SaveButton, { key: pkg.packageId, doc: pkg,
|
|
67
|
+
react_1.default.createElement(save_button_1.SaveButton, { key: pkg.packageId, doc: pkg, onClick: async () => {
|
|
68
|
+
await pkg.save();
|
|
69
|
+
await saveDeviceTagRef.current?.();
|
|
70
|
+
}, addActions: [
|
|
66
71
|
...addActions
|
|
67
72
|
] }))),
|
|
68
73
|
react_1.default.createElement(tabs_1.Tabs, { key: pkg.packageId, tabs: [
|
|
69
74
|
{
|
|
70
75
|
name: 'Info', content: react_1.default.createElement(tabs_1.ScreenTabBody, null,
|
|
71
|
-
react_1.default.createElement(package_info_1.PackageInfo, { pkg: pkg }))
|
|
76
|
+
react_1.default.createElement(package_info_1.PackageInfo, { pkg: pkg, saveDeviceTagRef: saveDeviceTagRef }))
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'Versions', content: react_1.default.createElement(tabs_1.ScreenTabBody, null,
|
|
80
|
+
react_1.default.createElement(package_versions_1.PackageVersionsList, { pkg: pkg }))
|
|
72
81
|
},
|
|
73
82
|
{
|
|
74
83
|
name: 'Components', content: react_1.default.createElement(tabs_1.ScreenTabBody, null, "TODO - show all of the different components in the package")
|
|
@@ -9,8 +9,87 @@ const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
|
9
9
|
const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
|
|
10
10
|
const tooltip_1 = require("../../components/tooltip");
|
|
11
11
|
const input_1 = require("../../components/input");
|
|
12
|
+
const hooks_1 = require("../../hooks");
|
|
13
|
+
const deviceVersionTagVar = (0, peers_sdk_1.groupDeviceVar)('deviceVersionTag');
|
|
12
14
|
const PackageInfo = (props) => {
|
|
13
15
|
const { pkg } = props;
|
|
16
|
+
const [activeVersionId] = (0, hooks_1.useObservable)(pkg.qs.activePackageVersionId);
|
|
17
|
+
const [versionFollowRange] = (0, hooks_1.useObservable)(pkg.qs.versionFollowRange);
|
|
18
|
+
const [followVersionTags] = (0, hooks_1.useObservable)(pkg.qs.followVersionTags);
|
|
19
|
+
const [deviceTag] = (0, hooks_1.useObservable)(deviceVersionTagVar);
|
|
20
|
+
const [deviceTagDraft, setDeviceTagDraft] = react_1.default.useState(deviceTag || '');
|
|
21
|
+
const savingRef = react_1.default.useRef(false);
|
|
22
|
+
react_1.default.useEffect(() => {
|
|
23
|
+
if (!savingRef.current) {
|
|
24
|
+
setDeviceTagDraft(deviceTag || '');
|
|
25
|
+
}
|
|
26
|
+
}, [deviceTag]);
|
|
27
|
+
const deviceTagDirty = deviceTagDraft.trim() !== (deviceTag || '');
|
|
28
|
+
const prevDirtyRef = react_1.default.useRef(false);
|
|
29
|
+
react_1.default.useEffect(() => {
|
|
30
|
+
if (deviceTagDirty && !prevDirtyRef.current) {
|
|
31
|
+
pkg.q((pkg.q() || 0) + 1);
|
|
32
|
+
}
|
|
33
|
+
else if (!deviceTagDirty && prevDirtyRef.current) {
|
|
34
|
+
pkg.q(Math.max(0, (pkg.q() || 0) - 1));
|
|
35
|
+
}
|
|
36
|
+
prevDirtyRef.current = deviceTagDirty;
|
|
37
|
+
}, [deviceTagDirty]);
|
|
38
|
+
if (props.saveDeviceTagRef) {
|
|
39
|
+
props.saveDeviceTagRef.current = async () => {
|
|
40
|
+
const val = deviceTagDraft.trim();
|
|
41
|
+
if (val !== (deviceTag || '')) {
|
|
42
|
+
savingRef.current = true;
|
|
43
|
+
try {
|
|
44
|
+
if (val) {
|
|
45
|
+
deviceVersionTagVar(val);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
await deviceVersionTagVar.delete();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
savingRef.current = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const activeVersion = (0, hooks_1.usePromise)(async () => {
|
|
58
|
+
if (!activeVersionId)
|
|
59
|
+
return null;
|
|
60
|
+
return (0, peers_sdk_1.PackageVersions)().get(activeVersionId);
|
|
61
|
+
}, undefined, [activeVersionId]);
|
|
62
|
+
const newerLevel = (0, hooks_1.usePromise)(async () => {
|
|
63
|
+
if (!activeVersionId)
|
|
64
|
+
return 'uptodate';
|
|
65
|
+
const all = await (0, peers_sdk_1.PackageVersions)().list({ packageId: pkg.packageId });
|
|
66
|
+
const active = all.find(v => v.packageVersionId === activeVersionId);
|
|
67
|
+
if (!active?.version)
|
|
68
|
+
return 'uptodate';
|
|
69
|
+
const parse = (v) => v.split('.').map(Number);
|
|
70
|
+
const [aMaj, aMin, aPat] = parse(active.version);
|
|
71
|
+
let highest = null;
|
|
72
|
+
for (const v of all) {
|
|
73
|
+
if (!v.version || v.packageVersionId === activeVersionId)
|
|
74
|
+
continue;
|
|
75
|
+
const [maj, min, pat] = parse(v.version);
|
|
76
|
+
if (maj > aMaj)
|
|
77
|
+
return 'major';
|
|
78
|
+
if (maj === aMaj && min > aMin) {
|
|
79
|
+
highest = 'minor';
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (maj === aMaj && min === aMin && pat > aPat && !highest) {
|
|
83
|
+
highest = 'patch';
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (maj === aMaj && min === aMin && pat === aPat && (v.createdAt || '') > (active.createdAt || '') && !highest) {
|
|
87
|
+
highest = 'patch';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return highest || 'uptodate';
|
|
91
|
+
}, undefined, [activeVersionId, pkg.packageId]);
|
|
92
|
+
const isPinned = versionFollowRange === 'pinned';
|
|
14
93
|
return (react_1.default.createElement("div", null,
|
|
15
94
|
react_1.default.createElement("small", null, "Name:"),
|
|
16
95
|
react_1.default.createElement(input_1.Input, { value: pkg.qs.name, className: "form-control mb-3 p-0 ps-2", placeholder: "Package name", title: "Package name", disabled: true }),
|
|
@@ -37,6 +116,46 @@ const PackageInfo = (props) => {
|
|
|
37
116
|
react_1.default.createElement("small", null,
|
|
38
117
|
"Description:",
|
|
39
118
|
react_1.default.createElement(tooltip_1.Tooltip, { markdownContent: `This should be edited in the package's README.md. It will automatically update when you restart Peers or reload the package.` })),
|
|
40
|
-
react_1.default.createElement(markdown_with_mentions_1.MarkdownWithMentions, { content: pkg.description || '' }))
|
|
119
|
+
react_1.default.createElement(markdown_with_mentions_1.MarkdownWithMentions, { content: pkg.description || '' })),
|
|
120
|
+
react_1.default.createElement("div", { className: "mt-2" },
|
|
121
|
+
react_1.default.createElement("hr", null),
|
|
122
|
+
react_1.default.createElement("small", null,
|
|
123
|
+
"Version:",
|
|
124
|
+
react_1.default.createElement(tooltip_1.Tooltip, { markdownContent: "The currently active version of this package. Manage versions in the Versions tab." })),
|
|
125
|
+
activeVersion ? (react_1.default.createElement("div", { className: "d-flex align-items-center mt-1 mb-3" },
|
|
126
|
+
react_1.default.createElement("strong", { className: "me-2" },
|
|
127
|
+
"v",
|
|
128
|
+
activeVersion.version),
|
|
129
|
+
activeVersion.versionTag && (react_1.default.createElement("span", { className: `badge ${activeVersion.versionTag.startsWith('beta') ? 'text-bg-warning' : 'text-bg-info'} me-2` }, activeVersion.versionTag)),
|
|
130
|
+
react_1.default.createElement("code", { className: "text-muted small me-2" }, activeVersion.packageVersionHash?.substring(0, 8)),
|
|
131
|
+
newerLevel === 'uptodate' ? (react_1.default.createElement("span", { className: "badge text-bg-success" }, "Up to date")) : newerLevel ? (react_1.default.createElement("span", { className: `badge text-bg-${newerLevel === 'major' ? 'danger' : newerLevel === 'minor' ? 'warning' : 'info'}` },
|
|
132
|
+
"Newer ",
|
|
133
|
+
newerLevel,
|
|
134
|
+
" version available")) : null)) : (react_1.default.createElement("div", { className: "text-muted small mt-1 mb-3" }, "No active version")),
|
|
135
|
+
react_1.default.createElement("div", { className: "mb-3" },
|
|
136
|
+
react_1.default.createElement("small", null,
|
|
137
|
+
"Auto-Update Range:",
|
|
138
|
+
react_1.default.createElement(tooltip_1.Tooltip, { markdownContent: "Controls which new versions are auto-activated. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." })),
|
|
139
|
+
react_1.default.createElement("select", { className: "form-select form-select-sm", value: versionFollowRange || 'latest', onChange: (e) => {
|
|
140
|
+
const val = e.target.value;
|
|
141
|
+
pkg.versionFollowRange = val === 'latest' ? undefined : val;
|
|
142
|
+
} },
|
|
143
|
+
react_1.default.createElement("option", { value: "latest" }, "Latest (auto-update to newest)"),
|
|
144
|
+
react_1.default.createElement("option", { value: "minor" }, "Minor (same major version)"),
|
|
145
|
+
react_1.default.createElement("option", { value: "patch" }, "Patch (same major.minor version)"),
|
|
146
|
+
react_1.default.createElement("option", { value: "pinned" }, "Pinned (no auto-updates)"))),
|
|
147
|
+
react_1.default.createElement("div", { className: `mb-3 ${isPinned ? 'opacity-50' : ''}` },
|
|
148
|
+
react_1.default.createElement("small", null,
|
|
149
|
+
"Follow Version Tags:",
|
|
150
|
+
react_1.default.createElement(tooltip_1.Tooltip, { markdownContent: "Which version tags to auto-activate. Leave empty for **current** (follows the active version's tag). Use `*` for any tag, or a comma-separated list like `stable,prod`." })),
|
|
151
|
+
react_1.default.createElement(input_1.Input, { value: pkg.qs.followVersionTags, className: "form-control form-control-sm p-0 ps-2", placeholder: `Following "${activeVersion?.versionTag || 'stable'}"`, disabled: isPinned })),
|
|
152
|
+
react_1.default.createElement("div", { className: "mb-2" },
|
|
153
|
+
react_1.default.createElement("small", null,
|
|
154
|
+
"Device-Specific Version Tag:",
|
|
155
|
+
react_1.default.createElement(tooltip_1.Tooltip, { markdownContent: "Override the tag policy for this device only. For example, set to `beta` to test pre-release versions on this device without affecting other devices or group members. This is not synced." })),
|
|
156
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center gap-2" },
|
|
157
|
+
react_1.default.createElement("input", { type: "text", className: "form-control form-control-sm p-0 ps-2", placeholder: "e.g. beta", value: deviceTagDraft, onChange: (e) => setDeviceTagDraft(e.target.value) }),
|
|
158
|
+
deviceTagDraft && (react_1.default.createElement("button", { className: "btn btn-sm btn-outline-secondary", title: "Clear device tag override", onClick: () => setDeviceTagDraft('') },
|
|
159
|
+
react_1.default.createElement("i", { className: "bi bi-x-lg" }))))))));
|
|
41
160
|
};
|
|
42
161
|
exports.PackageInfo = PackageInfo;
|
|
@@ -43,6 +43,20 @@ const tooltip_1 = require("../../components/tooltip");
|
|
|
43
43
|
const globals_1 = require("../../globals");
|
|
44
44
|
const hooks_1 = require("../../hooks");
|
|
45
45
|
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
46
|
+
const PackageVersionBadge = ({ activePackageVersionId }) => {
|
|
47
|
+
const pv = (0, hooks_1.usePromise)(async () => {
|
|
48
|
+
if (!activePackageVersionId)
|
|
49
|
+
return null;
|
|
50
|
+
return (0, peers_sdk_1.PackageVersions)().get(activePackageVersionId);
|
|
51
|
+
}, undefined, [activePackageVersionId]);
|
|
52
|
+
if (!pv)
|
|
53
|
+
return null;
|
|
54
|
+
return (react_1.default.createElement("span", { className: "ms-2" },
|
|
55
|
+
react_1.default.createElement("small", { className: "text-muted" },
|
|
56
|
+
"v",
|
|
57
|
+
pv.version),
|
|
58
|
+
pv.versionTag && (react_1.default.createElement("span", { className: `badge ms-1 ${pv.versionTag.startsWith('beta') ? 'text-bg-warning' : 'text-bg-info'}`, style: { fontSize: '0.65em' } }, pv.versionTag))));
|
|
59
|
+
};
|
|
46
60
|
const PackageList = () => {
|
|
47
61
|
const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(''));
|
|
48
62
|
const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
|
|
@@ -157,6 +171,7 @@ const PackageList = () => {
|
|
|
157
171
|
react_1.default.createElement("i", { className: "bi bi-box-fill" }),
|
|
158
172
|
"\u00A0\u00A0",
|
|
159
173
|
react_1.default.createElement("a", { href: `#packages/${pkg.packageId}` }, pkg.name),
|
|
174
|
+
react_1.default.createElement(PackageVersionBadge, { activePackageVersionId: pkg.activePackageVersionId }),
|
|
160
175
|
react_1.default.createElement(tooltip_1.Tooltip, { markdownContent: pkg.description, positions: ['bottom', 'top', 'right', 'left'] })));
|
|
161
176
|
});
|
|
162
177
|
}, loadingIndicator: react_1.default.createElement("div", { className: "d-flex justify-content-center", style: { height: 200 } },
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PackageVersionsList = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
39
|
+
const hooks_1 = require("../../hooks");
|
|
40
|
+
function formatDate(iso) {
|
|
41
|
+
try {
|
|
42
|
+
const d = new Date(iso);
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const diffMs = now.getTime() - d.getTime();
|
|
45
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
46
|
+
if (diffMins < 1)
|
|
47
|
+
return 'just now';
|
|
48
|
+
if (diffMins < 60)
|
|
49
|
+
return `${diffMins}m ago`;
|
|
50
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
51
|
+
if (diffHours < 24)
|
|
52
|
+
return `${diffHours}h ago`;
|
|
53
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
54
|
+
if (diffDays < 30)
|
|
55
|
+
return `${diffDays}d ago`;
|
|
56
|
+
return d.toLocaleDateString();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return iso;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function tagBadgeClass(tag) {
|
|
63
|
+
if (!tag)
|
|
64
|
+
return 'text-bg-secondary';
|
|
65
|
+
if (tag.startsWith('beta'))
|
|
66
|
+
return 'text-bg-warning';
|
|
67
|
+
if (tag === 'stable')
|
|
68
|
+
return 'text-bg-info';
|
|
69
|
+
return 'text-bg-secondary';
|
|
70
|
+
}
|
|
71
|
+
const PackageVersionsList = (props) => {
|
|
72
|
+
const { pkg } = props;
|
|
73
|
+
const [activeVersionId] = (0, hooks_1.useObservable)(pkg.qs.activePackageVersionId);
|
|
74
|
+
const [versionFollowRange] = (0, hooks_1.useObservable)(pkg.qs.versionFollowRange);
|
|
75
|
+
const refreshKey = (0, hooks_1.useObservableState)(0);
|
|
76
|
+
const [activating, setActivating] = (0, react_1.useState)(null);
|
|
77
|
+
const [deleting, setDeleting] = (0, react_1.useState)(null);
|
|
78
|
+
const [pinning, setPinning] = (0, react_1.useState)(false);
|
|
79
|
+
const isPinned = versionFollowRange === 'pinned';
|
|
80
|
+
const versions = (0, hooks_1.usePromise)(async () => {
|
|
81
|
+
const all = await (0, peers_sdk_1.PackageVersions)().list({ packageId: pkg.packageId });
|
|
82
|
+
const parse = (v) => v.split('.').map(Number);
|
|
83
|
+
const sorted = all.sort((a, b) => {
|
|
84
|
+
const [aMaj, aMin, aPat] = parse(a.version || '0.0.0');
|
|
85
|
+
const [bMaj, bMin, bPat] = parse(b.version || '0.0.0');
|
|
86
|
+
if (bMaj !== aMaj)
|
|
87
|
+
return bMaj - aMaj;
|
|
88
|
+
if (bMin !== aMin)
|
|
89
|
+
return bMin - aMin;
|
|
90
|
+
if (bPat !== aPat)
|
|
91
|
+
return bPat - aPat;
|
|
92
|
+
return (b.createdAt || '').localeCompare(a.createdAt || '');
|
|
93
|
+
});
|
|
94
|
+
const uniqueCreatorIds = [...new Set(sorted.map(pv => pv.createdBy).filter(Boolean))];
|
|
95
|
+
const userMap = new Map();
|
|
96
|
+
await Promise.all(uniqueCreatorIds.map(async (uid) => {
|
|
97
|
+
try {
|
|
98
|
+
const user = await (0, peers_sdk_1.Users)().get(uid, { useCache: true });
|
|
99
|
+
if (user?.name)
|
|
100
|
+
userMap.set(uid, user.name);
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
}));
|
|
104
|
+
return { sorted, userMap };
|
|
105
|
+
}, undefined, [pkg.packageId, refreshKey()]);
|
|
106
|
+
async function activateVersion(pv) {
|
|
107
|
+
setActivating(pv.packageVersionId);
|
|
108
|
+
try {
|
|
109
|
+
const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
|
|
110
|
+
if (current) {
|
|
111
|
+
current.activePackageVersionId = pv.packageVersionId;
|
|
112
|
+
await (0, peers_sdk_1.Packages)().signAndSave(current);
|
|
113
|
+
await pkg.load();
|
|
114
|
+
refreshKey(refreshKey() + 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
alert(`Failed to activate version: ${err}`);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
setActivating(null);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function deleteVersion(pv) {
|
|
125
|
+
if (!confirm(`Delete version v${pv.version} (${pv.packageVersionHash?.substring(0, 8)})?`))
|
|
126
|
+
return;
|
|
127
|
+
setDeleting(pv.packageVersionId);
|
|
128
|
+
try {
|
|
129
|
+
await (0, peers_sdk_1.PackageVersions)().delete(pv.packageVersionId);
|
|
130
|
+
refreshKey(refreshKey() + 1);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
alert(`Failed to delete version: ${err}`);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
setDeleting(null);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function pinVersion() {
|
|
140
|
+
setPinning(true);
|
|
141
|
+
try {
|
|
142
|
+
const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
|
|
143
|
+
if (current) {
|
|
144
|
+
current.versionFollowRange = 'pinned';
|
|
145
|
+
await (0, peers_sdk_1.Packages)().signAndSave(current);
|
|
146
|
+
await pkg.load();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
alert(`Failed to pin version: ${err}`);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
setPinning(false);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!versions) {
|
|
157
|
+
return react_1.default.createElement("div", { className: "text-center p-3" },
|
|
158
|
+
react_1.default.createElement("div", { className: "spinner-border spinner-border-sm" }));
|
|
159
|
+
}
|
|
160
|
+
const { sorted, userMap } = versions;
|
|
161
|
+
if (sorted.length === 0) {
|
|
162
|
+
return (react_1.default.createElement("div", { className: "text-muted text-center p-4" },
|
|
163
|
+
react_1.default.createElement("i", { className: "bi bi-tag me-1" }),
|
|
164
|
+
"No versions found for this package."));
|
|
165
|
+
}
|
|
166
|
+
return (react_1.default.createElement("div", null,
|
|
167
|
+
react_1.default.createElement("small", { className: "text-muted" },
|
|
168
|
+
sorted.length,
|
|
169
|
+
" version",
|
|
170
|
+
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
|
+
}))));
|
|
196
|
+
};
|
|
197
|
+
exports.PackageVersionsList = PackageVersionsList;
|
|
@@ -6,19 +6,16 @@ export declare const allPackages: import("@peers-app/peers-sdk").Observable<{
|
|
|
6
6
|
createdBy: string;
|
|
7
7
|
packageId: string;
|
|
8
8
|
localPath: string;
|
|
9
|
-
packageBundleFileId: string;
|
|
10
|
-
packageBundleFileHash: string;
|
|
11
9
|
disabled?: boolean | undefined;
|
|
12
|
-
remoteRepo?: string | undefined;
|
|
13
10
|
appNavs?: {
|
|
14
11
|
name: string;
|
|
15
12
|
iconClassName: string;
|
|
16
13
|
navigationPath: string;
|
|
17
14
|
displayName?: string | undefined;
|
|
18
15
|
}[] | undefined;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
remoteRepo?: string | undefined;
|
|
17
|
+
activePackageVersionId?: string | undefined;
|
|
18
|
+
versionFollowRange?: "pinned" | "patch" | "minor" | "latest" | undefined;
|
|
19
|
+
followVersionTags?: string | undefined;
|
|
23
20
|
}[]>;
|
|
24
21
|
export declare const loadAllRoutes: () => Promise<true | undefined>;
|
|
@@ -9,10 +9,10 @@ const loadAllRoutes = async () => {
|
|
|
9
9
|
if (allRoutesLoaded)
|
|
10
10
|
return;
|
|
11
11
|
allRoutesLoaded = true;
|
|
12
|
-
// Filter packages that have
|
|
12
|
+
// Filter packages that have an active version
|
|
13
13
|
let packagesWithUI = await (0, peers_sdk_1.Packages)().list({
|
|
14
14
|
disabled: { $ne: true },
|
|
15
|
-
|
|
15
|
+
activePackageVersionId: { $exists: true },
|
|
16
16
|
});
|
|
17
17
|
(0, exports.allPackages)(packagesWithUI);
|
|
18
18
|
await Promise.all(packagesWithUI.map(pkg => loadRoutesBundle(pkg)));
|
|
@@ -58,14 +58,21 @@ function loadRoutesBundle(pkg, forceRefresh) {
|
|
|
58
58
|
console.log(`waiting for registerPeersUIRoute to be defined on the window object`);
|
|
59
59
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
60
60
|
}
|
|
61
|
+
let routesBundleFileId;
|
|
62
|
+
if (pkg.activePackageVersionId) {
|
|
63
|
+
try {
|
|
64
|
+
const pv = await (0, peers_sdk_1.PackageVersions)().get(pkg.activePackageVersionId);
|
|
65
|
+
routesBundleFileId = pv?.routesBundleFileId;
|
|
66
|
+
}
|
|
67
|
+
catch { /* no version record */ }
|
|
68
|
+
}
|
|
61
69
|
let bundleCode = '';
|
|
62
|
-
if (
|
|
63
|
-
bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(
|
|
70
|
+
if (routesBundleFileId) {
|
|
71
|
+
bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(routesBundleFileId);
|
|
64
72
|
}
|
|
65
73
|
if (bundleCode) {
|
|
66
74
|
const exportRoutes = (peerRoutes) => {
|
|
67
75
|
peerRoutes.routes.forEach(route => {
|
|
68
|
-
// TODO maybe add package that this came from
|
|
69
76
|
window.registerPeersUIRoute(route);
|
|
70
77
|
});
|
|
71
78
|
};
|
|
@@ -73,8 +80,8 @@ function loadRoutesBundle(pkg, forceRefresh) {
|
|
|
73
80
|
bundleFunction(exportRoutes);
|
|
74
81
|
return {};
|
|
75
82
|
}
|
|
76
|
-
else {
|
|
77
|
-
console.warn(`Routes bundle file not found for ${pkg.name} (fileId: ${
|
|
83
|
+
else if (routesBundleFileId) {
|
|
84
|
+
console.warn(`Routes bundle file not found for ${pkg.name} (fileId: ${routesBundleFileId})`);
|
|
78
85
|
}
|
|
79
86
|
return null;
|
|
80
87
|
}
|
|
@@ -227,28 +227,35 @@ const UILoader = (args) => {
|
|
|
227
227
|
const uiLoadingPromises = {};
|
|
228
228
|
// Check if we're running in a React Native WebView (has injectUIBundle available)
|
|
229
229
|
const isReactNativeWebView = typeof window.ReactNativeWebView !== 'undefined';
|
|
230
|
+
async function resolveUiBundleFileId(pkg) {
|
|
231
|
+
if (!pkg.activePackageVersionId)
|
|
232
|
+
return undefined;
|
|
233
|
+
try {
|
|
234
|
+
const pv = await (0, peers_sdk_1.PackageVersions)().get(pkg.activePackageVersionId);
|
|
235
|
+
return pv?.uiBundleFileId;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
230
241
|
function loadUIBundle(pkg, forceRefresh) {
|
|
231
|
-
// Dynamically import the bundle
|
|
232
242
|
let importPromise = uiLoadingPromises[pkg.packageId];
|
|
233
243
|
if (!importPromise || forceRefresh) {
|
|
234
244
|
const sTime = Date.now();
|
|
235
245
|
console.log(`loading ui bundle for ${pkg.name}`);
|
|
236
246
|
importPromise = new Promise(async (resolve, reject) => {
|
|
237
247
|
try {
|
|
238
|
-
|
|
248
|
+
const uiBundleFileId = await resolveUiBundleFileId(pkg);
|
|
249
|
+
if (!uiBundleFileId) {
|
|
239
250
|
resolve();
|
|
240
251
|
return;
|
|
241
252
|
}
|
|
242
|
-
// Use fast injection path for React Native WebView
|
|
243
253
|
if (isReactNativeWebView && peers_sdk_1.rpcServerCalls.injectUIBundle) {
|
|
244
|
-
// Set up listeners for bundle load completion
|
|
245
254
|
const _window = window;
|
|
246
255
|
_window.__peersUIs = _window.__peersUIs || {};
|
|
247
256
|
const loadPromise = new Promise((resolveLoad, rejectLoad) => {
|
|
248
|
-
const fileId = pkg.uiBundleFileId;
|
|
249
257
|
_window.__peersUIBundleLoaded = (loadedFileId) => {
|
|
250
|
-
if (loadedFileId ===
|
|
251
|
-
// Copy loaded UIs to our local registry
|
|
258
|
+
if (loadedFileId === uiBundleFileId) {
|
|
252
259
|
Object.keys(_window.__peersUIs || {}).forEach(peersUIId => {
|
|
253
260
|
peersUIs[peersUIId] = _window.__peersUIs[peersUIId];
|
|
254
261
|
});
|
|
@@ -256,18 +263,17 @@ function loadUIBundle(pkg, forceRefresh) {
|
|
|
256
263
|
}
|
|
257
264
|
};
|
|
258
265
|
_window.__peersUIBundleError = (errorFileId, errorMsg) => {
|
|
259
|
-
if (errorFileId ===
|
|
266
|
+
if (errorFileId === uiBundleFileId) {
|
|
260
267
|
rejectLoad(new Error(errorMsg));
|
|
261
268
|
}
|
|
262
269
|
};
|
|
263
270
|
});
|
|
264
|
-
await peers_sdk_1.rpcServerCalls.injectUIBundle(
|
|
271
|
+
await peers_sdk_1.rpcServerCalls.injectUIBundle(uiBundleFileId);
|
|
265
272
|
await loadPromise;
|
|
266
273
|
console.log(`finished loading ui bundle for ${pkg.name}: ${(Date.now() - sTime).toFixed(0)}ms`);
|
|
267
274
|
}
|
|
268
275
|
else {
|
|
269
|
-
|
|
270
|
-
let bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(pkg.uiBundleFileId);
|
|
276
|
+
let bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(uiBundleFileId);
|
|
271
277
|
if (bundleCode) {
|
|
272
278
|
const exportUIs = (peerUIs) => {
|
|
273
279
|
peerUIs?.uis?.forEach(ui => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.10.1",
|
|
30
30
|
"bootstrap": "^5.3.3",
|
|
31
31
|
"react": "^18.0.0",
|
|
32
32
|
"react-dom": "^18.0.0"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@babel/preset-react": "^7.24.1",
|
|
38
38
|
"@babel/preset-typescript": "^7.27.1",
|
|
39
39
|
"@electron/rebuild": "^3.6.0",
|
|
40
|
-
"@peers-app/peers-sdk": "0.
|
|
40
|
+
"@peers-app/peers-sdk": "0.10.1",
|
|
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",
|
|
@@ -4,6 +4,7 @@ import { LoadingIndicator } from "../../components/loading-indicator";
|
|
|
4
4
|
import { ISaveButtonProps, SaveButton } from "../../components/save-button";
|
|
5
5
|
import { ScreenTabBody, Tabs } from "../../components/tabs";
|
|
6
6
|
import { PackageInfo } from "./package-info";
|
|
7
|
+
import { PackageVersionsList } from "./package-versions";
|
|
7
8
|
import { useObservableState, usePromise } from "../../hooks";
|
|
8
9
|
import { Input } from "../../components/input";
|
|
9
10
|
import { updateActiveTabTitle } from "../../tabs-layout/tabs-state";
|
|
@@ -16,6 +17,7 @@ interface IProps {
|
|
|
16
17
|
export const PackageDetails = (props: IProps) => {
|
|
17
18
|
|
|
18
19
|
const refresh = useObservableState(Date.now());
|
|
20
|
+
const saveDeviceTagRef = React.useRef<(() => Promise<void>) | null>(null);
|
|
19
21
|
|
|
20
22
|
const pkg = usePromise(async () => {
|
|
21
23
|
const pkg = await Packages().get(props.packageId);
|
|
@@ -80,6 +82,10 @@ export const PackageDetails = (props: IProps) => {
|
|
|
80
82
|
<SaveButton
|
|
81
83
|
key={pkg.packageId}
|
|
82
84
|
doc={pkg}
|
|
85
|
+
onClick={async () => {
|
|
86
|
+
await pkg.save();
|
|
87
|
+
await saveDeviceTagRef.current?.();
|
|
88
|
+
}}
|
|
83
89
|
addActions={[
|
|
84
90
|
...addActions
|
|
85
91
|
]}
|
|
@@ -93,7 +99,13 @@ export const PackageDetails = (props: IProps) => {
|
|
|
93
99
|
{
|
|
94
100
|
name: 'Info', content:
|
|
95
101
|
<ScreenTabBody>
|
|
96
|
-
<PackageInfo pkg={pkg} />
|
|
102
|
+
<PackageInfo pkg={pkg} saveDeviceTagRef={saveDeviceTagRef} />
|
|
103
|
+
</ScreenTabBody>
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Versions', content:
|
|
107
|
+
<ScreenTabBody>
|
|
108
|
+
<PackageVersionsList pkg={pkg} />
|
|
97
109
|
</ScreenTabBody>
|
|
98
110
|
},
|
|
99
111
|
{
|
|
@@ -1,11 +1,85 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { rpcServerCalls, IDoc, IPackage, packagesRootDir } from "@peers-app/peers-sdk";
|
|
2
|
+
import { rpcServerCalls, IDoc, IPackage, IPackageVersion, PackageVersions, packagesRootDir, groupDeviceVar } from "@peers-app/peers-sdk";
|
|
3
3
|
import { MarkdownWithMentions } from "../../components/markdown-with-mentions";
|
|
4
4
|
import { Tooltip } from "../../components/tooltip";
|
|
5
5
|
import { Input } from "../../components/input";
|
|
6
|
+
import { useObservable, usePromise } from "../../hooks";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const deviceVersionTagVar = groupDeviceVar<string | undefined>('deviceVersionTag');
|
|
9
|
+
|
|
10
|
+
export const PackageInfo = (props: {
|
|
11
|
+
pkg: IDoc<IPackage>,
|
|
12
|
+
saveDeviceTagRef?: React.MutableRefObject<(() => Promise<void>) | null>,
|
|
13
|
+
}) => {
|
|
8
14
|
const { pkg } = props;
|
|
15
|
+
const [activeVersionId] = useObservable(pkg.qs.activePackageVersionId);
|
|
16
|
+
const [versionFollowRange] = useObservable(pkg.qs.versionFollowRange);
|
|
17
|
+
const [followVersionTags] = useObservable(pkg.qs.followVersionTags);
|
|
18
|
+
const [deviceTag] = useObservable(deviceVersionTagVar);
|
|
19
|
+
|
|
20
|
+
const [deviceTagDraft, setDeviceTagDraft] = React.useState(deviceTag || '');
|
|
21
|
+
const savingRef = React.useRef(false);
|
|
22
|
+
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
if (!savingRef.current) {
|
|
25
|
+
setDeviceTagDraft(deviceTag || '');
|
|
26
|
+
}
|
|
27
|
+
}, [deviceTag]);
|
|
28
|
+
|
|
29
|
+
const deviceTagDirty = deviceTagDraft.trim() !== (deviceTag || '');
|
|
30
|
+
const prevDirtyRef = React.useRef(false);
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (deviceTagDirty && !prevDirtyRef.current) {
|
|
33
|
+
pkg.q((pkg.q() || 0) + 1);
|
|
34
|
+
} else if (!deviceTagDirty && prevDirtyRef.current) {
|
|
35
|
+
pkg.q(Math.max(0, (pkg.q() || 0) - 1));
|
|
36
|
+
}
|
|
37
|
+
prevDirtyRef.current = deviceTagDirty;
|
|
38
|
+
}, [deviceTagDirty]);
|
|
39
|
+
|
|
40
|
+
if (props.saveDeviceTagRef) {
|
|
41
|
+
props.saveDeviceTagRef.current = async () => {
|
|
42
|
+
const val = deviceTagDraft.trim();
|
|
43
|
+
if (val !== (deviceTag || '')) {
|
|
44
|
+
savingRef.current = true;
|
|
45
|
+
try {
|
|
46
|
+
if (val) {
|
|
47
|
+
deviceVersionTagVar(val);
|
|
48
|
+
} else {
|
|
49
|
+
await deviceVersionTagVar.delete();
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
savingRef.current = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const activeVersion = usePromise(async () => {
|
|
59
|
+
if (!activeVersionId) return null;
|
|
60
|
+
return PackageVersions().get(activeVersionId) as Promise<IPackageVersion | null>;
|
|
61
|
+
}, undefined, [activeVersionId]);
|
|
62
|
+
|
|
63
|
+
const newerLevel = usePromise(async (): Promise<'major' | 'minor' | 'patch' | 'uptodate'> => {
|
|
64
|
+
if (!activeVersionId) return 'uptodate';
|
|
65
|
+
const all = await PackageVersions().list({ packageId: pkg.packageId });
|
|
66
|
+
const active = all.find(v => v.packageVersionId === activeVersionId);
|
|
67
|
+
if (!active?.version) return 'uptodate';
|
|
68
|
+
const parse = (v: string) => v.split('.').map(Number);
|
|
69
|
+
const [aMaj, aMin, aPat] = parse(active.version);
|
|
70
|
+
let highest: 'major' | 'minor' | 'patch' | null = null;
|
|
71
|
+
for (const v of all) {
|
|
72
|
+
if (!v.version || v.packageVersionId === activeVersionId) continue;
|
|
73
|
+
const [maj, min, pat] = parse(v.version);
|
|
74
|
+
if (maj > aMaj) return 'major';
|
|
75
|
+
if (maj === aMaj && min > aMin) { highest = 'minor'; continue; }
|
|
76
|
+
if (maj === aMaj && min === aMin && pat > aPat && !highest) { highest = 'patch'; continue; }
|
|
77
|
+
if (maj === aMaj && min === aMin && pat === aPat && (v.createdAt || '') > (active.createdAt || '') && !highest) { highest = 'patch'; }
|
|
78
|
+
}
|
|
79
|
+
return highest || 'uptodate';
|
|
80
|
+
}, undefined, [activeVersionId, pkg.packageId]);
|
|
81
|
+
|
|
82
|
+
const isPinned = versionFollowRange === 'pinned';
|
|
9
83
|
|
|
10
84
|
return (
|
|
11
85
|
<div>
|
|
@@ -66,18 +140,97 @@ export const PackageInfo = (props: { pkg: IDoc<IPackage> }) => {
|
|
|
66
140
|
Description:
|
|
67
141
|
<Tooltip markdownContent={`This should be edited in the package's README.md. It will automatically update when you restart Peers or reload the package.`} />
|
|
68
142
|
</small>
|
|
69
|
-
{/* <div className="border rounded border-dark-subtle p-1">
|
|
70
|
-
<MarkdownWithMentions
|
|
71
|
-
content={pkg.description || ''}
|
|
72
|
-
/>
|
|
73
|
-
</div> */}
|
|
74
|
-
|
|
75
143
|
<MarkdownWithMentions
|
|
76
144
|
content={pkg.description || ''}
|
|
77
145
|
/>
|
|
78
146
|
</div>
|
|
79
147
|
|
|
148
|
+
<div className="mt-2">
|
|
149
|
+
<hr />
|
|
150
|
+
<small>
|
|
151
|
+
Version:
|
|
152
|
+
<Tooltip markdownContent="The currently active version of this package. Manage versions in the Versions tab." />
|
|
153
|
+
</small>
|
|
154
|
+
{activeVersion ? (
|
|
155
|
+
<div className="d-flex align-items-center mt-1 mb-3">
|
|
156
|
+
<strong className="me-2">v{activeVersion.version}</strong>
|
|
157
|
+
{activeVersion.versionTag && (
|
|
158
|
+
<span className={`badge ${activeVersion.versionTag.startsWith('beta') ? 'text-bg-warning' : 'text-bg-info'} me-2`}>
|
|
159
|
+
{activeVersion.versionTag}
|
|
160
|
+
</span>
|
|
161
|
+
)}
|
|
162
|
+
<code className="text-muted small me-2">{activeVersion.packageVersionHash?.substring(0, 8)}</code>
|
|
163
|
+
{newerLevel === 'uptodate' ? (
|
|
164
|
+
<span className="badge text-bg-success">Up to date</span>
|
|
165
|
+
) : newerLevel ? (
|
|
166
|
+
<span className={`badge text-bg-${newerLevel === 'major' ? 'danger' : newerLevel === 'minor' ? 'warning' : 'info'}`}>
|
|
167
|
+
Newer {newerLevel} version available
|
|
168
|
+
</span>
|
|
169
|
+
) : null}
|
|
170
|
+
</div>
|
|
171
|
+
) : (
|
|
172
|
+
<div className="text-muted small mt-1 mb-3">No active version</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
<div className="mb-3">
|
|
176
|
+
<small>
|
|
177
|
+
Auto-Update Range:
|
|
178
|
+
<Tooltip markdownContent="Controls which new versions are auto-activated. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." />
|
|
179
|
+
</small>
|
|
180
|
+
<select
|
|
181
|
+
className="form-select form-select-sm"
|
|
182
|
+
value={versionFollowRange || 'latest'}
|
|
183
|
+
onChange={(e) => {
|
|
184
|
+
const val = e.target.value as 'pinned' | 'patch' | 'minor' | 'latest';
|
|
185
|
+
pkg.versionFollowRange = val === 'latest' ? undefined : val;
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<option value="latest">Latest (auto-update to newest)</option>
|
|
189
|
+
<option value="minor">Minor (same major version)</option>
|
|
190
|
+
<option value="patch">Patch (same major.minor version)</option>
|
|
191
|
+
<option value="pinned">Pinned (no auto-updates)</option>
|
|
192
|
+
</select>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className={`mb-3 ${isPinned ? 'opacity-50' : ''}`}>
|
|
196
|
+
<small>
|
|
197
|
+
Follow Version Tags:
|
|
198
|
+
<Tooltip markdownContent="Which version tags to auto-activate. Leave empty for **current** (follows the active version's tag). Use `*` for any tag, or a comma-separated list like `stable,prod`." />
|
|
199
|
+
</small>
|
|
200
|
+
<Input
|
|
201
|
+
value={pkg.qs.followVersionTags}
|
|
202
|
+
className="form-control form-control-sm p-0 ps-2"
|
|
203
|
+
placeholder={`Following "${activeVersion?.versionTag || 'stable'}"`}
|
|
204
|
+
disabled={isPinned}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="mb-2">
|
|
209
|
+
<small>
|
|
210
|
+
Device-Specific Version Tag:
|
|
211
|
+
<Tooltip markdownContent="Override the tag policy for this device only. For example, set to `beta` to test pre-release versions on this device without affecting other devices or group members. This is not synced." />
|
|
212
|
+
</small>
|
|
213
|
+
<div className="d-flex align-items-center gap-2">
|
|
214
|
+
<input
|
|
215
|
+
type="text"
|
|
216
|
+
className="form-control form-control-sm p-0 ps-2"
|
|
217
|
+
placeholder="e.g. beta"
|
|
218
|
+
value={deviceTagDraft}
|
|
219
|
+
onChange={(e) => setDeviceTagDraft(e.target.value)}
|
|
220
|
+
/>
|
|
221
|
+
{deviceTagDraft && (
|
|
222
|
+
<button
|
|
223
|
+
className="btn btn-sm btn-outline-secondary"
|
|
224
|
+
title="Clear device tag override"
|
|
225
|
+
onClick={() => setDeviceTagDraft('')}
|
|
226
|
+
>
|
|
227
|
+
<i className="bi bi-x-lg"></i>
|
|
228
|
+
</button>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
80
234
|
</div>
|
|
81
235
|
)
|
|
82
236
|
}
|
|
83
|
-
|
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
import { ICursorIterable, IPackage, observable, Packages, packagesRootDir, rpcServerCalls } from "@peers-app/peers-sdk";
|
|
1
|
+
import { ICursorIterable, IPackage, observable, Packages, PackageVersions, packagesRootDir, rpcServerCalls } from "@peers-app/peers-sdk";
|
|
2
2
|
import React, { useEffect, useState } from 'react';
|
|
3
3
|
import { Input } from "../../components/input";
|
|
4
4
|
import { LazyList } from "../../components/lazy-list";
|
|
5
5
|
import { LoadingIndicator } from '../../components/loading-indicator';
|
|
6
6
|
import { Tooltip } from '../../components/tooltip';
|
|
7
7
|
import { isDesktop, mainContentPath } from '../../globals';
|
|
8
|
-
import { useObservable, useObservableState } from "../../hooks";
|
|
8
|
+
import { useObservable, useObservableState, usePromise } from "../../hooks";
|
|
9
9
|
import { registerInternalPeersUI } from "../../ui-router/ui-loader";
|
|
10
10
|
|
|
11
|
+
const PackageVersionBadge = ({ activePackageVersionId }: { activePackageVersionId?: string }) => {
|
|
12
|
+
const pv = usePromise(async () => {
|
|
13
|
+
if (!activePackageVersionId) return null;
|
|
14
|
+
return PackageVersions().get(activePackageVersionId);
|
|
15
|
+
}, undefined, [activePackageVersionId]);
|
|
16
|
+
|
|
17
|
+
if (!pv) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<span className="ms-2">
|
|
21
|
+
<small className="text-muted">v{pv.version}</small>
|
|
22
|
+
{pv.versionTag && (
|
|
23
|
+
<span className={`badge ms-1 ${pv.versionTag.startsWith('beta') ? 'text-bg-warning' : 'text-bg-info'}`}
|
|
24
|
+
style={{ fontSize: '0.65em' }}
|
|
25
|
+
>
|
|
26
|
+
{pv.versionTag}
|
|
27
|
+
</span>
|
|
28
|
+
)}
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
11
33
|
export const PackageList = () => {
|
|
12
34
|
const [searchTextObs] = useState(() => observable(''));
|
|
13
35
|
const [searchText] = useObservable(searchTextObs);
|
|
@@ -146,6 +168,7 @@ export const PackageList = () => {
|
|
|
146
168
|
<a href={`#packages/${pkg.packageId}`}>
|
|
147
169
|
{pkg.name}
|
|
148
170
|
</a>
|
|
171
|
+
<PackageVersionBadge activePackageVersionId={pkg.activePackageVersionId} />
|
|
149
172
|
<Tooltip
|
|
150
173
|
markdownContent={pkg.description}
|
|
151
174
|
positions={['bottom', 'top', 'right', 'left']}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { IDoc, IPackage, IPackageVersion, Packages, PackageVersions, Users } from "@peers-app/peers-sdk";
|
|
3
|
+
import { useObservable, useObservableState, usePromise } from "../../hooks";
|
|
4
|
+
|
|
5
|
+
function formatDate(iso: string): string {
|
|
6
|
+
try {
|
|
7
|
+
const d = new Date(iso);
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const diffMs = now.getTime() - d.getTime();
|
|
10
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
11
|
+
if (diffMins < 1) return 'just now';
|
|
12
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
13
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
14
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
15
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
16
|
+
if (diffDays < 30) return `${diffDays}d ago`;
|
|
17
|
+
return d.toLocaleDateString();
|
|
18
|
+
} catch {
|
|
19
|
+
return iso;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tagBadgeClass(tag?: string): string {
|
|
24
|
+
if (!tag) return 'text-bg-secondary';
|
|
25
|
+
if (tag.startsWith('beta')) return 'text-bg-warning';
|
|
26
|
+
if (tag === 'stable') return 'text-bg-info';
|
|
27
|
+
return 'text-bg-secondary';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
|
|
31
|
+
const { pkg } = props;
|
|
32
|
+
const [activeVersionId] = useObservable(pkg.qs.activePackageVersionId);
|
|
33
|
+
const [versionFollowRange] = useObservable(pkg.qs.versionFollowRange);
|
|
34
|
+
const refreshKey = useObservableState(0);
|
|
35
|
+
const [activating, setActivating] = useState<string | null>(null);
|
|
36
|
+
const [deleting, setDeleting] = useState<string | null>(null);
|
|
37
|
+
const [pinning, setPinning] = useState(false);
|
|
38
|
+
const isPinned = versionFollowRange === 'pinned';
|
|
39
|
+
|
|
40
|
+
const versions = usePromise(async () => {
|
|
41
|
+
const all = await PackageVersions().list({ packageId: pkg.packageId });
|
|
42
|
+
const parse = (v: string) => v.split('.').map(Number);
|
|
43
|
+
const sorted = all.sort((a, b) => {
|
|
44
|
+
const [aMaj, aMin, aPat] = parse(a.version || '0.0.0');
|
|
45
|
+
const [bMaj, bMin, bPat] = parse(b.version || '0.0.0');
|
|
46
|
+
if (bMaj !== aMaj) return bMaj - aMaj;
|
|
47
|
+
if (bMin !== aMin) return bMin - aMin;
|
|
48
|
+
if (bPat !== aPat) return bPat - aPat;
|
|
49
|
+
return (b.createdAt || '').localeCompare(a.createdAt || '');
|
|
50
|
+
});
|
|
51
|
+
const uniqueCreatorIds = [...new Set(sorted.map(pv => pv.createdBy).filter(Boolean))];
|
|
52
|
+
const userMap = new Map<string, string>();
|
|
53
|
+
await Promise.all(uniqueCreatorIds.map(async (uid) => {
|
|
54
|
+
try {
|
|
55
|
+
const user = await Users().get(uid, { useCache: true });
|
|
56
|
+
if (user?.name) userMap.set(uid, user.name);
|
|
57
|
+
} catch {}
|
|
58
|
+
}));
|
|
59
|
+
return { sorted, userMap };
|
|
60
|
+
}, undefined, [pkg.packageId, refreshKey()]);
|
|
61
|
+
|
|
62
|
+
async function activateVersion(pv: IPackageVersion) {
|
|
63
|
+
setActivating(pv.packageVersionId);
|
|
64
|
+
try {
|
|
65
|
+
const current = await Packages().get(pkg.packageId);
|
|
66
|
+
if (current) {
|
|
67
|
+
current.activePackageVersionId = pv.packageVersionId;
|
|
68
|
+
await Packages().signAndSave(current);
|
|
69
|
+
await pkg.load();
|
|
70
|
+
refreshKey(refreshKey() + 1);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
alert(`Failed to activate version: ${err}`);
|
|
74
|
+
} finally {
|
|
75
|
+
setActivating(null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function deleteVersion(pv: IPackageVersion) {
|
|
80
|
+
if (!confirm(`Delete version v${pv.version} (${pv.packageVersionHash?.substring(0, 8)})?`)) return;
|
|
81
|
+
setDeleting(pv.packageVersionId);
|
|
82
|
+
try {
|
|
83
|
+
await PackageVersions().delete(pv.packageVersionId);
|
|
84
|
+
refreshKey(refreshKey() + 1);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
alert(`Failed to delete version: ${err}`);
|
|
87
|
+
} finally {
|
|
88
|
+
setDeleting(null);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function pinVersion() {
|
|
93
|
+
setPinning(true);
|
|
94
|
+
try {
|
|
95
|
+
const current = await Packages().get(pkg.packageId);
|
|
96
|
+
if (current) {
|
|
97
|
+
current.versionFollowRange = 'pinned';
|
|
98
|
+
await Packages().signAndSave(current);
|
|
99
|
+
await pkg.load();
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
alert(`Failed to pin version: ${err}`);
|
|
103
|
+
} finally {
|
|
104
|
+
setPinning(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!versions) {
|
|
109
|
+
return <div className="text-center p-3"><div className="spinner-border spinner-border-sm" /></div>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { sorted, userMap } = versions;
|
|
113
|
+
|
|
114
|
+
if (sorted.length === 0) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="text-muted text-center p-4">
|
|
117
|
+
<i className="bi bi-tag me-1"></i>
|
|
118
|
+
No versions found for this package.
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<small className="text-muted">{sorted.length} version{sorted.length !== 1 ? 's' : ''}</small>
|
|
126
|
+
<div className="list-group mt-2">
|
|
127
|
+
{sorted.map(pv => {
|
|
128
|
+
const isActive = pv.packageVersionId === activeVersionId;
|
|
129
|
+
const creatorName = userMap.get(pv.createdBy);
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
key={pv.packageVersionId}
|
|
133
|
+
className={`list-group-item ${isActive ? 'list-group-item-success' : ''}`}
|
|
134
|
+
>
|
|
135
|
+
<div className="d-flex align-items-center justify-content-between">
|
|
136
|
+
<div>
|
|
137
|
+
<strong className="me-2">v{pv.version}</strong>
|
|
138
|
+
{pv.versionTag && (
|
|
139
|
+
<span className={`badge ${tagBadgeClass(pv.versionTag)} me-2`}>
|
|
140
|
+
{pv.versionTag}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
<code className="text-muted small me-2">
|
|
144
|
+
{pv.packageVersionHash?.substring(0, 8)}
|
|
145
|
+
</code>
|
|
146
|
+
<small className="text-muted me-2">{formatDate(pv.createdAt)}</small>
|
|
147
|
+
{creatorName && (
|
|
148
|
+
<small className="text-muted">
|
|
149
|
+
by {creatorName}
|
|
150
|
+
</small>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
<div className="d-flex gap-1">
|
|
154
|
+
{isActive ? (<>
|
|
155
|
+
<button
|
|
156
|
+
className="btn btn-sm btn-outline-secondary"
|
|
157
|
+
disabled={isPinned || pinning}
|
|
158
|
+
onClick={() => pinVersion()}
|
|
159
|
+
title={isPinned ? 'Already pinned' : 'Pin to this version (disable auto-updates)'}
|
|
160
|
+
>
|
|
161
|
+
{pinning ? (
|
|
162
|
+
<span className="spinner-border spinner-border-sm" />
|
|
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
|
+
</>) : (
|
|
170
|
+
<button
|
|
171
|
+
className="btn btn-sm btn-outline-primary"
|
|
172
|
+
disabled={activating === pv.packageVersionId}
|
|
173
|
+
onClick={() => activateVersion(pv)}
|
|
174
|
+
>
|
|
175
|
+
{activating === pv.packageVersionId ? (
|
|
176
|
+
<span className="spinner-border spinner-border-sm me-1" />
|
|
177
|
+
) : (
|
|
178
|
+
<i className="bi bi-check2-circle me-1"></i>
|
|
179
|
+
)}
|
|
180
|
+
Activate
|
|
181
|
+
</button>
|
|
182
|
+
)}
|
|
183
|
+
{!isActive && (
|
|
184
|
+
<button
|
|
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>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
};
|
package/src/system-apps/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { debounceByArgs, IPackage, IPeersPackageRoutes, observable, Packages, rpcServerCalls } from "@peers-app/peers-sdk";
|
|
1
|
+
import { debounceByArgs, IPackage, IPeersPackageRoutes, observable, Packages, PackageVersions, rpcServerCalls } from "@peers-app/peers-sdk";
|
|
2
2
|
import "../ui-defaults";
|
|
3
3
|
|
|
4
4
|
export const allPackages = observable<IPackage[]>([]);
|
|
@@ -7,10 +7,10 @@ let allRoutesLoaded = false;
|
|
|
7
7
|
export const loadAllRoutes = async () => {
|
|
8
8
|
if (allRoutesLoaded) return;
|
|
9
9
|
allRoutesLoaded = true;
|
|
10
|
-
// Filter packages that have
|
|
10
|
+
// Filter packages that have an active version
|
|
11
11
|
let packagesWithUI = await Packages().list({
|
|
12
12
|
disabled: { $ne: true },
|
|
13
|
-
|
|
13
|
+
activePackageVersionId: { $exists: true },
|
|
14
14
|
});
|
|
15
15
|
allPackages(packagesWithUI);
|
|
16
16
|
await Promise.all(
|
|
@@ -60,14 +60,22 @@ function loadRoutesBundle(pkg: IPackage, forceRefresh?: boolean): Promise<any> {
|
|
|
60
60
|
console.log(`waiting for registerPeersUIRoute to be defined on the window object`);
|
|
61
61
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
let routesBundleFileId: string | undefined;
|
|
65
|
+
if (pkg.activePackageVersionId) {
|
|
66
|
+
try {
|
|
67
|
+
const pv = await PackageVersions().get(pkg.activePackageVersionId);
|
|
68
|
+
routesBundleFileId = pv?.routesBundleFileId;
|
|
69
|
+
} catch { /* no version record */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
let bundleCode = '';
|
|
64
|
-
if (
|
|
65
|
-
bundleCode = await rpcServerCalls.getFileContents(
|
|
73
|
+
if (routesBundleFileId) {
|
|
74
|
+
bundleCode = await rpcServerCalls.getFileContents(routesBundleFileId);
|
|
66
75
|
}
|
|
67
76
|
if (bundleCode) {
|
|
68
77
|
const exportRoutes = (peerRoutes: IPeersPackageRoutes) => {
|
|
69
78
|
peerRoutes.routes.forEach(route => {
|
|
70
|
-
// TODO maybe add package that this came from
|
|
71
79
|
(window as any).registerPeersUIRoute(route);
|
|
72
80
|
});
|
|
73
81
|
}
|
|
@@ -76,8 +84,8 @@ function loadRoutesBundle(pkg: IPackage, forceRefresh?: boolean): Promise<any> {
|
|
|
76
84
|
bundleFunction(exportRoutes);
|
|
77
85
|
|
|
78
86
|
return {};
|
|
79
|
-
} else {
|
|
80
|
-
console.warn(`Routes bundle file not found for ${pkg.name} (fileId: ${
|
|
87
|
+
} else if (routesBundleFileId) {
|
|
88
|
+
console.warn(`Routes bundle file not found for ${pkg.name} (fileId: ${routesBundleFileId})`);
|
|
81
89
|
}
|
|
82
90
|
return null;
|
|
83
91
|
} catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { debounceByArgs, getUserContext, IPackage, IPeersPackageUIs, IPeersUI, IPeersUIRoute, newid, Packages, rpcServerCalls, toJSON, UIContext, zodAnyObject } from "@peers-app/peers-sdk";
|
|
1
|
+
import { debounceByArgs, getUserContext, IPackage, IPeersPackageUIs, IPeersUI, IPeersUIRoute, newid, Packages, PackageVersions, rpcServerCalls, toJSON, UIContext, zodAnyObject } from "@peers-app/peers-sdk";
|
|
2
2
|
import { orderBy } from "lodash";
|
|
3
3
|
import React, { Component, useEffect } from "react";
|
|
4
4
|
import { LoadingIndicator } from "../components/loading-indicator";
|
|
@@ -225,31 +225,34 @@ const uiLoadingPromises: Record<string, Promise<any>> = {};
|
|
|
225
225
|
// Check if we're running in a React Native WebView (has injectUIBundle available)
|
|
226
226
|
const isReactNativeWebView = typeof (window as any).ReactNativeWebView !== 'undefined';
|
|
227
227
|
|
|
228
|
+
async function resolveUiBundleFileId(pkg: IPackage): Promise<string | undefined> {
|
|
229
|
+
if (!pkg.activePackageVersionId) return undefined;
|
|
230
|
+
try {
|
|
231
|
+
const pv = await PackageVersions().get(pkg.activePackageVersionId);
|
|
232
|
+
return pv?.uiBundleFileId;
|
|
233
|
+
} catch { return undefined; }
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
function loadUIBundle(pkg: IPackage, forceRefresh?: boolean) {
|
|
229
|
-
// Dynamically import the bundle
|
|
230
237
|
let importPromise: Promise<any> = uiLoadingPromises[pkg.packageId];
|
|
231
238
|
if (!importPromise || forceRefresh) {
|
|
232
239
|
const sTime = Date.now();
|
|
233
240
|
console.log(`loading ui bundle for ${pkg.name}`);
|
|
234
241
|
importPromise = new Promise<void>(async (resolve, reject) => {
|
|
235
242
|
try {
|
|
236
|
-
|
|
243
|
+
const uiBundleFileId = await resolveUiBundleFileId(pkg);
|
|
244
|
+
if (!uiBundleFileId) {
|
|
237
245
|
resolve();
|
|
238
246
|
return;
|
|
239
247
|
}
|
|
240
248
|
|
|
241
|
-
// Use fast injection path for React Native WebView
|
|
242
249
|
if (isReactNativeWebView && rpcServerCalls.injectUIBundle) {
|
|
243
|
-
// Set up listeners for bundle load completion
|
|
244
250
|
const _window = window as any;
|
|
245
251
|
_window.__peersUIs = _window.__peersUIs || {};
|
|
246
252
|
|
|
247
253
|
const loadPromise = new Promise<void>((resolveLoad, rejectLoad) => {
|
|
248
|
-
const fileId = pkg.uiBundleFileId!;
|
|
249
|
-
|
|
250
254
|
_window.__peersUIBundleLoaded = (loadedFileId: string) => {
|
|
251
|
-
if (loadedFileId ===
|
|
252
|
-
// Copy loaded UIs to our local registry
|
|
255
|
+
if (loadedFileId === uiBundleFileId) {
|
|
253
256
|
Object.keys(_window.__peersUIs || {}).forEach(peersUIId => {
|
|
254
257
|
peersUIs[peersUIId] = _window.__peersUIs[peersUIId];
|
|
255
258
|
});
|
|
@@ -258,18 +261,17 @@ function loadUIBundle(pkg: IPackage, forceRefresh?: boolean) {
|
|
|
258
261
|
};
|
|
259
262
|
|
|
260
263
|
_window.__peersUIBundleError = (errorFileId: string, errorMsg: string) => {
|
|
261
|
-
if (errorFileId ===
|
|
264
|
+
if (errorFileId === uiBundleFileId) {
|
|
262
265
|
rejectLoad(new Error(errorMsg));
|
|
263
266
|
}
|
|
264
267
|
};
|
|
265
268
|
});
|
|
266
269
|
|
|
267
|
-
await rpcServerCalls.injectUIBundle(
|
|
270
|
+
await rpcServerCalls.injectUIBundle(uiBundleFileId);
|
|
268
271
|
await loadPromise;
|
|
269
272
|
console.log(`finished loading ui bundle for ${pkg.name}: ${(Date.now() - sTime).toFixed(0)}ms`);
|
|
270
273
|
} else {
|
|
271
|
-
|
|
272
|
-
let bundleCode = await rpcServerCalls.getFileContents(pkg.uiBundleFileId);
|
|
274
|
+
let bundleCode = await rpcServerCalls.getFileContents(uiBundleFileId);
|
|
273
275
|
if (bundleCode) {
|
|
274
276
|
const exportUIs = (peerUIs: IPeersPackageUIs) => {
|
|
275
277
|
peerUIs?.uis?.forEach(ui => {
|