Skip to content

feat(registry): version picker on plugin detail (#1034)#1125

Merged
ascorbic merged 3 commits into
mainfrom
feat/registry-release-picker
May 23, 2026
Merged

feat(registry): version picker on plugin detail (#1034)#1125
ascorbic merged 3 commits into
mainfrom
feat/registry-release-picker

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

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.version write path is unchanged — the existing install handler already accepts an optional version arg.

Closes #1034
Discussion: #296

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • `pnpm typecheck` passes
  • `pnpm lint` passes (baseline 56, no per-file deltas)
  • `pnpm test` passes (admin: 879/879)
  • `pnpm format` has been run
  • I have added/updated tests for my changes (if applicable) — RegistryPluginDetail.tsx has no existing test file; manual review covered the picker behaviors against the adversarial-reviewer findings (see below)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include `messages.po` changes except in translation PRs — a workflow extracts catalogs on merge to `main`.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: Marketplace Discussion #296

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.7

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, listReleases pagination, all-filtered silent dead-end) which were also addressed: the reset moved to the prev-prop-during-render pattern, the picker requests limit: 100, and an all-filtered banner mirrors the existing policy holdback notice.

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.
Copilot AI review requested due to automatic review settings May 20, 2026 20:54
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

🦋 Changeset detected

Latest commit: a606136

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@emdash-cms/admin Minor
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 20, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-perf-coordinator a606136 May 22 2026, 04:23 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 20, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-i18n a606136 May 22 2026, 04:24 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 20, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs a606136 May 22 2026, 04:23 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 20, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground a606136 May 22 2026, 04:25 PM

@ascorbic
Copy link
Copy Markdown
Collaborator Author

/review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 20, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1125

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1125

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1125

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1125

emdash

npm i https://pkg.pr.new/emdash@1125

create-emdash

npm i https://pkg.pr.new/create-emdash@1125

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1125

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1125

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1125

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1125

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1125

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1125

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1125

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1125

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1125

commit: a606136

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 20, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache a606136 May 22 2026, 04:25 PM

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 listReleases options (cursor/limit) and fetch up to 100 releases.
  • Update RegistryPluginDetail to render a version dropdown, swap displayed metadata by selected release, and filter malformed/yanked entries.
  • Add a changeset bumping @emdash-cms/admin and emdash for 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],
Comment on lines +594 to +598
* `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]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NITclassName="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.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 20, 2026

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:

  1. Misleading comment on defaultVersion fallback (says "highest installable" but actually returns absolute highest).
  2. Stale selection UX after release refetch — selected version disappearing leaves the UI without explanation.
  3. Narrow-viewport overflow — fixed 220px Select with no shrink/wrap protection in the header row.

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.

github run

ascorbic and others added 2 commits May 20, 2026 22:24
- 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).
@ascorbic ascorbic merged commit d0ff94b into main May 23, 2026
37 checks passed
@ascorbic ascorbic deleted the feat/registry-release-picker branch May 23, 2026 05:57
@emdashbot emdashbot Bot mentioned this pull request May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Registry admin: multi-release picker + per-release metadata

2 participants