mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
Notify mounted plugin slots on export registration
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c064507bd3
commit
4be1f16aa2
2 files changed
+82
-2
No files matched your search
@@ -1,5 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { _collectRegisterableExportNamesForTests } from "./slots";
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { createElement } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
PluginSlotMount,
|
||||
_collectRegisterableExportNamesForTests,
|
||||
_resetPluginModuleLoader,
|
||||
registerPluginWebComponent,
|
||||
type ResolvedPluginSlot,
|
||||
} from "./slots";
|
||||
|
||||
let roots: Root[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots) {
|
||||
flushSync(() => {
|
||||
root.unmount();
|
||||
});
|
||||
}
|
||||
roots = [];
|
||||
_resetPluginModuleLoader();
|
||||
});
|
||||
|
||||
describe("plugin slot export registration", () => {
|
||||
it("keeps declared missing exports visible for diagnostics", () => {
|
||||
@@ -26,4 +49,39 @@ describe("plugin slot export registration", () => {
|
||||
|
||||
expect(exports).toEqual(new Set(["Page", "RouteSidebar", "webComponentTag"]));
|
||||
});
|
||||
|
||||
it("updates an already-mounted placeholder when the slot export registers later", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
roots.push(root);
|
||||
const slot: ResolvedPluginSlot = {
|
||||
type: "routeSidebar",
|
||||
id: "content-machine-sidebar",
|
||||
displayName: "Content",
|
||||
exportName: "ContentMachineRouteSidebar",
|
||||
routePath: "content-machine",
|
||||
pluginId: "content-machine-plugin",
|
||||
pluginKey: "content-machine",
|
||||
pluginDisplayName: "Content Machine",
|
||||
pluginVersion: "1.0.0",
|
||||
};
|
||||
|
||||
flushSync(() => {
|
||||
root.render(createElement(PluginSlotMount, {
|
||||
slot,
|
||||
context: { companyId: "company-1", companyPrefix: "PAP" },
|
||||
missingBehavior: "placeholder",
|
||||
}));
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Content Machine: Content");
|
||||
|
||||
flushSync(() => {
|
||||
registerPluginWebComponent("content-machine", "ContentMachineRouteSidebar", "paperclip-test-sidebar");
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain("Content Machine: Content");
|
||||
expect(container.querySelector("paperclip-test-sidebar")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -124,11 +124,30 @@ type UsePluginSlotsResult = {
|
||||
* Keys are `${pluginKey}:${exportName}` to match manifest slot declarations.
|
||||
*/
|
||||
const registry = new Map<string, RegisteredPluginComponent>();
|
||||
const registryListeners = new Set<() => void>();
|
||||
|
||||
function buildRegistryKey(pluginKey: string, exportName: string): string {
|
||||
return `${pluginKey}:${exportName}`;
|
||||
}
|
||||
|
||||
function notifyRegistryListeners(): void {
|
||||
for (const listener of registryListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function usePluginRegistrySubscription(): void {
|
||||
const [, forceRerender] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => forceRerender((tick) => tick + 1);
|
||||
registryListeners.add(listener);
|
||||
return () => {
|
||||
registryListeners.delete(listener);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
function requiresEntityType(slotType: PluginUiSlotType): boolean {
|
||||
return slotType === "detailTab" || slotType === "taskDetailView" || slotType === "contextMenuItem" || slotType === "commentAnnotation" || slotType === "commentContextMenuItem" || slotType === "projectSidebarItem" || slotType === "toolbarButton";
|
||||
}
|
||||
@@ -150,6 +169,7 @@ export function registerPluginReactComponent(
|
||||
kind: "react",
|
||||
component,
|
||||
});
|
||||
notifyRegistryListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +184,7 @@ export function registerPluginWebComponent(
|
||||
kind: "web-component",
|
||||
tagName,
|
||||
});
|
||||
notifyRegistryListeners();
|
||||
}
|
||||
|
||||
function resolveRegisteredComponent(slot: ResolvedPluginSlot): RegisteredPluginComponent | null {
|
||||
@@ -802,6 +823,7 @@ export function PluginSlotMount({
|
||||
className,
|
||||
missingBehavior = "hidden",
|
||||
}: PluginSlotMountProps) {
|
||||
usePluginRegistrySubscription();
|
||||
const [, forceRerender] = useState(0);
|
||||
const component = resolveRegisteredComponent(slot);
|
||||
|
||||
|
||||
Reference in new issue
Block a user