Files
paseo/packages/app/src/plugins/settings/index.tsx
T
Mohamed Boudra 047f40e623 Install npm plugins and review source updates (#4975)
* 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
2026-09-17 10:22:20 +02:00

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 } }));