feat(frontend): Fix UI (#1009)
* feat(frontend): Fix UI * feat(frontend): Set correct default value when adding a new element to a typed array * feat(frontend): add refresh all * feat(frontend): add inline delete button * feat(frontend): fix alignment * feat(frontend): clean up * feat(frontend): rework editor * feat(frontend): Fix component dimensions * feat(frontend): Fix default min dimensions * feat(frontend): add missing alert * feat(frontend): Fix default data * feat(frontend): Support frontend/backend search * feat(frontend): finish picker
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Button, type ButtonType } from '$lib/components/common'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
@@ -9,14 +11,15 @@
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
export let configuration: Record<string, AppInput>
|
||||
|
||||
export let recomputeIds: string[] | undefined = undefined
|
||||
export let extraQueryParams: Record<string, any> = {}
|
||||
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
const { runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let labelValue: string = 'Default label'
|
||||
let color: ButtonType.Color
|
||||
let size: ButtonType.Size
|
||||
@@ -38,6 +41,12 @@
|
||||
<Button
|
||||
on:click={() => {
|
||||
runnableComponent?.runComponent()
|
||||
|
||||
if (recomputeIds) {
|
||||
recomputeIds.forEach((id) => {
|
||||
$runnableComponents[id]?.()
|
||||
})
|
||||
}
|
||||
}}
|
||||
{size}
|
||||
{color}
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
animation: false
|
||||
animation: false,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
|
||||
$: data = {
|
||||
|
||||
@@ -41,26 +41,30 @@
|
||||
|
||||
let page = 1
|
||||
let searchValue = ''
|
||||
|
||||
let result: Array<Record<string, any>> = []
|
||||
$: headers = Object.keys(result[0] || {}) || []
|
||||
|
||||
const extraQueryParams = { search: searchValue, page }
|
||||
$: headers = Array.from(new Set(result.flatMap((row) => Object.keys(row))))
|
||||
$: extraQueryParams = search === 'Backend' ? { search: searchValue, page } : { page }
|
||||
|
||||
export const reservedKeys: string[] = Object.keys(extraQueryParams)
|
||||
function searchInResult(searchValue: string) {
|
||||
if (searchValue === '') {
|
||||
return result
|
||||
}
|
||||
return result.filter((row) =>
|
||||
Object.values(row).some((value) => value.toString().includes(searchValue))
|
||||
)
|
||||
}
|
||||
|
||||
$: (search === 'Frontend' || search === 'Backend') && (extraQueryParams.search = searchValue)
|
||||
let filteredResult: Array<Record<string, any>> = []
|
||||
|
||||
$: search === 'Frontend' && (filteredResult = searchInResult(searchValue))
|
||||
$: (search === 'Backend' || search === 'Disabled') && (filteredResult = result)
|
||||
</script>
|
||||
|
||||
<InputValue input={configuration.search} bind:value={search} />
|
||||
<InputValue input={configuration.pagination} bind:value={pagination} />
|
||||
|
||||
<RunnableWrapper
|
||||
bind:componentInput
|
||||
{id}
|
||||
bind:result
|
||||
extraQueryParams={{ search: searchValue, page }}
|
||||
>
|
||||
<RunnableWrapper bind:componentInput {id} bind:result {extraQueryParams}>
|
||||
<div class="gap-2 flex flex-col mt-2">
|
||||
{#if search !== 'Disabled'}
|
||||
<div>
|
||||
@@ -89,7 +93,7 @@
|
||||
</thead>
|
||||
{/if}
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each result as row, rowIndex (rowIndex)}
|
||||
{#each filteredResult as row, rowIndex (rowIndex)}
|
||||
<tr
|
||||
class={classNames(
|
||||
selectedRowIndex === rowIndex ? 'bg-blue-100 hover:bg-blue-200' : 'hover:bg-blue-50'
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
$: classes = classNames(
|
||||
'flex w-full h-full flex-col',
|
||||
'flex w-full h-full',
|
||||
tailwindHorizontalAlignment(horizontalAlignment),
|
||||
tailwindVerticalAlignment(verticalAlignment)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { AppService, type CompletedJob } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppInputs, Runnable } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
@@ -23,7 +23,13 @@
|
||||
export let autoRefresh: boolean = true
|
||||
export let result: any = undefined
|
||||
|
||||
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore, runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
onMount(() => {
|
||||
$runnableComponents[id] = async () => {
|
||||
await executeComponent()
|
||||
}
|
||||
})
|
||||
|
||||
let pagePath = $page.params.path
|
||||
let args: Record<string, any> = {}
|
||||
@@ -78,7 +84,7 @@
|
||||
async function loadSchemaFromTriggerable(
|
||||
workspace: string,
|
||||
path: string,
|
||||
runType: 'script' | 'flow'
|
||||
runType: 'script' | 'flow' | 'hubscript'
|
||||
) {
|
||||
schema = await loadSchema(workspace, path, runType)
|
||||
}
|
||||
@@ -87,6 +93,7 @@
|
||||
$: if ($workspaceStore && runnable?.type === 'runnableByPath' && !schema) {
|
||||
// Remote schema needs to be loaded
|
||||
const { path, runType } = runnable
|
||||
|
||||
loadSchemaFromTriggerable($workspaceStore, path, runType)
|
||||
} else if (runnable?.type === 'runnableByName' && !schema) {
|
||||
const { inlineScriptName } = runnable
|
||||
@@ -179,8 +186,8 @@
|
||||
})
|
||||
}
|
||||
|
||||
export function runComponent() {
|
||||
executeComponent()
|
||||
export async function runComponent() {
|
||||
await executeComponent()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import DebouncedInput from '../helpers/DebouncedInput.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
export let id: string
|
||||
@@ -17,9 +16,7 @@
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<string>
|
||||
}
|
||||
$: if(value || !value) {
|
||||
outputs?.result.set(value || '')
|
||||
}
|
||||
$: (value || !value) && outputs?.result.set(value || '')
|
||||
</script>
|
||||
|
||||
<InputValue input={configuration.label} bind:value={labelValue} />
|
||||
@@ -29,5 +26,5 @@
|
||||
<div>
|
||||
{labelValue}
|
||||
</div>
|
||||
<DebouncedInput bind:value={value} debounceDelay={300} type="text" placeholder="Type..." />
|
||||
<input type="text" bind:value placeholder="Type..." />
|
||||
</label>
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
input: undefined
|
||||
})
|
||||
|
||||
const runnableComponents = writable<Record<string, () => void>>({})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
worldStore,
|
||||
staticOutputs,
|
||||
@@ -49,22 +51,16 @@
|
||||
selectedComponent,
|
||||
mode,
|
||||
connectingInput,
|
||||
breakpoint
|
||||
breakpoint,
|
||||
runnableComponents
|
||||
})
|
||||
|
||||
function clearSelectionOnPreview() {
|
||||
if ($mode === 'preview') {
|
||||
$selectedComponent = undefined
|
||||
}
|
||||
}
|
||||
|
||||
let mounted = false
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
})
|
||||
|
||||
$: mounted && ($worldStore = buildWorld($staticOutputs))
|
||||
$: $mode && $selectedComponent && clearSelectionOnPreview()
|
||||
$: selectedTab = $selectedComponent ? 'settings' : 'insert'
|
||||
$: previewing = $mode === 'preview'
|
||||
|
||||
@@ -79,11 +75,11 @@
|
||||
<AppPreview app={$appStore} />
|
||||
{:else}
|
||||
<SplitPanesWrapper class="max-w-full overflow-hidden">
|
||||
<Pane size={previewing ? 0 : 20} minSize={previewing ? 0 : 20} maxSize={40}>
|
||||
<Pane size={20} minSize={15} maxSize={40}>
|
||||
<ContextPanel appPath={path} />
|
||||
</Pane>
|
||||
<Pane size={previewing ? 100 : 60}>
|
||||
<div class="p-4 bg-gray-100 h-full w-full relative">
|
||||
<Pane size={60}>
|
||||
<div class="p-4 bg-gray-100 min-h-full w-full relative">
|
||||
{#if $appStore.grid}
|
||||
<div class={classNames('mx-auto h-full', width)}>
|
||||
<GridEditor />
|
||||
@@ -96,7 +92,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={previewing ? 0 : 25} minSize={previewing ? 0 : 20} maxSize={40}>
|
||||
<Pane size={30} minSize={25} maxSize={40}>
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
<Tab value="insert" size="xs">
|
||||
<div class="m-1 flex flex-row gap-2">
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<div class="border-b flex flex-row justify-between py-1 px-4 items-center">
|
||||
<input class="text-sm w-64" bind:value={title} />
|
||||
<div class="flex gap-8">
|
||||
<div class="flex gap-2">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={mode}>
|
||||
<ToggleButton position="left" value="dnd" startIcon={{ icon: faHand }} size="xs">
|
||||
@@ -76,7 +76,7 @@
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex flex-row gap-2 w-64 justify-end">
|
||||
<Button color="dark" size="xs" variant="border" startIcon={{ icon: faExternalLink }}>
|
||||
Publish
|
||||
</Button>
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
input: undefined
|
||||
})
|
||||
|
||||
const runnableComponents = writable<Record<string, () => void>>({})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
worldStore,
|
||||
staticOutputs,
|
||||
@@ -35,7 +37,8 @@
|
||||
selectedComponent,
|
||||
mode,
|
||||
connectingInput,
|
||||
breakpoint
|
||||
breakpoint,
|
||||
runnableComponents
|
||||
})
|
||||
|
||||
let mounted = false
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<div class="h-full flex flex-col w-full">
|
||||
{#if shouldDisplayOverlay}
|
||||
<ComponentHeader {component} {selected} />
|
||||
<ComponentHeader {component} {selected} on:delete />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { AppComponent } from '../types'
|
||||
import { displayData } from '../utils'
|
||||
import { X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<span
|
||||
@@ -15,3 +19,14 @@
|
||||
>
|
||||
{displayData[component.type].name} - {component.id}
|
||||
</span>
|
||||
<button
|
||||
class={classNames(
|
||||
'text-white px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute -top-5 right-0 cursor-pointer',
|
||||
'bg-gray-600 hover:bg-gray-800'
|
||||
)}
|
||||
on:click={() => {
|
||||
dispatch('delete')
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
import Grid from 'svelte-grid'
|
||||
import ComponentEditor from './ComponentEditor.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { columnConfiguration, disableDrag, enableDrag } from '../gridUtils'
|
||||
import { columnConfiguration, disableDrag, enableDrag, gridColumns } from '../gridUtils'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { fly } from 'svelte/transition'
|
||||
import gridHelp from 'svelte-grid/build/helper/index.mjs'
|
||||
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
|
||||
|
||||
const { selectedComponent, app, mode, connectingInput } =
|
||||
const { selectedComponent, app, mode, connectingInput, staticOutputs, runnableComponents } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
// The drag is disabled when the user is connecting an input
|
||||
@@ -19,6 +21,23 @@
|
||||
} else {
|
||||
$app.grid.map((gridItem) => enableDrag(gridItem))
|
||||
}
|
||||
|
||||
function deleteComponent(component) {
|
||||
if (component) {
|
||||
$app.grid = $app.grid.filter((gridComponent) => gridComponent.data.id !== component?.id)
|
||||
|
||||
gridColumns.forEach((colIndex) => {
|
||||
$app.grid = gridHelp.adjust($app.grid, colIndex)
|
||||
})
|
||||
|
||||
// Delete static inputs
|
||||
delete $staticOutputs[component.id]
|
||||
$staticOutputs = $staticOutputs
|
||||
|
||||
delete $runnableComponents[component.id]
|
||||
$runnableComponents = $runnableComponents
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -26,6 +45,8 @@
|
||||
class="bg-white h-full relative"
|
||||
on:click|preventDefault={() => ($selectedComponent = undefined)}
|
||||
>
|
||||
<RecomputeAllComponents />
|
||||
|
||||
<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}
|
||||
@@ -45,6 +66,7 @@
|
||||
<ComponentEditor
|
||||
bind:component={gridComponent.data}
|
||||
selected={$selectedComponent === dataItem.data.id}
|
||||
on:delete={() => deleteComponent(gridComponent.data)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -68,8 +90,8 @@
|
||||
$connectingInput.input = undefined
|
||||
}}
|
||||
>
|
||||
Stop connecting</Button
|
||||
>
|
||||
Stop connecting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../types'
|
||||
|
||||
const { runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let loading: boolean = false
|
||||
|
||||
async function onRefresh() {
|
||||
await Promise.all(
|
||||
Object.keys($runnableComponents).map((id) => {
|
||||
return $runnableComponents?.[id]?.()
|
||||
})
|
||||
)
|
||||
loading = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
btnClasses="m-2 mb-4"
|
||||
startIcon={{ icon: faRefresh, classes: classNames(loading ? 'animate-spin' : '', 'mr-2') }}
|
||||
color="light"
|
||||
variant="border"
|
||||
disabled={Object.keys($runnableComponents).length === 0}
|
||||
on:click={() => {
|
||||
loading = true
|
||||
onRefresh()
|
||||
}}
|
||||
>
|
||||
Recompute all ({Object.keys($runnableComponents).length})
|
||||
</Button>
|
||||
@@ -14,14 +14,31 @@
|
||||
import { gridColumns } from '../../gridUtils'
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const COLS = 6
|
||||
|
||||
function addComponent(
|
||||
appComponent: AppComponent,
|
||||
defaultDimensions: Size,
|
||||
minDimensions: Size = { w: 2, h: 1 },
|
||||
maxDimensions: Size = { w: 12, h: 12 }
|
||||
) {
|
||||
function getMinDimensionsByComponent(componentType: string, column: number): Size {
|
||||
console.log(componentType, column)
|
||||
if (componentType === 'buttoncomponent') {
|
||||
return column === 3 ? { w: 1, h: 1 } : { w: 3, h: 1 }
|
||||
} else if (componentType === 'textcomponent') {
|
||||
return column === 3 ? { w: 1, h: 1 } : { w: 3, h: 1 }
|
||||
} else if (componentType === 'textinputcomponent') {
|
||||
return column === 3 ? { w: 1, h: 2 } : { w: 3, h: 2 }
|
||||
} else if (componentType === 'barchartcomponent') {
|
||||
return column === 3 ? { w: 2, h: 4 } : { w: 6, h: 4 }
|
||||
} else if (componentType === 'piechartcomponent') {
|
||||
return column === 3 ? { w: 2, h: 4 } : { w: 6, h: 4 }
|
||||
} else if (componentType === 'tablecomponent') {
|
||||
return column === 3 ? { w: 3, h: 4 } : { w: 12, h: 4 }
|
||||
} else if (componentType === 'displaycomponent') {
|
||||
return column === 3 ? { w: 2, h: 2 } : { w: 6, h: 4 }
|
||||
} else if (componentType === 'checkboxcomponent') {
|
||||
return column === 3 ? { w: 1, h: 1 } : { w: 3, h: 1 }
|
||||
} else {
|
||||
return { w: 2, h: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
function addComponent(appComponent: AppComponent) {
|
||||
const grid = $app.grid ?? []
|
||||
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
|
||||
|
||||
@@ -34,8 +51,7 @@
|
||||
customDragger: false,
|
||||
customResizer: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
...defaultDimensions
|
||||
y: 0
|
||||
}
|
||||
|
||||
const newItem: GridItem = {
|
||||
@@ -43,28 +59,24 @@
|
||||
id: id
|
||||
}
|
||||
|
||||
function getMinMaxDimensions(column) {
|
||||
if (column === 3) {
|
||||
return {
|
||||
min: { w: 1, h: 1 },
|
||||
max: { w: 3, h: 12 }
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
min: minDimensions,
|
||||
max: maxDimensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gridColumns.forEach((column) => {
|
||||
newItem[column] = newComponent
|
||||
const position = gridHelp.findSpace(newItem, grid, column)
|
||||
const dimensions = getMinMaxDimensions(column)
|
||||
newItem[column] = { ...newItem[column], ...position, ...dimensions }
|
||||
const min = getMinDimensionsByComponent(appComponent.type, column)
|
||||
|
||||
const max = { w: 12, h: 12 }
|
||||
|
||||
newItem[column].w = min.w
|
||||
newItem[column].h = min.h
|
||||
|
||||
newItem[column] = { ...newItem[column], ...position, min, max }
|
||||
})
|
||||
|
||||
$app.grid = [...grid, newItem]
|
||||
|
||||
gridColumns.forEach((colIndex) => {
|
||||
$app.grid = gridHelp.adjust($app.grid, colIndex)
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -89,7 +101,7 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2 p-2">
|
||||
{#each components as item}
|
||||
<button
|
||||
on:click={() => addComponent(item, { w: 2, h: 2 })}
|
||||
on:click={() => addComponent(item)}
|
||||
title={displayData[item.type].name}
|
||||
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"
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
import type { ComponentSet } from '../../types'
|
||||
import { defaultAlignement } from './componentDefaultProps'
|
||||
|
||||
const windmillComponents: ComponentSet = {
|
||||
title: 'Windmill Components',
|
||||
components: [
|
||||
{
|
||||
id: 'displaycomponent',
|
||||
type: 'displaycomponent',
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
defaultValue: 'Lorem Ipsum',
|
||||
value: 'Lorem Ipsum'
|
||||
},
|
||||
configuration: {},
|
||||
card: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const textInputs: ComponentSet = {
|
||||
title: 'Text Inputs',
|
||||
const inputs: ComponentSet = {
|
||||
title: 'Inputs',
|
||||
components: [
|
||||
{
|
||||
id: 'textinputcomponent',
|
||||
@@ -30,21 +12,32 @@ const textInputs: ComponentSet = {
|
||||
label: {
|
||||
type: 'static',
|
||||
visible: false,
|
||||
value: 'Title',
|
||||
value: 'Label',
|
||||
fieldType: 'textarea',
|
||||
defaultValue: 'Title'
|
||||
defaultValue: 'Label'
|
||||
}
|
||||
},
|
||||
card: false
|
||||
},
|
||||
{
|
||||
...defaultAlignement,
|
||||
id: 'checkboxcomponent',
|
||||
type: 'checkboxcomponent',
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
visible: true,
|
||||
value: 'Lorem ipsum',
|
||||
fieldType: 'textarea',
|
||||
defaultValue: 'Lorem ipsum'
|
||||
}
|
||||
},
|
||||
componentInput: undefined,
|
||||
card: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const numberInputs: ComponentSet = {
|
||||
title: 'Number Inputs',
|
||||
components: []
|
||||
}
|
||||
|
||||
const buttons: ComponentSet = {
|
||||
title: 'Buttons',
|
||||
components: [
|
||||
@@ -58,7 +51,7 @@ const buttons: ComponentSet = {
|
||||
defaultValue: '',
|
||||
value: ''
|
||||
},
|
||||
recompute: undefined,
|
||||
recomputeIds: undefined,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
@@ -79,9 +72,9 @@ const buttons: ComponentSet = {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
visible: true,
|
||||
value: 'md',
|
||||
value: 'xs',
|
||||
optionValuesKey: 'buttonSizeOptions',
|
||||
defaultValue: 'md'
|
||||
defaultValue: 'xs'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,33 +83,6 @@ const buttons: ComponentSet = {
|
||||
]
|
||||
}
|
||||
|
||||
const selectInputs: ComponentSet = {
|
||||
title: 'Select Inputs',
|
||||
components: [
|
||||
{
|
||||
...defaultAlignement,
|
||||
id: 'checkboxcomponent',
|
||||
type: 'checkboxcomponent',
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
visible: true,
|
||||
value: undefined,
|
||||
fieldType: 'textarea',
|
||||
defaultValue: 'Lorem ipsum'
|
||||
}
|
||||
},
|
||||
componentInput: undefined,
|
||||
card: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const dateTimeInputs: ComponentSet = {
|
||||
title: 'Date and Time Inputs',
|
||||
components: []
|
||||
}
|
||||
|
||||
const dataDisplay: ComponentSet = {
|
||||
title: 'Data Display',
|
||||
components: [
|
||||
@@ -156,6 +122,7 @@ const dataDisplay: ComponentSet = {
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'object',
|
||||
defaultValue: [
|
||||
{
|
||||
id: 1,
|
||||
@@ -197,14 +164,17 @@ const dataDisplay: ComponentSet = {
|
||||
},
|
||||
labels: {
|
||||
type: 'static',
|
||||
value: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum'],
|
||||
value: ['First', 'Second', 'Third'],
|
||||
fieldType: 'array',
|
||||
defaultValue: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum']
|
||||
|
||||
subFieldType: 'text',
|
||||
defaultValue: ['First', 'Second', 'Third']
|
||||
}
|
||||
},
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'number',
|
||||
defaultValue: [25, 50, 25],
|
||||
value: [25, 50, 25]
|
||||
},
|
||||
@@ -225,28 +195,34 @@ const dataDisplay: ComponentSet = {
|
||||
type: 'static',
|
||||
value: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum'],
|
||||
fieldType: 'array',
|
||||
subFieldType: 'text',
|
||||
defaultValue: ['Lorem ipsum', 'Lorem ipsum', 'Lorem ipsum']
|
||||
}
|
||||
},
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'number',
|
||||
defaultValue: [25, 50, 25],
|
||||
value: [25, 50, 25]
|
||||
},
|
||||
card: true
|
||||
},
|
||||
{
|
||||
id: 'displaycomponent',
|
||||
type: 'displaycomponent',
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
defaultValue: 'Lorem Ipsum',
|
||||
value: 'Lorem Ipsum'
|
||||
},
|
||||
configuration: {},
|
||||
card: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const componentSets = [
|
||||
windmillComponents,
|
||||
textInputs,
|
||||
numberInputs,
|
||||
buttons,
|
||||
selectInputs,
|
||||
dateTimeInputs,
|
||||
dataDisplay
|
||||
]
|
||||
const componentSets = [buttons, inputs, dataDisplay]
|
||||
|
||||
export { componentSets }
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { StaticAppInput } from '../../inputType'
|
||||
import { staticValues } from '../componentsPanel/componentStaticValues'
|
||||
import SubTypeEditor from './SubTypeEditor.svelte'
|
||||
|
||||
type ArrayComponentInput = Extract<StaticAppInput, { fieldType: 'array' }>
|
||||
|
||||
export let canHide: boolean = false
|
||||
export let componentInput: ArrayComponentInput
|
||||
|
||||
function addElementByType() {
|
||||
if (componentInput.subFieldType && componentInput.value) {
|
||||
if (componentInput.subFieldType === 'boolean') {
|
||||
componentInput.value.push(false)
|
||||
} else if (componentInput.subFieldType === 'number') {
|
||||
componentInput.value.push(0)
|
||||
} else if (componentInput.subFieldType === 'object') {
|
||||
componentInput.value.push({})
|
||||
} else if (
|
||||
componentInput.subFieldType === 'text' ||
|
||||
componentInput.subFieldType === 'textarea' ||
|
||||
// TODO: Add support for these types
|
||||
componentInput.subFieldType === 'date' ||
|
||||
componentInput.subFieldType === 'time' ||
|
||||
componentInput.subFieldType === 'datetime'
|
||||
) {
|
||||
componentInput.value.push('')
|
||||
} else if (componentInput.subFieldType === 'select') {
|
||||
componentInput.value.push(staticValues[componentInput.optionValuesKey][0])
|
||||
}
|
||||
}
|
||||
componentInput = componentInput
|
||||
}
|
||||
|
||||
function deleteElementByType(index: number) {
|
||||
if (componentInput.value) {
|
||||
componentInput.value.splice(index, 1)
|
||||
componentInput = componentInput
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 flex-col mt-2">
|
||||
{#if componentInput.value}
|
||||
{#each componentInput.value as value, index (index)}
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<SubTypeEditor bind:componentInput bind:value {canHide} />
|
||||
|
||||
<div>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => deleteElementByType(index)}
|
||||
iconOnly
|
||||
startIcon={{ icon: faTrashAlt }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => addElementByType()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ToggleButton, ToggleButtonGroup } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import PickScript from '$lib/components/flows/pickers/PickScript.svelte'
|
||||
import {
|
||||
faArrowRight,
|
||||
faBolt,
|
||||
faClose,
|
||||
faCode,
|
||||
faMinimize,
|
||||
faPen,
|
||||
faTrashAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
@@ -22,24 +22,32 @@
|
||||
import type { AppComponent, AppEditorContext } from '../../types'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
|
||||
import PickFlow from './PickFlow.svelte'
|
||||
import gridHelp from 'svelte-grid/build/helper/index.mjs'
|
||||
import PickInlineScript from './PickInlineScript.svelte'
|
||||
import TableActions from './TableActions.svelte'
|
||||
import { gridColumns } from '../../gridUtils'
|
||||
import StaticInputEditor from './StaticInputEditor.svelte'
|
||||
import ConnectedInputEditor from './ConnectedInputEditor.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import { fieldTypeToTsType } from '../../utils'
|
||||
import Recompute from './Recompute.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import RunnableSelector from './mainInput/RunnableSelector.svelte'
|
||||
|
||||
export let component: AppComponent | undefined
|
||||
export let onDelete: (() => void) | undefined = undefined
|
||||
|
||||
const { app, staticOutputs } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, staticOutputs, runnableComponents } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function removeGridElement() {
|
||||
if (onDelete && component) {
|
||||
delete $staticOutputs[component.id]
|
||||
$staticOutputs = $staticOutputs
|
||||
|
||||
delete $runnableComponents[component.id]
|
||||
$runnableComponents = $runnableComponents
|
||||
|
||||
onDelete()
|
||||
// Delete static inputs
|
||||
} else {
|
||||
@@ -53,6 +61,9 @@
|
||||
// Delete static inputs
|
||||
delete $staticOutputs[component.id]
|
||||
$staticOutputs = $staticOutputs
|
||||
|
||||
delete $runnableComponents[component.id]
|
||||
$runnableComponents = $runnableComponents
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,9 +73,16 @@
|
||||
<div class="flex flex-col w-full divide-y">
|
||||
{#if component.componentInput}
|
||||
<PanelSection title="Main input">
|
||||
<div class="flex flex-col w-full gap-2 my-2">
|
||||
<svelte:fragment slot="action">
|
||||
<Badge color="blue">
|
||||
{component.componentInput.fieldType === 'array' && component.componentInput.subFieldType
|
||||
? `${capitalize(fieldTypeToTsType(component.componentInput.subFieldType))}[]`
|
||||
: capitalize(fieldTypeToTsType(component.componentInput.fieldType))}
|
||||
</Badge>
|
||||
</svelte:fragment>
|
||||
<div class="w-full">
|
||||
<ToggleButtonGroup bind:selected={component.componentInput.type}>
|
||||
<ToggleButton position="left" value="static" startIcon={{ icon: faBolt }} size="xs">
|
||||
<ToggleButton position="left" value="static" startIcon={{ icon: faPen }} size="xs">
|
||||
Static
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
@@ -73,13 +91,14 @@
|
||||
startIcon={{ icon: faArrowRight }}
|
||||
size="xs"
|
||||
>
|
||||
Connect
|
||||
Connected
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="runnable" startIcon={{ icon: faCode }} size="xs">
|
||||
Computed
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2 my-2">
|
||||
{#if component.componentInput.type === 'static'}
|
||||
<StaticInputEditor bind:componentInput={component.componentInput} />
|
||||
{:else if component.componentInput.type === 'connected' && component.componentInput !== undefined}
|
||||
@@ -94,90 +113,57 @@
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faClose }}
|
||||
on:click={() => {
|
||||
// @ts-ignore
|
||||
component.componentInput.runnable = undefined
|
||||
if (component?.componentInput?.type === 'runnable') {
|
||||
component.componentInput.runnable = undefined
|
||||
component.componentInput.fields = {}
|
||||
component = component
|
||||
}
|
||||
}}
|
||||
>
|
||||
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={18} />
|
||||
|
||||
<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>
|
||||
<RunnableSelector
|
||||
inlineScripts={Object.keys($app.inlineScripts)}
|
||||
bind:componentInput={component.componentInput}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</PanelSection>
|
||||
{/if}
|
||||
{#if component.componentInput?.type === 'runnable'}
|
||||
<PanelSection title="Runnable inputs">
|
||||
<InputsSpecsEditor bind:inputSpecs={component.componentInput.fields} />
|
||||
{#if component.componentInput?.type === 'runnable' && Object.keys(component.componentInput.fields ?? {}).length > 0}
|
||||
<div class="border w-full">
|
||||
<PanelSection
|
||||
title={`Runnable inputs (${
|
||||
Object.keys(component.componentInput.fields ?? {}).length
|
||||
})`}
|
||||
smallPadding
|
||||
>
|
||||
{#if component.type === 'buttoncomponent'}
|
||||
<Alert title="Button inputs" type="info" size="xs">
|
||||
The runnable inputs of a button component are not settable by the user. They must
|
||||
be defined statically or connected.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<InputsSpecsEditor
|
||||
bind:inputSpecs={component.componentInput.fields}
|
||||
userInputEnabled={component.type !== 'buttoncomponent'}
|
||||
/>
|
||||
</PanelSection>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if component.type === 'buttoncomponent'}
|
||||
<div class="w-full">
|
||||
<Alert size="xs" type="warning" title="Result output">
|
||||
This input is not directly used by the component. It is piped to the component's
|
||||
<code>result</code> output.
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</PanelSection>
|
||||
{/if}
|
||||
|
||||
@@ -193,34 +179,55 @@
|
||||
|
||||
{#if component.verticalAlignment !== undefined}
|
||||
<PanelSection title="Alignment">
|
||||
<div class="w-full text-xs font-bold">Horizontal alignment</div>
|
||||
<svelte:fragment slot="action">
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
if (component) {
|
||||
component.verticalAlignment = 'center'
|
||||
component.horizontalAlignment = 'center'
|
||||
}
|
||||
}}
|
||||
startIcon={{ icon: faMinimize }}
|
||||
>
|
||||
Center
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="w-full text-xs font-semibold">Horizontal alignment</div>
|
||||
|
||||
<ToggleButtonGroup bind:selected={component.horizontalAlignment}>
|
||||
<ToggleButton position="left" value="left" size="xs">
|
||||
<AlignStartHorizontal size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="center" value="center" size="xs">
|
||||
<AlignCenterHorizontal size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="right" size="xs">
|
||||
<AlignEndHorizontal size={14} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<div class="w-full text-xs font-bold">Vertical alignment</div>
|
||||
|
||||
<ToggleButtonGroup bind:selected={component.verticalAlignment}>
|
||||
<ToggleButton position="left" value="top" size="xs">
|
||||
<AlignStartVertical size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="center" value="center" size="xs">
|
||||
<AlignCenterVertical size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="bottom" size="xs">
|
||||
<AlignEndVertical size={14} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<div class="w-full">
|
||||
<ToggleButtonGroup bind:selected={component.horizontalAlignment}>
|
||||
<ToggleButton position="left" value="left" size="xs">
|
||||
<AlignStartVertical size={16} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="center" value="center" size="xs">
|
||||
<AlignCenterVertical size={16} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="right" size="xs">
|
||||
<AlignEndVertical size={16} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="w-full text-xs font-semibold">Vertical alignment</div>
|
||||
<div class="w-full">
|
||||
<ToggleButtonGroup bind:selected={component.verticalAlignment}>
|
||||
<ToggleButton position="left" value="top" size="xs">
|
||||
<AlignStartHorizontal size={16} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="center" value="center" size="xs">
|
||||
<AlignCenterHorizontal size={16} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="bottom" size="xs">
|
||||
<AlignEndHorizontal size={16} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</PanelSection>
|
||||
{/if}
|
||||
{#if component.type === 'buttoncomponent'}
|
||||
<Recompute bind:recomputeIds={component.recomputeIds} ownId={component.id} />
|
||||
{/if}
|
||||
|
||||
<PanelSection title="Danger zone">
|
||||
<Button
|
||||
size="xs"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import { Badge, Button } from '$lib/components/common'
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faArrowRight, faClose } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
|
||||
@@ -30,38 +30,39 @@
|
||||
{#if componentInput.type === 'connected'}
|
||||
{#if componentInput.connection}
|
||||
<div class="flex justify-between w-full">
|
||||
<span class="text-xs font-bold">Status</span>
|
||||
<span class="text-xs">Status</span>
|
||||
<Badge color="green">Connected</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between w-full">
|
||||
<span class="text-xs font-bold">Component</span>
|
||||
<span class="text-xs">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>
|
||||
<span class="text-xs">Path</span>
|
||||
<Badge color="indigo">{componentInput.connection.path}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: faLink }}
|
||||
startIcon={{ icon: faClose }}
|
||||
color="red"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
if (componentInput.type === 'connected') {
|
||||
componentInput.connection = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear connection
|
||||
Disconnect
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="flex justify-between w-full">
|
||||
<span class="text-xs font-bold">Status</span>
|
||||
<Badge color="dark-yellow">Not connected</Badge>
|
||||
<span class="text-xs">Status</span>
|
||||
<Badge color="yellow">Not connected</Badge>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: faLink }}
|
||||
color="dark"
|
||||
endIcon={{ icon: faArrowRight }}
|
||||
color="blue"
|
||||
on:click={() => {
|
||||
if (componentInput.type === 'connected') {
|
||||
$connectingInput = {
|
||||
@@ -72,7 +73,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connect this input to an output
|
||||
Connect
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { ConnectedAppInput, StaticAppInput, UserAppInput } from '../../inputType'
|
||||
import ConnectedInputEditor from './ConnectedInputEditor.svelte'
|
||||
import StaticInputEditor from './StaticInputEditor.svelte'
|
||||
|
||||
export let componentInput: AppInput
|
||||
export let componentInput: StaticAppInput | ConnectedAppInput | UserAppInput
|
||||
export let canHide: boolean = false
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,79 +1,68 @@
|
||||
<script lang="ts">
|
||||
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 { AppInputs } from '../../inputType'
|
||||
import { fieldTypeToTsType, sanitizeInputSpec } from '../../utils'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import { faArrowRight, faPen, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { fieldTypeToTsType } from '../../utils'
|
||||
import InputsSpecEditor from './InputsSpecEditor.svelte'
|
||||
import type { ConnectedAppInput, StaticAppInput, UserAppInput } from '../../inputType'
|
||||
|
||||
export let inputSpecs: AppInputs
|
||||
export let inputSpecs: Record<string, StaticAppInput | ConnectedAppInput | UserAppInput>
|
||||
export let userInputEnabled: boolean = true
|
||||
export let staticOnly: boolean = true
|
||||
|
||||
let openedProp: string | undefined = inputSpecs ? Object.keys(inputSpecs)[0] : undefined
|
||||
export let staticOnly: boolean = false
|
||||
</script>
|
||||
|
||||
{#if inputSpecs}
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
{#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}
|
||||
{#if true}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs font-semibold">{capitalize(inputSpecKey)}</span>
|
||||
|
||||
<div class="flex gap-2 items-center">
|
||||
<Badge color="blue">
|
||||
{input.fieldType === 'array' && input.subFieldType
|
||||
? `${capitalize(fieldTypeToTsType(input.subFieldType))}[]`
|
||||
: capitalize(fieldTypeToTsType(input.fieldType))}
|
||||
</Badge>
|
||||
|
||||
<ToggleButtonGroup bind:selected={inputSpecs[inputSpecKey].type}>
|
||||
<ToggleButton position="left" value="static" startIcon={{ icon: faBolt }} size="xs">
|
||||
Static
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
position="left"
|
||||
value="static"
|
||||
startIcon={{ icon: faPen }}
|
||||
size="xs"
|
||||
iconOnly
|
||||
/>
|
||||
<ToggleButton
|
||||
position={userInputEnabled ? 'center' : 'right'}
|
||||
value="connected"
|
||||
startIcon={{ icon: faLink }}
|
||||
startIcon={{ icon: faArrowRight }}
|
||||
size="xs"
|
||||
>
|
||||
Connect
|
||||
</ToggleButton>
|
||||
iconOnly
|
||||
disabled={staticOnly}
|
||||
/>
|
||||
{#if userInputEnabled}
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value="user"
|
||||
startIcon={{ icon: faUser }}
|
||||
size="xs"
|
||||
>
|
||||
User
|
||||
</ToggleButton>
|
||||
iconOnly
|
||||
disabled={staticOnly && !userInputEnabled}
|
||||
/>
|
||||
{/if}
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
<InputsSpecEditor
|
||||
bind:componentInput={inputSpecs[inputSpecKey]}
|
||||
canHide={userInputEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<InputsSpecEditor
|
||||
bind:componentInput={inputSpecs[inputSpecKey]}
|
||||
canHide={userInputEnabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
|
||||
export let recomputeIds: string[] | undefined = undefined
|
||||
export let ownId: string
|
||||
|
||||
const { runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function onChange(
|
||||
event: Event & {
|
||||
currentTarget: EventTarget & HTMLInputElement
|
||||
},
|
||||
id: string
|
||||
) {
|
||||
if (event.currentTarget.checked) {
|
||||
recomputeIds = [...(recomputeIds ?? []), id]
|
||||
} else {
|
||||
recomputeIds = recomputeIds?.filter((id) => id !== id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<PanelSection title="Recompute">
|
||||
{#if Object.keys($runnableComponents ?? {}).length > 0}
|
||||
<table class="divide-y divide-gray-300 border w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-2 py-2 text-left text-xs font-medium text-gray-500">
|
||||
Component
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500">
|
||||
Recompute
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.keys($runnableComponents ?? {}).filter((id) => id !== ownId) as id}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-4 py-2 text-xs">
|
||||
<Badge color="blue">{id}</Badge>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap px-4 py-2 ">
|
||||
<input type="checkbox" on:change={(event) => onChange(event, id)} />
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<div class="text-xs">No components to recompute</div>
|
||||
{/if}
|
||||
</PanelSection>
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
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'
|
||||
import type { StaticAppInput } from '../../inputType'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import ArrayStaticInputEditor from './ArrayStaticInputEditor.svelte'
|
||||
|
||||
export let componentInput: AppInput | undefined
|
||||
export let componentInput: StaticAppInput | undefined
|
||||
export let canHide: boolean = false
|
||||
</script>
|
||||
|
||||
@@ -29,41 +28,21 @@
|
||||
</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}
|
||||
<div class="border rounded-sm">
|
||||
<SimpleEditor
|
||||
lang="json"
|
||||
code={JSON.stringify(componentInput.value[index], null, 2)}
|
||||
class="few-lines-editor"
|
||||
on:change={(e) => {
|
||||
if (componentInput?.type === 'static' && componentInput.value) {
|
||||
componentInput.value[index] = JSON.parse(e.detail.code)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/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}
|
||||
{:else if componentInput.fieldType === 'object'}
|
||||
<div class="border rounded-sm w-full">
|
||||
<SimpleEditor
|
||||
lang="json"
|
||||
code={JSON.stringify(componentInput.value, null, 2)}
|
||||
class="few-lines-editor"
|
||||
on:change={(e) => {
|
||||
if (componentInput?.type === 'static' && componentInput.value) {
|
||||
componentInput.value = JSON.parse(e.detail.code)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if componentInput.fieldType === 'array'}
|
||||
<ArrayStaticInputEditor bind:componentInput {canHide} />
|
||||
{:else}
|
||||
<input bind:value={componentInput.value} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { StaticAppInput } from '../../inputType'
|
||||
import StaticInputEditor from './StaticInputEditor.svelte'
|
||||
|
||||
export let value: any
|
||||
export let canHide: boolean = false
|
||||
export let componentInput: StaticAppInput
|
||||
|
||||
let fakeComponentInput: StaticAppInput = {
|
||||
...componentInput,
|
||||
value,
|
||||
visible: componentInput.visible,
|
||||
// We don't support array of arrays
|
||||
// @ts-ignore
|
||||
fieldType: componentInput.subFieldType
|
||||
}
|
||||
|
||||
// Bubble up changes to the real componentInput
|
||||
$: fakeComponentInput && (value = fakeComponentInput.value)
|
||||
</script>
|
||||
|
||||
<StaticInputEditor bind:componentInput={fakeComponentInput} {canHide} />
|
||||
@@ -53,7 +53,7 @@
|
||||
defaultValue: '',
|
||||
value: ''
|
||||
},
|
||||
recompute: undefined,
|
||||
recomputeIds: undefined,
|
||||
card: false
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import { classNames } from '$lib/utils'
|
||||
|
||||
export let title: string
|
||||
export let smallPadding: boolean = false
|
||||
</script>
|
||||
|
||||
<div class={classNames('flex flex-col gap-2 items-start p-4')}>
|
||||
<div class={classNames('flex flex-col gap-2 items-start', smallPadding ? 'p-2' : 'p-4')}>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="text-sm font-bold">{title}</div>
|
||||
<div class="text-sm font-extrabold">{title}</div>
|
||||
<slot name="action" />
|
||||
</div>
|
||||
<slot />
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
|
||||
|
||||
export let filter = ''
|
||||
export let inlineScripts: string[] = []
|
||||
|
||||
type Item = { title: string }
|
||||
let filteredItems: (Item & { marked?: string })[] = []
|
||||
$: items = inlineScripts.map((x) => ({ title: x }))
|
||||
$: prefilteredItems = items ?? []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<SearchItems {filter} items={prefilteredItems} bind:filteredItems f={(x) => x.summary} />
|
||||
<div class="w-full flex mt-1 items-center gap-2">
|
||||
<slot />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search inline scripts"
|
||||
bind:value={filter}
|
||||
class="text-2xl grow mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if inlineScripts.length == 0}
|
||||
Should create
|
||||
{:else if filteredItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-200 border rounded-md">
|
||||
{#each filteredItems as item (item)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
|
||||
on:click={() => dispatch('pick', item.title)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<RowIcon kind="script" />
|
||||
|
||||
<div class="w-full text-left font-normal ">
|
||||
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
|
||||
{#if item.marked}
|
||||
{@html item.marked ?? ''}
|
||||
{:else}
|
||||
{item.title ?? ''}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import { faMousePointer } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
|
||||
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
|
||||
import { Building, Globe2 } from 'lucide-svelte'
|
||||
import InlineScriptList from './InlineScriptList.svelte'
|
||||
import type { AppInput } from '$lib/components/apps/inputType'
|
||||
import WorkspaceScriptList from './WorkspaceScriptList.svelte'
|
||||
import WorkspaceFlowList from './WorkspaceFlowList.svelte'
|
||||
|
||||
type Tab = 'hubscripts' | 'hubflows' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
|
||||
|
||||
export let inlineScripts: string[]
|
||||
|
||||
export let componentInput: AppInput
|
||||
let tab: Tab = 'workspacescripts'
|
||||
let filter: string = ''
|
||||
|
||||
let picker: Drawer
|
||||
|
||||
function pickScript(path: string) {
|
||||
if (componentInput.type === 'runnable') {
|
||||
componentInput.runnable = {
|
||||
type: 'runnableByPath',
|
||||
path,
|
||||
runType: 'script'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pickFlow(path: string) {
|
||||
if (componentInput.type === 'runnable') {
|
||||
componentInput.runnable = {
|
||||
type: 'runnableByPath',
|
||||
path,
|
||||
runType: 'flow'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pickInlineScript(inlineScriptName: string) {
|
||||
if (componentInput.type === 'runnable') {
|
||||
componentInput.runnable = {
|
||||
type: 'runnableByName',
|
||||
inlineScriptName
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={picker} size="1000px">
|
||||
<DrawerContent title="Picker" on:close={picker.closeDrawer}>
|
||||
<div>
|
||||
<div class="max-w-6xl">
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab size="sm" value="inlinescripts">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size={18} />
|
||||
Inline Scripts
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="sm" value="workspacescripts">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size={18} />
|
||||
Workspace Scripts
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="sm" value="workspaceflows">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size={18} />
|
||||
Workspace Flows
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="sm" value="hubscripts">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Globe2 size={18} />
|
||||
Hub Scripts
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div class="my-2" />
|
||||
<div class="flex flex-col gap-y-16">
|
||||
<div class="flex flex-col">
|
||||
{#if tab == 'inlinescripts'}
|
||||
<InlineScriptList {inlineScripts} on:pick={(e) => pickInlineScript(e.detail)} />
|
||||
{:else if tab == 'workspacescripts'}
|
||||
<WorkspaceScriptList on:pick={(e) => pickScript(e.detail)} />
|
||||
{:else if tab == 'workspaceflows'}
|
||||
<WorkspaceFlowList on:pick={(e) => pickFlow(e.detail)} />
|
||||
{:else if tab == 'hubscripts'}
|
||||
<PickHubScript bind:filter on:pick={(e) => pickScript(e.detail.path)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Button
|
||||
on:click={() => picker?.openDrawer()}
|
||||
size="sm"
|
||||
spacingSize="md"
|
||||
color="blue"
|
||||
startIcon={{ icon: faMousePointer }}
|
||||
>
|
||||
Pick
|
||||
</Button>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
|
||||
import { FlowService, Script, ScriptService, type Flow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
|
||||
export let filter = ''
|
||||
|
||||
let flows: Flow[] | undefined = undefined
|
||||
let loading: boolean = false
|
||||
let filteredItems: (Flow & { marked?: string })[] = []
|
||||
$: prefilteredItems = flows ?? []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function loadFlow(): Promise<void> {
|
||||
const loadedFlows = await FlowService.listFlows({
|
||||
workspace: $workspaceStore!,
|
||||
perPage: 300
|
||||
})
|
||||
|
||||
flows = loadedFlows
|
||||
loading = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loading = true
|
||||
loadFlow()
|
||||
})
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (emptyString(x.summary) ? x.path : x.summary + ' (' + x.path + ')')}
|
||||
/>
|
||||
<div class="w-full flex mt-1 items-center gap-2">
|
||||
<slot />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search workspace scripts"
|
||||
bind:value={filter}
|
||||
class="text-2xl grow mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if flows}
|
||||
{#if filteredItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-200 border rounded-md">
|
||||
{#each filteredItems as item (item)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
|
||||
on:click={() => dispatch('pick', item.path)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<RowIcon kind="flow" />
|
||||
|
||||
<div class="w-full text-left font-normal ">
|
||||
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
|
||||
{#if item.marked}
|
||||
{@html item.marked ?? ''}
|
||||
{:else}
|
||||
{!item.summary || item.summary.length == 0 ? item.path : item.summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-600 text-xs ">
|
||||
{item.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
|
||||
import { Script, ScriptService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
|
||||
export let filter = ''
|
||||
|
||||
let scripts: Script[] | undefined = undefined
|
||||
let loading: boolean = false
|
||||
let filteredItems: (Script & { marked?: string })[] = []
|
||||
$: prefilteredItems = scripts ?? []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
perPage: 300
|
||||
})
|
||||
|
||||
scripts = loadedScripts
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loading = true
|
||||
loadScripts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (emptyString(x.summary) ? x.path : x.summary + ' (' + x.path + ')')}
|
||||
/>
|
||||
<div class="w-full flex mt-1 items-center gap-2">
|
||||
<slot />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search workspace scripts"
|
||||
bind:value={filter}
|
||||
class="text-2xl grow mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if scripts}
|
||||
{#if filteredItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-200 border rounded-md">
|
||||
{#each filteredItems as item (item)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
|
||||
on:click={() => dispatch('pick', item.path)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<RowIcon kind="script" />
|
||||
|
||||
<div class="w-full text-left font-normal ">
|
||||
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
|
||||
{#if item.marked}
|
||||
{@html item.marked ?? ''}
|
||||
{:else}
|
||||
{!item.summary || item.summary.length == 0 ? item.path : item.summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-600 text-xs ">
|
||||
{item.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -10,6 +10,7 @@ export type InputType =
|
||||
| 'time'
|
||||
| 'datetime'
|
||||
| 'object'
|
||||
| 'array'
|
||||
|
||||
// Connection to an output of another component
|
||||
// defined by the id of the component and the path of the output
|
||||
@@ -39,7 +40,7 @@ export type StaticInput<U> = {
|
||||
|
||||
type RunnableByPath = {
|
||||
path: string
|
||||
runType: 'script' | 'flow'
|
||||
runType: 'script' | 'flow' | 'hubscript'
|
||||
type: 'runnableByPath'
|
||||
}
|
||||
|
||||
@@ -53,16 +54,22 @@ export type Runnable = RunnableByPath | RunnableByName | undefined
|
||||
// Runnable input, set by the developer in the component panel
|
||||
export type ResultInput = {
|
||||
runnable: Runnable
|
||||
fields: AppInputs
|
||||
fields: Record<string, StaticAppInput | ConnectedAppInput>
|
||||
type: 'runnable'
|
||||
}
|
||||
|
||||
type AppInputSpec<T, U> = (StaticInput<U> | ConnectedInput | UserInput<U> | ResultInput) &
|
||||
InputConfiguration<T, U>
|
||||
type AppInputSpec<T extends InputType, U, V extends InputType = never> = (
|
||||
| StaticInput<U>
|
||||
| ConnectedInput
|
||||
| UserInput<U>
|
||||
| ResultInput
|
||||
) &
|
||||
InputConfiguration<T, U, V>
|
||||
|
||||
type InputConfiguration<T, U> = {
|
||||
type InputConfiguration<T extends InputType, U, V extends InputType> = {
|
||||
fieldType: T
|
||||
defaultValue: U
|
||||
subFieldType?: V
|
||||
}
|
||||
|
||||
export type AppInput =
|
||||
@@ -74,12 +81,27 @@ export type AppInput =
|
||||
| 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
|
||||
})
|
||||
| AppInputSpec<'array', string[], 'text'>
|
||||
| AppInputSpec<'array', string[], 'textarea'>
|
||||
| AppInputSpec<'array', number[], 'number'>
|
||||
| AppInputSpec<'array', boolean[], 'boolean'>
|
||||
| AppInputSpec<'array', string[], 'date'>
|
||||
| AppInputSpec<'array', string[], 'time'>
|
||||
| AppInputSpec<'array', string[], 'datetime'>
|
||||
| AppInputSpec<'array', object[], 'object'>
|
||||
| (AppInputSpec<'array', string[], 'select'> & {
|
||||
optionValuesKey: keyof typeof staticValues
|
||||
})
|
||||
|
||||
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
|
||||
export type ConnectedAppInput = Extract<AppInput, { type: 'connected' }>
|
||||
export type UserAppInput = Extract<AppInput, { type: 'user' }>
|
||||
export type ResultAppInput = Extract<AppInput, { type: 'runnable' }>
|
||||
|
||||
export type AppInputs = Record<string, AppInput>
|
||||
|
||||
@@ -2,7 +2,13 @@ import type { Schema } from '$lib/common'
|
||||
import type { Preview } from '$lib/gen'
|
||||
import type { FilledItem } from 'svelte-grid'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { AppInput, ConnectedInput } from './inputType'
|
||||
import type {
|
||||
AppInput,
|
||||
ConnectedAppInput,
|
||||
ConnectedInput,
|
||||
StaticAppInput,
|
||||
UserAppInput
|
||||
} from './inputType'
|
||||
import type { World } from './rx'
|
||||
|
||||
type BaseComponent<T extends string> = {
|
||||
@@ -12,7 +18,7 @@ type BaseComponent<T extends string> = {
|
||||
export type TextComponent = BaseComponent<'textcomponent'>
|
||||
export type TextInputComponent = BaseComponent<'textinputcomponent'>
|
||||
export type ButtonComponent = BaseComponent<'buttoncomponent'> & {
|
||||
recompute: string[] | undefined
|
||||
recomputeIds: string[] | undefined
|
||||
}
|
||||
|
||||
export type RunFormComponent = BaseComponent<'runformcomponent'>
|
||||
@@ -40,7 +46,7 @@ export type Aligned = {
|
||||
export interface BaseAppComponent extends Partial<Aligned> {
|
||||
id: ComponentID
|
||||
componentInput: AppInput | undefined
|
||||
configuration: Record<string, AppInput>
|
||||
configuration: Record<string, StaticAppInput | ConnectedAppInput | UserAppInput>
|
||||
card: boolean | undefined
|
||||
// TODO: add min/max width/height
|
||||
}
|
||||
@@ -107,6 +113,7 @@ export type AppEditorContext = {
|
||||
mode: Writable<EditorMode>
|
||||
connectingInput: Writable<ConnectingInput>
|
||||
breakpoint: Writable<EditorBreakpoint>
|
||||
runnableComponents: Writable<Record<string, () => void>>
|
||||
}
|
||||
|
||||
export type EditorMode = 'dnd' | 'preview'
|
||||
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
Type
|
||||
} from 'lucide-svelte'
|
||||
import type { InputType } from 'zlib'
|
||||
import type { AppInput, AppInputs } from './inputType'
|
||||
import type { AppInputs } from './inputType'
|
||||
import type { AppComponent } from './types'
|
||||
|
||||
export async function loadSchema(
|
||||
workspace: string,
|
||||
path: string,
|
||||
runType: 'script' | 'flow'
|
||||
runType: 'script' | 'flow' | 'hubscript'
|
||||
): Promise<Schema> {
|
||||
if (runType === 'script') {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
@@ -30,13 +30,21 @@ export async function loadSchema(
|
||||
})
|
||||
|
||||
return script.schema
|
||||
} else {
|
||||
} else if (runType === 'flow') {
|
||||
const flow = await FlowService.getFlowByPath({
|
||||
workspace,
|
||||
path
|
||||
})
|
||||
|
||||
return flow.schema
|
||||
} else {
|
||||
const script = await ScriptService.getHubScriptByPath({
|
||||
path
|
||||
})
|
||||
|
||||
debugger
|
||||
|
||||
return script.schema
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +63,7 @@ export function schemaToInputsSpec(schema: Schema): AppInputs {
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const displayData: Record<AppComponent['type'], {name: string, icon: any}> = {
|
||||
export const displayData: Record<AppComponent['type'], { name: string; icon: any }> = {
|
||||
displaycomponent: {
|
||||
name: 'Result',
|
||||
icon: Monitor
|
||||
@@ -130,68 +138,17 @@ export function accessPropertyByPath<T>(object: T, path: string): T | undefined
|
||||
return object
|
||||
}
|
||||
|
||||
export function fieldTypeToTsType(InputType: InputType): string {
|
||||
switch (InputType) {
|
||||
export function fieldTypeToTsType(inputType: InputType): string {
|
||||
switch (inputType) {
|
||||
case 'number':
|
||||
return 'number'
|
||||
case 'boolean':
|
||||
return 'boolean'
|
||||
case 'object':
|
||||
return 'object'
|
||||
case 'array':
|
||||
return 'array'
|
||||
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
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
export let marked: string | undefined
|
||||
export let starred: boolean
|
||||
export let canFavorite: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -22,7 +23,10 @@
|
||||
}[kind]
|
||||
</script>
|
||||
|
||||
<a class="hover:bg-gray-50 cursor-pointer w-full flex items-center p-4 gap-4 {color}" {href}>
|
||||
<a
|
||||
class="hover:bg-gray-50 cursor-pointer w-full flex items-center p-4 gap-4 {color} rounded-md"
|
||||
{href}
|
||||
>
|
||||
<RowIcon {kind} />
|
||||
|
||||
<div class="w-full">
|
||||
@@ -44,15 +48,17 @@
|
||||
<div class="flex gap-1 items-center justify-end">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
<div class="text-left text-sm font-semibold text-gray-900">
|
||||
<Star
|
||||
{kind}
|
||||
{path}
|
||||
{starred}
|
||||
workspace_id={workspaceId}
|
||||
on:starred={() => {
|
||||
dispatch('change')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if canFavorite}
|
||||
<div class="text-left text-sm font-semibold text-gray-900">
|
||||
<Star
|
||||
{kind}
|
||||
{path}
|
||||
{starred}
|
||||
workspace_id={workspaceId}
|
||||
on:starred={() => {
|
||||
dispatch('change')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
@@ -88,11 +88,11 @@
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}</span
|
||||
>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="font-normal text-xs text-left italic overflow-hidden"
|
||||
>{path ?? ''}</span
|
||||
>
|
||||
>{path ?? ''}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs font-light italic text-left">{description ?? ''}</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{#each filteredItems as item (item)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center"
|
||||
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
|
||||
on:click={() => dispatch('pick', item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -44,7 +44,7 @@
|
||||
{#each filteredItems.slice(0, maxItems) as item (item.path)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow hover:bg-gray-50 bg-white transition-all items-center"
|
||||
class="p-4 gap-4 flex flex-row grow hover:bg-gray-50 bg-white transition-all items-center rounded-md"
|
||||
on:click={() => dispatch('pick', item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
import CreateActionsScript from '$lib/components/scripts/CreateActionsScript.svelte'
|
||||
import { getScriptByPath } from '$lib/utils'
|
||||
import type { HubItem } from '$lib/components/flows/pickers/model'
|
||||
import { faCodeFork, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCodeFork } from '@fortawesome/free-solid-svg-icons'
|
||||
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
|
||||
import PickHubFlow from './PickHubFlow.svelte'
|
||||
import PickHubFlow from '../lib/components/flows/pickers/PickHubFlow.svelte'
|
||||
import FlowViewer from '$lib/components/FlowViewer.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import { Building, Globe2 } from 'lucide-svelte'
|
||||
|
||||
Reference in New Issue
Block a user