diff --git a/src/servers/api/plugins/router.ts b/src/servers/api/plugins/router.ts index c53c2845..346ec85f 100644 --- a/src/servers/api/plugins/router.ts +++ b/src/servers/api/plugins/router.ts @@ -8,6 +8,7 @@ import { fetchCatalogue, fetchReadme } from '../../plugins/marketplace'; import { findOrphanTables } from '../../plugins/orphans'; import { fetchPlugin } from '../../plugins/fetch'; import { discoverPlugins } from '../../plugins/discover'; +import { listPluginInstalls } from 'officerdb'; import { iconUrl } from '../../app-store/assets'; import { installPlugin, @@ -309,7 +310,18 @@ pluginsRouter.post('/fetch-from-url', async (ctx) => { ); } - return ctx.json({ ok: true, appName, label: found.manifest?.label ?? appName }); + /* + Whether it is ALREADY installed, so the UI can say the true thing and show the right tab. + + A directory and an install row are separate facts — deleting a plugin's source leaves the row, so a + re-fetch can land on something the platform still considers installed. Reported here because the + caller has just been told "added" and needs to know where to look; "click it to install" pointing at + an empty tab is worse than no message. + */ + const installs = await listPluginInstalls(); + const installed = installs.some((entry) => entry.appName === appName && entry.enabled); + + return ctx.json({ ok: true, appName, label: found.manifest?.label ?? appName, installed }); }); pluginsRouter.post('/:appName/install', async (ctx) => { diff --git a/src/workspaces/officerdev/src/apps/Plugins/InstallFromUrl.tsx b/src/workspaces/officerdev/src/apps/Plugins/InstallFromUrl.tsx index 8be0bf78..3c865826 100644 --- a/src/workspaces/officerdev/src/apps/Plugins/InstallFromUrl.tsx +++ b/src/workspaces/officerdev/src/apps/Plugins/InstallFromUrl.tsx @@ -27,7 +27,7 @@ import { GitBranch, X, Loader2 } from 'lucide-react'; export const InstallFromUrl = ({ onFetch, }: { - onFetch: (url: string) => Promise<{ ok: boolean; appName?: string; error?: string }>; + onFetch: (url: string) => Promise<{ ok: boolean; appName?: string; installed?: boolean; error?: string }>; }) => { const [open, setOpen] = useState(false); const [url, setUrl] = useState(''); @@ -62,9 +62,18 @@ export const InstallFromUrl = ({ const result = await onFetch(trimmed); setBusy(false); if (result.ok) { - // Named, because the name was derived rather than typed — this is where you find out what to look - // for in the grid. - setNote({ ok: true, text: `${result.appName} added — click it to install` }); + /* + Named, because the name was derived rather than typed — this is where you find out what to look + for. And it says which of the two things happened: a plugin whose source was deleted but whose + install row survived comes back ALREADY installed, and telling somebody to "click it to install" + while it sits in a different tab is worse than saying nothing. + */ + setNote({ + ok: true, + text: result.installed + ? `${result.appName} added — already installed` + : `${result.appName} added — click it to install`, + }); setOpen(false); setUrl(''); } else { diff --git a/src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx b/src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx index 0bce3384..f911b66b 100644 --- a/src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx +++ b/src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx @@ -39,7 +39,7 @@ export const MarketplaceOverview = ({ official: Entry[]; available: Entry[]; renderCard: (entry: Entry) => ReactNode; - onFetch: (url: string) => Promise<{ ok: boolean; appName?: string; error?: string }>; + onFetch: (url: string) => Promise<{ ok: boolean; appName?: string; installed?: boolean; error?: string }>; }) => { // Opens on what you HAVE. A store that opens on the catalogue is selling; this is mostly a place // people come to check on something they already installed. @@ -74,7 +74,16 @@ export const MarketplaceOverview = ({ you cannot see, which is the mistake this whole section is correcting. */} {tab === 'available' && (