mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 16:02:24 +00:00
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:
@@ -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>
|
||||
@@ -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!))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user