mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 16:09:39 +00:00
feat(frontend): Fork + Fix table (#1037)
* feat(frontend): WIP * feat(frontend): WIP * feat(frontend): add support unused inline scripts * feat(frontend): fix all interaction * feat(frontend): Fix pick * feat(frontend): add ability to fork * feat(frontend): fix mobile preview * feat(frontend): fix table * feat(frontend): fix table actions * feat(frontend): Fix build error
This commit is contained in:
@@ -23,7 +23,8 @@
|
||||
async function createApp() {
|
||||
const appJson: App = {
|
||||
grid: [],
|
||||
title: 'New app'
|
||||
title: 'New app',
|
||||
unusedInlineScripts: []
|
||||
}
|
||||
|
||||
const policy = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { AppInput } from '../../inputType'
|
||||
import { isScriptByNameDefined, isScriptByPathDefined } from '../../utils'
|
||||
import NonRunnableComponent from './NonRunnableComponent.svelte'
|
||||
import RunnableComponent from './RunnableComponent.svelte'
|
||||
|
||||
@@ -13,18 +14,7 @@
|
||||
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
|
||||
return isScriptByNameDefined(componentInput) || isScriptByPathDefined(componentInput)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
let page = 1
|
||||
|
||||
const options = writable<TableOptions<T>>({
|
||||
data: result,
|
||||
data: [],
|
||||
columns: [],
|
||||
...tableOptions
|
||||
})
|
||||
|
||||
const table = createSvelteTable(options)
|
||||
let table = createSvelteTable(options)
|
||||
|
||||
const { worldStore, staticOutputs: staticOutputsStore } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -91,12 +91,18 @@
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
selectedRow: Output<any>
|
||||
}
|
||||
|
||||
function rerender() {
|
||||
table = createSvelteTable(options)
|
||||
}
|
||||
|
||||
$: result && rerender()
|
||||
</script>
|
||||
|
||||
<InputValue input={configuration.search} bind:value={search} />
|
||||
<InputValue input={configuration.pagination} bind:value={pagination} />
|
||||
|
||||
<RunnableWrapper bind:componentInput {id} bind:result {extraQueryParams}>
|
||||
<RunnableWrapper bind:componentInput {id} bind:result {extraQueryParams} autoRefresh={false}>
|
||||
<div class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full">
|
||||
<div class="py-2 px-4">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -129,7 +135,7 @@
|
||||
{/each}
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white ">
|
||||
{#each $table.getRowModel().rows as row, rowIndex}
|
||||
{#each $table.getRowModel().rows as row, rowIndex (row.id)}
|
||||
<tr
|
||||
class={classNames(
|
||||
selectedRowIndex === rowIndex
|
||||
@@ -143,7 +149,7 @@
|
||||
)}
|
||||
on:click={() => toggleRow(row, rowIndex)}
|
||||
>
|
||||
{#each row.getVisibleCells() as cell}
|
||||
{#each row.getVisibleCells() as cell, index (index)}
|
||||
<td class="p-4 whitespace-nowrap text-xs text-gray-900">
|
||||
<svelte:component
|
||||
this={flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
@@ -153,7 +159,7 @@
|
||||
|
||||
{#if actionButtons.length > 0}
|
||||
<td class="flex flex-row gap-2 p-4">
|
||||
{#each actionButtons as props}
|
||||
{#each actionButtons as props, actionIndex (actionIndex)}
|
||||
<AppButton
|
||||
{...props}
|
||||
extraQueryParams={{ row }}
|
||||
|
||||
@@ -66,9 +66,15 @@
|
||||
})
|
||||
|
||||
$: mounted && ($worldStore = buildWorld($staticOutputs))
|
||||
$: selectedTab = $selectedComponent ? 'settings' : 'insert'
|
||||
$: previewing = $mode === 'preview'
|
||||
$: width = $breakpoint === 'sm' ? 'w-[640px]' : 'min-w-[900px] w-full'
|
||||
$: width = $breakpoint === 'sm' ? 'w-[640px]' : 'min-w-[1080px] w-full'
|
||||
|
||||
let selectedTab: 'insert' | 'settings' = 'insert'
|
||||
$: if ($selectedComponent) {
|
||||
selectedTab = 'settings'
|
||||
} else {
|
||||
selectedTab = 'insert'
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
@@ -77,7 +83,7 @@
|
||||
{/if}
|
||||
|
||||
{#if previewing}
|
||||
<AppPreview app={$appStore} appPath={path} />
|
||||
<AppPreview app={$appStore} appPath={path} {breakpoint} />
|
||||
{:else}
|
||||
<SplitPanesWrapper class="max-w-full overflow-hidden">
|
||||
<Pane size={20}>
|
||||
@@ -85,10 +91,10 @@
|
||||
</Pane>
|
||||
<Pane size={60}>
|
||||
<SplitPanesWrapper horizontal>
|
||||
<Pane size={75}>
|
||||
<div class="bg-gray-100 p-4 relative min-h-full w-full">
|
||||
<Pane size={70}>
|
||||
<div class={classNames('bg-gray-100 mx-auto relative min-h-full', width)}>
|
||||
{#if $appStore.grid}
|
||||
<div class={classNames('mx-auto h-full bg-gray-100', width)}>
|
||||
<div class={classNames('mx-auto p-4 w-full h-full bg-gray-100', width)}>
|
||||
<GridEditor />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -99,7 +105,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={25}>
|
||||
<Pane size={30}>
|
||||
<InlineScriptsPanel />
|
||||
</Pane>
|
||||
</SplitPanesWrapper>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, setContext } from 'svelte'
|
||||
|
||||
import { writable } from 'svelte/store'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { buildWorld, type World } from '../rx'
|
||||
import type {
|
||||
App,
|
||||
@@ -16,13 +16,13 @@
|
||||
|
||||
export let app: App
|
||||
export let appPath: string
|
||||
export let breakpoint: Writable<EditorBreakpoint>
|
||||
|
||||
const appStore = writable<App>(app)
|
||||
const worldStore = writable<World | undefined>(undefined)
|
||||
const staticOutputs = writable<Record<string, string[]>>({})
|
||||
const selectedComponent = writable<string | undefined>(undefined)
|
||||
const mode = writable<EditorMode>('preview')
|
||||
const breakpoint = writable<EditorBreakpoint>('lg')
|
||||
|
||||
const connectingInput = writable<ConnectingInput>({
|
||||
opened: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { AppComponent } from '../types'
|
||||
import { Lock, X } from 'lucide-svelte'
|
||||
import { Anchor, Lock, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
@@ -30,9 +30,9 @@
|
||||
}}
|
||||
>
|
||||
{#if locked}
|
||||
<Lock size={16} class="text-red-500" />
|
||||
<Anchor size={16} class="text-orange-500" />
|
||||
{:else}
|
||||
<Lock size={16} />
|
||||
<Anchor size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import type { AppEditorContext, InlineScript } from '../types'
|
||||
import Grid from 'svelte-grid'
|
||||
import ComponentEditor from './ComponentEditor.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
@@ -29,12 +29,30 @@
|
||||
$app.grid.map((gridItem) => enableDrag(gridItem))
|
||||
}
|
||||
|
||||
function deleteComponent(component) {
|
||||
function removeGridElement(component) {
|
||||
if (component) {
|
||||
$app.grid = $app.grid.filter((gridComponent) => gridComponent.data.id !== component?.id)
|
||||
$app.grid = $app.grid.filter((gridComponent) => {
|
||||
if (gridComponent.data.id === component.id) {
|
||||
if (
|
||||
gridComponent.data.componentInput?.runnable?.type === 'runnableByName' &&
|
||||
gridComponent.data.componentInput?.runnable.inlineScript
|
||||
) {
|
||||
const { name, inlineScript } = gridComponent.data.componentInput?.runnable
|
||||
|
||||
gridColumns.forEach((colIndex) => {
|
||||
$app.grid = gridHelp.adjust($app.grid, colIndex)
|
||||
if (!$app.unusedInlineScripts) {
|
||||
$app.unusedInlineScripts = []
|
||||
}
|
||||
|
||||
$app.unusedInlineScripts.push({
|
||||
name,
|
||||
inlineScript
|
||||
})
|
||||
|
||||
$app = $app
|
||||
}
|
||||
}
|
||||
|
||||
return gridComponent.data.id !== component?.id
|
||||
})
|
||||
|
||||
// Delete static inputs
|
||||
@@ -43,6 +61,8 @@
|
||||
|
||||
delete $runnableComponents[component.id]
|
||||
$runnableComponents = $runnableComponents
|
||||
|
||||
$selectedComponent = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -55,7 +75,6 @@
|
||||
rowHeight={64}
|
||||
cols={columnConfiguration}
|
||||
fastStart={true}
|
||||
throttleUpdate={50}
|
||||
on:pointerup={({ detail }) => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = detail.id
|
||||
@@ -73,7 +92,7 @@
|
||||
<ComponentEditor
|
||||
bind:component={gridComponent.data}
|
||||
selected={$selectedComponent === dataItem.data.id}
|
||||
on:delete={() => deleteComponent(gridComponent.data)}
|
||||
on:delete={() => removeGridElement(gridComponent.data)}
|
||||
on:lock={() => {
|
||||
gridComponent = toggleFixed(gridComponent)
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
|
||||
import { Script, type Preview } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { fly } from 'svelte/transition'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
|
||||
export let name: string
|
||||
|
||||
const { appPath } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function inferInlineScriptSchema(
|
||||
language: Preview.language,
|
||||
content: string,
|
||||
schema: Schema
|
||||
): Promise<Schema> {
|
||||
try {
|
||||
await inferArgs(language, content, schema)
|
||||
} catch (e) {
|
||||
console.error("Couldn't infer args", e)
|
||||
}
|
||||
|
||||
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 newInlineScript = {
|
||||
content,
|
||||
language,
|
||||
path: fullPath,
|
||||
schema
|
||||
}
|
||||
dispatch('new', newInlineScript)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col p-4 gap-2 text-sm" in:fly={{ duration: 50 }}>
|
||||
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={() => {
|
||||
createInlineScriptByLanguage(lang, name)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<FlowScriptPicker
|
||||
label={`PostgreSQL`}
|
||||
lang="pgsql"
|
||||
on:click={() => {
|
||||
createInlineScriptByLanguage(Script.language.DENO, name, 'pgsql')
|
||||
}}
|
||||
/>
|
||||
<FlowScriptPicker
|
||||
label={`MySQL`}
|
||||
lang="mysql"
|
||||
on:click={() => {
|
||||
createInlineScriptByLanguage(Script.language.DENO, name, 'mysql')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
+68
-152
@@ -1,15 +1,11 @@
|
||||
<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 type { Preview } from '$lib/gen'
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import type { InlineScript } from '../../types'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { 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'
|
||||
@@ -17,13 +13,9 @@
|
||||
import { fly } from 'svelte/transition'
|
||||
|
||||
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
|
||||
export let componentInput: ResultAppInput
|
||||
|
||||
const name =
|
||||
componentInput.runnable?.type === 'runnableByName' ? componentInput.runnable?.name : ''
|
||||
|
||||
$: shouldDisplay = componentInput.runnable?.type === 'runnableByName'
|
||||
const { appPath } = getContext<AppEditorContext>('AppEditorContext')
|
||||
export let inlineScript: InlineScript
|
||||
export let name: string | undefined = undefined
|
||||
|
||||
let validCode = false
|
||||
|
||||
@@ -43,152 +35,76 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (
|
||||
componentInput.runnable?.type === 'runnableByName' &&
|
||||
componentInput.runnable.inlineScript
|
||||
) {
|
||||
componentInput.runnable.inlineScript.schema = await inferInlineScriptSchema(
|
||||
componentInput.runnable.inlineScript?.language,
|
||||
componentInput.runnable.inlineScript?.content,
|
||||
componentInput.runnable.inlineScript?.schema
|
||||
if (inlineScript) {
|
||||
inlineScript.schema = await inferInlineScriptSchema(
|
||||
inlineScript?.language,
|
||||
inlineScript?.content,
|
||||
inlineScript?.schema
|
||||
)
|
||||
}
|
||||
})
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if componentInput.runnable && componentInput.runnable.type === 'runnableByName' && componentInput.runnable.inlineScript}
|
||||
<InlineScriptEditorDrawer
|
||||
bind:this={inlineScriptEditorDrawer}
|
||||
bind:inlineScript={componentInput.runnable.inlineScript}
|
||||
/>
|
||||
{/if}
|
||||
<InlineScriptEditorDrawer bind:this={inlineScriptEditorDrawer} bind:inlineScript />
|
||||
|
||||
{#if shouldDisplay}
|
||||
{#if componentInput?.runnable?.type === 'runnableByName' && componentInput?.runnable?.inlineScript}
|
||||
<div class="h-full p-4 flex flex-col gap-2" transition:fly={{ duration: 50 }}>
|
||||
<div class="flex justify-between w-full flex-row items-center">
|
||||
{#if componentInput?.runnable?.name !== undefined}
|
||||
<input bind:value={componentInput.runnable.name} placeholder="Inline script name" />
|
||||
{/if}
|
||||
<div class="flex w-full flex-row gap-2 items-center justify-end">
|
||||
{#if validCode}
|
||||
<Badge color="green">
|
||||
<CheckCircle size={16} />
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge color="red">
|
||||
<X size={16} />
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
iconOnly
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => {
|
||||
if (componentInput?.runnable?.type === 'runnableByName') {
|
||||
componentInput.runnable = undefined
|
||||
componentInput.fields = {}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
inlineScriptEditorDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
<div class="flex gap-1 items-center">
|
||||
<Code2 size={16} />
|
||||
Open full editor
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</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>
|
||||
<div class="h-full p-4 flex flex-col gap-2" transition:fly={{ duration: 50 }}>
|
||||
<div class="flex justify-between w-full flex-row items-center">
|
||||
{#if name !== undefined}
|
||||
<input bind:value={name} placeholder="Inline script name" />
|
||||
{/if}
|
||||
<div class="flex w-full flex-row gap-2 items-center justify-end">
|
||||
{#if validCode}
|
||||
<Badge color="green">
|
||||
<CheckCircle size={16} />
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge color="red">
|
||||
<X size={16} />
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col p-4 gap-2 text-sm" in:fly={{ duration: 50 }}>
|
||||
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={() => {
|
||||
createInlineScriptByLanguage(lang, name)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<FlowScriptPicker
|
||||
label={`PostgreSQL`}
|
||||
lang="pgsql"
|
||||
on:click={() => {
|
||||
createInlineScriptByLanguage(Script.language.DENO, name, 'pgsql')
|
||||
}}
|
||||
/>
|
||||
<FlowScriptPicker
|
||||
label={`MySQL`}
|
||||
lang="mysql"
|
||||
on:click={() => {
|
||||
createInlineScriptByLanguage(Script.language.DENO, name, 'mysql')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
iconOnly
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => dispatch('delete')}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
on:click={() => {
|
||||
inlineScriptEditorDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
<div class="flex gap-1 items-center">
|
||||
<Code2 size={16} />
|
||||
Open full editor
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border h-full">
|
||||
<SimpleEditor
|
||||
class="flex flex-1 grow h-full"
|
||||
lang="typescript"
|
||||
bind:code={inlineScript.content}
|
||||
fixedOverflowWidgets={false}
|
||||
on:change={async () => {
|
||||
if (inlineScript) {
|
||||
let schema = await inferInlineScriptSchema(
|
||||
inlineScript?.language,
|
||||
inlineScript.content,
|
||||
inlineScript.schema
|
||||
)
|
||||
inlineScript.schema = schema
|
||||
inlineScript = inlineScript
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import FlowModuleScript from '$lib/components/flows/content/FlowModuleScript.svelte'
|
||||
import { getScriptByPath } from '$lib/utils'
|
||||
import { faCodeBranch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { r } from 'svelte-highlight/languages'
|
||||
import type { ResultAppInput } from '../../inputType'
|
||||
import { clearResultAppInput } from '../../utils'
|
||||
import EmptyInlineScript from './EmptyInlineScript.svelte'
|
||||
import InlineScriptEditor from './InlineScriptEditor.svelte'
|
||||
|
||||
export let componentInput: ResultAppInput
|
||||
|
||||
async function fork(path: string) {
|
||||
const { content, language, schema } = await getScriptByPath(path)
|
||||
componentInput.runnable = {
|
||||
type: 'runnableByName',
|
||||
name: path,
|
||||
inlineScript: {
|
||||
content,
|
||||
language,
|
||||
schema,
|
||||
path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if componentInput?.runnable?.type === 'runnableByName' && componentInput?.runnable?.name !== undefined}
|
||||
{#if componentInput.runnable.inlineScript}
|
||||
<InlineScriptEditor
|
||||
bind:inlineScript={componentInput.runnable.inlineScript}
|
||||
bind:name={componentInput.runnable.name}
|
||||
on:delete={() => {
|
||||
componentInput = clearResultAppInput(componentInput)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<EmptyInlineScript
|
||||
name={componentInput.runnable.name}
|
||||
on:new={(e) => {
|
||||
if (componentInput?.runnable?.type === 'runnableByName') {
|
||||
componentInput.runnable.inlineScript = e.detail
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else if componentInput?.runnable?.type === 'runnableByPath' && componentInput?.runnable?.path}
|
||||
<div class="p-2 h-full flex flex-col gap-2 ">
|
||||
<div>
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: faCodeBranch }}
|
||||
on:click={() => {
|
||||
if (componentInput.runnable?.type === 'runnableByPath') {
|
||||
fork(componentInput.runnable.path)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
<div class="border w-full">
|
||||
<FlowModuleScript path={componentInput.runnable.path} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
+24
-1
@@ -4,6 +4,7 @@
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import { Pane } from 'svelte-splitpanes'
|
||||
import InlineScriptsPanelList from './InlineScriptsPanelList.svelte'
|
||||
import InlineScriptEditorPanel from './InlineScriptEditorPanel.svelte'
|
||||
import InlineScriptEditor from './InlineScriptEditor.svelte'
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -18,8 +19,30 @@
|
||||
<Pane size={75}>
|
||||
{#each $app.grid as gridComponent, index (index)}
|
||||
{#if gridComponent.data.id === selectedScriptComponentId}
|
||||
<InlineScriptEditor bind:componentInput={gridComponent.data.componentInput} />
|
||||
<InlineScriptEditorPanel bind:componentInput={gridComponent.data.componentInput} />
|
||||
{/if}
|
||||
|
||||
{#if gridComponent.data.type === 'tablecomponent'}
|
||||
{#each gridComponent.data.actionButtons as actionButton, index (index)}
|
||||
{#if actionButton.id === selectedScriptComponentId}
|
||||
<InlineScriptEditorPanel bind:componentInput={actionButton.componentInput} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
{#if Array.isArray($app.unusedInlineScripts)}
|
||||
{#each $app.unusedInlineScripts as unusedInlineScript, index (index)}
|
||||
{#if unusedInlineScript.name === selectedScriptComponentId}
|
||||
<InlineScriptEditor
|
||||
bind:inlineScript={unusedInlineScript.inlineScript}
|
||||
on:delete={() => {
|
||||
// remove the script from the array at the index
|
||||
$app.unusedInlineScripts.splice(index, 1)
|
||||
$app.unusedInlineScripts = [...$app.unusedInlineScripts]
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</Pane>
|
||||
</SplitPanesWrapper>
|
||||
|
||||
+94
-9
@@ -3,19 +3,36 @@
|
||||
import { classNames } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppComponent, AppEditorContext } from '../../types'
|
||||
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
|
||||
|
||||
export let selectedScriptComponentId: string | undefined = undefined
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function selectInlineScript(id: string) {
|
||||
selectedScriptComponentId = id
|
||||
function selectInlineScript(id: string, subId?: string) {
|
||||
selectedScriptComponentId = subId ? subId : id
|
||||
$selectedComponent = id
|
||||
}
|
||||
|
||||
$: componentInlineScripts = $app.grid.reduce((acc, gridComponent) => {
|
||||
const componentInput: AppInput = gridComponent.data.componentInput
|
||||
$: runnablesByName = $app.grid.reduce((acc, gridComponent) => {
|
||||
const component: AppComponent = gridComponent.data
|
||||
|
||||
if (component.type === 'tablecomponent') {
|
||||
component.actionButtons.forEach((actionButton) => {
|
||||
if (actionButton.componentInput?.type === 'runnable') {
|
||||
if (actionButton.componentInput.runnable?.type === 'runnableByName') {
|
||||
acc.push({
|
||||
name: actionButton.componentInput.runnable.name,
|
||||
id: gridComponent.id,
|
||||
subId: actionButton.id
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const componentInput = component.componentInput
|
||||
|
||||
if (componentInput?.type === 'runnable') {
|
||||
if (componentInput.runnable?.type === 'runnableByName') {
|
||||
@@ -27,13 +44,81 @@
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
$: runnablesByPath = $app.grid.reduce((acc, gridComponent) => {
|
||||
const componentInput: AppInput = gridComponent.data.componentInput
|
||||
|
||||
if (componentInput?.type === 'runnable') {
|
||||
if (componentInput.runnable?.type === 'runnableByPath') {
|
||||
acc.push({
|
||||
name: componentInput.runnable.path,
|
||||
id: gridComponent.id
|
||||
})
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// When seleced component changes, update selectedScriptComponentId
|
||||
$: {
|
||||
if (selectedComponent) {
|
||||
selectedScriptComponentId = $selectedComponent
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<PanelSection title="Inline scripts">
|
||||
<PanelSection title="Inline scripts" smallPadding>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if componentInlineScripts.length > 0}
|
||||
{#if runnablesByName.length > 0}
|
||||
<div class="flex gap-2 flex-col ">
|
||||
{#each componentInlineScripts as { name, id }, index (index)}
|
||||
{#each runnablesByName as { name, id, subId }, 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-sm cursor-pointer hover:bg-blue-50 hover:text-blue-400',
|
||||
selectedScriptComponentId === (subId ? subId : id) ? 'border-blue-500 border' : ''
|
||||
)},"
|
||||
on:click={() => selectInlineScript(id, subId)}
|
||||
>
|
||||
<span class="text-xs">{name}</span>
|
||||
<div>
|
||||
<Badge color="dark-indigo">{id}</Badge>
|
||||
{#if subId}
|
||||
<Badge color="dark-indigo">{subId}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-gray-500">No inline scripts</div>
|
||||
{/if}
|
||||
|
||||
{#if Array.isArray($app.unusedInlineScripts) && $app.unusedInlineScripts.length > 0}
|
||||
<div class="flex gap-2 flex-col ">
|
||||
{#each $app.unusedInlineScripts as unusedInlineScript, 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',
|
||||
selectedScriptComponentId === '' ? 'bg-blue-100 text-blue-600' : ''
|
||||
)},"
|
||||
on:click={() => selectInlineScript(unusedInlineScript.name)}
|
||||
>
|
||||
<span class="text-xs">{unusedInlineScript.name}</span>
|
||||
<Badge color="red">Unused</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Runnable by path" smallPadding>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if runnablesByPath.length > 0}
|
||||
<div class="flex gap-2 flex-col ">
|
||||
{#each runnablesByPath as { name, id }, index (index)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{classNames(
|
||||
|
||||
@@ -39,10 +39,6 @@
|
||||
if (component) {
|
||||
$app.grid = $app.grid.filter((gridComponent) => gridComponent.data.id !== component?.id)
|
||||
|
||||
gridColumns.forEach((colIndex) => {
|
||||
$app.grid = gridHelp.adjust($app.grid, colIndex)
|
||||
})
|
||||
|
||||
// Delete static inputs
|
||||
delete $staticOutputs[component.id]
|
||||
$staticOutputs = $staticOutputs
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { faClose, faEdit } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { ResultAppInput } from '../../inputType'
|
||||
import { clearResultAppInput } from '../../utils'
|
||||
import InlineScriptEditorDrawer from '../inlineScriptsPanel/InlineScriptEditorDrawer.svelte'
|
||||
|
||||
export let appInput: ResultAppInput
|
||||
@@ -14,11 +15,7 @@
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (appInput) {
|
||||
appInput.runnable = undefined
|
||||
appInput.fields = {}
|
||||
appInput = appInput
|
||||
}
|
||||
appInput = clearResultAppInput(appInput)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+6
-3
@@ -26,9 +26,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if inlineScripts.length == 0}
|
||||
Should create
|
||||
{:else if filteredItems.length == 0}
|
||||
{#if inlineScripts.length === 0}
|
||||
<div class="flex flex-col w-full h-full">
|
||||
<div class="text-md ">No inline scripts</div>
|
||||
<div class="text-sm">Add inline scripts to your app</div>
|
||||
</div>
|
||||
{:else if filteredItems.length === 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-200 border rounded-md">
|
||||
|
||||
+19
-5
@@ -40,13 +40,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function pickInlineScript(name: string, inlineScript: InlineScript) {
|
||||
if (appInput.type === 'runnable') {
|
||||
function pickInlineScript(name: string) {
|
||||
const unusedInlineScriptIndex = $app.unusedInlineScripts?.findIndex(
|
||||
(script) => script.name === name
|
||||
)
|
||||
const unusedInlineScript = $app.unusedInlineScripts?.[unusedInlineScriptIndex]
|
||||
if (appInput.type === 'runnable' && unusedInlineScript?.inlineScript) {
|
||||
appInput.runnable = {
|
||||
type: 'runnableByName',
|
||||
name,
|
||||
inlineScript
|
||||
inlineScript: unusedInlineScript.inlineScript
|
||||
}
|
||||
|
||||
$app.unusedInlineScripts.splice(unusedInlineScriptIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +73,10 @@
|
||||
return acc
|
||||
}, [] as string[])
|
||||
|
||||
while (names.includes(newScriptPath)) {
|
||||
const unusedNames = Object.keys($app.unusedInlineScripts ?? {})
|
||||
|
||||
// Find a name that is not used by any other inline script
|
||||
while (names.includes(newScriptPath) || unusedNames.includes(newScriptPath)) {
|
||||
newScriptPath = `inline_script_${++index}`
|
||||
}
|
||||
|
||||
@@ -117,7 +126,12 @@
|
||||
<div class="flex flex-col gap-y-16">
|
||||
<div class="flex flex-col">
|
||||
{#if tab == 'inlinescripts'}
|
||||
<InlineScriptList on:pick={(e) => pickInlineScript('', e.detail)} />
|
||||
<InlineScriptList
|
||||
on:pick={(e) => pickInlineScript(e.detail)}
|
||||
inlineScripts={$app.unusedInlineScripts
|
||||
? $app.unusedInlineScripts.map((uis) => uis.name)
|
||||
: []}
|
||||
/>
|
||||
{:else if tab == 'workspacescripts'}
|
||||
<WorkspaceScriptList on:pick={(e) => pickScript(e.detail)} />
|
||||
{:else if tab == 'workspaceflows'}
|
||||
|
||||
@@ -113,6 +113,10 @@ export type InlineScript = {
|
||||
export type App = {
|
||||
grid: GridItem[]
|
||||
title: string
|
||||
unusedInlineScripts: Array<{
|
||||
name: string
|
||||
inlineScript: InlineScript
|
||||
}>
|
||||
}
|
||||
|
||||
export type ConnectingInput = {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Calendar,
|
||||
ToggleLeft
|
||||
} from 'lucide-svelte'
|
||||
import type { AppInput, AppInputs, InputType } from './inputType'
|
||||
import type { AppInput, AppInputs, InputType, ResultAppInput } from './inputType'
|
||||
import type { AppComponent } from './types'
|
||||
|
||||
export async function loadSchema(
|
||||
@@ -194,3 +194,9 @@ export function isScriptByPathDefined(appInput: AppInput | undefined): boolean {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function clearResultAppInput(appInput: ResultAppInput): ResultAppInput {
|
||||
appInput.runnable = undefined
|
||||
appInput.fields = {}
|
||||
return appInput
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
|
||||
import type { EditorBreakpoint } from '$lib/components/apps/types'
|
||||
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
@@ -16,6 +17,7 @@
|
||||
import { AppService, AppWithLastVersion } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faPen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
let app: AppWithLastVersion | undefined = undefined
|
||||
|
||||
@@ -26,6 +28,8 @@
|
||||
$: if ($workspaceStore) {
|
||||
loadApp()
|
||||
}
|
||||
|
||||
const breakpoint = writable<EditorBreakpoint>('lg')
|
||||
</script>
|
||||
|
||||
<Skeleton loading={app == undefined} layout={[10]} />
|
||||
@@ -41,7 +45,7 @@
|
||||
</div>
|
||||
|
||||
<div class="border rounded-md p-2">
|
||||
<AppPreview app={app.value} appPath={app.path} />
|
||||
<AppPreview app={app.value} appPath={app.path} {breakpoint} />
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
Reference in New Issue
Block a user