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:
Faton Ramadani
2022-12-12 14:35:17 +01:00
committed by GitHub
parent 4e633cda4d
commit 05003abdf3
37 changed files with 980 additions and 442 deletions

View File

@@ -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}

View File

@@ -48,7 +48,8 @@
const options = {
responsive: true,
animation: false
animation: false,
maintainAspectRatio: false
}
$: data = {

View File

@@ -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'

View File

@@ -27,7 +27,7 @@
}
$: classes = classNames(
'flex w-full h-full flex-col',
'flex w-full h-full',
tailwindHorizontalAlignment(horizontalAlignment),
tailwindVerticalAlignment(verticalAlignment)
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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"

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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} />

View File

@@ -53,7 +53,7 @@
defaultValue: '',
value: ''
},
recompute: undefined,
recomputeIds: undefined,
card: false
}

View File

@@ -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 />

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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'