Repository navigation
fix(desktop): v2 Open In editor is per-project, not per-workspace #3393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,49 +8,59 @@ import { | |
| import { toast } from "@superset/ui/sonner"; | ||
| import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; | ||
| import { cn } from "@superset/ui/utils"; | ||
| import { useCallback, useMemo, useState } from "react"; | ||
| import { eq } from "@tanstack/db"; | ||
| import { useLiveQuery } from "@tanstack/react-db"; | ||
| import { useCallback, useMemo } from "react"; | ||
| import { HiChevronDown } from "react-icons/hi2"; | ||
| import { | ||
| getAppOption, | ||
| OpenInExternalDropdownItems, | ||
| } from "renderer/components/OpenInExternalDropdown"; | ||
| import { HotkeyLabel, useHotkey, useHotkeyDisplay } from "renderer/hotkeys"; | ||
| import { electronTrpc } from "renderer/lib/electron-trpc"; | ||
| import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; | ||
| import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; | ||
| import { useThemeStore } from "renderer/stores"; | ||
|
|
||
| interface V2OpenInMenuButtonProps { | ||
| worktreePath: string; | ||
| branch: string; | ||
| workspaceId: string; | ||
| projectId: string; | ||
| } | ||
|
|
||
| export function V2OpenInMenuButton({ | ||
| worktreePath, | ||
| branch, | ||
| workspaceId, | ||
| projectId, | ||
| }: V2OpenInMenuButtonProps) { | ||
| const collections = useCollections(); | ||
| const { ensureProjectInSidebar } = useDashboardSidebarState(); | ||
| const activeTheme = useThemeStore((state) => state.activeTheme); | ||
|
|
||
| const localState = collections.v2WorkspaceLocalState.get(workspaceId); | ||
| const [defaultApp, setDefaultApp] = useState<ExternalApp>( | ||
| (localState?.defaultOpenInApp as ExternalApp) ?? "finder", | ||
| const { data: sidebarProjectRows = [] } = useLiveQuery( | ||
| (q) => | ||
| q | ||
| .from({ sp: collections.v2SidebarProjects }) | ||
| .where(({ sp }) => eq(sp.projectId, projectId)) | ||
| .select(({ sp }) => ({ defaultOpenInApp: sp.defaultOpenInApp })), | ||
| [collections, projectId], | ||
| ); | ||
| const resolvedApp: ExternalApp = | ||
| (sidebarProjectRows[0]?.defaultOpenInApp as ExternalApp | null) ?? "finder"; | ||
|
|
||
| const handleDefaultAppChange = useCallback( | ||
| const persistDefaultApp = useCallback( | ||
| (app: ExternalApp) => { | ||
| setDefaultApp(app); | ||
| collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { | ||
| ensureProjectInSidebar(projectId); | ||
| collections.v2SidebarProjects.update(projectId, (draft) => { | ||
| draft.defaultOpenInApp = app; | ||
| }); | ||
| }, | ||
| [collections, workspaceId], | ||
| [collections, ensureProjectInSidebar, projectId], | ||
| ); | ||
|
Comment on lines
+51
to
59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When no
A safer approach would be to scope this field write so it only mutates an already-existing record, and treats a missing record as "no preference stored yet": const persistDefaultApp = useCallback(
(app: ExternalApp) => {
if (collections.v2SidebarProjects.get(projectId)) {
collections.v2SidebarProjects.update(projectId, (draft) => {
draft.defaultOpenInApp = app;
});
}
// No insert fallback: the sidebar manages the authoritative record.
},
[collections, projectId],
);If the intent is genuinely to persist the choice before the sidebar creates its record, the insertion should use the sidebar's own "create project entry" logic so that |
||
|
|
||
| const openInApp = electronTrpc.external.openInApp.useMutation({ | ||
| onSuccess: (_data, variables) => { | ||
| handleDefaultAppChange(variables.app); | ||
| persistDefaultApp(variables.app); | ||
| }, | ||
| onError: (error) => toast.error(`Failed to open: ${error.message}`), | ||
| }); | ||
|
|
@@ -60,8 +70,8 @@ export function V2OpenInMenuButton({ | |
| }); | ||
|
|
||
| const currentApp = useMemo( | ||
| () => getAppOption(defaultApp) ?? null, | ||
| [defaultApp], | ||
| () => getAppOption(resolvedApp) ?? null, | ||
| [resolvedApp], | ||
| ); | ||
| const openInDisplay = useHotkeyDisplay("OPEN_IN_APP"); | ||
| const copyPathDisplay = useHotkeyDisplay("COPY_PATH"); | ||
|
|
@@ -72,8 +82,8 @@ export function V2OpenInMenuButton({ | |
|
|
||
| const handleOpenInEditor = useCallback(() => { | ||
| if (openInApp.isPending || copyPath.isPending) return; | ||
| openInApp.mutate({ path: worktreePath, app: defaultApp }); | ||
| }, [worktreePath, defaultApp, openInApp, copyPath.isPending]); | ||
| openInApp.mutate({ path: worktreePath, app: resolvedApp }); | ||
| }, [worktreePath, resolvedApp, openInApp, copyPath.isPending]); | ||
|
|
||
| const handleOpenInOtherApp = useCallback( | ||
| (appId: ExternalApp) => { | ||
|
|
@@ -162,12 +172,12 @@ export function V2OpenInMenuButton({ | |
| <DropdownMenuContent align="end" className="w-48"> | ||
| <OpenInExternalDropdownItems | ||
| isDark={isDark} | ||
| activeApp={defaultApp} | ||
| activeApp={resolvedApp} | ||
| onOpenIn={handleOpenInOtherApp} | ||
| onCopyPath={handleCopyPath} | ||
| renderAppTrailing={(appId, group) => { | ||
| if ( | ||
| appId !== defaultApp || | ||
| appId !== resolvedApp || | ||
| !showOpenInShortcut || | ||
| group === "jetbrains" | ||
| ) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string | nulltoExternalAppdefaultOpenInAppis typed asstring | nullin the schema, but it is cast directly toExternalApp | nullhere. If a stale or manually-edited localStorage entry contains a string that is not a member of theExternalAppunion,resolvedAppwill silently carry an invalid value.getAppOption(resolvedApp)does guard against this (returningnull→currentAppisnull→ the button shows "Select an editor from the dropdown"), so there is no crash, but the mismatch between the declared type and the runtime value could hide bugs in callers that trust the type annotation.Consider validating against the known
ExternalAppvalues before the cast, or narrowing the schema to an enum so Zod rejects unknown strings at parse time:where
isExternalAppis a simple type-guard against theExternalAppunion.