fix: allow renaming of backend runnables in the UI

This commit is contained in:
Ruben Fiszel
2026-02-13 21:43:13 +00:00
parent 33ab90c536
commit 61aa2aad7d
3 changed files with 153 additions and 90 deletions
@@ -6,9 +6,8 @@
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Modal from '$lib/components/common/modal/Modal.svelte'
import { LayoutDashboard, Loader2, Plus, Code2, FlaskConical } from 'lucide-svelte'
import { LayoutDashboard, Loader2, Plus, Code2 } from 'lucide-svelte'
import { importStore } from '../apps/store'
import Badge from '$lib/components/common/badge/Badge.svelte'
import YAML from 'yaml'
let drawer: Drawer | undefined = undefined
@@ -16,9 +15,7 @@
let importType: 'yaml' | 'json' = $state('yaml')
// Modal states
let appTypeModalOpen = $state(false)
let featurePreviewModalOpen = $state(false)
async function importRaw() {
$importStore = importType === 'yaml' ? YAML.parse(pendingRaw) : JSON.parse(pendingRaw)
@@ -37,11 +34,6 @@
function selectFullCode() {
appTypeModalOpen = false
featurePreviewModalOpen = true
}
function confirmFullCode() {
featurePreviewModalOpen = false
goto(`${base}/apps_raw/add?nodraft=true`)
}
</script>
@@ -116,59 +108,16 @@
<h3 class="font-semibold text-primary">Full-code App</h3>
<p class="text-xs text-tertiary mt-1">
Build with React or Svelte with full control and a powerful AI agent.
<br /><br />
Better for complex apps or apps that require full flexibility and control.
</p>
</div>
<br /><br />
<div class="flex flex-row items-center gap-2">
<Badge color="yellow">
<FlaskConical size={12} class="mr-1" />
Preview (unstable)
</Badge>
</div>
</button>
</div>
</div>
</Modal>
<!-- Feature Preview Warning Modal -->
<Modal kind="X" bind:open={featurePreviewModalOpen} title="Feature Preview Notice">
<div class="flex flex-col gap-4">
<div
class="flex items-start gap-3 p-4 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800"
>
<FlaskConical size={24} class="text-yellow-600 dark:text-yellow-400 shrink-0 mt-0.5" />
<div class="text-sm text-yellow-800 dark:text-yellow-200">
<p class="font-semibold mb-2">Full-code Apps are in Feature Preview</p>
<p>
This feature is not yet stable and may change significantly before it reaches General
Availability (GA), which is expected in <strong>January 2026</strong>.
</p>
<ul class="list-disc list-inside mt-2 space-y-1 text-yellow-700 dark:text-yellow-300">
<li>APIs and interfaces may change without notice</li>
<li>Some functionality may be incomplete or buggy</li>
<li>Not recommended for production use yet</li>
</ul>
</div>
</div>
<div class="flex justify-end gap-3 pt-2">
<Button
variant="default"
unifiedSize="md"
onClick={() => {
featurePreviewModalOpen = false
appTypeModalOpen = true
}}
>
Go back
</Button>
<Button unifiedSize="md" variant="accent" on:click={confirmFullCode}
>I understand, continue</Button
>
</div>
</div>
</Modal>
<!-- Raw JSON -->
<Drawer bind:this={drawer} size="800px">
<DrawerContent
@@ -5,9 +5,10 @@
import Popover from '../Popover.svelte'
import type { Runnable } from '../apps/inputType'
import { getNextId } from '$lib/components/flows/idUtils'
import { getNextId, forbiddenIds } from '$lib/components/flows/idUtils'
import { rawAppLintStore } from './lintStore'
import RunnableRow from './RunnableRow.svelte'
import { sendUserToast } from '$lib/toast'
interface Props {
selectedRunnable: string | undefined
@@ -17,6 +18,46 @@
let { selectedRunnable = $bindable(), runnables, onSelect }: Props = $props()
let editingId: string | undefined = $state(undefined)
const validIdPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/
function renameRunnable(oldId: string, newId: string) {
if (newId === oldId) {
editingId = undefined
return
}
if (!newId) {
sendUserToast('ID cannot be empty', true)
editingId = undefined
return
}
if (!validIdPattern.test(newId)) {
sendUserToast('ID must be a valid identifier (letters, digits, underscores)', true)
editingId = undefined
return
}
if (newId in runnables) {
sendUserToast(`ID "${newId}" is already in use`, true)
editingId = undefined
return
}
if (forbiddenIds.includes(newId)) {
sendUserToast(`"${newId}" is a reserved keyword`, true)
editingId = undefined
return
}
runnables[newId] = runnables[oldId]
delete runnables[oldId]
if (selectedRunnable === oldId) {
selectedRunnable = newId
onSelect?.(newId)
}
editingId = undefined
}
// Subscribe to lint store for reactive updates
let lintSnapshot = $state(rawAppLintStore.getSnapshot())
@@ -75,6 +116,7 @@
{id}
{runnable}
isSelected={selectedRunnable === id}
isEditing={editingId === id}
onSelect={() => {
selectedRunnable = id
onSelect?.(id)
@@ -85,6 +127,9 @@
selectedRunnable = undefined
}
}}
onRename={(newId) => renameRunnable(id, newId)}
onRequestEdit={() => (editingId = id)}
onCancelEdit={() => (editingId = undefined)}
/>
{/if}
{/each}
@@ -1,53 +1,122 @@
<script lang="ts">
import { Badge, Button } from '$lib/components/common'
import { Ellipsis } from 'lucide-svelte'
import { Ellipsis, Pencil } from 'lucide-svelte'
import DropdownV2 from '../DropdownV2.svelte'
import { twMerge } from 'tailwind-merge'
import type { Runnable } from '../apps/inputType'
import TextInput from '../text_input/TextInput.svelte'
import { tick } from 'svelte'
interface Props {
id: string
runnable: Runnable
isSelected: boolean
isEditing: boolean
onSelect: () => void
onDelete: () => void
onRename: (newId: string) => void
onRequestEdit: () => void
onCancelEdit: () => void
}
let { id, runnable, isSelected, onSelect, onDelete }: Props = $props()
let {
id,
runnable,
isSelected,
isEditing,
onSelect,
onDelete,
onRename,
onRequestEdit,
onCancelEdit
}: Props = $props()
let dropdownOpen = $state(false)
let editValue = $state(id)
let textInputElement: TextInput | undefined = $state()
function finishEdit() {
const trimmed = editValue.trim()
if (trimmed && trimmed !== id) {
onRename(trimmed)
} else {
onCancelEdit()
}
}
function handleInputKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
finishEdit()
} else if (e.key === 'Escape') {
editValue = id
onCancelEdit()
}
}
$effect(() => {
if (isEditing && textInputElement) {
editValue = id
tick().then(() => {
textInputElement?.focus()
textInputElement?.select()
})
}
})
</script>
<button
{id}
class="relative w-full gap-1 flex items-center h-6 rounded-md px-1 group
{isSelected ? 'bg-surface-accent-selected text-accent' : 'hover:bg-surface-hover'}"
onclick={onSelect}
>
<Badge color="indigo" class={isSelected ? 'bg-surface-tertiary' : ''}>{id}</Badge>
<span class="text-xs truncate font-normal pr-6">{runnable?.name}</span>
<DropdownV2
items={[
{
displayName: 'Delete',
action: onDelete,
type: 'delete'
}
]}
class={twMerge(
'absolute -translate-y-1/2 top-1/2 right-1 hidden group-hover:block',
dropdownOpen ? 'block' : ''
)}
bind:open={dropdownOpen}
{#if isEditing}
<div
class="w-full flex items-center gap-1 h-6 rounded-md px-1
{isSelected ? 'bg-surface-accent-selected text-accent' : ''}"
>
{#snippet buttonReplacement()}
<Button
iconOnly
unifiedSize="xs"
variant="subtle"
startIcon={{ icon: Ellipsis }}
nonCaptureEvent
/>
{/snippet}
</DropdownV2>
</button>
<TextInput
bind:this={textInputElement}
bind:value={editValue}
inputProps={{
onkeydown: handleInputKeydown,
onblur: () => finishEdit(),
type: 'text'
}}
size="xs"
/>
</div>
{:else}
<button
{id}
class="relative w-full gap-1 flex items-center h-6 rounded-md px-1 group
{isSelected ? 'bg-surface-accent-selected text-accent' : 'hover:bg-surface-hover'}"
onclick={onSelect}
>
<Badge color="indigo" class={isSelected ? 'bg-surface-tertiary' : ''}>{id}</Badge>
<span class="text-xs truncate font-normal pr-6">{runnable?.name}</span>
<DropdownV2
items={[
{
displayName: 'Rename',
icon: Pencil,
action: () => onRequestEdit()
},
{
displayName: 'Delete',
action: onDelete,
type: 'delete'
}
]}
class={twMerge(
'absolute -translate-y-1/2 top-1/2 right-1 hidden group-hover:block',
dropdownOpen ? 'block' : ''
)}
bind:open={dropdownOpen}
>
{#snippet buttonReplacement()}
<Button
iconOnly
unifiedSize="xs"
variant="subtle"
startIcon={{ icon: Ellipsis }}
nonCaptureEvent
/>
{/snippet}
</DropdownV2>
</button>
{/if}