diff --git a/app/(main)/admin/__tests__/marketplace-quick-install.test.tsx b/app/(main)/admin/__tests__/marketplace-quick-install.test.tsx index b2cdc9d4f..78b7b6d89 100644 --- a/app/(main)/admin/__tests__/marketplace-quick-install.test.tsx +++ b/app/(main)/admin/__tests__/marketplace-quick-install.test.tsx @@ -85,16 +85,29 @@ describe('Quick install', () => { expect(within(card).queryByText('hook')).toBeNull(); }); - it('asks to turn plugins on for users when they are off', async () => { + it('offers to turn plugins on in the same dialog when they are off, and asks nothing after', async () => { policy = { features: { pluginsEnabled: false } }; render(); const card = (await screen.findByText('Send Checks')).closest('.group') as HTMLElement; fireEvent.click(within(card).getByRole('button', { name: /Quick install/ })); - fireEvent.click(within(await screen.findByRole('alertdialog')).getByRole('button', { name: 'Install' })); - const question = await screen.findByText('Turn on plugins for users too?'); - fireEvent.click(within(question.closest('[role="alertdialog"]') as HTMLElement).getByRole('button', { name: 'Turn on' })); + const dialog = await screen.findByRole('alertdialog'); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Install and turn on plugins' })); + expect(within(dialog).queryByText(/turned on for them/)).toBeNull(); await waitFor(() => expect(calls).toContainEqual(expect.objectContaining({ url: '/api/admin/policy', method: 'PUT', body: expect.objectContaining({ features: { pluginsEnabled: true } }), }))); + expect(installs()).toHaveLength(1); + expect(screen.queryByText('Turn on plugins for users too?')).toBeNull(); + }); + + it('installs only, leaving plugins off, when the admin says so', async () => { + policy = { features: { pluginsEnabled: false } }; + render(); + const card = (await screen.findByText('Send Checks')).closest('.group') as HTMLElement; + fireEvent.click(within(card).getByRole('button', { name: /Quick install/ })); + fireEvent.click(await within(await screen.findByRole('alertdialog')).findByRole('button', { name: 'Install only' })); + await waitFor(() => expect(installs()).toHaveLength(1)); + await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull()); + expect(calls.some((c) => c.url === '/api/admin/policy' && c.method === 'PUT')).toBe(false); }); }); diff --git a/app/(main)/admin/_pages/marketplace.tsx b/app/(main)/admin/_pages/marketplace.tsx index 9672d9966..8639609e0 100644 --- a/app/(main)/admin/_pages/marketplace.tsx +++ b/app/(main)/admin/_pages/marketplace.tsx @@ -21,10 +21,11 @@ import { EmptyState } from '@/components/admin/empty-state'; import { useAdminFeedback } from '@/components/admin/feedback'; import { ConfigNote, inputClass } from '@/components/admin/config-items'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; +import { Dialog } from '@/components/ui/dialog'; import { useConfirmDialog } from '@/hooks/use-confirm-dialog'; import { ChangelogBadges, ChangelogEntries, anyFlag, type ChangelogEntry, type ChangelogFlags } from '@/components/admin/changelog'; import { PluginPermissionList } from '@/components/plugins/plugin-permission-list'; -import { offerToTurnOnPlugins, turnOnNote, type TurnOnOutcome } from './turn-on-plugins'; +import { offerToTurnOnPlugins, pluginsOffForUsers, turnOnNote, type TurnOnOutcome } from './turn-on-plugins'; import { pluginTypeLabel } from './plugin-type-label'; const CURRENT_APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0'; @@ -176,6 +177,16 @@ export function MarketplacePage() { const [updatesLoading, setUpdatesLoading] = useState(true); // A plugin's quick install asks first, with what it may do (themes don't). const [quickInstall, setQuickInstall] = useState(null); + // Whether plugins are off for users, so the one quick-install dialog can offer to + // turn them on too instead of asking again after installing. null: not known yet. + const [quickPluginsOff, setQuickPluginsOff] = useState(null); + useEffect(() => { + if (!quickInstall) return; + let current = true; + setQuickPluginsOff(null); + void pluginsOffForUsers().then((off) => { if (current) setQuickPluginsOff(off); }); + return () => { current = false; }; + }, [quickInstall]); const fetchExtensions = useCallback(async () => { setLoading(true); @@ -238,7 +249,8 @@ export function MarketplacePage() { return next; }); - async function install(ext: { slug: string; name: string; type: 'plugin' | 'theme' }, version: string, isUpdate: boolean) { + /** `turnOn`: the admin already said whether to turn plugins on for users; otherwise they're asked after installing. */ + async function install(ext: { slug: string; name: string; type: 'plugin' | 'theme' }, version: string, isUpdate: boolean, turnOn?: boolean) { markInstalling(ext.slug, true); setMessage(null); const result = await installVersion({ slug: ext.slug, version, type: ext.type }, confirm); @@ -249,7 +261,9 @@ export function MarketplacePage() { } const warnings = result.warnings.length ? ` (${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'})` : ''; const signed = result.signature === 'verified' ? ', signed by the directory' : ''; - const outcome: TurnOnOutcome = !isUpdate && ext.type === 'plugin' ? await offerToTurnOnPlugins(confirm) : 'already-on'; + const outcome: TurnOnOutcome = !isUpdate && ext.type === 'plugin' + ? await offerToTurnOnPlugins(turnOn === undefined ? confirm : () => turnOn) + : 'already-on'; setMessage({ type: 'success', text: isUpdate @@ -305,24 +319,50 @@ export function MarketplacePage() { /> - setQuickInstall(null)} - onConfirm={() => { - const ext = quickInstall; - setQuickInstall(null); - if (ext?.latestVersion) void install(ext, ext.latestVersion, false); - }} + role="alertdialog" + showClose={false} title={`Install ${quickInstall?.name ?? ''}?`} - message={`${quickInstall?.author ? `By ${quickInstall.author.displayName}. ` : ''}It is installed for every user and turned on for them; they can turn it off in their settings. What it may do in each mailbox:`} - details={quickInstall ? ( + description={`${quickInstall?.author ? `By ${quickInstall.author.displayName}. ` : ''}${quickPluginsOff + ? 'It is installed for every user. Plugins are off for users, so it only runs once you turn them on.' + : 'It is installed for every user and turned on for them; they can turn it off in their settings.'} What it may do in each mailbox:`} + footer={( + <> + + {quickPluginsOff && ( + + )} + + + )} + > + {quickInstall && (
- ) : null} - confirmText="Install" - cancelText="Cancel" - /> + )} +
{updates && noticeCount > 0 && ( diff --git a/app/(main)/admin/_pages/turn-on-plugins.ts b/app/(main)/admin/_pages/turn-on-plugins.ts index 7788026b5..a04dddeca 100644 --- a/app/(main)/admin/_pages/turn-on-plugins.ts +++ b/app/(main)/admin/_pages/turn-on-plugins.ts @@ -44,6 +44,18 @@ export async function offerToTurnOnPlugins( } } +/** Whether plugins are off for users; null when the policy can't be read. */ +export async function pluginsOffForUsers(): Promise { + try { + const res = await apiFetch('/api/admin/policy'); + if (!res.ok) return null; + const policy = (await res.json()) as SettingsPolicy; + return !policy.features?.pluginsEnabled; + } catch { + return null; + } +} + /** The line added to the "installed" message for each outcome. */ export function turnOnNote(outcome: TurnOnOutcome): string { switch (outcome) {