mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +00:00
fix(app): render settings tab icons inside the plugin's runtime
Component icons now mount under the installation provider, so plugin SDK hooks such as useTranslation work. Each icon has its own error boundary; an icon that throws leaves its tab with the label alone.
This commit is contained in:
@@ -15,7 +15,10 @@ type SettingsClient = Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>
|
||||
const TABBED_PLUGIN_ID = "settings-tabs";
|
||||
/** Two settings screens, so the settings page shows a tab per screen. */
|
||||
const TABBED_SOURCE = `import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import { Text, View } from "react-native";
|
||||
import { defineTranslations, useTranslation } from "@getpaseo/plugin/client";
|
||||
|
||||
const messages = defineTranslations({ resources: { en: { icon: "advanced-tab-icon" } } });
|
||||
|
||||
function General({ theme }) {
|
||||
return <Text style={{ color: theme.colors.foreground }}>General settings body</Text>;
|
||||
@@ -25,9 +28,19 @@ function Advanced({ theme }) {
|
||||
return <Text style={{ color: theme.colors.foreground }}>Advanced settings body</Text>;
|
||||
}
|
||||
|
||||
// Component icons run with the plugin's runtime; one that throws costs its tab only the icon.
|
||||
function TranslatedIcon({ color, size }) {
|
||||
const { t } = useTranslation(messages);
|
||||
return <View testID={t("icon")} aria-hidden style={{ width: size, height: size, borderRadius: size / 2, backgroundColor: color }} />;
|
||||
}
|
||||
|
||||
function BrokenIcon() {
|
||||
throw new Error("Settings tab icon exploded");
|
||||
}
|
||||
|
||||
export default function contribute(client) {
|
||||
client.addSettingsScreen({ id: "general", title: "General", icon: "Settings", Component: General });
|
||||
client.addSettingsScreen({ id: "advanced", title: "Advanced", icon: "Wrench", Component: Advanced });
|
||||
client.addSettingsScreen({ id: "general", title: "General", icon: BrokenIcon, Component: General });
|
||||
client.addSettingsScreen({ id: "advanced", title: "Advanced", icon: TranslatedIcon, Component: Advanced });
|
||||
return () => {};
|
||||
}
|
||||
`;
|
||||
@@ -114,6 +127,8 @@ export async function expectNoSettingsCogWithoutScreens(page: Page) {
|
||||
export async function openTabbedSettings(page: Page) {
|
||||
await page.getByRole("button", { name: `Settings for ${TABBED_PLUGIN_ID}`, exact: true }).click();
|
||||
await expectSettingsTab(page, "General");
|
||||
// The Advanced icon reads the plugin's translations; the General icon throws and is dropped.
|
||||
await expect(page.getByTestId("advanced-tab-icon")).toBeVisible();
|
||||
}
|
||||
function settingsTab(page: Page, title: string) {
|
||||
return page.getByTestId("plugin-settings-tabs").getByRole("button", { name: title, exact: true });
|
||||
|
||||
@@ -50,14 +50,42 @@ export function PluginSettingsButton({ serverId, pluginId }: Omit<SettingsIdenti
|
||||
);
|
||||
}
|
||||
|
||||
/** One tab per registered settings screen. A plugin with a single screen shows no tabs. */
|
||||
function renderNoIcon() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A plugin-supplied icon component may throw; its tab then shows the label alone. */
|
||||
function SettingsTabIcon({
|
||||
plugin,
|
||||
icon,
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
plugin: InstalledPlugin;
|
||||
icon: InstalledPlugin["settingsScreens"][number]["icon"];
|
||||
size: number;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<SurfaceErrorBoundary installation={plugin} Surface={icon} renderError={renderNoIcon}>
|
||||
<PluginIconView icon={icon} size={size} color={color} />
|
||||
</SurfaceErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One tab per registered settings screen. A plugin with a single screen shows no tabs.
|
||||
* Render inside the installation's provider: tab icons may be plugin components.
|
||||
*/
|
||||
function SettingsTabs({ plugin, screenId }: { plugin: InstalledPlugin; screenId: string }) {
|
||||
const options = useMemo<SegmentedControlOption<string>[]>(
|
||||
() =>
|
||||
plugin.settingsScreens.map((screen) => ({
|
||||
value: screen.id,
|
||||
label: plugin.i18n.resolve(screen.title, plugin.locale),
|
||||
icon: ({ color, size }) => <PluginIconView icon={screen.icon} size={size} color={color} />,
|
||||
icon: ({ color, size }) => (
|
||||
<SettingsTabIcon plugin={plugin} icon={screen.icon} size={size} color={color} />
|
||||
),
|
||||
testID: `plugin-settings-tab-${screen.id}`,
|
||||
})),
|
||||
[plugin],
|
||||
@@ -124,19 +152,19 @@ function SettingsContent({
|
||||
return <Text style={styles.message}>{t("settings.plugins.screens.unavailable")}</Text>;
|
||||
const Component = screen.Component;
|
||||
return (
|
||||
<View>
|
||||
<SettingsTabs plugin={plugin} screenId={screen.id} />
|
||||
<SurfaceErrorBoundary
|
||||
installation={plugin}
|
||||
Surface={Component}
|
||||
resetKey={attempt}
|
||||
renderError={renderError}
|
||||
>
|
||||
<PluginInstallationProvider plugin={plugin}>
|
||||
<PluginInstallationProvider plugin={plugin}>
|
||||
<View>
|
||||
<SettingsTabs plugin={plugin} screenId={screen.id} />
|
||||
<SurfaceErrorBoundary
|
||||
installation={plugin}
|
||||
Surface={Component}
|
||||
resetKey={attempt}
|
||||
renderError={renderError}
|
||||
>
|
||||
<Component theme={theme} layout={layout} host={host} />
|
||||
</PluginInstallationProvider>
|
||||
</SurfaceErrorBoundary>
|
||||
</View>
|
||||
</SurfaceErrorBoundary>
|
||||
</View>
|
||||
</PluginInstallationProvider>
|
||||
);
|
||||
}
|
||||
const ThemedSettingsContent = withUnistyles(SettingsContent);
|
||||
|
||||
Reference in New Issue
Block a user