From 4b2dcf9a1ac2656d6ae85e028397d3ac1c8502bf Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 17 Aug 2026 19:07:25 -0700 Subject: [PATCH] Open Settings in a dialog on the web build (#581) --- apps/yaak-client/commands/openSettings.tsx | 35 ++++++++++++------- .../components/Settings/Settings.tsx | 21 +++++------ .../workspaces/$workspaceId/settings.tsx | 3 +- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/apps/yaak-client/commands/openSettings.tsx b/apps/yaak-client/commands/openSettings.tsx index 0bef76a9..7d7e103c 100644 --- a/apps/yaak-client/commands/openSettings.tsx +++ b/apps/yaak-client/commands/openSettings.tsx @@ -1,33 +1,42 @@ import { platform } from "@yaakapp-internal/platform"; -import type { SettingsTab } from "../components/Settings/Settings"; +import type { SettingsTab, SettingsTabWithSubtab } from "../components/Settings/Settings"; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace"; import { createFastMutation } from "../hooks/useFastMutation"; +import { showDialog } from "../lib/dialog"; import { jotaiStore } from "../lib/jotai"; import { router } from "../lib/router"; import { rpc } from "../lib/rpc"; -// Allow tab with optional subtab (e.g., "plugins:installed") -type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}` | null; - -export const openSettings = createFastMutation({ +export const openSettings = createFastMutation({ mutationKey: ["open_settings"], mutationFn: async (tab) => { const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); if (workspaceId == null) return; - const to = "/workspaces/$workspaceId/settings" as const; - const params = { workspaceId }; - const search = { tab: (tab ?? undefined) as SettingsTab | undefined }; - // Settings is its own window where the host has windows to give. Where it - // doesn't — a browser tab — the same route opens in place, which is the - // whole difference: it is already a route, not a separate app. + // doesn't — a browser tab — it's a dialog like any other, so opening it + // doesn't take you away from the request you were working on. if (!platform.capabilities.multiWindow) { - await router.navigate({ to, params, search }); + // Imported here so Settings stays out of the startup bundle, the way the + // route that renders it on desktop already keeps it + const { default: Settings } = await import("../components/Settings/Settings"); + showDialog({ + id: "settings", + size: "md", + className: "h-[calc(100vh-5rem)] max-h-150! overflow-hidden", + noPadding: true, + noScroll: true, + // Keyed so opening a specific tab while the dialog is already up moves to it + render: ({ hide }) => , + }); return; } - const location = router.buildLocation({ to, params, search }); + const location = router.buildLocation({ + to: "/workspaces/$workspaceId/settings", + params: { workspaceId }, + search: { tab: (tab ?? undefined) as SettingsTab | undefined }, + }); await rpc("cmd_new_child_window", { url: location.href, diff --git a/apps/yaak-client/components/Settings/Settings.tsx b/apps/yaak-client/components/Settings/Settings.tsx index 6a57b271..5588734d 100644 --- a/apps/yaak-client/components/Settings/Settings.tsx +++ b/apps/yaak-client/components/Settings/Settings.tsx @@ -1,4 +1,3 @@ -import { useSearch } from "@tanstack/react-router"; import { platform } from "@yaakapp-internal/platform"; import { useLicense } from "@yaakapp-internal/license"; import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models"; @@ -20,6 +19,8 @@ import { SettingsProxy } from "./SettingsProxy"; import { SettingsTheme } from "./SettingsTheme"; interface Props { + tab?: SettingsTabWithSubtab | null; + /** Set when Settings is in a dialog rather than owning a window. */ hide?: () => void; } @@ -42,25 +43,19 @@ const tabs = [ TAB_LICENSE, ] as const; export type SettingsTab = (typeof tabs)[number]; +export type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}`; -export default function Settings({ hide }: Props) { - const { tab: tabFromQuery } = useSearch({ from: "/workspaces/$workspaceId/settings" }); +export default function Settings({ tab, hide }: Props) { // Parse tab and subtab (e.g., "plugins:installed") - const [mainTab, subtab] = tabFromQuery?.split(":") ?? []; + const [mainTab, subtab] = tab?.split(":") ?? []; const settings = useAtomValue(settingsAtom); const plugins = useAtomValue(pluginsAtom); const licenseCheck = useLicense(); - // Close settings window on escape + // Close settings window on escape. In a dialog, the dialog handles Escape itself. // TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window useKeyPressEvent("Escape", async () => { - if (hide != null) { - // It's being shown in a dialog, so close the dialog - hide(); - } else { - // It's being shown in a window, so close the window - await platform.window.close(); - } + if (hide == null) await platform.window.close(); }); return ( @@ -90,7 +85,7 @@ export default function Settings({ hide }: Props) { )} ; + const { tab } = Route.useSearch(); + return ; }