Refactor flow UI/UX + added fork and create script from inline script (#175)

* Refactor flow UI/UX + added fork and create script from inline script

* Prevent infinite loop when remove steps

* Fix forking a script from the Hub

* Fix viewing code of  a script from the Hub

* Fix PR comments

* Fix code highlight

* Fix path

* Find next available path

* Fix copy first step schema
This commit is contained in:
Faton Ramadani
2022-07-13 11:29:43 +02:00
committed by GitHub
parent 8403fbbc02
commit 5502047474
17 changed files with 708 additions and 288 deletions
+24
View File
@@ -89,6 +89,30 @@
@apply text-sm;
}
.default-secondary-button-v2 {
@apply text-gray-900;
@apply bg-white;
@apply border;
@apply border-gray-200;
@apply font-medium;
@apply rounded-lg;
@apply text-sm;
@apply px-5;
@apply py-2.5;
@apply text-center;
@apply inline-flex;
@apply items-center;
}
.default-secondary-button-v2:focus {
@apply ring-4;
@apply outline-none;
@apply ring-gray-100;
}
.default-secondary-button-v2:hover {
@apply bg-gray-100
}
.input-error {
@apply border-red-500 !important;
}
+15 -10
View File
@@ -2,11 +2,12 @@
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { FlowService, ScriptService, type Flow } from '$lib/gen'
import { clearPreviewResults, workspaceStore, hubScripts } from '$lib/stores'
import { clearPreviewResults, hubScripts, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import { onMount } from 'svelte'
import SvelteMarkdown from 'svelte-markdown'
import FlowEditor from './FlowEditor.svelte'
import { flowStore, initFlow } from './flows/flowStore'
import Path from './Path.svelte'
import Required from './Required.svelte'
import ScriptSchema from './ScriptSchema.svelte'
@@ -26,7 +27,7 @@
}
async function saveFlow(): Promise<void> {
if (initialPath == '') {
if (initialPath === '') {
await FlowService.createFlow({
workspace: $workspaceStore!,
requestBody: {
@@ -40,7 +41,7 @@
} else {
await FlowService.updateFlow({
workspace: $workspaceStore!,
path: initialPath,
path: flow.path,
requestBody: {
path: flow.path,
summary: flow.summary,
@@ -58,10 +59,12 @@
goto(`?step=${step}`)
}
$: {
flowStore.subscribe((flow: Flow) => {
$page.url.searchParams.set('state', btoa(JSON.stringify(flow)))
history.replaceState({}, '', $page.url)
}
})
$: flow && initFlow(flow)
onMount(() => {
loadSearchData()
@@ -107,12 +110,14 @@
class="default-button px-6 max-h-8"
on:click={() => {
changeStep(step + 1)
}}>Next</button
}}
>
Next
</button>
{#if step == 2}
<button class="default-button-secondary px-6 max-h-8 mr-2" on:click={saveFlow}
>Save</button
>
<button class="default-button-secondary px-6 max-h-8 mr-2" on:click={saveFlow}>
Save
</button>
{/if}
{:else}
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
@@ -187,7 +192,7 @@
</div>
</div>
{:else if step === 2}
<FlowEditor bind:flow />
<FlowEditor />
{:else if step === 3}
<ScriptSchema
synchronizedHeader={false}
+13 -39
View File
@@ -1,73 +1,47 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { FlowModuleValue, type Flow } from '$lib/gen'
import { loadSchema } from '$lib/scripts'
import { emptySchema } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import FlowPreview from './FlowPreview.svelte'
import CopyFirstStepSchema from './flows/CopyFirstStepSchema.svelte'
import { addModule, flowStore } from './flows/flowStore'
import ModuleStep from './ModuleStep.svelte'
import SchemaEditor from './SchemaEditor.svelte'
import type SchemaForm from './SchemaForm.svelte'
export let flow: Flow
let args: Record<string, any> = {}
let schemas: Schema[] = []
let schemaForms: (SchemaForm | undefined)[] = []
function addModule() {
schemaForms.push(undefined)
let newModule = {
value: { type: FlowModuleValue.type.SCRIPT, path: '' },
input_transform: {}
}
flow.value.modules = flow.value.modules.concat(newModule)
schemas.push(emptySchema())
}
$: numberOfSteps = $flowStore?.value.modules.length - 1
</script>
<!-- <PageHeader title="Flow" /> -->
<div class="flow-root bg-gray-50 rounded-xl border border-gray-200">
<ul class="relative -mt-10">
<span class="absolute top-0 left-1/2 h-full w-1 bg-gray-400" aria-hidden="true" />
<div class="relative">
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
<div class="bg-white border border-gray xl-rounded shadow-lg w-full mx-4 xl:mx-20">
<div
class="flex items-center justify-between flex-wra px-4 py-5 border-b border-gray-200 sm:px-6"
>
<h3 class="text-lg leading-6 font-medium text-gray-900">Flow Input</h3>
<button
class="text-xs default-button-secondary max-h-6 place-self-end"
disabled={flow.value.modules.length == 0 ||
flow.value.modules[0].value.path == undefined}
on:click={async () => {
flow.schema = await loadSchema(flow.value.modules[0].value.path ?? '')
}}
>Copy from step 1's schema
</button>
<CopyFirstStepSchema />
</div>
<div class="p-4">
<SchemaEditor bind:schema={flow.schema} />
<SchemaEditor bind:schema={$flowStore.schema} />
<div class="my-4" />
<FlowPreview {flow} i={flow.value.modules.length - 1} bind:args />
<FlowPreview bind:flow={$flowStore} i={numberOfSteps} bind:args />
</div>
</div>
</li>
{#each flow.value.modules as mod, i}
<ModuleStep bind:flow bind:mod bind:schemas bind:schemaForms bind:args {i} />
{#each $flowStore?.value.modules as mod, i}
<ModuleStep bind:mod bind:args {i} />
{/each}
<li class="relative m-20 ">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-gray-300" />
</div>
<div class="relative flex justify-center">
<button
class="default-button h-10 w-10 shadow-blue-600/40 border-blue-600 shadow"
on:click={addModule}><Icon class="text-white mb-1" data={faPlus} scale={0.9} /></button
on:click={() => addModule()}
>
<Icon class="text-white mb-1" data={faPlus} />
Add step
</button>
</div>
</li>
</div>
@@ -85,10 +85,9 @@
})
</script>
<h2 class="mb-5 mt-2">
<h2 class="flex justify-center">
<button
type="submit"
class="underline text-gray-700 inline-flex items-center"
class="default-button "
on:click={() => {
viewPreview = !viewPreview
}}
+66 -153
View File
@@ -1,173 +1,86 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { FlowModuleValue, type Flow, type FlowModule } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { loadSchema as UloadSchema } from '$lib/scripts'
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
import { addPreviewResult, previewResults, workspaceStore } from '$lib/stores'
import {
buildExtraLib,
emptySchema,
objectToTsType,
schemaToObject,
schemaToTsType
} from '$lib/utils'
import { language, mehO } from 'svelte-awesome/icons'
import { FlowModuleValue, type FlowModule } from '$lib/gen'
import { addPreviewResult, previewResults } from '$lib/stores'
import { buildExtraLib, objectToTsType, schemaToObject, schemaToTsType } from '$lib/utils'
import Editor from './Editor.svelte'
import FlowPreview from './FlowPreview.svelte'
import RadioButton from './RadioButton.svelte'
import FlowInputs from './flows/FlowInputs.svelte'
import FlowModuleHeader from './flows/FlowModuleHeader.svelte'
import {
createInlineScriptModule,
flowStore,
loadSchema,
pickScript,
schemasStore
} from './flows/flowStore'
import SchemaForm from './SchemaForm.svelte'
import ScriptPicker from './ScriptPicker.svelte'
export let flow: Flow
export let i: number
export let mod: FlowModule
export let args: Record<string, any> = {}
export let schemas: Schema[] = []
export let schemaForms: (SchemaForm | undefined)[] = []
let editor: Editor
let flowPreview: FlowPreview
$: previousSchema = i === 0 ? schemaToObject(flow.schema) : $previewResults[i]
$: schema = $schemasStore[i]
$: shouldPick = mod.value.path === '' && mod.value.language === undefined
$: previousSchema = i === 0 ? schemaToObject($flowStore?.schema) : $previewResults[i]
$: extraLib = buildExtraLib(
i == 0 ? schemaToTsType(flow.schema) : objectToTsType($previewResults[i])
i === 0 ? schemaToTsType($flowStore?.schema) : objectToTsType($previewResults[i])
)
function initContent(lang: string) {
const newStart = lang == 'deno' ? DENO_INIT_CODE : PYTHON_INIT_CODE
if (editor) {
editor.setCode(newStart)
} else {
mod.value.content = newStart
}
}
$: mod.value.type == 'rawscript' &&
mod.value.language == undefined &&
(mod.value.language = FlowModuleValue.language.DENO)
$: mod.value.type == 'rawscript' && mod.value.language && initContent(mod.value.language)
export async function loadSchema() {
let isRaw = mod.value.type == 'rawscript'
if ((!isRaw && mod.value.path) || isRaw) {
let schema: Schema
if (isRaw) {
schema = emptySchema()
await inferArgs(mod.value.language!, mod.value.content!, schema)
} else {
schema = await UloadSchema(mod.value.path!)
}
if (
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(mod.input_transform).sort())
) {
let it = {}
Object.keys(schema?.properties ?? {}).map(
(x) =>
(it[x] = {
type: 'static',
value: ''
})
)
schemaForms[i]?.setArgs(it)
}
schemas[i] = schema ?? emptySchema()
} else {
schemaForms[i]?.setArgs({})
schemas[i] = emptySchema()
}
schemas = schemas
}
$: $workspaceStore && loadSchema()
</script>
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
<div class="bg-white border border-gray xl-rounded shadow-lg w-full max-w-4xl mx-4 md:mx-auto">
<div
class="flex items-center justify-between flex-wra px-4 py-5 border-b border-gray-200 sm:px-6"
>
<h3 class="text-lg leading-6 font-medium text-gray-900">Step {i + 1}</h3>
<button
class="text-xs default-button-secondary max-h-6 place-self-end"
on:click={() => {
flow.value.modules.splice(i, 1)
schemas.splice(i, 1)
schemaForms.splice(i, 1)
flow = flow
}}
>Remove this step
</button>
<div class="flex items-center justify-between flex-wra p-4 sm:px-6">
<FlowModuleHeader bind:i bind:shouldPick>
<h3 class="text-lg font-bold text-gray-900">Step {i + 1}</h3>
<p>{mod.value.path ?? ''}</p>
</FlowModuleHeader>
</div>
<div class="p-10">
<h2 class="mb-4">Step script</h2>
<RadioButton
small={true}
options={[
['Pick from an existing script', 'script'],
['Edit in-place', 'rawscript']
]}
bind:value={mod.value.type}
/>
{#if mod.value.type == 'script'}
<ScriptPicker allowHub={true} bind:scriptPath={mod.value.path} on:select={loadSchema} />
{:else}
<div class="mt-2" />
<RadioButton
label="Language"
small={true}
options={[
['Python 3.10', 'python3'],
['Typescript (Deno)', 'deno']
]}
bind:value={mod.value.language}
<div class="border-b border-gray-200" />
<div class="p-6">
{#if shouldPick}
<FlowInputs
on:pick={(e) => pickScript(e.detail.path, i)}
on:new={(e) => createInlineScriptModule(e.detail.language, i)}
/>
{/if}
{#if mod.value.type === FlowModuleValue.type.RAWSCRIPT}
<div class="h-96">
<Editor
class="h-full"
bind:code={mod.value.content}
deno={mod.value.language === FlowModuleValue.language.DENO}
/>
</div>
<button class="default-button w-full p-1 mt-4" on:click={() => loadSchema(i)}>
Infer schema
</button>
{/if}
{#if !shouldPick}
<h2 class="mb-4">Step inputs</h2>
<SchemaForm
inputTransform={true}
{schema}
{extraLib}
{i}
{previousSchema}
bind:args={mod.input_transform}
/>
<div class="h-96 mt-4">
{#if mod.value.language == 'deno'}
<Editor
bind:this={editor}
class="h-full"
bind:code={mod.value.content}
deno={true}
cmdEnterAction={() => flowPreview.runPreview(mod.input_transform)}
/>
{:else}
<Editor
bind:this={editor}
class="h-full"
bind:code={mod.value.content}
deno={false}
cmdEnterAction={() => flowPreview.runPreview(mod.input_transform)}
/>
{/if}
</div>
<button class="default-button w-full p-1 mt-4" on:click={loadSchema}>Infer schema</button>
{/if}
<div class="my-4" />
<h2 class="mb-4">Step inputs</h2>
<SchemaForm
bind:this={schemaForms[i]}
inputTransform={true}
schema={schemas[i]}
{extraLib}
{i}
{previousSchema}
bind:args={mod.input_transform}
/>
<FlowPreview
bind:this={flowPreview}
{flow}
{i}
bind:args
{schemas}
on:change={(e) => {
addPreviewResult(e.detail.result, i + 1)
}}
/>
</div>
{#if !shouldPick}
<div class="border-b border-gray-200" />
<div class="p-3">
<FlowPreview
bind:args
bind:flow={$flowStore}
{i}
bind:schemas={$schemasStore}
on:change={(e) => {
addPreviewResult(e.detail.result, i + 1)
}}
/>
</div>
{/if}
</div>
</li>
+8 -80
View File
@@ -1,34 +1,27 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import type { InputTransform } from '$lib/gen'
import { allTrue } from '$lib/utils'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
import ArgInput from './ArgInput.svelte'
import Editor from './Editor.svelte'
import FieldHeader from './FieldHeader.svelte'
import DynamicInputHelpBox from './flows/DynamicInputHelpBox.svelte'
import PropPicker from './flows/PropPicker.svelte'
import RadioButton from './RadioButton.svelte'
export let inputTransform = false
export let schema: Schema
export let args: Record<string, any> = {}
export let isValid: boolean = true
export let args: Record<string, InputTransform> = {}
export let editableSchema = false
export let extraLib: string
export let isValid: boolean = true
export let i: number
export let previousSchema: Object
let inputCheck: { [id: string]: boolean } = {}
let seeHelp: { [id: string]: boolean } = {}
let editor: Editor
export function setArgs(nargs: Record<string, any>) {
args = nargs
}
function getDefaultExpr(i: number, key: string = 'myfield') {
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill@${i}'
@@ -113,73 +106,7 @@ previous_result.${key}`
{/if}
</div>
<div class="text-xs flex flex-row-reverse">
<span
class="underline mr-4"
on:click={() => {
seeHelp[argName] = seeHelp[argName] == undefined ? true : !seeHelp[argName]
}}
>Help<Icon
class="ml-2"
data={seeHelp[argName] ? faChevronUp : faChevronDown}
scale={0.7}
/></span
>
</div>
{#if seeHelp[argName]}
<div
transition:slide
class="bg-gray-100 border-l-4 border-gray-600 text-gray-700 p-4 m-4"
role="alert"
>
<p class="font-bold">Dynamic arg help</p>
<p>
When a field is "dynamic", its value is computed dynamically as the evaluation of
its corresponding typescript snippet.
</p>
That snippet can be single line:
<pre><code>last_result.myarg</code></pre>
or multiline:
<pre><code
>let x = 5;
x + 2</code
></pre>
<p>
If it is multiline, the last statement before the final expression<b
>MUST END WITH ; and a newline</b
>
</p>
The snippet can also be a string template:
<pre><code
>`Hello $&#123;params.name&#125;, all your base $&#123;previous_result.base_name&#125;
belong to us`</code
></pre>
However, the last line must always be the final expression.
<p>
The snippet can use any typescript primitives, and the following flow specific
objects and functions:
</p>
<ul class="ml-4">
<li>
<b>previous_result</b>: the object containing the result of the previous step
</li>
<li><b>flow_input</b>: the object containing the flow input arguments</li>
<li><b>params</b>: the object containing the current step static values</li>
<li>
<b>step(n)</b>: the function returning the result of step number n. One can also
use a negative n. step(0) == flow_input , step(-1) == previous_result
</li>
<li>
<b>variable(path)</b>: the function returning the variable (including secrets)
at given path as a string
</li>
<li>
<b>resource(path)</b>: the function returning the resource at a given path as an
object
</li>
</ul>
</div>
{/if}
<DynamicInputHelpBox />
{/if}
{:else}
<p>Not recognized arg type {args[argName].type}</p>
@@ -199,7 +126,8 @@ belong to us`</code
contentEncoding={schema.properties[argName].contentEncoding}
bind:itemsType={schema.properties[argName].items}
{editableSchema}
/>{/if}
/>
{/if}
{/each}
{:else}
<p class="italic text-sm">No settable input</p>
@@ -0,0 +1,11 @@
<script lang="ts">
import { copyFirstStepSchema, isCopyFirstStepSchemaDisabled } from './flowStore'
</script>
<button
class="default-secondary-button-v2"
disabled={$isCopyFirstStepSchemaDisabled}
on:click={copyFirstStepSchema}
>
Copy from step 1's schema
</button>
@@ -0,0 +1,73 @@
<script lang="ts">
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
$: opened = false
</script>
<div class="text-xs flex flex-row-reverse">
<span
class="underline mr-4"
on:click={() => {
opened = !opened
}}
>
Help
<Icon class="ml-2" data={opened ? faChevronUp : faChevronDown} scale={0.7} />
</span>
</div>
{#if opened}
<div
transition:slide
class="bg-gray-100 border-l-4 border-gray-600 text-gray-700 p-4 m-4"
role="alert"
>
<p class="font-bold">Dynamic arg help</p>
<p>
When a field is "dynamic", its value is computed dynamically as the evaluation of its
corresponding typescript snippet.
</p>
That snippet can be single line:
<pre><code>last_result.myarg</code></pre>
or multiline:
<pre><code
>let x = 5;
x + 2</code
></pre>
<p>
If it is multiline, the last statement before the final expression<b
>MUST END WITH ; and a newline</b
>
</p>
The snippet can also be a string template:
<pre><code
>`Hello $&#123;params.name&#125;, all your base $&#123;previous_result.base_name&#125;
belong to us`</code
></pre>
However, the last line must always be the final expression.
<p>
The snippet can use any typescript primitives, and the following flow specific objects and
functions:
</p>
<ul class="ml-4">
<li>
<b>previous_result</b>: the object containing the result of the previous step
</li>
<li><b>flow_input</b>: the object containing the flow input arguments</li>
<li><b>params</b>: the object containing the current step static values</li>
<li>
<b>step(n)</b>: the function returning the result of step number n. One can also use a
negative n. step(0) == flow_input , step(-1) == previous_result
</li>
<li>
<b>variable(path)</b>: the function returning the variable (including secrets) at given path
as a string
</li>
<li>
<b>resource(path)</b>: the function returning the resource at a given path as an object
</li>
</ul>
</div>
{/if}
@@ -0,0 +1,28 @@
<script lang="ts">
import { FlowModuleValue } from '$lib/gen/models/FlowModuleValue'
import { faCode } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import FlowScriptPicker from './pickers/FlowScriptPicker.svelte'
import PickHubScript from './pickers/PickHubScript.svelte'
import PickScript from './pickers/PickScript.svelte'
const dispatch = createEventDispatcher()
</script>
<div class="columns-2">
<PickScript on:pick />
<PickHubScript on:pick />
<FlowScriptPicker
label="New Typescript script (Deno)"
icon={faCode}
iconColor="text-blue-800"
on:click={() => dispatch('new', { language: FlowModuleValue.language.DENO })}
/>
<FlowScriptPicker
label="New Python script (3.10)"
icon={faCode}
iconColor="text-yellow-500"
on:click={() => dispatch('new', { language: FlowModuleValue.language.PYTHON3 })}
/>
</div>
@@ -0,0 +1,76 @@
<script lang="ts">
import { FlowModuleValue, Script } from '$lib/gen'
import { faCode, faCodeBranch, faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Highlight } from 'svelte-highlight'
import { python, typescript } from 'svelte-highlight/languages'
import github from 'svelte-highlight/styles/github'
import Modal from '../Modal.svelte'
import { createScriptFromInlineScript, flowStore, fork, removeModule } from './flowStore'
import { getScriptByPath } from './utils'
export let i: number
export let shouldPick = false
$: mod = $flowStore.value.modules[i]
let modalViewer: Modal
let modalViewerContent = ''
let modalViewerLanguage: Script.language = Script.language.DENO
async function viewCode() {
const { content, language } = await getScriptByPath(mod.value.path!)
modalViewerContent = content
modalViewerLanguage = language
modalViewer.openModal()
}
</script>
<svelte:head>
{@html github}
</svelte:head>
<div>
<slot />
</div>
<div>
{#if mod.value.type === FlowModuleValue.type.SCRIPT && !shouldPick}
<button type="button" on:click={() => fork(i)} class="default-secondary-button-v2 text-xs">
<Icon data={faCodeBranch} class={`w-4 mr-2 h-4`} />
Fork
</button>
<button type="button" on:click={viewCode} class="default-secondary-button-v2 text-xs">
<Icon data={faCode} class="w-4 mr-2 h-4" />
View code
</button>
{/if}
{#if mod.value.type === FlowModuleValue.type.RAWSCRIPT && !shouldPick}
<button
type="button"
on:click={() => createScriptFromInlineScript(i)}
class="default-secondary-button-v2 text-sm"
>
<Icon data={faPlus} class="w-4 mr-2" />
Create script
</button>
{/if}
<button
type="button"
on:click={() => removeModule(i)}
class="default-secondary-button-v2 text-xs"
>
<Icon data={faTrashAlt} class="w-4 mr-2 h-4" />
Remove step
</button>
</div>
<Modal bind:this={modalViewer}>
<div slot="title">Script {mod?.value.path}</div>
<div slot="content">
{#if modalViewerLanguage === 'python3'}
<Highlight language={python} code={modalViewerContent} />
{:else if modalViewerLanguage === 'deno'}
<Highlight language={typescript} code={modalViewerContent} />
{/if}
</div>
</Modal>
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { derived, writable } from 'svelte/store'
import ObjectViewer from './ObjectViewer.svelte'
import { keepByKey } from './utils'
@@ -7,7 +6,6 @@
export let props: Object = {}
const EMPTY_STRING = ''
const dispatch = createEventDispatcher()
const search = writable(EMPTY_STRING)
$: propsFiltered = derived(search, ($search: string) => {
@@ -0,0 +1,190 @@
import type { Schema } from '$lib/common'
import { FlowModuleValue, ScriptService, type Flow, type FlowModule } from '$lib/gen'
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
import { userStore, workspaceStore } from '$lib/stores'
import { derived, get, writable } from 'svelte/store'
import { createInlineScriptModuleFromPath, getFirstStepSchema, loadSchemaFromModule } from './utils'
export const flowStore = writable<Flow>(undefined)
export const schemasStore = writable<Schema[]>([])
export function initFlow(flow: Flow) {
flowStore.set(flow)
// For each module in flow, we should load the corresponding schema
flow.value.modules.forEach((_, index) => {
loadSchema(index)
})
}
export const isCopyFirstStepSchemaDisabled = derived(flowStore, (flow: Flow) => {
const modules = flow.value.modules
const [firstModule] = modules
return (
modules.length === 0 || (firstModule.value.path === '' && firstModule.value.type === 'script')
)
})
export function addModule() {
const newModule: FlowModule = {
value: { type: FlowModuleValue.type.SCRIPT, path: '' },
input_transform: {}
}
flowStore.update((flow: Flow) => {
flow.value.modules = flow.value.modules.concat(newModule)
return flow
})
}
export async function pickScript(path: string, step: number) {
flowStore.update((flow: Flow) => {
if (flow.value.modules[step]) {
flow.value.modules[step].value.path = path
}
return flow
})
await loadSchema(step)
}
export async function createInlineScriptModule(language: FlowModuleValue.language, step: number) {
const code = language === FlowModuleValue.language.DENO ? DENO_INIT_CODE : PYTHON_INIT_CODE
flowStore.update((flow: Flow) => {
flow.value.modules[step].value = {
type: FlowModuleValue.type.RAWSCRIPT,
content: code,
language
}
return flow
})
await loadSchema(step)
}
export async function loadSchema(step: number) {
const flow = get(flowStore)
const module = flow.value.modules[step]
const { input_transform, schema } = await loadSchemaFromModule(module)
flowStore.update((flow: Flow) => {
flow.value.modules[step].input_transform = input_transform
return flow
})
schemasStore.update((schemas) => {
schemas[step] = schema
return schemas
})
}
export async function fork(step: number) {
const flow = get(flowStore)
const flowModuleValue = flow.value.modules[step].value
if (flowModuleValue.path) {
const moduleValue = await createInlineScriptModuleFromPath(flowModuleValue.path)
flowStore.update((flow: Flow) => {
flow.value.modules[step].value = moduleValue
return flow
})
}
}
export async function createScriptFromInlineScript(step: number) {
const flow = get(flowStore)
const schemas = get(schemasStore)
const user = get(userStore)
const flowModuleValue = flow.value.modules[step].value
const originalScriptPath = flowModuleValue.path
const wasForked = Boolean(originalScriptPath)
let suffix = `step-${step}`
if (wasForked && originalScriptPath) {
const [first, second, ...others] = originalScriptPath.split('/')
suffix = others.join('/')
}
const path = `${flow.path}/${suffix}`
const forkedDescription = wasForked ? `as a fork of ${originalScriptPath}` : ''
const description = `This script was edited in place of flow ${flow.path} ${forkedDescription} by ${user?.username} at step ${step}.`
const availablePath = await findNextAvailablePath(path)
await ScriptService.createScript({
workspace: get(workspaceStore)!,
requestBody: {
path: availablePath,
summary: '',
description,
content: flowModuleValue.content!,
parent_hash: undefined,
schema: schemas[step],
is_template: false,
language: flowModuleValue.language!
}
})
flowStore.update((flow: Flow) => {
flow.value.modules[step].value = {
type: FlowModuleValue.type.SCRIPT,
path: availablePath
}
return flow
})
await loadSchema(step)
}
export function removeModule(step: number) {
flowStore.update((flow: Flow) => {
flow.value.modules.splice(step, 1)
return flow
})
schemasStore.update((schemas: Schema[]) => {
schemas.splice(step, 1)
return schemas
})
}
export async function copyFirstStepSchema() {
const flow = get(flowStore)
const flowSchema = await getFirstStepSchema(flow)
flowStore.update((flow: Flow) => {
flow.schema = flowSchema
return flow
})
}
export async function findNextAvailablePath(path: string): Promise<string> {
try {
await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
path
})
const [_, version] = path.split(/.*_([0-9]*)/)
if (version.length > 0) {
path = path.slice(0, -(version.length + 1))
}
path = `${path}_${Number(version) + 1}`
return findNextAvailablePath(path)
} catch (e) {
// Catching an error means the path is available
return path
}
}
export function shouldPickOrCreateScript(flow: Flow, step: number): boolean {
const module = flow.value.modules[step]
return module.value.path === '' && module.value.language === undefined
}
@@ -0,0 +1,13 @@
<script lang="ts">
import type { IconDefinition } from '@fortawesome/free-brands-svg-icons'
import Icon from 'svelte-awesome'
export let icon: IconDefinition
export let label: string
export let iconColor: string
</script>
<button type="button" on:click class="default-secondary-button-v2 mb-2 w-full">
<Icon data={icon} class={`w-4 h-4 mr-2 -ml-2 ${iconColor}`} />
{label}
</button>
@@ -0,0 +1,34 @@
<script lang="ts">
import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
import ItemPicker from '$lib/components/ItemPicker.svelte'
import { hubScripts } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
type Item = { summary: String; path: String; version?: String }
const items: Item[] = $hubScripts ?? []
let itemPicker: ItemPicker
const dispatch = createEventDispatcher()
</script>
<ItemPicker
bind:this={itemPicker}
pickCallback={(path) => {
dispatch('pick', { path })
}}
itemName={'Script'}
extraField="summary"
loadItems={async () => {
return items
}}
/>
<FlowScriptPicker
label={'Pick a script from the Hub'}
icon={faUserGroup}
iconColor="text-blue-500"
on:click={() => itemPicker.openModal()}
/>
@@ -0,0 +1,45 @@
<script lang="ts">
import ItemPicker from '$lib/components/ItemPicker.svelte'
import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
import { ScriptService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import FlowScriptPicker from './FlowScriptPicker.svelte'
type Item = { summary: String; path: String; version?: String }
let items: Item[] = []
let itemPicker: ItemPicker
const dispatch = createEventDispatcher()
async function loadItems(): Promise<void> {
items = await ScriptService.listScripts({ workspace: $workspaceStore! })
}
$: {
if ($workspaceStore) {
loadItems()
}
}
</script>
<ItemPicker
bind:this={itemPicker}
pickCallback={(path) => {
dispatch('pick', { path })
}}
itemName={'Script'}
extraField="summary"
loadItems={async () => {
return items
}}
/>
<FlowScriptPicker
label={'Pick a script from your workspace'}
icon={faUserGroup}
iconColor="text-blue-500"
on:click={() => itemPicker.openModal()}
/>
+109
View File
@@ -1,3 +1,18 @@
import type { Schema } from '$lib/common'
import {
FlowModuleValue,
InputTransform,
ScriptService,
type Flow,
type FlowModule
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { loadSchema } from '$lib/scripts'
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
import { workspaceStore } from '$lib/stores'
import { emptySchema } from '$lib/utils'
import { get } from 'svelte/store'
function filterByKey(obj: Object, key: string): Object {
if (Object(obj) !== obj) {
return obj
@@ -51,3 +66,97 @@ export function formatValue(arg: any) {
}
return arg
}
export async function getFirstStepSchema(flow: Flow): Promise<Schema> {
const [firstModule] = flow.value.modules
if (firstModule.value.type === FlowModuleValue.type.RAWSCRIPT) {
const { language, content } = firstModule.value
if (language && content) {
const schema = emptySchema()
await inferArgs(language, content, schema)
return schema
}
} else if (firstModule.value.path) {
return await loadSchema(firstModule.value.path)
}
return emptySchema()
}
export function createInlineScriptModule(language: FlowModuleValue.language): FlowModuleValue {
const code = language === FlowModuleValue.language.DENO ? DENO_INIT_CODE : PYTHON_INIT_CODE
return {
type: FlowModuleValue.type.RAWSCRIPT,
content: code,
language
}
}
export async function getScriptByPath(path: string): Promise<{
content: string
language: FlowModuleValue.language
}> {
if (path.startsWith('hub/')) {
const content = await ScriptService.getHubScriptContentByPath({ path })
return {
content,
language: FlowModuleValue.language.DENO
}
} else {
const script = await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
path: path ?? ''
})
return {
content: script.content,
language: script.language
}
}
}
export async function createInlineScriptModuleFromPath(path: string): Promise<FlowModuleValue> {
const { content, language } = await getScriptByPath(path)
return {
type: FlowModuleValue.type.RAWSCRIPT,
language: language,
content: content,
path
}
}
export async function loadSchemaFromModule(module: FlowModule): Promise<{
input_transform: Record<string, InputTransform>
schema: Schema
}> {
const isRaw = module?.value.type === FlowModuleValue.type.RAWSCRIPT
if (isRaw || Boolean(module.value.path)) {
let schema: Schema
if (isRaw) {
schema = emptySchema()
await inferArgs(module.value.language!, module.value.content!, schema)
} else {
schema = await loadSchema(module.value.path!)
}
const keys = Object.keys(schema?.properties ?? {})
const inputTransform = keys.reduce((accu, key) => {
accu[key] = {
type: 'static',
value: ''
}
return accu
}, {})
return {
input_transform: inputTransform,
schema: schema ?? emptySchema()
}
}
return {
input_transform: {},
schema: emptySchema()
}
}
@@ -2,8 +2,8 @@
import { FlowService, type Flow } from '$lib/gen'
import { page } from '$app/stores'
import { workspaceStore } from '$lib/stores'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { workspaceStore } from '$lib/stores'
import { emptySchema } from '$lib/utils'
const initialState = $page.url.searchParams.get('state')