Skip to content

Commit afc26a2

Browse files
DavidsonGomesclaude
andcommitted
fix(plugins): refresh UI registry after install/update/uninstall + replace window.confirm with modal
Two related UX bugs Davidson hit on the v0.1.3 install: (1) Stale UI registry — sidebar + page bundles pointed at the pre-update manifest. After installing v0.1.3 over v0.1.2, the sidebar still showed old labels (Anthropometry, Foods) and the browser fetched patients.js?v=0.1.2 (404). Cause: hydratePluginUiRegistry runs once on login and is never refreshed; install/update/uninstall handlers only re-fetch the plugin list, not the UI registry. Added explicit hydratePluginUiRegistry(true) calls to: - PluginInstallModal onInstalled (Plugins.tsx) - UpdatePreviewModal onApplied (PluginDetail.tsx) - PluginUninstall onUninstalled (PluginDetail.tsx) - simple uninstall path after api.delete completes (PluginDetail.tsx) (2) Native browser confirm popup — uninstall of plugins without safe_uninstall used window.confirm(), which is unbranded, ugly, and blocks the page. Replaced with an in-app dark-themed confirm modal that matches the rest of the app (same styling as the safe_uninstall wizard header). Both fixes are frontend-only; no backend or schema change. Direct push to develop per Davidson's earlier instruction (small UX polish, no PR overhead needed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8b8b88c commit afc26a2

2 files changed

Lines changed: 81 additions & 10 deletions

File tree

dashboard/frontend/src/pages/PluginDetail.tsx

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ import {
77
ToggleRight, ToggleLeft, Terminal, X,
88
} from 'lucide-react'
99
import { api } from '../lib/api'
10+
import { hydratePluginUiRegistry } from '../lib/plugin-ui-registry'
1011
import type { Plugin } from '../components/PluginCard'
1112
import UpdatePreviewModal from '../components/UpdatePreviewModal'
1213
import PluginUninstall, { type SafeUninstallSpec } from '../components/PluginUninstall'
1314

