From 13380fabaa8df017cee58de289c304f1cf01e2a7 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Tue, 13 Jan 2026 16:33:32 +0000 Subject: [PATCH] fix(frontend): update raw app editor to brand guidelines (#7545) * nit * Update sidebar to brand guidelines * use dropdown for file delete and rename * update runnable script styling * fix icon sizes * Polish app creation form * Add missing languages icons * Allow to delete runnable from the left pannel * update top bar to brand guidelines * improve svelte template * fix runnables binding * nit * Deselect frontend file when selecting background runnable * fix renaming wip * fix add ing files, update only after adding * Change filename when already existing * update deployment page to brand guidelines * fix folder open error * expand folder if adding file inside * nit * fix bad link * use alert for ai not configured message --- .../apps/editor/AppEditorHeaderDeploy.svelte | 61 ++-- .../common/languageIcons/TypeScript.svelte | 14 +- .../lib/components/common/modal/Modal.svelte | 2 +- .../components/flows/CreateActionsApp.svelte | 10 +- .../src/lib/components/icons/CssIcon.svelte | 27 ++ .../src/lib/components/icons/HtmlIcon.svelte | 27 ++ .../components/icons/JavaScriptIcon.svelte | 18 +- .../src/lib/components/icons/JsonIcon.svelte | 16 +- .../src/lib/components/icons/LessIcon.svelte | 23 ++ .../lib/components/icons/MarkdownIcon.svelte | 27 ++ .../src/lib/components/icons/ReactIcon.svelte | 18 +- .../src/lib/components/icons/SassIcon.svelte | 36 ++ .../lib/components/icons/SvelteIcon.svelte | 18 +- .../src/lib/components/icons/VueIcon.svelte | 18 +- .../src/lib/components/icons/YamlIcon.svelte | 27 ++ .../raw_apps/DefaultDatabaseSelector.svelte | 14 +- .../components/raw_apps/FileEditorIcon.svelte | 22 +- .../components/raw_apps/FileTreeNode.svelte | 218 +++++++----- .../raw_apps/RawAppDataTableList.svelte | 21 +- .../components/raw_apps/RawAppEditor.svelte | 2 +- .../raw_apps/RawAppEditorHeader.svelte | 33 +- .../RawAppInlineScriptPanelList.svelte | 44 ++- .../raw_apps/RawAppInlineScriptsPanel.svelte | 23 +- .../components/raw_apps/RawAppModules.svelte | 20 +- .../components/raw_apps/RawAppSidebar.svelte | 334 +++++++++++------- .../components/raw_apps/RunnableRow.svelte | 53 +++ .../components/text_input/TextInput.svelte | 4 + .../(root)/(logged)/apps_raw/add/+page.svelte | 241 ++++++------- .../(root)/(logged)/apps_raw/add/templates.ts | 72 ++-- 29 files changed, 936 insertions(+), 507 deletions(-) create mode 100644 frontend/src/lib/components/icons/HtmlIcon.svelte create mode 100644 frontend/src/lib/components/icons/LessIcon.svelte create mode 100644 frontend/src/lib/components/icons/MarkdownIcon.svelte create mode 100644 frontend/src/lib/components/icons/SassIcon.svelte create mode 100644 frontend/src/lib/components/icons/YamlIcon.svelte create mode 100644 frontend/src/lib/components/raw_apps/RunnableRow.svelte diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte index 81f59a5028..6bcdf298f8 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte @@ -14,6 +14,7 @@ import { base } from '$lib/base' import { isCloudHosted } from '$lib/cloud' import EEOnly from '$lib/components/EEOnly.svelte' + import TextInput from '$lib/components/text_input/TextInput.svelte' let { policy, @@ -117,44 +118,46 @@
{/if} -Summary -
+ +
- { - e.stopPropagation() - }} - onkeyup={() => { - if (appPath == '' && summary?.length > 0 && !dirtyPath) { - path?.setName( - summary - .toLowerCase() - .replace(/[^a-z0-9_]/g, '_') - .replace(/-+/g, '_') - .replace(/^-|-$/g, '') - ) + { + e.stopPropagation() + }, + onkeyup: () => { + if (appPath == '' && summary?.length > 0 && !dirtyPath) { + path?.setName( + summary + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/-+/g, '_') + .replace(/^-|-$/g, '') + ) + } } }} + bind:value={summary} />
-
-Deployment message -
+
+ +
-
-
-Path +
+ interface Props { - width?: number; - height?: number; + width?: number + height?: number + size?: number [key: string]: any } - let { width = 16, height = 16, ...rest }: Props = $props(); + let { width = 16, height = 16, size = undefined, ...rest }: Props = $props() + + const derivedWidth = $derived(size ? size : width) + const derivedHeight = $derived(size ? size : height) {/if}
-
+

