Clarify plugin authoring and external dev workflow
This commit is contained in:
@@ -8,6 +8,7 @@ import { queryKeys } from "@/lib/queryKeys";
|
||||
import { PluginSlotMount } from "@/plugins/slots";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { NotFoundPage } from "./NotFound";
|
||||
|
||||
/**
|
||||
* Company-context plugin page. Renders a plugin's `page` slot at
|
||||
@@ -25,12 +26,18 @@ export function PluginPage() {
|
||||
}>();
|
||||
const { companies, selectedCompanyId } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const routeCompany = useMemo(() => {
|
||||
if (!routeCompanyPrefix) return null;
|
||||
const requested = routeCompanyPrefix.toUpperCase();
|
||||
return companies.find((c) => c.issuePrefix.toUpperCase() === requested) ?? null;
|
||||
}, [companies, routeCompanyPrefix]);
|
||||
const hasInvalidCompanyPrefix = Boolean(routeCompanyPrefix) && !routeCompany;
|
||||
|
||||
const resolvedCompanyId = useMemo(() => {
|
||||
if (!routeCompanyPrefix) return selectedCompanyId ?? null;
|
||||
const requested = routeCompanyPrefix.toUpperCase();
|
||||
return companies.find((c) => c.issuePrefix.toUpperCase() === requested)?.id ?? selectedCompanyId ?? null;
|
||||
}, [companies, routeCompanyPrefix, selectedCompanyId]);
|
||||
if (routeCompany) return routeCompany.id;
|
||||
if (routeCompanyPrefix) return null;
|
||||
return selectedCompanyId ?? null;
|
||||
}, [routeCompany, routeCompanyPrefix, selectedCompanyId]);
|
||||
|
||||
const companyPrefix = useMemo(
|
||||
() => (resolvedCompanyId ? companies.find((c) => c.id === resolvedCompanyId)?.issuePrefix ?? null : null),
|
||||
@@ -92,6 +99,9 @@ export function PluginPage() {
|
||||
}, [pageSlot, companyPrefix, setBreadcrumbs]);
|
||||
|
||||
if (!resolvedCompanyId) {
|
||||
if (hasInvalidCompanyPrefix) {
|
||||
return <NotFoundPage scope="invalid_company_prefix" requestedPrefix={routeCompanyPrefix} />;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">Select a company to view this page.</p>
|
||||
@@ -117,6 +127,9 @@ export function PluginPage() {
|
||||
}
|
||||
|
||||
if (!pageSlot) {
|
||||
if (pluginRoutePath) {
|
||||
return <NotFoundPage scope="board" />;
|
||||
}
|
||||
// No page slot: redirect to plugin settings where plugin info is always shown
|
||||
const settingsPath = pluginId ? `/instance/settings/plugins/${pluginId}` : "/instance/settings/plugins";
|
||||
return <Navigate to={settingsPath} replace />;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* @see PLUGIN_SPEC.md §19.0.2 — Bundle Isolation
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
usePluginData,
|
||||
usePluginAction,
|
||||
@@ -60,61 +59,11 @@ export function initPluginBridge(
|
||||
react,
|
||||
reactDom,
|
||||
sdkUi: {
|
||||
// Bridge hooks
|
||||
usePluginData,
|
||||
usePluginAction,
|
||||
useHostContext,
|
||||
usePluginStream,
|
||||
usePluginToast,
|
||||
|
||||
// Placeholder shared UI components — plugins that use these will get
|
||||
// functional stubs. Full implementations matching the host's design
|
||||
// system can be added later.
|
||||
MetricCard: createStubComponent("MetricCard"),
|
||||
StatusBadge: createStubComponent("StatusBadge"),
|
||||
DataTable: createStubComponent("DataTable"),
|
||||
TimeseriesChart: createStubComponent("TimeseriesChart"),
|
||||
MarkdownBlock: createStubComponent("MarkdownBlock"),
|
||||
KeyValueList: createStubComponent("KeyValueList"),
|
||||
ActionBar: createStubComponent("ActionBar"),
|
||||
LogView: createStubComponent("LogView"),
|
||||
JsonTree: createStubComponent("JsonTree"),
|
||||
Spinner: createStubComponent("Spinner"),
|
||||
ErrorBoundary: createPassthroughComponent("ErrorBoundary"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub component helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createStubComponent(name: string): unknown {
|
||||
const fn = (props: Record<string, unknown>) => {
|
||||
// Import React from the registry to avoid import issues
|
||||
const React = globalThis.__paperclipPluginBridge__?.react as typeof import("react") | undefined;
|
||||
if (!React) return null;
|
||||
return React.createElement("div", {
|
||||
"data-plugin-component": name,
|
||||
style: {
|
||||
padding: "8px",
|
||||
border: "1px dashed #666",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
color: "#888",
|
||||
},
|
||||
}, `[${name}]`);
|
||||
};
|
||||
Object.defineProperty(fn, "name", { value: name });
|
||||
return fn;
|
||||
}
|
||||
|
||||
function createPassthroughComponent(name: string): unknown {
|
||||
const fn = (props: { children?: ReactNode }) => {
|
||||
const ReactLib = globalThis.__paperclipPluginBridge__?.react as typeof import("react") | undefined;
|
||||
if (!ReactLib) return null;
|
||||
return ReactLib.createElement(ReactLib.Fragment, null, props.children);
|
||||
};
|
||||
Object.defineProperty(fn, "name", { value: name });
|
||||
return fn;
|
||||
}
|
||||
|
||||
@@ -257,14 +257,8 @@ function getShimBlobUrl(specifier: "react" | "react-dom" | "react-dom/client" |
|
||||
case "sdk-ui":
|
||||
source = `
|
||||
const SDK = globalThis.__paperclipPluginBridge__?.sdkUi ?? {};
|
||||
const { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast,
|
||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||
Spinner, ErrorBoundary } = SDK;
|
||||
export { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast,
|
||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||
Spinner, ErrorBoundary };
|
||||
const { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast } = SDK;
|
||||
export { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast };
|
||||
`;
|
||||
break;
|
||||
}
|
||||
@@ -294,8 +288,6 @@ function rewriteBareSpecifiers(source: string): string {
|
||||
"'@paperclipai/plugin-sdk/ui'": `'${getShimBlobUrl("sdk-ui")}'`,
|
||||
'"@paperclipai/plugin-sdk/ui/hooks"': `"${getShimBlobUrl("sdk-ui")}"`,
|
||||
"'@paperclipai/plugin-sdk/ui/hooks'": `'${getShimBlobUrl("sdk-ui")}'`,
|
||||
'"@paperclipai/plugin-sdk/ui/components"': `"${getShimBlobUrl("sdk-ui")}"`,
|
||||
"'@paperclipai/plugin-sdk/ui/components'": `'${getShimBlobUrl("sdk-ui")}'`,
|
||||
'"react/jsx-runtime"': `"${getShimBlobUrl("react/jsx-runtime")}"`,
|
||||
"'react/jsx-runtime'": `'${getShimBlobUrl("react/jsx-runtime")}'`,
|
||||
'"react-dom/client"': `"${getShimBlobUrl("react-dom/client")}"`,
|
||||
|
||||
Reference in New Issue
Block a user