mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 18:22:38 +00:00
* feat(plugins): unify source installation and simplify plugin settings * feat(plugins): install npm sources and review CLI updates Treat install selectors as one-time choices. Keep source identity separate from the installed revision, and resolve ordinary updates from the current upstream default. npm only acquires directories; plugin loading remains independent of npm. * fix(plugins): preserve CLI diagnostics and exercise reviewed updates Move new source capability coverage to real browser journeys. Update the requirements regression to exercise preview and apply, and preserve the existing duplicate-selector diagnostic across positional option scopes. * test(plugins): remove unused pending helper * test(plugins): pack registry fixtures with the Windows command runner * test(plugins): assert failed client contribution setup * docs(plugins): consolidate guides and document npm publishing * docs(plugins): introduce publishing choices before npm instructions * test(plugins): own browser journey cleanup in a fixture * feat(plugins): prepare scaffolded packages for publishing
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
import { useCallback, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Platform, Text, View } from "react-native";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import { router } from "expo-router";
|
|
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
|
import { Button } from "@/components/ui/button";
|
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
|
import { useHostFeature } from "@/runtime/host-features";
|
|
import { useHostRuntimeClient, useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime";
|
|
import type { Theme } from "@/styles/theme";
|
|
import { useInstalledPlugin } from "../registry";
|
|
import { PluginRuntimeBoundary } from "../runtime-boundary";
|
|
import { SurfaceErrorBoundary } from "../surface-error-boundary";
|
|
import { toPluginTheme } from "../theme";
|
|
import { buildPluginSettingsRoute } from "./routes";
|
|
|
|
interface SettingsIdentity {
|
|
serverId: string;
|
|
pluginId: string;
|
|
screenId: string;
|
|
}
|
|
|
|
function PluginSettingsMenuItem({
|
|
serverId,
|
|
pluginId,
|
|
screenId,
|
|
title,
|
|
disabled,
|
|
}: SettingsIdentity & { title: string; disabled?: boolean }) {
|
|
const open = useCallback(
|
|
() => router.push(buildPluginSettingsRoute(serverId, pluginId, screenId)),
|
|
[serverId, pluginId, screenId],
|
|
);
|
|
return (
|
|
<DropdownMenuItem onSelect={open} disabled={disabled}>
|
|
{title}
|
|
</DropdownMenuItem>
|
|
);
|
|
}
|
|
|
|
export function PluginSettingsMenuItems({
|
|
serverId,
|
|
pluginId,
|
|
disabled,
|
|
}: Omit<SettingsIdentity, "screenId"> & { disabled?: boolean }) {
|
|
const plugin = useInstalledPlugin(serverId, pluginId);
|
|
const supported = useHostFeature(serverId, "pluginSettings");
|
|
if (!supported || !plugin) return null;
|
|
return (
|
|
<>
|
|
{plugin.settingsScreens.map((screen) => (
|
|
<PluginSettingsMenuItem
|
|
key={screen.id}
|
|
serverId={serverId}
|
|
pluginId={pluginId}
|
|
screenId={screen.id}
|
|
title={screen.title}
|
|
disabled={disabled}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SettingsContent({
|
|
serverId,
|
|
pluginId,
|
|
screenId,
|
|
theme,
|
|
}: SettingsIdentity & { theme: PluginHostProps["theme"] }) {
|
|
const { t } = useTranslation();
|
|
const plugin = useInstalledPlugin(serverId, pluginId);
|
|
const client = useHostRuntimeClient(serverId);
|
|
const connected = useHostRuntimeIsConnected(serverId);
|
|
const supported = useHostFeature(serverId, "pluginSettings");
|
|
const compact = useIsCompactFormFactor();
|
|
const hosts = useHosts();
|
|
const [attempt, setAttempt] = useState(0);
|
|
const screen = plugin?.settingsScreens.find((item) => item.id === screenId);
|
|
const retry = useCallback(() => setAttempt((value) => value + 1), []);
|
|
const renderError = useCallback(
|
|
(error: string) => (
|
|
<View>
|
|
<Text style={styles.message}>{error}</Text>
|
|
<Button variant="outline" size="sm" onPress={retry}>
|
|
{t("common.actions.retry")}
|
|
</Button>
|
|
</View>
|
|
),
|
|
[retry, t],
|
|
);
|
|
const host = useMemo(
|
|
() => ({
|
|
id: serverId,
|
|
label: hosts.find((candidate) => candidate.serverId === serverId)?.label ?? serverId,
|
|
}),
|
|
[hosts, serverId],
|
|
);
|
|
const platform = Platform.OS === "ios" || Platform.OS === "android" ? Platform.OS : "web";
|
|
const layout = useMemo<PluginHostProps["layout"]>(
|
|
() => ({ compact, platform }),
|
|
[compact, platform],
|
|
);
|
|
if (!connected)
|
|
return <Text style={styles.message}>{t("settings.plugins.screens.offline")}</Text>;
|
|
// COMPAT(pluginSettings): added in v0.8, remove after 2027-03-05.
|
|
if (!supported) return <Text style={styles.message}>{t("settings.plugins.screens.update")}</Text>;
|
|
if (!plugin || !screen || !client)
|
|
return <Text style={styles.message}>{t("settings.plugins.screens.unavailable")}</Text>;
|
|
const Component = screen.Component;
|
|
return (
|
|
<View>
|
|
<SurfaceErrorBoundary
|
|
installation={plugin}
|
|
Surface={Component}
|
|
resetKey={attempt}
|
|
renderError={renderError}
|
|
>
|
|
<PluginRuntimeBoundary plugin={plugin} client={client}>
|
|
<Component theme={theme} layout={layout} host={host} />
|
|
</PluginRuntimeBoundary>
|
|
</SurfaceErrorBoundary>
|
|
</View>
|
|
);
|
|
}
|
|
const ThemedSettingsContent = withUnistyles(SettingsContent);
|
|
const themeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme) });
|
|
export function PluginSettingsContent(props: SettingsIdentity) {
|
|
return <ThemedSettingsContent {...props} uniProps={themeMapping} />;
|
|
}
|
|
const styles = StyleSheet.create((theme) => ({ message: { color: theme.colors.foregroundMuted } }));
|