App editor v2 (#1001)

* fix(frontend): Fix app InputValue sync

* feat(frontend): WIP

* feat(frontend): WIP

* feat(frontend): Type aligned

* feat(frontend): working

* feat(frontend): working

* feat(frontend): Fix text binding

* feat(frontend): Connect + script working

* feat(frontend): Fix reactity issues
This commit is contained in:
Faton Ramadani
2022-12-08 13:26:01 +01:00
committed by GitHub
parent 2aa46e048d
commit aa8b8b35fd
38 changed files with 1097 additions and 749 deletions
@@ -26,6 +26,7 @@
export async function abstractRun(fn: () => Promise<string>) {
try {
intervalId && clearIntervalAsync(intervalId)
if (isLoading && job) {
JobService.cancelQueuedJob({
workspace: workspace!,
@@ -36,6 +37,7 @@
isLoading = true
const testId = await fn()
await watchJob(testId)
} catch (err) {
isLoading = false
@@ -1,29 +1,20 @@
<script lang="ts">
import DisplayResult from '$lib/components/DisplayResult.svelte'
import { getContext } from 'svelte'
import type { AppEditorContext, InputsSpec } from '../types'
import InputValue from './helpers/InputValue.svelte'
export let componentInputs: InputsSpec
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
let resultValue: any = undefined
import type { AppInput } from '../inputType'
import RunnableWrapper from './helpers/RunnableWrapper.svelte'
export let id: string
export let componentInput: AppInput | undefined
export const staticOutputs: string[] = []
let result: any = undefined
</script>
<InputValue input={componentInputs.result} bind:value={resultValue} />
{#if $worldStore}
<RunnableWrapper bind:result bind:componentInput {id}>
<div class="w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm">
Results
</div>
<div class="p-2">
{#if resultValue === undefined && componentInputs.result.type === 'output'}
<span class="text-sm">Waiting for result</span>
{:else}
<DisplayResult result={resultValue} />
{/if}
<DisplayResult {result} />
</div>
{/if}
</RunnableWrapper>
@@ -1,16 +1,15 @@
<script lang="ts">
import { Button, type ButtonType } from '$lib/components/common'
import type { InputsSpec } from '../../types'
import type { AppInput } from '../../inputType'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import RunnableComponent from '../helpers/RunnableComponent.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
export let id: string
export let inputs: InputsSpec
export let path: string | undefined = undefined
export let runType: 'script' | 'flow' | undefined = undefined
export let inlineScriptName: string | undefined = undefined
export let componentInputs: InputsSpec
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let extraQueryParams: Record<string, any> = {}
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
@@ -24,19 +23,16 @@
let runnableComponent: RunnableComponent
</script>
<InputValue input={componentInputs.label} bind:value={labelValue} />
<InputValue input={componentInputs.color} bind:value={color} />
<InputValue input={componentInputs.size} bind:value={size} />
<InputValue input={configuration.label} bind:value={labelValue} />
<InputValue input={configuration.color} bind:value={color} />
<InputValue input={configuration.size} bind:value={size} />
<RunnableComponent
bind:this={runnableComponent}
bind:inputs
{path}
{runType}
{inlineScriptName}
<RunnableWrapper
bind:runnableComponent
bind:componentInput
{id}
autoRefresh={false}
{extraQueryParams}
autoRefresh={false}
>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<Button
@@ -47,6 +43,6 @@
{color}
>
{labelValue}
</Button></AlignWrapper
>
</RunnableComponent>
</Button>
</AlignWrapper>
</RunnableWrapper>
@@ -14,15 +14,13 @@
} from 'chart.js'
import type { ChartData } from 'chart.js'
import type { InputsSpec } from '../../types'
import RunnableComponent from '../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import InputValue from '../helpers/InputValue.svelte'
export let id: string
export let inputs: InputsSpec
export let path: string | undefined = undefined
export let runType: 'script' | 'flow' | undefined = undefined
export let inlineScriptName: string | undefined = undefined
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['loading', 'result']
@@ -38,10 +36,38 @@
)
let result: ChartData<'bar', number[], unknown> | undefined = undefined
let labels: string[] = []
let theme: string = 'theme1'
$: backgroundColor = {
theme1: ['#FF6384', '#4BC0C0', '#FFCE56', '#E7E9ED', '#36A2EB'],
// blue theme
theme2: ['#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b'],
// red theme
theme3: ['#e74a3b', '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e']
}[theme]
const options = {
responsive: true,
animation: false
}
$: data = {
labels,
datasets: [
{
data: result,
backgroundColor
}
]
}
</script>
<RunnableComponent {id} {path} {runType} {inlineScriptName} bind:inputs bind:result>
{#if result}
<Bar data={result} options={{ responsive: true, animation: false }} />
<InputValue input={configuration.theme} bind:value={theme} />
<InputValue input={configuration.labels} bind:value={labels} />
<RunnableWrapper bind:componentInput {id} bind:result>
{#if data}
<Bar {data} {options} />
{/if}
</RunnableComponent>
</RunnableWrapper>
@@ -13,14 +13,13 @@
ArcElement
} from 'chart.js'
import type { ChartData } from 'chart.js'
import type { InputsSpec } from '../../types'
import RunnableComponent from '../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import InputValue from '../helpers/InputValue.svelte'
export let id: string
export let inputs: InputsSpec
export let path: string | undefined = undefined
export let runType: 'script' | 'flow' | undefined = undefined
export let inlineScriptName: string | undefined = undefined
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['loading', 'result']
@@ -35,43 +34,39 @@
ArcElement
)
let options = {
let result: ChartData<'bar', number[], unknown> | undefined = undefined
let labels: string[] = []
let theme: string = 'theme1'
$: backgroundColor = {
theme1: ['#FF6384', '#4BC0C0', '#FFCE56', '#E7E9ED', '#36A2EB'],
// blue theme
theme2: ['#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b'],
// red theme
theme3: ['#e74a3b', '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e']
}[theme]
const options = {
responsive: true,
animation: false
}
let nextColor = 0
// TODO: Replace with nicer windmill branded color pallet.
const colors = ['#3b82f6', '#ff6384', '#4bc0c0', '#ff9f40', '#9966ff', '#ffcd56', '#c9cbcf']
function generateColor() {
const col = colors[nextColor]
nextColor = (nextColor + 1) % colors.length
return col
}
let result: { name: string; value: number; color: string | undefined }[] | undefined = undefined
let data: ChartData<'pie', number[], string> | undefined = undefined
$: if (Array.isArray(result)) {
nextColor = 0
data = {
datasets: [
{
data: result.map((x) => x.value),
backgroundColor: result.map((x) => x.color ?? generateColor())
}
],
labels: result.map((x) => x.name)
}
} else {
data = undefined
$: data = {
labels,
datasets: [
{
data: result,
backgroundColor: backgroundColor
}
]
}
</script>
<RunnableComponent {id} {path} {runType} {inlineScriptName} bind:inputs bind:result>
{#if result}
<InputValue input={configuration.theme} bind:value={theme} />
<InputValue input={configuration.labels} bind:value={labels} />
<RunnableWrapper bind:componentInput {id} bind:result>
{#if data}
<Pie {data} {options} />
{/if}
</RunnableComponent>
</RunnableWrapper>
@@ -3,18 +3,16 @@
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import type { Output } from '../../rx'
import type { AppEditorContext, BaseAppComponent, ButtonComponent, InputsSpec } from '../../types'
import type { AppEditorContext, BaseAppComponent, ButtonComponent } from '../../types'
import InputValue from '../helpers/InputValue.svelte'
import DebouncedInput from '../helpers/DebouncedInput.svelte'
import RunnableComponent from '../helpers/RunnableComponent.svelte'
import AppButton from '../buttons/AppButton.svelte'
import type { AppInput } from '../../inputType'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
export let id: string
export let inputs: InputsSpec
export let path: string | undefined = undefined
export let runType: 'script' | 'flow' | undefined = undefined
export let inlineScriptName: string | undefined = undefined
export let componentInputs: InputsSpec
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let actionButtons: (BaseAppComponent & ButtonComponent)[]
const { worldStore, staticOutputs: staticOutputsStore } =
@@ -38,7 +36,7 @@
}
}
let searchEnabledValue: boolean | undefined = undefined
let searchConfiguration: 'Frontend' | 'Backend' | 'Disabled' = 'Disabled'
let paginationEnabled: boolean | undefined = undefined
let page = 1
@@ -50,22 +48,17 @@
const extraQueryParams = { search, page }
export const reservedKeys: string[] = Object.keys(extraQueryParams)
$: (searchConfiguration === 'Frontend' || searchConfiguration === 'Backend') &&
(extraQueryParams.search = search)
</script>
<InputValue input={componentInputs.searchEnabled} bind:value={searchEnabledValue} />
<InputValue input={componentInputs.paginationEnabled} bind:value={paginationEnabled} />
<InputValue input={configuration.searchConfiguration} bind:value={searchConfiguration} />
<InputValue input={configuration.paginationEnabled} bind:value={paginationEnabled} />
<RunnableComponent
{id}
{path}
{runType}
{inlineScriptName}
bind:inputs
bind:result
extraQueryParams={{ search, page }}
>
<RunnableWrapper bind:componentInput {id} bind:result extraQueryParams={{ search, page }}>
<div class="gap-2 flex flex-col mt-2">
{#if searchEnabledValue}
{#if searchConfiguration !== 'Disabled'}
<div>
<div>
<DebouncedInput placeholder="Search..." bind:value={search} />
@@ -111,7 +104,7 @@
<AppButton
{...props}
extraQueryParams={{ row }}
bind:inputs={props.inputs}
bind:componentInput={props.componentInput}
bind:staticOutputs={$staticOutputsStore[props.id]}
/>
{/each}
@@ -148,4 +141,4 @@
</div>
{/if}
</div>
</RunnableComponent>
</RunnableWrapper>
@@ -1,20 +1,27 @@
<script lang="ts">
import SvelteMarkdown from 'svelte-markdown'
import type { InputsSpec } from '../../types'
import type { AppInput } from '../../inputType'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
export let componentInputs: InputsSpec
export let id: string
export let componentInput: AppInput | undefined
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = []
export const staticOutputs: string[] = ['result', 'loading']
let contentValue: string = ''
let result: string = ''
</script>
<InputValue input={componentInputs.content} bind:value={contentValue} />
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<SvelteMarkdown source={String(contentValue)} />
</AlignWrapper>
<RunnableWrapper bind:componentInput {id} bind:result>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if result === ''}
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
No text
</div>
{:else}
<SvelteMarkdown source={String(result)} />
{/if}
</AlignWrapper>
</RunnableWrapper>
@@ -0,0 +1,49 @@
- Input () => any
- text field
- Checkbox
- select
- Display: (data: Static | Connect | Result, configuration: List<Static>) => Outputs
- Table :
actions: List<Button>,
configuration: {
Search: Frontend | Backend | Disabled,
Pagination: Frontend | Backend | Disabled
}
Outputs: {
selectedRow,
page,
data
}
- Charts
Outputs: {
data,
selected: {
id,
value
}
}
- Text
Outputs: {
data,
}
- Image
Outputs: {
data,
}
IFrame: URL => void
outputs: Record<id, any>
- Run form (action: Result, configuration: List<Static>) => any
- Button (action: Result | ForceRefresh, configuration: List<Static>) => any
Result (fields: List<Static | Connect | User >) => Data
ForceRefresh (List<ID of Display>) => void
Global Refresh
@@ -1,11 +1,12 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext, AppInputTransform } from '../../types'
import type { AppInput } from '../../inputType'
import type { AppEditorContext } from '../../types'
import { accessPropertyByPath } from '../../utils'
type T = string | number | boolean | Record<string | number, any> | undefined
export let input: AppInputTransform
export let input: AppInput
export let value: T
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
@@ -13,7 +14,7 @@
$: input && $worldStore && handleConnection()
function handleConnection() {
if (input.type === 'output') {
if (input.type === 'connected') {
$worldStore?.connect<any>(input, onValueChange)
} else if (input.type === 'static') {
setValue()
@@ -29,11 +30,24 @@
}
function onValueChange(newValue: any): void {
if (input.type === 'output') {
if (input.name?.includes('.')) {
const path = input.name.split('.').slice(1).join('.')
if (input.type === 'connected' && newValue !== undefined && newValue !== null) {
const { connection } = input
value = accessPropertyByPath<T>(newValue, path)
if (!connection) {
// No connection
return
}
const { componentId, path } = connection
const hasSubPath = ['.', '['].some((x) => path.includes(x))
if (hasSubPath) {
// Must remove top level property from path
// Which was manually added, i.e. result
const realPath = path.split('.').slice(1).join('.')
value = accessPropertyByPath<T>(newValue, realPath)
} else {
value = newValue
}
@@ -0,0 +1,31 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext } from '../../types'
import InputValue from './InputValue.svelte'
export let result: any = undefined
export let componentInput: AppInput
export let id: string
// Sync the result to the output
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: outputs = $worldStore?.outputsById[id] as {
result: Output<any>
}
function setOutput() {
if (outputs) {
outputs.result?.set(result)
}
}
$: result !== undefined && setOutput()
</script>
{#if componentInput.type !== 'runnable'}
<InputValue input={componentInput} bind:value={result} />
{/if}
<slot />
@@ -9,17 +9,16 @@
import { workspaceStore } from '$lib/stores'
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext, InputsSpec } from '../../types'
import type { AppEditorContext } from '../../types'
import { loadSchema, schemaToInputsSpec } from '../../utils'
import InputValue from './InputValue.svelte'
// Component props
export let id: string
export let inputs: InputsSpec
export let path: string | undefined = undefined
export let runType: 'script' | 'flow' | undefined = undefined
export let inlineScriptName: string | undefined = undefined
export let inputs: AppInputs
export let runnable: Runnable
export let extraQueryParams: Record<string, any> = {}
export let autoRefresh: boolean = true
export let result: any = undefined
@@ -34,9 +33,8 @@
$: mergedArgs = { ...args, ...extraQueryParams, ...runnableInputValues }
// TODO: Review
function setStaticInputsToArgs() {
Object.entries(inputs).forEach(([key, value]) => {
Object.entries(inputs ?? {}).forEach(([key, value]) => {
if (value.type === 'static') {
args[key] = value.value
}
@@ -47,9 +45,9 @@
$: inputs && setStaticInputsToArgs()
function argMergedArgsValid(mergedArgs: Record<string, any>) {
function argMergedArgsValid(mergedArgs: Record<string, any>, testJobLoader) {
if (
Object.keys(inputs).filter((k) => inputs[k].type !== 'user').length !==
Object.keys(inputs ?? {}).filter((k) => inputs[k].type !== 'user').length !==
Object.keys(runnableInputValues).length
) {
return false
@@ -59,14 +57,14 @@
(arg) => arg !== undefined && arg !== null
)
if (areAllArgsValid && autoRefresh) {
if (areAllArgsValid && autoRefresh && testJobLoader) {
executeComponent()
}
return areAllArgsValid
}
$: isValid = argMergedArgsValid(mergedArgs)
$: isValid = argMergedArgsValid(mergedArgs, testJobLoader)
// Test job internal state
let testJob: CompletedJob | undefined = undefined
@@ -86,17 +84,19 @@
}
// Only loads the schema
$: if ($workspaceStore && path && runType && !schema) {
$: if ($workspaceStore && runnable?.type === 'runnableByPath' && !schema) {
// Remote schema needs to be loaded
const { path, runType } = runnable
loadSchemaFromTriggerable($workspaceStore, path, runType)
} else if (inlineScriptName && $app.inlineScripts[inlineScriptName] && !schema) {
} else if (runnable?.type === 'runnableByName' && !schema) {
const { inlineScriptName } = runnable
// Inline scripts directly provide the schema
schema = $app.inlineScripts[inlineScriptName].schema
}
// 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) {
$: if (schema && Object.keys(schema.properties).length !== Object.keys(inputs ?? {}).length) {
let schemaWithoutExtraQueries: Schema = JSON.parse(JSON.stringify(schema))
// Remove extra query params from the schema, which are not directly configurable by the user
@@ -109,18 +109,18 @@
let schemaStripped: Schema | undefined = undefined
function stripSchema(schema: Schema, inputs: InputsSpec) {
function stripSchema(schema: Schema, inputs: AppInputs) {
schemaStripped = JSON.parse(JSON.stringify(schema))
// Remove hidden static inputs
Object.keys(inputs).forEach((key: string) => {
Object.keys(inputs ?? {}).forEach((key: string) => {
const input = inputs[key]
if (input.type === 'static' && !input.visible && schemaStripped !== undefined) {
delete schemaStripped.properties[key]
}
if (input.type === 'output' && schemaStripped !== undefined) {
if (input.type === 'connected' && schemaStripped !== undefined) {
delete schemaStripped.properties[key]
}
})
@@ -135,7 +135,7 @@
$: schema && inputs && stripSchema(schema, inputs)
$: disabledArgs = Object.keys(inputs).reduce(
$: disabledArgs = Object.keys(inputs ?? {}).reduce(
(disabledArgsAccumulator: string[], inputName: string) => {
if (inputs[inputName].type === 'static') {
disabledArgsAccumulator = [...disabledArgsAccumulator, inputName]
@@ -150,7 +150,7 @@
return
}
outputs?.loading.set(true)
outputs?.loading?.set(true)
await testJobLoader?.abstractRun(() => {
const requestBody = {
@@ -158,13 +158,16 @@
force_viewer_static_fields: {}
}
if (inlineScriptName && $app.inlineScripts[inlineScriptName]) {
if (runnable?.type === 'runnableByName') {
const { inlineScriptName } = runnable
requestBody['raw_code'] = {
content: $app.inlineScripts[inlineScriptName].content,
language: $app.inlineScripts[inlineScriptName].language,
path: $app.inlineScripts[inlineScriptName].path
}
} else if (path && runType) {
} else if (runnable?.type === 'runnableByPath') {
const { path, runType } = runnable
requestBody['path'] = `${runType}/${path}`
}
@@ -181,17 +184,16 @@
}
</script>
{#each Object.keys(inputs) as key}
{#each Object.keys(inputs ?? {}) as key}
<InputValue input={inputs[key]} bind:value={runnableInputValues[key]} />
{/each}
<TestJobLoader
on:done={() => {
if (testJob) {
outputs.result.set(testJob?.result)
outputs?.loading.set(false)
result = testJob?.result
if (testJob && outputs) {
outputs.result?.set(testJob?.result)
outputs.loading?.set(false)
result = testJob.result
}
}}
bind:isLoading={testIsLoading}
@@ -203,7 +205,7 @@
<SchemaForm schema={schemaStripped} bind:args {isValid} {disabledArgs} shouldHideNoInputs />
{/if}
{#if inlineScriptName === undefined && path === undefined && runType === undefined && autoRefresh}
{#if !runnable}
<Alert type="warning" size="xs" class="mt-2" title="Missing runnable">
Please select a runnable
</Alert>
@@ -0,0 +1,34 @@
<script lang="ts">
import type { AppInput } from '../../inputType'
import NonRunnableComponent from './NonRunnableComponent.svelte'
import RunnableComponent from './RunnableComponent.svelte'
export let componentInput: AppInput | undefined
export let id: string
export let result: any = undefined
// Optional props
export let extraQueryParams: Record<string, any> = {}
export let autoRefresh: boolean = true
export let runnableComponent: RunnableComponent | undefined = undefined
</script>
{#if componentInput === undefined}
<slot />
{:else if componentInput.type === 'runnable' && componentInput.runnable}
<RunnableComponent
bind:this={runnableComponent}
bind:inputs={componentInput.fields}
bind:result
runnable={componentInput.runnable}
{autoRefresh}
{id}
{extraQueryParams}
>
<slot />
</RunnableComponent>
{:else}
<NonRunnableComponent bind:result {id} {componentInput}>
<slot />
</NonRunnableComponent>
{/if}
@@ -1,54 +0,0 @@
import { default as AppButton } from './buttons/AppButton.svelte'
export { default as AppButton } from './buttons/AppButton.svelte'
import { default as AppBarChart } from './dataDisplay/AppBarChart.svelte'
export { default as AppBarChart } from './dataDisplay/AppBarChart.svelte'
import { default as AppPieChart } from './dataDisplay/AppPieChart.svelte'
export { default as AppPieChart } from './dataDisplay/AppPieChart.svelte'
import { default as AppTable } from './dataDisplay/AppTable.svelte'
export { default as AppTable } from './dataDisplay/AppTable.svelte'
import { default as AppText } from './dataDisplay/AppText.svelte'
export { default as AppText } from './dataDisplay/AppText.svelte'
import { default as AppCheckbox } from './selectInputs/AppCheckbox.svelte'
export { default as AppCheckbox } from './selectInputs/AppCheckbox.svelte'
export { default as AlignWrapper } from './helpers/AlignWrapper.svelte'
export { default as DebouncedInput } from './helpers/DebouncedInput.svelte'
export { default as InputValue } from './helpers/InputValue.svelte'
export { default as RunnableComponent } from './helpers/RunnableComponent.svelte'
// Component groups
const textInputs = []
const numberInputs = []
const buttons = [
AppButton
]
const selectInputs = [
AppCheckbox
]
const dateTimeInputs = []
const dataDisplay = [
AppBarChart,
AppPieChart,
AppTable,
AppText
]
// Aggregated component groups for ease of import
const APP_COMPONENTS = [
textInputs,
numberInputs,
buttons,
selectInputs,
dateTimeInputs,
dataDisplay
]
export {
APP_COMPONENTS,
textInputs,
numberInputs,
buttons,
selectInputs,
dateTimeInputs,
dataDisplay
}
@@ -1,13 +1,14 @@
<script lang="ts">
import Toggle from '$lib/components/Toggle.svelte'
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext, InputsSpec } from '../../types'
import type { AppEditorContext } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
export let id: string
export let componentInputs: InputsSpec
export let configuration: Record<string, AppInput>
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
@@ -18,12 +19,15 @@
let labelValue: string = 'Default label'
let value: boolean = false
// As the checkbox is a special case and has no input
// we need to manually set the output
$: outputs = $worldStore?.outputsById[id] as {
result: Output<boolean>
}
</script>
<InputValue input={componentInputs.label} bind:value={labelValue} />
<InputValue input={configuration.label} bind:value={labelValue} />
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<Toggle
@@ -10,8 +10,7 @@
AppEditorContext,
ConnectingInput,
EditorBreakpoint,
EditorMode,
InputType
EditorMode
} from '../types'
import AppEditorHeader from './AppEditorHeader.svelte'
import GridEditor from './GridEditor.svelte'
@@ -25,6 +24,7 @@
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
import ContextPanel from './contextPanel/ContextPanel.svelte'
import { classNames } from '$lib/utils'
import AppPreview from './AppPreview.svelte'
export let app: App
export let path: string
@@ -37,7 +37,7 @@
const mode = writable<EditorMode>(initialMode)
const breakpoint = writable<EditorBreakpoint>('lg')
const connectingInput = writable<ConnectingInput<InputType, any>>({
const connectingInput = writable<ConnectingInput>({
opened: false,
input: undefined
})
@@ -75,50 +75,59 @@
<AppEditorHeader bind:title={$appStore.title} bind:mode={$mode} bind:breakpoint={$breakpoint} />
{/if}
<SplitPanesWrapper class="max-w-full overflow-hidden">
<Pane size={previewing ? 0 : 20} minSize={previewing ? 0 : 20} maxSize={40}>
<ContextPanel appPath={path} />
</Pane>
<Pane size={previewing ? 100 : 60}>
<div class="p-4 bg-gray-100 h-full w-full">
{#if $appStore.grid}
<div class={classNames('mx-auto h-full', width)}>
<GridEditor />
</div>
{/if}
</div>
</Pane>
<Pane size={previewing ? 0 : 25} minSize={previewing ? 0 : 20} maxSize={40}>
<Tabs bind:selected={selectedTab}>
<Tab value="insert" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<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}
<div class="p-4 text-sm">No component selected.</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
</svelte:fragment>
</Tabs>
</Pane>
</SplitPanesWrapper>
{#if previewing}
<AppPreview app={$appStore} />
{:else}
<SplitPanesWrapper class="max-w-full overflow-hidden">
<Pane size={previewing ? 0 : 20} minSize={previewing ? 0 : 20} maxSize={40}>
<ContextPanel appPath={path} />
</Pane>
<Pane size={previewing ? 100 : 60}>
<div class="p-4 bg-gray-100 h-full w-full relative">
{#if $appStore.grid}
<div class={classNames('mx-auto h-full', width)}>
<GridEditor />
</div>
{/if}
{#if $connectingInput.opened}
<div
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
/>
{/if}
</div>
</Pane>
<Pane size={previewing ? 0 : 25} minSize={previewing ? 0 : 20} maxSize={40}>
<Tabs bind:selected={selectedTab}>
<Tab value="insert" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<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}
<div class="p-4 text-sm">No component selected.</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
</svelte:fragment>
</Tabs>
</Pane>
</SplitPanesWrapper>
{/if}
@@ -8,24 +8,22 @@
AppEditorContext,
ConnectingInput,
EditorBreakpoint,
EditorMode,
InputType
EditorMode
} from '../types'
import GridEditor from './GridEditor.svelte'
import { classNames } from '$lib/utils'
export let app: App
export let initialMode: EditorMode = 'dnd'
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>(initialMode)
const mode = writable<EditorMode>('preview')
const breakpoint = writable<EditorBreakpoint>('lg')
const connectingInput = writable<ConnectingInput<InputType, any>>({
const connectingInput = writable<ConnectingInput>({
opened: false,
input: undefined
})
@@ -40,12 +38,6 @@
breakpoint
})
function clearSelectionOnPreview() {
if ($mode === 'preview') {
$selectedComponent = undefined
}
}
let mounted = false
onMount(() => {
@@ -53,7 +45,6 @@
})
$: mounted && ($worldStore = buildWorld($staticOutputs))
$: $mode && $selectedComponent && clearSelectionOnPreview()
$: width = $breakpoint === 'sm' ? 'w-[640px]' : 'w-full '
</script>
@@ -6,61 +6,70 @@
import TableComponent from '../components/dataDisplay/AppTable.svelte'
import TextComponent from '../components/dataDisplay/AppText.svelte'
import type { AppComponent, AppEditorContext } from '../types'
import { displayData } from '../utils'
import ButtonComponent from '../components/buttons/AppButton.svelte'
import PieChartComponent from '../components/dataDisplay/AppPieChart.svelte'
import CheckboxComponent from '../components/selectInputs/AppCheckbox.svelte'
import ComponentHeader from './ComponentHeader.svelte'
export let component: AppComponent
export let selected: boolean
const { staticOutputs, mode } = getContext<AppEditorContext>('AppEditorContext')
$: shouldDisplayOverlay = selected && $mode !== 'preview'
const { staticOutputs, mode, connectingInput } = getContext<AppEditorContext>('AppEditorContext')
</script>
<div class="h-full flex flex-col w-full">
{#if selected}
<span
class={classNames(
'text-white px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute -top-5',
selected ? 'bg-indigo-500' : 'bg-gray-500'
)}
>
{displayData[component.type].name} - {component.id}
</span>
{#if shouldDisplayOverlay}
<ComponentHeader {component} {selected} />
{/if}
<div
class={classNames(
' border overflow-auto cursor-pointer h-full bg-white',
selected ? 'border-blue-500' : 'border-white',
$mode === 'preview' ? 'border-white' : 'hover:border-blue-500',
component.card ? 'p-2' : ''
shouldDisplayOverlay ? 'border-blue-500' : 'border-white',
!selected && $mode !== 'preview' && !component.card ? 'border-gray-100' : '',
$mode !== 'preview' && !$connectingInput.opened ? 'hover:border-blue-500' : '',
component.card ? 'p-2' : '',
'relative'
)}
>
{#if component.type === 'displaycomponent'}
<DisplayComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
<DisplayComponent
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'barchartcomponent'}
<BarChartComponent
{...component}
bind:inputs={component.inputs}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'piechartcomponent'}
<PieChartComponent
{...component}
bind:staticOutputs={$staticOutputs[component.id]}
bind:inputs={component.inputs}
bind:componentInput={component.componentInput}
/>
{:else if component.type === 'tablecomponent'}
<TableComponent
{...component}
bind:staticOutputs={$staticOutputs[component.id]}
bind:inputs={component.inputs}
bind:componentInput={component.componentInput}
/>
{:else if component.type === 'textcomponent'}
<TextComponent {...component} />
<TextComponent
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'buttoncomponent'}
<ButtonComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
<ButtonComponent
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'checkboxcomponent'}
<CheckboxComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{/if}
@@ -0,0 +1,17 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import type { AppComponent } from '../types'
import { displayData } from '../utils'
export let component: AppComponent
export let selected: boolean
</script>
<span
class={classNames(
'text-white px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute -top-5',
selected ? 'bg-indigo-500' : 'bg-gray-500'
)}
>
{displayData[component.type].name} - {component.id}
</span>
@@ -5,10 +5,16 @@
import ComponentEditor from './ComponentEditor.svelte'
import { classNames } from '$lib/utils'
import { columnConfiguration, disableDrag, enableDrag } from '../gridUtils'
import { Alert } from '$lib/components/common'
import { fly } from 'svelte/transition'
const { selectedComponent, app, mode } = getContext<AppEditorContext>('AppEditorContext')
import Button from '$lib/components/common/button/Button.svelte'
$: if ($mode === 'preview') {
const { selectedComponent, app, mode, connectingInput } =
getContext<AppEditorContext>('AppEditorContext')
// The drag is disabled when the user is connecting an input
$: if ($mode === 'preview' || $connectingInput.opened) {
$app.grid.map((gridItem) => disableDrag(gridItem))
} else {
$app.grid.map((gridItem) => enableDrag(gridItem))
@@ -16,7 +22,10 @@
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="bg-white h-full" on:click|preventDefault={() => ($selectedComponent = undefined)}>
<div
class="bg-white h-full relative"
on:click|preventDefault={() => ($selectedComponent = undefined)}
>
<Grid bind:items={$app.grid} rowHeight={64} let:dataItem cols={columnConfiguration}>
{#each $app.grid as gridComponent (gridComponent.id)}
{#if gridComponent.data.id === dataItem.data.id}
@@ -28,7 +37,9 @@
gridComponent.data.card ? 'border border-gray-100' : ''
)}
on:click|preventDefault|stopPropagation={() => {
$selectedComponent = dataItem.data.id
if (!$connectingInput.opened) {
$selectedComponent = dataItem.data.id
}
}}
>
<ComponentEditor
@@ -39,6 +50,31 @@
{/if}
{/each}
</Grid>
{#if $connectingInput.opened}
<div
class="fixed top-32 left-0 w-full z-10 flex justify-center items-center"
transition:fly={{ duration: 100, y: -100 }}
>
<Alert title="Connecting" type="info">
<div class="flex gap-2 flex-col">
Click on the output of the component you want to connect to on the left panel.
<div>
<Button
color="blue"
variant="border"
size="xs"
on:click={() => {
$connectingInput.opened = false
$connectingInput.input = undefined
}}
>
Stop connecting</Button
>
</div>
</div>
</Alert>
</div>
{/if}
</div>
<style>
@@ -1,13 +1,8 @@
import type { Aligned } from "../../types"
const defaultProps = {
inputs: {},
componentInputs: {}
}
import type { Aligned } from '../../types'
const defaultAlignement: Aligned = {
horizontalAlignment: 'center',
verticalAlignment: 'center'
}
export { defaultProps, defaultAlignement }
export { defaultAlignement }
@@ -4,5 +4,7 @@ const buttonColorOptions = [...BUTTON_COLORS]
export const staticValues = {
buttonColorOptions,
buttonSizeOptions: ['xs', 'sm', 'md', 'lg', 'xl']
buttonSizeOptions: ['xs', 'sm', 'md', 'lg', 'xl'],
tableSearchOptions: ['Frontend', 'Backend', 'Disabled'],
chartThemeOptions: ['theme1', 'theme2', 'theme3']
} as const
@@ -1,22 +1,20 @@
import type { AppComponent, ComponentSet } from '../../types'
import { defaultAlignement, defaultProps } from './componentDefaultProps'
import type { ComponentSet } from '../../types'
import { defaultAlignement } from './componentDefaultProps'
const windmillComponents: ComponentSet = {
title: 'Windmill Components',
components: [
{
...defaultProps,
id: 'displaycomponent',
type: 'displaycomponent',
componentInputs: {
result: {
id: undefined,
name: undefined,
type: 'output',
defaultValue: {},
fieldType: 'object'
}
}
componentInput: {
type: 'static',
fieldType: 'text',
defaultValue: 'Lorem Ipsum',
value: 'Lorem Ipsum'
},
configuration: {},
card: false
}
]
}
@@ -35,11 +33,17 @@ const buttons: ComponentSet = {
title: 'Buttons',
components: [
{
...defaultProps,
...defaultAlignement,
id: 'buttoncomponent',
type: 'buttoncomponent',
componentInputs: {
componentInput: {
type: 'static',
fieldType: 'textarea',
defaultValue: '',
value: ''
},
recompute: undefined,
configuration: {
label: {
type: 'static',
visible: true,
@@ -64,7 +68,7 @@ const buttons: ComponentSet = {
defaultValue: 'md'
}
},
runnable: true,
card: false
}
]
@@ -74,11 +78,10 @@ const selectInputs: ComponentSet = {
title: 'Select Inputs',
components: [
{
...defaultProps,
...defaultAlignement,
id: 'checkboxcomponent',
type: 'checkboxcomponent',
componentInputs: {
configuration: {
label: {
type: 'static',
visible: true,
@@ -87,6 +90,7 @@ const selectInputs: ComponentSet = {
defaultValue: 'Lorem ipsum'
}
},
componentInput: undefined,
card: false
}
]
@@ -101,31 +105,29 @@ const dataDisplay: ComponentSet = {
title: 'Data Display',
components: [
{
...defaultProps,
...defaultAlignement,
id: 'textcomponent',
type: 'textcomponent',
componentInputs: {
content: {
type: 'static',
visible: true,
value: 'Lorem ipsum',
fieldType: 'textarea',
defaultValue: 'Lorem ipsum'
}
}
componentInput: {
type: 'static',
visible: true,
value: 'Lorem ipsum',
fieldType: 'textarea',
defaultValue: 'Lorem ipsum'
},
configuration: {},
card: false
},
{
...defaultProps,
id: 'tablecomponent',
type: 'tablecomponent',
componentInputs: {
searchEnabled: {
configuration: {
searchConfiguration: {
fieldType: 'select',
type: 'static',
value: false,
fieldType: 'boolean',
visible: true,
defaultValue: false
value: 'Disabled',
optionValuesKey: 'tableSearchOptions',
defaultValue: 'Disabled'
},
paginationEnabled: {
type: 'static',
@@ -135,22 +137,87 @@ const dataDisplay: ComponentSet = {
defaultValue: false
}
},
runnable: true,
componentInput: {
type: 'static',
fieldType: 'array',
defaultValue: [
{
id: 1,
name: 'Lorem ipsum',
age: 42
},
{
id: 2,
name: 'Lorem ipsum',
age: 42
}
],
value: [
{
id: 1,
name: 'Lorem ipsum',
age: 42
},
{
id: 2,
name: 'Lorem ipsum',
age: 42
}
]
},
card: true,
actionButtons: []
},
{
...defaultProps,
id: 'piechartcomponent',
type: 'piechartcomponent',
runnable: true,
configuration: {
theme: {
type: 'static',
value: 'theme1',
fieldType: 'select',
optionValuesKey: 'chartThemeOptions',
defaultValue: 'theme1'
},
labels: {
type: 'static',
value: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum'],
fieldType: 'array',
defaultValue: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum']
}
},
componentInput: {
type: 'static',
fieldType: 'array',
defaultValue: [25, 50, 25],
value: [25, 50, 25]
},
card: true
},
{
...defaultProps,
id: 'barchartcomponent',
type: 'barchartcomponent',
runnable: true,
configuration: {
theme: {
type: 'static',
value: 'theme1',
fieldType: 'select',
optionValuesKey: 'chartThemeOptions',
defaultValue: 'theme1'
},
labels: {
type: 'static',
value: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum'],
fieldType: 'array',
defaultValue: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum']
}
},
componentInput: {
type: 'static',
fieldType: 'array',
defaultValue: [25, 50, 25],
value: [25, 50, 25]
},
card: true
}
]
@@ -2,7 +2,6 @@
import { fade } from 'svelte/transition'
import type { Schema } from '$lib/common'
import { Drawer } from '$lib/components/common'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import ScriptEditor from '$lib/components/ScriptEditor.svelte'
@@ -24,16 +23,16 @@
$: isTakenPath = Object.keys($app.inlineScripts).includes(newScriptPath)
function connectInput(id: string, name: string) {
function connectInput(componentId: string, path: string) {
if ($connectingInput) {
$connectingInput = {
opened: false,
input: {
id,
name,
type: 'output',
defaultValue: undefined,
fieldType: 'any'
connection: {
componentId,
path
},
type: 'connected'
}
}
}
@@ -6,7 +6,10 @@
faAlignCenter,
faAlignLeft,
faAlignRight,
faArrowRight,
faBolt,
faClose,
faCode,
faTrashAlt
} from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
@@ -18,10 +21,11 @@
import gridHelp from 'svelte-grid/build/helper/index.mjs'
import PickInlineScript from './PickInlineScript.svelte'
import TableActions from './TableActions.svelte'
import { capitalize } from '$lib/utils'
import Badge from '$lib/components/common/badge/Badge.svelte'
import { gridColumns } from '../../gridUtils'
import { Plus } from 'svelte-lucide'
import StaticInputEditor from './StaticInputEditor.svelte'
import ConnectedInputEditor from './ConnectedInputEditor.svelte'
import { sanitizeInputSpec } from '../../utils'
export let component: AppComponent | undefined
export let onDelete: (() => void) | undefined = undefined
@@ -53,110 +57,130 @@
{#if component}
<div class="flex flex-col w-full divide-y">
{#if component.runnable}
<PanelSection title="Runnable">
{#if component.runnable && component['inlineScriptName']}
<div class="flex justify-between w-full items-center">
<span class="text-xs">{component['inlineScriptName']}</span>
<Button
{#if component.componentInput}
<PanelSection title="Main input">
<div class="flex flex-col w-full gap-2 my-2">
<ToggleButtonGroup bind:selected={component.componentInput.type}>
<ToggleButton position="left" value="static" startIcon={{ icon: faBolt }} size="xs">
Static
</ToggleButton>
<ToggleButton
value="connected"
position="center"
startIcon={{ icon: faArrowRight }}
size="xs"
color="red"
startIcon={{ icon: faClose }}
on:click={() => {
if (component) {
component['inlineScriptName'] = undefined
}
}}
>
Clear
</Button>
</div>
{/if}
Connect
</ToggleButton>
<ToggleButton position="right" value="runnable" startIcon={{ icon: faCode }} size="xs">
Script
</ToggleButton>
</ToggleButtonGroup>
{#if component.runnable && component['path']}
<div class="flex gap-2 items-center">
<div>
<Badge color="blue">{capitalize(component['runType'])}</Badge>
<span class="text-xs">{component['path']}</span>
</div>
<Button
size="xs"
color="red"
variant="border"
startIcon={{ icon: faClose }}
on:click={() => {
if (component) {
component['path'] = undefined
}
}}
>
Clear
</Button>
</div>
{/if}
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
<div class="text-sm">Inline scripts:</div>
<div class="flex gap-2">
<Button
btnClasses="w-24 truncate"
size="sm"
spacingSize="md"
variant="border"
color="light"
>
<div class="flex justify-center flex-col items-center gap-2">
<Plus size="18px" />
<span class="text-xs">Create</span>
</div>
</Button>
<PickInlineScript
scripts={(Object.keys($app.inlineScripts) || []).map((summary) => ({ summary }))}
on:pick={({ detail }) => {
if (component?.runnable) {
{#if component.componentInput.type === 'static'}
<StaticInputEditor bind:componentInput={component.componentInput} />
{:else if component.componentInput.type === 'connected' && component.componentInput !== undefined}
<ConnectedInputEditor bind:componentInput={component.componentInput} />
{:else if component && component.componentInput?.type === 'runnable' && component.componentInput.runnable}
<div class="flex justify-between w-full items-center">
<span class="text-xs">
{component.componentInput.runnable.type === 'runnableByName'
? component.componentInput.runnable.inlineScriptName
: component.componentInput.runnable.path}
</span>
<Button
size="xs"
color="red"
startIcon={{ icon: faClose }}
on:click={() => {
// @ts-ignore
component.inlineScriptName = detail.summary
}
}}
/>
</div>
component.componentInput.runnable = undefined
}}
>
Clear
</Button>
</div>
{:else}
<div class="text-sm">Inline scripts:</div>
<div class="flex gap-2">
<Button
btnClasses="w-24 truncate"
size="sm"
spacingSize="md"
variant="border"
color="light"
>
<div class="flex justify-center flex-col items-center gap-2">
<Plus size="18px" />
<div class="text-sm">Pick from workspace:</div>
<div class="flex gap-2">
<PickScript
kind="script"
on:pick={({ detail }) => {
if (component?.runnable) {
component['path'] = detail.path
component['runType'] = 'script'
}
}}
/>
<PickFlow
on:pick={({ detail }) => {
if (component?.runnable) {
component['path'] = detail.path
component['runType'] = 'flow'
}
}}
/>
</div>
{/if}
<span class="text-xs">Create</span>
</div>
</Button>
<PickInlineScript
scripts={(Object.keys($app.inlineScripts) || []).map((summary) => ({ summary }))}
on:pick={({ detail }) => {
if (
component &&
component.componentInput &&
component.componentInput.type === 'runnable'
) {
component.componentInput.runnable = {
type: 'runnableByName',
inlineScriptName: detail.summary
}
}
}}
/>
</div>
<div class="text-sm">Pick from workspace:</div>
<div class="flex gap-2">
<PickScript
kind="script"
on:pick={({ detail }) => {
if (
component &&
component.componentInput &&
component.componentInput.type === 'runnable'
) {
component.componentInput.runnable = {
type: 'runnableByPath',
path: detail.path,
runType: 'script'
}
}
}}
/>
<PickFlow
on:pick={({ detail }) => {
if (
component &&
component.componentInput &&
component.componentInput.type === 'runnable'
) {
component.componentInput.runnable = {
type: 'runnableByPath',
path: detail.path,
runType: 'flow'
}
}
}}
/>
</div>
{/if}
</div>
</PanelSection>
{/if}
{#if Object.values(component.inputs).length > 0}
{#if component.componentInput?.type === 'runnable'}
<PanelSection title="Runnable inputs">
<InputsSpecsEditor bind:inputSpecs={component.inputs} />
<InputsSpecsEditor bind:inputSpecs={component.componentInput.fields} />
</PanelSection>
{/if}
{#if Object.values(component.componentInputs).length > 0}
<PanelSection
title={`Component parameters (${Object.values(component.componentInputs).length})`}
>
<InputsSpecsEditor bind:inputSpecs={component.componentInputs} userInputEnabled={false} />
{#if Object.values(component.configuration).length > 0}
<PanelSection title={`Configuration (${Object.values(component.configuration).length})`}>
<InputsSpecsEditor bind:inputSpecs={component.configuration} userInputEnabled={false} />
</PanelSection>
{/if}
@@ -0,0 +1,78 @@
<script lang="ts">
import type { AppEditorContext } from '../../types'
import { Badge, Button } from '$lib/components/common'
import { faLink } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
export let componentInput: AppInput
const { connectingInput } = getContext<AppEditorContext>('AppEditorContext')
function applyConnection() {
if (
!$connectingInput.opened &&
$connectingInput.input !== undefined &&
componentInput.type === 'connected'
) {
componentInput.connection = $connectingInput.input.connection
$connectingInput = {
opened: false,
input: undefined,
sourceName: undefined
}
}
}
$: $connectingInput && applyConnection()
</script>
{#if componentInput.type === 'connected'}
{#if componentInput.connection}
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Status</span>
<Badge color="green">Connected</Badge>
</div>
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Component</span>
<Badge color="indigo">{componentInput.connection.componentId}</Badge>
</div>
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Path</span>
<Badge color="indigo">{componentInput.connection.path}</Badge>
</div>
<Button
size="xs"
startIcon={{ icon: faLink }}
color="red"
on:click={() => {
if (componentInput.type === 'connected') {
componentInput.connection = undefined
}
}}
>
Clear connection
</Button>
{:else}
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Status</span>
<Badge color="dark-yellow">Not connected</Badge>
</div>
<Button
size="xs"
startIcon={{ icon: faLink }}
color="dark"
on:click={() => {
if (componentInput.type === 'connected') {
$connectingInput = {
opened: true,
input: undefined,
sourceName: componentInput.connection?.path
}
}
}}
>
Connect this input to an output
</Button>
{/if}
{/if}
@@ -1,73 +0,0 @@
<script lang="ts">
import type { AppEditorContext, DynamicInput, InputType } from '../../types'
import { Badge, Button } from '$lib/components/common'
import { faLink } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
export let input: DynamicInput<InputType, any>
const { connectingInput, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
function applyConnection() {
if (!$connectingInput.opened && $connectingInput.input !== undefined) {
input.id = $connectingInput.input.id
input.name = $connectingInput.input.name
// TODO: CHeck whether types are ok
// TODO: Check whether this is needed
$selectedComponent = $selectedComponent
$connectingInput = {
opened: false,
input: undefined
}
}
}
$: $connectingInput && applyConnection()
</script>
{#if input.id && input.name}
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Status</span>
<Badge color="green">Connected</Badge>
</div>
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Component</span>
<Badge color="indigo">{input.id}</Badge>
</div>
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Field name</span>
<Badge color="indigo">{input.name}</Badge>
</div>
<Button
size="xs"
startIcon={{ icon: faLink }}
color="red"
on:click={() => {
input.id = undefined
input.name = undefined
}}
>
Clear connection
</Button>
{:else}
<div class="flex justify-between w-full">
<span class="text-xs font-bold">Status</span>
<Badge color="dark-yellow">Not connected</Badge>
</div>
<Button
size="xs"
startIcon={{ icon: faLink }}
color="dark"
on:click={() => {
$connectingInput = {
opened: true,
input: undefined
}
}}
>
Connect this input to an output
</Button>
{/if}
@@ -1,14 +1,14 @@
<script lang="ts">
import type { AppInputTransform } from '../../types'
import DynamicInputEditor from './DynamicInputEditor.svelte'
import type { AppInput } from '../../inputType'
import ConnectedInputEditor from './ConnectedInputEditor.svelte'
import StaticInputEditor from './StaticInputEditor.svelte'
export let appInputTransform: AppInputTransform
export let componentInput: AppInput
export let canHide: boolean = false
</script>
{#if appInputTransform.type === 'static'}
<StaticInputEditor bind:input={appInputTransform} {canHide} />
{:else if appInputTransform.type === 'output'}
<DynamicInputEditor bind:input={appInputTransform} />
{#if componentInput.type === 'static'}
<StaticInputEditor bind:componentInput {canHide} />
{:else if componentInput.type === 'connected'}
<ConnectedInputEditor bind:componentInput />
{/if}
@@ -2,103 +2,80 @@
import { Badge, ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import { capitalize, classNames } from '$lib/utils'
import { faBolt, faLink, faUser } from '@fortawesome/free-solid-svg-icons'
import type { InputsSpec } from '../../types'
import { fieldTypeToTsType } from '../../utils'
import type { AppInputs } from '../../inputType'
import { fieldTypeToTsType, sanitizeInputSpec } from '../../utils'
import InputsSpecEditor from './InputsSpecEditor.svelte'
export let inputSpecs: InputsSpec
export let inputSpecs: AppInputs
export let userInputEnabled: boolean = true
export let staticOnly: boolean = true
let openedProp: string | undefined = Object.keys(inputSpecs)[0]
const userTypeKeys = ['value']
const staticTypeKeys = ['value']
const dynamicTypeKeys = ['id', 'name']
function sanitizeInputSpec(type: 'user' | 'static' | 'output', inputSpecKey: string) {
const inputSpec = inputSpecs[inputSpecKey]
if (type === 'user') {
for (const key of staticTypeKeys) {
delete inputSpec[key]
}
for (const key of dynamicTypeKeys) {
delete inputSpec[key]
}
} else if (type === 'static') {
for (const key of userTypeKeys) {
delete inputSpec[key]
}
for (const key of dynamicTypeKeys) {
delete inputSpec[key]
}
} else if (type === 'output') {
for (const key of userTypeKeys) {
delete inputSpec[key]
}
for (const key of staticTypeKeys) {
delete inputSpec[key]
}
}
inputSpecs[inputSpecKey] = inputSpec
}
let openedProp: string | undefined = inputSpecs ? Object.keys(inputSpecs)[0] : undefined
</script>
<div class="w-full flex flex-col gap-2">
{#each Object.keys(inputSpecs) as inputSpecKey, index (index)}
{@const input = inputSpecs[inputSpecKey]}
<div>
<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',
openedProp === inputSpecKey ? 'outline outline-gray-500 outline-offset-1' : ''
)}
on:keypress
on:click={() => {
if (openedProp === inputSpecKey) {
openedProp = undefined
} else {
openedProp = inputSpecKey
}
}}
>
{inputSpecKey}
{#if input?.fieldType}
<Badge color={openedProp === inputSpecKey ? 'dark-blue' : 'blue'}>
{capitalize(fieldTypeToTsType(input.fieldType))}
</Badge>
{#if inputSpecs}
<div class="w-full flex flex-col gap-2">
{#each Object.keys(inputSpecs) as inputSpecKey, index (index)}
{@const input = inputSpecs[inputSpecKey]}
<div>
<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',
openedProp === inputSpecKey ? 'outline outline-gray-500 outline-offset-1' : ''
)}
on:keypress
on:click={() => {
if (openedProp === inputSpecKey) {
openedProp = undefined
} else {
openedProp = inputSpecKey
}
}}
>
{inputSpecKey}
{#if input?.fieldType}
<Badge color={openedProp === inputSpecKey ? 'dark-blue' : 'blue'}>
{capitalize(fieldTypeToTsType(input.fieldType))}
</Badge>
{/if}
</div>
{#if inputSpecKey === openedProp}
<div class="flex flex-col w-full gap-2 my-2">
{#if staticOnly}
<ToggleButtonGroup bind:selected={inputSpecs[inputSpecKey].type}>
<ToggleButton position="left" value="static" startIcon={{ icon: faBolt }} size="xs">
Static
</ToggleButton>
<ToggleButton
position={userInputEnabled ? 'center' : 'right'}
value="connected"
startIcon={{ icon: faLink }}
size="xs"
>
Connect
</ToggleButton>
{#if userInputEnabled}
<ToggleButton
position="right"
value="user"
startIcon={{ icon: faUser }}
size="xs"
>
User
</ToggleButton>
{/if}
</ToggleButtonGroup>
{/if}
<InputsSpecEditor
bind:componentInput={inputSpecs[inputSpecKey]}
canHide={userInputEnabled}
/>
</div>
{/if}
</div>
{#if inputSpecKey === openedProp}
<div class="flex flex-col w-full gap-2 my-2">
<ToggleButtonGroup
bind:selected={inputSpecs[inputSpecKey].type}
on:selected={({ detail }) => sanitizeInputSpec(detail, inputSpecKey)}
>
<ToggleButton position="left" value="static" startIcon={{ icon: faBolt }} size="xs">
Static
</ToggleButton>
<ToggleButton
position={userInputEnabled ? 'center' : 'right'}
value="output"
startIcon={{ icon: faLink }}
size="xs"
>
Dynamic
</ToggleButton>
{#if userInputEnabled}
<ToggleButton position="right" value="user" startIcon={{ icon: faUser }} size="xs">
User
</ToggleButton>
{/if}
</ToggleButtonGroup>
<InputsSpecEditor
bind:appInputTransform={inputSpecs[inputSpecKey]}
canHide={userInputEnabled}
/>
</div>
{/if}
</div>
{/each}
</div>
{/each}
</div>
{:else}
<div class="text-gray-500 text-sm">No inputs</div>
{/if}
@@ -29,7 +29,7 @@
/>
<Button
on:click={() => itemPicker.openDrawer()}
on:click={() => itemPicker?.openDrawer()}
btnClasses="w-24 truncate"
size="sm"
spacingSize="md"
@@ -1,32 +1,58 @@
<script lang="ts">
import type { AppInputTransform } from '../../types'
import Toggle from '$lib/components/Toggle.svelte'
import { staticValues } from '../componentsPanel/componentStaticValues'
import type { AppInput } from '../../inputType'
import Button from '$lib/components/common/button/Button.svelte'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
export let input: AppInputTransform
export let canHide: boolean
export let componentInput: AppInput | undefined
export let canHide: boolean = false
</script>
{#if input.type === 'static'}
{#if componentInput?.type === 'static'}
{#if canHide}
<Toggle bind:checked={input.visible} options={{ right: 'Visible' }} />
<Toggle bind:checked={componentInput.visible} options={{ right: 'Visible' }} />
{/if}
{#if input.fieldType === 'number'}
<input type="number" bind:value={input.value} />
{:else if input.fieldType === 'textarea'}
<textarea bind:value={input.value} />
{:else if input.fieldType === 'boolean'}
<Toggle bind:checked={input.value} />
{:else if input.fieldType === 'select'}
<select bind:value={input.value}>
{#each staticValues[input.optionValuesKey] || [] as option}
{#if componentInput.fieldType === 'number'}
<input type="number" bind:value={componentInput.value} />
{:else if componentInput.fieldType === 'textarea'}
<textarea bind:value={componentInput.value} />
{:else if componentInput.fieldType === 'boolean'}
<Toggle bind:checked={componentInput.value} />
{:else if componentInput.fieldType === 'select'}
<select bind:value={componentInput.value}>
{#each staticValues[componentInput.optionValuesKey] || [] as option}
<option value={option}>
{option}
</option>
{/each}
</select>
{:else if componentInput.fieldType === 'array'}
<div class="flex gap-2 flex-col mt-2">
{#if componentInput.value}
{#each componentInput.value as value, index}
<input bind:value={componentInput.value[index]} />
{/each}
<Button
size="xs"
color="dark"
startIcon={{ icon: faPlus }}
on:click={() => {
if (
componentInput?.fieldType === 'array' &&
componentInput.type === 'static' &&
componentInput.value
) {
componentInput.value.push('')
}
}}
>
Add
</Button>
{/if}
</div>
{:else}
<input bind:value={input.value} />
<input bind:value={componentInput.value} />
{/if}
{/if}
@@ -0,0 +1,13 @@
<script lang="ts">
import InputValue from '../../components/helpers/InputValue.svelte'
import type { AppInput } from '../../inputType'
export let componentInput: AppInput | undefined
let label: string = ''
</script>
{#if componentInput !== undefined}
<InputValue input={componentInput} bind:value={label} />
{/if}
{label}
@@ -7,9 +7,9 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { ButtonComponent, AppEditorContext, BaseAppComponent } from '../../types'
import { defaultProps } from '../componentsPanel/componentDefaultProps'
import PanelSection from './common/PanelSection.svelte'
import ComponentPanel from './ComponentPanel.svelte'
import TableActionLabel from './TableActionLabel.svelte'
export let components: (BaseAppComponent & ButtonComponent)[]
@@ -20,10 +20,9 @@
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
const newComponent: BaseAppComponent & ButtonComponent = {
...defaultProps,
id,
type: 'buttoncomponent',
componentInputs: {
configuration: {
label: {
type: 'static',
visible: true,
@@ -48,7 +47,14 @@
defaultValue: 'xs'
}
},
runnable: true
componentInput: {
type: 'static',
fieldType: 'textarea',
defaultValue: '',
value: ''
},
recompute: undefined,
card: false
}
components = [...components, newComponent]
@@ -91,10 +97,8 @@
}}
on:keypress
>
<div
>{component.componentInputs.label.type === 'static'
? component.componentInputs.label.value
: ''}
<div>
<TableActionLabel componentInput={component.componentInput} />
</div>
<Badge color="dark-blue">
Component: {component.id}
@@ -6,7 +6,7 @@
<div class={classNames('flex flex-col gap-2 items-start p-4')}>
<div class="flex justify-between items-center w-full">
<div class="text-xs font-bold">{title}</div>
<div class="text-sm font-bold">{title}</div>
<slot name="action" />
</div>
<slot />
@@ -0,0 +1,85 @@
import type { staticValues } from './editor/componentsPanel/componentStaticValues'
export type InputType =
| 'text'
| 'textarea'
| 'number'
| 'boolean'
| 'select'
| 'date'
| 'time'
| 'datetime'
| 'object'
// Connection to an output of another component
// defined by the id of the component and the path of the output
export type InputConnection = {
componentId: string
path: string
}
// Connected input, connected to an output of another component by the developer
export type ConnectedInput = {
type: 'connected'
connection: InputConnection | undefined
}
// User input, set by the user in the app
export type UserInput<U> = {
type: 'user'
value: U | undefined
}
// Static input, set by the developer in the component panel
export type StaticInput<U> = {
value: U | undefined
type: 'static'
visible?: boolean | undefined
}
type RunnableByPath = {
path: string
runType: 'script' | 'flow'
type: 'runnableByPath'
}
type RunnableByName = {
inlineScriptName: string
type: 'runnableByName'
}
export type Runnable = RunnableByPath | RunnableByName | undefined
// Runnable input, set by the developer in the component panel
export type ResultInput = {
runnable: Runnable
fields: AppInputs
type: 'runnable'
}
type AppInputSpec<T, U> = (StaticInput<U> | ConnectedInput | UserInput<U> | ResultInput) &
InputConfiguration<T, U>
type InputConfiguration<T, U> = {
fieldType: T
defaultValue: U
}
export type AppInput =
| AppInputSpec<'text', string>
| AppInputSpec<'textarea', string>
| AppInputSpec<'number', number>
| AppInputSpec<'boolean', boolean>
| AppInputSpec<'date', string>
| AppInputSpec<'time', string>
| AppInputSpec<'datetime', string>
| AppInputSpec<'object', Record<string | number, any>>
| AppInputSpec<'array', any[]>
| (AppInputSpec<'select', string> & {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
export type AppInputs = Record<string, AppInput>
+29 -9
View File
@@ -1,4 +1,4 @@
import type { AppInputTransform } from './types'
import type { AppInput } from './inputType'
export interface Subscriber<T> {
next(v: T)
@@ -17,7 +17,7 @@ export interface Input<T> extends Subscriber<T> {
export type World = {
outputsById: Record<string, Record<string, Output<any>>>
connect: <T>(inputSpec: AppInputTransform, next: (x: T) => void) => Input<T>
connect: <T>(inputSpec: AppInput, next: (x: T) => void) => Input<T>
}
export function buildWorld(components: Record<string, string[]>) {
@@ -26,6 +26,7 @@ export function buildWorld(components: Record<string, string[]>) {
for (const [k, outputs] of Object.entries(components)) {
outputsById[k] = {}
for (const o of outputs) {
outputsById[k][o] = newWorld.newOutput(k, o)
}
@@ -37,26 +38,38 @@ export function buildWorld(components: Record<string, string[]>) {
export function buildObservableWorld() {
const observables: Record<string, Output<any>> = {}
function connect<T>(inputSpec: AppInputTransform, next: (x: T) => void): Input<T> {
function connect<T>(inputSpec: AppInput, next: (x: T) => void): Input<T> {
if (inputSpec.type === 'static') {
return {
peak: () => inputSpec.value,
next: () => {}
}
} else if (inputSpec.type === 'output') {
} else if (inputSpec.type === 'connected') {
const input = cachedInput(next)
const [name] = inputSpec.name ? inputSpec.name.split('.') : [undefined]
const connection = inputSpec.connection
let obs = observables[`${inputSpec.id}.${name}`]
if (!obs) {
console.warn('Observable at ' + inputSpec.id + '.' + name + ' not found')
if (!connection) {
return {
peak: () => undefined,
next: () => {}
}
}
const { componentId, path } = connection
const [p] = path ? path.split('.') : [undefined]
let obs = observables[`${componentId}.${p}`]
if (!obs) {
console.warn('Observable at ' + componentId + '.' + p + ' not found')
return {
peak: () => undefined,
next: () => {}
}
}
obs.subscribe(input)
return input
} else if (inputSpec.type === 'user') {
@@ -82,6 +95,7 @@ export function buildObservableWorld() {
}
export function cachedInput<T>(nextParan: (x: T) => void): Input<T> {
let value: T | undefined = undefined
function peak(): T | undefined {
return value
}
@@ -104,12 +118,18 @@ export function settableOutput<T>(): Output<T> {
function subscribe(x: Subscriber<T>) {
if (!subscribers.includes(x)) {
subscribers.push(x)
// Send the current value to the new subscriber if it already exists
if (value !== undefined) {
x.next(value)
}
}
}
function set(x: T, force: boolean = false) {
if (value != x || force) {
value = x
subscribers.forEach((x) => x.next(value!))
}
}
+18 -88
View File
@@ -1,87 +1,26 @@
import type { Schema } from '$lib/common'
import type { Preview } from '$lib/gen'
import type { ComponentProps } from 'svelte'
import type { FilledItem } from 'svelte-grid'
import type { Writable } from 'svelte/store'
import type { AppButton } from './components'
import type { staticValues } from './editor/componentsPanel/componentStaticValues'
import type { AppInput, ConnectedInput } from './inputType'
import type { World } from './rx'
export type UserInput<T, V> = {
type: 'user'
value: V | undefined
defaultValue: V
fieldType: T
}
export type DynamicInput<T, V> = {
type: 'output'
id: FieldID | undefined
name: string | undefined
defaultValue: V
fieldType: T
}
export type InputType =
| 'text'
| 'textarea'
| 'number'
| 'boolean'
| 'select'
| 'date'
| 'time'
| 'datetime'
| 'object'
export type StaticInput<T, V> = {
value: V | undefined
type: 'static'
visible?: boolean
defaultValue: V
fieldType: T
}
type AppInput<T extends InputType, V> = StaticInput<T, V> | DynamicInput<T, V> | UserInput<T, V>
export type AppInputTransform =
| AppInput<'text', string>
| AppInput<'textarea', string>
| AppInput<'number', number>
| AppInput<'boolean', boolean>
| (AppInput<'select', string> & {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
| AppInput<'date', string>
| AppInput<'time', string>
| AppInput<'datetime', string>
| AppInput<'object', Record<string | number, any>>
// Inner inputs, (search, filter, page, inputs of a script or flow)
export type InputsSpec = Record<FieldID, AppInputTransform>
type Runnable = {
inlineScriptName?: string
path?: string
runType?: 'script' | 'flow'
}
type BaseComponent<T extends string> = {
type: T
}
export type TextComponent = BaseComponent<'textcomponent'>
export type TextInputComponent = BaseComponent<'textinputcomponent'>
export type ButtonComponent = Runnable & BaseComponent<'buttoncomponent'>
export type RunFormComponent = Runnable & BaseComponent<'runformcomponent'>
export type ButtonComponent = BaseComponent<'buttoncomponent'> & {
recompute: string[] | undefined
}
export type RunFormComponent = BaseComponent<'runformcomponent'>
export type BarChartComponent = BaseComponent<'barchartcomponent'>
export type PieChartComponent = Runnable & BaseComponent<'piechartcomponent'>
export type TableComponent = Runnable &
BaseComponent<'tablecomponent'> & {
actionButtons: (BaseAppComponent & ButtonComponent)[]
}
export type PieChartComponent = BaseComponent<'piechartcomponent'>
export type TableComponent = BaseComponent<'tablecomponent'> & {
actionButtons: (BaseAppComponent & ButtonComponent)[]
}
export type DisplayComponent = BaseComponent<'displaycomponent'>
export type ImageComponent = BaseComponent<'imagecomponent'>
@@ -92,6 +31,7 @@ export type RadioComponent = BaseComponent<'radiocomponent'>
export type HorizontalAlignment = 'left' | 'center' | 'right'
export type VerticalAlignment = 'top' | 'center' | 'bottom'
export type Aligned = {
horizontalAlignment: HorizontalAlignment
verticalAlignment: VerticalAlignment
@@ -99,11 +39,9 @@ export type Aligned = {
export interface BaseAppComponent extends Partial<Aligned> {
id: ComponentID
inputs: InputsSpec
componentInputs: InputsSpec
runnable?: boolean | undefined
card?: boolean | undefined
componentInput: AppInput | undefined
configuration: Record<string, AppInput>
card: boolean | undefined
// TODO: add min/max width/height
}
@@ -155,9 +93,10 @@ export type App = {
title: string
}
export type ConnectingInput<T, V> = {
export type ConnectingInput = {
opened: boolean
input?: DynamicInput<T, V>
input?: ConnectedInput
sourceName?: string
}
export type AppEditorContext = {
@@ -166,20 +105,11 @@ export type AppEditorContext = {
app: Writable<App>
selectedComponent: Writable<string | undefined>
mode: Writable<EditorMode>
connectingInput: Writable<ConnectingInput<any, any>>
connectingInput: Writable<ConnectingInput>
breakpoint: Writable<EditorBreakpoint>
}
export type EditorMode = 'dnd' | 'preview'
export type EditorBreakpoint = 'sm' | 'lg'
type FieldID = string
type ComponentID = string
export type EditorConfig = {
staticInputDisabled: boolean
outputInputDisabled: boolean
userInputEnabled: boolean
visibiltyEnabled: boolean
}
+58 -9
View File
@@ -1,4 +1,3 @@
import type { InputsSpec, InputType } from './types'
import type { Schema } from '$lib/common'
import { FlowService, ScriptService } from '$lib/gen'
@@ -9,6 +8,8 @@ import {
faMobileScreenButton,
faPieChart
} from '@fortawesome/free-solid-svg-icons'
import type { InputType } from 'zlib'
import type { AppInput, AppInputs } from './inputType'
export async function loadSchema(
workspace: string,
@@ -32,7 +33,7 @@ export async function loadSchema(
}
}
export function schemaToInputsSpec(schema: Schema): InputsSpec {
export function schemaToInputsSpec(schema: Schema): AppInputs {
return Object.keys(schema.properties).reduce((accu, key) => {
const property = schema.properties[key]
@@ -100,18 +101,66 @@ export function accessPropertyByPath<T>(object: T, path: string): T | undefined
export function fieldTypeToTsType(InputType: InputType): string {
switch (InputType) {
case 'text':
case 'textarea':
case 'date':
case 'time':
case 'datetime':
case 'select':
return 'string'
case 'number':
return 'number'
case 'boolean':
return 'boolean'
case 'object':
return 'object'
default:
return 'string'
}
}
const userTypeKeys = ['value']
const staticTypeKeys = ['value']
const dynamicTypeKeys = ['connection']
const runnableTypeKeys = ['runnable', 'fields']
export function sanitizeInputSpec(componentInput: AppInput): AppInput {
if (componentInput.type === 'user') {
for (const key of staticTypeKeys) {
delete componentInput[key]
}
for (const key of dynamicTypeKeys) {
delete componentInput[key]
}
for (const key of runnableTypeKeys) {
delete componentInput[key]
}
} else if (componentInput.type === 'static') {
for (const key of userTypeKeys) {
delete componentInput[key]
}
for (const key of dynamicTypeKeys) {
delete componentInput[key]
}
for (const key of runnableTypeKeys) {
delete componentInput[key]
}
} else if (componentInput.type === 'connected') {
for (const key of userTypeKeys) {
delete componentInput[key]
}
for (const key of staticTypeKeys) {
delete componentInput[key]
}
for (const key of runnableTypeKeys) {
delete componentInput[key]
}
} else if (componentInput.type === 'runnable') {
for (const key of userTypeKeys) {
delete componentInput[key]
}
for (const key of staticTypeKeys) {
delete componentInput[key]
}
for (const key of dynamicTypeKeys) {
delete componentInput[key]
}
}
return componentInput
}
@@ -42,7 +42,7 @@
</div>
<div class="border rounded-md p-2">
<AppPreview app={app.value} initialMode="preview" />
<AppPreview app={app.value} />
</div>
{/if}
</CenteredPage>