mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
App editor inline editor (#917)
* fix(frontend): add table * fix(frontend): Rework the context panel * fix(frontend): WIP * fix(frontend): hide script selector when selected * fix(frontend): Fix preview mode + remove errors * fix(frontend): Fix table search * fix(frontend): temporary fix
This commit is contained in:
@@ -13,10 +13,16 @@
|
||||
componentInputs.result.name
|
||||
|
||||
$: inputResult = hasConnection
|
||||
? $worldStore?.connect<any>(componentInputs.result, (x) => {
|
||||
? $worldStore?.connect<any>(componentInputs.result, () => {
|
||||
update()
|
||||
})
|
||||
: undefined
|
||||
: {
|
||||
peak: () => {
|
||||
if (componentInputs.result.type === 'static') {
|
||||
return componentInputs.result.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result: any
|
||||
|
||||
@@ -24,6 +30,8 @@
|
||||
result = inputResult?.peak()
|
||||
}
|
||||
|
||||
$: !hasConnection && componentInputs.result && update()
|
||||
|
||||
export const staticOutputs: string[] = []
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
export let inputs: InputsSpec
|
||||
export let path: string | undefined = undefined
|
||||
export let runType: 'script' | 'flow' | undefined = undefined
|
||||
export let inlineScriptName: string | undefined = undefined
|
||||
|
||||
export const staticOutputs = ['loading', 'result']
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { worldStore, app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
let pagePath = $page.params.path
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
@@ -42,6 +44,11 @@
|
||||
loadSchemaFromTriggerable($workspaceStore, path, runType)
|
||||
}
|
||||
|
||||
$: if (inlineScriptName) {
|
||||
schema = $app.inlineScripts[inlineScriptName].schema
|
||||
reloadSchemaAndArgs()
|
||||
}
|
||||
|
||||
$: if (inputs && schema !== undefined) {
|
||||
if (Object.keys(schema.properties).length !== Object.keys(inputs).length) {
|
||||
inputs = schemaToInputsSpec(schema)
|
||||
@@ -82,17 +89,28 @@
|
||||
}, [])
|
||||
|
||||
async function executeComponent() {
|
||||
await testJobLoader?.abstractRun(() =>
|
||||
AppService.executeComponent({
|
||||
await testJobLoader?.abstractRun(() => {
|
||||
const requestBody = {
|
||||
args,
|
||||
force_viewer_static_fields: {}
|
||||
}
|
||||
|
||||
if (inlineScriptName && $app.inlineScripts[inlineScriptName]) {
|
||||
requestBody['raw_code'] = {
|
||||
content: $app.inlineScripts[inlineScriptName].content,
|
||||
language: $app.inlineScripts[inlineScriptName].language,
|
||||
path: $app.inlineScripts[inlineScriptName].path
|
||||
}
|
||||
} else if (path && runType) {
|
||||
requestBody['path'] = `${runType}/${path}`
|
||||
}
|
||||
|
||||
return AppService.executeComponent({
|
||||
workspace: $workspaceStore!,
|
||||
path: pagePath,
|
||||
requestBody: {
|
||||
path: `${runType}/${path}`,
|
||||
args,
|
||||
force_viewer_static_fields: {}
|
||||
}
|
||||
requestBody
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
outputs?.loading.set(true)
|
||||
}
|
||||
|
||||
@@ -1,108 +1,141 @@
|
||||
<script lang="ts">
|
||||
import type {} from '$lib/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Output } from '../rx'
|
||||
import type { AppEditorContext, ComponentInputsSpec, InputsSpec } from '../types'
|
||||
import ComponentInputValue from './helpers/ComponentInputValue.svelte'
|
||||
import DebouncedInput from './helpers/DebouncedInput.svelte'
|
||||
import RunnableComponent from './helpers/RunnableComponent.svelte'
|
||||
|
||||
export let title: string
|
||||
export let description: string | undefined = undefined
|
||||
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: ComponentInputsSpec
|
||||
|
||||
export let headers: string[]
|
||||
export let data: Array<Record<string, any>>
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
export const staticOutputs: string[] = []
|
||||
// ComponentInput: Static/dynamic
|
||||
// ScriptInput: Run form: Static/Dynamic/User
|
||||
// paramInput: Search : configurable only at component level (toggle)
|
||||
|
||||
let query: string = ''
|
||||
let page: number = 1
|
||||
export const staticOutputs: string[] = ['selectedRow', 'loading', 'result']
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
selectedRow: Output<any>
|
||||
result: Output<Array<string>>
|
||||
loading: Output<boolean>
|
||||
}
|
||||
|
||||
let selectedRowIndex = -1
|
||||
|
||||
function toggleRow(row: Record<string, any>, rowIndex: number) {
|
||||
if (selectedRowIndex === rowIndex) {
|
||||
selectedRowIndex = -1
|
||||
outputs.selectedRow.set(null)
|
||||
} else {
|
||||
selectedRowIndex = rowIndex
|
||||
outputs?.selectedRow.set(row)
|
||||
}
|
||||
}
|
||||
|
||||
let searchEnabledValue: boolean | undefined = undefined
|
||||
let paginationEnabled: boolean | undefined = undefined
|
||||
|
||||
let page = 1
|
||||
let search = ''
|
||||
|
||||
let result: Array<Record<string, any>> = []
|
||||
$: headers = Object.keys(result[0] || {}) || []
|
||||
</script>
|
||||
|
||||
<div class="p-8 w-full">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-xl font-semibold text-gray-900">{title}</h1>
|
||||
{#if description}
|
||||
<p class="mt-2 text-sm text-gray-700">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex flex-col">
|
||||
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
{#each headers as header}
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{header}
|
||||
</th>
|
||||
{/each}
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each data as x}
|
||||
<tr>
|
||||
{#each headers as header}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{x[header]}
|
||||
</td>
|
||||
{/each}
|
||||
<td
|
||||
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
|
||||
>
|
||||
<a href="#" class="text-indigo-600 hover:text-indigo-900">
|
||||
Edit
|
||||
<span class="sr-only">, Lindsay Walton </span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<ComponentInputValue input={componentInputs.searchEnabled} bind:value={searchEnabledValue} />
|
||||
<ComponentInputValue input={componentInputs.paginationEnabled} bind:value={paginationEnabled} />
|
||||
|
||||
<RunnableComponent
|
||||
{id}
|
||||
{path}
|
||||
{runType}
|
||||
{inlineScriptName}
|
||||
bind:inputs
|
||||
bind:result
|
||||
extraQueryParams={{ search, page }}
|
||||
>
|
||||
<div class="gap-2 flex flex-col mt-2">
|
||||
{#if searchEnabledValue}
|
||||
<div>
|
||||
<div>
|
||||
<DebouncedInput placeholder="Search..." bind:value={search} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<table class="divide-y divide-gray-300 border">
|
||||
{#if headers}
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
{#each headers as header}
|
||||
<th
|
||||
scope="col"
|
||||
class="px-4 py-2 text-left text-xs font-medium text-gray-500 tracking-wider"
|
||||
>
|
||||
{header.replace(/([A-Z]+)*([A-Z][a-z])/g, '$1 $2')}
|
||||
</th>
|
||||
{/each}
|
||||
<th scope="col" class="relative py-2 px-4">
|
||||
<span class="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each result as row, rowIndex (rowIndex)}
|
||||
<tr
|
||||
class={classNames(
|
||||
selectedRowIndex === rowIndex ? 'bg-blue-100 hover:bg-blue-200' : 'hover:bg-blue-50'
|
||||
)}
|
||||
on:click={() => toggleRow(row, rowIndex)}
|
||||
>
|
||||
{#each headers as header}
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||
{row[header]}
|
||||
</td>
|
||||
{/each}
|
||||
<td class="relative whitespace-nowrap px-4 py-2 text-right ">
|
||||
{#if false}
|
||||
<Button color="blue" size="xs" variant="contained">Edit</Button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="inline-flex -space-x-px">
|
||||
<li>
|
||||
<button
|
||||
on:click={() => (page -= 1)}
|
||||
class="text-sm py-2 px-4 text-gray-500 bg-white rounded-l-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700"
|
||||
{#if paginationEnabled}
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
on:click={() => {
|
||||
page = page - 1
|
||||
}}
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
disabled={page === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{#each Array(5) as x, i}
|
||||
<li>
|
||||
<button
|
||||
on:click={() => (page = i)}
|
||||
class={classNames(
|
||||
'text-sm py-2 px-4 text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700',
|
||||
page === i ? 'bg-blue-100 font-bold' : 'bg-white'
|
||||
)}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
<li>
|
||||
<button
|
||||
on:click={() => (page += 1)}
|
||||
class="text-sm py-2 px-4 text-gray-500 bg-white rounded-r-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => {
|
||||
page = page + 1
|
||||
}}
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border">Next</Button
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</RunnableComponent>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { Pie } from 'svelte-chartjs'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
ArcElement
|
||||
} from 'chart.js'
|
||||
import type { ChartData } from 'chart.js'
|
||||
import ComponentInputValue from '../helpers/ComponentInputValue.svelte'
|
||||
import type { ComponentInputsSpec } from '../../types'
|
||||
|
||||
export let componentInputs: ComponentInputsSpec
|
||||
export const staticOutputs: string[] = []
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
ArcElement
|
||||
)
|
||||
|
||||
let options = {
|
||||
responsive: true
|
||||
}
|
||||
|
||||
let dataSetValue: Record<string, number> | undefined = undefined
|
||||
|
||||
let data: ChartData<'pie', number[], unknown> = { datasets: [], labels: [] }
|
||||
|
||||
function populateDataSet() {
|
||||
if (dataSetValue) {
|
||||
data.datasets = [
|
||||
{
|
||||
data: Object.values(dataSetValue).filter((val) => typeof val === 'number'),
|
||||
backgroundColor: ['#F7464A', '#46BFBD', '#FDB45C', '#949FB1', '#4D5360', '#AC64AD'],
|
||||
hoverBackgroundColor: ['#FF5A5E', '#5AD3D1', '#FFC870', '#A8B3C5', '#616774', '#DA92DB']
|
||||
}
|
||||
]
|
||||
data.labels = Object.keys(dataSetValue)
|
||||
.filter((key) => typeof dataSetValue?.[key] === 'number')
|
||||
.map((s) => s.replace(/([A-Z]+)*([A-Z][a-z])/g, '$1 $2'))
|
||||
|
||||
data = data
|
||||
}
|
||||
}
|
||||
|
||||
$: dataSetValue && populateDataSet()
|
||||
</script>
|
||||
|
||||
<ComponentInputValue input={componentInputs.dataset} bind:value={dataSetValue} />
|
||||
{#if data.datasets.length > 0}
|
||||
<Pie {data} {options} />
|
||||
{:else}
|
||||
<span>No dataset</span>
|
||||
{/if}
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import type { ComponentInputsSpec, InputsSpec } from '../../types'
|
||||
import ComponentInputValue from '../helpers/ComponentInputValue.svelte'
|
||||
import RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.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: ComponentInputsSpec
|
||||
export let horizontalAlignement: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignement: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
let labelValue: string = 'Default label'
|
||||
|
||||
let tick = 0
|
||||
</script>
|
||||
|
||||
<ComponentInputValue input={componentInputs.label} bind:value={labelValue} />
|
||||
|
||||
<RunnableComponent bind:inputs {path} {runType} {inlineScriptName} {id} shouldTick={tick}>
|
||||
<AlignWrapper {horizontalAlignement} {verticalAlignement}>
|
||||
<Button
|
||||
on:click={() => {
|
||||
tick = tick + 1
|
||||
}}
|
||||
>
|
||||
{labelValue}
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
</RunnableComponent>
|
||||
@@ -1,22 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import type { AppEditorContext, ComponentInputsSpec } from '../../types'
|
||||
import AlignWrapper from './AlignWrapper.svelte'
|
||||
import type { ComponentInputsSpec } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import ComponentInputValue from '../helpers/ComponentInputValue.svelte'
|
||||
|
||||
export let componentInputs: ComponentInputsSpec
|
||||
export let horizontalAlignement: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignement: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
export const staticOutputs: string[] = []
|
||||
|
||||
let contentValue: string = ''
|
||||
</script>
|
||||
|
||||
{#if $worldStore && componentInputs?.content.type === 'static'}
|
||||
<div class="prose">
|
||||
<AlignWrapper {horizontalAlignement} {verticalAlignement}>
|
||||
<SvelteMarkdown source={componentInputs?.content?.value} />
|
||||
</AlignWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
<ComponentInputValue input={componentInputs.content} bind:value={contentValue} />
|
||||
|
||||
<AlignWrapper {horizontalAlignement} {verticalAlignement}>
|
||||
<SvelteMarkdown source={contentValue} />
|
||||
</AlignWrapper>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { StaticInput, DynamicInput, AppEditorContext } from '../../types'
|
||||
|
||||
export let input: DynamicInput | StaticInput
|
||||
export let value: any
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
$: hasConnection = input.type === 'output' && input.id && input.name
|
||||
|
||||
$: inputResult = hasConnection
|
||||
? $worldStore?.connect<any>(input, () => updateValue())
|
||||
: {
|
||||
peak: () => {
|
||||
if (input.type === 'static') {
|
||||
return input.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue() {
|
||||
value = inputResult?.peak()
|
||||
}
|
||||
|
||||
$: !hasConnection && input && updateValue()
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
export let placeholder: string = 'Search...'
|
||||
export let value: string
|
||||
export let debounceDelay: number = 500
|
||||
|
||||
let timer: NodeJS.Timeout
|
||||
|
||||
function debounce(event: KeyboardEvent) {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
const target = event.target as HTMLInputElement
|
||||
value = target.value
|
||||
}, debounceDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<input {placeholder} on:keyup={debounce} />
|
||||
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import type { Schema } from '$lib/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
import { AppService, type CompletedJob } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faArrowsRotate } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, InputsSpec } from '../../types'
|
||||
import { buildArgs, loadSchema, schemaToInputsSpec } from '../../utils'
|
||||
|
||||
// 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 extraQueryParams: Record<string, any> = {}
|
||||
|
||||
export let shouldTick: number | undefined = undefined
|
||||
|
||||
export let result: any = undefined
|
||||
|
||||
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
let pagePath = $page.params.path
|
||||
|
||||
// Local state
|
||||
let args: Record<string, any> = {}
|
||||
let schema: Schema | undefined = undefined
|
||||
let schemaClone: Schema | undefined = undefined
|
||||
|
||||
let isValid = true
|
||||
let testIsLoading = false
|
||||
|
||||
$: if (outputs) {
|
||||
outputs.loading.set(testIsLoading)
|
||||
}
|
||||
|
||||
let testJob: CompletedJob | undefined = undefined
|
||||
let testJobLoader: TestJobLoader | undefined = undefined
|
||||
|
||||
$: if ($workspaceStore && path && runType) {
|
||||
loadSchemaFromTriggerable($workspaceStore, path, runType)
|
||||
}
|
||||
|
||||
$: if (inlineScriptName && $app.inlineScripts[inlineScriptName]) {
|
||||
schema = $app.inlineScripts[inlineScriptName].schema
|
||||
|
||||
Object.keys(extraQueryParams).forEach((key) => {
|
||||
if (schema?.properties[key]) {
|
||||
delete schema.properties[key]
|
||||
}
|
||||
})
|
||||
|
||||
reloadSchemaAndArgs()
|
||||
}
|
||||
|
||||
$: if (inputs && schema !== undefined) {
|
||||
if (Object.keys(schema.properties).length !== Object.keys(inputs).length) {
|
||||
inputs = schemaToInputsSpec(schema)
|
||||
}
|
||||
|
||||
reloadSchemaAndArgs()
|
||||
}
|
||||
|
||||
// Load once
|
||||
async function loadSchemaFromTriggerable(
|
||||
workspace: string,
|
||||
path: string,
|
||||
runType: 'script' | 'flow'
|
||||
) {
|
||||
schema = await loadSchema(workspace, path, runType)
|
||||
|
||||
Object.keys(extraQueryParams).forEach((key) => {
|
||||
if (schema?.properties[key]) {
|
||||
delete schema.properties[key]
|
||||
}
|
||||
})
|
||||
args = buildArgs(inputs, schema)
|
||||
}
|
||||
|
||||
async function reloadSchemaAndArgs() {
|
||||
schemaClone = JSON.parse(JSON.stringify(schema))
|
||||
|
||||
if (schemaClone !== undefined) {
|
||||
args = buildArgs(inputs, schemaClone)
|
||||
|
||||
Object.keys(schemaClone.properties).forEach((propKey) => {
|
||||
if (!Object.keys(args).includes(propKey)) {
|
||||
delete schemaClone!.properties[propKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: disabledArgs = Object.keys(inputs).reduce((a: string[], c: string) => {
|
||||
if (inputs[c].type === 'static') {
|
||||
a = [...a, c]
|
||||
}
|
||||
return a
|
||||
}, [])
|
||||
|
||||
async function executeComponent() {
|
||||
await testJobLoader?.abstractRun(() => {
|
||||
const requestBody = {
|
||||
args: {
|
||||
...args,
|
||||
...extraQueryParams
|
||||
},
|
||||
force_viewer_static_fields: {}
|
||||
}
|
||||
|
||||
if (inlineScriptName && $app.inlineScripts[inlineScriptName]) {
|
||||
requestBody['raw_code'] = {
|
||||
content: $app.inlineScripts[inlineScriptName].content,
|
||||
language: $app.inlineScripts[inlineScriptName].language,
|
||||
path: $app.inlineScripts[inlineScriptName].path
|
||||
}
|
||||
} else if (path && runType) {
|
||||
requestBody['path'] = `${runType}/${path}`
|
||||
}
|
||||
|
||||
return AppService.executeComponent({
|
||||
workspace: $workspaceStore!,
|
||||
path: pagePath,
|
||||
requestBody
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$: if (testJobLoader && shouldTick) {
|
||||
executeComponent()
|
||||
}
|
||||
|
||||
$: extraQueryParams && executeComponent()
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<Array<any>>
|
||||
loading: Output<boolean>
|
||||
}
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
on:done={() => {
|
||||
if (testJob) {
|
||||
outputs.result.set(testJob?.result)
|
||||
result = testJob?.result
|
||||
}
|
||||
}}
|
||||
bind:isLoading={testIsLoading}
|
||||
bind:job={testJob}
|
||||
bind:this={testJobLoader}
|
||||
/>
|
||||
|
||||
{#if schemaClone !== undefined}
|
||||
<SchemaForm schema={schemaClone} bind:args bind:isValid {disabledArgs} />
|
||||
{/if}
|
||||
|
||||
{#if shouldTick === undefined}
|
||||
<Button size="xs" color="dark" on:click={() => executeComponent()} disabled={!isValid}>
|
||||
<div>
|
||||
{Object.keys(args).length > 0 ? 'Submit' : 'Refresh'}
|
||||
{#if testIsLoading}
|
||||
<Icon data={faArrowsRotate} class="animate-spin ml-2" scale={0.8} />
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
<slot />
|
||||
@@ -17,14 +17,13 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faPlus, faSliders } from '@fortawesome/free-solid-svg-icons'
|
||||
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
|
||||
import PanelSection from './settingsPanel/common/PanelSection.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import ContextPanel from './contextPanel/ContextPanel.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
|
||||
export let app: App
|
||||
export let path: string
|
||||
|
||||
const appStore = writable<App>(app)
|
||||
|
||||
const worldStore = writable<World | undefined>(undefined)
|
||||
const staticOutputs = writable<Record<string, string[]>>({})
|
||||
const selectedComponent = writable<string | undefined>(undefined)
|
||||
@@ -56,14 +55,14 @@
|
||||
let mounted = false
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
console.log($staticOutputs, $appStore.grid)
|
||||
})
|
||||
|
||||
$: $mode && $selectedComponent && clearSelectionOnPreview()
|
||||
$: mounted && ($worldStore = buildWorld($staticOutputs))
|
||||
|
||||
$: $mode && $selectedComponent && clearSelectionOnPreview()
|
||||
$: selectedTab = 'settings'
|
||||
|
||||
// If ever the the selected component changes, we need to update the selected tab
|
||||
selectedComponent.subscribe(() => {
|
||||
if (selectedTab === 'insert') {
|
||||
setTimeout(() => {
|
||||
@@ -71,90 +70,51 @@
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function connectInput(id: string, name: string) {
|
||||
if ($connectingInput) {
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
input: {
|
||||
id,
|
||||
name,
|
||||
type: 'output',
|
||||
defaultValue: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppEditorHeader title={app.title} bind:mode={$mode} />
|
||||
<SplitPanesWrapper>
|
||||
{#if $mode !== 'preview'}
|
||||
<Pane size={20} minSize={20} maxSize={40}>
|
||||
<PanelSection title="Component output">
|
||||
{#each Object.entries($staticOutputs) as [componentId, outputs], index}
|
||||
{#if outputs.length > 0}
|
||||
<Badge color="blue">{componentId}</Badge>
|
||||
|
||||
{#each outputs as output}
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
disabled={!$connectingInput.opened}
|
||||
on:click={() => {
|
||||
connectInput(componentId, output)
|
||||
}}
|
||||
>
|
||||
{output}
|
||||
</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Context">Todo</PanelSection>
|
||||
</Pane>
|
||||
{/if}
|
||||
<Pane>
|
||||
<div class="p-4 bg-gray-100">
|
||||
<Pane size={20} minSize={20} maxSize={40}>
|
||||
<ContextPanel appPath={path} />
|
||||
</Pane>
|
||||
<Pane size={60} maxSize={100}>
|
||||
<div class="p-4 bg-gray-100 h-full" id="faton">
|
||||
{#if $appStore.grid}
|
||||
<GridEditor />
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
{#if $mode !== 'preview'}
|
||||
<Pane size={20} minSize={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>
|
||||
{/if}
|
||||
<Pane size={20} minSize={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>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import TextComponent from '../components/common/TextComponent.svelte'
|
||||
import type { AppComponent, AppEditorContext } from '../types'
|
||||
import { displayData } from '../utils'
|
||||
import ButtonComponent from '../components/common/ButtonComponent.svelte'
|
||||
import PieChartComponent from '../components/charts/PieChartComponent.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -44,14 +46,26 @@
|
||||
<DisplayComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{:else if component.type === 'barchartcomponent'}
|
||||
<BarChartComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{:else if component.type === 'piechartcomponent'}
|
||||
<PieChartComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{:else if component.type === 'tablecomponent'}
|
||||
<TableComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
<TableComponent
|
||||
{...component}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
bind:inputs={component.inputs}
|
||||
/>
|
||||
{:else if component.type === 'textcomponent'}
|
||||
<TextComponent
|
||||
{...component}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
bind:componentInputs={component.componentInputs}
|
||||
/>
|
||||
{:else if component.type === 'buttoncomponent'}
|
||||
<ButtonComponent
|
||||
{...component}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
bind:componentInputs={component.componentInputs}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,21 +11,21 @@
|
||||
|
||||
$: if ($mode === 'preview') {
|
||||
$app.grid.map((c) => {
|
||||
c[6].customDragger = true
|
||||
c[6].customResizer = true
|
||||
c[COLS].customDragger = true
|
||||
c[COLS].customResizer = true
|
||||
return c
|
||||
})
|
||||
} else {
|
||||
$app.grid.map((c) => {
|
||||
c[6].customDragger = false
|
||||
c[6].customResizer = false
|
||||
c[COLS].customDragger = false
|
||||
c[COLS].customResizer = false
|
||||
return c
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-white">
|
||||
<Grid bind:items={$app.grid} rowHeight={100} let:dataItem {cols}>
|
||||
<div class="bg-white h-full">
|
||||
<Grid bind:items={$app.grid} rowHeight={32} let:dataItem {cols}>
|
||||
{@const index = $app.grid.findIndex((c) => c.data.id === dataItem.data.id)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
|
||||
const COLS = 6
|
||||
|
||||
function add(
|
||||
function addComponent(
|
||||
appComponent: AppComponent,
|
||||
defaultDimensions: Size,
|
||||
minDimensions: Size = { w: 1, h: 1 },
|
||||
maxDimensions: Size = { w: 6, h: 6 }
|
||||
maxDimensions: Size = { w: 6, h: 12 }
|
||||
) {
|
||||
const grid = $app.grid ?? []
|
||||
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
|
||||
@@ -58,12 +58,12 @@
|
||||
{#each componentSets as componentSet, index (index)}
|
||||
<div class="px-4 pt-4 text-sm font-semibold">{componentSet.title}</div>
|
||||
|
||||
<section class="grid grid-cols-3 gap-2 p-4">
|
||||
<section class="grid grid-cols-3 gap-1 p-4">
|
||||
{#each componentSet.components as item, componentIndex (componentIndex)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="border shadow-sm h-20 p-2 flex flex-col gap-2 items-center justify-center bg-white rounded-md scale-100 hover:scale-105 ease-in duration-75"
|
||||
on:click={() => add(item, { w: 2, h: 2 })}
|
||||
class="border shadow-sm h-16 p-2 flex flex-col gap-2 items-center justify-center bg-white rounded-md scale-100 hover:scale-105 ease-in duration-75"
|
||||
on:click={() => addComponent(item, { w: 2, h: 2 })}
|
||||
>
|
||||
<Icon data={displayData[item.type].icon} scale={1.6} class="text-blue-800" />
|
||||
<div class="text-xs">{displayData[item.type].name}</div>
|
||||
|
||||
@@ -45,7 +45,16 @@ const plainComponents = {
|
||||
{
|
||||
...defaultProps,
|
||||
id: 'buttoncomponent',
|
||||
type: 'buttoncomponent'
|
||||
type: 'buttoncomponent',
|
||||
componentInputs: {
|
||||
label: {
|
||||
type: 'static',
|
||||
visible: true,
|
||||
value: 'Lorem ipsum',
|
||||
fieldType: 'textarea'
|
||||
}
|
||||
},
|
||||
runnable: true
|
||||
},
|
||||
{
|
||||
...defaultProps,
|
||||
@@ -81,7 +90,15 @@ const chartComponents = {
|
||||
{
|
||||
...defaultProps,
|
||||
id: 'piechartcomponent',
|
||||
type: 'piechartcomponent'
|
||||
type: 'piechartcomponent',
|
||||
componentInputs: {
|
||||
dataset: {
|
||||
type: 'static',
|
||||
visible: true,
|
||||
value: {},
|
||||
fieldType: 'textarea'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
...defaultProps,
|
||||
@@ -91,6 +108,30 @@ const chartComponents = {
|
||||
] as AppComponent[]
|
||||
}
|
||||
|
||||
const componentSets = [windmillComponents, plainComponents, chartComponents]
|
||||
const tableComponents = {
|
||||
title: 'Table',
|
||||
components: [
|
||||
{
|
||||
...defaultProps,
|
||||
id: 'tablecomponent',
|
||||
type: 'tablecomponent',
|
||||
componentInputs: {
|
||||
searchEnabled: {
|
||||
type: 'static',
|
||||
value: false,
|
||||
fieldType: 'boolean'
|
||||
},
|
||||
paginationEnabled: {
|
||||
type: 'static',
|
||||
value: false,
|
||||
fieldType: 'boolean'
|
||||
}
|
||||
},
|
||||
runnable: true
|
||||
}
|
||||
] as AppComponent[]
|
||||
}
|
||||
|
||||
const componentSets = [windmillComponents, plainComponents, chartComponents, tableComponents]
|
||||
|
||||
export { componentSets }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
|
||||
export let outputs: string[] = []
|
||||
export let componentId: string
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let object = {}
|
||||
|
||||
outputs.forEach((output) => {
|
||||
console.log({ output })
|
||||
$worldStore?.outputsById[componentId][output].subscribe({
|
||||
next: (value) => {
|
||||
object[output] = value
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<ObjectViewer json={object} on:select topBrackets={true} />
|
||||
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
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'
|
||||
import { Preview } from '$lib/gen'
|
||||
import { DENO_INIT_CODE_CLEAR } from '$lib/script_helpers'
|
||||
import { classNames, emptySchema } from '$lib/utils'
|
||||
import { faEdit, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
|
||||
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
|
||||
|
||||
export let appPath: string
|
||||
|
||||
const { connectingInput, staticOutputs, app, worldStore } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function connectInput(id: string, name: string) {
|
||||
if ($connectingInput) {
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
input: {
|
||||
id,
|
||||
name,
|
||||
type: 'output',
|
||||
defaultValue: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createScript() {
|
||||
const input = document.getElementById('scriptPath') as HTMLInputElement
|
||||
const scriptPath = input.value
|
||||
|
||||
const path = `${appPath}/inline-script/${scriptPath}`
|
||||
const inlineScript = {
|
||||
content: DENO_INIT_CODE_CLEAR,
|
||||
language: Preview.language.DENO,
|
||||
path,
|
||||
schema: emptySchema()
|
||||
}
|
||||
|
||||
if ($app.inlineScripts) {
|
||||
$app.inlineScripts[scriptPath] = inlineScript
|
||||
} else {
|
||||
$app.inlineScripts = {
|
||||
[scriptPath]: inlineScript
|
||||
}
|
||||
}
|
||||
scriptCreationDrawer.closeDrawer()
|
||||
}
|
||||
|
||||
// Inline DENO, Inline Python, Inline GO, Inline SQL
|
||||
|
||||
let selectedScript:
|
||||
| { content: string; language: Preview.language; path: string; schema: Schema }
|
||||
| undefined = undefined
|
||||
let scriptEditorDrawer: Drawer
|
||||
|
||||
let scriptCreationDrawer: Drawer
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={scriptCreationDrawer} size="1000px">
|
||||
<DrawerContent
|
||||
title="Script creation"
|
||||
on:close={() => {
|
||||
scriptCreationDrawer.closeDrawer()
|
||||
}}
|
||||
>
|
||||
<input value="" id="scriptPath" />
|
||||
<Button on:click={createScript}>Create</Button>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={scriptEditorDrawer} size="1000px">
|
||||
<DrawerContent
|
||||
title="Script Editor"
|
||||
noPadding
|
||||
on:close={() => {
|
||||
scriptEditorDrawer.closeDrawer()
|
||||
}}
|
||||
>
|
||||
{#if selectedScript}
|
||||
<ScriptEditor
|
||||
lang={selectedScript.language}
|
||||
bind:code={selectedScript.content}
|
||||
path={selectedScript.path}
|
||||
bind:schema={selectedScript.schema}
|
||||
/>
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<PanelSection title="Inline scripts">
|
||||
<svelte:fragment slot="action">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="contained"
|
||||
on:click={() => {
|
||||
scriptCreationDrawer?.openDrawer()
|
||||
}}
|
||||
startIcon={{ icon: faPlus }}
|
||||
>
|
||||
<span>Add script</span>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="w-full border rounded-sm">
|
||||
{#each $app.inlineScripts ? Object.entries($app.inlineScripts) : [] as [key, value], index}
|
||||
<div
|
||||
class={classNames(
|
||||
'flex justify-between flex-row w-full items-center p-2',
|
||||
index % 2 === 0 ? 'bg-gray-100' : 'bg-white'
|
||||
)}
|
||||
>
|
||||
<span class="text-xs">{key}</span>
|
||||
<div>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
iconOnly
|
||||
startIcon={{ icon: faEdit }}
|
||||
on:click={() => {
|
||||
if (value) {
|
||||
selectedScript = value
|
||||
scriptEditorDrawer.openDrawer()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="border"
|
||||
iconOnly
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => {
|
||||
if ($app.inlineScripts[key]) {
|
||||
delete $app.inlineScripts[key]
|
||||
$app = $app
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Outputs">
|
||||
{#each Object.entries($staticOutputs) as [componentId, outputs], index}
|
||||
{#if outputs.length > 0}
|
||||
<Badge color="blue">Component: {componentId}</Badge>
|
||||
|
||||
<div class="w-full p-2 rounded-xs border">
|
||||
<ComponentOutputViewer
|
||||
{outputs}
|
||||
{componentId}
|
||||
on:select={({ detail }) => {
|
||||
const [output] = detail.split('.')
|
||||
connectInput(componentId, output)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</PanelSection>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Preview } from '$lib/gen'
|
||||
import { DENO_INIT_CODE_CLEAR } from '$lib/script_helpers'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
export let appPath: string
|
||||
|
||||
function createScript() {
|
||||
const scriptPath = 'name'
|
||||
const path = `${appPath}/inline-script/${scriptPath}`
|
||||
const inlineScript = {
|
||||
content: DENO_INIT_CODE_CLEAR,
|
||||
language: Preview.language.DENO,
|
||||
path,
|
||||
schema: emptySchema()
|
||||
}
|
||||
|
||||
if ($app.inlineScripts) {
|
||||
$app.inlineScripts[scriptPath] = inlineScript
|
||||
} else {
|
||||
$app.inlineScripts = {
|
||||
[scriptPath]: inlineScript
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<input value="" />
|
||||
<Button on:click={createScript}>Create</Button>
|
||||
+2
-2
@@ -11,13 +11,13 @@
|
||||
</script>
|
||||
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
{#each Object.keys(componentInputSpecs) as inputSpecKey}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#each Object.keys(componentInputSpecs) as inputSpecKey, index (index)}
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs font-bold border rounded-md py-1 px-2 cursor-pointer hover:bg-gray-800 hover:text-white transition-all',
|
||||
openedProp !== inputSpecKey ? 'bg-gray-200 ' : 'bg-gray-600 text-gray-300'
|
||||
)}
|
||||
on:keypress
|
||||
on:click={() => {
|
||||
openedProp = inputSpecKey
|
||||
}}
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function remove() {
|
||||
function removeGridElement() {
|
||||
const COLS = 6
|
||||
const index = $app.grid.findIndex((c) => c.data.id === component?.id)
|
||||
const index = $app.grid.findIndex((gridComponent) => gridComponent.data.id === component?.id)
|
||||
$app.grid.splice(index, 1)
|
||||
$app.grid = gridHelp.adjust($app.grid, COLS)
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
<InputsSpecsEditor bind:inputSpecs={component.inputs} />
|
||||
{/if}
|
||||
|
||||
{#if component.type === 'runformcomponent' && component.path === undefined}
|
||||
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
|
||||
<span class="text-sm">Select a script or a flow to continue</span>
|
||||
<PickScript
|
||||
kind="script"
|
||||
@@ -57,6 +57,22 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
|
||||
{#each Object.keys($app.inlineScripts ?? {}) as inlineScriptName}
|
||||
<Button
|
||||
on:click={() => {
|
||||
if (component?.runnable) {
|
||||
// @ts-ignore
|
||||
component.inlineScriptName = inlineScriptName
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
Link {inlineScriptName}
|
||||
</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if component.componentInputs}
|
||||
<ComponentInputsSpecsEditor bind:componentInputSpecs={component.componentInputs} />
|
||||
{/if}
|
||||
@@ -98,7 +114,7 @@
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faTrashAlt }}
|
||||
on:click={remove}
|
||||
on:click={removeGridElement}
|
||||
>
|
||||
Delete component
|
||||
</Button>
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
<Toggle bind:checked={input.visible} options={{ right: 'Visible' }} />
|
||||
{/if}
|
||||
|
||||
{#if input.fieldType === 'text'}
|
||||
<input bind:value={input.value} />
|
||||
{:else if input.fieldType === 'number'}
|
||||
{#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}
|
||||
<input bind:value={input.value} />
|
||||
{/if}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
</script>
|
||||
|
||||
<div class="p-4 flex flex-col gap-2 items-start">
|
||||
<div class="text-sm font-bold">{title}</div>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="text-xs font-bold">{title}</div>
|
||||
<slot name="action" />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Schema, SchemaProperty } from '$lib/common'
|
||||
import type { Preview } from '$lib/gen'
|
||||
import type { FilledItem } from 'svelte-grid'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { World } from './rx'
|
||||
@@ -40,26 +41,30 @@ export type TextInputComponent = {
|
||||
type: 'textinputcomponent'
|
||||
}
|
||||
|
||||
export type RunFormComponent = {
|
||||
type: 'runformcomponent'
|
||||
export type ButtonComponent = {
|
||||
type: 'buttoncomponent'
|
||||
}
|
||||
|
||||
type Runnable = {
|
||||
inlineScriptName?: string
|
||||
path?: string
|
||||
runType?: 'script' | 'flow'
|
||||
}
|
||||
|
||||
export type BarChartComponent = {
|
||||
type: 'barchartcomponent'
|
||||
inputs: {}
|
||||
export type RunFormComponent = Runnable & {
|
||||
type: 'runformcomponent'
|
||||
}
|
||||
|
||||
export type TableComponent = {
|
||||
export type BarChartComponent = {
|
||||
type: 'barchartcomponent'
|
||||
}
|
||||
|
||||
export type PieChartComponent = {
|
||||
type: 'piechartcomponent'
|
||||
}
|
||||
|
||||
export type TableComponent = Runnable & {
|
||||
type: 'tablecomponent'
|
||||
inputs: {}
|
||||
path: string
|
||||
runType: 'script' | 'flow'
|
||||
title: string
|
||||
description: string | undefined
|
||||
headers: string[]
|
||||
data: Array<Record<string, any>>
|
||||
}
|
||||
|
||||
export type DisplayComponent = {
|
||||
@@ -74,6 +79,9 @@ export type AppComponent =
|
||||
| BarChartComponent
|
||||
| TableComponent
|
||||
| TextComponent
|
||||
| TableComponent
|
||||
| ButtonComponent
|
||||
| PieChartComponent
|
||||
) & {
|
||||
id: ComponentID
|
||||
width: number
|
||||
@@ -83,6 +91,9 @@ export type AppComponent =
|
||||
inputs: InputsSpec
|
||||
// Only dynamic inputs (Result of display)
|
||||
componentInputs: ComponentInputsSpec
|
||||
runnable?: boolean | undefined
|
||||
|
||||
// TODO: add min/max width/height
|
||||
}
|
||||
|
||||
type SectionID = string
|
||||
@@ -99,6 +110,10 @@ export type GridItem = FilledItem<{
|
||||
|
||||
export type App = {
|
||||
grid: GridItem[]
|
||||
inlineScripts: Record<
|
||||
string,
|
||||
{ content: string; language: Preview.language; path: string; schema: Schema }
|
||||
>
|
||||
title: string
|
||||
}
|
||||
|
||||
@@ -118,7 +133,7 @@ export type AppEditorContext = {
|
||||
resizing: Writable<boolean>
|
||||
}
|
||||
|
||||
export type EditorMode = 'width' | 'dnd' | 'preview'
|
||||
export type EditorMode = 'dnd' | 'preview'
|
||||
|
||||
type FieldID = string
|
||||
|
||||
|
||||
@@ -132,5 +132,9 @@ export const displayData = {
|
||||
barchartcomponent: {
|
||||
name: 'Bar chart',
|
||||
icon: faBarChart
|
||||
},
|
||||
tablecomponent: {
|
||||
name: 'Table',
|
||||
icon: faBarChart
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
export let element: ButtonType.Element | undefined = undefined
|
||||
export let id: string = ''
|
||||
export let nonCaptureEvent: boolean = false
|
||||
export let buttonType: 'button' | 'submit' | 'reset' = 'button'
|
||||
|
||||
let loading = false
|
||||
|
||||
@@ -70,7 +71,8 @@
|
||||
disabled,
|
||||
href,
|
||||
target,
|
||||
tabindex: disabled ? -1 : 0
|
||||
tabindex: disabled ? -1 : 0,
|
||||
type: buttonType
|
||||
}
|
||||
|
||||
async function onClick(event: MouseEvent) {
|
||||
@@ -100,10 +102,11 @@
|
||||
<svelte:element
|
||||
this={href ? 'a' : 'button'}
|
||||
bind:this={element}
|
||||
on:click|stopPropagation={onClick}
|
||||
on:click={onClick}
|
||||
on:focus
|
||||
on:blur
|
||||
{...buttonProps}
|
||||
type="submit"
|
||||
>
|
||||
{#if loading}
|
||||
<Icon
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
async function createApp() {
|
||||
const appJson: App = {
|
||||
grid: [],
|
||||
title: 'New app'
|
||||
title: 'New app',
|
||||
inlineScripts: {}
|
||||
}
|
||||
|
||||
const policy = {
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
|
||||
{#if app}
|
||||
<div class="h-screen">
|
||||
<AppEditor app={app.value} />
|
||||
<AppEditor app={app.value} path={app.path} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user