Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3250264
feat(files): extract PDF viewer behind SSR boundary and polish file p…
waleedlatif1 Apr 28, 2026
4d3da79
fix(files): replace instanceof Error checks with toError() and fix sk…
waleedlatif1 Apr 28, 2026
809c9d4
fix(files): address PR review findings
waleedlatif1 Apr 28, 2026
e794eeb
chore(files): revert accidental pptxgenjs.cjs re-minification
waleedlatif1 Apr 28, 2026
a05542a
fix(files): fix Monaco stale closure, XLSX Ctrl+S data loss, and asyn…
waleedlatif1 Apr 28, 2026
9f34a27
refactor(files): cleanup anti-patterns across file viewer components
waleedlatif1 Apr 28, 2026
773992b
improvement(files): replace stock Monaco theme with Sim design system…
waleedlatif1 Apr 28, 2026
d37a74b
fix(files): bump light theme comment color to #888888 for WCAG contrast
waleedlatif1 Apr 28, 2026
7de86d6
fix(files): fix dark mode comment contrast #4a4a4a → #606060 (~1.9:1 …
waleedlatif1 Apr 28, 2026
638dfc3
improvement(files): cursor to default color, video background to surf…
waleedlatif1 Apr 28, 2026
14f77b3
fix(files): stabilize setInputRef callback and guard against double-c…
waleedlatif1 Apr 28, 2026
8cec4a3
fix(files): preserve scroll position during Mothership streaming edits
waleedlatif1 Apr 28, 2026
a252ab4
fix(files): fix two scroll logic bugs introduced in previous streamin…
waleedlatif1 Apr 28, 2026
bdae6d3
chore(files): remove extraneous comments from file viewer and data table
waleedlatif1 Apr 28, 2026
4d6657b
refactor(files): split 2281-line file-viewer.tsx into focused modules
waleedlatif1 Apr 28, 2026
deec713
fix(files): remove unnecessary TextEditorProps export
waleedlatif1 Apr 28, 2026
f1d837d
refactor(files): four stellar-quality improvements to file-viewer split
waleedlatif1 Apr 28, 2026
bf6e630
test(files): extract pure modules and add 122-test suite for file vie…
waleedlatif1 Apr 28, 2026
12f746a
fix(files): add key to IframePreview and use monotonic seq for stream…
waleedlatif1 Apr 28, 2026
a579b85
fix(files): restore getFileExtension import dropped during refactor
waleedlatif1 Apr 28, 2026
87aa8f6
fix(files): clear loadError on PDF success and fix streaming null-flash
waleedlatif1 Apr 28, 2026
68aeb69
refactor(files): cleanup pass — effect, callback, state, and design f…
waleedlatif1 Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { updateWorkspaceFileContent } from '@/lib/uploads/contexts/workspace'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
Expand All @@ -17,7 +17,6 @@ const logger = createLogger('WorkspaceFileContentAPI')
*/
export const PUT = withRouteHandler(
async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => {
const requestId = generateRequestId()
const { id: workspaceId, fileId } = await params

try {
Expand All @@ -32,20 +31,19 @@ export const PUT = withRouteHandler(
workspaceId
)
if (userPermission !== 'admin' && userPermission !== 'write') {
logger.warn(
`[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}`
)
logger.warn(`User ${session.user.id} lacks write permission for workspace ${workspaceId}`)
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}

const body = await request.json()
const { content } = body as { content: string }
const { content, encoding } = body as { content: string; encoding?: 'base64' | 'utf-8' }

if (typeof content !== 'string') {
return NextResponse.json({ error: 'Content must be a string' }, { status: 400 })
}

const buffer = Buffer.from(content, 'utf-8')
const buffer =
encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf-8')

const maxFileSizeBytes = 50 * 1024 * 1024
if (buffer.length > maxFileSizeBytes) {
Expand All @@ -62,7 +60,7 @@ export const PUT = withRouteHandler(
buffer
)

logger.info(`[${requestId}] Updated content for workspace file: ${updatedFile.name}`)
logger.info(`Updated content for workspace file: ${updatedFile.name}`)

recordAudit({
workspaceId,
Expand All @@ -83,15 +81,15 @@ export const PUT = withRouteHandler(
file: updatedFile,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to update file content'
const errorMessage = toError(error).message || 'Failed to update file content'
const isNotFound = errorMessage.includes('File not found')
const isQuotaExceeded = errorMessage.includes('Storage limit exceeded')
const status = isNotFound ? 404 : isQuotaExceeded ? 402 : 500

if (status === 500) {
logger.error(`[${requestId}] Error updating file content:`, error)
logger.error('Error updating file content:', error)
} else {
logger.warn(`[${requestId}] ${errorMessage}`)
logger.warn(errorMessage)
}

return NextResponse.json({ success: false, error: errorMessage }, { status })
Expand Down
9 changes: 8 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/files/[fileId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from 'react'
import type { Metadata } from 'next'
import { Files } from '../files'

Expand All @@ -6,4 +7,10 @@ export const metadata: Metadata = {
robots: { index: false },
}

export default Files
export default function FilesFilePage() {
return (
<Suspense fallback={null}>
<Files />
</Suspense>
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,92 @@
import { memo } from 'react'
'use client'

import { forwardRef, memo, useImperativeHandle, useRef, useState } from 'react'
import { cn } from '@/lib/core/utils/cn'

interface EditConfig {
onCellChange: (row: number, col: number, value: string) => void
onHeaderChange: (col: number, value: string) => void
}

interface DataTableProps {
headers: string[]
rows: string[][]
editConfig?: EditConfig
}

export interface DataTableHandle {
commitEdit: () => void
}

export const DataTable = memo(function DataTable({ headers, rows }: DataTableProps) {
type EditingCell = { row: number; col: number } | null

const DataTableBase = forwardRef<DataTableHandle, DataTableProps>(function DataTable(
{ headers, rows, editConfig },
ref
) {
const [editingCell, setEditingCell] = useState<EditingCell>(null)
const [editValue, setEditValue] = useState('')

// Always-current ref so the imperative handle doesn't go stale
const editStateRef = useRef({ editingCell, editValue, editConfig })
editStateRef.current = { editingCell, editValue, editConfig }

useImperativeHandle(
ref,
() => ({
commitEdit: () => {
const { editingCell, editValue, editConfig } = editStateRef.current
if (!editingCell || !editConfig) return
const { row, col } = editingCell
if (row === -1) {
editConfig.onHeaderChange(col, editValue)
} else {
editConfig.onCellChange(row, col, editValue)
}
setEditingCell(null)
},
}),
[]
)

const setInputRef = (node: HTMLInputElement | null) => {
if (node) {
node.focus()
node.select()
}
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
}
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

const startEdit = (row: number, col: number, currentValue: string) => {
if (!editConfig) return
setEditingCell({ row, col })
setEditValue(currentValue)
}

const commitEdit = () => {
if (!editingCell || !editConfig) return
const { row, col } = editingCell
if (row === -1) {
editConfig.onHeaderChange(col, editValue)
} else {
editConfig.onCellChange(row, col, editValue)
}
setEditingCell(null)
}

const cancelEdit = () => setEditingCell(null)

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault()
commitEdit()
} else if (e.key === 'Escape') {
cancelEdit()
}
}

const isEditing = (row: number, col: number) =>
editingCell?.row === row && editingCell?.col === col

return (
<div className='overflow-x-auto rounded-md border border-[var(--border)]'>
<table className='w-full border-collapse text-[13px]'>
Expand All @@ -14,9 +95,24 @@ export const DataTable = memo(function DataTable({ headers, rows }: DataTablePro
{headers.map((header, i) => (
<th
key={i}
className='whitespace-nowrap px-3 py-2 text-left font-semibold text-[12px] text-[var(--text-primary)]'
className={cn(
'whitespace-nowrap px-3 py-2 text-left font-semibold text-[12px] text-[var(--text-primary)]',
editConfig && 'cursor-pointer select-none hover:bg-[var(--surface-3)]'
)}
onClick={() => editConfig && startEdit(-1, i, String(header ?? ''))}
>
{String(header ?? '')}
{isEditing(-1, i) ? (
<input
ref={setInputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={commitEdit}
onKeyDown={handleKeyDown}
className='w-full min-w-[60px] bg-transparent font-semibold text-[12px] text-[var(--text-primary)] outline-none ring-1 ring-[var(--brand-secondary)] ring-inset'
/>
) : (
String(header ?? '')
)}
</th>
))}
</tr>
Expand All @@ -25,8 +121,26 @@ export const DataTable = memo(function DataTable({ headers, rows }: DataTablePro
{rows.map((row, ri) => (
<tr key={ri} className='border-[var(--border)] border-t'>
{headers.map((_, ci) => (
<td key={ci} className='whitespace-nowrap px-3 py-2 text-[var(--text-secondary)]'>
{String(row[ci] ?? '')}
<td
key={ci}
className={cn(
'whitespace-nowrap px-3 py-2 text-[var(--text-secondary)]',
editConfig && 'cursor-pointer select-none hover:bg-[var(--surface-2)]'
)}
onClick={() => editConfig && startEdit(ri, ci, String(row[ci] ?? ''))}
>
{isEditing(ri, ci) ? (
<input
ref={setInputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={commitEdit}
onKeyDown={handleKeyDown}
className='w-full min-w-[60px] bg-transparent text-[13px] text-[var(--text-secondary)] outline-none ring-1 ring-[var(--brand-secondary)] ring-inset'
/>
) : (
String(row[ci] ?? '')
)}
</td>
))}
</tr>
Expand All @@ -36,3 +150,5 @@ export const DataTable = memo(function DataTable({ headers, rows }: DataTablePro
</div>
)
})

export const DataTable = memo(DataTableBase)
Loading
Loading