Notify mounted plugin slots on export registration

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-22 15:52:44 +00:00
1 parent c064507bd3
commit 4be1f16aa2
2 files changed
+82 -2

No files matched your search

+60 -2
View File
@@ -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();
});
});
+22
View File
@@ -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);