mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
refactor(files): cleanup anti-patterns across file viewer components
Six-pass cleanup over the file-viewer directory:
Effects (you-might-not-need-an-effect):
- AudioPreview, VideoPreview: replace reset useEffect with key={file.id} so
the component remounts on file change — React's canonical solution
- DocxPreview: same key-prop fix; removes a 5-setState reset effect that was
also clearing containerRef.current.innerHTML unnecessarily
Callbacks (you-might-not-need-a-callback):
- handleEditorMount, handleEditorChange: remove useCallback — MonacoEditor is
dynamic(), not React.memo, so reference stability has no observer
- markSavedContent: remove useCallback — called only through an onSaveRef,
never directly observed
- DataTable.setInputRef: remove useCallback — callback refs on native elements
are called regardless of reference identity
Design tokens (emcn-design-review):
- VideoPreview: bg-black → bg-[var(--surface-inverted)]
- HtmlPreview iframe: bg-white → bg-[var(--surface-2)]
useMemo, useState, and react-query passes found no issues.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, memo, useCallback, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { forwardRef, memo, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
interface EditConfig {
|
||||
@@ -49,12 +49,12 @@ const DataTableBase = forwardRef<DataTableHandle, DataTableProps>(function DataT
|
||||
[]
|
||||
)
|
||||
|
||||
const setInputRef = useCallback((node: HTMLInputElement | null) => {
|
||||
const setInputRef = (node: HTMLInputElement | null) => {
|
||||
if (node) {
|
||||
node.focus()
|
||||
node.select()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
const startEdit = (row: number, col: number, currentValue: string) => {
|
||||
if (!editConfig) return
|
||||
|
||||
@@ -521,9 +521,9 @@ function useTextEditorContentState(options: SyncTextEditorContentStateOptions) {
|
||||
dispatch({ type: 'edit', content })
|
||||
}, [])
|
||||
|
||||
const markSavedContent = useCallback((content: string) => {
|
||||
const markSavedContent = (content: string) => {
|
||||
dispatch({ type: 'save-success', content })
|
||||
}, [])
|
||||
}
|
||||
|
||||
return {
|
||||
content: state.content,
|
||||
@@ -596,15 +596,22 @@ export function FileViewer({
|
||||
}
|
||||
|
||||
if (category === 'audio-previewable') {
|
||||
return <AudioPreview file={file} workspaceId={workspaceId} />
|
||||
return <AudioPreview key={file.id} file={file} workspaceId={workspaceId} />
|
||||
}
|
||||
|
||||
if (category === 'video-previewable') {
|
||||
return <VideoPreview file={file} workspaceId={workspaceId} />
|
||||
return <VideoPreview key={file.id} file={file} workspaceId={workspaceId} />
|
||||
}
|
||||
|
||||
if (category === 'docx-previewable') {
|
||||
return <DocxPreview file={file} workspaceId={workspaceId} streamingContent={streamingContent} />
|
||||
return (
|
||||
<DocxPreview
|
||||
key={file.id}
|
||||
file={file}
|
||||
workspaceId={workspaceId}
|
||||
streamingContent={streamingContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (category === 'pptx-previewable') {
|
||||
@@ -704,7 +711,6 @@ function TextEditor({
|
||||
})
|
||||
contentRef.current = content
|
||||
|
||||
// Sync external content (initial load + streaming) to Monaco model
|
||||
useEffect(() => {
|
||||
const editor = monacoEditorRef.current
|
||||
if (!editor) return
|
||||
@@ -713,9 +719,6 @@ function TextEditor({
|
||||
const monacoValue = model.getValue()
|
||||
if (monacoValue === content) return
|
||||
|
||||
// Only override Monaco when we're pushing external content, not user edits:
|
||||
// - During streaming/reconciling: always push
|
||||
// - On first init (monacoValue matches last synced value): push
|
||||
if (isStreamInteractionLocked || monacoValue === lastSyncedContentRef.current) {
|
||||
model.setValue(content)
|
||||
lastSyncedContentRef.current = content
|
||||
@@ -825,7 +828,6 @@ function TextEditor({
|
||||
const toggled = toggleMarkdownCheckbox(content, checkboxIndex, checked)
|
||||
if (toggled !== content) {
|
||||
setDraftContent(toggled)
|
||||
// Also update Monaco synchronously so the user sees the change
|
||||
const model = monacoEditorRef.current?.getModel()
|
||||
if (model) {
|
||||
model.setValue(toggled)
|
||||
@@ -836,7 +838,7 @@ function TextEditor({
|
||||
[content, setDraftContent]
|
||||
)
|
||||
|
||||
const handleEditorMount: OnMount = useCallback((editor, monaco) => {
|
||||
const handleEditorMount: OnMount = (editor, monaco) => {
|
||||
monacoEditorRef.current = editor
|
||||
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||||
@@ -854,14 +856,11 @@ function TextEditor({
|
||||
hasAutoFocusedRef.current = true
|
||||
editor.focus()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
setDraftContent(value ?? '')
|
||||
},
|
||||
[setDraftContent]
|
||||
)
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
setDraftContent(value ?? '')
|
||||
}
|
||||
|
||||
const isStreaming = isStreamInteractionLocked
|
||||
const isEditorReadOnly = isStreamInteractionLocked || !canEdit
|
||||
@@ -1221,10 +1220,6 @@ const AudioPreview = memo(function AudioPreview({
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
replaceBlobUrl(null)
|
||||
}, [file.id, file.key, replaceBlobUrl])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (blobUrlRef.current) {
|
||||
@@ -1290,10 +1285,6 @@ const VideoPreview = memo(function VideoPreview({
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
replaceBlobUrl(null)
|
||||
}, [file.id, file.key, replaceBlobUrl])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (blobUrlRef.current) {
|
||||
@@ -1320,7 +1311,7 @@ const VideoPreview = memo(function VideoPreview({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-full items-center justify-center bg-black'>
|
||||
<div className='flex h-full items-center justify-center bg-[var(--surface-inverted)]'>
|
||||
{blobUrl && (
|
||||
// biome-ignore lint/a11y/useMediaCaption: video from workspace files
|
||||
<video src={blobUrl} controls className='max-h-full max-w-full' />
|
||||
@@ -1440,16 +1431,6 @@ const DocxPreview = memo(function DocxPreview({
|
||||
const [rendering, setRendering] = useState(false)
|
||||
const [hasRenderedPreview, setHasRenderedPreview] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
lastSuccessfulHtmlRef.current = ''
|
||||
setRenderError(null)
|
||||
setRendering(false)
|
||||
setHasRenderedPreview(false)
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = ''
|
||||
}
|
||||
}, [file.id, file.key])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !fileData || streamingContent !== undefined) return
|
||||
|
||||
@@ -2019,7 +2000,6 @@ const XlsxPreview = memo(function XlsxPreview({
|
||||
)
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
// Commit any in-progress cell edit before reading the workbook
|
||||
dataTableRef.current?.commitEdit()
|
||||
const wb = workbookRef.current
|
||||
if (!wb || isSavingRef.current) return
|
||||
@@ -2033,7 +2013,6 @@ const XlsxPreview = memo(function XlsxPreview({
|
||||
const binary: number[] = XLSX.write(wb, { type: 'array', bookType: 'xlsx' })
|
||||
const bytes = new Uint8Array(binary)
|
||||
|
||||
// Convert to base64 in chunks to avoid call stack overflow
|
||||
const chunkSize = 8192
|
||||
const parts: string[] = []
|
||||
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||
|
||||
@@ -851,7 +851,7 @@ const HtmlPreview = memo(function HtmlPreview({ content }: { content: string })
|
||||
sandbox='allow-scripts'
|
||||
referrerPolicy='no-referrer'
|
||||
title='HTML Preview'
|
||||
className='h-full w-full border-0 bg-white'
|
||||
className='h-full w-full border-0 bg-[var(--surface-2)]'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user