Inline script editor (#1029)

* feat(frontend): WIP

* feat(frontend): WI

* feat(frontend): Fix inline scripts

* feat(frontend): Fix inline scripts
This commit is contained in:
Faton Ramadani
2022-12-19 16:44:28 +01:00
committed by GitHub
parent f5a01fc768
commit b4e2735315
26 changed files with 593 additions and 463 deletions
@@ -9,7 +9,6 @@
import { encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import Path from './Path.svelte'
import RadioButton from './RadioButton.svelte'
import Required from './Required.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import CenteredPage from './CenteredPage.svelte'
@@ -14,7 +14,7 @@
let result: string = ''
</script>
<RunnableWrapper bind:componentInput {id} bind:result>
<RunnableWrapper bind:componentInput {id} bind:result autoRefresh={false}>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if result === ''}
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
@@ -22,7 +22,7 @@
export let result: any = undefined
export let forceSchemaDisplay: boolean = false
const { app, worldStore, runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
const { worldStore, runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
onMount(() => {
$runnableComponents[id] = async () => {
@@ -95,9 +95,11 @@
loadSchemaFromTriggerable($workspaceStore, path, runType)
} else if (runnable?.type === 'runnableByName' && !schema) {
const { inlineScriptName } = runnable
const { inlineScript } = runnable
// Inline scripts directly provide the schema
schema = $app.inlineScripts[inlineScriptName].schema
if (inlineScript) {
schema = inlineScript.schema
}
}
// When the schema is loaded, we need to update the inputs spec
@@ -165,12 +167,14 @@
}
if (runnable?.type === 'runnableByName') {
const { inlineScriptName } = runnable
const { inlineScript } = runnable
requestBody['raw_code'] = {
content: $app.inlineScripts[inlineScriptName].content,
language: $app.inlineScripts[inlineScriptName].language,
path: $app.inlineScripts[inlineScriptName].path
if (inlineScript) {
requestBody['raw_code'] = {
content: inlineScript.content,
language: inlineScript.language,
path: inlineScript.path
}
}
} else if (runnable?.type === 'runnableByPath') {
const { path, runType } = runnable
@@ -11,11 +11,26 @@
export let autoRefresh: boolean = true
export let runnableComponent: RunnableComponent | undefined = undefined
export let forceSchemaDisplay: boolean = false
function isRunnableDefined() {
if (!componentInput) return false
if (componentInput.type !== 'runnable') return false
if (
(componentInput.runnable?.type === 'runnableByName' &&
componentInput.runnable.inlineScript) ||
(componentInput.runnable?.type === 'runnableByPath' && componentInput.runnable.path)
) {
return true
}
return false
}
</script>
{#if componentInput === undefined}
<slot />
{:else if componentInput.type === 'runnable'}
{:else if componentInput.type === 'runnable' && isRunnableDefined()}
<RunnableComponent
bind:this={runnableComponent}
bind:inputs={componentInput.fields}
@@ -55,6 +55,10 @@
}
function setOptions(filteredResult: Array<Record<string, any>>) {
if (!Array.isArray(result)) {
return
}
const headers = Array.from(new Set(result.flatMap((row) => Object.keys(row))))
$options = {
@@ -13,11 +13,11 @@
<span
class={classNames(
'text-white px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute -top-5',
'text-white px-2 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute -top-5',
selected ? 'bg-indigo-500' : 'bg-gray-500'
)}
>
{displayData[component.type].name} - {component.id}
{component.id}
</span>
<button
class={classNames(
@@ -47,7 +47,14 @@
>
<RecomputeAllComponents />
<Grid bind:items={$app.grid} rowHeight={64} let:dataItem cols={columnConfiguration}>
<Grid
bind:items={$app.grid}
let:dataItem
rowHeight={64}
cols={columnConfiguration}
fastStart={true}
fillSpace={true}
>
{#each $app.grid as gridComponent (gridComponent.id)}
{#if gridComponent.data.id === dataItem.data.id}
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -13,20 +13,22 @@
import { isOpenStore } from './store'
import { gridColumns } from '../../gridUtils'
const { app } = getContext<AppEditorContext>('AppEditorContext')
const { app, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
function getMinDimensionsByComponent(componentType: AppComponent['type'], column: number): Size {
// Dimensions key formula: <mobile width>:<mobile height>-<desktop width>:<desktop height>
const dimensions: Record<`${number}:${number}-${number}:${number}`, AppComponent['type'][]> = {
'1:1-3:1': [ 'buttoncomponent', 'textcomponent', 'checkboxcomponent' ],
'1:2-3:2': [ 'textinputcomponent', 'numberinputcomponent', 'selectcomponent' ],
'2:2-6:4': [ 'displaycomponent' ],
'2:3-6:4': [ 'formcomponent' ],
'2:4-6:4': [ 'barchartcomponent', 'piechartcomponent' ],
'3:4-12:4': [ 'tablecomponent' ],
'1:1-3:1': ['buttoncomponent', 'textcomponent', 'checkboxcomponent'],
'1:2-3:2': ['textinputcomponent', 'numberinputcomponent', 'selectcomponent'],
'2:2-6:4': ['displaycomponent'],
'2:3-6:4': ['formcomponent'],
'2:4-6:4': ['barchartcomponent', 'piechartcomponent'],
'3:4-12:4': ['tablecomponent']
}
// Finds the key that is associated with the component type and extracts the dimensions from it
const [dimension] = Object.entries(dimensions).find(([_, value]) => value.includes(componentType)) || ['2:1-2:1']
const [dimension] = Object.entries(dimensions).find(([_, value]) =>
value.includes(componentType)
) || ['2:1-2:1']
const size = dimension.split('-')[column === 3 ? 0 : 1].split(':')
return { w: +size[0], h: +size[1] }
}
@@ -53,19 +55,17 @@
}
gridColumns.forEach((column) => {
newItem[column] = newComponent
const position = gridHelp.findSpace(newItem, grid, column)
const min = getMinDimensionsByComponent(appComponent.type, column)
const max = { w: 12, h: 12 }
newItem[column].w = min.w
newItem[column].h = min.h
newItem[column] = { ...newComponent, min, max, w: min.w, h: min.h }
const position = gridHelp.findSpace(newItem, grid, column)
newItem[column] = { ...newItem[column], ...position, min, max }
})
$app.grid = [...grid, newItem]
$selectedComponent = id
}
onMount(() => {
@@ -36,27 +36,39 @@
<PanelSection title="Outputs">
{#each Object.entries($staticOutputs) as [componentId, outputs], index}
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
<button
on:click={() => ($selectedComponent = componentId)}
class={classNames(
'px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit -mb-2',
$selectedComponent === componentId
? ' bg-indigo-500 text-white'
: 'bg-gray-200 text-gray-500'
)}
>
{getComponentNameById(componentId)} -
{componentId}
</button>
<div class="flex flex-row justify-between w-full -mb-2 ">
<button
on:click={() => ($selectedComponent = componentId)}
class={classNames(
'px-2 text-2xs py-0.5 font-bold rounded-t-sm w-fit',
$selectedComponent === componentId
? ' bg-indigo-500 text-white'
: 'bg-gray-200 text-gray-500'
)}
>
{componentId}
</button>
<span
class={classNames(
'px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit',
'bg-gray-500 text-white'
)}
>
{getComponentNameById(componentId)}
</span>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click={() => ($selectedComponent = componentId)}
class={classNames(
'w-full py-2 border',
$selectedComponent === componentId ? 'border border-blue-500' : 'cursor-pointer'
'w-full py-2 border relative',
$selectedComponent === componentId ? 'border border-blue-500 ' : 'cursor-pointer'
)}
>
{#if $selectedComponent === componentId && $connectingInput?.opened}
<div class="absolute top-0 left-0 w-full h-full bg-gray-500 bg-opacity-50 z-10" />
{/if}
<ComponentOutputViewer
{outputs}
{componentId}
@@ -0,0 +1,183 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { Preview, Script } from '$lib/gen'
import { initialCode } from '$lib/script_helpers'
import { emptySchema } from '$lib/utils'
import { faTrash } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../../types'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { Check, CheckCheck, CheckCircle, Code2, X } from 'lucide-svelte'
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
import type { ResultAppInput } from '../../inputType'
import InlineScriptEditorDrawer from './InlineScriptEditorDrawer.svelte'
import { inferArgs } from '$lib/infer'
import type { Schema } from '$lib/common'
import Badge from '$lib/components/common/badge/Badge.svelte'
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
export let componentInput: ResultAppInput
export let selectedScriptName: string | undefined = undefined
$: shouldDisplay =
componentInput.runnable?.type === 'runnableByName' &&
componentInput.runnable?.name === selectedScriptName
const { appPath } = getContext<AppEditorContext>('AppEditorContext')
let validCode = false
async function inferInlineScriptSchema(
language: Preview.language,
content: string,
schema: Schema
): Promise<Schema> {
try {
await inferArgs(language, content, schema)
validCode = true
} catch (e) {
console.error("Couldn't infer args", e)
validCode = false
}
return schema
}
async function createInlineScriptByLanguage(
language: Preview.language,
path: string,
subkind: 'pgsql' | 'mysql' | undefined = undefined
) {
const fullPath = `${appPath}/inline-script/${path}`
const content = initialCode(language, Script.kind.SCRIPT, subkind)
let schema: Schema = emptySchema()
schema = await inferInlineScriptSchema(language, content, schema)
const inlineScript = {
content,
language,
path: fullPath,
schema
}
if (componentInput?.runnable?.type === 'runnableByName') {
componentInput.runnable.inlineScript = inlineScript
}
}
</script>
{#if componentInput.runnable && componentInput.runnable.type === 'runnableByName' && componentInput.runnable.inlineScript}
<InlineScriptEditorDrawer
bind:this={inlineScriptEditorDrawer}
bind:inlineScript={componentInput.runnable.inlineScript}
/>
{/if}
{#if shouldDisplay}
{#if componentInput?.runnable?.type === 'runnableByName' && componentInput?.runnable?.inlineScript}
<div class="h-full p-4 flex flex-col gap-2 ">
<div class="flex w-full flex-row-reverse gap-2 items-center">
<Button
size="xs"
color="light"
variant="border"
on:click={() => {
if (selectedScriptName) {
inlineScriptEditorDrawer?.openDrawer()
}
}}
>
<div class="flex gap-1 items-center">
<Code2 size={16} />
Open full editor
</div>
</Button>
<Button
size="xs"
color="light"
variant="border"
iconOnly
startIcon={{ icon: faTrash }}
on:click={() => {
if (componentInput?.runnable?.type === 'runnableByName') {
componentInput.runnable = undefined
componentInput.fields = {}
}
}}
/>
{#if validCode}
<Badge color="green">
<CheckCircle size={16} />
</Badge>
{:else}
<Badge color="red">
<X size={16} />
</Badge>
{/if}
</div>
{#if componentInput?.runnable?.type === 'runnableByName' && componentInput?.runnable?.inlineScript}
<div class="border h-full">
<SimpleEditor
class="flex flex-1 grow h-full"
lang="typescript"
bind:code={componentInput.runnable.inlineScript.content}
fixedOverflowWidgets={false}
on:change={async () => {
if (
componentInput?.runnable?.type === 'runnableByName' &&
componentInput?.runnable?.inlineScript
) {
let schema = await inferInlineScriptSchema(
componentInput?.runnable?.inlineScript?.language,
componentInput.runnable.inlineScript.content,
componentInput.runnable.inlineScript.schema
)
componentInput.runnable.inlineScript.schema = schema
componentInput = componentInput
}
}}
/>
</div>
{/if}
</div>
{:else}
<div class="flex flex-col p-4 gap-2 text-sm">
Please choose a language:
<div class="flex gap-2 flex-row flex-wrap">
{#each Object.values(Script.language) as lang}
<FlowScriptPicker
label={lang}
{lang}
on:click={() => {
if (selectedScriptName) {
createInlineScriptByLanguage(lang, selectedScriptName)
}
}}
/>
{/each}
<FlowScriptPicker
label={`PostgreSQL`}
lang="pgsql"
on:click={() => {
if (selectedScriptName) {
createInlineScriptByLanguage(Script.language.DENO, selectedScriptName, 'pgsql')
}
}}
/>
<FlowScriptPicker
label={`MySQL`}
lang="mysql"
on:click={() => {
if (selectedScriptName) {
createInlineScriptByLanguage(Script.language.DENO, selectedScriptName, 'mysql')
}
}}
/>
</div>
</div>
{/if}
{/if}
@@ -1,58 +1,14 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import ScriptEditor from '$lib/components/ScriptEditor.svelte'
import { Preview } from '$lib/gen'
import { DENO_INIT_CODE_CLEAR } from '$lib/script_helpers'
import { emptySchema } from '$lib/utils'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../../types'
import type { InlineScript } from '../../types'
type InlineScript = {
content: string
language: Preview.language
path: string
schema: Schema
}
const { app, appPath } = getContext<AppEditorContext>('AppEditorContext')
let scriptEditorDrawer: Drawer
let selectedScript: InlineScript | undefined
export let inlineScript: InlineScript
export function openDrawer(path: string) {
if ($app.inlineScripts[path]) {
selectedScript = $app.inlineScripts[path]
scriptEditorDrawer.openDrawer?.()
}
}
export function createScript(): string {
let index = 0
let newScriptPath = `inline_script_${index}`
while ($app.inlineScripts?.[newScriptPath]) {
newScriptPath = `inline_script_${++index}`
}
const path = `${appPath}/inline-script/inline_script_${index}`
const inlineScript = {
content: DENO_INIT_CODE_CLEAR,
language: Preview.language.DENO,
path,
schema: emptySchema()
}
if ($app.inlineScripts) {
$app.inlineScripts[newScriptPath] = inlineScript
} else {
$app.inlineScripts = {
[newScriptPath]: inlineScript
}
}
return newScriptPath
export function openDrawer() {
scriptEditorDrawer.openDrawer?.()
}
</script>
@@ -63,12 +19,12 @@
forceOverflowVisible
on:close={scriptEditorDrawer.closeDrawer}
>
{#if selectedScript}
{#if inlineScript}
<ScriptEditor
lang={selectedScript.language}
bind:code={selectedScript.content}
path={selectedScript.path}
bind:schema={selectedScript.schema}
lang={inlineScript.language}
bind:code={inlineScript.content}
path={inlineScript.path}
bind:schema={inlineScript.schema}
fixedOverflowWidgets={false}
/>
{/if}
@@ -1,200 +1,26 @@
<script lang="ts">
import { fade } from 'svelte/transition'
import type { Schema } from '$lib/common'
import { Drawer } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import { Preview } from '$lib/gen'
import { DENO_INIT_CODE_CLEAR } from '$lib/script_helpers'
import { classNames, emptySchema } from '$lib/utils'
import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../../types'
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
import { Pane } from 'svelte-splitpanes'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
import { Code2 } from 'lucide-svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import InlineScriptEditorDrawer from './InlineScriptEditorDrawer.svelte'
import InlineScriptsPanelList from './InlineScriptsPanelList.svelte'
import InlineScriptEditor from './InlineScriptEditor.svelte'
const { app, appPath } = getContext<AppEditorContext>('AppEditorContext')
const { app } = getContext<AppEditorContext>('AppEditorContext')
let newScriptPath: string
let ignorePathError = false
$: isTakenPath = Object.keys($app.inlineScripts).includes(newScriptPath)
function createScript() {
// To prevent the error message flashing up just before the drawer is closed
ignorePathError = true
const path = `${appPath}/inline-script/${newScriptPath}`
const inlineScript = {
content: DENO_INIT_CODE_CLEAR,
language: Preview.language.DENO,
path,
schema: emptySchema()
}
if ($app.inlineScripts) {
$app.inlineScripts[newScriptPath] = inlineScript
} else {
$app.inlineScripts = {
[newScriptPath]: inlineScript
}
}
scriptCreationDrawer?.closeDrawer?.()
selectedScript = inlineScript
scriptEditorDrawer.openDrawer?.()
}
function afterCreateScript() {
newScriptPath = ''
ignorePathError = false
}
$: selectedScript = undefined as
| { content: string; language: Preview.language; path: string; schema: Schema }
| undefined
let scriptEditorDrawer: Drawer
let scriptCreationDrawer: Drawer | undefined = undefined
$: selectedScriptName = undefined as string | undefined
$: scriptsUsedByComponents = new Map<string, string>()
$: {
scriptsUsedByComponents.clear()
$app.grid
.filter((gridComponent) => gridComponent)
.forEach((gridComponent) => {
if (
gridComponent.data?.componentInput?.type === 'runnable' &&
gridComponent.data.componentInput.runnable !== undefined &&
gridComponent.data.componentInput.runnable.type === 'runnableByName'
) {
scriptsUsedByComponents.set(
gridComponent.data.componentInput.runnable.inlineScriptName,
gridComponent.data.id
)
}
})
}
function deleteInlineScript() {
const key = Object.keys($app.inlineScripts).find(
(key) => $app.inlineScripts[key].path === selectedScript?.path
)
if (key && $app.inlineScripts[key]) {
delete $app.inlineScripts[key]
$app = $app
selectedScript = undefined
}
}
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
let selectedScriptName: string | undefined = undefined
</script>
<Drawer bind:this={scriptCreationDrawer} size="600px" on:afterClose={afterCreateScript}>
<DrawerContent title="Script creation" on:close={scriptCreationDrawer.closeDrawer}>
<label for="pathInput" class="text-sm font-semibold"> Script name </label>
<div class="flex justify-between items-center gap-4">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
id="pathInput"
class="grow min-w-[150px]"
bind:value={newScriptPath}
on:keypress={(e) => e.key === 'Enter' && createScript()}
/>
<Button on:click={createScript} size="sm" disabled={isTakenPath} startIcon={{ icon: faPlus }}>
Create
</Button>
</div>
{#if isTakenPath && !ignorePathError}
<div transition:fade={{ duration: 100 }} class="text-sm text-red-600 h-5 mt-1">
This name is already used.
</div>
{/if}
</DrawerContent>
</Drawer>
<InlineScriptEditorDrawer bind:this={inlineScriptEditorDrawer} />
<SplitPanesWrapper>
<Pane size={25}>
<PanelSection title="Inline scripts">
<div class="flex flex-col gap-2 w-full">
{#if $app.inlineScripts && Object.keys($app.inlineScripts).length > 0}
<div class="flex gap-2 flex-col ">
{#each $app.inlineScripts ? Object.entries($app.inlineScripts) : [] as [key, value], index}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScript?.path === value.path ? 'bg-blue-100 text-blue-600' : ''
)},"
on:click={() => {
selectedScript = value
selectedScriptName = key
}}
>
<span class="text-xs">{key}</span>
{#if scriptsUsedByComponents.get(key)}
<Badge color="blue">{scriptsUsedByComponents.get(key)}</Badge>
{:else}
<Badge color="red">Unused</Badge>
{/if}
</div>
{/each}
</div>
{:else}
<div class="text-sm text-gray-500">No inline scripts</div>
{/if}
</div>
</PanelSection>
<InlineScriptsPanelList bind:selectedScriptName />
</Pane>
<Pane size={75}>
{#key selectedScript?.path}
{#if selectedScript}
<div class="h-full p-4 flex flex-col gap-2 ">
<div class="flex w-full flex-row-reverse gap-2 items-center">
<Button
size="xs"
color="light"
variant="border"
on:click={() => {
if (selectedScriptName) {
inlineScriptEditorDrawer?.openDrawer(selectedScriptName)
}
}}
>
<div class="flex gap-1 items-center">
<Code2 size={16} />
Open full editor
</div>
</Button>
<Button
size="xs"
color="light"
variant="border"
iconOnly
startIcon={{ icon: faTrash }}
on:click={deleteInlineScript}
/>
</div>
<div class="border h-full">
<SimpleEditor
class="flex flex-1 grow h-full"
lang="typescript"
bind:code={selectedScript.content}
fixedOverflowWidgets={false}
/>
</div>
</div>
{/if}
{/key}
{#each $app.grid as gridComponent, index (index)}
<InlineScriptEditor
bind:componentInput={gridComponent.data.componentInput}
{selectedScriptName}
/>
{/each}
</Pane>
</SplitPanesWrapper>
@@ -0,0 +1,54 @@
<script lang="ts">
import { Badge } from '$lib/components/common'
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { AppEditorContext } from '../../types'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
export let selectedScriptName: string | undefined = undefined
const { app } = getContext<AppEditorContext>('AppEditorContext')
function selectInlineScript(name: string) {
selectedScriptName = name
}
$: componentInlineScripts = $app.grid.reduce((acc, gridComponent) => {
const componentInput: AppInput = gridComponent.data.componentInput
if (componentInput?.type === 'runnable') {
if (componentInput.runnable?.type === 'runnableByName') {
acc.push({
name: componentInput.runnable.name,
id: gridComponent.id
})
}
}
return acc
}, [])
</script>
<PanelSection title="Inline scripts">
<div class="flex flex-col gap-2 w-full">
{#if componentInlineScripts.length > 0}
<div class="flex gap-2 flex-col ">
{#each componentInlineScripts as { name, id }, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptName === name ? 'bg-blue-100 text-blue-600' : ''
)},"
on:click={() => selectInlineScript(name)}
>
<span class="text-xs">{name}</span>
<Badge color="dark-indigo">{id}</Badge>
</div>
{/each}
</div>
{:else}
<div class="text-sm text-gray-500">No inline scripts</div>
{/if}
</div>
</PanelSection>
@@ -0,0 +1,51 @@
<script lang="ts">
import { ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import {
AlignCenterHorizontal,
AlignCenterVertical,
AlignEndHorizontal,
AlignEndVertical,
AlignStartHorizontal,
AlignStartVertical
} from 'lucide-svelte'
import type { AppComponent } from '../../types'
import PanelSection from './common/PanelSection.svelte'
export let component: AppComponent
</script>
{#if component.verticalAlignment !== undefined}
<PanelSection title="Alignment">
<div class="w-full text-xs font-semibold">Horizontal</div>
<div class="w-full">
<ToggleButtonGroup bind:selected={component.horizontalAlignment}>
<ToggleButton position="left" value="left" size="xs">
<AlignStartVertical size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterVertical size={16} />
</ToggleButton>
<ToggleButton position="right" value="right" size="xs">
<AlignEndVertical size={16} />
</ToggleButton>
</ToggleButtonGroup>
</div>
{#if component.type !== 'formcomponent'}
<div class="w-full text-xs font-semibold">Vertical</div>
<div class="w-full">
<ToggleButtonGroup bind:selected={component.verticalAlignment}>
<ToggleButton position="left" value="top" size="xs">
<AlignStartHorizontal size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterHorizontal size={16} />
</ToggleButton>
<ToggleButton position="right" value="bottom" size="xs">
<AlignEndHorizontal size={16} />
</ToggleButton>
</ToggleButtonGroup>
</div>
{/if}
</PanelSection>
{/if}
@@ -0,0 +1,35 @@
<script lang="ts">
import { ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import { faArrowRight, faCode, faPen } from '@fortawesome/free-solid-svg-icons'
import type { AppInput } from '../../inputType'
export let componentInput: AppInput
export let disableStatic: boolean = false
</script>
{#if componentInput.fieldType !== 'any'}
<div class="w-full">
<ToggleButtonGroup bind:selected={componentInput.type}>
<ToggleButton
position="left"
value="static"
startIcon={{ icon: faPen }}
size="xs"
disable={disableStatic}
>
Static
</ToggleButton>
<ToggleButton
value="connected"
position="center"
startIcon={{ icon: faArrowRight }}
size="xs"
>
Connected
</ToggleButton>
<ToggleButton position="right" value="runnable" startIcon={{ icon: faCode }} size="xs">
Computed
</ToggleButton>
</ToggleButtonGroup>
</div>
{/if}
@@ -1,45 +1,28 @@
<script lang="ts">
import { ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import {
faArrowRight,
faClose,
faCode,
faEdit,
faMinimize,
faPen,
faTrashAlt
} from '@fortawesome/free-solid-svg-icons'
import {
AlignStartHorizontal,
AlignStartVertical,
AlignCenterHorizontal,
AlignCenterVertical,
AlignEndHorizontal,
AlignEndVertical
} from 'lucide-svelte'
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppComponent, AppEditorContext } from '../../types'
import PanelSection from './common/PanelSection.svelte'
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
import TableActions from './TableActions.svelte'
import StaticInputEditor from './StaticInputEditor.svelte'
import ConnectedInputEditor from './ConnectedInputEditor.svelte'
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import { capitalize } from '$lib/utils'
import { fieldTypeToTsType } from '../../utils'
import Recompute from './Recompute.svelte'
import Alert from '$lib/components/common/alert/Alert.svelte'
import RunnableSelector from './mainInput/RunnableSelector.svelte'
import gridHelp from 'svelte-grid/build/helper/index.mjs'
import { gridColumns } from '../../gridUtils'
import InlineScriptEditorDrawer from '../inlineScriptsPanel/InlineScriptEditorDrawer.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import ComponentInputTypeEditor from './ComponentInputTypeEditor.svelte'
import AlignmentEditor from './AlignmentEditor.svelte'
import RunnableInputEditor from './inputEditor/RunnableInputEditor.svelte'
export let component: AppComponent | undefined
export let onDelete: (() => void) | undefined = undefined
const { app, staticOutputs, runnableComponents, appPath } =
const { app, staticOutputs, runnableComponents } =
getContext<AppEditorContext>('AppEditorContext')
function removeGridElement() {
@@ -69,12 +52,9 @@
}
}
}
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
</script>
{#if component}
<InlineScriptEditorDrawer bind:this={inlineScriptEditorDrawer} />
<div class="flex flex-col w-full divide-y">
{#if component.componentInput}
<PanelSection
@@ -90,92 +70,15 @@
</Badge>
{/if}
</svelte:fragment>
{#if component.componentInput.fieldType !== 'any'}
<div class="w-full">
<ToggleButtonGroup bind:selected={component.componentInput.type}>
<ToggleButton
position="left"
value="static"
startIcon={{ icon: faPen }}
size="xs"
disable={component.type === 'buttoncomponent'}
>
Static
</ToggleButton>
<ToggleButton
value="connected"
position="center"
startIcon={{ icon: faArrowRight }}
size="xs"
>
Connected
</ToggleButton>
<ToggleButton
position="right"
value="runnable"
startIcon={{ icon: faCode }}
size="xs"
>
Computed
</ToggleButton>
</ToggleButtonGroup>
</div>
{/if}
<ComponentInputTypeEditor bind:componentInput={component.componentInput} />
<div class="flex flex-col w-full gap-2 my-2">
{#if component.componentInput.type === 'static'}
<StaticInputEditor bind:componentInput={component.componentInput} />
{:else if component.componentInput.type === 'connected' && component.componentInput !== undefined}
<ConnectedInputEditor bind:componentInput={component.componentInput} />
{:else if component && component.componentInput?.type === 'runnable' && component.componentInput.runnable}
<div class="flex justify-between w-full items-center">
<span class="text-xs font-semibold">
{component.componentInput.runnable.type === 'runnableByName'
? component.componentInput.runnable.inlineScriptName
: component.componentInput.runnable.path}
</span>
<div>
<Button
size="xs"
color="light"
variant="border"
startIcon={{ icon: faEdit }}
on:click={() => {
if (
component?.componentInput?.type === 'runnable' &&
component?.componentInput?.runnable?.type === 'runnableByName' &&
component?.componentInput?.runnable.inlineScriptName
) {
inlineScriptEditorDrawer.openDrawer(
component.componentInput.runnable.inlineScriptName
)
}
}}
>
Edit
</Button>
<Button
size="xs"
color="red"
variant="border"
startIcon={{ icon: faClose }}
on:click={() => {
if (component?.componentInput?.type === 'runnable') {
component.componentInput.runnable = undefined
component.componentInput.fields = {}
component = component
}
}}
>
Clear
</Button>
</div>
</div>
{:else}
<RunnableSelector
inlineScripts={Object.keys($app.inlineScripts)}
bind:componentInput={component.componentInput}
{inlineScriptEditorDrawer}
/>
{:else if component.componentInput?.type === 'runnable' && component.componentInput !== undefined}
<RunnableInputEditor bind:appInput={component.componentInput} />
{/if}
</div>
{#if component.componentInput?.type === 'runnable' && Object.keys(component.componentInput.fields ?? {}).length > 0}
@@ -213,41 +116,7 @@
<TableActions bind:components={component.actionButtons} />
{/if}
{#if component.verticalAlignment !== undefined}
<PanelSection title="Alignment">
<div class="w-full text-xs font-semibold">Horizontal</div>
<div class="w-full">
<ToggleButtonGroup bind:selected={component.horizontalAlignment}>
<ToggleButton position="left" value="left" size="xs">
<AlignStartVertical size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterVertical size={16} />
</ToggleButton>
<ToggleButton position="right" value="right" size="xs">
<AlignEndVertical size={16} />
</ToggleButton>
</ToggleButtonGroup>
</div>
{#if component.type !== 'formcomponent'}
<div class="w-full text-xs font-semibold">Vertical</div>
<div class="w-full">
<ToggleButtonGroup bind:selected={component.verticalAlignment}>
<ToggleButton position="left" value="top" size="xs">
<AlignStartHorizontal size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterHorizontal size={16} />
</ToggleButton>
<ToggleButton position="right" value="bottom" size="xs">
<AlignEndHorizontal size={16} />
</ToggleButton>
</ToggleButtonGroup>
</div>
{/if}
</PanelSection>
{/if}
<AlignmentEditor bind:component />
{#if component.type === 'buttoncomponent' || component.type === 'formcomponent'}
<Recompute bind:recomputeIds={component.recomputeIds} ownId={component.id} />
{/if}
@@ -1,7 +1,7 @@
<script lang="ts">
import type { ConnectedAppInput, StaticAppInput, UserAppInput } from '../../inputType'
import ConnectedInputEditor from './ConnectedInputEditor.svelte'
import StaticInputEditor from './StaticInputEditor.svelte'
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
export let componentInput: StaticAppInput | ConnectedAppInput | UserAppInput
export let canHide: boolean = false
@@ -0,0 +1,54 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { faClose, faEdit } from '@fortawesome/free-solid-svg-icons'
import type { ResultAppInput } from '../../inputType'
import InlineScriptEditorDrawer from '../inlineScriptsPanel/InlineScriptEditorDrawer.svelte'
export let appInput: ResultAppInput
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
function edit() {
if (appInput.runnable?.type === 'runnableByName') {
inlineScriptEditorDrawer?.openDrawer()
}
}
function clear() {
if (appInput) {
appInput.runnable = undefined
appInput.fields = {}
appInput = appInput
}
}
</script>
{#if appInput.runnable && appInput.runnable.type === 'runnableByName' && appInput.runnable.inlineScript}
<InlineScriptEditorDrawer
bind:this={inlineScriptEditorDrawer}
bind:inlineScript={appInput.runnable.inlineScript}
/>
{/if}
<div class="flex justify-between w-full items-center">
<span class="text-xs font-semibold">
{#if appInput.runnable?.type === 'runnableByName'}
{appInput.runnable.name}
{:else if appInput.runnable?.type === 'runnableByPath'}
{appInput.runnable.path}
{/if}
</span>
<div>
{#if appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript}
<Button size="xs" color="light" variant="border" startIcon={{ icon: faEdit }} on:click={edit}>
Edit
</Button>
{/if}
<Button size="xs" color="red" variant="border" startIcon={{ icon: faClose }} on:click={clear}>
Clear
</Button>
</div>
</div>
{#if appInput.runnable?.type === 'runnableByName' && !appInput.runnable.inlineScript}
<span class="text-xs text-gray-500">
Please configure the language in the inline script panel
</span>
{/if}
@@ -1,6 +1,6 @@
<script lang="ts">
import type { StaticAppInput } from '../../inputType'
import StaticInputEditor from './StaticInputEditor.svelte'
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
export let value: any
export let canHide: boolean = false
@@ -1,9 +1,9 @@
<script lang="ts">
import type { AppEditorContext } from '../../types'
import type { AppEditorContext } from '../../../types'
import { Badge, Button } from '$lib/components/common'
import { faArrowRight, faClose } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { AppInput } from '../../../inputType'
export let componentInput: AppInput
@@ -16,7 +16,6 @@
componentInput.type === 'connected' &&
!componentInput.connection
) {
debugger
componentInput.connection = $connectingInput.input.connection
$connectingInput = {
opened: false,
@@ -0,0 +1,16 @@
<script lang="ts">
import type { ResultAppInput } from '$lib/components/apps/inputType'
import { isScriptByNameDefined, isScriptByPathDefined } from '$lib/components/apps/utils'
import RunnableSelector from '../mainInput/RunnableSelector.svelte'
import SelectedRunnable from '../SelectedRunnable.svelte'
export let appInput: ResultAppInput
$: isRunnableSelected = isScriptByPathDefined(appInput) || isScriptByNameDefined(appInput)
</script>
{#if isRunnableSelected}
<SelectedRunnable bind:appInput />
{:else}
<RunnableSelector bind:appInput />
{/if}
@@ -1,9 +1,9 @@
<script lang="ts">
import Toggle from '$lib/components/Toggle.svelte'
import { staticValues } from '../componentsPanel/componentStaticValues'
import type { StaticAppInput } from '../../inputType'
import { staticValues } from '../../componentsPanel/componentStaticValues'
import type { StaticAppInput } from '../../../inputType'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import ArrayStaticInputEditor from './ArrayStaticInputEditor.svelte'
import ArrayStaticInputEditor from '../ArrayStaticInputEditor.svelte'
export let componentInput: StaticAppInput | undefined
export let canHide: boolean = false
@@ -4,24 +4,25 @@
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
import { Building, Globe2 } from 'lucide-svelte'
import InlineScriptList from './InlineScriptList.svelte'
import type { AppInput } from '$lib/components/apps/inputType'
import type { ResultAppInput } from '$lib/components/apps/inputType'
import WorkspaceScriptList from './WorkspaceScriptList.svelte'
import WorkspaceFlowList from './WorkspaceFlowList.svelte'
import InlineScriptEditorDrawer from '../../inlineScriptsPanel/InlineScriptEditorDrawer.svelte'
import type { AppEditorContext, GridItem, InlineScript } from '$lib/components/apps/types'
import { getContext } from 'svelte'
type Tab = 'hubscripts' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
export let inlineScripts: string[]
export let componentInput: AppInput
export let appInput: ResultAppInput
let tab: Tab = 'inlinescripts'
let filter: string = ''
let picker: Drawer
const { app } = getContext<AppEditorContext>('AppEditorContext')
function pickScript(path: string) {
if (componentInput.type === 'runnable') {
componentInput.runnable = {
if (appInput.type === 'runnable') {
appInput.runnable = {
type: 'runnableByPath',
path,
runType: 'script'
@@ -30,8 +31,8 @@
}
function pickFlow(path: string) {
if (componentInput.type === 'runnable') {
componentInput.runnable = {
if (appInput.type === 'runnable') {
appInput.runnable = {
type: 'runnableByPath',
path,
runType: 'flow'
@@ -39,16 +40,47 @@
}
}
function pickInlineScript(inlineScriptName: string) {
if (componentInput.type === 'runnable') {
componentInput.runnable = {
function pickInlineScript(name: string, inlineScript: InlineScript) {
if (appInput.type === 'runnable') {
appInput.runnable = {
type: 'runnableByName',
inlineScriptName
name,
inlineScript
}
}
}
export let inlineScriptEditorDrawer: InlineScriptEditorDrawer
function createScript(): string {
let index = 0
let newScriptPath = `inline_script_${index}`
const names = $app.grid.reduce((acc, gridItem: GridItem) => {
const { componentInput } = gridItem.data
if (
componentInput.type === 'runnable' &&
componentInput.runnable?.type === 'runnableByName'
) {
acc.push(componentInput.runnable.name)
}
return acc
}, [] as string[])
while (names.includes(newScriptPath)) {
newScriptPath = `inline_script_${++index}`
}
appInput.runnable = {
type: 'runnableByName',
name: newScriptPath,
inlineScript: undefined
}
appInput = appInput
return newScriptPath
}
</script>
<Drawer bind:this={picker} size="1000px">
@@ -85,7 +117,7 @@
<div class="flex flex-col gap-y-16">
<div class="flex flex-col">
{#if tab == 'inlinescripts'}
<InlineScriptList {inlineScripts} on:pick={(e) => pickInlineScript(e.detail)} />
<InlineScriptList on:pick={(e) => pickInlineScript('', e.detail)} />
{:else if tab == 'workspacescripts'}
<WorkspaceScriptList on:pick={(e) => pickScript(e.detail)} />
{:else if tab == 'workspaceflows'}
@@ -100,17 +132,9 @@
</DrawerContent>
</Drawer>
<InlineScriptEditorDrawer bind:this={inlineScriptEditorDrawer} />
<div class="flex flex-row gap-2 justify-between">
<div class="flex flex-col gap-2">
<Button
on:click={() => {
const name = inlineScriptEditorDrawer.createScript()
inlineScriptEditorDrawer.openDrawer(name)
setTimeout(() => {
pickInlineScript(name)
})
}}
on:click={createScript}
size="sm"
color="light"
variant="border"
@@ -1,4 +1,5 @@
import type { staticValues } from './editor/componentsPanel/componentStaticValues'
import type { InlineScript } from './types'
export type InputType =
| 'text'
@@ -46,7 +47,8 @@ type RunnableByPath = {
}
type RunnableByName = {
inlineScriptName: string
name: string
inlineScript: InlineScript | undefined
type: 'runnableByName'
}
@@ -108,7 +108,6 @@ export type InlineScript = {
export type App = {
grid: GridItem[]
inlineScripts: Record<string, InlineScript>
title: string
}
+25 -4
View File
@@ -3,7 +3,6 @@ import { FlowService, ScriptService } from '$lib/gen'
import {
BarChart4,
Binary,
BoxSelect,
CircleDot,
FormInput,
Inspect,
@@ -16,7 +15,7 @@ import {
Type,
ToggleLeft
} from 'lucide-svelte'
import type { AppInputs, InputType } from './inputType'
import type { AppInput, AppInputs, InputType } from './inputType'
import type { AppComponent } from './types'
export async function loadSchema(
@@ -43,8 +42,6 @@ export async function loadSchema(
path
})
debugger
return script.schema
}
}
@@ -163,3 +160,27 @@ export function fieldTypeToTsType(inputType: InputType): string {
return 'string'
}
}
export function isScriptByNameDefined(appInput: AppInput | undefined): boolean {
if (!appInput) {
return false
}
if (appInput.type === 'runnable' && appInput.runnable?.type == 'runnableByName') {
return Boolean(appInput.runnable?.name)
}
return false
}
export function isScriptByPathDefined(appInput: AppInput | undefined): boolean {
if (!appInput) {
return false
}
if (appInput.type === 'runnable' && appInput.runnable?.type == 'runnableByPath') {
return Boolean(appInput.runnable?.path)
}
return false
}