fix(frontend): improve raw app history (#7625)
* fix raw app header overflow * update ui-builder hash * Make monaco default size match brand guidelines * nit * Move run button to test panel * wip improve history * add current checkout point * fix logic to switch wetween history state * improve history visualisation * improve animations * nit * remove test page * fix timing issue when selecty history entries * update ui_builder hash * remove dev file * nit * revert setting editor font to 13.5 px * update ui builder --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
@@ -20,7 +20,7 @@ console.log('Running postinstall for root project');
|
|||||||
|
|
||||||
import { x } from 'tar'
|
import { x } from 'tar'
|
||||||
|
|
||||||
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-c37c2f6.tar.gz'
|
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-b0e4676.tar.gz'
|
||||||
const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz')
|
const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz')
|
||||||
const extractTo = path.join(process.cwd(), 'static/ui_builder/')
|
const extractTo = path.join(process.cwd(), 'static/ui_builder/')
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/common'
|
import { Button, ButtonType } from '$lib/components/common'
|
||||||
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
|
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
|
||||||
|
|
||||||
export let isLoading
|
interface Props {
|
||||||
export let hideShortcut = false
|
isLoading: boolean
|
||||||
export let onRun: () => Promise<void>
|
hideShortcut?: boolean
|
||||||
export let onCancel: () => Promise<void>
|
onRun: () => Promise<void>
|
||||||
|
onCancel: () => Promise<void>
|
||||||
|
size?: ButtonType.UnifiedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isLoading, hideShortcut = false, onRun, onCancel, size = 'sm' }: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !isLoading}
|
{#if !isLoading}
|
||||||
<Button
|
<Button
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
size="sm"
|
unifiedSize={size}
|
||||||
variant="accent"
|
variant="accent"
|
||||||
btnClasses="!px-2 !py-1"
|
onClick={() => onRun()}
|
||||||
on:click={() => onRun()}
|
|
||||||
shortCut={{ Icon: CornerDownLeft, hide: hideShortcut }}
|
shortCut={{ Icon: CornerDownLeft, hide: hideShortcut }}
|
||||||
>
|
>
|
||||||
Run
|
Run
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button unifiedSize={size} variant="accent" destructive onClick={() => onCancel()}>
|
||||||
size="sm"
|
|
||||||
variant="accent"
|
|
||||||
destructive
|
|
||||||
btnClasses="!px-2 !py-1"
|
|
||||||
on:click={() => onCancel()}
|
|
||||||
>
|
|
||||||
<Loader2 size={14} class="animate-spin mr-2" />
|
<Loader2 size={14} class="animate-spin mr-2" />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -297,7 +297,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let fontSize = $derived(small ? 12 : 14)
|
let fontSize = $derived(small ? 12 : 13.5)
|
||||||
|
|
||||||
async function loadMonaco() {
|
async function loadMonaco() {
|
||||||
setMonacoJsonOptions()
|
setMonacoJsonOptions()
|
||||||
|
|||||||
@@ -190,6 +190,20 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFilesAndSelectInIframe(newFiles: Record<string, string>, pathToSelect: string) {
|
||||||
|
const files = Object.fromEntries(
|
||||||
|
Object.entries(newFiles).filter(([path, _]) => !path.endsWith('/'))
|
||||||
|
)
|
||||||
|
iframe?.contentWindow?.postMessage(
|
||||||
|
{
|
||||||
|
type: 'setFilesAndSelect',
|
||||||
|
files: files,
|
||||||
|
pathToSelect: pathToSelect
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function populateRunnables() {
|
function populateRunnables() {
|
||||||
iframe?.contentWindow?.postMessage(
|
iframe?.contentWindow?.postMessage(
|
||||||
{
|
{
|
||||||
@@ -298,9 +312,9 @@
|
|||||||
files = {}
|
files = {}
|
||||||
}
|
}
|
||||||
files[path] = content
|
files[path] = content
|
||||||
setFilesInIframe(files)
|
|
||||||
selectedDocument = path
|
selectedDocument = path
|
||||||
handleSelectFile(path)
|
// Use combined setFilesAndSelect to avoid race condition
|
||||||
|
setFilesAndSelectInIframe(files, path)
|
||||||
return lint()
|
return lint()
|
||||||
},
|
},
|
||||||
deleteFrontendFile: (path) => {
|
deleteFrontendFile: (path) => {
|
||||||
@@ -733,9 +747,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleHistorySelect(id: number) {
|
function handleHistorySelect(id: number) {
|
||||||
// Create a snapshot if we have pending changes before navigating
|
// Save current state temporarily (not as a snapshot) when navigating to history
|
||||||
if (historyManager.needsSnapshotBeforeNav) {
|
// Only if we're currently at the "current" state (not already viewing history)
|
||||||
historyManager.manualSnapshot(files ?? {}, runnables, summary, data)
|
if (historyManager.selectedEntryId === undefined) {
|
||||||
|
historyManager.saveTemporaryCurrentState(files ?? {}, runnables, summary, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = historyManager.selectEntry(id)
|
const entry = historyManager.selectEntry(id)
|
||||||
@@ -756,19 +771,15 @@
|
|||||||
summary = entry.summary
|
summary = entry.summary
|
||||||
data = structuredClone($state.snapshot(entry.data))
|
data = structuredClone($state.snapshot(entry.data))
|
||||||
|
|
||||||
setFilesInIframe(entry.files)
|
// If there's a selected document that exists in the new files, use the combined message
|
||||||
populateRunnables()
|
|
||||||
|
|
||||||
// Re-select the current document if it exists in the new files
|
|
||||||
if (selectedDocument && entry.files[selectedDocument] !== undefined) {
|
if (selectedDocument && entry.files[selectedDocument] !== undefined) {
|
||||||
iframe?.contentWindow?.postMessage(
|
// Use combined setFilesAndSelect message to avoid race condition
|
||||||
{
|
setFilesAndSelectInIframe(entry.files, selectedDocument)
|
||||||
type: 'selectFile',
|
} else {
|
||||||
path: selectedDocument
|
// Otherwise just set files normally
|
||||||
},
|
setFilesInIframe(entry.files)
|
||||||
'*'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
populateRunnables()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to apply entry:', error)
|
console.error('Failed to apply entry:', error)
|
||||||
sendUserToast('Failed to apply entry: ' + (error as Error).message, true)
|
sendUserToast('Failed to apply entry: ' + (error as Error).message, true)
|
||||||
@@ -819,8 +830,8 @@
|
|||||||
onRedo={handleRedo}
|
onRedo={handleRedo}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Splitpanes id="o2" class="grow">
|
<Splitpanes id="o2" class="grow min-h-0">
|
||||||
<Pane bind:size={sidebarPanelSize} maxSize={20}>
|
<Pane bind:size={sidebarPanelSize} maxSize={20} class="h-full overflow-y-auto">
|
||||||
<RawAppSidebar
|
<RawAppSidebar
|
||||||
bind:files={
|
bind:files={
|
||||||
() => files,
|
() => files,
|
||||||
@@ -855,6 +866,15 @@
|
|||||||
{historyManager}
|
{historyManager}
|
||||||
historySelectedId={historyManager.selectedEntryId}
|
historySelectedId={historyManager.selectedEntryId}
|
||||||
onHistorySelect={handleHistorySelect}
|
onHistorySelect={handleHistorySelect}
|
||||||
|
onHistorySelectCurrent={() => {
|
||||||
|
// Restore the temporary current state if it exists
|
||||||
|
const tempState = historyManager.getAndClearTemporaryState()
|
||||||
|
if (tempState) {
|
||||||
|
applyEntry(tempState)
|
||||||
|
}
|
||||||
|
// Clear selection to indicate we're at current state
|
||||||
|
historyManager.clearSelection()
|
||||||
|
}}
|
||||||
onManualSnapshot={() => {
|
onManualSnapshot={() => {
|
||||||
historyManager.manualSnapshot(files ?? {}, runnables, summary, data, true)
|
historyManager.manualSnapshot(files ?? {}, runnables, summary, data, true)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HistoryEntry, HistoryBranch } from './RawAppHistoryManager.svelte'
|
import type { HistoryEntry, HistoryBranch } from './RawAppHistoryManager.svelte'
|
||||||
import { classNames, displayDate } from '$lib/utils'
|
import { displayDate } from '$lib/utils'
|
||||||
import { GitBranch } from 'lucide-svelte'
|
import { Circle, CircleDot, CircleDotDashed, CircleDashed } from 'lucide-svelte'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
entries: HistoryEntry[]
|
entries: HistoryEntry[]
|
||||||
branches: HistoryBranch[]
|
branches: HistoryBranch[]
|
||||||
selectedId: number | undefined
|
selectedId: number | undefined
|
||||||
onSelect: (id: number) => void
|
onSelect: (id: number) => void
|
||||||
|
onSelectCurrent?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { entries, branches, selectedId, onSelect }: Props = $props()
|
let { entries, branches, selectedId, onSelect, onSelectCurrent }: Props = $props()
|
||||||
|
|
||||||
|
// Constants for layout
|
||||||
|
const LINE_X = 8 // X position of main timeline line (center of dots)
|
||||||
|
const BRANCH_OFFSET_X = 45 // How far branches are offset horizontally
|
||||||
|
const ENTRY_HEIGHT = 28 // Height of each entry
|
||||||
|
const CURRENT_HEIGHT = 32 // Height of current state button
|
||||||
|
|
||||||
// Build a map of fork points to their branches for rendering
|
// Build a map of fork points to their branches for rendering
|
||||||
const branchesByForkPoint = $derived(
|
const branchesByForkPoint = $derived(
|
||||||
@@ -26,98 +34,298 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Entries in reverse order (newest first)
|
// Entries in reverse order (newest first) for display
|
||||||
const reversedEntries = $derived(entries.slice().reverse())
|
const reversedEntries = $derived(entries.slice().reverse())
|
||||||
|
|
||||||
|
// Calculate positions for rendering
|
||||||
|
const entryPositions = $derived.by(() => {
|
||||||
|
const positions: Record<number, { x: number; y: number; index: number }> = {}
|
||||||
|
|
||||||
|
let currentY = CURRENT_HEIGHT + 12 // Start below current button
|
||||||
|
|
||||||
|
reversedEntries.forEach((entry, index) => {
|
||||||
|
// For each entry, calculate space needed for branches above it
|
||||||
|
const entryBranches = branchesByForkPoint[entry.id] ?? []
|
||||||
|
const branchSpaceAbove = entryBranches.reduce((total, branch) => {
|
||||||
|
return total + branch.entries.length * ENTRY_HEIGHT
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
// Add space for branches above this entry
|
||||||
|
currentY += branchSpaceAbove
|
||||||
|
|
||||||
|
// Position this entry
|
||||||
|
positions[entry.id] = { x: LINE_X, y: currentY, index }
|
||||||
|
|
||||||
|
// Move to next entry position
|
||||||
|
currentY += ENTRY_HEIGHT
|
||||||
|
})
|
||||||
|
|
||||||
|
return positions
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate total height needed for the container
|
||||||
|
const totalHeight = $derived.by(() => {
|
||||||
|
const mainTimelineMaxY = Object.values(entryPositions).reduce(
|
||||||
|
(max, pos) => Math.max(max, pos.y),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const branchMaxY = Object.values(branchPositions)
|
||||||
|
.flatMap((groups) => Object.values(groups).flatMap((nodes) => nodes.map((node) => node.y)))
|
||||||
|
.reduce((max, y) => Math.max(max, y), 0)
|
||||||
|
return Math.max(mainTimelineMaxY, branchMaxY, 150) + 20 // Add padding at bottom
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate branch positions
|
||||||
|
const branchPositions = $derived.by(() => {
|
||||||
|
const positions: Record<
|
||||||
|
number,
|
||||||
|
Record<number, Array<{ id: number; x: number; y: number }>>
|
||||||
|
> = {}
|
||||||
|
|
||||||
|
Object.entries(branchesByForkPoint).forEach(([forkPointId, forkBranches]) => {
|
||||||
|
const forkPoint = entryPositions[Number(forkPointId)]
|
||||||
|
if (!forkPoint) return
|
||||||
|
|
||||||
|
positions[Number(forkPointId)] = {}
|
||||||
|
|
||||||
|
forkBranches.forEach((branch) => {
|
||||||
|
positions[Number(forkPointId)][branch.id] = []
|
||||||
|
|
||||||
|
// Branch flows from fork point upward (toward newer/current)
|
||||||
|
// Start just above the fork point and continue upward
|
||||||
|
// Oldest branch entry is closest to fork point, newest is furthest
|
||||||
|
let currentY = forkPoint.y - ENTRY_HEIGHT
|
||||||
|
|
||||||
|
branch.entries.forEach((entry) => {
|
||||||
|
positions[Number(forkPointId)][branch.id].push({
|
||||||
|
id: entry.id,
|
||||||
|
x: LINE_X + BRANCH_OFFSET_X,
|
||||||
|
y: currentY
|
||||||
|
})
|
||||||
|
currentY -= ENTRY_HEIGHT // Move upward (decreasing Y)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return positions
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper to create SVG path for branch connection
|
||||||
|
function createBranchPath(startX: number, startY: number, endX: number, endY: number): string {
|
||||||
|
// Control points are vertically offset to create vertical tangents
|
||||||
|
const controlOffset = Math.abs(startY - endY) * 0.85 // 85% of vertical distance for very intense curve
|
||||||
|
// Both tangents point upward (toward smaller Y values)
|
||||||
|
// Start control point: above the start point
|
||||||
|
// End control point: above the end point (since endY < startY, we add to go further up)
|
||||||
|
return `M ${startX} ${startY} C ${startX} ${startY - controlOffset}, ${endX} ${endY + controlOffset}, ${endX} ${endY}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet timelineButton(
|
||||||
|
label: string,
|
||||||
|
timestamp: string | null,
|
||||||
|
isSelected: boolean,
|
||||||
|
onClick: () => void,
|
||||||
|
position: { x: number; y: number },
|
||||||
|
type: 'current' | 'main' | 'branch' = 'main'
|
||||||
|
)}
|
||||||
|
{@const colors = {
|
||||||
|
current: {
|
||||||
|
selected: 'text-accent',
|
||||||
|
default: 'text-primary',
|
||||||
|
textSelected: 'text-accent',
|
||||||
|
bgSelected: 'bg-surface-accent-selected'
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
selected: 'text-accent',
|
||||||
|
default: 'text-primary',
|
||||||
|
textSelected: 'text-accent',
|
||||||
|
bgSelected: 'bg-surface-accent-selected'
|
||||||
|
},
|
||||||
|
branch: {
|
||||||
|
selected: 'text-amber-500 dark:text-amber-400',
|
||||||
|
default: 'text-secondary',
|
||||||
|
textSelected: 'text-amber-600 dark:text-amber-400',
|
||||||
|
bgSelected: 'bg-amber-50 dark:bg-amber-900/20'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{@const color = colors[type]}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={onClick}
|
||||||
|
aria-label={timestamp ? `Snapshot from ${displayDate(timestamp, true, false)}` : label}
|
||||||
|
aria-current={isSelected ? 'true' : 'false'}
|
||||||
|
class={'absolute flex items-center gap-1 transition-all duration-200 animate-fadeIn'}
|
||||||
|
style="left: {position.x - 6}px; top: {position.y - 6}px;"
|
||||||
|
>
|
||||||
|
{#if isSelected}
|
||||||
|
{#if type === 'current'}
|
||||||
|
<CircleDotDashed size={12} class="bg-surface {color.selected}" />
|
||||||
|
{:else}
|
||||||
|
<CircleDot size={12} class="bg-surface {color.selected}" />
|
||||||
|
{/if}
|
||||||
|
{:else if type === 'current'}
|
||||||
|
<CircleDashed size={12} class="bg-surface {color.default}" />
|
||||||
|
{:else}
|
||||||
|
<Circle size={12} class="bg-surface {color.default}" />
|
||||||
|
{/if}
|
||||||
|
<span
|
||||||
|
class={twMerge(
|
||||||
|
'text-2xs truncate px-2 py-1 rounded-md',
|
||||||
|
type === 'current' ? 'font-semibold text-emphasis' : 'font-normal',
|
||||||
|
isSelected ? color.textSelected : 'text-tertiary',
|
||||||
|
isSelected ? color.bgSelected : 'hover:bg-surface-hover'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{timestamp ? displayDate(timestamp, true, false) : label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#if entries.length === 0}
|
{#if entries.length === 0}
|
||||||
<div class="text-tertiary py-2 text-center text-2xs">
|
<div class="text-tertiary py-2 text-center text-2xs">
|
||||||
No snapshots yet. Auto-saved every 5 min.
|
No snapshots yet. Auto-saved every 5 min.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="relative w-full">
|
<div
|
||||||
<!-- Timeline line -->
|
class="relative w-full"
|
||||||
<div class="absolute left-[0.95rem] top-2 bottom-2 w-px bg-gray-200 dark:bg-gray-700"></div>
|
style="height: {totalHeight}px; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);"
|
||||||
|
role="list"
|
||||||
|
aria-label="History timeline"
|
||||||
|
>
|
||||||
|
<!-- SVG for lines and connections -->
|
||||||
|
<svg class="absolute inset-0 pointer-events-none" style="width: 100%; height: 100%;">
|
||||||
|
<!-- Always show the main line if there are entries -->
|
||||||
|
{#if reversedEntries.length > 0}
|
||||||
|
{@const mainTimelineMaxY = Math.max(...Object.values(entryPositions).map((pos) => pos.y))}
|
||||||
|
{@const branchMaxY = Object.values(branchPositions)
|
||||||
|
.flatMap((groups) =>
|
||||||
|
Object.values(groups).flatMap((nodes) => nodes.map((node) => node.y))
|
||||||
|
)
|
||||||
|
.reduce((max, y) => Math.max(max, y), 0)}
|
||||||
|
{@const maxY = Math.max(mainTimelineMaxY, branchMaxY)}
|
||||||
|
<line
|
||||||
|
x1={LINE_X}
|
||||||
|
y1={8}
|
||||||
|
x2={LINE_X}
|
||||||
|
y2={maxY + 8}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
opacity="1"
|
||||||
|
class="text-primary"
|
||||||
|
style="transition: y2 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each reversedEntries as entry, i (entry.id)}
|
<!-- Branch connection lines -->
|
||||||
{@const isSelected = selectedId === entry.id}
|
{#each Object.entries(branchPositions) as [forkPointId, branchGroups] (forkPointId)}
|
||||||
{@const isFirst = i === 0}
|
{@const forkPoint = entryPositions[Number(forkPointId)]}
|
||||||
{@const entryBranches = branchesByForkPoint[entry.id] ?? []}
|
{#if forkPoint}
|
||||||
|
{#each Object.entries(branchGroups) as [branchId, branchNodes] (branchId)}
|
||||||
|
{#if branchNodes.length > 0}
|
||||||
|
<!-- Connection from fork point to first branch entry -->
|
||||||
|
<path
|
||||||
|
d={createBranchPath(forkPoint.x, forkPoint.y, branchNodes[0].x, branchNodes[0].y)}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
fill="none"
|
||||||
|
stroke-dasharray="4 2"
|
||||||
|
opacity="0.6"
|
||||||
|
class="text-secondary transition-all duration-300"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Render branches ABOVE their fork point (newest first within branch) -->
|
<!-- Vertical line connecting branch entries within this branch -->
|
||||||
{#each entryBranches as branch (branch.id)}
|
{#if branchNodes.length > 1}
|
||||||
<div
|
{@const firstNode = branchNodes[0]}
|
||||||
class="ml-4 relative border-l border-dashed border-gray-300 dark:border-gray-600 pl-2 my-1"
|
{@const lastNode = branchNodes[branchNodes.length - 1]}
|
||||||
>
|
<line
|
||||||
<div class="absolute left-3 bottom-2 text-tertiary">
|
x1={firstNode.x}
|
||||||
<GitBranch size={10} />
|
y1={firstNode.y}
|
||||||
</div>
|
x2={lastNode.x}
|
||||||
<!-- Branch entries in reverse order (newest first) -->
|
y2={lastNode.y}
|
||||||
{#each branch.entries.slice().reverse() as branchEntry (branchEntry.id)}
|
stroke="currentColor"
|
||||||
{@const isBranchSelected = selectedId === branchEntry.id}
|
stroke-width="1.5"
|
||||||
<button
|
stroke-dasharray="4 2"
|
||||||
onclick={() => onSelect(branchEntry.id)}
|
opacity="0.6"
|
||||||
class={classNames(
|
class="text-secondary"
|
||||||
'relative flex items-center gap-2 py-1 pr-1 pl-2 w-full text-left rounded transition-colors',
|
style="transition: y1 0.3s cubic-bezier(0.4, 0, 0.2, 1), y2 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||||
'hover:bg-surface-hover',
|
/>
|
||||||
isBranchSelected ? 'bg-amber-50 dark:bg-amber-900/20' : ''
|
{/if}
|
||||||
)}
|
{/if}
|
||||||
>
|
|
||||||
<!-- Branch dot -->
|
|
||||||
<div
|
|
||||||
class={classNames(
|
|
||||||
'w-1 h-1 rounded-full',
|
|
||||||
isBranchSelected
|
|
||||||
? 'bg-amber-500 dark:bg-amber-400'
|
|
||||||
: 'bg-gray-300 dark:bg-gray-600'
|
|
||||||
)}
|
|
||||||
></div>
|
|
||||||
<span
|
|
||||||
class={classNames(
|
|
||||||
'text-2xs truncate',
|
|
||||||
isBranchSelected
|
|
||||||
? 'text-amber-600 dark:text-amber-400 font-medium'
|
|
||||||
: 'text-tertiary'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{displayDate(branchEntry.timestamp.toISOString(), true, false)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
</svg>
|
||||||
|
|
||||||
<!-- Main timeline entry -->
|
<!-- Interactive elements -->
|
||||||
<button
|
<div class="relative">
|
||||||
onclick={() => onSelect(entry.id)}
|
<!-- Current Working State -->
|
||||||
class={classNames(
|
{@render timelineButton(
|
||||||
'relative flex items-center gap-2 py-1 pr-1 pl-3 w-full text-left rounded transition-colors',
|
'Current',
|
||||||
'hover:bg-surface-hover',
|
null,
|
||||||
isSelected ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
selectedId === undefined,
|
||||||
)}
|
() => onSelectCurrent?.(),
|
||||||
>
|
{ x: LINE_X, y: 6 },
|
||||||
<!-- Timeline dot -->
|
'current'
|
||||||
<div
|
)}
|
||||||
class={classNames(
|
|
||||||
'absolute left-0 w-1.5 h-1.5 rounded-full border-[1.5px] bg-surface',
|
|
||||||
isSelected
|
|
||||||
? 'border-blue-500 dark:border-blue-400'
|
|
||||||
: 'border-gray-300 dark:border-gray-600'
|
|
||||||
)}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<span
|
<!-- Main timeline entries -->
|
||||||
class={classNames(
|
{#each reversedEntries as entry (entry.id)}
|
||||||
'text-2xs truncate',
|
{@const pos = entryPositions[entry.id]}
|
||||||
isSelected ? 'text-blue-600 dark:text-blue-400 font-medium' : 'text-secondary'
|
{@const isSelected = selectedId === entry.id}
|
||||||
|
{@const entryBranches = branchesByForkPoint[entry.id] ?? []}
|
||||||
|
|
||||||
|
{#if pos}
|
||||||
|
<!-- Main entry button -->
|
||||||
|
{@render timelineButton(
|
||||||
|
'',
|
||||||
|
entry.timestamp.toISOString(),
|
||||||
|
isSelected,
|
||||||
|
() => onSelect(entry.id),
|
||||||
|
pos,
|
||||||
|
'main'
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
{#if isFirst && !isSelected}
|
<!-- Branch entries -->
|
||||||
<span class="text-tertiary">Latest · </span>
|
{#if entryBranches.length > 0}
|
||||||
|
{#each Object.values(branchPositions[entry.id] ?? {}) as branchNodes}
|
||||||
|
{#each branchNodes as branchNode (branchNode.id)}
|
||||||
|
{@const branchEntry = branches
|
||||||
|
.flatMap((b) => b.entries)
|
||||||
|
.find((e) => e.id === branchNode.id)}
|
||||||
|
{@const isBranchSelected = selectedId === branchNode.id}
|
||||||
|
{#if branchEntry}
|
||||||
|
{@render timelineButton(
|
||||||
|
'',
|
||||||
|
branchEntry.timestamp.toISOString(),
|
||||||
|
isBranchSelected,
|
||||||
|
() => onSelect(branchNode.id),
|
||||||
|
{ x: branchNode.x, y: branchNode.y - 4 },
|
||||||
|
'branch'
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{displayDate(entry.timestamp.toISOString(), true, false)}
|
{/if}
|
||||||
</span>
|
{/each}
|
||||||
</button>
|
</div>
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fadeIn {
|
||||||
|
animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export class RawAppHistoryManager {
|
|||||||
private branchIdCounter = $state(0)
|
private branchIdCounter = $state(0)
|
||||||
// Track if current state has pending changes
|
// Track if current state has pending changes
|
||||||
private hasPendingChanges = $state(false)
|
private hasPendingChanges = $state(false)
|
||||||
|
// Temporary storage for current state when viewing history
|
||||||
|
private temporaryCurrentState = $state<HistoryEntry | undefined>(undefined)
|
||||||
|
|
||||||
// Derived state
|
// Derived state
|
||||||
public readonly hasEntries = $derived(this.entries.length > 0)
|
public readonly hasEntries = $derived(this.entries.length > 0)
|
||||||
@@ -93,6 +95,9 @@ export class RawAppHistoryManager {
|
|||||||
this.currentIndex === -1 && this.currentBranchId === undefined && this.hasPendingChanges
|
this.currentIndex === -1 && this.currentBranchId === undefined && this.hasPendingChanges
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Whether we have a temporary current state saved
|
||||||
|
public readonly hasTemporaryState = $derived(this.temporaryCurrentState !== undefined)
|
||||||
|
|
||||||
public readonly canUndo = $derived(
|
public readonly canUndo = $derived(
|
||||||
this.currentIndex > 0 ||
|
this.currentIndex > 0 ||
|
||||||
(this.currentIndex === -1 && this.entries.length > 1) ||
|
(this.currentIndex === -1 && this.entries.length > 1) ||
|
||||||
@@ -178,19 +183,25 @@ export class RawAppHistoryManager {
|
|||||||
* When making changes from a historical position, create a branch from the "future"
|
* When making changes from a historical position, create a branch from the "future"
|
||||||
*/
|
*/
|
||||||
markPendingChanges(): void {
|
markPendingChanges(): void {
|
||||||
// If we're on a branch and making changes, that branch becomes main
|
// If we're viewing history (not at current state)
|
||||||
if (this.currentBranchId !== undefined) {
|
if (this.currentIndex !== -1 || this.currentBranchId !== undefined) {
|
||||||
this.promoteBranchToMain()
|
// Convert temporary current state to a real snapshot first
|
||||||
}
|
this.convertTemporaryToSnapshot()
|
||||||
// If we're at a historical position on main timeline
|
|
||||||
else if (this.currentIndex !== -1 && this.currentIndex < this.entries.length - 1) {
|
|
||||||
this.createBranchFromFuture()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear selection - we're now in a new unsaved state
|
// If we're on a branch and making changes, that branch becomes main
|
||||||
this.currentIndex = -1
|
if (this.currentBranchId !== undefined) {
|
||||||
this.currentBranchId = undefined
|
this.promoteBranchToMain()
|
||||||
this.currentBranchEntryIndex = -1
|
}
|
||||||
|
// If we're at a historical position on main timeline
|
||||||
|
else if (this.currentIndex !== -1 && this.currentIndex < this.entries.length - 1) {
|
||||||
|
this.createBranchFromFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selection - we're now at current state
|
||||||
|
this.currentIndex = -1
|
||||||
|
this.currentBranchId = undefined
|
||||||
|
this.currentBranchEntryIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
this.hasPendingChanges = true
|
this.hasPendingChanges = true
|
||||||
}
|
}
|
||||||
@@ -341,12 +352,44 @@ export class RawAppHistoryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear selection (go back to latest state)
|
* Clear selection (go back to current state)
|
||||||
*/
|
*/
|
||||||
clearSelection(): void {
|
clearSelection(): void {
|
||||||
this.currentIndex = -1
|
this.currentIndex = -1
|
||||||
this.currentBranchId = undefined
|
this.currentBranchId = undefined
|
||||||
this.currentBranchEntryIndex = -1
|
this.currentBranchEntryIndex = -1
|
||||||
|
this.hasPendingChanges = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current state temporarily when viewing history
|
||||||
|
*/
|
||||||
|
public saveTemporaryCurrentState(
|
||||||
|
files: Record<string, string>,
|
||||||
|
runnables: Record<string, Runnable>,
|
||||||
|
summary: string,
|
||||||
|
data: RawAppData
|
||||||
|
): void {
|
||||||
|
this.temporaryCurrentState = this.createSnapshot(files, runnables, summary, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get and clear temporary current state
|
||||||
|
*/
|
||||||
|
public getAndClearTemporaryState(): HistoryEntry | undefined {
|
||||||
|
const temp = this.temporaryCurrentState
|
||||||
|
this.temporaryCurrentState = undefined
|
||||||
|
return temp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert temporary current state to a real snapshot
|
||||||
|
*/
|
||||||
|
convertTemporaryToSnapshot(): void {
|
||||||
|
if (this.temporaryCurrentState) {
|
||||||
|
this.addSnapshot(this.temporaryCurrentState)
|
||||||
|
this.temporaryCurrentState = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -407,6 +450,7 @@ export class RawAppHistoryManager {
|
|||||||
this.currentIndex = -1
|
this.currentIndex = -1
|
||||||
this.currentBranchId = undefined
|
this.currentBranchId = undefined
|
||||||
this.currentBranchEntryIndex = -1
|
this.currentBranchEntryIndex = -1
|
||||||
|
this.temporaryCurrentState = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
import DiffEditor from '$lib/components/DiffEditor.svelte'
|
import DiffEditor from '$lib/components/DiffEditor.svelte'
|
||||||
import type { InlineScript, StaticAppInput, UserAppInput, CtxAppInput } from '../apps/inputType'
|
import type { InlineScript, StaticAppInput, UserAppInput, CtxAppInput } from '../apps/inputType'
|
||||||
import CacheTtlPopup from '../apps/editor/inlineScriptsPanel/CacheTtlPopup.svelte'
|
import CacheTtlPopup from '../apps/editor/inlineScriptsPanel/CacheTtlPopup.svelte'
|
||||||
import RunButton from '$lib/components/RunButton.svelte'
|
|
||||||
import { computeFields } from '../apps/editor/inlineScriptsPanel/utils'
|
import { computeFields } from '../apps/editor/inlineScriptsPanel/utils'
|
||||||
import EditorBar from '../EditorBar.svelte'
|
import EditorBar from '../EditorBar.svelte'
|
||||||
import { LanguageIcon } from '../common/languageIcons'
|
import { LanguageIcon } from '../common/languageIcons'
|
||||||
@@ -40,6 +39,7 @@
|
|||||||
signDebugRequest,
|
signDebugRequest,
|
||||||
getDebugErrorMessage
|
getDebugErrorMessage
|
||||||
} from '$lib/components/debug'
|
} from '$lib/components/debug'
|
||||||
|
import TextInput from '../text_input/TextInput.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
inlineScript: (InlineScript & { language: ScriptLang }) | undefined
|
inlineScript: (InlineScript & { language: ScriptLang }) | undefined
|
||||||
@@ -47,9 +47,7 @@
|
|||||||
id: string
|
id: string
|
||||||
fields?: Record<string, StaticAppInput | UserAppInput | CtxAppInput>
|
fields?: Record<string, StaticAppInput | UserAppInput | CtxAppInput>
|
||||||
path: string
|
path: string
|
||||||
isLoading?: boolean
|
|
||||||
onRun: () => Promise<void>
|
onRun: () => Promise<void>
|
||||||
onCancel: () => Promise<void>
|
|
||||||
editor?: Editor | undefined
|
editor?: Editor | undefined
|
||||||
lastDeployedCode?: string | undefined
|
lastDeployedCode?: string | undefined
|
||||||
/** Called when code is selected in the editor */
|
/** Called when code is selected in the editor */
|
||||||
@@ -70,9 +68,7 @@
|
|||||||
id,
|
id,
|
||||||
fields = $bindable(undefined),
|
fields = $bindable(undefined),
|
||||||
path,
|
path,
|
||||||
isLoading = false,
|
|
||||||
onRun,
|
onRun,
|
||||||
onCancel,
|
|
||||||
editor = $bindable(undefined),
|
editor = $bindable(undefined),
|
||||||
lastDeployedCode,
|
lastDeployedCode,
|
||||||
onSelectionChange
|
onSelectionChange
|
||||||
@@ -170,7 +166,9 @@
|
|||||||
const dapServerUrl = $derived(
|
const dapServerUrl = $derived(
|
||||||
getDebugServerUrl((inlineScript?.language || 'python3') as DebugLanguage)
|
getDebugServerUrl((inlineScript?.language || 'python3') as DebugLanguage)
|
||||||
)
|
)
|
||||||
const debugFilePath = $derived(`/tmp/script${getDebugFileExtension(inlineScript?.language ?? '')}`)
|
const debugFilePath = $derived(
|
||||||
|
`/tmp/script${getDebugFileExtension(inlineScript?.language ?? '')}`
|
||||||
|
)
|
||||||
const isDebuggableScript = $derived(isDebuggable(inlineScript?.language ?? ''))
|
const isDebuggableScript = $derived(isDebuggable(inlineScript?.language ?? ''))
|
||||||
const showDebugPanel = $derived(
|
const showDebugPanel = $derived(
|
||||||
debugMode && $debugState.connected && ($debugState.running || $debugState.stopped)
|
debugMode && $debugState.connected && ($debugState.running || $debugState.stopped)
|
||||||
@@ -306,7 +304,11 @@
|
|||||||
|
|
||||||
let signedPayload
|
let signedPayload
|
||||||
try {
|
try {
|
||||||
signedPayload = await signDebugRequest($workspaceStore ?? '', code ?? '', inlineScript.language ?? 'python3')
|
signedPayload = await signDebugRequest(
|
||||||
|
$workspaceStore ?? '',
|
||||||
|
code ?? '',
|
||||||
|
inlineScript.language ?? 'python3'
|
||||||
|
)
|
||||||
debugSessionJobId = signedPayload.job_id
|
debugSessionJobId = signedPayload.job_id
|
||||||
} catch (signError) {
|
} catch (signError) {
|
||||||
sendUserToast(getDebugErrorMessage(signError), true)
|
sendUserToast(getDebugErrorMessage(signError), true)
|
||||||
@@ -314,12 +316,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get static args from fields
|
// Get static args from fields
|
||||||
const args = Object.entries(fields ?? {}).reduce<Record<string, unknown>>((acc, [key, obj]) => {
|
const args = Object.entries(fields ?? {}).reduce<Record<string, unknown>>(
|
||||||
if (obj.type === 'static') {
|
(acc, [key, obj]) => {
|
||||||
acc[key] = obj.value
|
if (obj.type === 'static') {
|
||||||
}
|
acc[key] = obj.value
|
||||||
return acc
|
}
|
||||||
}, {})
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
await dapClient.connect()
|
await dapClient.connect()
|
||||||
await dapClient.initialize()
|
await dapClient.initialize()
|
||||||
@@ -597,19 +602,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if name !== undefined}
|
{#if name !== undefined}
|
||||||
<div class="flex flex-row gap-2 w-full items-center">
|
<div class="flex flex-row gap-2 w-full items-center">
|
||||||
<input
|
<TextInput
|
||||||
onkeydown={stopPropagation(bubble('keydown'))}
|
inputProps={{
|
||||||
|
onkeydown: () => stopPropagation(bubble('keydown')),
|
||||||
|
placeholder: 'Inline script name'
|
||||||
|
}}
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
placeholder="Inline script name"
|
size="sm"
|
||||||
class="!text-xs !rounded-sm !shadow-none"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
title="Clear script"
|
title="Clear script"
|
||||||
size="xs2"
|
variant="subtle"
|
||||||
color="light"
|
|
||||||
variant="contained"
|
|
||||||
aria-label="Clear script"
|
aria-label="Clear script"
|
||||||
|
destructive
|
||||||
|
unifiedSize="sm"
|
||||||
on:click={() => dispatch('delete')}
|
on:click={() => dispatch('delete')}
|
||||||
endIcon={{ icon: Trash2 }}
|
endIcon={{ icon: Trash2 }}
|
||||||
iconOnly
|
iconOnly
|
||||||
@@ -622,14 +629,13 @@
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="xs2"
|
unifiedSize="sm"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
editor?.format()
|
editor?.format()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Format
|
Format
|
||||||
</Button>
|
</Button>
|
||||||
<RunButton {isLoading} {onRun} {onCancel} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -639,7 +645,7 @@
|
|||||||
{editor}
|
{editor}
|
||||||
lang={inlineScript.language}
|
lang={inlineScript.language}
|
||||||
{websocketAlive}
|
{websocketAlive}
|
||||||
iconOnly={width < 950}
|
iconOnly={width < 1250}
|
||||||
kind={'script'}
|
kind={'script'}
|
||||||
template={'script'}
|
template={'script'}
|
||||||
on:showDiffMode={showDiffMode}
|
on:showDiffMode={showDiffMode}
|
||||||
@@ -705,7 +711,11 @@
|
|||||||
if (inlineScript.schema == undefined) {
|
if (inlineScript.schema == undefined) {
|
||||||
inlineScript.schema = emptySchema()
|
inlineScript.schema = emptySchema()
|
||||||
}
|
}
|
||||||
await inferInlineScriptSchema(inlineScript?.language, e.detail, inlineScript.schema)
|
await inferInlineScriptSchema(
|
||||||
|
inlineScript?.language,
|
||||||
|
e.detail,
|
||||||
|
inlineScript.schema
|
||||||
|
)
|
||||||
if (JSON.stringify(inlineScript.schema) != oldSchema) {
|
if (JSON.stringify(inlineScript.schema) != oldSchema) {
|
||||||
inlineScript = inlineScript
|
inlineScript = inlineScript
|
||||||
syncFields()
|
syncFields()
|
||||||
@@ -786,12 +796,17 @@
|
|||||||
<Modal title="Debug Feature (Beta)" bind:open={showDebugBetaWarning}>
|
<Modal title="Debug Feature (Beta)" bind:open={showDebugBetaWarning}>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-800/50">
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-800/50"
|
||||||
|
>
|
||||||
<AlertTriangle class="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
|
<AlertTriangle class="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-secondary text-sm">
|
<div class="text-secondary text-sm">
|
||||||
<p>The Debug feature is currently in <strong>beta</strong>. You may encounter unexpected behavior or limitations.</p>
|
<p
|
||||||
|
>The Debug feature is currently in <strong>beta</strong>. You may encounter unexpected
|
||||||
|
behavior or limitations.</p
|
||||||
|
>
|
||||||
<p class="mt-2">By continuing, you acknowledge that this feature is experimental.</p>
|
<p class="mt-2">By continuing, you acknowledge that this feature is experimental.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
import { DebugToolbar, DebugPanel, debugState } from '$lib/components/debug'
|
import { DebugToolbar, DebugPanel, debugState } from '$lib/components/debug'
|
||||||
import LogViewer from '$lib/components/LogViewer.svelte'
|
import LogViewer from '$lib/components/LogViewer.svelte'
|
||||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||||
|
import RunButton from '$lib/components/RunButton.svelte'
|
||||||
import { userStore, workspaceStore } from '$lib/stores'
|
import { userStore, workspaceStore } from '$lib/stores'
|
||||||
|
|
||||||
type RunnableWithInlineScript = RunnableWithFields & {
|
type RunnableWithInlineScript = RunnableWithFields & {
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedTab = $state('inputs')
|
let selectedTab = $state('test')
|
||||||
let args = $state({})
|
let args = $state({})
|
||||||
|
|
||||||
function getSchema(runnable: RunnableWithFields) {
|
function getSchema(runnable: RunnableWithFields) {
|
||||||
@@ -88,16 +89,18 @@
|
|||||||
let inlineScriptEditor: RawAppInlineScriptEditor | undefined = $state()
|
let inlineScriptEditor: RawAppInlineScriptEditor | undefined = $state()
|
||||||
|
|
||||||
// Get debug state from the editor
|
// Get debug state from the editor
|
||||||
const editorDebugState = $derived(inlineScriptEditor?.getDebugState?.() ?? {
|
const editorDebugState = $derived(
|
||||||
debugMode: false,
|
inlineScriptEditor?.getDebugState?.() ?? {
|
||||||
isDebuggableScript: false,
|
debugMode: false,
|
||||||
showDebugPanel: false,
|
isDebuggableScript: false,
|
||||||
hasDebugResult: false,
|
showDebugPanel: false,
|
||||||
dapClient: null,
|
hasDebugResult: false,
|
||||||
selectedDebugFrameId: null,
|
dapClient: null,
|
||||||
debugSessionJobId: null,
|
selectedDebugFrameId: null,
|
||||||
debugBreakpoints: new Set()
|
debugSessionJobId: null,
|
||||||
})
|
debugBreakpoints: new Set()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Reactive debug state values
|
// Reactive debug state values
|
||||||
const debugMode = $derived(editorDebugState.debugMode)
|
const debugMode = $derived(editorDebugState.debugMode)
|
||||||
@@ -190,13 +193,7 @@
|
|||||||
bind:inlineScript={runnable.inlineScript}
|
bind:inlineScript={runnable.inlineScript}
|
||||||
bind:name={runnable.name}
|
bind:name={runnable.name}
|
||||||
bind:fields={runnable.fields}
|
bind:fields={runnable.fields}
|
||||||
isLoading={testIsLoading}
|
|
||||||
onRun={testPreview}
|
onRun={testPreview}
|
||||||
onCancel={async () => {
|
|
||||||
if (jobLoader) {
|
|
||||||
await jobLoader.cancelJob()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
on:delete
|
on:delete
|
||||||
path={appPath}
|
path={appPath}
|
||||||
{onSelectionChange}
|
{onSelectionChange}
|
||||||
@@ -221,8 +218,8 @@
|
|||||||
</Pane>
|
</Pane>
|
||||||
<Pane size={45}>
|
<Pane size={45}>
|
||||||
<Tabs bind:selected={selectedTab}>
|
<Tabs bind:selected={selectedTab}>
|
||||||
<Tab value="inputs" label="Inputs" />
|
|
||||||
<Tab value="test" label="Test" />
|
<Tab value="test" label="Test" />
|
||||||
|
<Tab value="inputs" label="Inputs" />
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
{#if selectedTab == 'inputs'}
|
{#if selectedTab == 'inputs'}
|
||||||
{#if runnable?.fields}
|
{#if runnable?.fields}
|
||||||
@@ -275,6 +272,18 @@
|
|||||||
<Splitpanes horizontal class="grow">
|
<Splitpanes horizontal class="grow">
|
||||||
<Pane size={50}>
|
<Pane size={50}>
|
||||||
<div class="px-2 py-3 h-full overflow-auto">
|
<div class="px-2 py-3 h-full overflow-auto">
|
||||||
|
<div class="mx-auto w-fit">
|
||||||
|
<RunButton
|
||||||
|
isLoading={testIsLoading}
|
||||||
|
onRun={testPreview}
|
||||||
|
onCancel={async () => {
|
||||||
|
if (jobLoader) {
|
||||||
|
await jobLoader.cancelJob()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<SchemaForm
|
<SchemaForm
|
||||||
on:keydownCmdEnter={testPreview}
|
on:keydownCmdEnter={testPreview}
|
||||||
disabledArgs={Object.entries(runnable?.fields ?? {})
|
disabledArgs={Object.entries(runnable?.fields ?? {})
|
||||||
@@ -307,7 +316,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="h-full flex items-center justify-center text-sm text-tertiary">
|
<div
|
||||||
|
class="h-full flex items-center justify-center text-sm text-tertiary"
|
||||||
|
>
|
||||||
{#if $debugState.running && !$debugState.stopped}
|
{#if $debugState.running && !$debugState.stopped}
|
||||||
Running...
|
Running...
|
||||||
{:else if $debugState.stopped}
|
{:else if $debugState.stopped}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
historyManager?: RawAppHistoryManager
|
historyManager?: RawAppHistoryManager
|
||||||
historySelectedId?: number | undefined
|
historySelectedId?: number | undefined
|
||||||
onHistorySelect?: (id: number) => void
|
onHistorySelect?: (id: number) => void
|
||||||
|
onHistorySelectCurrent?: () => void
|
||||||
onManualSnapshot?: () => void
|
onManualSnapshot?: () => void
|
||||||
dataTableRefs?: DataTableRef[]
|
dataTableRefs?: DataTableRef[]
|
||||||
onDataTableRefsChange?: (refs: DataTableRef[]) => void
|
onDataTableRefsChange?: (refs: DataTableRef[]) => void
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
historyManager,
|
historyManager,
|
||||||
historySelectedId,
|
historySelectedId,
|
||||||
onHistorySelect,
|
onHistorySelect,
|
||||||
|
onHistorySelectCurrent,
|
||||||
onManualSnapshot,
|
onManualSnapshot,
|
||||||
dataTableRefs = [],
|
dataTableRefs = [],
|
||||||
onDataTableRefsChange,
|
onDataTableRefsChange,
|
||||||
@@ -444,6 +446,7 @@
|
|||||||
branches={historyManager.allBranches}
|
branches={historyManager.allBranches}
|
||||||
selectedId={historySelectedId}
|
selectedId={historySelectedId}
|
||||||
onSelect={onHistorySelect}
|
onSelect={onHistorySelect}
|
||||||
|
onSelectCurrent={onHistorySelectCurrent}
|
||||||
/>
|
/>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Auto-detect operating system
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
IS_MAC=true
|
||||||
|
else
|
||||||
|
IS_MAC=false
|
||||||
|
fi
|
||||||
|
|
||||||
cd ../../windmill-code-ui-builder
|
cd ../../windmill-code-ui-builder
|
||||||
HASH=$(git rev-parse --short HEAD)
|
HASH=$(git rev-parse --short HEAD)
|
||||||
HASH=${HASH::-1}
|
HASH=${HASH::-1}
|
||||||
|
|
||||||
echo "Using UI Builder hash: ${HASH}"
|
echo "Using UI Builder hash: ${HASH}"
|
||||||
sed -i "s/ui_builder-[^.]*\.tar\.gz/ui_builder-${HASH}.tar.gz/" ../windmill/frontend/scripts/untar_ui_builder.js
|
|
||||||
|
if [ "$IS_MAC" = true ]; then
|
||||||
|
sed -i '' "s/ui_builder-[^.]*\.tar\.gz/ui_builder-${HASH}.tar.gz/" ../windmill/frontend/scripts/untar_ui_builder.js
|
||||||
|
else
|
||||||
|
sed -i "s/ui_builder-[^.]*\.tar\.gz/ui_builder-${HASH}.tar.gz/" ../windmill/frontend/scripts/untar_ui_builder.js
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user