@peers-app/peers-ui 0.18.8 → 0.19.6

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.
@@ -2,27 +2,27 @@ import {
2
2
  doesTagMatch,
3
3
  getEffectivePackagePrefs,
4
4
  groupDeviceVar,
5
+ hasDeviceFollowOverride,
5
6
  type IDoc,
6
7
  type IPackage,
7
8
  type IPackageVersion,
9
+ Packages,
8
10
  PackageVersions,
9
11
  packagePrefsVar,
10
12
  packagesRootDir,
11
13
  rpcServerCalls,
12
14
  updatePackagePrefs,
13
15
  } from "@peers-app/peers-sdk";
14
- import React from "react";
15
16
  import { Input } from "../../components/input";
16
17
  import { MarkdownWithMentions } from "../../components/markdown-with-mentions";
17
18
  import { Tooltip } from "../../components/tooltip";
18
19
  import { useObservable, usePromise } from "../../hooks";
19
20
 
20
- export const PackageInfo = (props: {
21
- pkg: IDoc<IPackage>;
22
- saveDeviceTagRef?: React.MutableRefObject<(() => Promise<void>) | null>;
23
- }) => {
21
+ /** Info tab for the package details screen. */
22
+ export const PackageInfo = (props: { pkg: IDoc<IPackage> }) => {
24
23
  const { pkg } = props;
25
24
  const [followVersionTags] = useObservable(pkg.qs.followVersionTags);
25
+ const [versionFollowRange] = useObservable(pkg.qs.versionFollowRange);
26
26
 
27
27
  const localPathVar = groupDeviceVar<string>(`packageLocalPath_${pkg.packageId}`, {
28
28
  defaultValue: `${packagesRootDir}/${pkg.name}`,
@@ -32,45 +32,16 @@ export const PackageInfo = (props: {
32
32
  const [devicePrefs] = useObservable(packagePrefsVar(pkg.packageId));
33
33
  const effective = getEffectivePackagePrefs(pkg.toJS(), devicePrefs);
34
34
  const activeVersionId = effective.activePackageVersionId;
35
- const deviceFollowTags = devicePrefs?.followTags;
36
35
  const isPinned = effective.isPinned;
37
36
  const followRange = effective.followRange;
38
37
 
39
- const [deviceTagDraft, setDeviceTagDraft] = React.useState(deviceFollowTags || "");
40
- const savingRef = React.useRef(false);
38
+ const hasOverride = hasDeviceFollowOverride(devicePrefs);
41
39
 
42
- React.useEffect(() => {
43
- if (!savingRef.current) {
44
- setDeviceTagDraft(deviceFollowTags || "");
45
- }
46
- }, [deviceFollowTags]);
40
+ const groupFollowTags = followVersionTags === "stable,beta" ? "stable,beta" : "stable";
41
+ const groupRange = versionFollowRange || "latest";
47
42
 
48
- const deviceTagDirty = deviceTagDraft.trim() !== (deviceFollowTags || "");
49
- const prevDirtyRef = React.useRef(false);
50
- React.useEffect(() => {
51
- if (deviceTagDirty && !prevDirtyRef.current) {
52
- pkg.q((pkg.q() || 0) + 1);
53
- } else if (!deviceTagDirty && prevDirtyRef.current) {
54
- pkg.q(Math.max(0, (pkg.q() || 0) - 1));
55
- }
56
- prevDirtyRef.current = deviceTagDirty;
57
- }, [deviceTagDirty, pkg.q]);
58
-
59
- if (props.saveDeviceTagRef) {
60
- props.saveDeviceTagRef.current = async () => {
61
- const val = deviceTagDraft.trim();
62
- if (val !== (deviceFollowTags || "")) {
63
- savingRef.current = true;
64
- try {
65
- await updatePackagePrefs(pkg.packageId, {
66
- followTags: val || undefined,
67
- });
68
- } finally {
69
- savingRef.current = false;
70
- }
71
- }
72
- };
73
- }
43
+ const effectiveFollowTags = effective.followTags || "stable";
44
+ const followTagsValue = effectiveFollowTags === "stable,beta" ? "stable,beta" : "stable";
74
45
 
75
46
  const activeVersion = usePromise(
76
47
  async () => {
@@ -87,7 +58,6 @@ export const PackageInfo = (props: {
87
58
  const all = await PackageVersions().list({ packageId: pkg.packageId });
88
59
  const active = all.find((v) => v.packageVersionId === activeVersionId);
89
60
  if (!active?.version) return "uptodate";
90
- const effectiveFollowTags = deviceFollowTags || followVersionTags;
91
61
  const parse = (v: string) => v.split(".").map(Number);
92
62
  const [aMaj, aMin, aPat] = parse(active.version);
93
63
  let highest: "major" | "minor" | "patch" | null = null;
@@ -118,7 +88,7 @@ export const PackageInfo = (props: {
118
88
  return highest || "uptodate";
119
89
  },
120
90
  undefined,
121
- [activeVersionId, pkg.packageId, followVersionTags, deviceFollowTags],
91
+ [activeVersionId, pkg.packageId, effectiveFollowTags],
122
92
  );
123
93
 
124
94
  const remoteRepoUrl = pkg.remoteRepo;
@@ -217,18 +187,20 @@ export const PackageInfo = (props: {
217
187
 
218
188
  <div className="mb-3">
219
189
  <small>
220
- Auto-Update Range (this device):
221
- <Tooltip markdownContent="Controls which new versions are auto-activated on this device. **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." />
190
+ Auto-Update Range:
191
+ <Tooltip markdownContent="Controls which new versions are auto-activated for the group. When an admin device auto-upgrades, the group's active version advances for everyone. **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." />
222
192
  </small>
223
193
  <select
224
194
  className="form-select form-select-sm"
225
- value={isPinned ? "pinned" : followRange}
195
+ value={groupRange}
226
196
  onChange={async (e) => {
227
197
  const val = e.target.value as "pinned" | "patch" | "minor" | "latest";
228
- await updatePackagePrefs(pkg.packageId, {
229
- pinned: val === "pinned",
230
- followRange: val === "pinned" ? undefined : val,
231
- });
198
+ const current = await Packages().get(pkg.packageId);
199
+ if (current) {
200
+ current.versionFollowRange = val;
201
+ await Packages().signAndSave(current);
202
+ await pkg.load();
203
+ }
232
204
  }}
233
205
  >
234
206
  <option value="latest">Latest (auto-update to newest)</option>
@@ -238,31 +210,114 @@ export const PackageInfo = (props: {
238
210
  </select>
239
211
  </div>
240
212
 
241
- <div className={`mb-3 ${isPinned ? "opacity-50" : ""}`}>
213
+ <div className={`mb-3 ${groupRange === "pinned" ? "opacity-50" : ""}`}>
242
214
  <small>
243
- Follow Version Tags (this device):
244
- <Tooltip markdownContent="Which version tags to auto-activate on this device. Leave empty to follow the group's tag policy. Use `*` for any tag, or a comma-separated list like `stable,beta`." />
215
+ Following:
216
+ <Tooltip markdownContent="Which release channel the group follows. Devices using group settings will auto-upgrade within this channel. **Stable** = only fully promoted releases. **Stable + Beta** = also includes beta pre-releases for early testing." />
245
217
  </small>
246
- <div className="d-flex align-items-center gap-2">
218
+ <select
219
+ className="form-select form-select-sm"
220
+ value={groupFollowTags}
221
+ disabled={groupRange === "pinned"}
222
+ onChange={async (e) => {
223
+ const val = e.target.value;
224
+ const current = await Packages().get(pkg.packageId);
225
+ if (current) {
226
+ current.followVersionTags = val === "stable" ? undefined : val;
227
+ await Packages().signAndSave(current);
228
+ await pkg.load();
229
+ }
230
+ }}
231
+ >
232
+ <option value="stable">Stable</option>
233
+ <option value="stable,beta">Stable + Beta</option>
234
+ </select>
235
+ </div>
236
+
237
+ <div className="mb-3">
238
+ <div className="form-check">
247
239
  <input
248
- type="text"
249
- className="form-control form-control-sm p-0 ps-2"
250
- placeholder={`Following "${activeVersion?.versionTag || "stable"}"`}
251
- value={deviceTagDraft}
252
- onChange={(e) => setDeviceTagDraft(e.target.value)}
253
- disabled={isPinned}
240
+ className="form-check-input"
241
+ type="checkbox"
242
+ id={`deviceOverride_${pkg.packageId}`}
243
+ checked={hasOverride}
244
+ onChange={async (e) => {
245
+ if (e.target.checked) {
246
+ await updatePackagePrefs(pkg.packageId, {
247
+ followRange:
248
+ groupRange === "pinned"
249
+ ? undefined
250
+ : (groupRange as "patch" | "minor" | "latest"),
251
+ followTags: followVersionTags || undefined,
252
+ pinned: groupRange === "pinned" ? true : undefined,
253
+ });
254
+ } else {
255
+ await updatePackagePrefs(pkg.packageId, {
256
+ followRange: undefined,
257
+ followTags: undefined,
258
+ pinned: undefined,
259
+ });
260
+ }
261
+ }}
254
262
  />
255
- {deviceTagDraft && (
256
- <button
257
- className="btn btn-sm btn-outline-secondary"
258
- title="Clear device tag override"
259
- onClick={() => setDeviceTagDraft("")}
260
- >
261
- <i className="bi bi-x-lg"></i>
262
- </button>
263
- )}
263
+ <label className="form-check-label" htmlFor={`deviceOverride_${pkg.packageId}`}>
264
+ <small>Override on this device</small>
265
+ </label>
266
+ <small>
267
+ <Tooltip
268
+ markdownContent={`**Off:** This device follows the group's auto-update and channel settings. When an admin device upgrades, the group's active version advances for all devices.\n\n**On:** This device uses its own settings below. Upgrades only affect this device — the group's active version is not changed.`}
269
+ />
270
+ </small>
264
271
  </div>
265
272
  </div>
273
+
274
+ {hasOverride && (
275
+ <div className="ps-3 border-start mb-3">
276
+ <div className="mb-3">
277
+ <small>
278
+ Auto-Update Range (this device):
279
+ <Tooltip markdownContent="Controls which new versions are auto-activated on **this device only**. Upgrades here do not change the group's active version." />
280
+ </small>
281
+ <select
282
+ className="form-select form-select-sm"
283
+ value={isPinned ? "pinned" : followRange}
284
+ onChange={async (e) => {
285
+ const val = e.target.value as "pinned" | "patch" | "minor" | "latest";
286
+ await updatePackagePrefs(pkg.packageId, {
287
+ pinned: val === "pinned",
288
+ followRange: val === "pinned" ? undefined : val,
289
+ });
290
+ }}
291
+ >
292
+ <option value="latest">Latest (auto-update to newest)</option>
293
+ <option value="minor">Minor (same major version)</option>
294
+ <option value="patch">Patch (same major.minor version)</option>
295
+ <option value="pinned">Pinned (no auto-updates)</option>
296
+ </select>
297
+ </div>
298
+
299
+ <div className={isPinned ? "opacity-50" : ""}>
300
+ <small>
301
+ Following (this device):
302
+ <Tooltip markdownContent="Which release channel to follow on **this device only**, overriding the group setting." />
303
+ </small>
304
+ <select
305
+ className="form-select form-select-sm"
306
+ value={followTagsValue}
307
+ disabled={isPinned}
308
+ onChange={async (e) => {
309
+ const val = e.target.value;
310
+ await updatePackagePrefs(pkg.packageId, {
311
+ followTags: val === "stable" ? "stable" : val,
312
+ });
313
+ }}
314
+ >
315
+ <option value="stable">Stable</option>
316
+ <option value="stable,beta">Stable + Beta</option>
317
+ </select>
318
+ </div>
319
+ </div>
320
+ )}
266
321
  </div>
267
322
 
268
323
  <div className="mt-2">
@@ -1,12 +1,15 @@
1
1
  import {
2
- doesTagMatch,
2
+ getEffectivePackagePrefs,
3
3
  type ICursorIterable,
4
4
  type IPackage,
5
+ type IPackageVersion,
5
6
  observable,
6
7
  Packages,
7
8
  PackageVersions,
9
+ packagePrefsVar,
8
10
  packagesRootDir,
9
11
  rpcServerCalls,
12
+ updatePackagePrefs,
10
13
  } from "@peers-app/peers-sdk";
11
14
  import type React from "react";
12
15
  import { useCallback, useEffect, useState } from "react";
@@ -17,88 +20,217 @@ import { Tooltip } from "../../components/tooltip";
17
20
  import { isDesktop, mainContentPath } from "../../globals";
18
21
  import { useObservable, useObservableState, usePromise } from "../../hooks";
19
22
  import { registerInternalPeersUI } from "../../ui-router/ui-loader";
23
+ import {
24
+ activatePackageVersion,
25
+ checkVersionStatus,
26
+ type IVersionStatus,
27
+ type UpdateLevel,
28
+ updateLevelRank,
29
+ } from "./package-helpers";
20
30
 
21
- const PackageVersionBadge = ({
22
- activePackageVersionId,
23
- packageId,
24
- followVersionTags,
25
- }: {
26
- activePackageVersionId?: string;
27
- packageId: string;
28
- followVersionTags?: string;
29
- }) => {
30
- const data = usePromise(
31
- async () => {
32
- if (!activePackageVersionId) return null;
33
- const all = await PackageVersions().list({ packageId });
34
- const active = all.find((v) => v.packageVersionId === activePackageVersionId);
35
- if (!active) return null;
31
+ interface IPackageRowStatus extends IVersionStatus {
32
+ /** When on a dev version, the group's non-dev active PV (if any). */
33
+ groupReleasePv: IPackageVersion | null;
34
+ /** When on a non-dev version, the latest dev PV (if any). */
35
+ latestDevPv: IPackageVersion | null;
36
+ }
36
37
 
37
- let newerLevel: "major" | "minor" | "patch" | null = null;
38
- if (active.version) {
39
- const parse = (v: string) => v.split(".").map(Number);
40
- const [aMaj, aMin, aPat] = parse(active.version);
41
- for (const v of all) {
42
- if (!v.version || v.packageVersionId === activePackageVersionId) continue;
43
- if (!doesTagMatch(active.versionTag, v.versionTag, followVersionTags, undefined))
44
- continue;
45
- const [maj, min, pat] = parse(v.version);
46
- if (maj > aMaj) {
47
- newerLevel = "major";
48
- break;
49
- }
50
- if (maj === aMaj && min > aMin) {
51
- newerLevel = "minor";
52
- continue;
53
- }
54
- if (maj === aMaj && min === aMin && pat > aPat && !newerLevel) {
55
- newerLevel = "patch";
56
- continue;
57
- }
58
- if (
59
- maj === aMaj &&
60
- min === aMin &&
61
- pat === aPat &&
62
- (v.createdAt || "") > (active.createdAt || "") &&
63
- !newerLevel
64
- ) {
65
- newerLevel = "patch";
38
+ function tagBadgeClass(tag: string): string {
39
+ if (tag === "dev") return "text-bg-danger";
40
+ if (tag.startsWith("beta")) return "text-bg-warning";
41
+ return "text-bg-success";
42
+ }
43
+
44
+ function updateBadgeClass(level: UpdateLevel): string {
45
+ if (level === "major") return "text-bg-danger";
46
+ if (level === "minor") return "text-bg-warning";
47
+ return "text-bg-info";
48
+ }
49
+
50
+ const PackageRow = ({ pkg, onUpdated }: { pkg: IPackage; onUpdated: () => void }) => {
51
+ const [updating, setUpdating] = useState(false);
52
+ const [devicePrefs] = useObservable(packagePrefsVar(pkg.packageId));
53
+ const effective = getEffectivePackagePrefs(pkg, devicePrefs);
54
+
55
+ const status = usePromise(
56
+ async (): Promise<IPackageRowStatus | null> => {
57
+ if (!effective.activePackageVersionId) return null;
58
+ const s = await checkVersionStatus(
59
+ pkg.packageId,
60
+ effective.activePackageVersionId,
61
+ effective.followTags,
62
+ );
63
+ if (!s) return null;
64
+
65
+ let groupReleasePv: IPackageVersion | null = null;
66
+ if (
67
+ s.activePv.versionTag === "dev" &&
68
+ pkg.activePackageVersionId &&
69
+ pkg.activePackageVersionId !== effective.activePackageVersionId
70
+ ) {
71
+ try {
72
+ const gpv = await PackageVersions().get(pkg.activePackageVersionId);
73
+ if (gpv && gpv.versionTag !== "dev") groupReleasePv = gpv;
74
+ } catch {}
75
+ }
76
+ let latestDevPv: IPackageVersion | null = null;
77
+ if (s.activePv.versionTag !== "dev") {
78
+ const allVersions = await PackageVersions().list({ packageId: pkg.packageId });
79
+ for (const v of allVersions) {
80
+ if (v.versionTag !== "dev") continue;
81
+ if (!latestDevPv || (v.createdAt || "") > (latestDevPv.createdAt || "")) {
82
+ latestDevPv = v;
66
83
  }
67
84
  }
68
85
  }
69
- return { pv: active, newerLevel };
86
+
87
+ return { ...s, groupReleasePv, latestDevPv };
70
88
  },
71
89
  undefined,
72
- [activePackageVersionId, packageId, followVersionTags],
90
+ [
91
+ effective.activePackageVersionId,
92
+ pkg.packageId,
93
+ effective.followTags,
94
+ pkg.activePackageVersionId,
95
+ ],
73
96
  );
74
97
 
75
- if (!data) return null;
76
- const { pv, newerLevel } = data;
98
+ const isOnDev = status?.activePv.versionTag === "dev";
99
+ const hasGroupRelease = !!status?.groupReleasePv;
100
+ const hasUpdate = !isOnDev && !!status?.newerLevel && !!status?.newestPv;
101
+ const hasDevVersion = !isOnDev && !!status?.latestDevPv;
102
+ const isDeviceOverridden =
103
+ !!effective.activePackageVersionId &&
104
+ !!pkg.activePackageVersionId &&
105
+ effective.activePackageVersionId !== pkg.activePackageVersionId;
106
+
107
+ async function handleUpdate() {
108
+ if (!status?.newestPv) return;
109
+ setUpdating(true);
110
+ try {
111
+ await activatePackageVersion(pkg.packageId, status.newestPv);
112
+ onUpdated();
113
+ } catch (err: unknown) {
114
+ alert(`Failed to update: ${err instanceof Error ? err.message : String(err)}`);
115
+ } finally {
116
+ setUpdating(false);
117
+ }
118
+ }
119
+
120
+ async function handleUseRelease() {
121
+ setUpdating(true);
122
+ try {
123
+ await updatePackagePrefs(pkg.packageId, { activePackageVersionId: undefined });
124
+ onUpdated();
125
+ } catch (err: unknown) {
126
+ alert(`Failed to switch: ${err instanceof Error ? err.message : String(err)}`);
127
+ } finally {
128
+ setUpdating(false);
129
+ }
130
+ }
131
+
132
+ async function handleUseDev() {
133
+ if (!status?.latestDevPv) return;
134
+ setUpdating(true);
135
+ try {
136
+ await updatePackagePrefs(pkg.packageId, {
137
+ activePackageVersionId: status.latestDevPv.packageVersionId,
138
+ });
139
+ onUpdated();
140
+ } catch (err: unknown) {
141
+ alert(`Failed to switch: ${err instanceof Error ? err.message : String(err)}`);
142
+ } finally {
143
+ setUpdating(false);
144
+ }
145
+ }
77
146
 
78
147
  return (
79
- <span className="ms-2">
80
- <small className="text-muted">v{pv.version}</small>
81
- {pv.versionTag && (
82
- <span
83
- className={`badge ms-1 ${pv.versionTag === "dev" ? "text-bg-danger" : pv.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"}`}
84
- style={{ fontSize: "0.65em" }}
148
+ <div className="container-fluid pb-4 d-flex align-items-center">
149
+ <div className="flex-grow-1" style={{ minWidth: 0 }}>
150
+ <i className="bi bi-box-fill"></i>&nbsp;&nbsp;
151
+ <a href={`#packages/${pkg.packageId}`}>{pkg.name}</a>
152
+ {isDeviceOverridden && (
153
+ <span
154
+ className="text-warning ms-1"
155
+ title="This device is using a different version than the group"
156
+ >
157
+ <i className="bi bi-pc-display" />
158
+ </span>
159
+ )}
160
+ {status && (
161
+ <span className="ms-2">
162
+ <small className="text-muted">v{status.activePv.version}</small>
163
+ {status.activePv.versionTag && (
164
+ <span
165
+ className={`badge ms-1 ${tagBadgeClass(status.activePv.versionTag)}`}
166
+ style={{ fontSize: "0.65em" }}
167
+ >
168
+ {status.activePv.versionTag}
169
+ </span>
170
+ )}
171
+ {hasUpdate ? (
172
+ <span
173
+ className={`badge ms-1 ${updateBadgeClass(status.newerLevel)}`}
174
+ style={{ fontSize: "0.65em" }}
175
+ >
176
+ v{status.newestPv?.version} available
177
+ </span>
178
+ ) : isOnDev && hasGroupRelease ? (
179
+ <span className="badge ms-1 text-bg-secondary" style={{ fontSize: "0.65em" }}>
180
+ release: v{status.groupReleasePv?.version}
181
+ </span>
182
+ ) : (
183
+ <span className="badge ms-1 text-bg-success" style={{ fontSize: "0.65em" }}>
184
+ up to date
185
+ </span>
186
+ )}
187
+ </span>
188
+ )}
189
+ <Tooltip markdownContent={pkg.description} positions={["bottom", "top", "right", "left"]} />
190
+ </div>
191
+ {hasUpdate && (
192
+ <button
193
+ className="btn btn-sm btn-outline-primary ms-2 text-nowrap"
194
+ disabled={updating}
195
+ onClick={handleUpdate}
85
196
  >
86
- {pv.versionTag}
87
- </span>
197
+ {updating ? (
198
+ <span className="spinner-border spinner-border-sm me-1" />
199
+ ) : (
200
+ <i className="bi bi-arrow-up-circle me-1" />
201
+ )}
202
+ Update
203
+ </button>
88
204
  )}
89
- {newerLevel ? (
90
- <span
91
- className={`badge ms-1 text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`}
92
- style={{ fontSize: "0.65em" }}
205
+ {isOnDev && hasGroupRelease && (
206
+ <button
207
+ className="btn btn-sm btn-outline-secondary ms-2 text-nowrap"
208
+ disabled={updating}
209
+ onClick={handleUseRelease}
210
+ >
211
+ {updating ? (
212
+ <span className="spinner-border spinner-border-sm me-1" />
213
+ ) : (
214
+ <i className="bi bi-box-arrow-in-right me-1" />
215
+ )}
216
+ Use Release
217
+ </button>
218
+ )}
219
+ {hasDevVersion && (
220
+ <button
221
+ className="btn btn-sm btn-outline-danger ms-2 text-nowrap"
222
+ disabled={updating}
223
+ onClick={handleUseDev}
93
224
  >
94
- {newerLevel} update
95
- </span>
96
- ) : (
97
- <span className="badge ms-1 text-bg-success" style={{ fontSize: "0.65em" }}>
98
- up to date
99
- </span>
225
+ {updating ? (
226
+ <span className="spinner-border spinner-border-sm me-1" />
227
+ ) : (
228
+ <i className="bi bi-tools me-1" />
229
+ )}
230
+ Use Dev
231
+ </button>
100
232
  )}
101
- </span>
233
+ </div>
102
234
  );
103
235
  };
104
236
 
@@ -106,6 +238,7 @@ export const PackageList = () => {
106
238
  const [searchTextObs] = useState(() => observable(""));
107
239
  const [searchText] = useObservable(searchTextObs);
108
240
  const addingPackage = useObservableState(false);
241
+ const refreshKey = useObservableState(0);
109
242
 
110
243
  const [cursorObs] = useState(() => observable<ICursorIterable<IPackage> | undefined>());
111
244
 
@@ -140,6 +273,44 @@ export const PackageList = () => {
140
273
  if (moreMatches.length === 0) {
141
274
  cursorObs(undefined);
142
275
  }
276
+
277
+ if (!searchText.length && existing.length === 0 && moreMatches.length > 0) {
278
+ const DEV_WITH_RELEASE_RANK = 0.5;
279
+ const rankMap = new Map<string, number>();
280
+ await Promise.all(
281
+ moreMatches.map(async (pkg) => {
282
+ try {
283
+ const pVar = packagePrefsVar(pkg.packageId);
284
+ await pVar.loadingPromise;
285
+ const eff = getEffectivePackagePrefs(pkg, pVar());
286
+ if (!eff.activePackageVersionId) return;
287
+ const s = await checkVersionStatus(
288
+ pkg.packageId,
289
+ eff.activePackageVersionId,
290
+ eff.followTags,
291
+ );
292
+ if (!s) return;
293
+ if (s.newerLevel) {
294
+ rankMap.set(pkg.packageId, updateLevelRank[s.newerLevel]);
295
+ } else if (s.activePv.versionTag === "dev" && pkg.activePackageVersionId) {
296
+ try {
297
+ const gpv = await PackageVersions().get(pkg.activePackageVersionId);
298
+ if (gpv && gpv.versionTag !== "dev") {
299
+ rankMap.set(pkg.packageId, DEV_WITH_RELEASE_RANK);
300
+ }
301
+ } catch {}
302
+ }
303
+ } catch {}
304
+ }),
305
+ );
306
+ moreMatches.sort((a, b) => {
307
+ const aRank = rankMap.get(a.packageId) ?? 0;
308
+ const bRank = rankMap.get(b.packageId) ?? 0;
309
+ if (bRank !== aRank) return bRank - aRank;
310
+ return (a.name || "").localeCompare(b.name || "");
311
+ });
312
+ }
313
+
143
314
  return moreMatches;
144
315
  }
145
316
 
@@ -148,7 +319,6 @@ export const PackageList = () => {
148
319
  const name = searchText.trim();
149
320
  if (!name) return;
150
321
 
151
- // check if name is a remote repo url
152
322
  if (name.startsWith("http") || name.endsWith(".git")) {
153
323
  if (!confirm(`Add remote package: ${name}`)) return;
154
324
  try {
@@ -160,7 +330,6 @@ export const PackageList = () => {
160
330
  }
161
331
  } catch (err: unknown) {
162
332
  let errMessage = err instanceof Error ? err.message : String(err);
163
- // replace all whitespace with a single space for confirm dialog
164
333
  errMessage = errMessage.replace(/\s+/g, " ");
165
334
  confirm(`Error adding remote package: ${errMessage}`);
166
335
  } finally {
@@ -261,27 +430,17 @@ export const PackageList = () => {
261
430
 
262
431
  <div className="peers-list-container">
263
432
  <LazyList
264
- resetTrigger={searchText}
433
+ resetTrigger={`${searchText}_${refreshKey()}`}
265
434
  loadMore={loadMore}
266
435
  scrollThreshold={0.6}
267
436
  renderItems={(packages) => {
268
- return packages.map((pkg) => {
269
- return (
270
- <div key={pkg.packageId} className="container-fluid pb-4">
271
- <i className="bi bi-box-fill"></i>&nbsp;&nbsp;
272
- <a href={`#packages/${pkg.packageId}`}>{pkg.name}</a>
273
- <PackageVersionBadge
274
- activePackageVersionId={pkg.activePackageVersionId}
275
- packageId={pkg.packageId}
276
- followVersionTags={pkg.followVersionTags}
277
- />
278
- <Tooltip
279
- markdownContent={pkg.description}
280
- positions={["bottom", "top", "right", "left"]}
281
- />
282
- </div>
283
- );
284
- });
437
+ return packages.map((pkg) => (
438
+ <PackageRow
439
+ key={pkg.packageId}
440
+ pkg={pkg}
441
+ onUpdated={() => refreshKey(refreshKey() + 1)}
442
+ />
443
+ ));
285
444
  }}
286
445
  loadingIndicator={
287
446
  <div className="d-flex justify-content-center" style={{ height: 200 }}>