From 51fd60d4c6d80213f354b852ec417737106200b5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 3 Nov 2026 13:23:13 +0100 Subject: [PATCH] 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. --- .../e2e/support/helpers/plugin-settings.ts | 21 ++++++- packages/app/src/plugins/settings/index.tsx | 56 ++++++++++++++----- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/packages/app/e2e/support/helpers/plugin-settings.ts b/packages/app/e2e/support/helpers/plugin-settings.ts index 942356bf3d..cc22bd1586 100644 --- a/packages/app/e2e/support/helpers/plugin-settings.ts +++ b/packages/app/e2e/support/helpers/plugin-settings.ts @@ -15,7 +15,10 @@ type SettingsClient = Awaited 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 General settings body; @@ -25,9 +28,19 @@ function Advanced({ theme }) { return Advanced settings body; } +// 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 ; +} + +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 }); diff --git a/packages/app/src/plugins/settings/index.tsx b/packages/app/src/plugins/settings/index.tsx index 69ebfbf341..46e35ad232 100644 --- a/packages/app/src/plugins/settings/index.tsx +++ b/packages/app/src/plugins/settings/index.tsx @@ -50,14 +50,42 @@ export function PluginSettingsButton({ serverId, pluginId }: Omit + + + ); +} + +/** + * 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[]>( () => plugin.settingsScreens.map((screen) => ({ value: screen.id, label: plugin.i18n.resolve(screen.title, plugin.locale), - icon: ({ color, size }) => , + icon: ({ color, size }) => ( + + ), testID: `plugin-settings-tab-${screen.id}`, })), [plugin], @@ -124,19 +152,19 @@ function SettingsContent({ return {t("settings.plugins.screens.unavailable")}; const Component = screen.Component; return ( - - - - + + + + - - - + + + ); } const ThemedSettingsContent = withUnistyles(SettingsContent);