Add custom Pane and Splipanes for layout saving

This commit is contained in:
Guilhem
2025-05-28 16:51:21 +02:00
parent 5dcefeff84
commit d600cd0ec7
9 changed files with 251 additions and 28 deletions

View File

@@ -83,6 +83,11 @@
} from './triggers/utils'
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
import { Triggers } from './triggers/triggers.svelte'
import {
SplitPanesLayout,
setSplitPanesLayoutContext,
type PanesLayout
} from './splitPanes/SplitPanesLayout.svelte'
export let initialPath: string = ''
export let pathStoreInit: string | undefined = undefined
@@ -102,6 +107,7 @@
export let setSavedraftCb: ((cb: () => void) => void) | undefined = undefined
export let draftTriggersFromUrl: Trigger[] | undefined = undefined
export let selectedTriggerIndexFromUrl: number | undefined = undefined
export let savedSplitPanesLayout: Record<string, PanesLayout> | undefined = undefined
let initialPathStore = writable(initialPath)
$: initialPathStore.set(initialPath)
@@ -134,6 +140,15 @@
$: setContext('customUi', customUi)
const splitPanesLayout = new SplitPanesLayout(savedSplitPanesLayout ?? {}, saveSessionDraft)
setSplitPanesLayoutContext(splitPanesLayout)
export function setSplitPanesLayout(layout: Record<string, PanesLayout> | undefined) {
if (layout !== undefined) {
splitPanesLayout.layout = layout
}
}
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
savedValue: savedFlow,
@@ -490,7 +505,8 @@
path: $pathStore,
selectedId: $selectedIdStore,
draft_triggers: triggersState.getDraftTriggersSnapshot(),
selected_trigger: triggersState.getSelectedTriggerSnapshot()
selected_trigger: triggersState.getSelectedTriggerSnapshot(),
split_panes_layout: splitPanesLayout.getLayoutSnapshot()
})
)
} catch (err) {

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import FlowEditorPanel from './content/FlowEditorPanel.svelte'
import FlowModuleSchemaMap from './map/FlowModuleSchemaMap.svelte'
import WindmillIcon from '../icons/WindmillIcon.svelte'
@@ -8,12 +7,12 @@
import type { FlowEditorContext } from './types'
import type { FlowCopilotContext } from '../copilot/flow'
import { classNames } from '$lib/utils'
import { writable } from 'svelte/store'
import type { PropPickerContext, FlowPropPickerConfig } from '$lib/components/prop_picker'
import type { PickableProperties } from '$lib/components/flows/previousResults'
import type { Flow } from '$lib/gen'
import type { Trigger } from '$lib/components/triggers/utils'
import { Splitpanes, Pane } from '$lib/components/splitPanes/index'
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
export let loading: boolean
@@ -31,8 +30,6 @@
| undefined = undefined
export let onDeployTrigger: (trigger: Trigger) => void = () => {}
let size = 50
const { currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext>('FlowCopilotContext')
@@ -49,8 +46,8 @@
$copilotCurrentStepStore !== undefined ? 'border-gray-500/75' : ''
)}
>
<Splitpanes>
<Pane {size} minSize={15} class="h-full relative z-0">
<Splitpanes id="flow-editor">
<Pane minSize={15} class="h-full relative z-0" index={0} defaultSize={60}>
<div class="grow overflow-hidden bg-gray h-full bg-surface-secondary relative">
{#if loading}
<div class="p-2 pt-10">
@@ -72,7 +69,7 @@
{/if}
</div>
</Pane>
<Pane class="relative z-10" size={100 - size} minSize={40}>
<Pane class="relative z-10" index={1} defaultSize={40}>
{#if loading}
<div class="w-full h-full">
<div class="block m-auto pt-40 w-10">

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { Splitpanes, Pane } from '$lib/components/splitPanes/index'
import Tab from '$lib/components/common/tabs/Tab.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Editor from '$lib/components/Editor.svelte'
@@ -202,8 +202,6 @@
}
let forceReload = 0
let editorPanelSize = noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50
let editorSettingsPanelSize = 100 - editorPanelSize
$: $selectedId && onSelectedIdChange()
@@ -260,13 +258,6 @@
$: parentLoop =
$flowStore && flowModule ? checkIfParentLoop($flowStore, flowModule.id) : undefined
let leftPanelSize = 0
$: if (selected === 'test') {
leftPanelSize = 50
} else {
leftPanelSize = 100
}
function showDiffMode() {
diffMode = true
diffEditor?.setOriginal((savedModule?.value as RawScript).content ?? '')
@@ -280,6 +271,11 @@
diffEditor?.hide()
editor?.show()
}
let panesSizes = [
noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50,
100 - (noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50)
]
</script>
<svelte:window on:keydown={onKeyDown} />
@@ -380,8 +376,8 @@
{/if}
<div class="min-h-0 flex-grow" id="flow-editor-editor">
<Splitpanes horizontal>
<Pane bind:size={editorPanelSize} minSize={10} class="relative">
<Splitpanes id={`flow-editor-split-${flowModule.id}`} horizontal>
<Pane minSize={10} class="relative" index={0} defaultSize={panesSizes[0]}>
{#if flowModule.value.type === 'rawscript'}
{#if !noEditor}
{#key flowModule.id}
@@ -457,9 +453,9 @@
{/key}
{/if}
</Pane>
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
<Splitpanes>
<Pane minSize={36} bind:size={leftPanelSize}>
<Pane minSize={20} index={1} defaultSize={panesSizes[1]}>
<Splitpanes id={`flow-editor-step-input-${flowModule.id}`}>
<Pane minSize={36} index={0} defaultSize={selected === 'inputs' ? 100 : 50}>
<Tabs bind:selected>
{#if !preprocessorModule}
<Tab value="inputs">Step Input</Tab>
@@ -480,6 +476,7 @@
pickableProperties={stepPropPicker.pickableProperties}
error={failureModule}
noPadding
moduleId={flowModule.id}
>
{#if reloadError}
<div
@@ -781,7 +778,7 @@
</div>
</Pane>
{#if selected === 'test'}
<Pane minSize={20}>
<Pane minSize={20} index={1} defaultSize={50}>
<ModulePreviewResultViewer
lang={flowModule.value['language'] ?? 'deno'}
{editor}

View File

@@ -26,7 +26,7 @@
import PropPickerResult from '$lib/components/propertyPicker/PropPickerResult.svelte'
import { clickOutside } from '$lib/utils'
import { createEventDispatcher, getContext, setContext } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { Pane, Splitpanes } from '$lib/components/splitPanes/index'
import { writable, type Writable } from 'svelte/store'
import type { PickableProperties } from '../previousResults'
import { twMerge } from 'tailwind-merge'
@@ -44,6 +44,7 @@
export let alwaysOn: boolean = false
export let paneClass: string = ''
export let noFlowPlugConnect = false
export let moduleId: string = ''
const propPickerConfig: Writable<PropPickerConfig | undefined> = writable<
PropPickerConfig | undefined
@@ -100,17 +101,22 @@
flowPropPickerConfig.set(undefined)
}}
>
<Splitpanes class={$propPickerConfig ? 'splitpanes-remove-splitter' : ''}>
<Splitpanes
class={$propPickerConfig ? 'splitpanes-remove-splitter' : ''}
id={`prop-picker-wrapper-${moduleId}`}
>
<Pane
index={0}
minSize={20}
size={60}
defaultSize={60}
class={twMerge('relative !transition-none ', noPadding ? '' : 'p-2')}
>
<slot />
</Pane>
<Pane
index={1}
minSize={20}
size={40}
defaultSize={40}
class="!transition-none z-1000 {$propPickerConfig ? 'ml-[-1px]' : ''} {paneClass}"
>
<AnimatedButton

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import { Pane } from 'svelte-splitpanes'
import { getSplitPanesLayout } from './SplitPanesLayout.svelte'
import type { Snippet } from 'svelte'
import { type ComponentProps, getContext, onDestroy, onMount } from 'svelte'
type SplitpanesProps = ComponentProps<Pane>
type Props = SplitpanesProps & {
index: number
defaultSize: number
children?: Snippet
}
let { index, defaultSize, children, ...rest }: Props = $props()
const splitPanesId = getContext<string>('splitPanesId')
const splitPanesLayout = getSplitPanesLayout()
onMount(() => {
splitPanesLayout.mountPane(splitPanesId, index, defaultSize)
})
onDestroy(() => {
splitPanesLayout.unmountPane(splitPanesId, index)
})
</script>
<Pane size={splitPanesLayout.layout[splitPanesId]?.[index]?.size ?? defaultSize} {...rest}>
{@render children?.()}
</Pane>

View File

@@ -0,0 +1,127 @@
import { setContext, getContext, tick } from 'svelte'
const KEY = 'splitPanesLayout'
export type Pane = {
size: number | undefined
active: boolean
}
export type PanesLayout = Pane[]
export class SplitPanesLayout {
#layout: Record<string, PanesLayout> = $state({})
#changeCb: (() => void) | undefined = undefined
#readyPanes: Record<string, boolean> = $state({})
constructor(initialPaneLayouts?: Record<string, PanesLayout>, changeCb?: () => void) {
this.#layout = initialPaneLayouts ?? {}
this.#changeCb = changeCb
}
get layout() {
return this.#layout
}
set layout(layout: Record<string, PanesLayout>) {
this.#layout = layout
this.#changeCb?.()
}
mountPane(layoutId: string, paneIndex: number, defaultSize: number) {
if (!this.#layout[layoutId]) {
this.#layout[layoutId] = [{ size: defaultSize, active: true }]
} else if (this.#readyPanes[layoutId]) {
// Ensure array is long enough
while (this.#layout[layoutId].length <= paneIndex) {
this.#layout[layoutId].push({ size: undefined, active: false })
}
this.#layout[layoutId][paneIndex].active = true
this.#layout[layoutId][paneIndex].size = this.#layout[layoutId][paneIndex].size ?? defaultSize
this.#scalePanes(layoutId, paneIndex)
} else {
// Ensure array is long enough
while (this.#layout[layoutId].length <= paneIndex) {
this.#layout[layoutId].push({ size: undefined, active: false })
}
const currentPane = this.#layout[layoutId][paneIndex]
currentPane.active = true
currentPane.size = currentPane.size ?? defaultSize
}
this.#changeCb?.()
}
#scalePanes(layoutId: string, paneIndex: number) {
// Scale other active panes while keeping the current pane size
const otherPanesTotal = this.#layout[layoutId]
.filter((pane, idx) => idx !== paneIndex && pane.active)
.reduce((sum, pane) => sum + (pane.size ?? 0), 0)
if (otherPanesTotal > 0) {
const scale = (100 - (this.#layout[layoutId][paneIndex].size ?? 0)) / otherPanesTotal
this.#layout[layoutId] = this.#layout[layoutId].map((pane, idx) =>
idx === paneIndex ? pane : { ...pane, size: (pane.size ?? 0) * scale }
)
}
}
#scalePanesTo100(layoutId: string) {
const activePanesTotal = this.#layout[layoutId]
.filter((pane) => pane.active)
.reduce((sum, pane) => sum + (pane.size ?? 0), 0)
if (activePanesTotal > 0) {
const scale = 100 / activePanesTotal
this.#layout[layoutId] = this.#layout[layoutId].map((pane) =>
pane.active ? { ...pane, size: (pane.size ?? 0) * scale } : pane
)
}
}
async unmountPane(layoutId: string, paneIndex: number) {
// Wait for the dom refresh to avoid update in the case the splitpane has been removed
await tick()
if (this.#readyPanes[layoutId]) {
this.#layout[layoutId][paneIndex].active = false
this.#scalePanesTo100(layoutId)
}
this.#changeCb?.()
}
handleResize(layoutId: string, newSizes: { size: number; index: number }[]) {
if (this.#layout[layoutId]) {
this.#layout[layoutId] = this.#layout[layoutId].map((pane, idx) => {
const newSize = newSizes.find((s) => s.index === idx)
return newSize ? { ...pane, size: newSize.size } : pane
})
}
this.#changeCb?.()
}
handleSplitPaneReady(layoutId: string) {
this.#readyPanes[layoutId] = true
this.#scalePanesTo100(layoutId)
this.#changeCb?.()
}
handleSplitPaneDestroy(layoutId: string) {
this.#readyPanes[layoutId] = false
this.#changeCb?.()
}
setChangeCb(cb: () => void) {
this.#changeCb = cb
}
getLayoutSnapshot(): Record<string, PanesLayout> {
return $state.snapshot(this.#layout)
}
}
export function getSplitPanesLayout(): SplitPanesLayout {
return getContext<SplitPanesLayout>(KEY)
}
export function setSplitPanesLayoutContext(layout: SplitPanesLayout) {
setContext<SplitPanesLayout>(KEY, layout)
}

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { Splitpanes } from 'svelte-splitpanes'
import { getSplitPanesLayout } from './SplitPanesLayout.svelte'
import { onDestroy, onMount, setContext, type Snippet } from 'svelte'
import type { ComponentProps } from 'svelte'
type SplitpanesProps = ComponentProps<Splitpanes>
type Props = SplitpanesProps & {
id: string
children?: Snippet
}
let { id, children, ...rest }: Props = $props()
let timeout: ReturnType<typeof setTimeout> | undefined = undefined
setContext<string>('splitPanesId', id)
const splitPanesLayout = getSplitPanesLayout()
onMount(() => {
splitPanesLayout.handleSplitPaneReady(id)
})
onDestroy(() => {
splitPanesLayout.handleSplitPaneDestroy(id)
clearTimeout(timeout)
})
</script>
<Splitpanes
on:resized={({ detail }) => {
splitPanesLayout.handleResize(
id,
detail.map((d, index) => ({ size: d.size, index }))
)
}}
{...rest}
>
{@render children?.()}
</Splitpanes>

View File

@@ -0,0 +1,2 @@
export { default as Splitpanes } from './Splitpanes.svelte'
export { default as Pane } from './Pane.svelte'

View File

@@ -16,6 +16,7 @@
import type { ScheduleTrigger } from '$lib/components/triggers'
import type { GetInitialAndModifiedValues } from '$lib/components/common/confirmationModal/unsavedTypes'
import type { Trigger } from '$lib/components/triggers/utils'
import type { PanesLayout } from '$lib/components/splitPanes/SplitPanesLayout.svelte'
let version: undefined | number = undefined
let nodraft = $page.url.searchParams.get('nodraft')
@@ -63,6 +64,7 @@
let draftTriggersFromUrl: Trigger[] | undefined = undefined
let selectedTriggerIndexFromUrl: number | undefined = undefined
let splitPanesLayoutFromUrl: Record<string, PanesLayout> | undefined = undefined
let flowBuilder: FlowBuilder | undefined = undefined
@@ -93,6 +95,8 @@
flow = stateLoadedFromUrl.flow
draftTriggersFromUrl = stateLoadedFromUrl.draft_triggers
selectedTriggerIndexFromUrl = stateLoadedFromUrl.selected_trigger
splitPanesLayoutFromUrl = stateLoadedFromUrl.split_panes_layout
flowBuilder?.setSplitPanesLayout(splitPanesLayoutFromUrl)
flowBuilder?.setDraftTriggers(draftTriggersFromUrl)
flowBuilder?.setSelectedTriggerIndex(selectedTriggerIndexFromUrl)
const selectedId = stateLoadedFromUrl?.selectedId ?? 'settings-metadata'
@@ -262,6 +266,7 @@
initialPath={$page.params.path}
newFlow={false}
{selectedId}
savedSplitPanesLayout={splitPanesLayoutFromUrl}
{initialArgs}
{loading}
bind:this={flowBuilder}