diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index ab8fc1ea9f..0e3be901cb 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -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 | 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 | 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) { diff --git a/frontend/src/lib/components/flows/FlowEditor.svelte b/frontend/src/lib/components/flows/FlowEditor.svelte index a7c22a2e16..49a62bf28e 100644 --- a/frontend/src/lib/components/flows/FlowEditor.svelte +++ b/frontend/src/lib/components/flows/FlowEditor.svelte @@ -1,5 +1,4 @@ @@ -380,8 +376,8 @@ {/if}
- - + + {#if flowModule.value.type === 'rawscript'} {#if !noEditor} {#key flowModule.id} @@ -457,9 +453,9 @@ {/key} {/if} - - - + + + {#if !preprocessorModule} Step Input @@ -480,6 +476,7 @@ pickableProperties={stepPropPicker.pickableProperties} error={failureModule} noPadding + moduleId={flowModule.id} > {#if reloadError}
{#if selected === 'test'} - + = writable< PropPickerConfig | undefined @@ -100,17 +101,22 @@ flowPropPickerConfig.set(undefined) }} > - + + 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 + + type Props = SplitpanesProps & { + index: number + defaultSize: number + children?: Snippet + } + + let { index, defaultSize, children, ...rest }: Props = $props() + + const splitPanesId = getContext('splitPanesId') + + const splitPanesLayout = getSplitPanesLayout() + + onMount(() => { + splitPanesLayout.mountPane(splitPanesId, index, defaultSize) + }) + + onDestroy(() => { + splitPanesLayout.unmountPane(splitPanesId, index) + }) + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/splitPanes/SplitPanesLayout.svelte.ts b/frontend/src/lib/components/splitPanes/SplitPanesLayout.svelte.ts new file mode 100644 index 0000000000..2d6b33257b --- /dev/null +++ b/frontend/src/lib/components/splitPanes/SplitPanesLayout.svelte.ts @@ -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 = $state({}) + #changeCb: (() => void) | undefined = undefined + #readyPanes: Record = $state({}) + + constructor(initialPaneLayouts?: Record, changeCb?: () => void) { + this.#layout = initialPaneLayouts ?? {} + this.#changeCb = changeCb + } + + get layout() { + return this.#layout + } + + set layout(layout: Record) { + 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 { + return $state.snapshot(this.#layout) + } +} + +export function getSplitPanesLayout(): SplitPanesLayout { + return getContext(KEY) +} + +export function setSplitPanesLayoutContext(layout: SplitPanesLayout) { + setContext(KEY, layout) +} diff --git a/frontend/src/lib/components/splitPanes/Splitpanes.svelte b/frontend/src/lib/components/splitPanes/Splitpanes.svelte new file mode 100644 index 0000000000..df79c6aa9b --- /dev/null +++ b/frontend/src/lib/components/splitPanes/Splitpanes.svelte @@ -0,0 +1,41 @@ + + + { + splitPanesLayout.handleResize( + id, + detail.map((d, index) => ({ size: d.size, index })) + ) + }} + {...rest} +> + {@render children?.()} + diff --git a/frontend/src/lib/components/splitPanes/index.ts b/frontend/src/lib/components/splitPanes/index.ts new file mode 100644 index 0000000000..db32ce07c3 --- /dev/null +++ b/frontend/src/lib/components/splitPanes/index.ts @@ -0,0 +1,2 @@ +export { default as Splitpanes } from './Splitpanes.svelte' +export { default as Pane } from './Pane.svelte' diff --git a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte index 8001ee0281..0fad932fbf 100644 --- a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte @@ -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 | 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}