fix(web): keep plugin settings open while saving (#44838)

This commit is contained in:
Winoren authored and GitHub committed 2026-10-30 04:38:34 +00:00
1 parent b1d2e2983b
commit caf04b0d7e
3 files changed
+39 -18

No files matched your search

@@ -152,24 +152,27 @@ it('keeps an earlier category rollback separate from the current category sessio
)
changeCategory(PluginCategoryEnum.tool)
await waitFor(() =>
expect(settingsTrigger()).toHaveTextContent('plugin.autoUpdate.strategy.fixOnly.name'),
expect(getSettings).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/fetch', {
params: { category: PluginCategoryEnum.tool },
}),
)
await user.click(settingsTrigger())
expect(strategy('fixOnly')).toBeChecked()
await act(async () => {
pendingSave.reject(new Error('Save failed'))
})
expect(strategy('fixOnly')).toBeChecked()
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(settingsTrigger()).toHaveTextContent('plugin.autoUpdate.strategy.fixOnly.name')
await user.click(settingsTrigger())
expect(strategy('fixOnly')).toBeChecked()
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
changeCategory(PluginCategoryEnum.model)
await waitFor(() =>
expect(settingsTrigger()).toHaveTextContent('plugin.autoUpdate.strategy.disabled.name'),
)
})
it('blocks another save while one is pending and rolls back a failed save without losing the open draft', async () => {
it('keeps a failed save open for retry and blocks dismissal while pending', async () => {
const pendingSave = deferred<void>()
saveSettings.mockReturnValue(pendingSave.promise)
const user = userEvent.setup()
@@ -180,26 +183,32 @@ it('blocks another save while one is pending and rolls back a failed save withou
await user.click(settingsTrigger())
await user.click(strategy('latest'))
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(settingsTrigger()).toHaveTextContent('plugin.autoUpdate.strategy.latest.name')
const trigger = settingsTrigger()
await user.click(trigger)
const dialog = screen.getByRole('dialog')
const save = screen.getByRole('button', { name: 'common.operation.save' })
const cancel = screen.getByRole('button', { name: 'common.operation.cancel' })
expect(dialog).toBeVisible()
expect(save).toBeDisabled()
expect(cancel).toBeDisabled()
await user.click(save)
await user.keyboard('{Escape}')
expect(dialog).toBeVisible()
expect(saveSettings).toHaveBeenCalledOnce()
await act(async () => {
pendingSave.reject(new Error('Save failed'))
})
await waitFor(() => expect(trigger).toHaveTextContent('plugin.autoUpdate.strategy.disabled.name'))
expect(save).toBeEnabled()
expect(dialog).toBeVisible()
await waitFor(() => expect(save).toBeEnabled())
expect(cancel).toBeEnabled()
expect(strategy('latest')).toBeChecked()
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
await user.click(cancel)
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(settingsTrigger()).toHaveTextContent('plugin.autoUpdate.strategy.disabled.name')
await user.click(settingsTrigger())
expect(strategy('disabled')).toBeChecked()
expect(saveSettings).toHaveBeenCalledOnce()
})
it('reports success after the submitted form has closed', async () => {
it('closes and reports success only after the save succeeds', async () => {
const pendingSave = deferred<void>()
saveSettings.mockReturnValue(pendingSave.promise)
const success = vi.spyOn(toast, 'success')
@@ -209,10 +218,11 @@ it('reports success after the submitted form has closed', async () => {
await user.click(settingsTrigger())
await user.click(strategy('latest'))
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
expect(screen.getByRole('dialog')).toBeVisible()
expect(success).not.toHaveBeenCalled()
await act(async () => {
pendingSave.resolve()
})
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
await waitFor(() => expect(success).toHaveBeenCalledWith('common.api.actionSuccess'))
})
@@ -254,7 +254,10 @@ export function UpdateSettingDialogForm({
)}
</div>
<div className="flex h-19 items-center justify-end gap-2 px-6 pt-5 pb-6">
<DialogClose render={<Button variant="secondary" className="min-w-18" />}>
<DialogClose
disabled={isSavePending}
render={<Button variant="secondary" className="min-w-18" />}
>
{t(($) => $['operation.cancel'], { ns: 'common' })}
</DialogClose>
<Button type="submit" variant="primary" className="min-w-18" disabled={isSavePending}>
@@ -40,6 +40,7 @@ export function UpdateSettingDialog({ category }: Props) {
category,
onSuccess: () => {
toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }))
actionsRef.current?.close()
},
})
const savedAutoUpgrade = autoUpgradeSetting?.auto_upgrade
@@ -62,11 +63,15 @@ export function UpdateSettingDialog({ category }: Props) {
: ''
const handleSave = (autoUpgrade: AutoUpdateConfig) => {
saveAutoUpgrade(autoUpgrade)
actionsRef.current?.close()
}
return (
<Dialog actionsRef={actionsRef}>
<Dialog
actionsRef={actionsRef}
onOpenChange={(open, details) => {
if (!open && isSavePending && details.reason !== 'imperative-action') details.cancel()
}}
>
<DialogTrigger
render={
<Button variant="secondary" className="h-8 system-sm-medium">
@@ -85,7 +90,10 @@ export function UpdateSettingDialog({ category }: Props) {
<DialogTitle>
{t(($) => $['autoUpdate.autoUpdateSettings'], { ns: 'plugin' })}
</DialogTitle>
<DialogHeaderCloseButton aria-label={t(($) => $['operation.close'], { ns: 'common' })} />
<DialogHeaderCloseButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
disabled={isSavePending}
/>
</DialogHeader>
{isSettingsLoading && (
<DialogBody>