mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
* feat(editors): responsive top-bars + script test-pane pixel-min + flow graph overlay
Editor top bars now collapse on narrow widths (measured via container
clientWidth, not viewport — they live inside drawers / session panes
where the viewport stays wide):
- FlowBuilder: Diff + Save draft fold into the ellipsis menu when
the top bar narrows below 720px (Save draft keeps its ⌘S / Ctrl+S
shortcut indicator). Test-flow button moves out of the top bar
and into a graph-pane overlay matching the dev page; the overlay
position flips from top-2 right-2 to top-14 left-1/2 when the
graph pane itself is narrower than 800px. FlowEditor exposes a
graphOverlay snippet prop for that.
- ScriptBuilder: Settings + Draft labels collapse to icon-only;
a new DropdownV2 ellipsis surfaces Tag / Settings / Save draft
when even icons don't fit. The ellipsis itself uses variant=subtle.
- AppEditorHeader / RawAppEditorHeader: fullscreen / dark-mode /
breakpoint toggle group + Debug-runs / Jobs buttons hide; Save
draft moves into the Deploy dropdown.
- EditorBar: a "Helpers" DropdownV2 collapse for Context var /
Variable / S3 / Resource / Git repo / Resource type / Database /
Ducklake / Data table / Reset when the bar narrows below 800px
(EDITOR_BAR_HELPERS_COMPACT_THRESHOLD). Above that, the existing
icon-only mode (1420px threshold) still applies.
- ScriptEditor's test pane gets a pixel-based minimum width (400px)
derived from the splitpane's clientWidth. The Pane uses Svelte 5
function-binding so the splitter writes to a raw $state while the
splitpane reads the clamped derived value — no $effect, no
release-time bounce, drag stops at the boundary. Cap raised to
80% so the test pane can take most of the editor on very narrow
layouts while leaving a sliver of code visible.
- VS Code button on ScriptEditor: collapses to icon-only below the
EDITOR_BAR_WIDTH_THRESHOLD (1420px) instead of being hidden
entirely by viewport `lg:` breakpoint; hidden completely when the
editor is rendered inside a session pane.
- AI wand button on ScriptEditor + RawAppEditorHeader: hidden inside
a session pane (detected via `getContext('aiChatManager')`) — the
session owns its own AI chat.
- DeployButton: drops the unused `newFlow` gate (callers updated).
- FlowDiffViewer / FlowGraphDiffViewer: inlineDiff prop forwarding
+ onHeight callback on FlowGraphV2 so diff viewers can equalize
side-by-side graph heights.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui: unify Debug / wand / test-toggle button sizes + HideButton defaults
Two small consistency passes on shared button components:
- ScriptEditor's Debug, AI wand and Test-panel-toggle buttons all
use unifiedSize="sm" so they line up in the toolbar; Test toggle
switches from custom marine btnClasses to variant="accent-secondary".
HideButton gains a passthrough unifiedSize prop so the wand and
test toggle can match Debug without overriding btnClasses.
- HideButton's own defaults shift to variant="subtle" + sm
unifiedSize, dropping the legacy color="light" / variant="contained"
+ tailwind-merge background overlay; the selected (hidden) state
is now a tinted wrapper div instead of overriding btnClasses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui(script): editor toolbar polish
Small consistency tweaks on the script editor's top-right overlay:
- Lowercase "test" / "Exit debug" panel labels.
- `bg-surface` on the overlay container so the absolute-positioned
buttons read as a single panel over the graph rather than disjoint
pills.
- Debug button picks up `destructive={debugMode}` so the active
state reads as "you're in debug mode" instead of accent.
- Console and "Delegating to git repo" buttons drop the custom
`btnClasses` border-on-surface treatment and switch from
`size="xs"` to `unifiedSize="sm"` so they match the other buttons
in the cluster.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(raw-app): collapsible file sidebar, default-collapsed in session preview
The raw-app editor's left sidebar (file tree, runnables, history) ate a
lot of horizontal space — fine in the standalone editor, painful in the
session preview pane where the chat is already taking half the screen.
Add a small collapse / expand toggle. Persist the user's preference in
localStorage so it sticks across opens.
Two independent localStorage keys via the new `sidebarStorageKey` prop:
- standalone editor: `raw-app-sidebar-collapsed` (default expanded)
- session preview: `raw-app-sidebar-collapsed-preview` (default collapsed)
Otherwise the two contexts would race for the same key — whichever
opens first would dictate the other's default. Splitting the keys lets
each have its own remembered preference.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fixup(editors): use untyped getContext for AI-chat-manager session detection
The cherry-picks landed `getContext<AIChatManager>('aiChatManager')` to
hide per-editor AI/VSCode buttons when rendered inside a session pane.
The `AIChatManager` class is exported only on the sessions branch (used
for typing session-provided manager overrides). On `main` the manager
file exports only the singleton instance, so importing the class fails
the type-check.
The session-pane detection just needs a truthy/falsy probe — drop the
type parameter and the class import. `inSessionPane` ends up as
`getContext('aiChatManager')` (returns `unknown`, coerced to boolean
via `!!`). Same runtime behaviour, no class-export dependency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ui(editors): flow test in top bar; ellipsis folds draft/jobs/tutorials
* refactor(app-editor): drop dead AppEditorTutorial button path
* ui(editors): wire compactHelpers in flow-step + raw-app inline editors
* ui(raw-app): sidebar Cmd/Ctrl+B toggle + uppercase section titles
* ui(editors): keep Diff/Settings inline as icon-only when narrow
* fix(editors): address review nits on test-pane/Helpers/thresholds
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
218 lines
6.1 KiB
Svelte
218 lines
6.1 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/components/common'
|
|
import { Plus, File, AlertCircle, AlertTriangle } from 'lucide-svelte'
|
|
import PanelSection, {
|
|
SUBTLE_PANEL_TITLE
|
|
} from '../apps/editor/settingsPanel/common/PanelSection.svelte'
|
|
import Popover from '../Popover.svelte'
|
|
|
|
import type { Runnable } from '../apps/inputType'
|
|
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
|
|
runnables: Record<string, Runnable>
|
|
onSelect?: (id: string) => void
|
|
}
|
|
|
|
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())
|
|
|
|
$effect(() => {
|
|
const unsubscribe = rawAppLintStore.subscribe((s) => {
|
|
lintSnapshot = s
|
|
})
|
|
return unsubscribe
|
|
})
|
|
|
|
const errorCount = $derived(
|
|
Object.values(lintSnapshot.errors).reduce((acc, arr) => acc + arr.length, 0)
|
|
)
|
|
const warningCount = $derived(
|
|
Object.values(lintSnapshot.warnings).reduce((acc, arr) => acc + arr.length, 0)
|
|
)
|
|
|
|
function createBackgroundScript() {
|
|
const nid = getNextId(Object.keys(runnables ?? {}))
|
|
const newScriptPath = `Backend Runnable ${nid}`
|
|
runnables[nid] = {
|
|
name: newScriptPath,
|
|
inlineScript: undefined,
|
|
type: 'inline'
|
|
}
|
|
|
|
selectedRunnable = nid
|
|
onSelect?.(nid)
|
|
}
|
|
</script>
|
|
|
|
<PanelSection
|
|
size="sm"
|
|
fullHeight={false}
|
|
title="backend"
|
|
titleClass={SUBTLE_PANEL_TITLE}
|
|
id="app-editor-runnable-panel"
|
|
>
|
|
{#snippet action()}
|
|
<div class="flex flex-row gap-1">
|
|
<Button
|
|
unifiedSize="xs"
|
|
variant="subtle"
|
|
title="Create a new background runnable"
|
|
aria-label="Create a new background runnable"
|
|
on:click={createBackgroundScript}
|
|
id="create-background-runnable"
|
|
btnClasses="gap-0.5 px-1"
|
|
>
|
|
<Plus size={12} />
|
|
<File size={12} />
|
|
</Button>
|
|
</div>
|
|
{/snippet}
|
|
<div class="w-full flex flex-col gap-6 py-1">
|
|
<div>
|
|
<div class="flex flex-col gap-1 w-full">
|
|
{#if Object.keys(runnables ?? {}).length > 0}
|
|
{#each Object.entries(runnables ?? {}) as [id, runnable]}
|
|
{#if runnable}
|
|
<RunnableRow
|
|
{id}
|
|
{runnable}
|
|
isSelected={selectedRunnable === id}
|
|
isEditing={editingId === id}
|
|
onSelect={() => {
|
|
selectedRunnable = id
|
|
onSelect?.(id)
|
|
}}
|
|
onDelete={() => {
|
|
delete runnables[id]
|
|
if (selectedRunnable === id) {
|
|
selectedRunnable = undefined
|
|
}
|
|
}}
|
|
onRename={(newId) => renameRunnable(id, newId)}
|
|
onRequestEdit={() => (editingId = id)}
|
|
onCancelEdit={() => (editingId = undefined)}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
{:else}
|
|
<div class="text-xs text-primary">No backend runnable</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if errorCount > 0 || warningCount > 0}
|
|
<Popover notClickable placement="right">
|
|
<div
|
|
class="flex items-center gap-2 px-2 py-1.5 mt-2 rounded border text-xs
|
|
{errorCount > 0
|
|
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
|
|
: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800'}"
|
|
>
|
|
{#if errorCount > 0}
|
|
<span class="flex items-center gap-1 text-red-600 dark:text-red-400">
|
|
<AlertCircle size={14} />
|
|
{errorCount} error{errorCount !== 1 ? 's' : ''}
|
|
</span>
|
|
{/if}
|
|
{#if warningCount > 0}
|
|
<span class="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
|
|
<AlertTriangle size={14} />
|
|
{warningCount} warning{warningCount !== 1 ? 's' : ''}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{#snippet text()}
|
|
<div class="max-w-md max-h-80 overflow-auto text-xs">
|
|
{#if errorCount > 0}
|
|
<div class="mb-2">
|
|
<div class="font-semibold text-red-600 dark:text-red-400 mb-1">Errors</div>
|
|
{#each Object.entries(lintSnapshot.errors) as [key, errors]}
|
|
{#if errors.length > 0}
|
|
<div class="mb-1">
|
|
<span class="font-medium text-secondary">{key}:</span>
|
|
<ul class="ml-2 list-disc list-inside">
|
|
{#each errors as error}
|
|
<li class="text-tertiary">
|
|
Line {error.startLineNumber}: {error.message}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{#if warningCount > 0}
|
|
<div>
|
|
<div class="font-semibold text-yellow-600 dark:text-yellow-400 mb-1">Warnings</div
|
|
>
|
|
{#each Object.entries(lintSnapshot.warnings) as [key, warnings]}
|
|
{#if warnings.length > 0}
|
|
<div class="mb-1">
|
|
<span class="font-medium text-secondary">{key}:</span>
|
|
<ul class="ml-2 list-disc list-inside">
|
|
{#each warnings as warning}
|
|
<li class="text-tertiary">
|
|
Line {warning.startLineNumber}: {warning.message}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</PanelSection>
|