mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-12 08:19:54 +02:00
113 lines
5.0 KiB
TypeScript
113 lines
5.0 KiB
TypeScript
import { isCsvFile } from "@/lib/csv-preview";
|
|
import { useEffect, useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Check, Copy, Download, Eye, Code2 } from "lucide-react";
|
|
import { fileResourcesApi } from "@/api/file-resources";
|
|
import { FileViewerBody, FileViewerMetadataRow } from "@/components/FileViewerSheet";
|
|
import { Button } from "@/components/ui/button";
|
|
import type { FileViewerUrlState } from "@/context/FileViewerContext";
|
|
import { copyTextToClipboard } from "@/lib/clipboard";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import type { TaskSidePanelTabPayload } from "@/lib/task-side-panel-state";
|
|
|
|
type WorkspaceFilePayload = Extract<TaskSidePanelTabPayload, { kind: "workspace-file" }>;
|
|
|
|
export function TaskWorkspaceFilePanel({
|
|
issueId,
|
|
payload,
|
|
onFallbackToProject,
|
|
}: {
|
|
issueId: string;
|
|
payload: WorkspaceFilePayload;
|
|
onFallbackToProject?: () => void;
|
|
}) {
|
|
const [previewMode, setPreviewMode] = useState<"raw" | "rendered">("rendered");
|
|
useEffect(() => setPreviewMode("rendered"), [payload.path]);
|
|
const [copied, setCopied] = useState(false);
|
|
const [announcement, setAnnouncement] = useState("");
|
|
const state: FileViewerUrlState = {
|
|
path: payload.path,
|
|
workspace: payload.workspace,
|
|
projectId: payload.projectId,
|
|
workspaceId: payload.workspaceId,
|
|
line: payload.line,
|
|
column: payload.column,
|
|
};
|
|
const resourceQuery = useQuery({
|
|
queryKey: queryKeys.issues.fileResource(issueId, state),
|
|
queryFn: () => fileResourcesApi.resolve(issueId, state),
|
|
retry: false,
|
|
});
|
|
const resource = resourceQuery.data;
|
|
const contentQuery = useQuery({
|
|
queryKey: queryKeys.issues.fileResourceContent(issueId, state),
|
|
queryFn: () => fileResourcesApi.content(issueId, state),
|
|
enabled: resource?.capabilities.preview === true,
|
|
retry: false,
|
|
});
|
|
const downloadUrl = resource?.capabilities.download
|
|
? fileResourcesApi.downloadUrl(issueId, state)
|
|
: null;
|
|
|
|
const renderedPreview = resource && (isCsvFile(resource.displayPath || resource.title, resource.contentType ?? "") || /\.(md|markdown|mdown|mkdn|mkd)$/i.test(resource.displayPath || resource.title) || resource.contentType?.includes("markdown"));
|
|
|
|
return (
|
|
<div className="flex h-full min-h-0 flex-col bg-muted/20">
|
|
<header className="flex min-w-0 items-start gap-2 border-b border-border bg-card px-3 py-2">
|
|
<div className="min-w-0 flex-1">
|
|
<h2 className="truncate text-sm font-medium" title={payload.path}>{resource?.title ?? payload.path}</h2>
|
|
<p className="truncate font-mono text-xs text-muted-foreground" title={payload.path}>{payload.path}</p>
|
|
<FileViewerMetadataRow resolvedResource={resource} state={state} />
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
{contentQuery.data ? (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
aria-label="Copy file contents"
|
|
title={copied ? "Copied" : "Copy file contents"}
|
|
onClick={() => {
|
|
void copyTextToClipboard(contentQuery.data!.content.data).then(() => {
|
|
setCopied(true);
|
|
window.setTimeout(() => setCopied(false), 1800);
|
|
});
|
|
}}
|
|
>
|
|
{copied ? <Check aria-hidden /> : <Copy aria-hidden />}
|
|
</Button>
|
|
) : null}
|
|
{renderedPreview && contentQuery.data ? <div role="group" aria-label="File preview mode" className="flex gap-1">
|
|
<Button size="icon-sm" variant={previewMode === "rendered" ? "secondary" : "ghost"} aria-label="Rendered" title="Rendered" aria-pressed={previewMode === "rendered"} onClick={() => setPreviewMode("rendered")}><Eye aria-hidden /></Button>
|
|
<Button size="icon-sm" variant={previewMode === "raw" ? "secondary" : "ghost"} aria-label="Raw" title="Raw" aria-pressed={previewMode === "raw"} onClick={() => setPreviewMode("raw")}><Code2 aria-hidden /></Button>
|
|
</div> : null}
|
|
{downloadUrl ? (
|
|
<Button asChild variant="ghost" size="icon-sm">
|
|
<a href={downloadUrl} download={resource?.title} aria-label="Download file" title="Download file">
|
|
<Download aria-hidden />
|
|
</a>
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</header>
|
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
<div className="sr-only" aria-live="polite">{announcement}</div>
|
|
<FileViewerBody
|
|
previewMode={previewMode}
|
|
resolveQuery={resourceQuery}
|
|
contentQuery={contentQuery}
|
|
elapsedMs={0}
|
|
canPreview={resource?.capabilities.preview ?? false}
|
|
highlightedLine={payload.line}
|
|
onRetry={() => {
|
|
void resourceQuery.refetch();
|
|
if (resource?.capabilities.preview) void contentQuery.refetch();
|
|
}}
|
|
onSetAnnouncement={setAnnouncement}
|
|
onFallbackToProject={onFallbackToProject ?? null}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|