From 6fc7ab42635293ded749e1af3b28713ec00adce8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 4 Apr 2026 19:15:55 -0700 Subject: [PATCH 01/11] feat(files): expand file editor to support more formats, add docx/xlsx preview --- .../components/file-viewer/file-viewer.tsx | 363 ++++++++++++++++-- 1 file changed, 338 insertions(+), 25 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index fe8660aa464..5ff0991487f 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -29,11 +29,22 @@ const TEXT_EDITABLE_MIME_TYPES = new Set([ 'application/x-yaml', 'text/csv', 'text/html', + 'text/xml', + 'application/xml', + 'text/css', + 'text/javascript', + 'application/javascript', + 'application/typescript', + 'application/toml', + 'text/x-python', + 'text/x-sh', + 'text/x-sql', 'image/svg+xml', ]) const TEXT_EDITABLE_EXTENSIONS = new Set([ 'md', + 'mdx', 'txt', 'json', 'yaml', @@ -42,6 +53,48 @@ const TEXT_EDITABLE_EXTENSIONS = new Set([ 'html', 'htm', 'svg', + 'xml', + 'css', + 'scss', + 'less', + 'js', + 'jsx', + 'ts', + 'tsx', + 'py', + 'rb', + 'go', + 'rs', + 'java', + 'kt', + 'swift', + 'c', + 'cpp', + 'h', + 'hpp', + 'cs', + 'php', + 'sh', + 'bash', + 'zsh', + 'fish', + 'sql', + 'graphql', + 'gql', + 'toml', + 'ini', + 'conf', + 'cfg', + 'env', + 'log', + 'diff', + 'patch', + 'dockerfile', + 'makefile', + 'gitignore', + 'editorconfig', + 'prettierrc', + 'eslintrc', ]) const IFRAME_PREVIEWABLE_MIME_TYPES = new Set(['application/pdf']) @@ -55,11 +108,23 @@ const PPTX_PREVIEWABLE_MIME_TYPES = new Set([ ]) const PPTX_PREVIEWABLE_EXTENSIONS = new Set(['pptx']) +const DOCX_PREVIEWABLE_MIME_TYPES = new Set([ + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', +]) +const DOCX_PREVIEWABLE_EXTENSIONS = new Set(['docx']) + +const XLSX_PREVIEWABLE_MIME_TYPES = new Set([ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', +]) +const XLSX_PREVIEWABLE_EXTENSIONS = new Set(['xlsx']) + type FileCategory = | 'text-editable' | 'iframe-previewable' | 'image-previewable' | 'pptx-previewable' + | 'docx-previewable' + | 'xlsx-previewable' | 'unsupported' function resolveFileCategory(mimeType: string | null, filename: string): FileCategory { @@ -67,12 +132,16 @@ function resolveFileCategory(mimeType: string | null, filename: string): FileCat if (mimeType && IFRAME_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'iframe-previewable' if (mimeType && IMAGE_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'image-previewable' if (mimeType && PPTX_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'pptx-previewable' + if (mimeType && DOCX_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'docx-previewable' + if (mimeType && XLSX_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'xlsx-previewable' const ext = getFileExtension(filename) if (TEXT_EDITABLE_EXTENSIONS.has(ext)) return 'text-editable' if (IFRAME_PREVIEWABLE_EXTENSIONS.has(ext)) return 'iframe-previewable' if (IMAGE_PREVIEWABLE_EXTENSIONS.has(ext)) return 'image-previewable' if (PPTX_PREVIEWABLE_EXTENSIONS.has(ext)) return 'pptx-previewable' + if (DOCX_PREVIEWABLE_EXTENSIONS.has(ext)) return 'docx-previewable' + if (XLSX_PREVIEWABLE_EXTENSIONS.has(ext)) return 'xlsx-previewable' return 'unsupported' } @@ -142,6 +211,14 @@ export function FileViewer({ return } + if (category === 'docx-previewable') { + return + } + + if (category === 'xlsx-previewable') { + return + } + return } @@ -339,16 +416,7 @@ function TextEditor({ }, [isStreaming, revealedContent]) if (streamingContent === undefined) { - if (isLoading) { - return ( -
- - - - -
- ) - } + if (isLoading) return DOCUMENT_SKELETON if (error) { return ( @@ -551,6 +619,32 @@ const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileR ) }) +function resolvePreviewError( + fetchError: Error | null, + renderError: string | null +): string | null { + if (fetchError) return fetchError instanceof Error ? fetchError.message : 'Failed to load file' + return renderError +} + +function PreviewError({ label, error }: { label: string; error: string }) { + return ( +
+

Failed to preview {label}

+

{error}

+
+ ) +} + +const DOCUMENT_SKELETON = ( +
+ + + + +
+) + const pptxSlideCache = new Map() function pptxCacheKey(fileId: string, dataUpdatedAt: number, byteLength: number): string { @@ -769,23 +863,10 @@ function PptxPreview({ } }, [fileData, dataUpdatedAt, streamingContent, cacheKey, workspaceId]) - const error = fetchError - ? fetchError instanceof Error - ? fetchError.message - : 'Failed to load file' - : renderError + const error = resolvePreviewError(fetchError, renderError) const loading = isFetching || rendering - if (error) { - return ( -
-

- Failed to preview presentation -

-

{error}

-
- ) - } + if (error) return if (loading && slides.length === 0) { return ( @@ -826,6 +907,238 @@ function toggleMarkdownCheckbox(markdown: string, targetIndex: number, checked: }) } +const DocxPreview = memo(function DocxPreview({ + file, + workspaceId, +}: { + file: WorkspaceFileRecord + workspaceId: string +}) { + const { + data: fileData, + isLoading, + error: fetchError, + } = useWorkspaceFileBinary(workspaceId, file.id, file.key) + + const [html, setHtml] = useState(null) + const [renderError, setRenderError] = useState(null) + + useEffect(() => { + if (!fileData) return + + let cancelled = false + + async function convert() { + try { + setRenderError(null) + const mammoth = await import('mammoth') + const result = await mammoth.convertToHtml({ arrayBuffer: fileData }) + if (!cancelled) setHtml(result.value) + } catch (err) { + if (!cancelled) { + const msg = err instanceof Error ? err.message : 'Failed to render document' + logger.error('DOCX render failed', { error: msg }) + setRenderError(msg) + } + } + } + + convert() + return () => { + cancelled = true + } + }, [fileData]) + + const error = resolvePreviewError(fetchError, renderError) + if (error) return + if (isLoading || html === null) return DOCUMENT_SKELETON + + return ( +
+