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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -930,6 +930,8 @@ export const ptBR: TranslationResources = {
openSettings: "Abrir configurações do projeto", openSettings: "Abrir configurações do projeto",
openNewWindow: "Abrir em nova janela", openNewWindow: "Abrir em nova janela",
openNewWindowFailed: "Não foi possível abrir uma 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", remove: "Remover projeto",
removing: "Removendo...", removing: "Removendo...",
}, },

View File

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

View File

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