Simplify plugin runtime and cleanup lifecycle

This commit is contained in:
Dotta
2026-03-13 16:58:29 -05:00
parent 80cdbdbd47
commit 12ccfc2c9a
21 changed files with 120 additions and 838 deletions

View File

@@ -17,7 +17,6 @@ import type {
PluginRecord,
PluginConfig,
PluginStatus,
CompanyPluginAvailability,
} from "@paperclipai/shared";
import { api } from "./client";
@@ -280,9 +279,6 @@ export const pluginsApi = {
* Returns normalized UI contribution declarations for ready plugins.
* Used by the slot host runtime and launcher discovery surfaces.
*
* When `companyId` is provided, the server filters out plugins that are
* disabled for that company before returning contributions.
*
* Response shape:
* - `slots`: concrete React mount declarations from `manifest.ui.slots`
* - `launchers`: host-owned entry points from `manifest.ui.launchers` plus
@@ -290,54 +286,14 @@ export const pluginsApi = {
*
* @example
* ```ts
* const rows = await pluginsApi.listUiContributions(companyId);
* const rows = await pluginsApi.listUiContributions();
* const toolbarLaunchers = rows.flatMap((row) =>
* row.launchers.filter((launcher) => launcher.placementZone === "toolbarButton"),
* );
* ```
*/
listUiContributions: (companyId?: string) =>
api.get<PluginUiContribution[]>(
`/plugins/ui-contributions${companyId ? `?companyId=${encodeURIComponent(companyId)}` : ""}`,
),
/**
* List plugin availability/settings for a specific company.
*
* @param companyId - UUID of the company.
* @param available - Optional availability filter.
*/
listForCompany: (companyId: string, available?: boolean) =>
api.get<CompanyPluginAvailability[]>(
`/companies/${companyId}/plugins${available === undefined ? "" : `?available=${available}`}`,
),
/**
* Fetch a single company-scoped plugin availability/settings record.
*
* @param companyId - UUID of the company.
* @param pluginId - Plugin UUID or plugin key.
*/
getForCompany: (companyId: string, pluginId: string) =>
api.get<CompanyPluginAvailability>(`/companies/${companyId}/plugins/${pluginId}`),
/**
* Create, update, or clear company-scoped plugin settings.
*
* Company availability is enabled by default. This endpoint stores explicit
* overrides in `plugin_company_settings` so the selected company can be
* disabled without affecting the global plugin installation.
*/
saveForCompany: (
companyId: string,
pluginId: string,
params: {
available: boolean;
settingsJson?: Record<string, unknown>;
lastError?: string | null;
},
) =>
api.put<CompanyPluginAvailability>(`/companies/${companyId}/plugins/${pluginId}`, params),
listUiContributions: () =>
api.get<PluginUiContribution[]>("/plugins/ui-contributions"),
// ===========================================================================
// Plugin configuration endpoints
@@ -398,8 +354,7 @@ export const pluginsApi = {
* @param pluginId - UUID of the plugin whose worker should handle the request
* @param key - Plugin-defined data key (e.g. `"sync-health"`)
* @param params - Optional query parameters forwarded to the worker handler
* @param companyId - Optional company scope. When present, the server rejects
* the call with HTTP 403 if the plugin is disabled for that company.
* @param companyId - Optional company scope used for board/company access checks.
* @param renderEnvironment - Optional launcher/page snapshot forwarded for
* launcher-backed UI so workers can distinguish modal, drawer, popover, and
* page execution.
@@ -439,8 +394,7 @@ export const pluginsApi = {
* @param pluginId - UUID of the plugin whose worker should handle the request
* @param key - Plugin-defined action key (e.g. `"resync"`)
* @param params - Optional parameters forwarded to the worker handler
* @param companyId - Optional company scope. When present, the server rejects
* the call with HTTP 403 if the plugin is disabled for that company.
* @param companyId - Optional company scope used for board/company access checks.
* @param renderEnvironment - Optional launcher/page snapshot forwarded for
* launcher-backed UI so workers can distinguish modal, drawer, popover, and
* page execution.

View File

@@ -80,15 +80,9 @@ export const queryKeys = {
examples: ["plugins", "examples"] as const,
detail: (pluginId: string) => ["plugins", pluginId] as const,
health: (pluginId: string) => ["plugins", pluginId, "health"] as const,
uiContributions: (companyId?: string | null) =>
["plugins", "ui-contributions", companyId ?? "global"] as const,
uiContributions: ["plugins", "ui-contributions"] as const,
config: (pluginId: string) => ["plugins", pluginId, "config"] as const,
dashboard: (pluginId: string) => ["plugins", pluginId, "dashboard"] as const,
logs: (pluginId: string) => ["plugins", pluginId, "logs"] as const,
company: (companyId: string) => ["plugins", "company", companyId] as const,
companyList: (companyId: string, available?: boolean) =>
["plugins", "company", companyId, "list", available ?? "all"] as const,
companyDetail: (companyId: string, pluginId: string) =>
["plugins", "company", companyId, pluginId] as const,
},
};

View File

@@ -61,7 +61,7 @@ function getPluginErrorSummary(plugin: PluginRecord): string {
* @see doc/plugins/PLUGIN_SPEC.md §3 — Plugin Lifecycle for status semantics.
*/
export function PluginManager() {
const { selectedCompany, selectedCompanyId } = useCompany();
const { selectedCompany } = useCompany();
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const { pushToast } = useToast();
@@ -93,10 +93,7 @@ export function PluginManager() {
const invalidatePluginQueries = () => {
queryClient.invalidateQueries({ queryKey: queryKeys.plugins.all });
queryClient.invalidateQueries({ queryKey: queryKeys.plugins.examples });
queryClient.invalidateQueries({ queryKey: ["plugins", "ui-contributions"] });
if (selectedCompanyId) {
queryClient.invalidateQueries({ queryKey: queryKeys.plugins.companyList(selectedCompanyId) });
}
queryClient.invalidateQueries({ queryKey: queryKeys.plugins.uiContributions });
};
const installMutation = useMutation({

View File

@@ -37,8 +37,8 @@ export function PluginPage() {
);
const { data: contributions } = useQuery({
queryKey: queryKeys.plugins.uiContributions(resolvedCompanyId ?? undefined),
queryFn: () => pluginsApi.listUiContributions(resolvedCompanyId ?? undefined),
queryKey: queryKeys.plugins.uiContributions,
queryFn: () => pluginsApi.listUiContributions(),
enabled: !!resolvedCompanyId && !!pluginId,
});

View File

@@ -261,8 +261,8 @@ export function usePluginLaunchers(
): UsePluginLaunchersResult {
const queryEnabled = filters.enabled ?? true;
const { data, isLoading, error } = useQuery({
queryKey: queryKeys.plugins.uiContributions(filters.companyId),
queryFn: () => pluginsApi.listUiContributions(filters.companyId ?? undefined),
queryKey: queryKeys.plugins.uiContributions,
queryFn: () => pluginsApi.listUiContributions(),
enabled: queryEnabled,
});

View File

@@ -552,8 +552,8 @@ function usePluginModuleLoader(contributions: PluginUiContribution[] | undefined
export function usePluginSlots(filters: SlotFilters): UsePluginSlotsResult {
const queryEnabled = filters.enabled ?? true;
const { data, isLoading: isQueryLoading, error } = useQuery({
queryKey: queryKeys.plugins.uiContributions(filters.companyId),
queryFn: () => pluginsApi.listUiContributions(filters.companyId ?? undefined),
queryKey: queryKeys.plugins.uiContributions,
queryFn: () => pluginsApi.listUiContributions(),
enabled: queryEnabled,
});