diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index e3215aa9cf..1cabcf8265 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -81,6 +81,7 @@ import type { ScriptEditorWhitelabelCustomUi } from './custom_ui' import Tabs from './common/tabs/Tabs.svelte' import Tab from './common/tabs/Tab.svelte' + import DraggableTabs, { type TabItem } from './common/tabs/DraggableTabs.svelte' import { slide } from 'svelte/transition' import CaptureTable from '$lib/components/triggers/CaptureTable.svelte' import CaptureButton from './triggers/CaptureButton.svelte' @@ -407,6 +408,30 @@ let supportsModules = $derived((lang === 'bun' || lang === 'python3') && isWacV2) let mainFileName = $derived('script.' + langToExt(scriptLangToEditorLang(lang))) + // A module path always ends in a file extension, so this id can't collide with one. + const MAIN_FILE_TAB_ID = '__main__' + // Tab order is view state only: `modules` is stored as JSONB, which re-sorts its keys on + // deploy, so an order written into it would not survive. + let moduleTabOrder: string[] = $state([]) + let fileTabs = $derived.by(() => { + const paths = Object.keys(modules ?? {}) + const rank = new Map(paths.map((p, i) => [p, i + moduleTabOrder.length])) + moduleTabOrder.forEach((p, i) => rank.has(p) && rank.set(p, i)) + return [ + { id: MAIN_FILE_TAB_ID, label: mainFileName, closable: false, pinned: 'left' }, + // Not closable through the strip: closing deletes the module's code, so it stays on + // the explicit × rather than Delete/Backspace and middle-click. + ...paths + .sort((a, b) => rank.get(a)! - rank.get(b)!) + .map((modulePath) => ({ + id: modulePath, + label: modulePath, + title: modulePath, + closable: false + })) + ] + }) + let modulePathInput = $state('') let showAddModulePopover = $state(false) let modulePathInputEl: HTMLInputElement | undefined = $state(undefined) @@ -561,6 +586,7 @@ delete modules[modulePath] delete moduleTestState[modulePath] modules = { ...modules } + moduleTabOrder = moduleTabOrder.filter((p) => p !== modulePath) } function validateRenameModulePath(newPath: string, oldPath: string): string { @@ -591,6 +617,11 @@ return 'path' in canonical && canonical.path === oldPath } + function prefillRename(modulePath: string) { + renameModuleInput = modulePath + renameModuleError = '' + } + function renameModule(oldPath: string) { if (!renameModuleInput.trim()) return const canonical = canonicalModulePath(renameModuleInput) @@ -611,6 +642,7 @@ delete modules[oldPath] modules[newPath] = { ...mod, language: newLang ?? mod.language } modules = { ...modules } + moduleTabOrder = moduleTabOrder.map((p) => (p === oldPath ? newPath : p)) if (moduleTestState[oldPath]) { moduleTestState[newPath] = moduleTestState[oldPath] delete moduleTestState[oldPath] @@ -2563,93 +2595,71 @@ {#snippet editorContent()}
{#if supportsModules} -
(id === MAIN_FILE_TAB_ID ? switchToMain() : switchToModule(id))} + onReorder={(next) => (moduleTabOrder = next.map((t) => t.id))} + class="shrink-0 border-b border-light bg-surface-secondary/50" > - - {#each Object.keys(modules ?? {}) as modulePath} -
- -
+ {#snippet tabAccessory(tab)} + {#if tab.id !== MAIN_FILE_TAB_ID} + + e.stopPropagation()}> prefillRename(tab.id), + onkeydown: (e: KeyboardEvent) => + (e.key === 'Enter' || e.key === ' ') && prefillRename(tab.id) + }} > {#snippet trigger()} { - e.stopPropagation() - renameModuleInput = modulePath - renameModuleError = '' - }} - onkeydown={(e) => { - if (e.key === 'Enter') { - e.stopPropagation() - renameModuleInput = modulePath - renameModuleError = '' - } - }} + class="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 rounded hover:bg-surface-hover w-4 h-4 inline-flex items-center justify-center" > - + {/snippet} {#snippet content({ close })} - {@render renameModuleForm(modulePath, close)} + {@render renameModuleForm(tab.id, close)} {/snippet} - { - e.stopPropagation() - removeModule(modulePath) - }} - onkeydown={(e) => { - if (e.key === 'Enter') { - e.stopPropagation() - removeModule(modulePath) - } - }} - > - - -
-
- {/each} - - {#snippet trigger()} - - - {/snippet} - {#snippet content({ close })} - {@render addModuleForm(close)} - {/snippet} - -
+