Skip to content

Commit aa5a7bf

Browse files
committed
fix(files): render image previews inline
1 parent 49593b3 commit aa5a7bf

3 files changed

Lines changed: 47 additions & 9 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.test.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { act } from 'react'
55
import { createRoot, type Root } from 'react-dom/client'
66
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
77
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
8+
import {
9+
createWorkspaceFileContentSource,
10+
FileContentSourceProvider,
11+
} from '@/hooks/use-file-content-source'
812
import { ImagePreview } from './image-preview'
913

1014
const file = {
@@ -43,7 +47,13 @@ afterEach(() => {
4347
})
4448

4549
function render(record: WorkspaceFileRecord = file) {
46-
act(() => root.render(<ImagePreview file={record} />))
50+
act(() =>
51+
root.render(
52+
<FileContentSourceProvider value={createWorkspaceFileContentSource(record.workspaceId)}>
53+
<ImagePreview file={record} />
54+
</FileContentSourceProvider>
55+
)
56+
)
4757
}
4858

4959
describe('ImagePreview', () => {
@@ -55,6 +65,13 @@ describe('ImagePreview', () => {
5565
expect(src).not.toContain('raw=1')
5666
})
5767

68+
it('streams browser-renderable images through the workspace inline endpoint', () => {
69+
render({ ...file, name: 'photo.png', key: 'workspace/ws-1/photo.png', type: 'image/png' })
70+
71+
const src = container.querySelector('img')?.getAttribute('src') ?? ''
72+
expect(src).toBe('/api/workspaces/ws-1/files/inline?key=workspace%2Fws-1%2Fphoto.png')
73+
})
74+
5875
it('falls back to the unsupported state when the image fails to decode', () => {
5976
render()
6077

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import { ZoomablePreview } from './zoomable-preview'
88

99
export const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileRecord }) {
1010
const source = useFileContentSource()
11-
/** `v` busts the browser cache across content writes; `preview` lets the server
12-
* substitute a renderable JPEG for a HEIC. */
13-
const serveUrl = source.buildUrl(file.key, {
11+
/** Workspace images use their content-addressed inline URL. Derivative-backed
12+
* sources use the version and preview flag to render formats such as HEIC. */
13+
const serveUrl = source.buildImageUrl(file, {
1414
version: Number(new Date(file.updatedAt)) || file.size,
1515
preview: true,
1616
})

apps/sim/hooks/use-file-content-source.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export interface ImageDimensionsSource {
5656
*/
5757
export interface FileContentSource {
5858
buildUrl: (key: string, opts?: FileContentUrlOptions) => string
59+
buildImageUrl: (
60+
file: { key: string; name: string; type: string },
61+
opts?: FileContentUrlOptions
62+
) => string
5963
/**
6064
* Map an embedded image `src` to a display URL scoped to the current context: the in-app source
6165
* points at the workspace-scoped inline route, the public source at the token-scoped cascade route.
@@ -81,7 +85,7 @@ function buildServeUrl(key: string, opts?: FileContentUrlOptions): string {
8185
function inlineImageSource(
8286
buildUrl: FileContentSource['buildUrl'],
8387
inlineBase: string
84-
): FileContentSource {
88+
): Pick<FileContentSource, 'buildUrl' | 'resolveImageSrc'> {
8589
return {
8690
buildUrl,
8791
resolveImageSrc: (src) => {
@@ -103,6 +107,16 @@ export function createWorkspaceFileContentSource(
103107
): FileContentSource {
104108
return {
105109
...inlineImageSource(buildServeUrl, `/api/workspaces/${workspaceId}/files/inline`),
110+
buildImageUrl: (file, opts) => {
111+
const heic =
112+
file.type === 'image/heic' ||
113+
file.type === 'image/heif' ||
114+
/\.(?:heic|heif)$/i.test(file.name)
115+
if (heic) return buildServeUrl(file.key, { ...opts, preview: true })
116+
117+
const params = new URLSearchParams({ key: file.key })
118+
return `/api/workspaces/${encodeURIComponent(workspaceId)}/files/inline?${params}`
119+
},
106120
...imageDimensions,
107121
}
108122
}
@@ -116,11 +130,17 @@ export function createPublicFileContentSource(
116130
token: string,
117131
contentUrl: string
118132
): FileContentSource {
119-
return inlineImageSource(
120-
(_key, opts) =>
133+
return {
134+
...inlineImageSource(
135+
(_key, opts) =>
136+
opts?.preview
137+
? `${contentUrl}${contentUrl.includes('?') ? '&' : '?'}preview=1`
138+
: contentUrl,
139+
`/api/files/public/${token}/inline`
140+
),
141+
buildImageUrl: (_file, opts) =>
121142
opts?.preview ? `${contentUrl}${contentUrl.includes('?') ? '&' : '?'}preview=1` : contentUrl,
122-
`/api/files/public/${token}/inline`
123-
)
143+
}
124144
}
125145

126146
/**
@@ -130,6 +150,7 @@ export function createPublicFileContentSource(
130150
*/
131151
export const workspaceFileContentSource: FileContentSource = {
132152
buildUrl: buildServeUrl,
153+
buildImageUrl: (file, opts) => buildServeUrl(file.key, { ...opts, preview: true }),
133154
resolveImageSrc: (src) => src,
134155
}
135156

0 commit comments

Comments
 (0)