feat(frontend): Properly support resource (#1039)

* feat(frontend): Properly support resource

* feat(frontend): remove unused import

* feat(frontend): Fix build errors

* feat(frontend): Fix table actions

* feat(frontend): Fix table parameters

* feat(frontend): Fix runnable inputs sync

* feat(frontend): Done

* fix

* fix

* feat(frontend): Fix typing issues

* feat(frontend): Fix id generation

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Faton Ramadani
2022-12-23 13:42:42 +01:00
committed by GitHub
parent 079fbd55ee
commit 2d55abfc44
29 changed files with 466 additions and 178 deletions
+67
View File
@@ -8,3 +8,70 @@ declare namespace svelte.JSX {
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
}
}
declare module 'svelte-grid' {
import type { SvelteComponentTyped } from 'svelte'
export interface Size {
w: number
h: number
}
export interface Positon {
x: number
y: number
}
interface ItemLayout extends Size, Positon {
fixed?: boolean
resizable?: boolean
draggable?: boolean
customDragger?: boolean
customResizer?: boolean
min?: Size
max?: Size
}
export type Item<T> = T & { [width: number]: ItemLayout; data: any }
export type FilledItem<T> = T & { [width: number]: Required<ItemLayout>; data: any }
export interface Props<T> {
fillSpace?: boolean
items: FilledItem<T>[]
rowHeight: number
cols: [number, number][]
gap?: [number, number]
fastStart?: boolean
throttleUpdate?: number
throttleResize?: number
scroller?: undefined
sensor?: number
}
export interface Slots<T> {
default: { item: ItemLayout; dataItem: Item<T> }
}
export default class Grid<T = {}> extends SvelteComponentTyped<
Props<T>,
{
pointerup: CustomEvent<{ id: string }>
},
Slots<T>
> {}
}
declare module 'svelte-grid/build/helper/index.mjs' {
import { ItemLayout } from 'svelte-grid'
const x: {
normalize(items: any[], col: any): unknown[]
adjust(items: any[], col: any): unknown[]
findSpace(item: any, items: any, cols: any): unknown
item<T>(obj: ItemLayout): Required<ItemLayout>
}
export default x
}
@@ -431,7 +431,8 @@
...editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets),
lineNumbers: 'off',
fontSize: 16,
suggestOnTriggerCharacters: true
suggestOnTriggerCharacters: true,
lineDecorationsWidth: 0
})
const stdLib = { content: libStdContent, filePath: 'es5.d.ts' }
@@ -154,7 +154,7 @@
if (err.status === 404) {
notfound = true
}
console.error(err)
console.warn(err)
}
return isCompleted
}
@@ -5,9 +5,10 @@
export let id: string
export let componentInput: AppInput | undefined
export const staticOutputs: string[] = []
let result: any = undefined
export const staticOutputs: string[] = ['result', 'loading']
</script>
<RunnableWrapper bind:result bind:componentInput {id}>
@@ -12,10 +12,9 @@
export let id: string | undefined = undefined
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: state = $worldStore?.state
$: input && $worldStore && handleConnection()
$: input && $state && input.type == 'template' && setValue()
function handleConnection() {
@@ -29,9 +28,6 @@
}
function computeGlobalContext() {
Object.prototype.toString = function () {
return JSON.stringify(this)
}
return Object.fromEntries(
Object.entries($worldStore?.outputsById ?? {})
.filter(([k, _]) => k != id)
@@ -45,7 +41,6 @@
}
function setValue() {
console.log(computeGlobalContext())
if (input.type === 'template' && isCodeInjection(input.eval)) {
try {
value = eval_like('`' + input.eval + '`', computeGlobalContext())
@@ -0,0 +1,14 @@
<script lang="ts">
import type { AppInput } from '../../inputType'
export let input: AppInput
</script>
{#if input.type === 'connected'}
<div class="flex gap-2 flex-col">
<div>This component is expecting a connected input: </div>
<div>
<code class="bg-gray-50 border text-gray-600">{input.connection?.path} </code>
</div>
</div>
{/if}
@@ -10,12 +10,13 @@
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext } from '../../types'
import { loadSchema, schemaToInputsSpec } from '../../utils'
import { fieldTypeToTsType, loadSchema, schemaToInputsSpec } from '../../utils'
import InputValue from './InputValue.svelte'
import MissingConnectionWarning from './MissingConnectionWarning.svelte'
// Component props
export let id: string
export let inputs: AppInputs
export let fields: AppInputs
export let runnable: Runnable
export let extraQueryParams: Record<string, any> = {}
export let autoRefresh: boolean = true
@@ -25,8 +26,10 @@
const { worldStore, runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
onMount(() => {
$runnableComponents[id] = async () => {
await executeComponent()
if (autoRefresh) {
$runnableComponents[id] = async () => {
await executeComponent()
}
}
})
@@ -39,7 +42,7 @@
$: mergedArgs = { ...extraQueryParams, ...runnableInputValues, ...args }
function setStaticInputsToArgs() {
Object.entries(inputs ?? {}).forEach(([key, value]) => {
Object.entries(fields ?? {}).forEach(([key, value]) => {
if (value.type === 'static') {
args[key] = value.value
}
@@ -48,15 +51,15 @@
args = args
}
$: inputs && setStaticInputsToArgs()
$: fields && setStaticInputsToArgs()
function argMergedArgsValid(mergedArgs: Record<string, any>, testJobLoader) {
if (!inputs) {
if (!fields) {
return false
}
if (
Object.keys(inputs).length !==
Object.keys(fields).length !==
Object.keys(mergedArgs).length - Object.keys(extraQueryParams).length
) {
return false
@@ -88,27 +91,55 @@
workspace: string,
path: string,
runType: 'script' | 'flow' | 'hubscript'
) {
schema = await loadSchema(workspace, path, runType)
): Promise<Schema> {
return loadSchema(workspace, path, runType)
}
// Only loads the schema
$: if ($workspaceStore && runnable?.type === 'runnableByPath' && !schema) {
// Remote schema needs to be loaded
const { path, runType } = runnable
$: runnable && loadSchemaAndInputsByName()
loadSchemaFromTriggerable($workspaceStore, path, runType)
} else if (runnable?.type === 'runnableByName' && !schema) {
const { inlineScript } = runnable
// Inline scripts directly provide the schema
if (inlineScript) {
schema = inlineScript.schema
async function loadSchemaAndInputsByName() {
if (runnable?.type === 'runnableByName') {
const { inlineScript } = runnable
// Inline scripts directly provide the schema
if (inlineScript) {
const newSchema = inlineScript.schema
schema = newSchema
const newFields = reloadInputs()
if (JSON.stringify(newFields) !== JSON.stringify(fields)) {
fields = newFields
setTimeout(() => {
fields = newFields
}, 0)
}
}
}
}
async function loadSchemaAndInputsByPath() {
if ($workspaceStore && runnable?.type === 'runnableByPath') {
// Remote schema needs to be loaded
const { path, runType } = runnable
const newSchema = await loadSchemaFromTriggerable($workspaceStore, path, runType)
schema = newSchema
let schemaWithoutExtraQueries: Schema = JSON.parse(JSON.stringify(schema))
// Remove extra query params from the schema, which are not directly configurable by the user
Object.keys(extraQueryParams).forEach((key) => {
delete schemaWithoutExtraQueries.properties[key]
})
fields = schemaToInputsSpec(schemaWithoutExtraQueries)
}
}
$: !schema && runnable?.type === 'runnableByPath' && loadSchemaAndInputsByPath()
// When the schema is loaded, we need to update the inputs spec
// in order to render the inputs the component panel
$: if (schema && Object.keys(schema.properties).length !== Object.keys(inputs ?? {}).length) {
function reloadInputs() {
let schemaWithoutExtraQueries: Schema = JSON.parse(JSON.stringify(schema))
// Remove extra query params from the schema, which are not directly configurable by the user
@@ -116,7 +147,29 @@
delete schemaWithoutExtraQueries.properties[key]
})
inputs = schemaToInputsSpec(schemaWithoutExtraQueries)
const result = {}
const newInputs = schemaToInputsSpec(schemaWithoutExtraQueries)
if (!fields) {
return newInputs
}
Object.keys(newInputs).forEach((key) => {
const newInput = newInputs[key]
const oldInput = fields[key]
// If the input is not present in the old inputs, add it
if (oldInput === undefined) {
result[key] = newInput
} else {
if (fieldTypeToTsType(newInput.fieldType) !== fieldTypeToTsType(oldInput.fieldType)) {
result[key] = newInput
} else {
result[key] = oldInput
}
}
})
return result
}
let schemaStripped: Schema | undefined = undefined
@@ -145,11 +198,11 @@
})
}
$: schema && inputs && stripSchema(schema, inputs)
$: schema && fields && stripSchema(schema, fields)
$: disabledArgs = Object.keys(inputs ?? {}).reduce(
$: disabledArgs = Object.keys(fields ?? {}).reduce(
(disabledArgsAccumulator: string[], inputName: string) => {
if (inputs[inputName].type === 'static') {
if (fields[inputName].type === 'static') {
disabledArgsAccumulator = [...disabledArgsAccumulator, inputName]
}
return disabledArgsAccumulator
@@ -162,6 +215,10 @@
return
}
if (outputs?.loading.peak() === true) {
return
}
outputs?.loading?.set(true)
await testJobLoader?.abstractRun(() => {
@@ -182,7 +239,7 @@
}
} else if (runnable?.type === 'runnableByPath') {
const { path, runType } = runnable
requestBody['path'] = `${runType}/${path}`
requestBody['path'] = runType !== 'hubscript' ? `${runType}/${path}` : `script/${path}`
}
return AppService.executeComponent({
@@ -198,8 +255,8 @@
}
</script>
{#each Object.keys(inputs ?? {}) as key}
<InputValue {id} input={inputs[key]} bind:value={runnableInputValues[key]} />
{#each Object.keys(fields ?? {}) as key}
<InputValue {id} input={fields[key]} bind:value={runnableInputValues[key]} />
{/each}
<TestJobLoader
@@ -228,8 +285,14 @@
{#if isValid}
<slot />
{:else}
<Alert type="warning" size="xs" class="mt-2" title="Missing inputs">
<Alert type="info" size="xs" class="mt-2" title="Missing inputs">
Please fill in all the inputs
{#each Object.keys(fields ?? {}) as key}
{#if fields[key].type === 'connected'}
<MissingConnectionWarning input={fields[key]} />
{/if}
{/each}
</Alert>
{/if}
{:else}
@@ -23,7 +23,7 @@
{:else if componentInput.type === 'runnable' && isRunnableDefined()}
<RunnableComponent
bind:this={runnableComponent}
bind:inputs={componentInput.fields}
bind:fields={componentInput.fields}
bind:result
runnable={componentInput.runnable}
{autoRefresh}
@@ -161,12 +161,12 @@
{#if actionButtons.length > 0}
<td class="flex flex-row gap-2 p-4">
{#each actionButtons as props, actionIndex (actionIndex)}
{#each actionButtons as actionButton, actionIndex (actionIndex)}
<AppButton
{...props}
extraQueryParams={{ row }}
bind:componentInput={props.componentInput}
bind:staticOutputs={$staticOutputsStore[props.id]}
{...actionButton}
extraQueryParams={{ row: row.original, rowIndex }}
bind:componentInput={actionButton.componentInput}
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
/>
{/each}
</td>
@@ -28,6 +28,9 @@
import { userStore } from '$lib/stores'
import InlineScriptsPanel from './inlineScriptsPanel/InlineScriptsPanel.svelte'
import TablePanel from './TablePanel.svelte'
import { grid } from 'd3-dag'
import SettingsPanel from './SettingsPanel.svelte'
export let app: App
export let path: string
@@ -127,13 +130,8 @@
<svelte:fragment slot="content">
<TabContent value="settings">
{#if $selectedComponent !== undefined}
{#each $appStore.grid as gridItem (gridItem.id)}
{#if gridItem.data.id === $selectedComponent}
<ComponentPanel bind:component={gridItem.data} />
{/if}
{/each}
{/if}
{#if $selectedComponent === undefined}
<SettingsPanel />
{:else}
<div class="p-4 text-sm">No component selected.</div>
{/if}
</TabContent>
@@ -64,6 +64,7 @@
{...component}
bind:staticOutputs={$staticOutputs[component.id]}
bind:componentInput={component.componentInput}
bind:actionButtons={component.actionButtons}
/>
{:else if component.type === 'textcomponent'}
<TextComponent
@@ -1,6 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext, InlineScript } from '../types'
import type { AppEditorContext } from '../types'
import Grid from 'svelte-grid'
import ComponentEditor from './ComponentEditor.svelte'
import { classNames } from '$lib/utils'
@@ -26,6 +26,7 @@
$app.grid = $app.grid.filter((gridComponent) => {
if (gridComponent.data.id === component.id) {
if (
gridComponent.data.componentInput?.type === 'runnable' &&
gridComponent.data.componentInput?.runnable?.type === 'runnableByName' &&
gridComponent.data.componentInput?.runnable.inlineScript
) {
@@ -64,7 +65,7 @@
<Grid
bind:items={$app.grid}
let:dataItem
rowHeight={64}
rowHeight={32}
cols={columnConfiguration}
fastStart={true}
on:pointerup={({ detail }) => {
@@ -0,0 +1,16 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext } from '../types'
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
import TablePanel from './TablePanel.svelte'
const { selectedComponent, app } = getContext<AppEditorContext>('AppEditorContext')
</script>
{#each $app.grid as gridItem (gridItem.data.id)}
{#if gridItem.data.id === $selectedComponent}
<ComponentPanel bind:component={gridItem.data} />
{:else if gridItem.data.type === 'tablecomponent'}
<TablePanel bind:component={gridItem.data} />
{/if}
{/each}
@@ -0,0 +1,19 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext, TableComponent } from '../types'
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
export let component: TableComponent
const { selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
</script>
{#each component.actionButtons as actionButton (actionButton.id)}
{#if actionButton.id === $selectedComponent}
<ComponentPanel
bind:component={actionButton}
onDelete={() => {
component.actionButtons = component.actionButtons.filter((c) => c.id !== actionButton.id)
}}
/>
{/if}
{/each}
@@ -18,8 +18,8 @@
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-2:1': ['textinputcomponent', 'numberinputcomponent', 'selectcomponent'],
'1:2-3:2': ['buttoncomponent', 'textcomponent', 'checkboxcomponent'],
'1:2-2:2': ['textinputcomponent', 'numberinputcomponent', 'selectcomponent'],
'2:2-6:4': ['displaycomponent'],
'2:3-6:4': ['formcomponent'],
'2:4-6:4': ['barchartcomponent', 'piechartcomponent'],
@@ -50,7 +50,20 @@
function addComponent(appComponent: AppComponent) {
const grid = $app.grid ?? []
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
const id = getNextId(
grid
.map((gridItem) => {
if (gridItem.data.type === 'tablecomponent') {
return [
gridItem.data.id,
...gridItem.data.actionButtons.map((actionButton) => actionButton.id)
]
} else {
return [gridItem.data.id]
}
})
.flat()
)
appComponent.id = id
@@ -74,7 +87,7 @@
const max = getMaxDimensionsByComponent(appComponent.type, column)
newItem[column] = { ...newComponent, min, max, w: min.w, h: min.h }
const position = gridHelp.findSpace(newItem, grid, column)
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
newItem[column] = { ...newItem[column], ...position, min, max }
})
@@ -14,7 +14,7 @@
function subscribeToAllOutputs(observableOutputs: Record<string, Output<any>>) {
if (observableOutputs) {
outputs.forEach((output: string) => {
observableOutputs[output].subscribe({
observableOutputs[output]?.subscribe({
next: (value) => {
object[output] = value
}
@@ -29,6 +29,8 @@
if (component?.data.type) {
return displayData[component?.data.type].name
} else {
return 'Table action'
}
}
</script>
@@ -4,7 +4,6 @@
import { faTrash } from '@fortawesome/free-solid-svg-icons'
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 InlineScriptEditorDrawer from './InlineScriptEditorDrawer.svelte'
import { inferArgs } from '$lib/infer'
@@ -103,6 +102,7 @@
inlineScript.content,
inlineScript.schema
)
inlineScript.schema = schema
inlineScript = inlineScript
}
@@ -3,65 +3,82 @@
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 type { AppInput, ResultAppInput } from '../../inputType'
import { clearResultAppInput } from '../../utils'
import EmptyInlineScript from './EmptyInlineScript.svelte'
import InlineScriptEditor from './InlineScriptEditor.svelte'
export let componentInput: ResultAppInput
export let componentInput: AppInput | undefined
async function fork(path: string) {
const { content, language, schema } = await getScriptByPath(path)
componentInput.runnable = {
type: 'runnableByName',
name: path,
inlineScript: {
content,
language,
schema,
path
if (componentInput && componentInput.type == 'runnable') {
componentInput.runnable = {
type: 'runnableByName',
name: path,
inlineScript: {
content,
language,
schema,
path
}
}
} else {
console.error('componentInput is undefined')
}
}
// $: inlineScript && (componentInput = componentInput)
</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)
{#if componentInput && componentInput.type == 'runnable'}
{#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={() => {
if (componentInput && componentInput.type == 'runnable') {
componentInput = clearResultAppInput(componentInput)
}
}}
>
Fork
</Button>
/>
{:else}
<EmptyInlineScript
name={componentInput.runnable.name}
on:new={(e) => {
if (
componentInput &&
componentInput.type == 'runnable' &&
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 &&
componentInput.type == 'runnable' &&
componentInput.runnable?.type === 'runnableByPath'
) {
fork(componentInput.runnable.path)
}
}}
>
Fork
</Button>
</div>
<div class="border w-full">
<FlowModuleScript path={componentInput.runnable.path} />
</div>
</div>
<div class="border w-full">
<FlowModuleScript path={componentInput.runnable.path} />
</div>
</div>
{/if}
{/if}
@@ -2,7 +2,6 @@
import { Badge } from '$lib/components/common'
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { AppComponent, AppEditorContext } from '../../types'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
@@ -12,7 +11,7 @@
function selectInlineScript(id: string, subId?: string) {
selectedScriptComponentId = subId ? subId : id
$selectedComponent = id
$selectedComponent = selectedScriptComponentId
}
$: runnablesByName = $app.grid.reduce((acc, gridComponent) => {
@@ -43,10 +42,26 @@
}
}
return acc
}, [])
}, [] as { name: string; id: string; subId?: string }[])
$: runnablesByPath = $app.grid.reduce((acc, gridComponent) => {
const componentInput: AppInput = gridComponent.data.componentInput
const component: AppComponent = gridComponent.data
if (component.type === 'tablecomponent') {
component.actionButtons.forEach((actionButton) => {
if (actionButton.componentInput?.type === 'runnable') {
if (actionButton.componentInput.runnable?.type === 'runnableByPath') {
acc.push({
name: actionButton.componentInput.runnable.path,
id: gridComponent.id,
subId: actionButton.id
})
}
}
})
}
const componentInput = component.componentInput
if (componentInput?.type === 'runnable') {
if (componentInput.runnable?.type === 'runnableByPath') {
@@ -57,7 +72,7 @@
}
}
return acc
}, [])
}, [] as { name: string; id: string; subId?: string }[])
// When seleced component changes, update selectedScriptComponentId
$: {
@@ -9,7 +9,7 @@
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 { capitalize, classNames } from '$lib/utils'
import { fieldTypeToTsType } from '../../utils'
import Recompute from './Recompute.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
@@ -18,6 +18,7 @@
import RunnableInputEditor from './inputEditor/RunnableInputEditor.svelte'
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
import type { Output } from '../../rx'
import { Alert } from '$lib/components/common'
export let component: AppComponent | undefined
export let onDelete: (() => void) | undefined = undefined
@@ -47,6 +48,25 @@
$runnableComponents = $runnableComponents
}
}
if (
component &&
component.componentInput?.type === 'runnable' &&
component.componentInput?.runnable?.type === 'runnableByName'
) {
const { name, inlineScript } = component.componentInput.runnable
if (inlineScript) {
if (!$app.unusedInlineScripts) {
$app.unusedInlineScripts = []
}
$app.unusedInlineScripts.push({
name,
inlineScript
})
}
}
}
export function buildExtraLib(components: Record<string, Record<string, Output<any>>>): string {
@@ -85,8 +105,25 @@ declare const ${k} = ${JSON.stringify(v)};
</Badge>
{/if}
</svelte:fragment>
<span
class={classNames(
'text-white px-2 text-2xs py-0.5 font-bold rounded-sm w-fit',
'bg-indigo-500'
)}
>
{`Selected component: ${component.id}`}
</span>
<ComponentInputTypeEditor bind:componentInput={component.componentInput} />
{#if onDelete}
<div class="w-full">
<Alert title="Special arguments" size="xs">
The row and the rowIndex are passed as arguments to the runnable.
</Alert>
</div>
{/if}
<div class="flex flex-col w-full gap-2 my-2">
{#if component.componentInput.type === 'static'}
<StaticInputEditor bind:componentInput={component.componentInput} />
@@ -114,6 +151,7 @@ declare const ${k} = ${JSON.stringify(v)};
</svelte:fragment>
<InputsSpecsEditor
shouldCapitalize={false}
bind:inputSpecs={component.componentInput.fields}
userInputEnabled={component.type !== 'buttoncomponent'}
/>
@@ -9,6 +9,7 @@
export let inputSpecs: Record<string, StaticAppInput | ConnectedAppInput | UserAppInput>
export let userInputEnabled: boolean = true
export let staticOnly: boolean = false
export let shouldCapitalize: boolean = true
</script>
{#if inputSpecs}
@@ -18,7 +19,9 @@
{#if true}
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center gap-1">
<span class="text-xs font-semibold">{capitalize(inputSpecKey)}</span>
<span class="text-xs font-semibold">
{shouldCapitalize ? capitalize(inputSpecKey) : inputSpecKey}
</span>
<div class="flex gap-2 items-center">
<Badge color="blue">
@@ -36,14 +39,14 @@
iconOnly
/>
<ToggleButton
position={userInputEnabled ? 'center' : 'right'}
position={userInputEnabled && input.format === undefined ? 'center' : 'right'}
value="connected"
startIcon={{ icon: faArrowRight }}
size="xs"
iconOnly
disabled={staticOnly}
/>
{#if userInputEnabled}
{#if userInputEnabled && input.format === undefined}
<ToggleButton
position="right"
value="user"
@@ -59,7 +62,7 @@
<InputsSpecEditor
bind:componentInput={inputSpecs[inputSpecKey]}
canHide={userInputEnabled}
canHide={userInputEnabled && input.format === undefined}
/>
</div>
{/if}
@@ -18,7 +18,7 @@
if (event.currentTarget.checked) {
recomputeIds = [...(recomputeIds ?? []), id]
} else {
recomputeIds = recomputeIds?.filter((id) => id !== id)
recomputeIds = recomputeIds?.filter((x) => x !== id)
}
}
</script>
@@ -43,7 +43,11 @@
<Badge color="blue">{id}</Badge>
</td>
<td class="relative whitespace-nowrap px-4 py-2 ">
<input type="checkbox" on:change={(event) => onChange(event, id)} />
<input
type="checkbox"
on:change={(event) => onChange(event, id)}
checked={recomputeIds?.includes(id)}
/>
</td>
</tr>
{/each}
@@ -1,6 +1,5 @@
<script lang="ts">
import { Badge } from '$lib/components/common'
import Alert from '$lib/components/common/alert/Alert.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { getNextId } from '$lib/components/flows/flowStateUtils'
import { classNames } from '$lib/utils'
@@ -8,12 +7,11 @@
import { getContext } from 'svelte'
import type { ButtonComponent, AppEditorContext, BaseAppComponent } from '../../types'
import PanelSection from './common/PanelSection.svelte'
import ComponentPanel from './ComponentPanel.svelte'
import TableActionLabel from './TableActionLabel.svelte'
export let components: (BaseAppComponent & ButtonComponent)[]
const { app } = getContext<AppEditorContext>('AppEditorContext')
const { app, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
function addComponent() {
const grid = $app.grid ?? []
@@ -48,10 +46,11 @@
}
},
componentInput: {
type: 'static',
fieldType: 'textarea',
defaultValue: '',
value: ''
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined,
defaultValue: undefined
},
recomputeIds: undefined,
card: false
@@ -59,8 +58,6 @@
components = [...components, newComponent]
}
let openedComponentId: string | undefined = components[0]?.id
</script>
<PanelSection title={`Table actions ${components.length > 0 ? `(${components.length})` : ''}`}>
@@ -74,26 +71,16 @@
iconOnly
/>
</svelte:fragment>
{#if components.length > 0}
<div class="w-full">
<Alert title="Special argument" size="xs">
The row is passed as an argument to the runnable.
</Alert>
</div>
{/if}
{#each components as component}
<div
class={classNames(
'w-full text-xs font-bold py-1.5 px-2 cursor-pointer transition-all justify-between flex items-center border border-gray-3 rounded-md',
'bg-white border-gray-300 hover:bg-gray-100 focus:bg-gray-100 text-gray-700',
openedComponentId === component.id ? 'outline outline-gray-500 outline-offset-1' : ''
$selectedComponent === component.id ? 'outline outline-blue-500 bg-red-400' : ''
)}
on:click={() => {
if (openedComponentId === component.id) {
openedComponentId = undefined
} else {
openedComponentId = component.id
}
$selectedComponent = component.id
}}
on:keypress
>
@@ -104,16 +91,5 @@
Component: {component.id}
</Badge>
</div>
{#if openedComponentId === component.id}
<div class="w-full border">
<ComponentPanel
bind:component
onDelete={() => {
components = components.filter((c) => c.id !== component.id)
}}
/>
</div>
{/if}
{/each}
</PanelSection>
@@ -4,6 +4,7 @@
import type { StaticAppInput } from '../../../inputType'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import ArrayStaticInputEditor from '../ArrayStaticInputEditor.svelte'
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
export let componentInput: StaticAppInput | undefined
export let canHide: boolean = false
@@ -29,18 +30,34 @@
{/each}
</select>
{:else if componentInput.fieldType === 'object'}
<div class="border rounded-sm w-full">
<SimpleEditor
lang="json"
code={JSON.stringify(componentInput.value, null, 2)}
class="few-lines-editor"
{#if componentInput.format}
<ResourcePicker
initialValue={componentInput.value?.split('$res:')[1] || ''}
on:change={(e) => {
if (componentInput?.type === 'static' && componentInput.value) {
componentInput.value = JSON.parse(e.detail.code)
let path = e.detail
if (componentInput && path) {
componentInput.value = `$res:${path}`
}
}}
resourceType={componentInput.format.split('-').length > 1
? componentInput.format.substring('resource-'.length)
: undefined}
/>
</div>
{:else}
<div class="border rounded-sm w-full">
<SimpleEditor
lang="json"
code={JSON.stringify(componentInput.value, null, 2)}
class="few-lines-editor"
on:change={(e) => {
if (componentInput?.type === 'static' && componentInput.value) {
componentInput.value = JSON.parse(e.detail.code)
}
}}
/>
</div>
{/if}
{:else if componentInput.fieldType === 'array'}
<ArrayStaticInputEditor bind:componentInput {canHide} />
{:else}
@@ -40,6 +40,16 @@
}
}
function pickHubScript(path: string) {
if (appInput.type === 'runnable') {
appInput.runnable = {
type: 'runnableByPath',
path,
runType: 'hubscript'
}
}
}
function pickInlineScript(name: string) {
const unusedInlineScriptIndex = $app.unusedInlineScripts?.findIndex(
(script) => script.name === name
@@ -137,7 +147,7 @@
{:else if tab == 'workspaceflows'}
<WorkspaceFlowList on:pick={(e) => pickFlow(e.detail)} />
{:else if tab == 'hubscripts'}
<PickHubScript bind:filter on:pick={(e) => pickScript(e.detail.path)} />
<PickHubScript bind:filter on:pick={(e) => pickHubScript(e.detail.path)} />
{/if}
</div>
</div>
@@ -79,6 +79,7 @@ type InputConfiguration<T extends InputType, U, V extends InputType> = {
fieldType: T
defaultValue: U
subFieldType?: V
format?: string | undefined
}
export type AppInput =
@@ -91,12 +92,13 @@ export type AppInput =
| AppInputSpec<'datetime', string>
| AppInputSpec<'any', any>
| AppInputSpec<'object', Record<string | number, any>>
| AppInputSpec<'object', string>
| (AppInputSpec<'select', string> & {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
| AppInputSpec<'array', string[], 'text'>
| AppInputSpec<'array', string[], 'textarea'>
| AppInputSpec<'array', number[], 'number'>
@@ -106,8 +108,8 @@ export type AppInput =
| AppInputSpec<'array', string[], 'datetime'>
| AppInputSpec<'array', object[], 'object'>
| (AppInputSpec<'array', string[], 'select'> & {
optionValuesKey: keyof typeof staticValues
})
optionValuesKey: keyof typeof staticValues
})
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
export type ConnectedAppInput = Extract<AppInput, { type: 'connected' }>
+20 -10
View File
@@ -17,14 +17,16 @@ export interface Input<T> extends Subscriber<T> {
peak(): T | any | undefined
}
export type World = {
outputsById: Record<string, Record<string, Output<any>>>
connect: <T>(inputSpec: AppInput, next: (x: T) => void, previousValue: T) => Input<T>
state: Writable<number>
}
export function buildWorld(components: Record<string, string[]>, previousWorld: World | undefined): World {
export function buildWorld(
components: Record<string, string[]>,
previousWorld: World | undefined
): World {
const newWorld = buildObservableWorld()
const outputsById: Record<string, Record<string, Output<any>>> = {}
const state = writable(0)
@@ -32,7 +34,12 @@ export function buildWorld(components: Record<string, string[]>, previousWorld:
outputsById[k] = {}
for (const o of outputs) {
outputsById[k][o] = newWorld.newOutput(k, o, state, previousWorld?.outputsById[k]?.[o].peak())
outputsById[k][o] = newWorld.newOutput(
k,
o,
state,
previousWorld?.outputsById[k]?.[o]?.peak()
)
}
}
state.update((x) => x + 1)
@@ -47,7 +54,7 @@ export function buildObservableWorld() {
if (inputSpec.type === 'static') {
return {
peak: () => inputSpec.value,
next: () => { }
next: () => {}
}
} else if (inputSpec.type === 'connected') {
const input = cachedInput(next)
@@ -57,7 +64,7 @@ export function buildObservableWorld() {
if (!connection) {
return {
peak: () => undefined,
next: () => { }
next: () => {}
}
}
@@ -71,7 +78,7 @@ export function buildObservableWorld() {
console.warn('Observable at ' + componentId + '.' + p + ' not found')
return {
peak: () => undefined,
next: () => { }
next: () => {}
}
}
@@ -80,15 +87,19 @@ export function buildObservableWorld() {
} else if (inputSpec.type === 'user') {
return {
peak: () => inputSpec.value,
next: () => { }
next: () => {}
}
} else {
throw Error('Unknown input type ' + inputSpec)
}
}
function newOutput<T>(id: string, name: string, state: Writable<number>, previousValue: T): Output<T> {
function newOutput<T>(
id: string,
name: string,
state: Writable<number>,
previousValue: T
): Output<T> {
const output = settableOutput<T>(state, previousValue)
observables[`${id}.${name}`] = output
return output
@@ -123,7 +134,6 @@ export function settableOutput<T>(state: Writable<number>, previousValue: T): Ou
function subscribe(x: Subscriber<T>) {
if (!subscribers.includes(x)) {
subscribers.push(x)
// Send the current value to the new subscriber if it already exists
+7 -2
View File
@@ -1,5 +1,6 @@
import type { Schema } from '$lib/common'
import { FlowService, ScriptService } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import {
BarChart4,
Binary,
@@ -44,6 +45,8 @@ export async function loadSchema(
path
})
await inferArgs(script.language, script.content, script.schema)
return script.schema
}
}
@@ -56,9 +59,11 @@ export function schemaToInputsSpec(schema: Schema): AppInputs {
type: 'static',
defaultValue: property.default,
value: undefined,
visible: true,
fieldType: property.type
visible: property.format ? false : true,
fieldType: property.type,
format: property.format
}
return accu
}, {})
}