From ae150b440bdf0466d854a5ba536a94c30dccd55e Mon Sep 17 00:00:00 2001
From: Adithya Krishna
Date: Fri, 27 Mar 2026 17:54:04 +0530
Subject: [PATCH 1/2] chore: fix rerenders on files
---
.../components/file-viewer/file-viewer.tsx | 24 +-
.../components/file-viewer/preview-panel.tsx | 21 +-
.../files-list-context-menu.tsx | 5 +-
.../workspace/[workspaceId]/files/files.tsx | 529 +++++++++++-------
4 files changed, 367 insertions(+), 212 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 5075ee61ba..10be5a8770 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
@@ -1,6 +1,6 @@
'use client'
-import { useCallback, useEffect, useRef, useState } from 'react'
+import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Skeleton } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
@@ -183,6 +183,8 @@ function TextEditor({
} = useWorkspaceFileContent(workspaceId, file.id, file.key, file.type === 'text/x-pptxgenjs')
const updateContent = useUpdateWorkspaceFileContent()
+ const updateContentRef = useRef(updateContent)
+ updateContentRef.current = updateContent
const [content, setContent] = useState('')
const [savedContent, setSavedContent] = useState('')
@@ -230,14 +232,14 @@ function TextEditor({
const currentContent = contentRef.current
if (currentContent === savedContentRef.current) return
- await updateContent.mutateAsync({
+ await updateContentRef.current.mutateAsync({
workspaceId,
fileId: file.id,
content: currentContent,
})
setSavedContent(currentContent)
savedContentRef.current = currentContent
- }, [workspaceId, file.id, updateContent])
+ }, [workspaceId, file.id])
const { saveStatus, saveImmediately, isDirty } = useAutosave({
content,
@@ -402,7 +404,7 @@ function TextEditor({
)
}
-function IframePreview({ file }: { file: WorkspaceFileRecord }) {
+const IframePreview = memo(function IframePreview({ file }: { file: WorkspaceFileRecord }) {
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
return (
@@ -417,9 +419,9 @@ function IframePreview({ file }: { file: WorkspaceFileRecord }) {
/>
)
-}
+})
-function ImagePreview({ file }: { file: WorkspaceFileRecord }) {
+const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileRecord }) {
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
return (
@@ -432,7 +434,7 @@ function ImagePreview({ file }: { file: WorkspaceFileRecord }) {
/>
)
-}
+})
const pptxSlideCache = new Map()
@@ -701,7 +703,11 @@ function PptxPreview({
)
}
-function UnsupportedPreview({ file }: { file: WorkspaceFileRecord }) {
+const UnsupportedPreview = memo(function UnsupportedPreview({
+ file,
+}: {
+ file: WorkspaceFileRecord
+}) {
const ext = getFileExtension(file.name)
return (
@@ -714,4 +720,4 @@ function UnsupportedPreview({ file }: { file: WorkspaceFileRecord }) {
)
-}
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx
index 692cb510ac..b3c9666d76 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx
@@ -42,7 +42,12 @@ interface PreviewPanelProps {
isStreaming?: boolean
}
-export function PreviewPanel({ content, mimeType, filename, isStreaming }: PreviewPanelProps) {
+export const PreviewPanel = memo(function PreviewPanel({
+ content,
+ mimeType,
+ filename,
+ isStreaming,
+}: PreviewPanelProps) {
const previewType = resolvePreviewType(mimeType, filename)
if (previewType === 'markdown')
@@ -52,7 +57,7 @@ export function PreviewPanel({ content, mimeType, filename, isStreaming }: Previ
if (previewType === 'svg') return
return null
-}
+})
const REMARK_PLUGINS = [remarkGfm, remarkBreaks]
@@ -197,7 +202,7 @@ const MarkdownPreview = memo(function MarkdownPreview({
)
})
-function HtmlPreview({ content }: { content: string }) {
+const HtmlPreview = memo(function HtmlPreview({ content }: { content: string }) {
return (
)
-}
+})
-function SvgPreview({ content }: { content: string }) {
+const SvgPreview = memo(function SvgPreview({ content }: { content: string }) {
const wrappedContent = useMemo(
() =>
`${content}`,
@@ -227,9 +232,9 @@ function SvgPreview({ content }: { content: string }) {
/>
)
-}
+})
-function CsvPreview({ content }: { content: string }) {
+const CsvPreview = memo(function CsvPreview({ content }: { content: string }) {
const { headers, rows } = useMemo(() => parseCsv(content), [content])
if (headers.length === 0) {
@@ -271,7 +276,7 @@ function CsvPreview({ content }: { content: string }) {
)
-}
+})
function parseCsv(text: string): { headers: string[]; rows: string[][] } {
const lines = text.split('\n').filter((line) => line.trim().length > 0)
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx
index 54346160ef..031213ead7 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx
@@ -1,5 +1,6 @@
'use client'
+import { memo } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -18,7 +19,7 @@ interface FilesListContextMenuProps {
disableUpload?: boolean
}
-export function FilesListContextMenu({
+export const FilesListContextMenu = memo(function FilesListContextMenu({
isOpen,
position,
onClose,
@@ -64,4 +65,4 @@ export function FilesListContextMenu({
)
-}
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
index 23697173f5..f5a14a5436 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useParams, useRouter } from 'next/navigation'
import {
@@ -41,6 +41,7 @@ import type {
HeaderAction,
ResourceColumn,
ResourceRow,
+ SearchConfig,
} from '@/app/workspace/[workspaceId]/components'
import {
InlineRenameInput,
@@ -159,7 +160,18 @@ export function Files() {
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 })
- const [searchTerm, setSearchTerm] = useState('')
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('')
+ const searchTimerRef = useRef>(null)
+ const searchValueRef = useRef('')
+
+ const handleSearchChange = useCallback((value: string) => {
+ searchValueRef.current = value
+ if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
+ searchTimerRef.current = setTimeout(() => {
+ setDebouncedSearchTerm(value)
+ }, 200)
+ }, [])
+
const [creatingFile, setCreatingFile] = useState(false)
const [isDirty, setIsDirty] = useState(false)
const [saveStatus, setSaveStatus] = useState('idle')
@@ -183,58 +195,96 @@ export function Files() {
() => (fileIdFromRoute ? files.find((f) => f.id === fileIdFromRoute) : null),
[fileIdFromRoute, files]
)
+ const selectedFileRef = useRef(selectedFile)
+ selectedFileRef.current = selectedFile
const filteredFiles = useMemo(() => {
- if (!searchTerm) return files
- const q = searchTerm.toLowerCase()
+ if (!debouncedSearchTerm) return files
+ const q = debouncedSearchTerm.toLowerCase()
return files.filter((f) => f.name.toLowerCase().includes(q))
- }, [files, searchTerm])
+ }, [files, debouncedSearchTerm])
- const rows: ResourceRow[] = useMemo(
- () =>
- filteredFiles.map((file) => {
- const Icon = getDocumentIcon(file.type || '', file.name)
- return {
- id: file.id,
- cells: {
- name: {
- icon: ,
- label: file.name,
- content:
- listRename.editingId === file.id ? (
-
-
-
-
-
-
- ) : undefined,
- },
- size: {
- label: formatFileSize(file.size, { includeBytes: true }),
- },
- type: {
- icon: ,
- label: formatFileType(file.type, file.name),
- },
- created: timeCell(file.uploadedAt),
- owner: ownerCell(file.uploadedBy, members),
- updated: timeCell(file.uploadedAt),
+ const rowCacheRef = useRef(
+ new Map()
+ )
+
+ const baseRows: ResourceRow[] = useMemo(() => {
+ const prevCache = rowCacheRef.current
+ const nextCache = new Map<
+ string,
+ { row: ResourceRow; file: WorkspaceFileRecord; members: typeof members }
+ >()
+
+ const result = filteredFiles.map((file) => {
+ const cached = prevCache.get(file.id)
+ if (cached && cached.file === file && cached.members === members) {
+ nextCache.set(file.id, cached)
+ return cached.row
+ }
+ const Icon = getDocumentIcon(file.type || '', file.name)
+ const row: ResourceRow = {
+ id: file.id,
+ cells: {
+ name: {
+ icon: ,
+ label: file.name,
},
- sortValues: {
- size: file.size,
- created: -new Date(file.uploadedAt).getTime(),
- updated: -new Date(file.uploadedAt).getTime(),
+ size: {
+ label: formatFileSize(file.size, { includeBytes: true }),
},
- }
- }),
- [filteredFiles, members, listRename.editingId, listRename.editValue]
- )
+ type: {
+ icon: ,
+ label: formatFileType(file.type, file.name),
+ },
+ created: timeCell(file.uploadedAt),
+ owner: ownerCell(file.uploadedBy, members),
+ updated: timeCell(file.uploadedAt),
+ },
+ sortValues: {
+ size: file.size,
+ created: -new Date(file.uploadedAt).getTime(),
+ updated: -new Date(file.uploadedAt).getTime(),
+ },
+ }
+ nextCache.set(file.id, { row, file, members })
+ return row
+ })
+
+ rowCacheRef.current = nextCache
+ return result
+ }, [filteredFiles, members])
+
+ const rows: ResourceRow[] = useMemo(() => {
+ if (!listRename.editingId) return baseRows
+ return baseRows.map((row) => {
+ if (row.id !== listRename.editingId) return row
+ const file = filteredFiles.find((f) => f.id === row.id)
+ if (!file) return row
+ const Icon = getDocumentIcon(file.type || '', file.name)
+ return {
+ ...row,
+ cells: {
+ ...row.cells,
+ name: {
+ ...row.cells.name,
+ content: (
+
+
+
+
+
+
+ ),
+ },
+ },
+ }
+ })
+ }, [baseRows, listRename.editingId, listRename.editValue, filteredFiles])
const handleFileChange = useCallback(
async (e: React.ChangeEvent) => {
@@ -288,8 +338,13 @@ export function Files() {
}
}, [])
+ const deleteTargetFileRef = useRef(deleteTargetFile)
+ deleteTargetFileRef.current = deleteTargetFile
+ const fileIdFromRouteRef = useRef(fileIdFromRoute)
+ fileIdFromRouteRef.current = fileIdFromRoute
+
const handleDelete = useCallback(async () => {
- const target = deleteTargetFile
+ const target = deleteTargetFileRef.current
if (!target) return
try {
@@ -299,7 +354,7 @@ export function Files() {
})
setShowDeleteConfirm(false)
setDeleteTargetFile(null)
- if (fileIdFromRoute === target.id) {
+ if (fileIdFromRouteRef.current === target.id) {
setIsDirty(false)
setSaveStatus('idle')
router.push(`/workspace/${workspaceId}/files`)
@@ -307,36 +362,44 @@ export function Files() {
} catch (err) {
logger.error('Failed to delete file:', err)
}
- }, [deleteTargetFile, workspaceId, fileIdFromRoute, router])
+ }, [workspaceId, router])
+
+ const isDirtyRef = useRef(isDirty)
+ isDirtyRef.current = isDirty
+ const saveStatusRef = useRef(saveStatus)
+ saveStatusRef.current = saveStatus
const handleSave = useCallback(async () => {
- if (!saveRef.current || !isDirty || saveStatus === 'saving') return
+ if (!saveRef.current || !isDirtyRef.current || saveStatusRef.current === 'saving') return
await saveRef.current()
- }, [isDirty, saveStatus])
+ }, [])
const handleBackAttempt = useCallback(() => {
- if (isDirty) {
+ if (isDirtyRef.current) {
setShowUnsavedChangesAlert(true)
} else {
setPreviewMode('editor')
router.push(`/workspace/${workspaceId}/files`)
}
- }, [isDirty, router, workspaceId])
+ }, [router, workspaceId])
const handleStartHeaderRename = useCallback(() => {
- if (selectedFile) headerRename.startRename(selectedFile.id, selectedFile.name)
- }, [selectedFile, headerRename.startRename])
+ const file = selectedFileRef.current
+ if (file) headerRename.startRename(file.id, file.name)
+ }, [headerRename.startRename])
const handleDownloadSelected = useCallback(() => {
- if (selectedFile) handleDownload(selectedFile)
- }, [selectedFile, handleDownload])
+ const file = selectedFileRef.current
+ if (file) handleDownload(file)
+ }, [handleDownload])
const handleDeleteSelected = useCallback(() => {
- if (selectedFile) {
- setDeleteTargetFile(selectedFile)
+ const file = selectedFileRef.current
+ if (file) {
+ setDeleteTargetFile(file)
setShowDeleteConfirm(true)
}
- }, [selectedFile])
+ }, [])
const fileDetailBreadcrumbs = useMemo(
() =>
@@ -379,9 +442,6 @@ export function Files() {
handleBackAttempt,
headerRename.editingId,
headerRename.editValue,
- headerRename.setEditValue,
- headerRename.submitRename,
- headerRename.cancelRename,
handleStartHeaderRename,
handleDownloadSelected,
handleDeleteSelected,
@@ -396,12 +456,15 @@ export function Files() {
router.push(`/workspace/${workspaceId}/files`)
}, [router, workspaceId])
+ const creatingFileRef = useRef(creatingFile)
+ creatingFileRef.current = creatingFile
+
const handleCreateFile = useCallback(async () => {
- if (creatingFile) return
+ if (creatingFileRef.current) return
setCreatingFile(true)
try {
- const existingNames = new Set(files.map((f) => f.name))
+ const existingNames = new Set(filesRef.current.map((f) => f.name))
let name = 'untitled.md'
let counter = 1
while (existingNames.has(name)) {
@@ -423,42 +486,49 @@ export function Files() {
} finally {
setCreatingFile(false)
}
- }, [creatingFile, files, workspaceId, router])
+ }, [workspaceId, router])
const handleRowContextMenu = useCallback(
(e: React.MouseEvent, rowId: string) => {
- const file = files.find((f) => f.id === rowId)
+ const file = filesRef.current.find((f) => f.id === rowId)
if (file) {
setContextMenuFile(file)
openContextMenu(e)
}
},
- [files, openContextMenu]
+ [openContextMenu]
)
+ const contextMenuFileRef = useRef(contextMenuFile)
+ contextMenuFileRef.current = contextMenuFile
+
const handleContextMenuOpen = useCallback(() => {
- if (!contextMenuFile) return
- router.push(`/workspace/${workspaceId}/files/${contextMenuFile.id}`)
+ const file = contextMenuFileRef.current
+ if (!file) return
+ router.push(`/workspace/${workspaceId}/files/${file.id}`)
closeContextMenu()
- }, [contextMenuFile, closeContextMenu, router, workspaceId])
+ }, [closeContextMenu, router, workspaceId])
const handleContextMenuDownload = useCallback(() => {
- if (!contextMenuFile) return
- handleDownload(contextMenuFile)
+ const file = contextMenuFileRef.current
+ if (!file) return
+ handleDownload(file)
closeContextMenu()
- }, [contextMenuFile, handleDownload, closeContextMenu])
+ }, [handleDownload, closeContextMenu])
const handleContextMenuRename = useCallback(() => {
- if (contextMenuFile) listRename.startRename(contextMenuFile.id, contextMenuFile.name)
+ const file = contextMenuFileRef.current
+ if (file) listRename.startRename(file.id, file.name)
closeContextMenu()
- }, [contextMenuFile, listRename.startRename, closeContextMenu])
+ }, [listRename.startRename, closeContextMenu])
const handleContextMenuDelete = useCallback(() => {
- if (!contextMenuFile) return
- setDeleteTargetFile(contextMenuFile)
+ const file = contextMenuFileRef.current
+ if (!file) return
+ setDeleteTargetFile(file)
setShowDeleteConfirm(true)
closeContextMenu()
- }, [contextMenuFile, closeContextMenu])
+ }, [closeContextMenu])
const handleContentContextMenu = useCallback(
(e: React.MouseEvent) => {
@@ -479,41 +549,46 @@ export function Files() {
closeListContextMenu()
}, [closeListContextMenu])
- useEffect(() => {
+ const prevFileIdRef = useRef(fileIdFromRoute)
+ if (fileIdFromRoute !== prevFileIdRef.current) {
+ prevFileIdRef.current = fileIdFromRoute
const isJustCreated =
fileIdFromRoute != null && justCreatedFileIdRef.current === fileIdFromRoute
if (justCreatedFileIdRef.current && !isJustCreated) {
justCreatedFileIdRef.current = null
}
- if (isJustCreated) {
- setPreviewMode('editor')
- } else {
- const file = fileIdFromRoute ? filesRef.current.find((f) => f.id === fileIdFromRoute) : null
- const canPreview = file ? isPreviewable(file) : false
- setPreviewMode(canPreview ? 'preview' : 'editor')
+ const nextMode: PreviewMode = isJustCreated
+ ? 'editor'
+ : (() => {
+ const file = fileIdFromRoute
+ ? filesRef.current.find((f) => f.id === fileIdFromRoute)
+ : null
+ return file && isPreviewable(file) ? 'preview' : 'editor'
+ })()
+ if (nextMode !== previewMode) {
+ setPreviewMode(nextMode)
}
- }, [fileIdFromRoute])
+ }
useEffect(() => {
- if (!selectedFile) return
const handleKeyDown = (e: KeyboardEvent) => {
+ if (!fileIdFromRouteRef.current) return
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault()
handleSave()
}
}
- window.addEventListener('keydown', handleKeyDown)
- return () => window.removeEventListener('keydown', handleKeyDown)
- }, [selectedFile, handleSave])
-
- useEffect(() => {
- if (!isDirty) return
- const handler = (e: BeforeUnloadEvent) => {
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ if (!isDirtyRef.current) return
e.preventDefault()
}
- window.addEventListener('beforeunload', handler)
- return () => window.removeEventListener('beforeunload', handler)
- }, [isDirty])
+ window.addEventListener('keydown', handleKeyDown)
+ window.addEventListener('beforeunload', handleBeforeUnload)
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown)
+ window.removeEventListener('beforeunload', handleBeforeUnload)
+ }
+ }, [handleSave])
const handleCyclePreviewMode = useCallback(() => {
setPreviewMode((prev) => {
@@ -592,27 +667,89 @@ export function Files() {
selectedFile,
saveStatus,
previewMode,
+ isDirty,
handleCyclePreviewMode,
handleTogglePreview,
handleSave,
- isDirty,
handleDownloadSelected,
handleDeleteSelected,
])
+ /** Stable refs for values used in callbacks to avoid dependency churn */
+ const listRenameRef = useRef(listRename)
+ listRenameRef.current = listRename
+ const headerRenameRef = useRef(headerRename)
+ headerRenameRef.current = headerRename
+
+ const handleRowClick = useCallback(
+ (id: string) => {
+ if (listRenameRef.current.editingId !== id && !headerRenameRef.current.editingId) {
+ router.push(`/workspace/${workspaceId}/files/${id}`)
+ }
+ },
+ [router, workspaceId]
+ )
+
+ const handleUploadClick = useCallback(() => {
+ fileInputRef.current?.click()
+ }, [])
+
+ const canEdit = userPermissions.canEdit === true
+
+ const searchConfig: SearchConfig = useMemo(
+ () => ({
+ get value() {
+ return searchValueRef.current
+ },
+ onChange: handleSearchChange,
+ placeholder: 'Search files...',
+ }),
+ [handleSearchChange]
+ )
+
+ const createConfig = useMemo(
+ () => ({
+ label: 'New file',
+ onClick: handleCreateFile,
+ disabled: uploading || creatingFile || !canEdit,
+ }),
+ [handleCreateFile, uploading, creatingFile, canEdit]
+ )
+
+ const uploadButtonLabel = useMemo(
+ () =>
+ uploading && uploadProgress.total > 0
+ ? `${uploadProgress.completed}/${uploadProgress.total}`
+ : uploading
+ ? 'Uploading...'
+ : 'Upload',
+ [uploading, uploadProgress.completed, uploadProgress.total]
+ )
+
+ const headerActionsConfig = useMemo(
+ () => [
+ {
+ label: uploadButtonLabel,
+ icon: Upload,
+ onClick: handleUploadClick,
+ },
+ ],
+ [uploadButtonLabel, handleUploadClick]
+ )
+
+ const handleNavigateToFiles = useCallback(() => {
+ router.push(`/workspace/${workspaceId}/files`)
+ }, [router, workspaceId])
+
+ const loadingBreadcrumbs = useMemo(
+ () => [{ label: 'Files', onClick: handleNavigateToFiles }, { label: '...' }],
+ [handleNavigateToFiles]
+ )
+
if (fileIdFromRoute && !selectedFile) {
return (
-
router.push(`/workspace/${workspaceId}/files`),
- },
- { label: '...' },
- ]}
- />
+
@@ -633,7 +770,7 @@ export function Files() {
key={selectedFile.id}
file={selectedFile}
workspaceId={workspaceId}
- canEdit={userPermissions.canEdit === true}
+ canEdit={canEdit}
previewMode={previewMode}
autoFocus={justCreatedFileIdRef.current === selectedFile.id}
onDirtyChange={setIsDirty}
@@ -672,43 +809,18 @@ export function Files() {
)
}
- const uploadButtonLabel =
- uploading && uploadProgress.total > 0
- ? `${uploadProgress.completed}/${uploadProgress.total}`
- : uploading
- ? 'Uploading...'
- : 'Upload'
-
return (
<>
fileInputRef.current?.click(),
- },
- ]}
+ headerActions={headerActionsConfig}
columns={COLUMNS}
rows={rows}
- onRowClick={(id) => {
- if (listRename.editingId !== id && !headerRename.editingId) {
- router.push(`/workspace/${workspaceId}/files/${id}`)
- }
- }}
+ onRowClick={handleRowClick}
onRowContextMenu={handleRowContextMenu}
isLoading={isLoading}
onContextMenu={handleContentContextMenu}
@@ -720,58 +832,20 @@ export function Files() {
onClose={closeListContextMenu}
onCreateFile={handleCreateFile}
onUploadFile={handleListUploadFile}
- disableCreate={uploading || creatingFile || userPermissions.canEdit !== true}
- disableUpload={uploading || userPermissions.canEdit !== true}
+ disableCreate={uploading || creatingFile || !canEdit}
+ disableUpload={uploading || !canEdit}
/>
- !open && closeContextMenu()}
- modal={false}
- >
-
-
-
- e.preventDefault()}
- >
-
-
- Open
-
-
-
- Download
-
- {userPermissions.canEdit === true && (
- <>
-
-
-
- Rename
-
-
-
- Delete
-
- >
- )}
-
-
+
void
+ onOpen: () => void
+ onDownload: () => void
+ onRename: () => void
+ onDelete: () => void
+ canEdit: boolean
+}
+
+const FileRowContextMenu = memo(function FileRowContextMenu({
+ isOpen,
+ position,
+ onClose,
+ onOpen,
+ onDownload,
+ onRename,
+ onDelete,
+ canEdit,
+}: FileRowContextMenuProps) {
+ return (
+ !open && onClose()} modal={false}>
+
+
+
+ e.preventDefault()}
+ >
+
+
+ Open
+
+
+
+ Download
+
+ {canEdit && (
+ <>
+
+
+
+ Rename
+
+
+
+ Delete
+
+ >
+ )}
+
+
+ )
+})
+
interface DeleteConfirmModalProps {
open: boolean
onOpenChange: (open: boolean) => void
@@ -802,7 +945,7 @@ interface DeleteConfirmModalProps {
isPending: boolean
}
-function DeleteConfirmModal({
+const DeleteConfirmModal = memo(function DeleteConfirmModal({
open,
onOpenChange,
fileName,
@@ -833,4 +976,4 @@ function DeleteConfirmModal({
)
-}
+})
From 3750f6e99d99cb2491e6e63835f8fdfa5c07c3fb Mon Sep 17 00:00:00 2001
From: Adithya Krishna
Date: Fri, 27 Mar 2026 18:24:57 +0530
Subject: [PATCH 2/2] chore: fix review changes
---
.../workspace/[workspaceId]/files/files.tsx | 34 ++++++++++++++-----
1 file changed, 26 insertions(+), 8 deletions(-)
diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
index f5a14a5436..221b658c4d 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
@@ -160,12 +160,12 @@ export function Files() {
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 })
+ const [inputValue, setInputValue] = useState('')
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('')
const searchTimerRef = useRef>(null)
- const searchValueRef = useRef('')
const handleSearchChange = useCallback((value: string) => {
- searchValueRef.current = value
+ setInputValue(value)
if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
searchTimerRef.current = setTimeout(() => {
setDebouncedSearchTerm(value)
@@ -175,7 +175,14 @@ export function Files() {
const [creatingFile, setCreatingFile] = useState(false)
const [isDirty, setIsDirty] = useState(false)
const [saveStatus, setSaveStatus] = useState('idle')
- const [previewMode, setPreviewMode] = useState('preview')
+ const [previewMode, setPreviewMode] = useState(() => {
+ if (fileIdFromRoute) {
+ const file = files.find((f) => f.id === fileIdFromRoute)
+ if (file && isPreviewable(file)) return 'preview'
+ return 'editor'
+ }
+ return 'preview'
+ })
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [contextMenuFile, setContextMenuFile] = useState(null)
@@ -284,7 +291,15 @@ export function Files() {
},
}
})
- }, [baseRows, listRename.editingId, listRename.editValue, filteredFiles])
+ }, [
+ baseRows,
+ listRename.editingId,
+ listRename.editValue,
+ listRename.setEditValue,
+ listRename.submitRename,
+ listRename.cancelRename,
+ filteredFiles,
+ ])
const handleFileChange = useCallback(
async (e: React.ChangeEvent) => {
@@ -696,15 +711,18 @@ export function Files() {
const canEdit = userPermissions.canEdit === true
+ const handleSearchClearAll = useCallback(() => {
+ handleSearchChange('')
+ }, [handleSearchChange])
+
const searchConfig: SearchConfig = useMemo(
() => ({
- get value() {
- return searchValueRef.current
- },
+ value: inputValue,
onChange: handleSearchChange,
+ onClearAll: handleSearchClearAll,
placeholder: 'Search files...',
}),
- [handleSearchChange]
+ [inputValue, handleSearchChange, handleSearchClearAll]
)
const createConfig = useMemo(