Skip to content

Commit 95eedb9

Browse files
laktekclaudejoshenlim
authored
Updates to Edge Functions dashboard code editor (supabase#39991)
* feat: use mgmt-api's function body endpoint * skip json files from static patterns * set the default content for deno.json * feat: add drag and drop file support to FileExplorerAndEditor - Add drag and drop functionality to accept files - Dropped files are automatically read and added to the files list - Visual feedback with drag overlay during drag operations - Files maintain existing data format with id, name, content, and selected state - Last dropped file is automatically selected for editing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: add binary file handling to FileExplorerAndEditor - Add binary file detection for common file extensions (.wasm, images, executables, etc.) - Show "Cannot Edit Selected File" error message when trying to edit binary files - Binary files dropped via drag-and-drop retain their original binary content - Only show error in editor view, files remain accessible in file list - Text files continue to work normally with full editing capabilities 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * do not ignore empty files * exclude wasm files from static patterns * Remove eszip parser test as dependency has been removed * Fix TS issues * Fix TS issues * Fix pnpm-lock * Fix * Fix * Nit --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Joshen Lim <[email protected]>
1 parent afff7f7 commit 95eedb9

File tree

14 files changed

+352
-500
lines changed

14 files changed

+352
-500
lines changed

apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { basename } from 'path'
1414
import { Card, CardContent, CardHeader, CardTitle, cn, Skeleton } from 'ui'
1515

1616
const EMPTY_FUNCTION_BODY: EdgeFunctionBodyData = {
17-
version: 0,
1817
files: EMPTY_ARR,
1918
}
2019

@@ -95,8 +94,12 @@ const FunctionDiff = ({
9594

9695
const language = useMemo(() => {
9796
if (!activeFileKey) return 'plaintext'
98-
if (activeFileKey.endsWith('.ts') || activeFileKey.endsWith('.tsx')) return 'typescript'
99-
if (activeFileKey.endsWith('.js') || activeFileKey.endsWith('.jsx')) return 'javascript'
97+
if (activeFileKey.endsWith('.ts') || activeFileKey.endsWith('.tsx')) {
98+
return 'typescript'
99+
}
100+
if (activeFileKey.endsWith('.js') || activeFileKey.endsWith('.jsx')) {
101+
return 'javascript'
102+
}
100103
if (activeFileKey.endsWith('.json')) return 'json'
101104
if (activeFileKey.endsWith('.sql')) return 'sql'
102105
return 'plaintext'

apps/studio/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const DeployEdgeFunctionWarningModal = ({
1717
<ConfirmationModal
1818
visible={visible}
1919
size="medium"
20-
title="Confirm deploying updates"
20+
title="Confirm to deploy updates"
2121
confirmLabel="Deploy updates"
2222
confirmLabelLoading="Deploying updates"
2323
variant="warning"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
export const isBinaryFile = (fileName: string): boolean => {
2+
const extension = fileName.split('.').pop()?.toLowerCase()
3+
const binaryExtensions = [
4+
'wasm',
5+
'jpg',
6+
'jpeg',
7+
'png',
8+
'gif',
9+
'bmp',
10+
'ico',
11+
'svg',
12+
'mp3',
13+
'mp4',
14+
'avi',
15+
'mov',
16+
'zip',
17+
'rar',
18+
'7z',
19+
'tar',
20+
'gz',
21+
'bz2',
22+
'pdf',
23+
]
24+
return binaryExtensions.includes(extension || '')
25+
}
26+
27+
export const getLanguageFromFileName = (fileName: string): string => {
28+
const extension = fileName.split('.').pop()?.toLowerCase()
29+
switch (extension) {
30+
case 'ts':
31+
case 'tsx':
32+
return 'typescript'
33+
case 'js':
34+
case 'jsx':
35+
return 'javascript'
36+
case 'json':
37+
return 'json'
38+
case 'html':
39+
return 'html'
40+
case 'css':
41+
return 'css'
42+
case 'md':
43+
return 'markdown'
44+
case 'csv':
45+
return 'csv'
46+
default:
47+
return 'plaintext' // Default to plaintext
48+
}
49+
}
Lines changed: 131 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AnimatePresence, motion } from 'framer-motion'
12
import { Edit, File, Plus, Trash } from 'lucide-react'
23
import { useEffect, useState } from 'react'
34

@@ -13,6 +14,7 @@ import {
1314
TreeView,
1415
TreeViewItem,
1516
} from 'ui'
17+
import { getLanguageFromFileName, isBinaryFile } from './FileExplorerAndEditor.utils'
1618

1719
interface FileData {
1820
id: number
@@ -32,35 +34,16 @@ interface FileExplorerAndEditorProps {
3234
}
3335
}
3436

35-
const getLanguageFromFileName = (fileName: string): string => {
36-
const extension = fileName.split('.').pop()?.toLowerCase()
37-
switch (extension) {
38-
case 'ts':
39-
case 'tsx':
40-
return 'typescript'
41-
case 'js':
42-
case 'jsx':
43-
return 'javascript'
44-
case 'json':
45-
return 'json'
46-
case 'html':
47-
return 'html'
48-
case 'css':
49-
return 'css'
50-
case 'md':
51-
return 'markdown'
52-
default:
53-
return 'typescript' // Default to typescript
54-
}
55-
}
37+
const denoJsonDefaultContent = JSON.stringify({ imports: {} }, null, '\t')
5638

57-
const FileExplorerAndEditor = ({
39+
export const FileExplorerAndEditor = ({
5840
files,
5941
onFilesChange,
6042
aiEndpoint,
6143
aiMetadata,
6244
}: FileExplorerAndEditorProps) => {
6345
const selectedFile = files.find((f) => f.selected) ?? files[0]
46+
const [isDragOver, setIsDragOver] = useState(false)
6447

6548
const [treeData, setTreeData] = useState({
6649
name: '',
@@ -95,9 +78,55 @@ const FileExplorerAndEditor = ({
9578
])
9679
}
9780

81+
const addDroppedFiles = async (droppedFiles: FileList) => {
82+
const newFiles: FileData[] = []
83+
const updatedFiles = files.map((f) => ({ ...f, selected: false }))
84+
85+
for (let i = 0; i < droppedFiles.length; i++) {
86+
const file = droppedFiles[i]
87+
const newId = Math.max(0, ...files.map((f) => f.id), ...newFiles.map((f) => f.id)) + 1
88+
89+
try {
90+
let content: string
91+
if (isBinaryFile(file.name)) {
92+
// For binary files, read as ArrayBuffer and convert to base64 or keep as binary data
93+
const arrayBuffer = await file.arrayBuffer()
94+
const bytes = new Uint8Array(arrayBuffer)
95+
content = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
96+
} else {
97+
content = await file.text()
98+
}
99+
100+
newFiles.push({
101+
id: newId,
102+
name: file.name,
103+
content,
104+
selected: i === droppedFiles.length - 1, // Select the last dropped file
105+
})
106+
} catch (error) {
107+
console.error(`Failed to read file ${file.name}:`, error)
108+
}
109+
}
110+
111+
if (newFiles.length > 0) {
112+
onFilesChange([...updatedFiles, ...newFiles])
113+
}
114+
}
115+
98116
const handleFileNameChange = (id: number, newName: string) => {
99117
if (!newName.trim()) return // Don't allow empty names
100-
const updatedFiles = files.map((file) => (file.id === id ? { ...file, name: newName } : file))
118+
const updatedFiles = files.map((file) =>
119+
file.id === id
120+
? {
121+
...file,
122+
name: newName,
123+
content:
124+
newName === 'deno.json' && file.content === ''
125+
? denoJsonDefaultContent
126+
: file.content,
127+
}
128+
: file
129+
)
101130
onFilesChange(updatedFiles)
102131
}
103132

@@ -145,6 +174,26 @@ const FileExplorerAndEditor = ({
145174
setTreeData(updatedTreeData)
146175
}
147176

177+
const handleDragOver = (e: React.DragEvent) => {
178+
e.preventDefault()
179+
setIsDragOver(true)
180+
}
181+
182+
const handleDragLeave = (e: React.DragEvent) => {
183+
e.preventDefault()
184+
setIsDragOver(false)
185+
}
186+
187+
const handleDrop = async (e: React.DragEvent) => {
188+
e.preventDefault()
189+
setIsDragOver(false)
190+
191+
const droppedFiles = e.dataTransfer.files
192+
if (droppedFiles.length > 0) {
193+
await addDroppedFiles(droppedFiles)
194+
}
195+
}
196+
148197
// Update treeData when files change
149198
useEffect(() => {
150199
setTreeData({
@@ -161,7 +210,27 @@ const FileExplorerAndEditor = ({
161210
}, [files])
162211

163212
return (
164-
<div className="flex-1 overflow-hidden flex h-full">
213+
<div
214+
className={`flex-1 overflow-hidden flex h-full relative ${isDragOver ? 'bg-blue-50' : ''}`}
215+
onDragOver={handleDragOver}
216+
onDragLeave={handleDragLeave}
217+
onDrop={handleDrop}
218+
>
219+
<AnimatePresence>
220+
{isDragOver && (
221+
<motion.div
222+
initial={{ opacity: 0 }}
223+
animate={{ opacity: 1 }}
224+
exit={{ opacity: 0 }}
225+
transition={{ duration: 0.1 }}
226+
className="absolute inset-0 bg bg-opacity-30 z-10 flex items-center justify-center"
227+
>
228+
<div className="w-96 py-20 bg bg-opacity-60 border-2 border-dashed border-muted flex items-center justify-center">
229+
<div className="text-base">Drop files here to add them</div>
230+
</div>
231+
</motion.div>
232+
)}
233+
</AnimatePresence>
165234
<div className="w-64 border-r bg-surface-200 flex flex-col">
166235
<div className="py-4 px-6 border-b flex items-center justify-between">
167236
<h3 className="text-sm font-normal font-mono uppercase text-lighter tracking-wide">
@@ -197,13 +266,17 @@ const FileExplorerAndEditor = ({
197266
icon={<File size={14} className="text-foreground-light shrink-0" />}
198267
isEditing={Boolean(element.metadata?.isEditing)}
199268
onEditSubmit={(value) => {
200-
if (originalId !== null) handleFileNameChange(originalId, value)
269+
if (originalId !== null) {
270+
handleFileNameChange(originalId, value)
271+
}
201272
}}
202273
onClick={() => {
203274
if (originalId !== null) handleFileSelect(originalId)
204275
}}
205276
onDoubleClick={() => {
206-
if (originalId !== null) handleStartRename(originalId)
277+
if (originalId !== null) {
278+
handleStartRename(originalId)
279+
}
207280
}}
208281
/>
209282
</div>
@@ -226,7 +299,9 @@ const FileExplorerAndEditor = ({
226299
<ContextMenuItem_Shadcn_
227300
className="gap-x-2"
228301
onSelect={() => {
229-
if (originalId !== null) handleFileDelete(originalId)
302+
if (originalId !== null) {
303+
handleFileDelete(originalId)
304+
}
230305
}}
231306
onFocusCapture={(e) => e.stopPropagation()}
232307
>
@@ -243,26 +318,36 @@ const FileExplorerAndEditor = ({
243318
</div>
244319
</div>
245320
<div className="flex-1 min-h-0 relative px-3 bg-surface-200">
246-
<AIEditor
247-
language={getLanguageFromFileName(selectedFile?.name || 'index.ts')}
248-
value={selectedFile?.content}
249-
onChange={handleChange}
250-
aiEndpoint={aiEndpoint}
251-
aiMetadata={aiMetadata}
252-
options={{
253-
tabSize: 2,
254-
fontSize: 13,
255-
minimap: { enabled: false },
256-
wordWrap: 'on',
257-
lineNumbers: 'on',
258-
folding: false,
259-
padding: { top: 20, bottom: 20 },
260-
lineNumbersMinChars: 3,
261-
}}
262-
/>
321+
{selectedFile && isBinaryFile(selectedFile.name) ? (
322+
<div className="flex items-center justify-center h-full">
323+
<div className="text-center">
324+
<div className="text-foreground-light text-lg mb-2">Cannot Edit Selected File</div>
325+
<div className="text-foreground-lighter text-sm">
326+
Binary files like .{selectedFile.name.split('.').pop()} cannot be edited in the text
327+
editor
328+
</div>
329+
</div>
330+
</div>
331+
) : (
332+
<AIEditor
333+
language={getLanguageFromFileName(selectedFile?.name || 'index.ts')}
334+
value={selectedFile?.content}
335+
onChange={handleChange}
336+
aiEndpoint={aiEndpoint}
337+
aiMetadata={aiMetadata}
338+
options={{
339+
tabSize: 2,
340+
fontSize: 13,
341+
minimap: { enabled: false },
342+
wordWrap: 'on',
343+
lineNumbers: 'on',
344+
folding: false,
345+
padding: { top: 20, bottom: 20 },
346+
lineNumbersMinChars: 3,
347+
}}
348+
/>
349+
)}
263350
</div>
264351
</div>
265352
)
266353
}
267-
268-
export default FileExplorerAndEditor

0 commit comments

Comments
 (0)