mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
fix(plugins): honor route onboarding and complete overlay context
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3f1fbf39c8
commit
34e13e5439
2 files changed
+30
-7
No files matched your search
@@ -5,13 +5,15 @@ import { flushSync } from "react-dom";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { PluginAppShellOverlays } from "./PluginAppShellOverlays";
|
||||
|
||||
const state = vi.hoisted(() => ({ userId: "alice" as string | null, settled: true, company: "first", onboarding: false, failed: false, mounts: 0, disposals: 0 }));
|
||||
const state = vi.hoisted(() => ({ userId: "alice" as string | null, settled: true, company: "first", onboarding: false, dismissed: false, pathname: "/ACME/issues", context: {} as Record<string, unknown>, failed: false, mounts: 0, disposals: 0 }));
|
||||
vi.mock("@/api/companies-query", () => ({ useAccountIdentity: () => ({ userId: state.userId, settled: state.settled }) }));
|
||||
vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompanyId: state.company, selectedCompany: { issuePrefix: "ACME" }, loading: false }) }));
|
||||
vi.mock("@/context/DialogContext", () => ({ useDialogState: () => ({ onboardingOpen: state.onboarding }) }));
|
||||
vi.mock("@/context/DialogContext", () => ({ useDialogState: () => ({ onboardingOpen: state.onboarding, onboardingRouteDismissed: state.dismissed }) }));
|
||||
vi.mock("@/lib/router", () => ({ useLocation: () => ({ pathname: state.pathname }) }));
|
||||
vi.mock("@/plugins/slots", () => ({
|
||||
usePluginSlots: () => ({ errorMessage: state.failed ? "unavailable" : null, slots: [{ pluginId: "fixture", pluginVersion: "1.0.0", id: "overlay" }] }),
|
||||
PluginSlotMount: () => {
|
||||
PluginSlotMount: ({ context }: { context: Record<string, unknown> }) => {
|
||||
state.context = context;
|
||||
const [draft, setDraft] = useState("");
|
||||
useEffect(() => { state.mounts++; return () => { state.disposals++; }; }, []);
|
||||
return <button onClick={() => setDraft("private draft")}>{draft || "empty"}</button>;
|
||||
@@ -26,9 +28,19 @@ function render(localTrusted = false) {
|
||||
afterEach(() => {
|
||||
if (root) flushSync(() => root!.unmount());
|
||||
root = undefined; container?.remove();
|
||||
Object.assign(state, { userId: "alice", settled: true, company: "first", onboarding: false, failed: false, mounts: 0, disposals: 0 });
|
||||
Object.assign(state, { userId: "alice", settled: true, company: "first", onboarding: false, dismissed: false, pathname: "/ACME/issues", context: {}, failed: false, mounts: 0, disposals: 0 });
|
||||
});
|
||||
describe("persistent app-shell plugin lifecycle", () => {
|
||||
it("passes the complete host context promised by PluginWidgetProps", () => {
|
||||
render();
|
||||
expect(state.context).toEqual({ companyId: "first", companyPrefix: "ACME", projectId: null, entityId: null, entityType: null, parentEntityId: null, userId: "alice" });
|
||||
});
|
||||
it("disposes drafts on route-driven onboarding and remounts after dismissal", () => {
|
||||
render(); flushSync(() => container.querySelector("button")!.click());
|
||||
state.pathname = "/ACME/onboarding"; render();
|
||||
expect(state.onboarding).toBe(false); expect(container.textContent).toBe(""); expect(state.disposals).toBe(1);
|
||||
state.dismissed = true; render(); expect(container.textContent).toBe("empty");
|
||||
});
|
||||
it("keeps a draft during shell rerenders and clears it on account/company transitions", () => {
|
||||
render(); flushSync(() => container.querySelector("button")!.click());
|
||||
render(); expect(container.textContent).toBe("private draft"); expect(state.mounts).toBe(1);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useAccountIdentity } from "@/api/companies-query";
|
||||
import { useCompany } from "@/context/CompanyContext";
|
||||
import { useDialogState } from "@/context/DialogContext";
|
||||
import { useLocation } from "@/lib/router";
|
||||
import { isOnboardingPath } from "@/lib/onboarding-route";
|
||||
import type { PluginHostContext } from "@/plugins/bridge";
|
||||
import { PluginSlotMount, usePluginSlots, type PluginSlotContext } from "@/plugins/slots";
|
||||
|
||||
function AppShellEntries({ context }: { context: PluginSlotContext }) {
|
||||
@@ -33,16 +36,24 @@ function AppShellEntries({ context }: { context: PluginSlotContext }) {
|
||||
export function PluginAppShellOverlays({ localTrusted = false }: { localTrusted?: boolean }) {
|
||||
const { userId, settled } = useAccountIdentity();
|
||||
const { selectedCompanyId, selectedCompany, loading } = useCompany();
|
||||
const { onboardingOpen } = useDialogState();
|
||||
const { onboardingOpen, onboardingRouteDismissed } = useDialogState();
|
||||
const { pathname } = useLocation();
|
||||
// Local-trusted instances intentionally have no login requirement. Still
|
||||
// prefer any real account and wait for identity resolution so account changes
|
||||
// cannot reuse the prior account's in-memory plugin state.
|
||||
const identity = settled ? userId ?? (localTrusted ? "local-board" : null) : null;
|
||||
if (!identity || loading || onboardingOpen) return null;
|
||||
const onboardingVisible = onboardingOpen || (!onboardingRouteDismissed && isOnboardingPath(pathname));
|
||||
if (!identity || loading || onboardingVisible) return null;
|
||||
const context: PluginSlotContext & PluginHostContext = {
|
||||
companyId: selectedCompanyId,
|
||||
companyPrefix: selectedCompany?.issuePrefix ?? null,
|
||||
projectId: null, entityId: null, entityType: null, parentEntityId: null,
|
||||
userId,
|
||||
};
|
||||
return (
|
||||
<AppShellEntries
|
||||
key={JSON.stringify([identity, selectedCompanyId])}
|
||||
context={{ companyId: selectedCompanyId, companyPrefix: selectedCompany?.issuePrefix ?? null }}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user