feat(registry): version picker on plugin detail (#1034)#1125
Conversation
Replaces the single `getLatestRelease` query on the registry plugin
detail page with a paged `listReleases` query and renders a version
dropdown next to the Install button. Selecting an older version swaps
the displayed version, indexed date, declared permissions, and source
link to that release; the Install button targets the selected version.
Pre-release identifiers (semver `MAJOR.MINOR.PATCH-…`) earn a
"Pre-release" badge. Yanked and lexicon-invalid releases are filtered
out client-side so they never reach the picker; an empty result after
filtering surfaces a "no installable releases" banner.
`selectedVersion` resets during render when `pluginId` changes so a
fast navigation between two registry plugin pages can't carry a stale
selection across packages.
`listRegistryReleases` now accepts `{ cursor, limit }` and the picker
requests `limit: 100` (the lexicon ceiling) to avoid losing access to
older releases on long-running plugins.
🦋 Changeset detectedLatest commit: a606136 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-perf-coordinator | a606136 | May 22 2026, 04:23 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-i18n | a606136 | May 22 2026, 04:24 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | a606136 | May 22 2026, 04:23 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | a606136 | May 22 2026, 04:25 PM |
|
/review |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | a606136 | May 22 2026, 04:25 PM |
There was a problem hiding this comment.
Pull request overview
Adds multi-release selection to the Admin registry plugin detail page so admins can view/install older versions and see per-release metadata (version/indexed date/permissions/source), including pre-release labeling and policy holdback behavior.
Changes:
- Extend the admin registry API wrapper to support
listReleasesoptions (cursor/limit) and fetch up to 100 releases. - Update
RegistryPluginDetailto render a version dropdown, swap displayed metadata by selected release, and filter malformed/yanked entries. - Add a changeset bumping
@emdash-cms/adminandemdashfor the new UI feature.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/admin/src/lib/api/registry.ts | Updates listReleases wrapper to accept {cursor, limit} options. |
| packages/admin/src/components/RegistryPluginDetail.tsx | Implements the version picker and per-release UI/behavior (policy holdback, pre-release badge, filtered-empty banner). |
| .changeset/registry-release-picker.md | Declares a minor release for the new version picker feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 100 releases would still lose access to the oldest, but that's far past | ||
| // what a single plugin would ever ship in the experimental phase. | ||
| const { data: releasesData } = useQuery({ | ||
| queryKey: ["registry", "releases", config.aggregatorUrl, pkg?.did, slug], |
| * `l.neg === true` marks a label as a *negation* (an earlier yank that was | ||
| * later retracted); a release with only a negated yank is back to installable. | ||
| */ | ||
| function isYanked(release: RegistryReleaseView): boolean { | ||
| return (release.labels ?? []).some((l) => l.val === YANKED_LABEL_VALUE && !l.neg); |
| releasePassesPolicy(r, { did: pkg.did, slug }, config.policy), | ||
| ); | ||
| return (passes ?? releases[0])?.version; | ||
| }, [pkg, releases, slug, config.policy]); |
There was a problem hiding this comment.
NIT — the docstring just above says the fallback is "the highest installable record", but the code is passes ?? releases[0]: when no release passes the policy holdback, this returns the absolute highest version, which is precisely not installable (it's still under holdback). The selected option then drives the Install button into the disabled-with-holdback-notice state, which is reasonable UX, but the comment overstates what's happening. Either re-word to "the highest version, even if blocked" or actually fall back to the most recent installable one. Non-blocking.
| () => releases.find((r) => r.version === effectiveVersion), | ||
| [releases, effectiveVersion], | ||
| ); | ||
| const isPreRelease = release ? isPreReleaseVersion(release.version) : false; |
There was a problem hiding this comment.
NIT — minor stale-selection edge case: if selectedVersion was set by the user and a subsequent releases refetch removes that version (e.g. it got yanked between visits, or acceptLabelers config changed), release resolves to undefined, the Select trigger displays an unmatched value, and the Install button silently goes disabled with no "that version is no longer available" hint. The pluginId-reset pattern handles cross-package navigation, but not in-package release-list churn. Worth either reconciling selectedVersion against releases (drop it if missing) or surfacing a small notice. Non-blocking — the install path is still gated, so this is purely UX polish.
| {releases.length > 1 ? ( | ||
| <Select | ||
| aria-label={t`Version`} | ||
| className="w-[220px]" |
There was a problem hiding this comment.
NIT — className="w-[220px]" plus the right-column flex row has no shrink-0 or wrap protection, and the middle column (min-w-0 flex-1) will keep yielding width until the 220px Select forces overflow on narrow viewports (e.g. embedded admin in a side panel, mobile widths). Consider max-w-[220px] + min-w-0, or letting the row wrap (flex-wrap) so the Install button stays on screen. Worth a quick check at <500px viewport before merging.
|
Review posted successfully with three line-anchored NIT comments. The PR overall looks well-executed — the author has already worked through two rounds of adversarial review themselves, and the remaining findings are all non-blocking polish items:
No security, correctness, or i18n/RTL issues found. The yanked filter and pre-release regex are well-reasoned, render-time pluginId reset correctly prevents the cross-package selection race, and all strings are properly wrapped for translation. |
- Include `config.acceptLabelers` in the releases queryKey so a runtime manifest reload doesn't serve a cached list hydrated under a different labeller set. - Drop the `!l.neg` guard in `isYanked` to match the server install handler's check, which ignores negation. Honoring `neg` on both sides is a separate follow-up. - Reconcile `selectedVersion` against the current release list during render. Covers the case where the selected version gets yanked between visits or the labeller config changes underneath the user. - Allow the detail-page header row to wrap on narrow viewports and let the version Select shrink within the actions container so the Install button can't be pushed off-screen below ~350px. - Reword the default-version fallback comment so it reflects what the code actually does (fall back to highest version, not highest installable).
What does this PR do?
Adds a version picker to the registry plugin detail page. Older releases of a registry-hosted plugin are now selectable from a dropdown next to the Install button, and the displayed version, indexed date, declared permissions, and source link swap to match the selected release. Pre-release versions (e.g.
1.0.0-alpha.1) are flagged with a "Pre-release" badge so admins can spot them before installing. Versions still inside the configured minimum-release-age holdback remain visible in the dropdown but stay non-installable until they age into the window.Yanked and lexicon-invalid releases are filtered out client-side so they never reach the picker; when filtering empties the list a new "no installable releases" banner explains the dead-end instead of leaving the user with a silently-disabled Install button. The
_plugin_state.versionwrite path is unchanged — the existing install handler already accepts an optionalversionarg.Closes #1034
Discussion: #296
Type of change
Checklist
RegistryPluginDetail.tsxhas no existing test file; manual review covered the picker behaviors against the adversarial-reviewer findings (see below)AI-generated code disclosure
Screenshots / test output
Two adversarial-reviewer passes were run against the diff before opening; round 1 surfaced 5 issues (state leak across navigation, missing yanked filter, loose pre-release regex, no per-option disable handling, Kumo Select trigger duplication) which were all addressed. Round 2 surfaced 3 follow-ups (cached-nav render flash via post-commit effect,
listReleasespagination, all-filtered silent dead-end) which were also addressed: the reset moved to the prev-prop-during-render pattern, the picker requestslimit: 100, and an all-filtered banner mirrors the existing policy holdback notice.