import { useState } from "react"; import { useAppState } from "../../store/appState"; import { projectItemState, type ProjectItemState } from "../../lib/marketplace"; import type { MarketplaceApi } from "../../hooks/useMarketplace"; import type { CatalogItem, InstallScope, MarketplaceItemRef } from "../../lib/types"; import Toggle from "../ui/Toggle"; import HookConfirmModal from "./HookConfirmModal"; import PluginConfirmModal from "./PluginConfirmModal"; const STATE_LABEL: Record = { none: "", inherited: "Inherited", opted_out: "Opted out", project: "This project", project_pinned_differently: "Pinned to a different commit", }; interface Props { mp: MarketplaceApi; item: CatalogItem; marketplaceId: string; /** * The marketplace head `item` was read at. Installs pin exactly this commit; * the backend refuses if the marketplace has moved on since (final review I2). */ headCommit: string | null; } /** A hook or plugin install waiting for confirmation, frozen at the moment it was asked for. */ interface PendingConfirm { scope: InstallScope; item: CatalogItem; commit: string; } export default function InstallControls({ mp, item, marketplaceId, headCommit }: Props) { const appSettings = useAppState((s) => s.appSettings); const projects = useAppState((s) => s.projects); const filterId = useAppState((s) => s.marketplaceFilterProjectId); const [pending, setPending] = useState(null); const [busy, setBusy] = useState(false); const ref: MarketplaceItemRef = { marketplace_id: marketplaceId, kind: item.kind, key: item.key }; const globalInstalls = appSettings?.global_marketplace_installs ?? []; const isGlobal = globalInstalls.some( (g) => g.marketplace_id === marketplaceId && g.kind === item.kind && g.key === item.key, ); const disabled = item.invalid !== null || busy; // "" never matches a head, so the backend explains that a refresh is needed. const commit = headCommit ?? ""; const shown = filterId ? projects.filter((p) => p.id === filterId) : projects; const run = async (fn: () => Promise) => { setBusy(true); try { await fn(); } finally { setBusy(false); } }; /** Every install goes through here so a hook or plugin is always confirmed first. */ const install = (scope: InstallScope) => { if (item.kind === "hook" || item.kind === "plugin") { setPending({ scope, item, commit }); return; } void run(() => mp.install(ref, scope, commit)); }; const toggleProject = (projectId: string, state: ProjectItemState) => { const scope: InstallScope = { type: "project", project_id: projectId }; switch (state) { case "none": install(scope); break; case "inherited": void run(() => mp.setDisabled(projectId, ref, true)); break; case "opted_out": void run(() => mp.setDisabled(projectId, ref, false)); break; case "project": case "project_pinned_differently": void run(() => mp.uninstall(ref, scope)); break; } }; return (
(v ? install({ type: "global" }) : void run(() => mp.uninstall(ref, { type: "global" })))} />
    {shown.map((p) => { const state = projectItemState(ref, globalInstalls, p); const checked = state === "inherited" || state === "project" || state === "project_pinned_differently"; return (
  • {STATE_LABEL[state] && ( {STATE_LABEL[state]} )}
  • ); })}
{projects.length === 0 && (

No projects yet — “All projects” also covers projects added later.

)} {pending && (() => { const confirm = () => { const { scope, commit: reviewed } = pending; setPending(null); void run(() => mp.install(ref, scope, reviewed)); }; const Confirm = pending.item.kind === "plugin" ? PluginConfirmModal : HookConfirmModal; return ( setPending(null)} onConfirm={confirm} /> ); })()}
); }