Open project and workspace folders from the sidebar (#2491)

* feat(app): open project folder from project context menu (#2487)

* refactor(app): give file manager action a home

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Kamil
2026-07-28 00:48:29 +02:00
committed by GitHub
parent f8dd0fc2e0
commit 89c2fac3c6
11 changed files with 84 additions and 0 deletions

View File

@@ -131,6 +131,7 @@ import {
getIsElectron,
} from "@/constants/platform";
import { getDesktopHost } from "@/desktop/host";
import { OpenInFileManagerMenuItem } from "@/workspace/open-in-file-manager/menu-item";
const workspaceKeyExtractor = (workspace: SidebarWorkspacePlacement) => workspace.workspaceKey;
@@ -609,6 +610,10 @@ function ProjectKebabMenu({
{t("sidebar.project.actions.openNewWindow")}
</DropdownMenuItem>
) : null}
<OpenInFileManagerMenuItem
path={projectPath}
testID={`sidebar-project-menu-open-folder-${projectKey}`}
/>
<DropdownMenuItem
testID={`sidebar-project-menu-remove-${projectKey}`}
leading={trash2LeadingIcon}
@@ -660,6 +665,7 @@ function WorkspaceRowRightGroup({
isPinned?: boolean;
onTogglePin?: () => void;
}) {
const workspacePath = workspace.workspaceDirectory ?? workspace.projectRootPath;
const { t } = useTranslation();
const showShortcut = showShortcutBadge && shortcutNumber !== null;
const showKebab = Boolean(onArchive && (isHovered || isTouchPlatform));
@@ -698,6 +704,7 @@ function WorkspaceRowRightGroup({
archiveShortcutKeys={archiveShortcutKeys}
isPinned={isPinned}
onTogglePin={onTogglePin}
openInFileManagerPath={workspacePath}
/>
) : null}
</SidebarWorkspaceTrailingActionOverlay>

View File

@@ -13,6 +13,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Shortcut } from "@/components/ui/shortcut";
import { OpenInFileManagerMenuItem } from "@/workspace/open-in-file-manager/menu-item";
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const foregroundMutedColorMapping = (theme: Theme) => ({
@@ -58,6 +59,7 @@ interface SidebarWorkspaceMenuProps {
archiveShortcutKeys?: ShortcutKey[][] | null;
isPinned?: boolean;
onTogglePin?: () => void;
openInFileManagerPath?: string | null;
}
export function SidebarWorkspaceMenu({
@@ -73,6 +75,7 @@ export function SidebarWorkspaceMenu({
archiveShortcutKeys,
isPinned,
onTogglePin,
openInFileManagerPath,
}: SidebarWorkspaceMenuProps) {
const { t } = useTranslation();
const archiveTrailing = useMemo(
@@ -137,6 +140,10 @@ export function SidebarWorkspaceMenu({
{isPinned ? t("sidebar.workspace.actions.unpin") : t("sidebar.workspace.actions.pin")}
</DropdownMenuItem>
) : null}
<OpenInFileManagerMenuItem
path={openInFileManagerPath}
testID={`sidebar-workspace-menu-open-folder-${workspaceKey}`}
/>
<DropdownMenuItem
testID={`sidebar-workspace-menu-archive-${workspaceKey}`}
leading={archiveLeadingIcon}

View File

@@ -908,6 +908,8 @@ export const ar: TranslationResources = {
openSettings: "افتح إعدادات المشروع",
openNewWindow: "Open in new window",
openNewWindowFailed: "Couldn't open a new window",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "إزالة المشروع",
removing: "جارٍ الإزالة...",
},

View File

@@ -918,6 +918,8 @@ export const en = {
openSettings: "Open project settings",
openNewWindow: "Open in new window",
openNewWindowFailed: "Couldn't open a new window",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "Remove project",
removing: "Removing...",
},

View File

@@ -939,6 +939,8 @@ export const es: TranslationResources = {
openSettings: "Abrir la configuración del proyecto",
openNewWindow: "Open in new window",
openNewWindowFailed: "Couldn't open a new window",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "Eliminar proyecto",
removing: "Eliminando...",
},

View File

@@ -938,6 +938,8 @@ export const fr: TranslationResources = {
openSettings: "Ouvrir les paramètres du projet",
openNewWindow: "Open in new window",
openNewWindowFailed: "Couldn't open a new window",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "Supprimer le projet",
removing: "Suppression...",
},

View File

@@ -919,6 +919,8 @@ export const ja: TranslationResources = {
openSettings: "プロジェクト設定を開く",
openNewWindow: "新しいウィンドウで開く",
openNewWindowFailed: "新しいウィンドウを開けませんでした",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "プロジェクトを削除",
removing: "削除中...",
},

View File

@@ -930,6 +930,8 @@ export const ptBR: TranslationResources = {
openSettings: "Abrir configurações do projeto",
openNewWindow: "Abrir em nova janela",
openNewWindowFailed: "Não foi possível abrir uma nova janela",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "Remover projeto",
removing: "Removendo...",
},

View File

@@ -930,6 +930,8 @@ export const ru: TranslationResources = {
openSettings: "Открыть настройки проекта",
openNewWindow: "Open in new window",
openNewWindowFailed: "Couldn't open a new window",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "Удалить проект",
removing: "Удаление...",
},

View File

@@ -899,6 +899,8 @@ export const zhCN: TranslationResources = {
openSettings: "打开 project 设置",
openNewWindow: "在新窗口中打开",
openNewWindowFailed: "无法打开新窗口",
openFolder: "Open in file manager",
openFolderFailed: "Couldn't open folder",
remove: "移除 project",
removing: "正在移除...",
},

View File

@@ -0,0 +1,54 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { FolderOpen } from "lucide-react-native";
import { withUnistyles } from "react-native-unistyles";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { getIsElectron } from "@/constants/platform";
import { useToast } from "@/contexts/toast-context";
import type { Theme } from "@/styles/theme";
import { openDesktopTarget, useDesktopOpenTargets } from "@/workspace/desktop-open-targets";
interface OpenInFileManagerMenuItemProps {
path?: string | null;
testID: string;
}
const ThemedFolderOpen = withUnistyles(FolderOpen);
const foregroundMutedColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
const leadingIcon = <ThemedFolderOpen size={14} uniProps={foregroundMutedColorMapping} />;
export function OpenInFileManagerMenuItem({ path, testID }: OpenInFileManagerMenuItemProps) {
const { t } = useTranslation();
const toast = useToast();
const isElectron = getIsElectron();
const workspacePath = path?.trim() ?? "";
const { targets } = useDesktopOpenTargets({
isLocalExecution: isElectron && workspacePath.length > 0,
});
const fileManagerTarget = targets.find((target) => target.kind === "file-manager");
const openInFileManager = useCallback(() => {
if (!fileManagerTarget || workspacePath.length === 0) return;
void openDesktopTarget({
editorId: fileManagerTarget.id,
workspacePath,
}).catch((error) => {
console.warn("[open-in-file-manager] open failed", error);
toast.error(t("sidebar.project.actions.openFolderFailed"));
});
}, [fileManagerTarget, t, toast, workspacePath]);
if (!isElectron || !fileManagerTarget || workspacePath.length === 0) {
return null;
}
return (
<DropdownMenuItem testID={testID} leading={leadingIcon} onSelect={openInFileManager}>
{t("sidebar.project.actions.openFolder")}
</DropdownMenuItem>
);
}