feat(frontend): Open/Close UI (#445)
* feat(frontend): Properly open/close and highlight a flowbox * feat(frontend): Fix removing first step
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
fork,
|
||||
loadFlowModuleSchema,
|
||||
pickScript,
|
||||
createScriptFromInlineScript
|
||||
createScriptFromInlineScript,
|
||||
isEmptyFlowModule
|
||||
} from './flows/flowStateUtils'
|
||||
import { jobsToResults } from './flows/utils'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
@@ -67,7 +68,7 @@
|
||||
index: "iteration's index"
|
||||
}
|
||||
|
||||
if (hasElements(stepBeforeLoop.previewResults)) {
|
||||
if (hasElements(stepBeforeLoop?.previewResults)) {
|
||||
const lastResults = getLast(stepBeforeLoop.previewResults)
|
||||
|
||||
if (hasElements(lastResults)) {
|
||||
@@ -89,7 +90,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: shouldPick = 'path' in mod.value && mod.value.path === '' && !('language' in mod.value)
|
||||
$: shouldPick = isEmptyFlowModule(mod)
|
||||
$: pickableProperties = getPickableProperties($flowStore, $flowStateStore)
|
||||
$: extraLib = buildExtraLib(
|
||||
schemaToTsType($flowStore?.schema),
|
||||
@@ -140,12 +141,11 @@
|
||||
$: opened = $stepOpened === String(indexes.join('-'))
|
||||
</script>
|
||||
|
||||
<FlowBox>
|
||||
<FlowBox bind:opened>
|
||||
<svelte:fragment slot="header">
|
||||
<FlowModuleHeader
|
||||
{mod}
|
||||
bind:mod
|
||||
bind:indexes
|
||||
{shouldPick}
|
||||
on:delete
|
||||
on:fork={() => apply(fork, mod)}
|
||||
on:createScriptFromInlineScript={() => {
|
||||
@@ -160,83 +160,75 @@
|
||||
|
||||
<div slot="content">
|
||||
{#if opened}
|
||||
{#if shouldPick}
|
||||
<FlowInputs
|
||||
shouldDisableTriggerScripts={indexes[0] > 0}
|
||||
shouldDisableLoopCreation={indexes.length > 1 || indexes[0] == 0}
|
||||
on:pick={(e) => apply(pickScript, e.detail.path)}
|
||||
on:new={(e) =>
|
||||
apply(createInlineScriptModule, {
|
||||
language: e.detail.language,
|
||||
isTrigger: e.detail.isTrigger
|
||||
})}
|
||||
on:loop={() => applyCreateLoop()}
|
||||
/>
|
||||
{/if}
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
<div class="mb-2 overflow-hidden">
|
||||
<EditorBar {editor} {websocketAlive} lang={mod.value.language ?? 'deno'} />
|
||||
</div>
|
||||
<div on:mouseleave={() => reload(mod)}>
|
||||
<Editor
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="{bigEditor ? 'h-2/3' : 'h-80'} border p-2 rounded"
|
||||
bind:code={mod.value.content}
|
||||
deno={mod.value.language === RawScript.language.DENO}
|
||||
automaticLayout={true}
|
||||
on:blur={() => reload(mod)}
|
||||
formatAction={() => reload(mod)}
|
||||
<div class="p-6 border-t border-gray-300">
|
||||
{#if shouldPick}
|
||||
<FlowInputs
|
||||
shouldDisableTriggerScripts={i != 0}
|
||||
shouldDisableLoopCreation={indexes.length > 1 || i == 0}
|
||||
on:pick={(e) => apply(pickScript, e.detail.path)}
|
||||
on:new={(e) =>
|
||||
apply(createInlineScriptModule, {
|
||||
language: e.detail.language,
|
||||
isTrigger: e.detail.isTrigger
|
||||
})}
|
||||
on:loop={() => applyCreateLoop()}
|
||||
/>
|
||||
<button
|
||||
class="w-full text-center"
|
||||
on:click={() => {
|
||||
bigEditor = !bigEditor
|
||||
}}
|
||||
>
|
||||
<Icon data={bigEditor ? faChevronUp : faChevronDown} scale={1.0} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 mb-8">
|
||||
<p class="text-gray-500 italic">
|
||||
Move the focus outside of the text editor to recompute the input schema or press
|
||||
Ctrl/Cmd+S
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !shouldPick}
|
||||
<p class="text-lg font-bold text-gray-900 mb-2">Step inputs</p>
|
||||
<SchemaForm
|
||||
{schema}
|
||||
{extraLib}
|
||||
inputTransform={true}
|
||||
importPath={String(indexes.join('-'))}
|
||||
bind:pickableProperties
|
||||
bind:args={mod.input_transform}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !shouldPick}
|
||||
<div class="border-b border-gray-200" />
|
||||
<div class="p-3">
|
||||
<FlowPreview
|
||||
bind:args
|
||||
flow={$flowStore}
|
||||
{i}
|
||||
{/if}
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
<div class="mb-2 overflow-hidden">
|
||||
<EditorBar {editor} {websocketAlive} lang={mod.value.language ?? 'deno'} />
|
||||
</div>
|
||||
<div on:mouseleave={() => reload(mod)}>
|
||||
<Editor
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="{bigEditor ? 'h-2/3' : 'h-80'} border p-2 rounded"
|
||||
bind:code={mod.value.content}
|
||||
deno={mod.value.language === RawScript.language.DENO}
|
||||
automaticLayout={true}
|
||||
on:blur={() => reload(mod)}
|
||||
formatAction={() => reload(mod)}
|
||||
/>
|
||||
<button
|
||||
class="w-full text-center"
|
||||
on:click={() => {
|
||||
bigEditor = !bigEditor
|
||||
}}
|
||||
>
|
||||
<Icon data={bigEditor ? faChevronUp : faChevronDown} scale={1.0} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 mb-8">
|
||||
<p class="text-gray-500 italic">
|
||||
Move the focus outside of the text editor to recompute the input schema or press
|
||||
Ctrl/Cmd+S
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !shouldPick}
|
||||
<p class="text-lg font-bold text-gray-900 mb-2">Step inputs</p>
|
||||
<SchemaForm
|
||||
{schema}
|
||||
on:change={(e) => onPreview(e.detail)}
|
||||
{extraLib}
|
||||
inputTransform={true}
|
||||
importPath={String(indexes.join('-'))}
|
||||
bind:pickableProperties
|
||||
bind:args={mod.input_transform}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col justify-center w-full">
|
||||
<button
|
||||
on:click={() => {
|
||||
stepOpened.update(() => String(indexes.join('-')))
|
||||
}}
|
||||
>
|
||||
<Icon data={faArrowDown} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if !shouldPick}
|
||||
<div class="border-b border-gray-200" />
|
||||
<div class="p-3">
|
||||
<FlowPreview
|
||||
bind:args
|
||||
flow={$flowStore}
|
||||
{i}
|
||||
{schema}
|
||||
on:change={(e) => onPreview(e.detail)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
let slots = $$props.$$slots
|
||||
</script>
|
||||
|
||||
<div class="bg-white border border-gray-300 rounded-md shadow-md ">
|
||||
<div class="bg-white border border-gray-300 rounded-md">
|
||||
<FlowBoxHeader {title}>
|
||||
<slot name="header" />
|
||||
</FlowBoxHeader>
|
||||
{#if slots.content}
|
||||
<div class="p-6 ">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
<slot name="content" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export let title: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between flex-wra px-6 py-2 border-b border-gray-300">
|
||||
<div class="flex items-center justify-between flex-wra px-6 py-2 ">
|
||||
{#if title}
|
||||
<h3 class="text-sm font-bold text-gray-900">{title}</h3>
|
||||
{/if}
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
<CopyFirstStepSchema />
|
||||
</div>
|
||||
<div slot="content">
|
||||
<SchemaEditor
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
schema={$flowStore.schema}
|
||||
/>
|
||||
<div class="p-6 border-t border-gray-300">
|
||||
<SchemaEditor
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
schema={$flowStore.schema}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FlowBox>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { getScriptByPath } from '$lib/utils'
|
||||
import { faCode, faCodeBranch, faSave, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faArrowDown,
|
||||
faClose,
|
||||
faCode,
|
||||
faCodeBranch,
|
||||
faSave,
|
||||
faTrashAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button } from 'flowbite-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
@@ -9,13 +16,14 @@
|
||||
import python from 'svelte-highlight/languages/python'
|
||||
import typescript from 'svelte-highlight/languages/typescript'
|
||||
import Modal from '../Modal.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { scrollIntoView } from './utils'
|
||||
import { isEmptyFlowModule } from './flowStateUtils'
|
||||
import { stepOpened } from './stepOpenedStore'
|
||||
|
||||
export let indexes: number[]
|
||||
export let shouldPick = false
|
||||
export let mod: FlowModule
|
||||
|
||||
$: shouldPick = isEmptyFlowModule(mod)
|
||||
|
||||
let modalViewer: Modal
|
||||
let modalViewerContent = ''
|
||||
let modalViewerLanguage: 'deno' | 'python3' = 'deno'
|
||||
@@ -31,15 +39,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function scrollTo({ target }) {
|
||||
const element = document.querySelector(target.getAttribute('href'))
|
||||
scrollIntoView(element)
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: opened = $stepOpened === String(indexes.join('-'))
|
||||
</script>
|
||||
|
||||
<a href="#module-{indexes.join('-')}" class="grow text-inherit" on:click|preventDefault={scrollTo}>
|
||||
<div on:click={() => stepOpened.update(() => String(indexes.join('-')))}>
|
||||
<h3 class="text-sm font-bold text-gray-900">
|
||||
{#if 'path' in mod.value && mod.value.path}
|
||||
{mod.value.path}
|
||||
@@ -49,7 +53,7 @@
|
||||
Select a script
|
||||
{/if}
|
||||
</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row space-x-2">
|
||||
{#if mod.value.type === 'script' && !shouldPick}
|
||||
@@ -79,6 +83,19 @@
|
||||
<Icon data={faTrashAlt} class="mr-2" />
|
||||
Remove step
|
||||
</Button>
|
||||
{#if opened}
|
||||
<Button size="xs" color="dark" on:click={() => stepOpened.update(() => undefined)}>
|
||||
<Icon data={faClose} />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={() => stepOpened.update(() => String(indexes.join('-')))}
|
||||
>
|
||||
<Icon data={faArrowDown} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modalViewer}>
|
||||
|
||||
@@ -103,60 +103,63 @@
|
||||
</div>
|
||||
|
||||
<div slot="content">
|
||||
<Path
|
||||
bind:error={pathError}
|
||||
bind:path={$flowStore.path}
|
||||
{initialPath}
|
||||
namePlaceholder="my_flow"
|
||||
kind="flow"
|
||||
>
|
||||
<div slot="ownerToolkit">
|
||||
Flow permissions depend on their path. Select the group <span class="font-mono">all</span>
|
||||
to share your flow, and <span class="font-mono">user</span> to keep it private.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
|
||||
</div>
|
||||
</Path>
|
||||
<div class="p-6 border-t border-gray-300">
|
||||
<Path
|
||||
bind:error={pathError}
|
||||
bind:path={$flowStore.path}
|
||||
{initialPath}
|
||||
namePlaceholder="my_flow"
|
||||
kind="flow"
|
||||
>
|
||||
<div slot="ownerToolkit">
|
||||
Flow permissions depend on their path. Select the group <span class="font-mono">all</span>
|
||||
to share your flow, and <span class="font-mono">user</span> to keep it private.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
|
||||
</div>
|
||||
</Path>
|
||||
|
||||
<label class="block mt-4">
|
||||
<span class="text-gray-700">Summary <Required required={false} /></span>
|
||||
<textarea
|
||||
bind:value={$flowStore.summary}
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
placeholder="A very short summary of the flow displayed when the flow is listed"
|
||||
rows="1"
|
||||
/>
|
||||
</label>
|
||||
<label class="block mt-4">
|
||||
<span class="text-gray-700">Summary <Required required={false} /></span>
|
||||
<textarea
|
||||
bind:value={$flowStore.summary}
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
placeholder="A very short summary of the flow displayed when the flow is listed"
|
||||
rows="1"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<CollapseLink text="set primary schedule" open={true}>
|
||||
<Tooltip>
|
||||
The primary schedule of a flow is simply a schedule that has the same name as a flow. It can
|
||||
be set and enabled directly within the flow editor. "Watching for new changes" flows are
|
||||
meant to be watching regularly for new items in an external systems. The primary schedule
|
||||
purpose is there to set the periodicity at which you want this watcher to operate.
|
||||
</Tooltip>
|
||||
<Toggle
|
||||
bind:checked={scheduleEnabled}
|
||||
options={{
|
||||
left: 'disabled',
|
||||
right: 'enabled'
|
||||
}}
|
||||
/>
|
||||
<div class="p-2 my-2 rounded" class:bg-gray-300={!scheduleEnabled}>
|
||||
{#if !scheduleEnabled}
|
||||
<span class="font-black">No next scheduled run when disabled</span>
|
||||
{/if}
|
||||
<CronInput bind:schedule={scheduleCron} />
|
||||
</div>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Button
|
||||
color="alternative"
|
||||
size="sm"
|
||||
on:click={() => (scheduleArgs = JSON.parse(JSON.stringify(previewArgs)))}
|
||||
>
|
||||
Copy from preview arguments
|
||||
</Button>
|
||||
</div>
|
||||
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
|
||||
</CollapseLink>
|
||||
<CollapseLink text="set primary schedule" open={true}>
|
||||
<Tooltip>
|
||||
The primary schedule of a flow is simply a schedule that has the same name as a flow. It
|
||||
can be set and enabled directly within the flow editor. "Watching for new changes" flows
|
||||
are meant to be watching regularly for new items in an external systems. The primary
|
||||
schedule purpose is there to set the periodicity at which you want this watcher to
|
||||
operate.
|
||||
</Tooltip>
|
||||
<Toggle
|
||||
bind:checked={scheduleEnabled}
|
||||
options={{
|
||||
left: 'disabled',
|
||||
right: 'enabled'
|
||||
}}
|
||||
/>
|
||||
<div class="p-2 my-2 rounded" class:bg-gray-300={!scheduleEnabled}>
|
||||
{#if !scheduleEnabled}
|
||||
<span class="font-black">No next scheduled run when disabled</span>
|
||||
{/if}
|
||||
<CronInput bind:schedule={scheduleCron} />
|
||||
</div>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Button
|
||||
color="alternative"
|
||||
size="sm"
|
||||
on:click={() => (scheduleArgs = JSON.parse(JSON.stringify(previewArgs)))}
|
||||
>
|
||||
Copy from preview arguments
|
||||
</Button>
|
||||
</div>
|
||||
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
|
||||
</CollapseLink>
|
||||
</div>
|
||||
</div>
|
||||
</FlowBox>
|
||||
|
||||
@@ -47,10 +47,13 @@ export function getTypeAsString(arg: any): string {
|
||||
export function scrollIntoView(element: any) {
|
||||
if (!element) return
|
||||
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
inline: 'nearest'
|
||||
var headerOffset = 45
|
||||
var elementPosition = element.getBoundingClientRect().top
|
||||
var offsetPosition = elementPosition + window.pageYOffset - headerOffset
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,8 +124,9 @@ export function getDefaultExpr(
|
||||
previousExpr?: string
|
||||
) {
|
||||
const expr = previousExpr ?? `previous_result.${key}`
|
||||
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill${importPath ? `@${importPath}` : ''
|
||||
}'
|
||||
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill${
|
||||
importPath ? `@${importPath}` : ''
|
||||
}'
|
||||
|
||||
${expr}`
|
||||
}
|
||||
@@ -187,7 +191,6 @@ export async function runFlowPreview(args: Record<string, any>, flow: Flow) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function codeToStaticTemplate(code?: string): string | undefined {
|
||||
if (!code) return undefined
|
||||
|
||||
|
||||
Reference in New Issue
Block a user