App bugfix (#1049)

* adapt static adapter to cloudflare

* fix(frontend): Various bug fix

* fix typecheck

* numerous fixes

* fix all connects

* fix all connects

* more improvements

* more improvements

* everything work?

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Faton Ramadani
2022-12-29 06:47:53 -05:00
committed by GitHub
parent faa046a3fd
commit fb395655e6
54 changed files with 752 additions and 479 deletions
+2 -2
View File
@@ -37,7 +37,7 @@ declare module 'svelte-grid' {
export interface Props<T> {
fillSpace?: boolean
items: FilledItem<T>[]
items: FilledItem<T>[],
rowHeight: number
cols: [number, number][]
gap?: [number, number]
@@ -59,7 +59,7 @@ declare module 'svelte-grid' {
pointerup: CustomEvent<{ id: string }>
},
Slots<T>
> {}
> { }
}
declare module 'svelte-grid/build/helper/index.mjs' {
@@ -27,6 +27,7 @@
export let pickForField: string | undefined = undefined
export let variableEditor: VariableEditor | undefined = undefined
export let itemPicker: ItemPicker | undefined = undefined
export let noVariablePicker = false
let monaco: SimpleEditor | undefined = undefined
let monacoTemplate: TemplateEditor | undefined = undefined
@@ -16,6 +16,9 @@
const dispatch = createEventDispatcher()
export let schema: Schema = emptySchema()
if (!schema) {
schema = emptySchema()
}
let schemaModal: SchemaModal
let schemaString: string = ''
+39 -35
View File
@@ -25,6 +25,7 @@
export let shouldHideNoInputs: boolean = false
export let compact = false
export let password: string | undefined = undefined
export let noVariablePicker = false
let clazz: string = ''
export { clazz as class }
@@ -48,8 +49,8 @@
$: schema?.properties && removeExtraKey()
let pickForField: string | undefined
let itemPicker: ItemPicker
let variableEditor: VariableEditor
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
</script>
<div class="w-full {clazz}">
@@ -66,6 +67,7 @@
bind:extraLib
{variableEditor}
{itemPicker}
{noVariablePicker}
bind:pickForField
/>
{:else if typeof args == 'object'}
@@ -102,40 +104,42 @@
{/if}
</div>
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
if (inputTransform) {
args[pickForField].value = '$var:' + path
} else {
args[pickForField] = '$var:' + path
{#if !noVariablePicker}
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
if (inputTransform) {
args[pickForField].value = '$var:' + path
} else {
args[pickForField] = '$var:' + path
}
}
}
}}
itemName="Variable"
extraField="name"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
name: x.path,
...x
}))}
>
<div
slot="submission"
class="flex flex-row-reverse w-full p-5 bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
}}
itemName="Variable"
extraField="name"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
name: x.path,
...x
}))}
>
<Button
variant="border"
color="blue"
size="sm"
on:click={() => {
variableEditor?.initNew?.()
}}
<div
slot="submission"
class="flex flex-row-reverse w-full p-5 bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
>
Create a new variable
</Button>
</div>
</ItemPicker>
<Button
variant="border"
color="blue"
size="sm"
on:click={() => {
variableEditor?.initNew?.()
}}
>
Create a new variable
</Button>
</div>
</ItemPicker>
<VariableEditor bind:this={variableEditor} on:create={itemPicker.openDrawer} />
<VariableEditor bind:this={variableEditor} on:create={itemPicker.openDrawer} />
{/if}
@@ -162,7 +162,7 @@
notfound = false
} catch (err) {
intervalId && clearIntervalAsync(intervalId!)
isLoading = false
clearCurrentJob()
if (err.status === 404) {
notfound = true
}
+20 -5
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
export let options: {
@@ -8,20 +9,30 @@
export let checked: boolean = false
export let disabled = false
export let size: 'sm' | 'xs' = 'sm'
const id = (Math.random() + 1).toString(36).substring(10)
const dispatch = createEventDispatcher()
</script>
<span class={$$props.class}>
<label for={id} class="inline-flex items-center mt-2 duration-200 {disabled ? 'grayscale opacity-50' : 'cursor-pointer'}">
<span class="{$$props.class} z-auto">
<label
for={id}
class="inline-flex items-center mt-2 duration-200 {disabled
? 'grayscale opacity-50'
: 'cursor-pointer'}"
>
{#if Boolean(options?.left)}
<span class="mr-2 text-sm font-medium duration-200 {disabled ? 'text-gray-600' : 'text-gray-900'}">
<span
class="mr-2 text-sm font-medium duration-200 {disabled ? 'text-gray-600' : 'text-gray-900'}"
>
{options?.left}
</span>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="relative" on:click|stopPropagation={() => {}}>
<div class="relative" on:pointerdown on:click|stopPropagation>
<input
on:focus
on:click
{disabled}
type="checkbox"
value={false}
@@ -40,7 +51,11 @@
/>
</div>
{#if Boolean(options?.right)}
<span class="ml-2 text-sm font-medium duration-200 {disabled ? 'text-gray-500' : 'text-gray-900'}">
<span
class="ml-2 text-sm font-medium duration-200
{disabled ? 'text-gray-500' : 'text-gray-900'}
{size === 'xs' ? 'text-xs' : 'text-sm'}"
>
{options?.right}
</span>
{/if}
@@ -1,6 +1,6 @@
<script lang="ts">
import { Button, type ButtonType } from '$lib/components/common'
import { faArrowRight, faRefresh, faSpinner } from '@fortawesome/free-solid-svg-icons'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
@@ -17,12 +17,13 @@
export let extraQueryParams: Record<string, any> = {}
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let noWFull = false
export const staticOutputs: string[] = ['loading', 'result']
const { runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let labelValue: string = 'Default label'
let labelValue: string
let color: ButtonType.Color
let size: ButtonType.Size
let runnableComponent: RunnableComponent
@@ -35,6 +36,10 @@
loading: Output<boolean>
}
$: if (outputs?.loading != undefined) {
outputs.loading.set(false, true)
}
$: outputs?.loading.subscribe({
next: (value) => {
isLoading = value
@@ -58,9 +63,15 @@
{extraQueryParams}
autoRefresh={false}
>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<Button
on:click={() => {
on:pointerdown={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
on:click={(e) => {
e?.stopPropagation()
e?.preventDefault()
ownClick = true
runnableComponent?.runComponent()
@@ -50,7 +50,12 @@
const options = {
responsive: true,
animation: false,
maintainAspectRatio: false
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
}
}
$: data = {
@@ -67,7 +72,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.labels} bind:value={labels} />
<RunnableWrapper bind:componentInput {id} bind:result>
<RunnableWrapper autoRefresh bind:componentInput {id} bind:result>
{#if result}
<Bar {data} {options} />
{/if}
@@ -66,7 +66,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.labels} bind:value={labels} />
<RunnableWrapper bind:componentInput {id} bind:result>
<RunnableWrapper autoRefresh bind:componentInput {id} bind:result>
{#if result}
<Pie {data} {options} />
{/if}
@@ -34,6 +34,10 @@
loading: Output<boolean>
}
$: if (outputs?.loading != undefined) {
outputs.loading.set(false, true)
}
$: outputs?.loading.subscribe({
next: (value) => {
isLoading = value
@@ -60,6 +64,10 @@
>
<AlignWrapper {horizontalAlignment}>
<Button
on:pointerdown={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
on:click={() => {
runnableComponent?.runComponent()
@@ -3,6 +3,7 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let noWFull = false
function tailwindHorizontalAlignment(horizontalAlignment) {
switch (horizontalAlignment) {
@@ -27,7 +28,8 @@
}
$: classes = classNames(
'flex w-full',
'flex z-auto',
noWFull ? '' : 'w-full',
tailwindHorizontalAlignment(horizontalAlignment),
tailwindVerticalAlignment(verticalAlignment),
verticalAlignment ? 'h-full' : ''
@@ -10,16 +10,26 @@
export let input: AppInput
export let value: T
export let id: string | undefined = undefined
export let row: Record<string, any> = {}
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: input && setDefault()
$: state = $worldStore?.state
$: input && $worldStore && handleConnection()
$: input && $worldStore && row && handleConnection()
$: input && $state && input.type == 'template' && setValue()
function setDefault() {
if (!value && input.defaultValue) {
value = input.defaultValue
}
}
function handleConnection() {
if (input.type === 'connected') {
$worldStore?.connect<any>(input, onValueChange, value)
$worldStore?.connect<any>(input, onValueChange)
} else if (input.type === 'row') {
value = row[input.column]
} else if (input.type === 'static' || input.type == 'template') {
setValue()
} else {
@@ -93,9 +103,11 @@
const hasSubPath = ['.', '['].some((x) => path.includes(x))
if (hasSubPath) {
// Must remove top level property from path
// Which was manually added, i.e. result
const realPath = path.split('.').slice(1).join('.')
const realPath = path
.replace(/\[(\w+)\]/g, '.$1')
.split('.')
.slice(1)
.join('.')
value = accessPropertyByPath<T>(newValue, realPath)
} else {
@@ -13,9 +13,14 @@
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: outputs = $worldStore?.outputsById[id] as {
loading: Output<boolean>
result: Output<any>
}
$: if (outputs?.loading != undefined) {
outputs.loading.set(false, true)
}
function setOutput() {
if (outputs) {
outputs.result?.set(result)
@@ -6,6 +6,7 @@
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import { AppService, type CompletedJob } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { emptySchema } from '$lib/utils'
import { getContext, onMount } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
@@ -39,16 +40,15 @@
let testIsLoading = false
let runnableInputValues: Record<string, any> = {}
$: mergedArgs = { ...extraQueryParams, ...runnableInputValues, ...args }
function setStaticInputsToArgs() {
let nargs = {}
Object.entries(fields ?? {}).forEach(([key, value]) => {
if (value.type === 'static') {
args[key] = value.value
nargs[key] = value.value
}
})
args = args
args = nargs
}
$: fields && setStaticInputsToArgs()
@@ -76,7 +76,10 @@
return areAllArgsValid
}
$: isValid = argMergedArgsValid(mergedArgs, testJobLoader)
$: isValid = argMergedArgsValid(
{ ...extraQueryParams, ...runnableInputValues, ...args },
testJobLoader
)
// Test job internal state
let testJob: CompletedJob | undefined = undefined
@@ -87,12 +90,16 @@
loading: Output<boolean>
}
$: if (outputs?.loading != undefined) {
outputs.loading.set(false, true)
}
async function loadSchemaFromTriggerable(
workspace: string,
path: string,
runType: 'script' | 'flow' | 'hubscript'
): Promise<Schema> {
return loadSchema(workspace, path, runType)
return loadSchema(workspace, path, runType) ?? emptySchema()
}
$: runnable && loadSchemaAndInputsByName()
@@ -181,7 +188,7 @@
Object.keys(inputs ?? {}).forEach((key: string) => {
const input = inputs[key]
if (input.type === 'static' && !input.visible && schemaStripped !== undefined) {
if (input.type === 'static' && schemaStripped !== undefined) {
delete schemaStripped.properties[key]
}
@@ -223,7 +230,7 @@
await testJobLoader?.abstractRun(() => {
const requestBody = {
args: mergedArgs,
args: { ...extraQueryParams, ...args, ...runnableInputValues },
force_viewer_static_fields: {}
}
@@ -256,7 +263,12 @@
</script>
{#each Object.keys(fields ?? {}) as key}
<InputValue {id} input={fields[key]} bind:value={runnableInputValues[key]} />
<InputValue
{id}
input={fields[key]}
bind:value={runnableInputValues[key]}
row={extraQueryParams['row'] ?? {}}
/>
{/each}
<TestJobLoader
@@ -274,7 +286,14 @@
<div class="h-full flex flex-col">
{#if schemaStripped !== undefined && (autoRefresh || forceSchemaDisplay)}
<SchemaForm schema={schemaStripped} bind:args {isValid} {disabledArgs} shouldHideNoInputs />
<SchemaForm
schema={schemaStripped}
bind:args
{isValid}
{disabledArgs}
shouldHideNoInputs
noVariablePicker
/>
{/if}
{#if !runnable && autoRefresh}
@@ -31,6 +31,10 @@
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
bind:value
options={{ right: labelValue }}
on:change={(e) => {
@@ -26,7 +26,7 @@
$: result = [] as Array<Record<string, any>>
let search: 'Frontend' | 'Backend' | 'Disabled' = 'Disabled'
let search: 'Runnable' | 'Component' | 'Disabled' = 'Disabled'
let searchValue = ''
let pagination: boolean | undefined = undefined
@@ -86,9 +86,9 @@
let filteredResult: Array<Record<string, any>> = []
$: filteredResult && setOptions(filteredResult)
$: extraQueryParams = search === 'Backend' ? { search: searchValue, page } : { page, search: '' }
$: search === 'Frontend' && (filteredResult = searchInResult(result, searchValue))
$: (search === 'Backend' || search === 'Disabled') && (filteredResult = result)
$: extraQueryParams = search === 'Runnable' ? { search: searchValue, page } : { page, search: '' }
$: search === 'Runnable' && (filteredResult = searchInResult(result, searchValue))
$: (search === 'Runnable' || search === 'Disabled') && (filteredResult = result)
$: outputs = $worldStore?.outputsById[id] as {
selectedRow: Output<any>
}
@@ -149,10 +149,12 @@
? 'divide-blue-200 hover:divide-blue-300'
: 'divide-gray-200'
)}
on:click={() => toggleRow(row, rowIndex)}
>
{#each row.getVisibleCells() as cell, index (index)}
<td class="p-4 whitespace-nowrap text-xs text-gray-900">
<td
on:click={() => toggleRow(row, rowIndex)}
class="p-4 whitespace-nowrap text-xs text-gray-900"
>
<svelte:component
this={flexRender(cell.column.columnDef.cell, cell.getContext())}
/>
@@ -160,11 +162,15 @@
{/each}
{#if actionButtons.length > 0}
<td class="flex flex-row gap-2 p-4">
<td
class="flex w-full flex-row gap-2 p-4"
on:click={() => toggleRow(row, rowIndex)}
>
{#each actionButtons as actionButton, actionIndex (actionIndex)}
<AppButton
noWFull
{...actionButton}
extraQueryParams={{ row: row.original, rowIndex }}
extraQueryParams={{ row: row.original }}
bind:componentInput={actionButton.componentInput}
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
/>
@@ -14,7 +14,8 @@
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
let input: HTMLInputElement
let labelValue: string = 'Title'
let placeholder: string | undefined = undefined
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string>
@@ -25,14 +26,18 @@
}
</script>
<InputValue {id} input={configuration.label} bind:value={labelValue} />
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
<AlignWrapper {verticalAlignment}>
<input
on:focus={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
type={inputType}
bind:this={input}
on:input={handleInput}
placeholder="Type..."
{placeholder}
class="h-full"
/>
</AlignWrapper>
@@ -17,7 +17,7 @@
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
import { Tab } from '$lib/components/common'
import { Alert, Button, Tab } from '$lib/components/common'
import ComponentList from './componentsPanel/ComponentList.svelte'
import Icon from 'svelte-awesome'
import { faPlus, faSliders } from '@fortawesome/free-solid-svg-icons'
@@ -31,6 +31,7 @@
import TablePanel from './TablePanel.svelte'
import { grid } from 'd3-dag'
import SettingsPanel from './SettingsPanel.svelte'
import { fly } from 'svelte/transition'
export let app: App
export let path: string
@@ -54,6 +55,7 @@
worldStore,
staticOutputs,
app: appStore,
lazyGrid: writable([]),
selectedComponent,
mode,
connectingInput,
@@ -95,51 +97,90 @@
<Pane size={55}>
<SplitPanesWrapper horizontal>
<Pane size={70}>
<div class={classNames('bg-gray-100 mx-auto relative min-h-full', width)}>
{#if $appStore.grid}
<div class={classNames('mx-auto p-4 w-full h-full bg-gray-100', width)}>
<GridEditor />
</div>
{/if}
<div class="bg-gray-100 w-full p-4 h-full overflow-auto">
<div class={classNames('bg-gray-100 mx-auto relative min-h-full', width)}>
{#if $appStore.grid}
<div class={classNames('w-full p-2 h-full bg-white', width)}>
<GridEditor />
</div>
{/if}
{#if $connectingInput.opened}
<div
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
/>
{/if}
</div>
</div>
</Pane>
<Pane size={30}>
<div class="relative h-full w-full">
<InlineScriptsPanel />
{#if $connectingInput.opened}
<div
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
/>
{/if}
</div>
</Pane>
<Pane size={30}>
<InlineScriptsPanel />
{/if}</div
>
</Pane>
</SplitPanesWrapper>
</Pane>
<Pane size={25} minSize={20} maxSize={33}>
<Tabs bind:selected={selectedTab}>
<Tab value="insert" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faPlus} />
<span>Insert</span>
<div class="relative">
<Tabs bind:selected={selectedTab}>
<Tab value="insert" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<svelte:fragment slot="content">
<TabContent value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
{:else}
<div class="p-4 text-sm">No component selected.</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
</svelte:fragment>
</Tabs>
{#if $connectingInput.opened}
<div
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
/>
<div
class="fixed top-32 p-2 z-10 flex justify-center items-center"
transition:fly={{ duration: 100, y: -100 }}
>
<Alert title="Connecting" type="info">
<div class="flex gap-2 flex-col">
Click on the output of the component you want to connect to on the left panel.
<div>
<Button
color="blue"
variant="border"
size="xs"
on:click={() => {
$connectingInput.opened = false
$connectingInput.input = undefined
}}
>
Stop connecting
</Button>
</div>
</div>
</Alert>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<svelte:fragment slot="content">
<TabContent value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
{:else}
<div class="p-4 text-sm">No component selected.</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
</svelte:fragment>
</Tabs>
{/if}
</div>
</Pane>
</SplitPanesWrapper>
{/if}
@@ -13,6 +13,7 @@
faMobileAlt
} from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
import { sendUserToast } from '../../../utils'
import type { AppEditorContext, EditorBreakpoint, EditorMode } from '../types'
@@ -57,10 +58,12 @@
<div class="flex gap-4 items-center">
<div>
<ToggleButtonGroup bind:selected={mode}>
<ToggleButton position="left" value="dnd" startIcon={{ icon: faHand }} size="xs">
<ToggleButton position="left" value="dnd" size="xs">
<Icon data={faHand} class="h-3 mr-1" />
Editor
</ToggleButton>
<ToggleButton position="right" value="preview" startIcon={{ icon: faDisplay }} size="xs">
<ToggleButton position="right" value="preview" size="xs">
<Icon data={faDisplay} class="h-4 mr-1" />
Preview
</ToggleButton>
</ToggleButtonGroup>
@@ -34,6 +34,7 @@
setContext<AppEditorContext>('AppEditorContext', {
worldStore,
staticOutputs,
lazyGrid: writable(app.grid),
app: appStore,
selectedComponent,
mode,
@@ -19,26 +19,24 @@
export let component: AppComponent
export let selected: boolean
export let locked: boolean = false
$: shouldDisplayOverlay = selected && $mode !== 'preview'
export let pointerdown: boolean = false
const { staticOutputs, mode, connectingInput } = getContext<AppEditorContext>('AppEditorContext')
</script>
<div class="h-full flex flex-col w-full">
{#if shouldDisplayOverlay}
<ComponentHeader {component} {selected} on:delete on:lock {locked} />
<div class="h-full flex flex-col w-full component">
{#if $mode !== 'preview'}
<ComponentHeader {pointerdown} {component} {selected} on:delete on:lock {locked} />
{/if}
<div
class={classNames(
'border cursor-pointer h-full bg-white',
shouldDisplayOverlay ? 'border-blue-500' : 'border-white',
selected && $mode !== 'preview' ? 'border-blue-500' : 'border-white',
!selected && $mode !== 'preview' && !component.card ? 'border-gray-100' : '',
$mode !== 'preview' && !$connectingInput.opened ? 'hover:border-blue-500' : '',
component.card ? 'p-2' : '',
component.softWrap ? '' : 'overflow-auto',
'relative'
'relative z-auto'
)}
>
{#if component.type === 'displaycomponent'}
@@ -1,49 +1,62 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import type { AppComponent } from '../types'
import { Anchor, Lock, X } from 'lucide-svelte'
import { Anchor, Lock, Move, X } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
export let component: AppComponent
export let selected: boolean
export let locked: boolean = false
export let pointerdown: boolean = false
const dispatch = createEventDispatcher()
</script>
<span
class={classNames(
'text-white px-2 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute -top-5',
selected ? 'bg-indigo-500' : 'bg-gray-500'
'px-2 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute z-50',
selected ? 'bg-indigo-500 text-white' : 'bg-gray-200/60 text-gray-500'
)}
>
{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-8 cursor-pointer',
'bg-gray-600 hover:bg-gray-800'
)}
on:click={() => {
dispatch('lock')
}}
>
{#if locked}
<Anchor size={16} class="text-orange-500" />
{:else}
<Anchor size={16} />
{/if}
</button>
{#if pointerdown || selected}
<button
class={classNames(
'text-white px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute right-8 z-50 cursor-pointer',
' hover:bg-gray-800',
selected ? 'bg-gray-600/90' : 'bg-gray-600/80'
)}
on:click={() => {
dispatch('lock')
}}
>
{#if locked}
<Anchor size={16} class="text-orange-500" />
{:else}
<Anchor size={16} />
{/if}
</button>
{/if}
<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>
{#if selected}
<span
on:mousedown|stopPropagation|capture
class={classNames(
'text-white px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute right-16 z-50 cursor-move',
'bg-gray-600/80'
)}><Move size={16} /></span
>
<button
class={classNames(
'text-white px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute right-0 z-50 cursor-pointer',
'bg-gray-600/80 hover:bg-gray-800'
)}
on:click={() => {
dispatch('delete')
}}
>
<X size={16} />
</button>
{/if}
@@ -1,24 +1,33 @@
<script lang="ts">
import { getContext } from 'svelte'
import { getContext, onDestroy, onMount } from 'svelte'
import type { AppEditorContext } from '../types'
import Grid from 'svelte-grid'
import ComponentEditor from './ComponentEditor.svelte'
import { classNames } from '$lib/utils'
import { columnConfiguration, disableDrag, enableDrag, isFixed, toggleFixed } from '../gridUtils'
import { Alert } from '$lib/components/common'
import { fly } from 'svelte/transition'
import Button from '$lib/components/common/button/Button.svelte'
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
const { selectedComponent, app, mode, connectingInput, staticOutputs, runnableComponents } =
getContext<AppEditorContext>('AppEditorContext')
const {
selectedComponent,
app,
mode,
connectingInput,
staticOutputs,
runnableComponents,
lazyGrid
} = getContext<AppEditorContext>('AppEditorContext')
// The drag is disabled when the user is connecting an input
$: if ($mode === 'preview' || $connectingInput.opened) {
$app.grid.map((gridItem) => disableDrag(gridItem))
} else {
$app.grid.map((gridItem) => enableDrag(gridItem))
$: setAllDrags($mode === 'preview' || $connectingInput.opened)
function setAllDrags(enable: boolean) {
if (enable) {
$app.grid.map((gridItem) => disableDrag(gridItem))
} else {
$app.grid.map((gridItem) => enableDrag(gridItem))
}
}
function removeGridElement(component) {
@@ -58,31 +67,65 @@
$selectedComponent = undefined
}
}
function selectComponent(id: string) {
if (!$connectingInput.opened) {
$selectedComponent = id
}
}
$: $app.grid && recomputeLazyGrid()
let intervalId: NodeJS.Timeout | undefined = undefined
function recomputeLazyGrid() {
{
if (intervalId) {
clearTimeout(intervalId)
}
intervalId = setTimeout(() => {
lazyGrid.set($app.grid)
intervalId = undefined
}, 100)
}
}
let pointerdown = false
const onpointerdown = () => {
pointerdown = true
}
const onpointerup = () => {
pointerdown = false
}
</script>
<div class="bg-white h-full relative">
<div
on:pointerdown={onpointerdown}
on:pointerleave={onpointerup}
on:pointerup={onpointerup}
class="bg-white h-full relative"
>
<RecomputeAllComponents />
<Grid
bind:items={$app.grid}
let:dataItem
rowHeight={32}
rowHeight={46}
cols={columnConfiguration}
fastStart={true}
on:pointerup={({ detail }) => {
if (!$connectingInput.opened) {
$selectedComponent = detail.id
}
}}
on:pointerup={({ detail }) => selectComponent(detail.id)}
gap={[4, 2]}
>
{#each $app.grid as gridComponent (gridComponent.id)}
{#each $lazyGrid as gridComponent (gridComponent.id)}
{#if gridComponent.data.id === dataItem.data.id}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'h-full w-full flex justify-center align-center',
gridComponent.data.card ? 'border border-gray-100' : ''
)}
on:click|preventDefault|capture|once
>
<ComponentEditor
{pointerdown}
bind:component={gridComponent.data}
selected={$selectedComponent === dataItem.data.id}
locked={isFixed(gridComponent)}
@@ -95,32 +138,6 @@
{/if}
{/each}
</Grid>
{#if $connectingInput.opened}
<div
class="fixed top-32 z-10 flex justify-center items-center"
transition:fly={{ duration: 100, y: -100 }}
>
<Alert title="Connecting" type="info">
<div class="flex gap-2 flex-col">
Click on the output of the component you want to connect to on the left panel.
<div>
<Button
color="blue"
variant="border"
size="xs"
on:click={() => {
$connectingInput.opened = false
$connectingInput.input = undefined
}}
>
Stop connecting
</Button>
</div>
</div>
</Alert>
</div>
{/if}
</div>
<style>
@@ -18,16 +18,13 @@
)
loading = false
}
$: disabled = Object.keys($runnableComponents).length === 0
</script>
<Button
size="xs"
btnClasses="m-2 mb-4"
btnClasses="m-2 mb-6"
startIcon={{ icon: faRefresh, classes: classNames(loading ? 'animate-spin' : '', 'mr-2') }}
color="dark"
{disabled}
on:click={onRefresh}
>
Recompute all ({Object.keys($runnableComponents).length})
@@ -4,10 +4,10 @@
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
import TablePanel from './TablePanel.svelte'
const { selectedComponent, app } = getContext<AppEditorContext>('AppEditorContext')
const { selectedComponent, lazyGrid } = getContext<AppEditorContext>('AppEditorContext')
</script>
{#each $app.grid as gridItem (gridItem.data.id)}
{#each $lazyGrid as gridItem (gridItem.data.id)}
{#if gridItem.data.id === $selectedComponent}
<ComponentPanel bind:component={gridItem.data} />
{:else if gridItem.data.type === 'tablecomponent'}
@@ -10,6 +10,7 @@
{#each component.actionButtons as actionButton (actionButton.id)}
{#if actionButton.id === $selectedComponent}
<ComponentPanel
rowColumns
bind:component={actionButton}
onDelete={() => {
component.actionButtons = component.actionButtons.filter((c) => c.id !== actionButton.id)
@@ -18,7 +18,7 @@
function getMinDimensionsByComponent(componentType: AppComponent['type'], column: number): Size {
// Dimensions key formula: <mobile width>:<mobile height>-<desktop width>:<desktop height>
const dimensions: Record<`${number}:${number}-${number}:${number}`, AppComponent['type'][]> = {
'1:2-2:2': [
'1:1-2:1': [
'buttoncomponent',
'textcomponent',
'checkboxcomponent',
@@ -47,7 +47,8 @@
'numberinputcomponent',
'selectcomponent',
'dateinputcomponent',
'passwordinputcomponent'
'passwordinputcomponent',
'buttoncomponent'
].includes(componentType)
) {
return { w: column, h: 1 }
@@ -57,20 +58,7 @@
function addComponent(appComponent: AppComponent) {
const grid = $app.grid ?? []
const id = getNextId(
grid
.map((gridItem) => {
if (gridItem.data.type === 'tablecomponent') {
return [
gridItem.data.id,
...gridItem.data.actionButtons.map((actionButton) => actionButton.id)
]
} else {
return [gridItem.data.id]
}
})
.flat()
)
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
appComponent.id = id
@@ -5,6 +5,6 @@ const buttonColorOptions = [...BUTTON_COLORS]
export const staticValues = {
buttonColorOptions,
buttonSizeOptions: ['xs', 'sm', 'md', 'lg', 'xl'],
tableSearchOptions: ['Frontend', 'Backend', 'Disabled'],
tableSearchOptions: ['Component', 'Runnable', 'Disabled'],
chartThemeOptions: ['theme1', 'theme2', 'theme3']
} as const
@@ -9,7 +9,14 @@ const inputs: ComponentSet = {
id: 'textinputcomponent',
type: 'textinputcomponent',
componentInput: undefined,
configuration: {},
configuration: {
placeholder: {
type: 'static',
value: 'Type...',
fieldType: 'text',
defaultValue: 'Type...'
},
},
card: false
},
{
@@ -36,14 +43,12 @@ const inputs: ComponentSet = {
configuration: {
minDate: {
type: 'static',
visible: false,
value: '',
fieldType: 'date',
defaultValue: ''
},
maxDate: {
type: 'static',
visible: false,
value: '',
fieldType: 'date',
defaultValue: ''
@@ -60,9 +65,8 @@ const inputs: ComponentSet = {
configuration: {
label: {
type: 'static',
visible: true,
value: 'Label',
fieldType: 'textarea',
fieldType: 'text',
defaultValue: 'Label'
}
},
@@ -118,15 +122,13 @@ const buttons: ComponentSet = {
configuration: {
label: {
type: 'static',
visible: true,
value: 'Lorem ipsum',
fieldType: 'textarea',
fieldType: 'text',
defaultValue: 'Lorem ipsum'
},
color: {
fieldType: 'select',
type: 'static',
visible: true,
value: 'blue',
optionValuesKey: 'buttonColorOptions',
defaultValue: 'blue'
@@ -134,7 +136,6 @@ const buttons: ComponentSet = {
size: {
fieldType: 'select',
type: 'static',
visible: true,
value: 'xs',
optionValuesKey: 'buttonSizeOptions',
defaultValue: 'xs'
@@ -158,15 +159,13 @@ const buttons: ComponentSet = {
configuration: {
label: {
type: 'static',
visible: true,
value: 'Submit',
fieldType: 'textarea',
fieldType: 'text',
defaultValue: 'formcomponent'
},
color: {
fieldType: 'select',
type: 'static',
visible: true,
value: 'dark',
optionValuesKey: 'buttonColorOptions',
defaultValue: 'dark'
@@ -174,7 +173,6 @@ const buttons: ComponentSet = {
size: {
fieldType: 'select',
type: 'static',
visible: true,
value: 'xs',
optionValuesKey: 'buttonSizeOptions',
defaultValue: 'xs'
@@ -195,7 +193,6 @@ const display: ComponentSet = {
type: 'textcomponent',
componentInput: {
type: 'static',
visible: true,
value: 'Lorem ipsum',
fieldType: 'textarea',
defaultValue: 'Lorem ipsum'
@@ -218,7 +215,6 @@ const display: ComponentSet = {
type: 'static',
value: false,
fieldType: 'boolean',
visible: true,
defaultValue: false
}
},
@@ -14,6 +14,7 @@
function subscribeToAllOutputs(observableOutputs: Record<string, Output<any>>) {
if (observableOutputs) {
outputs.forEach((output: string) => {
object[output] = undefined
observableOutputs[output]?.subscribe({
next: (value) => {
object[output] = value
@@ -35,50 +35,53 @@
}
</script>
<PanelSection title="Outputs">
{#each Object.entries($staticOutputs) as [componentId, outputs] (componentId)}
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
<div class="flex flex-row justify-between w-full -mb-2 ">
<button
on:click={() => ($selectedComponent = componentId)}
class={classNames(
'px-2 text-2xs py-0.5 font-bold rounded-t-sm w-fit',
$selectedComponent === componentId
? ' bg-indigo-500 text-white'
: 'bg-gray-200 text-gray-500'
)}
>
{componentId}
</button>
<span
class={classNames(
'px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit',
'bg-gray-500 text-white'
)}
>
{getComponentNameById(componentId)}
</span>
</div>
<PanelSection noPadding titlePadding="px-4 pt-2" title="Outputs">
<div class="overflow-auto border-t w-full relative flex flex-col gap-2 px-4">
{#each Object.entries($staticOutputs) as [componentId, outputs] (componentId)}
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
<div class="flex flex-row justify-between w-full -mb-2">
<button
on:click|stopPropagation|preventDefault={$connectingInput.opened
? undefined
: () => ($selectedComponent = componentId)}
class={classNames(
'px-2 text-2xs py-0.5 font-bold rounded-t-sm w-fit',
$selectedComponent === componentId
? ' bg-indigo-500 text-white'
: 'bg-gray-200 text-gray-500'
)}
>
{componentId}
</button>
<span
class={classNames(
'px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit',
'bg-gray-500 text-white'
)}
>
{getComponentNameById(componentId)}
</span>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click={() => ($selectedComponent = componentId)}
class={classNames(
'w-full py-2 border relative',
$selectedComponent === componentId ? 'border border-blue-500 ' : 'cursor-pointer'
)}
>
{#if $selectedComponent === componentId && $connectingInput?.opened}
<div class="absolute top-0 left-0 w-full h-full bg-gray-500 bg-opacity-50 z-10" />
{/if}
<ComponentOutputViewer
{outputs}
{componentId}
on:select={({ detail }) => {
connectInput(componentId, detail)
}}
/>
</div>
{/if}
{/each}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'w-full py-2 border relative',
$selectedComponent === componentId ? 'border border-blue-500 ' : ''
)}
>
{#if $selectedComponent === componentId && $connectingInput?.opened}
<div class="absolute top-0 left-0 w-full h-full bg-gray-500 bg-opacity-50 z-10" />
{/if}
<ComponentOutputViewer
{outputs}
{componentId}
on:select={({ detail }) => {
connectInput(componentId, detail)
}}
/>
</div>
{/if}
{/each}
</div>
</PanelSection>
@@ -1,8 +1,10 @@
<script lang="ts">
import { Drawer, DrawerContent } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import FlowModuleScript from '$lib/components/flows/content/FlowModuleScript.svelte'
import FlowPathViewer from '$lib/components/flows/content/FlowPathViewer.svelte'
import { getScriptByPath } from '$lib/utils'
import { faCodeBranch } from '@fortawesome/free-solid-svg-icons'
import { faCodeBranch, faExternalLinkAlt, faEye, faPen } from '@fortawesome/free-solid-svg-icons'
import type { AppInput, ResultAppInput } from '../../inputType'
import { clearResultAppInput } from '../../utils'
import EmptyInlineScript from './EmptyInlineScript.svelte'
@@ -28,9 +30,18 @@
}
}
let drawerFlowViewer: Drawer
let flowPath: string = ''
// $: inlineScript && (componentInput = componentInput)
</script>
<Drawer bind:this={drawerFlowViewer} size="1200px">
<DrawerContent title="Flow {flowPath}" on:close={drawerFlowViewer.closeDrawer}>
<FlowPathViewer path={flowPath ?? ''} />
</DrawerContent>
</Drawer>
{#if componentInput && componentInput.type == 'runnable'}
{#if componentInput?.runnable?.type === 'runnableByName' && componentInput?.runnable?.name !== undefined}
{#if componentInput.runnable.inlineScript}
@@ -59,25 +70,57 @@
{/if}
{:else if componentInput?.runnable?.type === 'runnableByPath' && componentInput?.runnable?.path}
<div class="p-2 h-full flex flex-col gap-2 ">
<div>
<Button
size="xs"
startIcon={{ icon: faCodeBranch }}
on:click={() => {
if (
componentInput &&
componentInput.type == 'runnable' &&
componentInput.runnable?.type === 'runnableByPath'
) {
fork(componentInput.runnable.path)
}
}}
>
Fork
</Button>
</div>
{#if componentInput.runnable.runType == 'script' || componentInput.runnable.runType == 'hubscript'}
<div>
<Button
size="xs"
startIcon={{ icon: faCodeBranch }}
on:click={() => {
if (
componentInput &&
componentInput.type == 'runnable' &&
componentInput.runnable?.type === 'runnableByPath'
) {
fork(componentInput.runnable.path)
}
}}
>
Fork
</Button>
</div>
{/if}
<div class="border w-full">
<FlowModuleScript path={componentInput.runnable.path} />
{#if componentInput.runnable.runType == 'script' || componentInput.runnable.runType == 'hubscript'}
<FlowModuleScript path={componentInput.runnable.path} />
{:else if componentInput.runnable.runType == 'flow'}
<div class="py-1 flex gap-2 w-full flex-row-reverse">
<Button
size="xs"
startIcon={{ icon: faEye }}
on:click={() => {
flowPath = componentInput?.['runnable']?.path
drawerFlowViewer.openDrawer()
}}>Expand</Button
>
<Button
size="xs"
startIcon={{ icon: faPen }}
endIcon={{ icon: faExternalLinkAlt }}
target="_blank"
href="/flows/edit/{componentInput?.['runnable']?.path}">Edit</Button
>
<Button
size="xs"
startIcon={{ icon: faEye }}
endIcon={{ icon: faExternalLinkAlt }}
target="_blank"
href="/flows/get/{componentInput?.['runnable']?.path}">Details page</Button
>
</div>
<FlowPathViewer path={componentInput.runnable.path} />
{:else}
Unrecognized runType {componentInput.runnable.runType}
{/if}
</div>
</div>
{/if}
@@ -7,7 +7,7 @@
import InlineScriptEditorPanel from './InlineScriptEditorPanel.svelte'
import InlineScriptEditor from './InlineScriptEditor.svelte'
const { app } = getContext<AppEditorContext>('AppEditorContext')
const { lazyGrid, app } = getContext<AppEditorContext>('AppEditorContext')
let selectedScriptComponentId: string | undefined = undefined
</script>
@@ -17,7 +17,7 @@
<InlineScriptsPanelList bind:selectedScriptComponentId />
</Pane>
<Pane size={75}>
{#each $app.grid as gridComponent, index (index)}
{#each $lazyGrid as gridComponent, index (index)}
{#if gridComponent.data.id === selectedScriptComponentId}
<InlineScriptEditorPanel bind:componentInput={gridComponent.data.componentInput} />
{/if}
@@ -7,14 +7,14 @@
export let selectedScriptComponentId: string | undefined = undefined
const { app, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
const { app, selectedComponent, lazyGrid } = getContext<AppEditorContext>('AppEditorContext')
function selectInlineScript(id: string, subId?: string) {
selectedScriptComponentId = subId ? subId : id
function selectInlineScript(id: string) {
selectedScriptComponentId = id
$selectedComponent = selectedScriptComponentId
}
$: runnablesByName = $app.grid.reduce((acc, gridComponent) => {
$: runnablesByName = $lazyGrid.reduce((acc, gridComponent) => {
const component: AppComponent = gridComponent.data
if (component.type === 'tablecomponent') {
@@ -23,8 +23,7 @@
if (actionButton.componentInput.runnable?.type === 'runnableByName') {
acc.push({
name: actionButton.componentInput.runnable.name,
id: gridComponent.id,
subId: actionButton.id
id: actionButton.id
})
}
}
@@ -42,9 +41,9 @@
}
}
return acc
}, [] as { name: string; id: string; subId?: string }[])
}, [] as { name: string; id: string }[])
$: runnablesByPath = $app.grid.reduce((acc, gridComponent) => {
$: runnablesByPath = $lazyGrid.reduce((acc, gridComponent) => {
const component: AppComponent = gridComponent.data
if (component.type === 'tablecomponent') {
@@ -53,8 +52,7 @@
if (actionButton.componentInput.runnable?.type === 'runnableByPath') {
acc.push({
name: actionButton.componentInput.runnable.path,
id: gridComponent.id,
subId: actionButton.id
id: actionButton.id
})
}
}
@@ -72,7 +70,7 @@
}
}
return acc
}, [] as { name: string; id: string; subId?: string }[])
}, [] as { name: string; id: string }[])
// When seleced component changes, update selectedScriptComponentId
$: {
@@ -82,73 +80,72 @@
}
</script>
<PanelSection title="Inline scripts" smallPadding>
<div class="flex flex-col gap-2 w-full">
{#if runnablesByName.length > 0}
<div class="flex gap-2 flex-col ">
{#each runnablesByName as { name, id, subId }, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-sm cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptComponentId === (subId ? subId : id) ? 'border-blue-500 border' : ''
)},"
on:click={() => selectInlineScript(id, subId)}
>
<span class="text-xs truncate">{name}</span>
<div>
<Badge color="dark-indigo">{id}</Badge>
{#if subId}
<Badge color="dark-indigo">{subId}</Badge>
{/if}
<div class="h-full flex flex-col gap-4">
<PanelSection title="Inline scripts" smallPadding>
<div class="flex flex-col gap-2 w-full">
{#if runnablesByName.length > 0}
<div class="flex gap-2 flex-col ">
{#each runnablesByName as { name, id }, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-sm cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptComponentId === id ? 'border-blue-500 border' : ''
)},"
on:click={() => selectInlineScript(id)}
>
<span class="text-xs truncate">{name}</span>
<div>
<Badge color="dark-indigo">{id}</Badge>
</div>
</div>
</div>
{/each}
</div>
{:else}
<div class="text-sm text-gray-500">No inline scripts</div>
{/if}
{/each}
</div>
{:else}
<div class="text-sm text-gray-500">No inline scripts</div>
{/if}
{#if Array.isArray($app.unusedInlineScripts) && $app.unusedInlineScripts.length > 0}
<div class="flex gap-2 flex-col ">
{#each $app.unusedInlineScripts as unusedInlineScript, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptComponentId === '' ? 'bg-blue-100 text-blue-600' : ''
)},"
on:click={() => selectInlineScript(unusedInlineScript.name)}
>
<span class="text-xs truncate">{unusedInlineScript.name}</span>
<Badge color="red">Unused</Badge>
</div>
{/each}
</div>
{/if}
</div>
</PanelSection>
{#if Array.isArray($app.unusedInlineScripts) && $app.unusedInlineScripts.length > 0}
<div class="flex gap-2 flex-col ">
{#each $app.unusedInlineScripts as unusedInlineScript, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptComponentId === '' ? 'bg-blue-100 text-blue-600' : ''
)},"
on:click={() => selectInlineScript(unusedInlineScript.name)}
>
<span class="text-xs truncate">{unusedInlineScript.name}</span>
<Badge color="red">Detached</Badge>
</div>
{/each}
</div>
{/if}
</div>
</PanelSection>
<PanelSection title="Runnable by path" smallPadding>
<div class="flex flex-col gap-2 w-full">
{#if runnablesByPath.length > 0}
<div class="flex gap-2 flex-col ">
{#each runnablesByPath as { name, id }, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptComponentId === id ? 'bg-blue-100 text-blue-600' : ''
)},"
on:click={() => selectInlineScript(id)}
>
<span class="text-xs truncate">{name}</span>
<Badge color="dark-indigo">{id}</Badge>
</div>
{/each}
</div>
{:else}
<div class="text-sm text-gray-500">No inline scripts</div>
{/if}
</div>
</PanelSection>
<PanelSection title="Others" smallPadding>
<div class="flex flex-col gap-2 w-full">
{#if runnablesByPath.length > 0}
<div class="flex gap-2 flex-col ">
{#each runnablesByPath as { name, id }, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptComponentId === id ? 'bg-blue-100 text-blue-600' : ''
)},"
on:click={() => selectInlineScript(id)}
>
<span class="text-xs truncate">{name}</span>
<Badge color="dark-indigo">{id}</Badge>
</div>
{/each}
</div>
{:else}
<div class="text-sm text-gray-500">No items</div>
{/if}
</div>
</PanelSection>
</div>
@@ -7,7 +7,6 @@
type ArrayComponentInput = Extract<StaticAppInput, { fieldType: 'array' }>
export let canHide: boolean = false
export let componentInput: ArrayComponentInput
function addElementByType() {
@@ -46,7 +45,7 @@
{#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} />
<SubTypeEditor bind:componentInput bind:value />
<div>
<Button
@@ -15,11 +15,11 @@
</script>
{#if componentInput.fieldType !== 'any'}
<div class="w-full">
<div class="w-full overflow-x-auto">
<ToggleButtonGroup bind:selected={componentInput.type}>
{#if componentInput.fieldType === 'textarea'}
<ToggleButton position="left" value="template" size="xs" disable={disableStatic}>
{brackets} Templatable
{brackets}&nbsp;<span class="hidden lg:block">Templatable</span>
</ToggleButton>
{:else}
<ToggleButton
@@ -29,7 +29,7 @@
size="xs"
disable={disableStatic}
>
Static
<span class="hidden lg:block"> Static </span>
</ToggleButton>
{/if}
@@ -39,10 +39,10 @@
startIcon={{ icon: faArrowRight }}
size="xs"
>
Connected
<span class="hidden lg:block"> Connected </span>
</ToggleButton>
<ToggleButton position="right" value="runnable" startIcon={{ icon: faCode }} size="xs">
Computed
<span class="hidden lg:block"> Computed </span>
</ToggleButton>
</ToggleButtonGroup>
</div>
@@ -18,10 +18,10 @@
import RunnableInputEditor from './inputEditor/RunnableInputEditor.svelte'
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
import type { Output } from '../../rx'
import { Alert } from '$lib/components/common'
export let component: AppComponent | undefined
export let onDelete: (() => void) | undefined = undefined
export let rowColumns = false
const { app, staticOutputs, runnableComponents, worldStore } =
getContext<AppEditorContext>('AppEditorContext')
@@ -96,6 +96,14 @@ declare const ${k} = ${JSON.stringify(v)};
title={component.componentInput.fieldType === 'any' ? 'Runnable' : 'Component input'}
>
<svelte:fragment slot="action">
<span
class={classNames(
'text-white px-2 text-2xs py-0.5 font-bold rounded-sm w-fit',
'bg-indigo-500'
)}
>
{`${component.id}`}
</span>
{#if component.componentInput.fieldType !== 'any'}
<Badge color="blue">
{component.componentInput.fieldType === 'array' &&
@@ -105,25 +113,9 @@ declare const ${k} = ${JSON.stringify(v)};
</Badge>
{/if}
</svelte:fragment>
<span
class={classNames(
'text-white px-2 text-2xs py-0.5 font-bold rounded-sm w-fit',
'bg-indigo-500'
)}
>
{`Selected component: ${component.id}`}
</span>
<ComponentInputTypeEditor bind:componentInput={component.componentInput} />
{#if onDelete}
<div class="w-full">
<Alert title="Special arguments" size="xs">
The row and the rowIndex are passed as arguments to the runnable.
</Alert>
</div>
{/if}
<div class="flex flex-col w-full gap-2 my-2">
{#if component.componentInput.type === 'static'}
<StaticInputEditor bind:componentInput={component.componentInput} />
@@ -156,6 +148,7 @@ declare const ${k} = ${JSON.stringify(v)};
shouldCapitalize={false}
bind:inputSpecs={component.componentInput.fields}
userInputEnabled={component.type !== 'buttoncomponent'}
{rowColumns}
/>
</PanelSection>
</div>
@@ -170,7 +163,7 @@ declare const ${k} = ${JSON.stringify(v)};
{/if}
{#if component.type === 'tablecomponent' && Array.isArray(component.actionButtons)}
<TableActions bind:components={component.actionButtons} />
<TableActions id={component.id} bind:components={component.actionButtons} />
{/if}
<AlignmentEditor bind:component />
@@ -1,14 +1,36 @@
<script lang="ts">
import type { ConnectedAppInput, StaticAppInput, UserAppInput } from '../../inputType'
import Toggle from '$lib/components/Toggle.svelte'
import type {
ConnectedAppInput,
RowAppInput,
StaticAppInput,
UserAppInput
} from '../../inputType'
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
import RowInputEditor from './inputEditor/RowInputEditor.svelte'
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
export let componentInput: StaticAppInput | ConnectedAppInput | UserAppInput
export let canHide: boolean = false
export let componentInput: StaticAppInput | ConnectedAppInput | UserAppInput | RowAppInput
export let userInputEnabled: boolean = false
</script>
{#if componentInput.type === 'static'}
<StaticInputEditor bind:componentInput {canHide} />
{:else if componentInput.type === 'connected'}
{#if componentInput.type === 'connected'}
<ConnectedInputEditor bind:componentInput />
{:else if componentInput.type === 'row'}
<RowInputEditor bind:componentInput />
{:else}
{#if userInputEnabled}
<Toggle
class="-mt-2 -mb-1"
checked={componentInput.type === 'user'}
on:change={({ detail }) => {
componentInput.type = detail ? 'user' : 'static'
}}
options={{ right: 'User input' }}
size="xs"
/>
{/if}
{#if componentInput.type === 'static'}
<StaticInputEditor bind:componentInput />
{/if}
{/if}
@@ -1,15 +1,24 @@
<script lang="ts">
import { Badge, ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import { capitalize } from '$lib/utils'
import { faArrowRight, faPen, faUser } from '@fortawesome/free-solid-svg-icons'
import { faArrowRight, faPen, faTableCells, faUser } from '@fortawesome/free-solid-svg-icons'
import { fieldTypeToTsType } from '../../utils'
import InputsSpecEditor from './InputsSpecEditor.svelte'
import type { ConnectedAppInput, StaticAppInput, UserAppInput } from '../../inputType'
import type {
ConnectedAppInput,
RowAppInput,
StaticAppInput,
UserAppInput
} from '../../inputType'
export let inputSpecs: Record<string, StaticAppInput | ConnectedAppInput | UserAppInput>
export let inputSpecs: Record<
string,
StaticAppInput | ConnectedAppInput | UserAppInput | RowAppInput
>
export let userInputEnabled: boolean = true
export let staticOnly: boolean = false
export let shouldCapitalize: boolean = true
export let rowColumns = false
</script>
{#if inputSpecs}
@@ -17,8 +26,8 @@
{#each Object.keys(inputSpecs) as inputSpecKey, index (index)}
{@const input = inputSpecs[inputSpecKey]}
{#if true}
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center gap-1">
<div class="flex flex-col gap-1">
<div class="flex justify-between items-end gap-1">
<span class="text-xs font-semibold">
{shouldCapitalize ? capitalize(inputSpecKey) : inputSpecKey}
</span>
@@ -38,31 +47,31 @@
size="xs"
iconOnly
/>
{#if rowColumns}
<ToggleButton
position="center"
value="row"
startIcon={{ icon: faTableCells }}
size="xs"
iconOnly
disabled={staticOnly}
/>
{/if}
<ToggleButton
position={userInputEnabled && input.format === undefined ? 'center' : 'right'}
position="right"
value="connected"
startIcon={{ icon: faArrowRight }}
size="xs"
iconOnly
disabled={staticOnly}
/>
{#if userInputEnabled && input.format === undefined}
<ToggleButton
position="right"
value="user"
startIcon={{ icon: faUser }}
size="xs"
iconOnly
disabled={staticOnly && !userInputEnabled}
/>
{/if}
</ToggleButtonGroup>
</div>
</div>
<InputsSpecEditor
bind:componentInput={inputSpecs[inputSpecKey]}
canHide={userInputEnabled && input.format === undefined}
userInputEnabled={userInputEnabled && input.format === undefined}
/>
</div>
{/if}
@@ -23,7 +23,7 @@
}
</script>
<PanelSection title="Recompute">
<PanelSection title="Recompute after this compute">
{#if Object.keys($runnableComponents ?? {}).filter((id) => id !== ownId).length > 0}
<table class="divide-y divide-gray-300 border w-full">
<thead class="bg-gray-50">
@@ -1,10 +1,15 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { faClose, faEdit } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { ResultAppInput } from '../../inputType'
import type { AppEditorContext } from '../../types'
import { clearResultAppInput } from '../../utils'
import InlineScriptEditorDrawer from '../inlineScriptsPanel/InlineScriptEditorDrawer.svelte'
const { app } = getContext<AppEditorContext>('AppEditorContext')
export let appInput: ResultAppInput
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
@@ -14,6 +19,17 @@
}
}
function detach() {
if (appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript) {
$app.unusedInlineScripts.push({
name: appInput.runnable.name,
inlineScript: appInput.runnable.inlineScript
})
$app = $app
appInput = clearResultAppInput(appInput)
}
}
function clear() {
appInput = clearResultAppInput(appInput)
}
@@ -25,19 +41,24 @@
bind:inlineScript={appInput.runnable.inlineScript}
/>
{/if}
<div class="flex justify-between w-full items-center gap-1">
<span class="text-xs font-semibold">
<div class="flex flex-col xl:flex-row justify-between w-full items-center gap-1">
<span class="text-xs font-semibold truncate">
{#if appInput.runnable?.type === 'runnableByName'}
{appInput.runnable.name}
{:else if appInput.runnable?.type === 'runnableByPath'}
{appInput.runnable.path}
{/if}
</span>
<div class="flex flex-wrap gap-1">
<div class="flex gap-1">
{#if appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript}
<Button size="xs" color="light" variant="border" startIcon={{ icon: faEdit }} on:click={edit}>
Edit
</Button>
<Button size="xs" color="light" variant="border" on:click={detach}
>Detach <Tooltip
>Detaching an inline script keep it for later to be reused by another component</Tooltip
></Button
>
{/if}
<Button size="xs" color="red" variant="border" startIcon={{ icon: faClose }} on:click={clear}>
Clear
@@ -3,13 +3,11 @@
import StaticInputEditor from './inputEditor/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
@@ -19,4 +17,4 @@
$: fakeComponentInput && (value = fakeComponentInput.value)
</script>
<StaticInputEditor bind:componentInput={fakeComponentInput} {canHide} />
<StaticInputEditor bind:componentInput={fakeComponentInput} />
@@ -3,35 +3,34 @@
import Button from '$lib/components/common/button/Button.svelte'
import { getNextId } from '$lib/components/flows/flowStateUtils'
import { classNames } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
import type { ButtonComponent, AppEditorContext, BaseAppComponent } from '../../types'
import PanelSection from './common/PanelSection.svelte'
import TableActionLabel from './TableActionLabel.svelte'
export let components: (BaseAppComponent & ButtonComponent)[]
export let id: string
const { app, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
const { selectedComponent, staticOutputs } = getContext<AppEditorContext>('AppEditorContext')
function addComponent() {
const grid = $app.grid ?? []
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
const actionId = getNextId(components.map((x) => x.id.split('-')[1]))
const newComponent: BaseAppComponent & ButtonComponent = {
id,
id: `${id}-${actionId}`,
type: 'buttoncomponent',
configuration: {
label: {
type: 'static',
visible: true,
value: 'Action',
fieldType: 'textarea',
fieldType: 'text',
defaultValue: 'Action'
},
color: {
fieldType: 'select',
type: 'static',
visible: true,
value: 'dark',
optionValuesKey: 'buttonColorOptions',
defaultValue: 'dark'
@@ -39,7 +38,6 @@
size: {
fieldType: 'select',
type: 'static',
visible: true,
value: 'xs',
optionValuesKey: 'buttonSizeOptions',
defaultValue: 'xs'
@@ -58,6 +56,13 @@
components = [...components, newComponent]
}
function deleteComponent(cid: string) {
components = components.filter((x) => x.id !== cid)
delete $staticOutputs[cid]
$staticOutputs = $staticOutputs
$selectedComponent = id
}
</script>
<PanelSection title={`Table actions ${components.length > 0 ? `(${components.length})` : ''}`}>
@@ -84,6 +89,11 @@
}}
on:keypress
>
<div>
<Button variant="border" color="red" on:click={() => deleteComponent(component.id)}>
<Icon class="h-3" data={faTrashAlt} />
</Button>
</div>
<div>
<TableActionLabel componentInput={component.configuration.label} />
</div>
@@ -3,11 +3,18 @@
export let title: string
export let smallPadding: boolean = false
export let noPadding: boolean = false
export let titlePadding: string = ''
</script>
<div class={classNames('flex flex-col gap-2 items-start', smallPadding ? 'p-2' : 'p-4')}>
<div
class={classNames(
'flex flex-col h-full gap-2 items-start',
noPadding ? '' : smallPadding ? 'p-2' : 'p-4'
)}
>
<div class="flex justify-between items-center w-full gap-1">
<div class="text-sm font-extrabold">{title}</div>
<div class="text-sm font-extrabold {titlePadding}">{title}</div>
<slot name="action" />
</div>
<slot />
@@ -0,0 +1,9 @@
<script lang="ts">
import type { RowAppInput } from '../../../inputType'
export let componentInput: RowAppInput | undefined
</script>
{#if componentInput}
<input type="text" placeholder="column" bind:value={componentInput.column} />
{/if}
@@ -7,14 +7,9 @@
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
export let componentInput: StaticAppInput | undefined
export let canHide: boolean = false
</script>
{#if componentInput?.type === 'static'}
{#if canHide}
<Toggle bind:checked={componentInput.visible} options={{ right: 'Visible' }} />
{/if}
{#if componentInput.fieldType === 'number'}
<input type="number" bind:value={componentInput.value} />
{:else if componentInput.fieldType === 'textarea'}
@@ -59,8 +54,8 @@
</div>
{/if}
{:else if componentInput.fieldType === 'array'}
<ArrayStaticInputEditor bind:componentInput {canHide} />
<ArrayStaticInputEditor bind:componentInput />
{:else}
<input bind:value={componentInput.value} />
<input type="text" placeholder="Static value" bind:value={componentInput.value} />
{/if}
{/if}
@@ -28,8 +28,7 @@
{#if inlineScripts.length === 0}
<div class="flex flex-col w-full h-full">
<div class="text-md ">No inline scripts</div>
<div class="text-sm">Add inline scripts to your app</div>
<div class="text-md">No detached inline scripts</div>
</div>
{:else if filteredItems.length === 0}
<NoItemFound />
@@ -64,11 +64,12 @@
$app.unusedInlineScripts.splice(unusedInlineScriptIndex, 1)
}
$app = $app
}
function createScript(): string {
let index = 0
let newScriptPath = `inline_script_${index}`
let newScriptPath = `Inline Script ${index}`
const names = $app.grid.reduce((acc, gridItem: GridItem) => {
const { componentInput } = gridItem.data
@@ -83,11 +84,11 @@
return acc
}, [] as string[])
const unusedNames = Object.keys($app.unusedInlineScripts ?? {})
const unusedNames = $app.unusedInlineScripts.map((x) => x.name)
// Find a name that is not used by any other inline script
while (names.includes(newScriptPath) || unusedNames.includes(newScriptPath)) {
newScriptPath = `inline_script_${++index}`
newScriptPath = `Inline Script ${++index}`
}
appInput.runnable = {
@@ -103,14 +104,14 @@
</script>
<Drawer bind:this={picker} size="1000px">
<DrawerContent title="Picker" on:close={picker.closeDrawer}>
<DrawerContent title="Script/Flow 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
Detached Inline Scripts
</div>
</Tab>
<Tab size="sm" value="workspacescripts">
@@ -172,6 +173,6 @@
color="blue"
startIcon={{ icon: faMousePointer }}
>
Select a script
Select a script or flow
</Button>
</div>
+16 -9
View File
@@ -33,11 +33,16 @@ export type UserInput<U> = {
value: U | undefined
}
export type RowInput = {
type: 'row'
column: string
}
// Static input, set by the developer in the component panel
export type StaticInput<U> = {
value: U | undefined
type: 'static'
visible?: boolean | undefined
}
export type TemplateInput = {
@@ -62,7 +67,7 @@ export type Runnable = RunnableByPath | RunnableByName | undefined
// Runnable input, set by the developer in the component panel
export type ResultInput = {
runnable: Runnable
fields: Record<string, StaticAppInput | ConnectedAppInput>
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput>
type: 'runnable'
}
@@ -70,6 +75,7 @@ type AppInputSpec<T extends InputType, U, V extends InputType = never> = (
| StaticInput<U>
| ConnectedInput
| UserInput<U>
| RowInput
| ResultInput
| TemplateInput
) &
@@ -94,11 +100,11 @@ export type AppInput =
| AppInputSpec<'object', Record<string | number, any>>
| AppInputSpec<'object', string>
| (AppInputSpec<'select', string> & {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
/**
* 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'>
@@ -108,9 +114,10 @@ export type AppInput =
| AppInputSpec<'array', string[], 'datetime'>
| AppInputSpec<'array', object[], 'object'>
| (AppInputSpec<'array', string[], 'select'> & {
optionValuesKey: keyof typeof staticValues
})
optionValuesKey: keyof typeof staticValues
})
export type RowAppInput = Extract<AppInput, { type: 'row' }>
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
export type ConnectedAppInput = Extract<AppInput, { type: 'connected' }>
export type UserAppInput = Extract<AppInput, { type: 'user' }>
+8 -7
View File
@@ -19,7 +19,7 @@ export interface Input<T> extends Subscriber<T> {
export type World = {
outputsById: Record<string, Record<string, Output<any>>>
connect: <T>(inputSpec: AppInput, next: (x: T) => void, previousValue: T) => Input<T>
connect: <T>(inputSpec: AppInput, next: (x: T) => void) => Input<T>
state: Writable<number>
}
@@ -50,11 +50,11 @@ export function buildWorld(
export function buildObservableWorld() {
const observables: Record<string, Output<any>> = {}
function connect<T>(inputSpec: AppInput, next: (x: T) => void, previousValue: T): Input<T> {
function connect<T>(inputSpec: AppInput, next: (x: T) => void): Input<T> {
if (inputSpec.type === 'static') {
return {
peak: () => inputSpec.value,
next: () => {}
next: () => { }
}
} else if (inputSpec.type === 'connected') {
const input = cachedInput(next)
@@ -64,13 +64,13 @@ export function buildObservableWorld() {
if (!connection) {
return {
peak: () => undefined,
next: () => {}
next: () => { }
}
}
const { componentId, path } = connection
const [p] = path ? path.split('.') : [undefined]
const [p] = path ? path.split('.')[0].split('[') : [undefined]
let obs = observables[`${componentId}.${p}`]
@@ -78,16 +78,17 @@ export function buildObservableWorld() {
console.warn('Observable at ' + componentId + '.' + p + ' not found')
return {
peak: () => undefined,
next: () => {}
next: () => { }
}
}
obs.subscribe(input)
input.next(obs.peak())
return input
} else if (inputSpec.type === 'user') {
return {
peak: () => inputSpec.value,
next: () => {}
next: () => { }
}
} else {
throw Error('Unknown input type ' + inputSpec)
+2 -1
View File
@@ -126,7 +126,8 @@ export type ConnectingInput = {
export type AppEditorContext = {
worldStore: Writable<World | undefined>
staticOutputs: Writable<Record<string, string[]>>
staticOutputs: Writable<Record<string, string[]>>,
lazyGrid: Writable<GridItem[]>,
app: Writable<App>
selectedComponent: Writable<string | undefined>
mode: Writable<EditorMode>
+1 -1
View File
@@ -183,7 +183,7 @@ export function isScriptByNameDefined(appInput: AppInput | undefined): boolean {
}
if (appInput.type === 'runnable' && appInput.runnable?.type == 'runnableByName') {
return Boolean(appInput.runnable?.name)
return appInput.runnable?.name != undefined
}
return false
@@ -69,7 +69,7 @@
ButtonType.SpacingClasses[spacingSize],
'focus:ring-2 font-semibold',
'duration-200 rounded-md',
'justify-center items-center text-center whitespace-nowrap inline-flex',
'justify-center items-center text-center whitespace-nowrap inline-flex truncate',
btnClasses,
disabled ? 'bg-gray-300' : ''
),
@@ -82,6 +82,7 @@
async function onClick(event: MouseEvent) {
if (!nonCaptureEvent) {
event.preventDefault()
event.stopPropagation()
dispatch('click', event)
if (href) {
if (href.startsWith('http') || target == '_blank') {
@@ -106,6 +107,7 @@
<svelte:element
this={href ? 'a' : 'button'}
bind:this={element}
on:pointerdown
on:click={onClick}
on:focus
on:blur
@@ -89,6 +89,6 @@
{/if}
{:else}
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4], 0.5]} />
<Skeleton layout={[0.5, [4]]} />
{/each}
{/if}