diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts index 3cf0168db4..05d64563a0 100644 --- a/frontend/src/global.d.ts +++ b/frontend/src/global.d.ts @@ -8,3 +8,70 @@ declare namespace svelte.JSX { onfinalize?: (event: CustomEvent> & { 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 & { [width: number]: ItemLayout; data: any } + export type FilledItem = T & { [width: number]: Required; data: any } + + export interface Props { + fillSpace?: boolean + items: FilledItem[] + rowHeight: number + cols: [number, number][] + gap?: [number, number] + fastStart?: boolean + throttleUpdate?: number + throttleResize?: number + + scroller?: undefined + sensor?: number + } + + export interface Slots { + default: { item: ItemLayout; dataItem: Item } + } + + export default class Grid extends SvelteComponentTyped< + Props, + { + pointerup: CustomEvent<{ id: string }> + }, + Slots + > {} +} + +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(obj: ItemLayout): Required + } + + export default x +} diff --git a/frontend/src/lib/components/TemplateEditor.svelte b/frontend/src/lib/components/TemplateEditor.svelte index 024a28d211..396b8e15e2 100644 --- a/frontend/src/lib/components/TemplateEditor.svelte +++ b/frontend/src/lib/components/TemplateEditor.svelte @@ -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' } diff --git a/frontend/src/lib/components/TestJobLoader.svelte b/frontend/src/lib/components/TestJobLoader.svelte index 75eaa30f0e..2998eab31e 100644 --- a/frontend/src/lib/components/TestJobLoader.svelte +++ b/frontend/src/lib/components/TestJobLoader.svelte @@ -154,7 +154,7 @@ if (err.status === 404) { notfound = true } - console.error(err) + console.warn(err) } return isCompleted } diff --git a/frontend/src/lib/components/apps/components/DisplayComponent.svelte b/frontend/src/lib/components/apps/components/DisplayComponent.svelte index 15f3a5b585..85fc34120f 100644 --- a/frontend/src/lib/components/apps/components/DisplayComponent.svelte +++ b/frontend/src/lib/components/apps/components/DisplayComponent.svelte @@ -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'] diff --git a/frontend/src/lib/components/apps/components/helpers/InputValue.svelte b/frontend/src/lib/components/apps/components/helpers/InputValue.svelte index b68f8387c5..4ecf7f655d 100644 --- a/frontend/src/lib/components/apps/components/helpers/InputValue.svelte +++ b/frontend/src/lib/components/apps/components/helpers/InputValue.svelte @@ -12,10 +12,9 @@ export let id: string | undefined = undefined const { worldStore } = getContext('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()) diff --git a/frontend/src/lib/components/apps/components/helpers/MissingConnectionWarning.svelte b/frontend/src/lib/components/apps/components/helpers/MissingConnectionWarning.svelte new file mode 100644 index 0000000000..7ade3eb6df --- /dev/null +++ b/frontend/src/lib/components/apps/components/helpers/MissingConnectionWarning.svelte @@ -0,0 +1,14 @@ + + +{#if input.type === 'connected'} +
+
This component is expecting a connected input:
+
+ {input.connection?.path} +
+
+{/if} diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte index cb29b83223..9a761ab8c7 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte @@ -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 = {} export let autoRefresh: boolean = true @@ -25,8 +26,10 @@ const { worldStore, runnableComponents } = getContext('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, 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 { + 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 @@ } -{#each Object.keys(inputs ?? {}) as key} - +{#each Object.keys(fields ?? {}) as key} + {/each} {:else} - + Please fill in all the inputs + + {#each Object.keys(fields ?? {}) as key} + {#if fields[key].type === 'connected'} + + {/if} + {/each} {/if} {:else} diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte index 684e0e8ff2..3440ebb78d 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte @@ -23,7 +23,7 @@ {:else if componentInput.type === 'runnable' && isRunnableDefined()} 0} - {#each actionButtons as props, actionIndex (actionIndex)} + {#each actionButtons as actionButton, actionIndex (actionIndex)} {/each} diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index bc3c3e1591..28a37a7978 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -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 @@ {#if $selectedComponent !== undefined} - {#each $appStore.grid as gridItem (gridItem.id)} - {#if gridItem.data.id === $selectedComponent} - - {/if} - {/each} - {/if} - {#if $selectedComponent === undefined} + + {:else}
No component selected.
{/if}
diff --git a/frontend/src/lib/components/apps/editor/ComponentEditor.svelte b/frontend/src/lib/components/apps/editor/ComponentEditor.svelte index 219e5f4236..f339a45e58 100644 --- a/frontend/src/lib/components/apps/editor/ComponentEditor.svelte +++ b/frontend/src/lib/components/apps/editor/ComponentEditor.svelte @@ -64,6 +64,7 @@ {...component} bind:staticOutputs={$staticOutputs[component.id]} bind:componentInput={component.componentInput} + bind:actionButtons={component.actionButtons} /> {:else if component.type === 'textcomponent'} 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 @@ { diff --git a/frontend/src/lib/components/apps/editor/SettingsPanel.svelte b/frontend/src/lib/components/apps/editor/SettingsPanel.svelte new file mode 100644 index 0000000000..e5cc2e1200 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/SettingsPanel.svelte @@ -0,0 +1,16 @@ + + +{#each $app.grid as gridItem (gridItem.data.id)} + {#if gridItem.data.id === $selectedComponent} + + {:else if gridItem.data.type === 'tablecomponent'} + + {/if} +{/each} diff --git a/frontend/src/lib/components/apps/editor/TablePanel.svelte b/frontend/src/lib/components/apps/editor/TablePanel.svelte new file mode 100644 index 0000000000..fefeca768c --- /dev/null +++ b/frontend/src/lib/components/apps/editor/TablePanel.svelte @@ -0,0 +1,19 @@ + + +{#each component.actionButtons as actionButton (actionButton.id)} + {#if actionButton.id === $selectedComponent} + { + component.actionButtons = component.actionButtons.filter((c) => c.id !== actionButton.id) + }} + /> + {/if} +{/each} diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/ComponentList.svelte b/frontend/src/lib/components/apps/editor/componentsPanel/ComponentList.svelte index 66b3411728..cd0ba26088 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/ComponentList.svelte +++ b/frontend/src/lib/components/apps/editor/componentsPanel/ComponentList.svelte @@ -18,8 +18,8 @@ function getMinDimensionsByComponent(componentType: AppComponent['type'], column: number): Size { // Dimensions key formula: :-: 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 } }) diff --git a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte index a84a243831..bfffc7d3a6 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte @@ -14,7 +14,7 @@ function subscribeToAllOutputs(observableOutputs: Record>) { if (observableOutputs) { outputs.forEach((output: string) => { - observableOutputs[output].subscribe({ + observableOutputs[output]?.subscribe({ next: (value) => { object[output] = value } diff --git a/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte b/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte index bb8e577815..fbfc506919 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte @@ -29,6 +29,8 @@ if (component?.data.type) { return displayData[component?.data.type].name + } else { + return 'Table action' } } diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte index 794370b196..bc49665256 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte @@ -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 } diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorPanel.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorPanel.svelte index a25a0439ef..23ff8a1830 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorPanel.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorPanel.svelte @@ -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) -{#if componentInput?.runnable?.type === 'runnableByName' && componentInput?.runnable?.name !== undefined} - {#if componentInput.runnable.inlineScript} - { - componentInput = clearResultAppInput(componentInput) - }} - /> - {:else} - { - if (componentInput?.runnable?.type === 'runnableByName') { - componentInput.runnable.inlineScript = e.detail - } - }} - /> - {/if} -{:else if componentInput?.runnable?.type === 'runnableByPath' && componentInput?.runnable?.path} -
-
- + /> + {:else} + { + if ( + componentInput && + componentInput.type == 'runnable' && + componentInput?.runnable?.type === 'runnableByName' + ) { + componentInput.runnable.inlineScript = e.detail + } + }} + /> + {/if} + {:else if componentInput?.runnable?.type === 'runnableByPath' && componentInput?.runnable?.path} +
+
+ +
+
+ +
-
- -
-
+ {/if} {/if} diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte index f3f90a8245..2c4d3faab0 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte @@ -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 $: { diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte index c4c4ce2477..3f4d5d9321 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte @@ -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 { @@ -85,8 +105,25 @@ declare const ${k} = ${JSON.stringify(v)}; {/if} + + {`Selected component: ${component.id}`} + + + {#if onDelete} +
+ + The row and the rowIndex are passed as arguments to the runnable. + +
+ {/if} +
{#if component.componentInput.type === 'static'} @@ -114,6 +151,7 @@ declare const ${k} = ${JSON.stringify(v)}; diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte index c277fc5ad9..5873678567 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte @@ -9,6 +9,7 @@ export let inputSpecs: Record export let userInputEnabled: boolean = true export let staticOnly: boolean = false + export let shouldCapitalize: boolean = true {#if inputSpecs} @@ -18,7 +19,9 @@ {#if true}
- {capitalize(inputSpecKey)} + + {shouldCapitalize ? capitalize(inputSpecKey) : inputSpecKey} +
@@ -36,14 +39,14 @@ iconOnly /> - {#if userInputEnabled} + {#if userInputEnabled && input.format === undefined}
{/if} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/Recompute.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/Recompute.svelte index 6997f43f39..13ff84d36a 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/Recompute.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/Recompute.svelte @@ -18,7 +18,7 @@ if (event.currentTarget.checked) { recomputeIds = [...(recomputeIds ?? []), id] } else { - recomputeIds = recomputeIds?.filter((id) => id !== id) + recomputeIds = recomputeIds?.filter((x) => x !== id) } } @@ -43,7 +43,11 @@ {id} - onChange(event, id)} /> + onChange(event, id)} + checked={recomputeIds?.includes(id)} + /> {/each} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/TableActions.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/TableActions.svelte index e94bf2cbd0..d4e93f05e6 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/TableActions.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/TableActions.svelte @@ -1,6 +1,5 @@ 0 ? `(${components.length})` : ''}`}> @@ -74,26 +71,16 @@ iconOnly /> - {#if components.length > 0} -
- - The row is passed as an argument to the runnable. - -
- {/if} + {#each components as component}
{ - if (openedComponentId === component.id) { - openedComponentId = undefined - } else { - openedComponentId = component.id - } + $selectedComponent = component.id }} on:keypress > @@ -104,16 +91,5 @@ Component: {component.id}
- - {#if openedComponentId === component.id} -
- { - components = components.filter((c) => c.id !== component.id) - }} - /> -
- {/if} {/each}
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte index b97faf1aac..fb34fd7701 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte @@ -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} {:else if componentInput.fieldType === 'object'} -
- { - 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} /> -
+ {:else} +
+ { + if (componentInput?.type === 'static' && componentInput.value) { + componentInput.value = JSON.parse(e.detail.code) + } + }} + /> +
+ {/if} {:else if componentInput.fieldType === 'array'} {:else} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte index b70ae283a6..a30757fd3b 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte @@ -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'} pickFlow(e.detail)} /> {:else if tab == 'hubscripts'} - pickScript(e.detail.path)} /> + pickHubScript(e.detail.path)} /> {/if}
diff --git a/frontend/src/lib/components/apps/inputType.ts b/frontend/src/lib/components/apps/inputType.ts index c674a9dba1..655cf30400 100644 --- a/frontend/src/lib/components/apps/inputType.ts +++ b/frontend/src/lib/components/apps/inputType.ts @@ -79,6 +79,7 @@ type InputConfiguration = { 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> + | 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 export type ConnectedAppInput = Extract diff --git a/frontend/src/lib/components/apps/rx.ts b/frontend/src/lib/components/apps/rx.ts index de33a656bb..135bd4533d 100644 --- a/frontend/src/lib/components/apps/rx.ts +++ b/frontend/src/lib/components/apps/rx.ts @@ -17,14 +17,16 @@ export interface Input extends Subscriber { peak(): T | any | undefined } - export type World = { outputsById: Record>> connect: (inputSpec: AppInput, next: (x: T) => void, previousValue: T) => Input state: Writable } -export function buildWorld(components: Record, previousWorld: World | undefined): World { +export function buildWorld( + components: Record, + previousWorld: World | undefined +): World { const newWorld = buildObservableWorld() const outputsById: Record>> = {} const state = writable(0) @@ -32,7 +34,12 @@ export function buildWorld(components: Record, 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(id: string, name: string, state: Writable, previousValue: T): Output { + function newOutput( + id: string, + name: string, + state: Writable, + previousValue: T + ): Output { const output = settableOutput(state, previousValue) observables[`${id}.${name}`] = output return output @@ -123,7 +134,6 @@ export function settableOutput(state: Writable, previousValue: T): Ou function subscribe(x: Subscriber) { if (!subscribers.includes(x)) { - subscribers.push(x) // Send the current value to the new subscriber if it already exists diff --git a/frontend/src/lib/components/apps/utils.ts b/frontend/src/lib/components/apps/utils.ts index ce603baf97..58e7bc8faa 100644 --- a/frontend/src/lib/components/apps/utils.ts +++ b/frontend/src/lib/components/apps/utils.ts @@ -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 }, {}) }