Skip to content

Commit 7ad813b

Browse files
author
Theodore Li
committed
Add download file shortcut on mothership file view
1 parent 7501ab1 commit 7ad813b

File tree

3 files changed

+90
-19
lines changed

3 files changed

+90
-19
lines changed

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { File as FilesIcon } from '@/components/emcn/icons'
2626
import { getDocumentIcon } from '@/components/icons/document-icons'
2727
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
2828
import {
29+
downloadWorkspaceFile,
2930
formatFileSize,
3031
getFileExtension,
3132
getMimeTypeFromExtension,
@@ -115,23 +116,6 @@ function formatFileType(mimeType: string | null, filename: string): string {
115116
return mimeType ?? 'File'
116117
}
117118

118-
async function downloadFile(file: WorkspaceFileRecord) {
119-
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace&t=${Date.now()}`
120-
const response = await fetch(serveUrl, { cache: 'no-store' })
121-
if (!response.ok) {
122-
throw new Error(`Failed to download file: ${response.statusText}`)
123-
}
124-
const blob = await response.blob()
125-
const url = URL.createObjectURL(blob)
126-
const a = document.createElement('a')
127-
a.href = url
128-
a.download = file.name
129-
document.body.appendChild(a)
130-
a.click()
131-
document.body.removeChild(a)
132-
URL.revokeObjectURL(url)
133-
}
134-
135119
export function Files() {
136120
const fileInputRef = useRef<HTMLInputElement>(null)
137121
const saveRef = useRef<(() => Promise<void>) | null>(null)
@@ -292,7 +276,7 @@ export function Files() {
292276

293277
const handleDownload = useCallback(async (file: WorkspaceFileRecord) => {
294278
try {
295-
await downloadFile(file)
279+
await downloadWorkspaceFile(file)
296280
} catch (err) {
297281
logger.error('Failed to download file:', err)
298282
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
'use client'
22

33
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'
4+
import { createLogger } from '@sim/logger'
45
import { Square } from 'lucide-react'
56
import { useRouter } from 'next/navigation'
67
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
7-
import { FileX, SquareArrowUpRight, WorkflowX } from '@/components/emcn/icons'
8+
import { Download, FileX, SquareArrowUpRight, WorkflowX } from '@/components/emcn/icons'
89
import {
910
markRunToolManuallyStopped,
1011
reportManualRunToolStop,
1112
} from '@/lib/copilot/client-sse/run-tool-execution'
13+
import { downloadWorkspaceFile } from '@/lib/uploads/utils/file-utils'
1214
import {
1315
FileViewer,
1416
type PreviewMode,
@@ -93,6 +95,8 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps)
9395
switch (resource.type) {
9496
case 'workflow':
9597
return <EmbeddedWorkflowActions workspaceId={workspaceId} workflowId={resource.id} />
98+
case 'file':
99+
return <EmbeddedFileActions workspaceId={workspaceId} fileId={resource.id} />
96100
case 'knowledgebase':
97101
return (
98102
<EmbeddedKnowledgeBaseActions workspaceId={workspaceId} knowledgeBaseId={resource.id} />
@@ -230,6 +234,68 @@ export function EmbeddedKnowledgeBaseActions({
230234
)
231235
}
232236

237+
const fileLogger = createLogger('EmbeddedFileActions')
238+
239+
interface EmbeddedFileActionsProps {
240+
workspaceId: string
241+
fileId: string
242+
}
243+
244+
function EmbeddedFileActions({ workspaceId, fileId }: EmbeddedFileActionsProps) {
245+
const router = useRouter()
246+
const { data: files = [] } = useWorkspaceFiles(workspaceId)
247+
const file = useMemo(() => files.find((f) => f.id === fileId), [files, fileId])
248+
249+
const handleDownload = useCallback(async () => {
250+
if (!file) return
251+
try {
252+
await downloadWorkspaceFile(file)
253+
} catch (err) {
254+
fileLogger.error('Failed to download file:', err)
255+
}
256+
}, [file])
257+
258+
const handleOpenInFiles = useCallback(() => {
259+
router.push(`/workspace/${workspaceId}/files?fileId=${fileId}`)
260+
}, [router, workspaceId, fileId])
261+
262+
return (
263+
<>
264+
<Tooltip.Root>
265+
<Tooltip.Trigger asChild>
266+
<Button
267+
variant='subtle'
268+
onClick={handleOpenInFiles}
269+
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
270+
aria-label='Open in files'
271+
>
272+
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
273+
</Button>
274+
</Tooltip.Trigger>
275+
<Tooltip.Content side='bottom'>
276+
<p>Open in files</p>
277+
</Tooltip.Content>
278+
</Tooltip.Root>
279+
<Tooltip.Root>
280+
<Tooltip.Trigger asChild>
281+
<Button
282+
variant='subtle'
283+
onClick={() => void handleDownload()}
284+
disabled={!file}
285+
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
286+
aria-label='Download file'
287+
>
288+
<Download className={RESOURCE_TAB_ICON_CLASS} />
289+
</Button>
290+
</Tooltip.Trigger>
291+
<Tooltip.Content side='bottom'>
292+
<p>Download</p>
293+
</Tooltip.Content>
294+
</Tooltip.Root>
295+
</>
296+
)
297+
}
298+
233299
interface EmbeddedWorkflowProps {
234300
workspaceId: string
235301
workflowId: string

apps/sim/lib/uploads/utils/file-utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,3 +739,24 @@ export function getViewerUrl(fileKey: string, workspaceId?: string): string | nu
739739

740740
return `/workspace/${resolvedWorkspaceId}/files/${fileKey}/view`
741741
}
742+
743+
/**
744+
* Downloads a workspace file to the user's device via the serve API.
745+
* Fetches the file as a blob and triggers a browser download.
746+
*/
747+
export async function downloadWorkspaceFile(file: { key: string; name: string }): Promise<void> {
748+
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace&t=${Date.now()}`
749+
const response = await fetch(serveUrl, { cache: 'no-store' })
750+
if (!response.ok) {
751+
throw new Error(`Failed to download file: ${response.statusText}`)
752+
}
753+
const blob = await response.blob()
754+
const url = URL.createObjectURL(blob)
755+
const a = document.createElement('a')
756+
a.href = url
757+
a.download = file.name
758+
document.body.appendChild(a)
759+
a.click()
760+
document.body.removeChild(a)
761+
URL.revokeObjectURL(url)
762+
}

0 commit comments

Comments
 (0)