From d46dd46f278dd98327b9004499cd73c5afd9ddb0 Mon Sep 17 00:00:00 2001 From: wdc <20417204@qq.com> Date: Thu, 6 Aug 2026 16:02:51 +0800 Subject: [PATCH] feat(plugins): add community discovery entry --- src/__tests__/plugins/pluginsAdmin.test.tsx | 34 ++++++++++ .../pages/plugins/PluginsPage.module.css | 25 ++++++- src/admin/pages/plugins/PluginsPage.tsx | 67 ++++++++++++++----- 3 files changed, 106 insertions(+), 20 deletions(-) diff --git a/src/__tests__/plugins/pluginsAdmin.test.tsx b/src/__tests__/plugins/pluginsAdmin.test.tsx index e38ad9933..0a1f4905d 100644 --- a/src/__tests__/plugins/pluginsAdmin.test.tsx +++ b/src/__tests__/plugins/pluginsAdmin.test.tsx @@ -10,6 +10,7 @@ import type { CmsCurrentUser } from '@core/persistence' import { makeSite } from '../fixtures' const originalFetch = globalThis.fetch +const originalWindowOpen = window.open const mapManifest = { id: 'local.map', @@ -152,10 +153,43 @@ beforeEach(() => { afterEach(() => { globalThis.fetch = originalFetch + window.open = originalWindowOpen cleanup() }) describe('PluginsPage', () => { + it('opens the community plugin directory from the empty state', async () => { + const opened: unknown[][] = [] + window.open = ((...args: unknown[]) => { + opened.push(args) + return null + }) as typeof window.open + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input) + if (url === '/admin/api/cms/plugins' && init?.method === 'GET') { + return json({ plugins: [], adminPages: [] }) + } + const ambient = ambientFetchFallback(url) + if (ambient) return ambient + return json({ error: `Unhandled ${url}` }, 500) + } + + render( + + + , + ) + + expect(await screen.findByText('No plugins installed yet.')).toBeDefined() + fireEvent.click(screen.getByRole('button', { name: 'Find Community Plugins' })) + + expect(opened).toEqual([[ + 'https://github.com/topics/instatic-plugin', + '_blank', + 'noopener,noreferrer', + ]]) + }) + it('lists active plugins and can disable or remove them', async () => { const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [] globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { diff --git a/src/admin/pages/plugins/PluginsPage.module.css b/src/admin/pages/plugins/PluginsPage.module.css index f1444b2db..c7db281b1 100644 --- a/src/admin/pages/plugins/PluginsPage.module.css +++ b/src/admin/pages/plugins/PluginsPage.module.css @@ -17,8 +17,7 @@ white-space: nowrap; } -.error, -.emptyState { +.error { margin: 0; font-size: var(--text-s); line-height: 1.5; @@ -56,13 +55,33 @@ } .emptyState { + display: grid; + justify-items: center; + gap: var(--space-m); padding: var(--space-6xl); border: 1px dashed var(--overlay-10); - border-radius: 8px; + border-radius: var(--editor-radius); color: var(--text-subtle); text-align: center; } +.emptyState h2, +.emptyState p { + margin: 0; +} + +.emptyState h2 { + color: var(--text); + font-size: var(--text-l); + line-height: 1.35; +} + +.emptyState p { + max-width: 560px; + font-size: var(--text-s); + line-height: 1.5; +} + .pluginsList { display: grid; gap: var(--space-px); diff --git a/src/admin/pages/plugins/PluginsPage.tsx b/src/admin/pages/plugins/PluginsPage.tsx index 85f38d56a..e133ad02a 100644 --- a/src/admin/pages/plugins/PluginsPage.tsx +++ b/src/admin/pages/plugins/PluginsPage.tsx @@ -1,4 +1,5 @@ import { Button } from '@ui/components/Button' +import { ExternalLinkSolidIcon } from 'pixel-art-icons/icons/external-link-solid' import { UploadIcon } from 'pixel-art-icons/icons/upload' import { AdminPageLayout } from '@admin/layouts/AdminPageLayout' import { PluginCard } from './components/PluginCard/PluginCard' @@ -21,6 +22,11 @@ import styles from './PluginsPage.module.css' // (e.g. host plugins + Analytics). PluginCard's `loading` prop owns // the actual skeleton markup — page-level code only decides count. const SKELETON_CARD_COUNT = 3 +const COMMUNITY_PLUGIN_DIRECTORY_URL = 'https://github.com/topics/instatic-plugin' + +function openCommunityPluginDirectory(): void { + window.open(COMMUNITY_PLUGIN_DIRECTORY_URL, '_blank', 'noopener,noreferrer') +} export function PluginsPage() { const currentUser = useAuthenticatedAdminUser() @@ -48,28 +54,40 @@ export function PluginsPage() { workspace="plugins" title="Plugins" titleId="plugins-title" - description="Install admin extensions and control what they add to the CMS." - actions={canInstall ? ( + description="Discover community extensions, install packages, and control what they add to the CMS." + actions={( <> - void vm.handleUpload(event)} - /> + {canInstall && ( + <> + + void vm.handleUpload(event)} + /> + + )} - ) : null} + )} >
{error && ( @@ -144,7 +162,22 @@ export function PluginsPage() { )) ) : payload.plugins.length === 0 ? ( -

No plugins installed yet.

+
+

No plugins installed yet.

+

+ Browse community plugins on GitHub, download a release ZIP, + then upload it here. Review the source and requested permissions + before installing any community package. +

+ +
) : ( payload.plugins.map((plugin) => (