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:
Faton Ramadani
2022-11-21 18:24:46 +01:00
committed by GitHub
parent c7030a94ce
commit 80c11aa314
27 changed files with 905 additions and 242 deletions
@@ -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>
@@ -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>
+29 -14
View File
@@ -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
+2 -1
View File
@@ -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}