{title}

{@render settings?.()} diff --git a/frontend/src/lib/components/flows/CreateActionsApp.svelte b/frontend/src/lib/components/flows/CreateActionsApp.svelte index 771698c93c..f0e18be7fb 100644 --- a/frontend/src/lib/components/flows/CreateActionsApp.svelte +++ b/frontend/src/lib/components/flows/CreateActionsApp.svelte @@ -154,16 +154,18 @@
- +
diff --git a/frontend/src/lib/components/icons/CssIcon.svelte b/frontend/src/lib/components/icons/CssIcon.svelte index e69de29bb2..89db9aca54 100644 --- a/frontend/src/lib/components/icons/CssIcon.svelte +++ b/frontend/src/lib/components/icons/CssIcon.svelte @@ -0,0 +1,27 @@ + + + + + + + diff --git a/frontend/src/lib/components/icons/HtmlIcon.svelte b/frontend/src/lib/components/icons/HtmlIcon.svelte new file mode 100644 index 0000000000..e31b85a055 --- /dev/null +++ b/frontend/src/lib/components/icons/HtmlIcon.svelte @@ -0,0 +1,27 @@ + + + + + + + diff --git a/frontend/src/lib/components/icons/JavaScriptIcon.svelte b/frontend/src/lib/components/icons/JavaScriptIcon.svelte index c6a4c75b71..b7699a21d8 100644 --- a/frontend/src/lib/components/icons/JavaScriptIcon.svelte +++ b/frontend/src/lib/components/icons/JavaScriptIcon.svelte @@ -1,13 +1,23 @@ - + interface Props { - height?: string; - width?: string; + height?: string + width?: string + size?: number } - let { height = '24px', width = '24px' }: Props = $props(); + let { height = '24px', width = '24px', size = undefined }: Props = $props() + + const derivedWidth = $derived(size ? size : width) + const derivedHeight = $derived(size ? size : height) - + interface Props { + height?: string + width?: string + size?: number + } + + let { height = '24px', width = '24px', size = undefined }: Props = $props() + + const derivedWidth = $derived(size ? size : width) + const derivedHeight = $derived(size ? size : height) + + + diff --git a/frontend/src/lib/components/icons/MarkdownIcon.svelte b/frontend/src/lib/components/icons/MarkdownIcon.svelte new file mode 100644 index 0000000000..60b1251626 --- /dev/null +++ b/frontend/src/lib/components/icons/MarkdownIcon.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/lib/components/icons/ReactIcon.svelte b/frontend/src/lib/components/icons/ReactIcon.svelte index 791235b5df..eb5b5a4198 100644 --- a/frontend/src/lib/components/icons/ReactIcon.svelte +++ b/frontend/src/lib/components/icons/ReactIcon.svelte @@ -1,13 +1,23 @@ - + diff --git a/frontend/src/lib/components/icons/SassIcon.svelte b/frontend/src/lib/components/icons/SassIcon.svelte new file mode 100644 index 0000000000..fed5e3283a --- /dev/null +++ b/frontend/src/lib/components/icons/SassIcon.svelte @@ -0,0 +1,36 @@ + + + diff --git a/frontend/src/lib/components/icons/SvelteIcon.svelte b/frontend/src/lib/components/icons/SvelteIcon.svelte index c0de67a482..3fa14e75ff 100644 --- a/frontend/src/lib/components/icons/SvelteIcon.svelte +++ b/frontend/src/lib/components/icons/SvelteIcon.svelte @@ -1,13 +1,23 @@ - + interface Props { - height?: string; - width?: string; + height?: string + width?: string + size?: number } - let { height = '24px', width = '24px' }: Props = $props(); + let { height = '24px', width = '24px', size = 16 }: Props = $props() + + const derivedWidth = $derived(size ? size : width) + const derivedHeight = $derived(size ? size : height) - + + interface Props { + height?: string + width?: string + size?: number + } + + let { height = '24px', width = '24px', size = undefined }: Props = $props() + + const derivedWidth = $derived(size ? size : width) + const derivedHeight = $derived(size ? size : height) + + + + + diff --git a/frontend/src/lib/components/raw_apps/DefaultDatabaseSelector.svelte b/frontend/src/lib/components/raw_apps/DefaultDatabaseSelector.svelte index 54d79ddb7b..9a5e4cba4d 100644 --- a/frontend/src/lib/components/raw_apps/DefaultDatabaseSelector.svelte +++ b/frontend/src/lib/components/raw_apps/DefaultDatabaseSelector.svelte @@ -9,6 +9,7 @@ toDatatableItems, toSchemaItems } from './datatableUtils.svelte' + import { Button } from '../common' interface Props { /** Currently selected datatable */ @@ -48,15 +49,18 @@ - + + -
+
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" />
{:else} - {#if isHovered && !isEditing} -
- {#if !noEdit} - - - {:else} - - {/if} -
+ {#if isHovered || dropdownOpen} + {#if !noEdit} + + {#snippet buttonReplacement()} + + {/snippet} + + {:else} + + {/if} {/if} {/if}
@@ -294,9 +325,10 @@ {onAddFolder} {onRename} {onDelete} + {onRequestEdit} + {onCancelEdit} {selectedPath} - {pathToRename} - {pathToExpand} + {pathToEdit} level={level + 1} /> {/each} diff --git a/frontend/src/lib/components/raw_apps/RawAppDataTableList.svelte b/frontend/src/lib/components/raw_apps/RawAppDataTableList.svelte index 75bcdb9a40..b6cacd06af 100644 --- a/frontend/src/lib/components/raw_apps/RawAppDataTableList.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppDataTableList.svelte @@ -3,6 +3,7 @@ import PanelSection from '../apps/editor/settingsPanel/common/PanelSection.svelte' import type { DataTableRef } from './dataTableRefUtils' import DefaultDatabaseSelector from './DefaultDatabaseSelector.svelte' + import { Button } from '../common' // Re-export for backwards compatibility export type { DataTableRef } from './dataTableRefUtils' @@ -86,7 +87,7 @@ {#snippet actionButtons()} -
+
{#if !hideDefaultSelector} - + + +
{/snippet} @@ -180,7 +183,7 @@ {#if standalone}
- Existing tables to use + Existing tables to use {@render actionButtons()}
{@render tableList()} @@ -188,7 +191,7 @@ {:else} { console.log('handle selection', selection) diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 7391d1d57f..b5a3f1aec4 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -8,10 +8,10 @@ import { Bug, DiffIcon, + EllipsisVertical, FileJson, FileUp, History, - MoreVertical, Pen, Save, WandSparkles @@ -48,7 +48,7 @@ import { aiChatManager } from '../copilot/chat/AIChatManager.svelte' import { AIBtnClasses } from '../copilot/chat/AIButtonStyle' import type { RawAppData } from './dataTableRefUtils' - + // async function hash(message) { // try { // const msgUint8 = new TextEncoder().encode(message) // encode as (utf-8) Uint8Array @@ -632,7 +632,7 @@ closeSaveDrawer()}> {#snippet actions()} -
+
+ {/snippet} @@ -795,8 +799,8 @@ jobsDrawerOpen = true }} color="light" - size="xs" - variant="border" + unifiedSize="md" + variant="default" btnClasses="relative" >
@@ -811,8 +815,7 @@
{/snippet} @@ -67,17 +71,21 @@ {#if Object.keys(runnables ?? {}).length > 0} {#each Object.entries(runnables ?? {}) as [id, runnable]} {#if runnable} - + {runnable} + isSelected={selectedRunnable === id} + onSelect={() => { + selectedRunnable = id + onSelect?.(id) + }} + onDelete={() => { + delete runnables[id] + if (selectedRunnable === id) { + selectedRunnable = undefined + } + }} + /> {/if} {/each} {:else} diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptsPanel.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptsPanel.svelte index 3477dab9ff..b9bccbb879 100644 --- a/frontend/src/lib/components/raw_apps/RawAppInlineScriptsPanel.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptsPanel.svelte @@ -9,16 +9,23 @@ appPath: string initRunnablesContent: Record /** Called when code is selected in the editor */ - onSelectionChange?: (selection: { - content: string - startLine: number - endLine: number - startColumn: number - endColumn: number - } | null) => void + onSelectionChange?: ( + selection: { + content: string + startLine: number + endLine: number + startColumn: number + endColumn: number + } | null + ) => void } - let { runnables, selectedRunnable = $bindable(), appPath, onSelectionChange }: Props = $props() + let { + runnables = $bindable(), + selectedRunnable = $bindable(), + appPath, + onSelectionChange + }: Props = $props() {#if !selectedRunnable} diff --git a/frontend/src/lib/components/raw_apps/RawAppModules.svelte b/frontend/src/lib/components/raw_apps/RawAppModules.svelte index e6da29ee38..faca222fa0 100644 --- a/frontend/src/lib/components/raw_apps/RawAppModules.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppModules.svelte @@ -1,6 +1,7 @@ - +
{#each ['direct', 'indirect', 'dev'] as type} {@const typeModules = filteredModules[type]} {#if typeModules.length > 0}
-
{type} - ({typeModules.length})
({typeModules.length})
{#each typeModules as mod (mod)} -
{mod}
{#if props.modules?.installed?.[mod]} - + {props.modules?.installed?.[mod]} {:else} @@ -72,5 +73,10 @@
{/if} {/each} + {#if filteredModules.direct.length === 0 && filteredModules.indirect.length === 0 && filteredModules.dev.length === 0} + No packages found. Edit package.json to add them. + {/if}
diff --git a/frontend/src/lib/components/raw_apps/RawAppSidebar.svelte b/frontend/src/lib/components/raw_apps/RawAppSidebar.svelte index 70ddd256c5..d98c17c45e 100644 --- a/frontend/src/lib/components/raw_apps/RawAppSidebar.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppSidebar.svelte @@ -74,47 +74,116 @@ } } - const fileTree = $derived(buildFileTree(Object.keys(files ?? {}))) + // Track pending new file/folder that hasn't been confirmed yet + let pendingNewFilePath = $state(undefined) - let pathToRename = $state(undefined) - let pathToExpand = $state(undefined) + const fileTree = $derived( + buildFileTree([ + ...Object.keys(files ?? {}), + ...(pendingNewFilePath ? [pendingNewFilePath] : []) + ]) + ) + + let pathToEdit = $state(undefined) + + // Helper to find a unique path by appending numbers if needed + function getUniquePath(basePath: string): string { + const existingPaths = new Set([...Object.keys(files ?? {}), pendingNewFilePath].filter(Boolean)) + + if (!existingPaths.has(basePath)) { + return basePath + } + + // Split path into name and extension (for files) or handle folders + const isFolder = basePath.endsWith('/') + let pathWithoutTrailing = isFolder ? basePath.slice(0, -1) : basePath + const lastSlash = pathWithoutTrailing.lastIndexOf('/') + const parentPath = pathWithoutTrailing.substring(0, lastSlash + 1) + const fileName = pathWithoutTrailing.substring(lastSlash + 1) + + let nameWithoutExt: string + let ext: string + + if (isFolder) { + nameWithoutExt = fileName + ext = '' + } else { + const dotIndex = fileName.lastIndexOf('.') + if (dotIndex > 0) { + nameWithoutExt = fileName.substring(0, dotIndex) + ext = fileName.substring(dotIndex) + } else { + nameWithoutExt = fileName + ext = '' + } + } + + // Try incrementing numbers until we find a unique path + let counter = 1 + let candidatePath: string + do { + const newName = `${nameWithoutExt} (${counter})${ext}` + candidatePath = isFolder ? `${parentPath}${newName}/` : `${parentPath}${newName}` + counter++ + } while (existingPaths.has(candidatePath)) + + return candidatePath + } function handleFileClick(path: string) { console.log('File clicked:', path) selectedDocument = path - onSelectFile?.(path) + // Only open files in the editor, not folders + if (!path.endsWith('/')) { + onSelectFile?.(path) + } } function handleAddFile(folderPath: string) { console.log('Add file to:', folderPath) - if (files) { - const nfiles = { ...files } - // Ensure folderPath ends with / - const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/' - const newPath = normalizedFolder + 'newfile.txt' - nfiles[newPath] = '' - files = nfiles - pathToRename = newPath - pathToExpand = normalizedFolder - } + // Ensure folderPath ends with / + const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/' + const basePath = normalizedFolder + 'newfile.txt' + const newPath = getUniquePath(basePath) + // Don't update files yet - just mark as pending and enter edit mode + pendingNewFilePath = newPath + pathToEdit = newPath } function handleRename(oldPath: string, newName: string) { - if (files) { - const nfiles = { ...files } - const pathParts = oldPath.split('/').filter(Boolean) - const parentPath = '/' + pathParts.slice(0, -1).join('/') - let newPath = parentPath === '/' ? '/' + newName : parentPath + '/' + newName + const pathParts = oldPath.split('/').filter(Boolean) + const parentPath = '/' + pathParts.slice(0, -1).join('/') + let newPath = parentPath === '/' ? '/' + newName : parentPath + '/' + newName - // Check if this is a folder (ends with /) - const isFolder = oldPath.endsWith('/') + // Check if this is a folder (ends with /) + const isFolder = oldPath.endsWith('/') - if (isFolder) { - // For folders, ensure new path also ends with / - if (!newPath.endsWith('/')) { - newPath = newPath + '/' - } + // For folders, ensure new path also ends with / + if (isFolder && !newPath.endsWith('/')) { + newPath = newPath + '/' + } + // Check if this is a pending new file/folder being created + const isPendingNew = pendingNewFilePath === oldPath + + // For existing items, skip if name didn't change + if (!isPendingNew && oldPath === newPath) { + pathToEdit = undefined + return + } + + if (!files) { + files = {} + } + const nfiles = { ...files } + + if (isFolder) { + if (isPendingNew) { + // Creating a new folder - just add it with the final name + nfiles[newPath] = '' + pendingNewFilePath = undefined + } else { + // Renaming existing folder const oldFolderPath = oldPath const newFolderPath = newPath @@ -138,107 +207,99 @@ nfiles[newPath] = nfiles[old] delete nfiles[old] }) - - selectedDocument = newFolderPath - } else { - // For files, simple rename - nfiles[newPath] = nfiles[oldPath] - delete nfiles[oldPath] - selectedDocument = newPath } - files = nfiles - pathToRename = undefined + selectedDocument = newPath + } else { + if (isPendingNew) { + // Creating a new file - just add it with the final name + nfiles[newPath] = '' + pendingNewFilePath = undefined + } else { + // Renaming existing file + nfiles[newPath] = nfiles[oldPath] + delete nfiles[oldPath] + } + selectedDocument = newPath + } + + files = nfiles + pathToEdit = undefined + + // Select the new file in the editor (only for files, not folders) + if (!isFolder) { + onSelectFile?.(newPath) } } function handleAddFolder(folderPath: string) { console.log('Add folder to:', folderPath) - if (files) { - const nfiles = { ...files } - // Ensure folderPath ends with / - const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/' - const newPath = normalizedFolder + 'newfolder/' - nfiles[newPath] = '' - files = nfiles - pathToRename = newPath - pathToExpand = normalizedFolder - } + // Ensure folderPath ends with / + const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/' + const basePath = normalizedFolder + 'newfolder/' + const newPath = getUniquePath(basePath) + // Don't update files yet - just mark as pending and enter edit mode + pendingNewFilePath = newPath + pathToEdit = newPath } function handleAddRootFile() { console.log('Add file to root or selected folder') - if (files) { - const nfiles = { ...files } - let newPath: string - let targetFolder: string | undefined + let basePath: string - if (selectedDocument) { - // If a folder is selected, add the file inside it - if (selectedDocument.endsWith('/')) { - newPath = selectedDocument + 'newfile.txt' - targetFolder = selectedDocument - } else { - // If a file is selected, add the new file in the same folder - const pathParts = selectedDocument.split('/').filter(Boolean) - if (pathParts.length > 1) { - // File is in a subfolder - const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/' - newPath = parentPath + 'newfile.txt' - targetFolder = parentPath - } else { - // File is at root - newPath = '/newfile.txt' - } - } + if (selectedDocument) { + // If a folder is selected, add the file inside it + if (selectedDocument.endsWith('/')) { + basePath = selectedDocument + 'newfile.txt' } else { - newPath = '/newfile.txt' - } - - nfiles[newPath] = '' - files = nfiles - pathToRename = newPath - if (targetFolder) { - pathToExpand = targetFolder + // If a file is selected, add the new file in the same folder + const pathParts = selectedDocument.split('/').filter(Boolean) + if (pathParts.length > 1) { + // File is in a subfolder + const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/' + basePath = parentPath + 'newfile.txt' + } else { + // File is at root + basePath = '/newfile.txt' + } } + } else { + basePath = '/newfile.txt' } + + const newPath = getUniquePath(basePath) + // Don't update files yet - just mark as pending and enter edit mode + pendingNewFilePath = newPath + pathToEdit = newPath } function handleAddRootFolder() { - if (files) { - const nfiles = { ...files } - let newPath: string - let targetFolder: string | undefined + let basePath: string - if (selectedDocument) { - // If a folder is selected, add the folder inside it - if (selectedDocument.endsWith('/')) { - newPath = selectedDocument + 'newfolder/' - targetFolder = selectedDocument - } else { - // If a file is selected, add the new folder in the same folder - const pathParts = selectedDocument.split('/').filter(Boolean) - if (pathParts.length > 1) { - // File is in a subfolder - const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/' - newPath = parentPath + 'newfolder/' - targetFolder = parentPath - } else { - // File is at root - newPath = '/newfolder/' - } - } + if (selectedDocument) { + // If a folder is selected, add the folder inside it + if (selectedDocument.endsWith('/')) { + basePath = selectedDocument + 'newfolder/' } else { - newPath = '/newfolder/' - } - - nfiles[newPath] = '' - files = nfiles - pathToRename = newPath - if (targetFolder) { - pathToExpand = targetFolder + // If a file is selected, add the new folder in the same folder + const pathParts = selectedDocument.split('/').filter(Boolean) + if (pathParts.length > 1) { + // File is in a subfolder + const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/' + basePath = parentPath + 'newfolder/' + } else { + // File is at root + basePath = '/newfolder/' + } } + } else { + basePath = '/newfolder/' } + + const newPath = getUniquePath(basePath) + // Don't update files yet - just mark as pending and enter edit mode + pendingNewFilePath = newPath + pathToEdit = newPath } function handleDelete(path: string) { @@ -272,26 +333,30 @@ } - + {#snippet action()}
-
- - + + + +
{/snippet} @@ -305,8 +370,12 @@ onRename={handleRename} onDelete={handleDelete} selectedPath={selectedDocument} - {pathToRename} - {pathToExpand} + {pathToEdit} + onRequestEdit={(path) => (pathToEdit = path)} + onCancelEdit={() => { + pathToEdit = undefined + pendingNewFilePath = undefined + }} /> {/each}
- + { + selectedDocument = undefined + }} +/>
- + {#snippet action()}
{historyManager.allEntries.length}/50 + btnClasses="px-1 gap-0.5" + title="Create a new snapshot" + > + + +
{/snippet} + import { Badge, Button } from '$lib/components/common' + import { Ellipsis } from 'lucide-svelte' + import DropdownV2 from '../DropdownV2.svelte' + import { twMerge } from 'tailwind-merge' + import type { Runnable } from '../apps/inputType' + + interface Props { + id: string + runnable: Runnable + isSelected: boolean + onSelect: () => void + onDelete: () => void + } + + let { id, runnable, isSelected, onSelect, onDelete }: Props = $props() + + let dropdownOpen = $state(false) + + + diff --git a/frontend/src/lib/components/text_input/TextInput.svelte b/frontend/src/lib/components/text_input/TextInput.svelte index 009401d809..bf6ba8c71c 100644 --- a/frontend/src/lib/components/text_input/TextInput.svelte +++ b/frontend/src/lib/components/text_input/TextInput.svelte @@ -57,6 +57,10 @@ inputEl?.focus() } + export function select() { + inputEl?.select() + } + let inputEl: HTMLInputElement | HTMLTextAreaElement | undefined = $state() let { diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte index f6d98a0b43..d48191bbc8 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte @@ -24,15 +24,7 @@ import Select from '$lib/components/select/Select.svelte' import Toggle from '$lib/components/Toggle.svelte' import Button from '$lib/components/common/button/Button.svelte' - import { - AlertTriangle, - Sparkles, - ArrowRight, - Plus, - List, - Ban, - ExternalLinkIcon - } from 'lucide-svelte' + import { Sparkles, Plus, List, Ban, ExternalLinkIcon } from 'lucide-svelte' import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' import RawAppDataTableList from '$lib/components/raw_apps/RawAppDataTableList.svelte' @@ -41,6 +33,7 @@ import { copilotInfo } from '$lib/aiStore' import { aiChatManager, AIMode } from '$lib/components/copilot/chat/AIChatManager.svelte' import TextInput from '$lib/components/text_input/TextInput.svelte' + import { Alert } from '$lib/components/common' let nodraft = $page.url.searchParams.get('nodraft') const templatePath = $page.url.searchParams.get('template') @@ -366,7 +359,7 @@
-

Summary

+

Summary

-
-

Framework

+
+

Framework

{#each templates as t, i} {/each}
-
-

Data Configuration

+
+

Data configuration

{#if hasNoDatatables} -
- -
- No datatables configured. - You can still create an app, but for data storage you won't be able to use data tables - which are HIGHLY RECOMMENDED. -
+ + You can still create an app, but for data storage you won't be able to use data tables + which are highly recommended. +
- {#if $userStore?.is_admin} - Configure datatables in - workspace settings - to enable this feature. - {:else} - - Ask your workspace admin to configure datatables in workspace settings to enable - this feature. - - {/if} -
-
+ {#if $userStore?.is_admin} + Configure datatables in + workspace settings + to enable this feature. + {:else} + Ask your workspace admin to configure datatables in workspace settings to enable this + feature. + {/if} + {:else}
-
- Default settings for new tables -
-
- +
+
+ Schema -
-
- - {#snippet children({ item })} - - - - {/snippet} - -
- {#if schemaMode === 'new'} - (userEditedSchemaName = true) - }} - class="flex-1" - error={newSchemaAlreadyExists} - /> - {:else if schemaMode === 'existing'} -
- +
+ {/if} +
+ {#if newSchemaAlreadyExists} + Schema "{newSchemaName}" already exists {/if}
- {#if newSchemaAlreadyExists} - Schema "{newSchemaName}" already exists - {/if}
@@ -502,7 +494,7 @@
-
+
-
-

- +
+

+ Start with AI (optional)

{#if !isAiEnabled} -
- -
- AI is not configured for this workspace. You can still create an app manually but using AI is highly recommended. -
- {#if $userStore?.is_admin} - Configure AI in - workspace settings - - to enable this feature. - {:else} - Ask your workspace admin to configure AI in workspace settings to enable this - feature. - {/if} -
-
+ + You can still create an app manually but using AI is highly recommended. +
+ {#if $userStore?.is_admin} + Configure AI in + workspace settings + + to enable this feature. + {:else} + Ask your workspace admin to configure AI in workspace settings to enable this feature. + {/if} +
{:else}
-
+
{#if isAiEnabled} diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/add/templates.ts b/frontend/src/routes/(root)/(logged)/apps_raw/add/templates.ts index a6d149ee4a..71225f9ce1 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/add/templates.ts +++ b/frontend/src/routes/(root)/(logged)/apps_raw/add/templates.ts @@ -37,21 +37,35 @@ const App = () => { }; export default App; -`; +` -const appSvelte = ` +

Hello {name}

+ + + + + {#if result} +

Result: {result}

+ {/if}
-` +` const indexSvelte = ` import { mount } from 'svelte'; @@ -81,13 +95,13 @@ createApp(App).mount('#root')` const indexCss = `.myclass { border: 1px solid gray; padding: 2px; -}`; +}` export const react19Template = { - '/index.tsx': reactIndex, - '/App.tsx': appTsx, - '/index.css': indexCss, - '/package.json': `{ + '/index.tsx': reactIndex, + '/App.tsx': appTsx, + '/index.css': indexCss, + '/package.json': `{ "dependencies": { "react": "19.0.0", "react-dom": "19.0.0", @@ -97,14 +111,14 @@ export const react19Template = { "@types/react-dom": "^19.0.0", "@types/react": "^19.0.0" } -}`, +}` } export const react18Template = { - '/index.tsx': reactIndex, - '/App.tsx': appTsx, - '/index.css': indexCss, - '/package.json': `{ + '/index.tsx': reactIndex, + '/App.tsx': appTsx, + '/index.css': indexCss, + '/package.json': `{ "dependencies": { "react": "18.3.1", "react-dom": "18.3.1" @@ -113,31 +127,31 @@ export const react18Template = { "@types/react-dom": "^19.0.0", "@types/react": "^19.0.0" } -}`, +}` } export const svelte5Template = { - '/index.ts': indexSvelte, - '/App.svelte': appSvelte, - '/index.css': indexCss, - '/package.json': `{ + '/index.ts': indexSvelte, + '/App.svelte': appSvelte, + '/index.css': indexCss, + '/package.json': `{ "dependencies": { "svelte": "5.45.2", "windmill-client": "^1" } -}`, +}` } export const vueTemplate = { - '/index.ts': indexVue, - '/App.vue': appVue, - '/index.css': indexCss, - '/package.json': `{ + '/index.ts': indexVue, + '/App.vue': appVue, + '/index.css': indexCss, + '/package.json': `{ "dependencies": { "core-js": "3.26.1", "vue": "3.5.13" } -}`, +}` } export const appVueRouter = `