Default Datatable & Schema
diff --git a/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte b/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte
index 1c06463bf1..f77cffa40f 100644
--- a/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte
+++ b/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte
@@ -1,3 +1,7 @@
+
+
{#if file.endsWith('.tsx')}
-
+
{:else if file.endsWith('.json')}
-
+
{:else if file.endsWith('.ts')}
-
+
{:else if file.endsWith('.js')}
-
+
{:else if file.endsWith('.vue')}
-
+
{:else if file.endsWith('.css')}
#
{:else if file.endsWith('.svelte')}
-
+
{/if}
diff --git a/frontend/src/lib/components/raw_apps/FileTreeNode.svelte b/frontend/src/lib/components/raw_apps/FileTreeNode.svelte
index b3aa65154e..79bbd1ad27 100644
--- a/frontend/src/lib/components/raw_apps/FileTreeNode.svelte
+++ b/frontend/src/lib/components/raw_apps/FileTreeNode.svelte
@@ -4,15 +4,30 @@
ChevronDown,
File,
Folder,
- FileJson,
- FileCode,
- ImageIcon,
- Palette,
Pencil,
Trash2,
- Lock
+ Lock,
+ Ellipsis,
+ ImageIcon
} from 'lucide-svelte'
import Self from './FileTreeNode.svelte'
+ import { twMerge } from 'tailwind-merge'
+ import DropdownV2 from '../DropdownV2.svelte'
+ import { Button } from '../common'
+ import TextInput from '../text_input/TextInput.svelte'
+ import TypeScript from '../common/languageIcons/TypeScript.svelte'
+ import JavaScriptIcon from '../icons/JavaScriptIcon.svelte'
+ import JsonIcon from '../icons/JsonIcon.svelte'
+ import ReactIcon from '../icons/ReactIcon.svelte'
+ import SvelteIcon from '../icons/SvelteIcon.svelte'
+ import VueIcon from '../icons/VueIcon.svelte'
+ import CssIcon from '../icons/CssIcon.svelte'
+ import SassIcon from '../icons/SassIcon.svelte'
+ import LessIcon from '../icons/LessIcon.svelte'
+ import HtmlIcon from '../icons/HtmlIcon.svelte'
+ import MarkdownIcon from '../icons/MarkdownIcon.svelte'
+ import YamlIcon from '../icons/YamlIcon.svelte'
+ import { tick } from 'svelte'
interface TreeNode {
name: string
@@ -28,9 +43,10 @@
onAddFolder?: (folderPath: string) => void
onRename?: (oldPath: string, newName: string) => void
onDelete?: (path: string) => void
+ onRequestEdit?: (path: string) => void
+ onCancelEdit?: () => void
selectedPath?: string
- pathToRename?: string
- pathToExpand?: string
+ pathToEdit?: string
noEdit?: boolean
level?: number
}
@@ -42,24 +58,34 @@
onAddFolder,
onRename,
onDelete,
+ onRequestEdit,
+ onCancelEdit,
selectedPath,
- pathToRename,
- pathToExpand,
+ pathToEdit,
noEdit = false,
level = 0
}: Props = $props()
- let expanded = $state(level === 0) // Root folders start expanded
+ let userExpanded = $state(null) // null = not set by user
let isHovered = $state(false)
- let isEditing = $state(false)
let editValue = $state(node.name)
- let inputElement: HTMLInputElement | undefined = $state()
+ let textInputElement: TextInput | undefined = $state()
+ let dropdownOpen = $state(false)
const isSelected = $derived(selectedPath === node.path)
+ const isEditing = $derived(pathToEdit === node.path)
+ const expanded = $derived(
+ // Auto-expand for editing nested paths takes priority
+ pathToEdit && node.isFolder && pathToEdit.startsWith(node.path)
+ ? true
+ : userExpanded !== null
+ ? userExpanded
+ : level === 0 // Default: root expanded
+ )
function toggleExpanded() {
if (node.isFolder) {
- expanded = !expanded
+ userExpanded = !expanded
}
}
@@ -75,8 +101,7 @@
function handleEdit(e: MouseEvent) {
e.stopPropagation()
- isEditing = true
- editValue = node.name
+ onRequestEdit?.(node.path)
}
function handleDelete(e: MouseEvent) {
@@ -85,58 +110,37 @@
}
function finishEdit() {
- if (isEditing && editValue.trim() && editValue !== node.name) {
+ if (isEditing && editValue.trim()) {
+ // Always call onRename - parent handles whether it's a new file or actual rename
onRename?.(node.path, editValue.trim())
+ } else {
+ onCancelEdit?.()
}
- isEditing = false
}
function handleInputKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
finishEdit()
} else if (e.key === 'Escape') {
- isEditing = false
- editValue = node.name
+ editValue = node.name // Reset to original
+ onCancelEdit?.()
}
}
- function handleInputBlur() {
+ function handleInputBlur(e: FocusEvent) {
finishEdit()
}
+ // Single effect for DOM operations only
$effect(() => {
- if (isEditing && inputElement) {
- inputElement.focus()
- inputElement.select()
- }
- })
-
- // Automatically enter edit mode for newly created files
- $effect(() => {
- if (pathToRename === node.path && !isEditing) {
- isEditing = true
+ if (isEditing && textInputElement) {
+ // Reset edit value when entering edit mode
editValue = node.name
- // If this is a folder, expand it
- if (node.isFolder) {
- expanded = true
- }
- }
- })
-
- // Expand parent folders when a child needs to be renamed
- $effect(() => {
- if (pathToRename && node.isFolder && pathToRename.startsWith(node.path + '/')) {
- expanded = true
- }
- })
-
- // Expand folder when pathToExpand matches this node or a parent of pathToExpand
- $effect(() => {
- if (pathToExpand && node.isFolder) {
- // Expand if this folder is the target or an ancestor of the target
- if (node.path === pathToExpand || pathToExpand.startsWith(node.path + '/')) {
- expanded = true
- }
+ // Focus and select input (DOM side effects)
+ tick().then(() => {
+ textInputElement?.focus()
+ textInputElement?.select()
+ })
}
})
@@ -164,22 +168,26 @@
switch (ext) {
case 'json':
- return { icon: FileJson, className: 'text-yellow-500' }
+ return { icon: JsonIcon }
case 'tsx':
+ return { icon: ReactIcon }
case 'jsx':
- return { icon: FileCode, className: 'text-blue-500' }
+ return { icon: ReactIcon }
case 'ts':
+ return { icon: TypeScript }
case 'js':
- return { icon: FileCode, className: 'text-blue-400' }
+ return { icon: JavaScriptIcon }
case 'svelte':
- return { icon: FileCode, className: 'text-orange-500' }
+ return { icon: SvelteIcon }
case 'vue':
- return { icon: FileCode, className: 'text-green-500' }
+ return { icon: VueIcon }
case 'css':
+ return { icon: CssIcon }
case 'scss':
case 'sass':
+ return { icon: SassIcon }
case 'less':
- return { icon: Palette, className: 'text-pink-500' }
+ return { icon: LessIcon }
case 'png':
case 'jpg':
case 'jpeg':
@@ -188,6 +196,15 @@
case 'webp':
case 'ico':
return { icon: ImageIcon, className: 'text-purple-500' }
+ case 'html':
+ case 'htm':
+ return { icon: HtmlIcon }
+ case 'md':
+ case 'markdown':
+ return { icon: MarkdownIcon }
+ case 'yaml':
+ case 'yml':
+ return { icon: YamlIcon }
default:
return { icon: File, className: 'text-tertiary' }
}
@@ -203,7 +220,7 @@
>
{#if isEditing}
{/if}
-
+
{:else}
{@const IconComponent = fileIcon.icon}
-
+
{/if}
- handleInputBlur(e),
+ type: 'text'
+ }}
+ size="xs"
/>