Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion apps/studio/src/client/components/file-actions-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import {
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";

import { useOpenTaskFile } from "../hooks/use-open-task-file";
import { useTaskFileOpenTarget } from "../hooks/use-task-file-open-target";
import { useTimedFlag } from "../hooks/use-timed-flag";
import { getRevealInFolderLabel } from "../lib/utils";
import { RevealInFolderIcon } from "./icons/reveal-in-folder";
import { OpenTargetIcon } from "./open-target-icon";
import { OpenWithMenu } from "./open-with-menu";
import { Button, type ButtonVariant } from "./ui/button";
import {
DropdownMenu,
Expand All @@ -36,11 +40,13 @@ export function FileActionsMenu({
variant?: ButtonVariant;
}) {
const fileActions = useFileActionVisibility(file);
const { showOpen } = useTaskFileOpenTarget(file);

if (
!onAddToChat &&
!fileActions.showCopy &&
!fileActions.showDownload &&
!showOpen &&
!fileActions.showReveal
) {
return null;
Expand Down Expand Up @@ -75,6 +81,8 @@ export function FileActionsMenuItems({
}) {
const { Item, Separator } = menuComponents;
const fileActions = useFileActionVisibility(file);
const openTaskFile = useOpenTaskFile();
const { openLabel, showOpen, showOpenWith } = useTaskFileOpenTarget(file);

const showTaskFileInFolderMutation = useMutation(
rpcClient.utils.showTaskFileInFolder.mutationOptions({
Expand Down Expand Up @@ -115,14 +123,33 @@ export function FileActionsMenuItems({
};

const hasFileActions =
fileActions.showCopy || fileActions.showDownload || fileActions.showReveal;
showOpen ||
fileActions.showCopy ||
fileActions.showDownload ||
fileActions.showReveal;

if (!onAddToChat && !hasFileActions) {
return null;
}

return (
<>
{showOpen && (
<>
<Item
onClick={() => {
openTaskFile(file);
}}
>
<OpenTargetIcon className="size-4" file={file} />
<span>{openLabel}</span>
</Item>
{showOpenWith && (
<OpenWithMenu file={file} menuComponents={menuComponents} />
)}
{(onAddToChat != null || hasFileActions) && <Separator />}
</>
)}
{onAddToChat && (
<>
<Item onClick={onAddToChat}>
Expand Down
85 changes: 50 additions & 35 deletions apps/studio/src/client/components/file-preview-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,28 @@ import {
import { useFileActionVisibility } from "@/client/hooks/use-file-action-visibility";
import { copyFileToClipboard, downloadFile } from "@/client/lib/file-actions";
import { fileKindLabel, getFileType } from "@/client/lib/get-file-type";
import { cn, getRevealInFolderLabel } from "@/client/lib/utils";
import { rpcClient } from "@/client/rpc/client";
import { cn } from "@/client/lib/utils";
import {
ArrowLineDownIcon,
CheckIcon,
CopyIcon,
ImageBrokenIcon,
PlayIcon,
} from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { useRef, useState } from "react";
import { toast } from "sonner";

import { useOpenTaskFile } from "../hooks/use-open-task-file";
import {
usePrefetchTaskFileOpenTarget,
useTaskFileOpenTarget,
} from "../hooks/use-task-file-open-target";
import { useTimedFlag } from "../hooks/use-timed-flag";
import { FileActionsMenu, FileActionsMenuItems } from "./file-actions-menu";
import { FileThumbnail } from "./file-thumbnail";
import { RevealInFolderIcon } from "./icons/reveal-in-folder";
import { ImageWithFallback } from "./image-with-fallback";
import { MediaCardShell } from "./media-card-shell";
import { MediaOverlayButton } from "./media-overlay-button";
import { OpenTargetIcon } from "./open-target-icon";
import {
ContextMenu,
ContextMenuContent,
Expand Down Expand Up @@ -155,7 +157,11 @@ function FileRowCard({
const isMissing = useTaskFileReferenceStatus(file) === "missing";
const fileActions = useFileActionVisibility(file);
const hasFileActions =
fileActions.showCopy || fileActions.showDownload || fileActions.showReveal;
fileActions.showCopy ||
fileActions.showDownload ||
fileActions.showOpen ||
fileActions.showReveal;
const prefetchOpenTarget = usePrefetchTaskFileOpenTarget();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const row = (
<div
Expand All @@ -166,6 +172,9 @@ function FileRowCard({
: "bg-card shadow-xs hover:bg-muted/40 dark:border dark:border-black/5 dark:hover:bg-muted/40",
)}
onClick={onClick}
onMouseEnter={() => {
prefetchOpenTarget(file);
}}
>
<FileThumbnail
file={file}
Expand Down Expand Up @@ -237,7 +246,8 @@ function ImagePreviewCard({
const { filename, mimeType } = file;
const url = useLiveAssetUrl(file);
const fileActions = useFileActionVisibility(file);
const actions = useFileActions(file);
const [resolveOpenTarget, setResolveOpenTarget] = useState(false);
const actions = useFileActions(file, { resolveOpenTarget });
const { active: copied, trigger: triggerCopied } = useTimedFlag();

const handleCopy = async () => {
Expand All @@ -255,9 +265,7 @@ function ImagePreviewCard({

const hasActions =
!hideActionsMenu &&
(fileActions.showCopy ||
fileActions.showDownload ||
fileActions.showReveal);
(fileActions.showCopy || fileActions.showDownload || actions.showOpen);

return (
<MediaCardShell
Expand All @@ -266,6 +274,9 @@ function ImagePreviewCard({
hideActionsMenu={hideActionsMenu}
isSelected={isSelected}
onClick={onClick}
onMouseEnter={() => {
setResolveOpenTarget(true);
}}
overlayActions={
hasActions ? (
<>
Expand Down Expand Up @@ -295,13 +306,15 @@ function ImagePreviewCard({
}}
/>
)}
{fileActions.showReveal && (
{actions.showOpen && (
<MediaOverlayButton
icon={<RevealInFolderIcon className="size-3.5 shrink-0" />}
label={getRevealInFolderLabel()}
icon={
<OpenTargetIcon className="size-3.5 shrink-0" file={file} />
}
label="Open"
onClick={(e) => {
e.stopPropagation();
actions.revealInFolder();
actions.open();
}}
/>
)}
Expand Down Expand Up @@ -365,27 +378,23 @@ function MissingMediaCard({
);
}

function useFileActions(file: TaskFileViewerFile) {
const showTaskFileInFolderMutation = useMutation(
rpcClient.utils.showTaskFileInFolder.mutationOptions({
onError: (error) => {
const label = getRevealInFolderLabel();
const lower = label.charAt(0).toLowerCase() + label.slice(1);
toast.error(`Failed to ${lower}`, { description: error.message });
},
}),
function useFileActions(
file: TaskFileViewerFile,
{ resolveOpenTarget }: { resolveOpenTarget: boolean },
) {
const openTaskFile = useOpenTaskFile();
const { showOpen } = useTaskFileOpenTarget(
resolveOpenTarget ? file : undefined,
);

return {
download: async () => {
await downloadFile(file);
},
revealInFolder: () => {
showTaskFileInFolderMutation.mutate({
filePath: file.filePath,
id: file.taskId,
});
open: () => {
openTaskFile(file);
},
showOpen,
};
}

Expand Down Expand Up @@ -420,10 +429,11 @@ function VideoPreviewCard({
}) {
const url = useLiveAssetUrl(file);
const fileActions = useFileActionVisibility(file);
const actions = useFileActions(file);
const [resolveOpenTarget, setResolveOpenTarget] = useState(false);
const actions = useFileActions(file, { resolveOpenTarget });

const hasActions =
!hideActionsMenu && (fileActions.showDownload || fileActions.showReveal);
!hideActionsMenu && (fileActions.showDownload || actions.showOpen);

const displayTime =
isPlaying && timeRemaining !== null ? timeRemaining : videoDuration;
Expand All @@ -450,7 +460,10 @@ function VideoPreviewCard({
hideActionsMenu={hideActionsMenu}
isSelected={isSelected}
onClick={onClick}
onMouseEnter={handleMouseEnter}
onMouseEnter={() => {
setResolveOpenTarget(true);
handleMouseEnter();
}}
onMouseLeave={handleMouseLeave}
overlayActions={
hasActions ? (
Expand All @@ -465,13 +478,15 @@ function VideoPreviewCard({
}}
/>
)}
{fileActions.showReveal && (
{actions.showOpen && (
<MediaOverlayButton
icon={<RevealInFolderIcon className="size-3.5 shrink-0" />}
label={getRevealInFolderLabel()}
icon={
<OpenTargetIcon className="size-3.5 shrink-0" file={file} />
}
label="Open"
onClick={(e) => {
e.stopPropagation();
actions.revealInFolder();
actions.open();
}}
/>
)}
Expand Down
40 changes: 33 additions & 7 deletions apps/studio/src/client/components/file-preview-fallback.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { type TaskFileViewerFile } from "@/client/atoms/task-file-viewer";
import { useOpenTaskFile } from "@/client/hooks/use-open-task-file";
import { useTaskFileOpenTarget } from "@/client/hooks/use-task-file-open-target";
import { ArrowLineDownIcon } from "@phosphor-icons/react";

import { FileIcon } from "./file-icon";
import { OpenTargetIcon } from "./open-target-icon";
import { Button } from "./ui/button";

export function FilePreviewFallback({
fallbackExtension,
file,
filename,
onDownload,
}: {
fallbackExtension?: string;
file?: Pick<TaskFileViewerFile, "filePath" | "taskId">;
filename: string;
onDownload?: () => void;
}) {
const openTaskFile = useOpenTaskFile();
const { appName, openLabel } = useTaskFileOpenTarget(file);
// Without a resolved app association, opening could dead-end in an OS
// error, so only promote open over download when an app is known.
const canOpen = file != null && appName != null;

return (
<div className="flex w-full max-w-md flex-col items-center justify-center gap-4 p-8 text-center text-foreground">
<div className="flex h-20 w-16 items-center justify-center rounded-lg bg-accent text-muted-foreground">
Expand All @@ -24,16 +36,30 @@ export function FilePreviewFallback({
<div>
<p className="text-sm font-medium">Preview not available</p>
<p className="mt-1 text-xs text-muted-foreground">
{onDownload
? "Download this file to view it"
: "This file cannot be previewed"}
{canOpen
? `Open this file in ${appName} to view it`
: onDownload
? "Download this file to view it"
: "This file cannot be previewed"}
</p>
</div>
{onDownload && (
<Button onClick={onDownload} size="sm">
<ArrowLineDownIcon className="size-4" />
Download
{canOpen ? (
<Button
onClick={() => {
openTaskFile(file);
}}
size="sm"
>
<OpenTargetIcon className="size-4" file={file} />
{openLabel}
</Button>
) : (
onDownload && (
<Button onClick={onDownload} size="sm">
<ArrowLineDownIcon className="size-4" />
Download
</Button>
)
)}
</div>
);
Expand Down
Loading