15+
// Re-fetch the UI registry after install / update / uninstall so the sidebar,
16+
// page bundles, and ?v=<version> cache-buster all match the freshly written
17+
// manifest_json in plugins_installed. Without this, the in-memory registry
18+
// keeps the stale manifest until full page reload — which is why a v0.1.2
19+
// install kept showing v0.1.2 sidebar entries after upgrading to v0.1.3.
20+
async function refreshPluginUiRegistry() {
21+
await hydratePluginUiRegistry(true)
22+
}
23+
1424
interface HealthResult {
1525
slug: string
1626
status: 'active' | 'broken' | 'not_installed'
@@ -150,6 +160,8 @@ export default function PluginDetail() {
150160

151161
// B3 — Safe uninstall wizard state
152162
const [showUninstallWizard, setShowUninstallWizard] = useState(false)
163+
// Simple confirm modal for plugins without safe_uninstall (replaces window.confirm).
164+
const [showSimpleConfirm, setShowSimpleConfirm] = useState(false)
153165

154166
// Wave 2.3 — MCP restart banner dismiss (persisted via localStorage)
155167
const mcpBannerKey = `mcp-restart-dismissed-${slug}`
@@ -204,15 +216,21 @@ export default function PluginDetail() {
204216
setShowUninstallWizard(true)
205217
return
206218
}
207-
// Legacy path: simple confirm dialog
208-
if (!window.confirm(t('plugins.confirmUninstall'))) return
219+
// Plugin without safe_uninstall — show simple in-app confirm modal.
220+
setShowSimpleConfirm(true)
221+
}
222+
223+
async function performSimpleUninstall() {
224+
setShowSimpleConfirm(false)
209225
setRemoving(true)
210-
api.delete(`/plugins/${slug}`)
211-
.then(() => navigate('/plugins'))
212-
.catch((e: unknown) => {
213-
setError(e instanceof Error ? e.message : t('common.unexpectedError'))
214-
setRemoving(false)
215-
})
226+
try {
227+
await api.delete(`/plugins/${slug}`)
228+
await refreshPluginUiRegistry()
229+
navigate('/plugins')
230+
} catch (e: unknown) {
231+
setError(e instanceof Error ? e.message : t('common.unexpectedError'))
232+
setRemoving(false)
233+
}
216234
}
217235

218236
async function handleToggle() {
@@ -449,9 +467,49 @@ export default function PluginDetail() {
449467
slug={slug}
450468
safeUninstall={_safeUninstallSpec}
451469
onClose={() => setShowUninstallWizard(false)}
452-
onUninstalled={() => navigate('/plugins')}
470+
onUninstalled={async () => {
471+
await refreshPluginUiRegistry()
472+
navigate('/plugins')
473+
}}
453474
/>
454475
)}
476+
477+
{/* Simple confirm modal — for plugins without safe_uninstall.
478+
Replaces the prior window.confirm() popup so the UX matches the rest
479+
of the app (dark theme, branded accents, in-page overlay). */}
480+
{showSimpleConfirm && (
481+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
482+
<div className="w-full max-w-md rounded-xl border border-neutral-800 bg-neutral-900 shadow-2xl">
483+
<div className="flex items-center gap-2 border-b border-neutral-800 px-6 py-4">
484+
<Trash2 className="h-5 w-5 text-red-400" />
485+
<span className="font-semibold text-white">Desinstalar plugin: {slug}</span>
486+
</div>
487+
<div className="px-6 py-5 space-y-4">
488+
<div className="flex items-start gap-3 rounded border border-red-800 bg-red-950/40 p-4">
489+
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-red-400" />
490+
<p className="text-sm text-red-200">
491+
Esta ação não pode ser desfeita. O plugin será removido completamente,
492+
incluindo seus dados (a menos que ele declare preservação explícita).
493+
</p>
494+
</div>
495+
<div className="flex justify-end gap-2">
496+
<button
497+
onClick={() => setShowSimpleConfirm(false)}
498+
className="rounded-lg border border-neutral-700 px-4 py-2 text-sm text-neutral-300 hover:bg-neutral-800"
499+
>
500+
Cancelar
501+
</button>
502+
<button
503+
onClick={() => { void performSimpleUninstall() }}
504+
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700"
505+
>
506+
Desinstalar
507+
</button>
508+
</div>
509+
</div>
510+
</div>
511+
</div>
512+
)}
455513
<div className="max-w-3xl mx-auto">
456514
{/* Back */}
457515
<button
@@ -692,6 +750,10 @@ export default function PluginDetail() {
692750
from: plugin.version,
693751
to: '…',
694752
}))
753+
// Re-fetch plugin list AND the UI registry — without the registry
754+
// refresh, the sidebar + page bundles keep pointing at the
755+
// pre-update manifest until full reload.
756+
await refreshPluginUiRegistry()
695757
const plugins = await api.get('/plugins') as Plugin[]
696758
const found = plugins.find((p) => p.slug === slug)
697759
if (found) {

dashboard/frontend/src/pages/Plugins.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CheckCircle, Star,
77
} from 'lucide-react'
88
import { api } from '../lib/api'
9+
import { hydratePluginUiRegistry } from '../lib/plugin-ui-registry'
910
import PluginCard, { type Plugin } from '../components/PluginCard'
1011
import PluginInstallModal from '../components/PluginInstallModal'
1112

@@ -313,7 +314,15 @@ export default function Plugins() {
313314
{showInstall && (
314315
<PluginInstallModal
315316
onClose={() => setShowInstall(false)}
316-
onInstalled={fetchPlugins}
317+
onInstalled={async () => {
318+
// Re-fetch plugin list AND the UI registry so the sidebar +
319+
// bundle URLs reflect the manifest just installed (without this,
320+
// the user has to hard-reload the browser to see the new pages).
321+
await Promise.all([
322+
hydratePluginUiRegistry(true),
323+
Promise.resolve(fetchPlugins()),
324+
])
325+
}}
317326
/>
318327
)}
319328
</div>

0 commit comments

Comments
 (0)