mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 00:14:18 +00:00
fix: ask about turning plugins on in the quick-install dialog itself, with Install only beside it
The test that answered a second question after Install now picks the choice in the first dialog, since that question is no longer asked on quick install.
This commit is contained in:
@@ -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(<MarketplacePage />);
|
||||
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(<MarketplacePage />);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<MarketplaceListing | null>(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<boolean | null>(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() {
|
||||
/>
|
||||
|
||||
<ConfirmDialog {...confirmProps} />
|
||||
<ConfirmDialog
|
||||
<Dialog
|
||||
isOpen={quickInstall !== null}
|
||||
onClose={() => 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={(
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setQuickInstall(null)}>Cancel</Button>
|
||||
{quickPluginsOff && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const ext = quickInstall;
|
||||
setQuickInstall(null);
|
||||
if (ext?.latestVersion) void install(ext, ext.latestVersion, false, false);
|
||||
}}
|
||||
>
|
||||
Install only
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={quickPluginsOff === null}
|
||||
onClick={() => {
|
||||
const ext = quickInstall;
|
||||
const turnOn = quickPluginsOff ?? false;
|
||||
setQuickInstall(null);
|
||||
if (ext?.latestVersion) void install(ext, ext.latestVersion, false, turnOn);
|
||||
}}
|
||||
>
|
||||
{quickPluginsOff ? 'Install and turn on plugins' : 'Install'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{quickInstall && (
|
||||
<div className="mt-3 max-h-64 overflow-y-auto" data-testid="quick-install-permissions">
|
||||
<PluginPermissionList permissions={quickInstall.permissions ?? []} compact />
|
||||
</div>
|
||||
) : null}
|
||||
confirmText="Install"
|
||||
cancelText="Cancel"
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
<div className="space-y-10">
|
||||
{updates && noticeCount > 0 && (
|
||||
|
||||
@@ -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<boolean | null> {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user