mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
Plugin screens can report unavailable while a successful catalog request is still pending. Track the host catalog state so these screens show progress until their contributions register. Keep missing surfaces, failed requests, and offline hosts distinct from loading. Retain existing installed surfaces during catalog refreshes. Cover pending-to-ready rendering with a real browser regression and catalog lifecycle tests. Related to getpaseo/paseo#7088
122 lines
4.7 KiB
TypeScript
122 lines
4.7 KiB
TypeScript
import type { PluginScreenLocation, PluginScreenParams } from "@getpaseo/plugin/client";
|
|
import {
|
|
parsePluginScreenParams,
|
|
parsePluginSurfaceRoute,
|
|
pluginScreenParamsFromRoute,
|
|
} from "./routes";
|
|
import type { InstalledPlugin } from "./types";
|
|
import type { PluginCatalogStatus } from "./registry";
|
|
import type { HostRuntimeConnectionStatus } from "@/runtime/host-runtime";
|
|
|
|
export type PluginSurfacePlaceholderStatus = "loading" | "offline" | "error" | "unavailable";
|
|
|
|
interface PluginSurfacePlaceholderInput {
|
|
connectionStatus: HostRuntimeConnectionStatus;
|
|
catalogStatus: PluginCatalogStatus;
|
|
}
|
|
|
|
export function resolvePluginSurfacePlaceholder({
|
|
connectionStatus,
|
|
catalogStatus,
|
|
}: PluginSurfacePlaceholderInput): PluginSurfacePlaceholderStatus {
|
|
if (connectionStatus === "offline" || connectionStatus === "error") return "offline";
|
|
if (connectionStatus !== "online") return "loading";
|
|
if (catalogStatus === "loading") return "loading";
|
|
if (catalogStatus === "error") return "error";
|
|
return "unavailable";
|
|
}
|
|
|
|
export type PluginSurfaceContributionIdentity =
|
|
| { kind: "sidebar"; id: string }
|
|
| { kind: "surface"; id: string };
|
|
|
|
/**
|
|
* A `sidebar` identity is a legacy `addSidebarItem` route: it resolves to that item and its screen,
|
|
* or, once the plugin has moved that item to `addScreen` under the same id, to that screen.
|
|
*/
|
|
export function resolvePluginSurfaceContribution(
|
|
plugin: InstalledPlugin | null,
|
|
identity: PluginSurfaceContributionIdentity | null,
|
|
): {
|
|
sidebarItem: InstalledPlugin["legacySidebarItems"][number] | null;
|
|
surface: InstalledPlugin["surfaces"][number] | null;
|
|
} {
|
|
if (!identity || !plugin) return { sidebarItem: null, surface: null };
|
|
const sidebarItem =
|
|
identity.kind === "sidebar"
|
|
? (plugin.legacySidebarItems.find((contribution) => contribution.id === identity.id) ?? null)
|
|
: null;
|
|
const surfaceId = sidebarItem ? sidebarItem.surface : identity.id;
|
|
const surface = surfaceId
|
|
? (plugin.surfaces.find((contribution) => contribution.id === surfaceId) ?? null)
|
|
: null;
|
|
return { sidebarItem, surface };
|
|
}
|
|
|
|
/**
|
|
* The screen header's title. A legacy `addSidebarItem` pointing at the screen lends its title, as
|
|
* before screens had one. A title function that throws or returns an empty string falls back to the
|
|
* screen id, so a bad title never takes the screen down.
|
|
*/
|
|
export function resolvePluginScreenTitle(
|
|
screen: InstalledPlugin["surfaces"][number],
|
|
legacySidebarItem: InstalledPlugin["legacySidebarItems"][number] | null,
|
|
params: PluginScreenParams,
|
|
resolve: (text: import("@getpaseo/plugin/client").PluginText) => string,
|
|
): string {
|
|
if (legacySidebarItem) return resolve(legacySidebarItem.title);
|
|
if (typeof screen.title !== "function") return resolve(screen.title);
|
|
try {
|
|
const title = resolve(screen.title(params));
|
|
if (typeof title === "string" && title.trim()) return title.trim();
|
|
} catch (error) {
|
|
console.warn(`[Plugins] Screen ${screen.id} title failed`, error);
|
|
}
|
|
return screen.id;
|
|
}
|
|
|
|
/**
|
|
* The installation's screen open at `pathname` with the route's search params, or null when the
|
|
* route shows anything else.
|
|
*/
|
|
export function currentPluginScreen(
|
|
plugin: InstalledPlugin,
|
|
pathname: string,
|
|
routeParams: Readonly<Record<string, string | string[] | undefined>>,
|
|
): PluginScreenLocation | null {
|
|
const route = parsePluginSurfaceRoute(pathname);
|
|
if (!route || route.serverId !== plugin.serverId || route.pluginId !== plugin.id) return null;
|
|
const screenId = resolvePluginSurfaceContribution(plugin, route.identity).surface?.id;
|
|
return screenId ? { screenId, params: pluginScreenParamsFromRoute(routeParams) } : null;
|
|
}
|
|
|
|
/** Checks an `openScreen` input against the installation's screens. Throws when it is invalid. */
|
|
export function parsePluginOpenScreenInput(
|
|
plugin: InstalledPlugin,
|
|
input: unknown,
|
|
): PluginScreenLocation {
|
|
if (typeof input !== "object" || input === null || !("screenId" in input)) {
|
|
throw new Error("openScreen takes { screenId, params? }");
|
|
}
|
|
const { screenId } = input;
|
|
if (typeof screenId !== "string" || !plugin.surfaces.some((surface) => surface.id === screenId)) {
|
|
throw new Error(`Plugin screen is unavailable: ${String(screenId)}`);
|
|
}
|
|
const params = "params" in input ? input.params : undefined;
|
|
return { screenId, params: parsePluginScreenParams(params) };
|
|
}
|
|
|
|
export function getPluginSurfaceContributionServerIds(
|
|
installations: readonly InstalledPlugin[],
|
|
pluginId: string,
|
|
identity: PluginSurfaceContributionIdentity,
|
|
): string[] {
|
|
return installations
|
|
.filter(
|
|
(installation) =>
|
|
installation.id === pluginId &&
|
|
resolvePluginSurfaceContribution(installation, identity).surface !== null,
|
|
)
|
|
.map((installation) => installation.serverId);
|
|
}
|