From 9a70089d6b09067f69db9c72b838e7bd33c021e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 28 Aug 2026 23:56:27 +0100 Subject: [PATCH] a Marketplace section, and the git field lives where its result appears MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a new row above the tags, and where the store now opens. the tags say what a plugin is FOR; this says what state the machine is in, which is the question somebody opening a store asks first — narrowing by kind is what you do after. THREE TABS, because there are genuinely three states and the old grid had two. `Installed` is what runs. the other two are both "not installed" and are not the same thing: Official the catalogue — somewhere else, a download away. Available already ON THIS MACHINE, not installed. where a plugin fetched from a git URL lands, and where one deleted-but-not-purged sits. collapsing those hid the entire point of fetching from a repository: you add something and it is indistinguishable from twenty things you have not downloaded. it opens on Installed rather than the catalogue. a store that opens on what it can sell you is selling; this is mostly where somebody checks on something they already have. THE GIT FIELD MOVED INTO THE AVAILABLE TAB, which is the tab its result appears in — and only shows there. floating above a filtered grid it was an action whose outcome landed somewhere off screen, which is how the first version installed a plugin with no feedback at all. same mistake, one layer up. the tag grid is untouched and still what a filter shows. two views because they answer different questions: "what have I got" wants installed-versus-not, "what is there for my calendar" wants everything of that kind whatever its state. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- .../src/apps/Plugins/MarketplaceOverview.tsx | 110 ++++++++++++++++++ .../src/apps/Plugins/PluginGrid.tsx | 40 +++++-- .../officerdev/src/apps/Plugins/TagNav.tsx | 35 +++++- 3 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx diff --git a/src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx b/src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx new file mode 100644 index 00000000..0bce3384 --- /dev/null +++ b/src/workspaces/officerdev/src/apps/Plugins/MarketplaceOverview.tsx @@ -0,0 +1,110 @@ +import { useState, type ReactNode } from 'react'; +import { Check, Download, GitBranch, Store } from 'lucide-react'; +import { InstallFromUrl } from './InstallFromUrl'; +import type { AvailablePlugin, PluginItem } from './usePlugins'; + +/** + * The Marketplace section: what this machine has, and what it could have. + * + * ── Three tabs, because there are genuinely three states ── + * + * `Installed` is what is running. The other two are both "not installed" and they are NOT the same + * thing, which is the distinction the old single grid lost: + * + * `Official` the marketplace catalogue — somewhere else, one click and a download away. + * `Available` already ON THIS MACHINE and not installed. A plugin fetched from a git URL lands + * here, and so does one that was deleted without being purged. + * + * Collapsing those two hid the whole point of fetching from a repository: you add something, and it is + * indistinguishable from twenty things you have not downloaded yet. + * + * ── Why the git field lives in Available ── + * + * That is the tab its result appears in. A button that adds something to the list you are looking at is + * legible; the same button floating above a filtered grid was not, which is how the first version + * managed to install a plugin with no feedback at all. + */ + +type Entry = (PluginItem | AvailablePlugin) & { installed?: boolean }; +type TabKey = 'installed' | 'official' | 'available'; + +export const MarketplaceOverview = ({ + installed, + official, + available, + renderCard, + onFetch, +}: { + installed: Entry[]; + official: Entry[]; + available: Entry[]; + renderCard: (entry: Entry) => ReactNode; + onFetch: (url: string) => Promise<{ ok: boolean; appName?: string; 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. + const [tab, setTab] = useState('installed'); + + const tabs: { key: TabKey; label: string; icon: typeof Check; items: Entry[] }[] = [ + { key: 'installed', label: 'Installed', icon: Check, items: installed }, + { key: 'official', label: 'Official', icon: Store, items: official }, + { key: 'available', label: 'Available', icon: Download, items: available }, + ]; + const current = tabs.find((t) => t.key === tab) ?? tabs[0]!; + + return ( +
+ {/* One ruled row, same border as the nav beside it, so the two panels read as one screen. */} +
+ {tabs.map(({ key, label, icon: Icon, items }) => ( + + ))} + + {/* Only on the tab it affects. Elsewhere it would be an action whose result appears somewhere + you cannot see, which is the mistake this whole section is correcting. */} + {tab === 'available' && ( +
+ +
+ )} +
+ +
+ {current.items.length === 0 ? ( + + ) : ( +
+ {current.items.map((entry) => renderCard(entry))} +
+ )} +
+
+ ); +}; + +/** Says what the tab is FOR, not just that it is empty — an empty tab is otherwise a dead end. */ +const Empty = ({ tab }: { tab: TabKey }) => { + const text = + tab === 'installed' + ? 'Nothing installed yet. Official has the catalogue.' + : tab === 'official' + ? 'The marketplace catalogue is empty or unreachable.' + : 'Nothing waiting to be installed. Add one from a Git repository above.'; + + return ( +
+ +

{text}

+
+ ); +}; diff --git a/src/workspaces/officerdev/src/apps/Plugins/PluginGrid.tsx b/src/workspaces/officerdev/src/apps/Plugins/PluginGrid.tsx index 9d43f979..914a223c 100644 --- a/src/workspaces/officerdev/src/apps/Plugins/PluginGrid.tsx +++ b/src/workspaces/officerdev/src/apps/Plugins/PluginGrid.tsx @@ -2,7 +2,7 @@ import { Link, useSearchParams } from 'react-router'; import { Check, Download } from 'lucide-react'; import { usePlugins, type AvailablePlugin, type PluginItem } from './usePlugins'; import { resolveIcon } from '../../utils/resolve-icon'; -import { InstallFromUrl } from './InstallFromUrl'; +import { MarketplaceOverview } from './MarketplaceOverview'; /** * The store: cards in a grid, filtered by the tag on the left. @@ -85,7 +85,28 @@ export const PluginGrid = () => { const [params] = useSearchParams(); const tag = params.get('tag'); - if (isLoading) return
Loading…
; + if (isLoading) return
Loading…
; + + /* + No tag means the Marketplace section — the state of this machine, in three tabs. + + The tag grid below is unchanged and still what a filter shows. Two views rather than one because + they answer different questions: "what have I got" wants installed-versus-not, and "what is there + for my calendar" wants everything of that kind together regardless of state. + */ + if (!tag) { + const installed = plugins.filter((p) => p.installed); + const onDisk = plugins.filter((p) => !p.installed); + return ( + } + /> + ); + } // Installed first, then the rest. Both are the same thing to a person browsing — a plugin — and the // ones they already have are the ones they are most likely looking for. @@ -107,16 +128,11 @@ export const PluginGrid = () => { return (
- {/* The count and the escape hatch share a row: the catalogue is the way in, and this is the - exception beside it rather than an equal option above it. */} -
- - {shown.length} plugin{shown.length === 1 ? '' : 's'} - {tag && tag !== '__untagged' ? ` in ${tag}` : ''} - -
- -
+ {/* No git field here any more: it lives in the Marketplace section's Available tab, which is where + what it adds actually shows up. */} +
+ {shown.length} plugin{shown.length === 1 ? '' : 's'} + {tag && tag !== '__untagged' ? ` in ${tag}` : ''}
{shown.map((entry) => ( diff --git a/src/workspaces/officerdev/src/apps/Plugins/TagNav.tsx b/src/workspaces/officerdev/src/apps/Plugins/TagNav.tsx index ef8d1bbd..318e25f7 100644 --- a/src/workspaces/officerdev/src/apps/Plugins/TagNav.tsx +++ b/src/workspaces/officerdev/src/apps/Plugins/TagNav.tsx @@ -1,5 +1,5 @@ import { Link, useSearchParams } from 'react-router'; -import { Puzzle, AlertTriangle, Database, Package } from 'lucide-react'; +import { Puzzle, AlertTriangle, Database, Package, Store } from 'lucide-react'; import { usePlugins, type OrphanTable } from './usePlugins'; import { resolveIcon } from '../../utils/resolve-icon'; @@ -72,6 +72,25 @@ export const TagNav = () => { ); const untagged = all.filter((p) => !p.tags?.length).length; + // On disk and not installed — a plugin fetched from a git URL lands here, which is why it is counted + // beside the catalogue rather than folded into it. + const notInstalled = plugins.filter((p) => !p.installed).length; + + /** + * The Marketplace section — a view, not a filter. + * + * `?tag=` narrows the same grid; this replaces it, so it clears the tag rather than setting one. It is + * also the DEFAULT: arriving with neither parameter lands here, because "what do I have, and what can + * I get" is the question somebody opening a store is asking, and the tag list is how you narrow it + * afterwards. + */ + const overviewLink = () => { + const next = new URLSearchParams(params); + next.delete('tag'); + next.delete('selected'); + return { search: next.toString() ? `?${next}` : '' }; + }; + const onOverview = !active; const link = (tag: string | null) => { const next = new URLSearchParams(params); @@ -108,7 +127,19 @@ export const TagNav = () => { return (