Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/__tests__/plugins/pluginsAdmin.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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(
<Wrapper>
<PluginsPage />
</Wrapper>,
)

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) => {
Expand Down
25 changes: 22 additions & 3 deletions src/admin/pages/plugins/PluginsPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
white-space: nowrap;
}

.error,
.emptyState {
.error {
margin: 0;
font-size: var(--text-s);
line-height: 1.5;
Expand Down Expand Up @@ -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);
Expand Down
67 changes: 50 additions & 17 deletions src/admin/pages/plugins/PluginsPage.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand Down Expand Up @@ -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={(
<>
<Button
variant="primary"
variant="secondary"
size="md"
disabled={uploading}
onClick={() => fileInputRef.current?.click()}
onClick={openCommunityPluginDirectory}
>
<UploadIcon size={15} aria-hidden="true" />
<span>{uploading ? 'Uploading' : 'Upload Plugin'}</span>
<ExternalLinkSolidIcon size={15} aria-hidden="true" />
<span>Browse Community Plugins</span>
</Button>
<input
ref={fileInputRef}
className={styles.fileInput}
aria-label="Plugin file"
type="file"
accept="application/json,.json,.plugin.json,.pbplugin,.zip,application/zip"
onChange={(event) => void vm.handleUpload(event)}
/>
{canInstall && (
<>
<Button
variant="primary"
size="md"
disabled={uploading}
onClick={() => fileInputRef.current?.click()}
>
<UploadIcon size={15} aria-hidden="true" />
<span>{uploading ? 'Uploading' : 'Upload Plugin'}</span>
</Button>
<input
ref={fileInputRef}
className={styles.fileInput}
aria-label="Plugin file"
type="file"
accept="application/json,.json,.plugin.json,.pbplugin,.zip,application/zip"
onChange={(event) => void vm.handleUpload(event)}
/>
</>
)}
</>
) : null}
)}
>
<div className={styles.pluginsBody} data-testid="plugins-admin-canvas">
{error && (
Expand Down Expand Up @@ -144,7 +162,22 @@ export function PluginsPage() {
<PluginCard key={i} loading />
))
) : payload.plugins.length === 0 ? (
<p className={styles.emptyState}>No plugins installed yet.</p>
<div className={styles.emptyState}>
<h2>No plugins installed yet.</h2>
<p>
Browse community plugins on GitHub, download a release ZIP,
then upload it here. Review the source and requested permissions
before installing any community package.
</p>
<Button
variant="secondary"
size="md"
onClick={openCommunityPluginDirectory}
>
<ExternalLinkSolidIcon size={15} aria-hidden="true" />
<span>Find Community Plugins</span>
</Button>
</div>
) : (
payload.plugins.map((plugin) => (
<PluginCard
Expand Down