lightarginput for apps forms

This commit is contained in:
Ruben Fiszel
2023-03-16 00:13:09 +01:00
parent 99ec12e10c
commit e27de7fb5d
22 changed files with 389 additions and 102 deletions
+1 -14
View File
@@ -7,7 +7,7 @@
faPlus
} from '@fortawesome/free-solid-svg-icons'
import { setInputCat as computeInputCat, type InputCat } from '$lib/utils'
import { setInputCat as computeInputCat } from '$lib/utils'
import { Badge, Button } from './common'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
@@ -450,19 +450,6 @@
{/if}
</div>
{/if}
{#if !required && inputCat != 'resource-object'}
<!-- <Tooltip placement="bottom" content="Reset to default value">
<Button
on:click={() => (value = undefined)}
{disabled}
color="alternative"
size="sm"
class="h-8"
>
<Icon data={faArrowRotateLeft} />
</Button>
</Tooltip> -->
{/if}
<slot name="actions" />
</div>
{#if !compact || (error && error != '')}
@@ -0,0 +1,331 @@
<script lang="ts">
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
import { setInputCat as computeInputCat } from '$lib/utils'
import { Badge, Button } from './common'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import FieldHeader from './FieldHeader.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import type { SchemaProperty } from '$lib/common'
import autosize from 'svelte-autosize'
import Toggle from './Toggle.svelte'
import Range from './Range.svelte'
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
export let label: string = ''
export let value: any
export let defaultValue: any = undefined
export let description: string = ''
export let format: string = ''
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
export let type: string | undefined = undefined
export let required = false
export let pattern: undefined | string = undefined
export let valid = required ? false : true
export let maxRows = 10
export let enum_: string[] | undefined = undefined
export let itemsType:
| { type?: 'string' | 'number' | 'bytes'; contentEncoding?: 'base64' }
| undefined = undefined
export let displayHeader = true
export let properties: { [name: string]: SchemaProperty } | undefined = undefined
export let extra: Record<string, any> = {}
const dispatch = createEventDispatcher()
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
$: validateInput(pattern, value)
let error: string = ''
let el: HTMLTextAreaElement | undefined = undefined
let rawValue: string | undefined = undefined
$: {
if (rawValue) {
try {
value = JSON.parse(rawValue)
error = ''
} catch (err) {
error = err.toString()
}
}
}
$: {
error = ''
if (inputCat === 'object') {
evalValueToRaw()
validateInput(pattern, value)
}
}
export function evalValueToRaw() {
if (value) {
rawValue = JSON.stringify(value, null, 4)
}
}
function fileChanged(e: any, cb: (v: string | undefined) => void) {
let t = e.target
if (t && 'files' in t && t.files.length > 0) {
let reader = new FileReader()
reader.onload = (e: any) => {
cb(e.target.result.split('base64,')[1])
}
reader.readAsDataURL(t.files[0])
} else {
cb(undefined)
}
}
export function focus() {
el?.focus()
if (el) {
el.style.height = '5px'
el.style.height = el.scrollHeight + 50 + 'px'
}
}
function validateInput(pattern: string | undefined, v: any): void {
if (required && (v == undefined || v == null || v === '')) {
error = 'This field is required'
valid = false
} else {
if (pattern && !testRegex(pattern, v)) {
error = `Should match ${pattern}`
valid = false
} else {
error = ''
valid = true
}
}
}
function testRegex(pattern: string, value: any): boolean {
try {
const regex = new RegExp(pattern)
return regex.test(value)
} catch (err) {
return false
}
}
$: {
if (value == undefined || value == null) {
value = defaultValue
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
value = ''
} else if (inputCat == 'enum') {
value = enum_?.[0]
} else if (inputCat == 'boolean') {
value = false
}
}
}
}
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
</script>
<div class="flex flex-col w-full min-w-[250px]">
<div>
{#if displayHeader}
<FieldHeader {label} {required} {type} {contentEncoding} {format} {itemsType} />
{/if}
{#if description}
<div class="text-sm italic pb-1">
{description}
</div>
{/if}
<div class="flex space-x-1">
{#if inputCat == 'number'}
{#if extra['min'] != undefined && extra['max'] != undefined}
<div class="flex w-full gap-1">
<span>{extra['min']}</span>
<div class="grow">
<Range bind:value min={extra['min']} max={extra['max']} />
</div>
<span>{extra['max']}</span>
<span class="mx-2"><Badge large color="blue">{value}</Badge></span>
</div>
{:else}
<input
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
type="number"
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
placeholder={defaultValue ?? ''}
bind:value
min={extra['min']}
max={extra['max']}
on:input={() => dispatch('input', { value, isRaw: true })}
/>
{/if}
{:else if inputCat == 'boolean'}
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
bind:checked={value}
/>
{#if type == 'boolean' && value == undefined}
<span>&nbsp; Not set</span>
{/if}
{:else if inputCat == 'list'}
<div>
<div>
{#each value ?? [] as v, i}
<div class="flex flex-row max-w-md mt-1">
{#if itemsType?.type == 'number'}
<input type="number" bind:value={v} />
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
multiple={false}
/>
{:else}
<input type="text" bind:value={v} />
{/if}
<Button
variant="border"
color="red"
size="sm"
btnClasses="mx-6"
on:click={() => {
value = value.filter((el) => el != v)
if (value.length == 0) {
value = undefined
}
}}
>
<Icon data={faMinus} />
</Button>
</div>
{/each}
</div>
<Button
variant="border"
color="blue"
size="sm"
btnClasses="mt-1"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
value = value.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
<span class="ml-2">
{(value ?? []).length} item{(value ?? []).length > 1 ? 's' : ''}
</span>
</div>
{:else if inputCat == 'resource-object'}
<ObjectResourceInput {format} bind:value />
{:else if inputCat == 'object'}
{#if properties && Object.keys(properties).length > 0}
<div class="p-4 pl-8 border rounded w-full">
<LightweightSchemaForm
schema={{ properties, $schema: '', required: [], type: 'object' }}
bind:args={value}
/>
</div>
{:else}
<textarea
bind:this={el}
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
use:autosize
style="max-height: {maxHeight}"
on:input={() => {
dispatch('input', { rawValue: value, isRaw: false })
}}
class="col-span-10 {valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
placeholder={defaultValue ? JSON.stringify(defaultValue, null, 4) : ''}
bind:value={rawValue}
/>
{/if}
{:else if inputCat == 'enum'}
<select
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
class="px-6"
bind:value
>
{#each enum_ ?? [] as e}
<option>{e}</option>
{/each}
</select>
{:else if inputCat == 'date'}
<input class="inline-block" type="datetime-local" bind:value />
{:else if inputCat == 'base64'}
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value = val))}
multiple={false}
/>
{:else if inputCat == 'resource-string'}
<ResourcePicker
bind:value
resourceType={format.split('-').length > 1
? format.substring('resource-'.length)
: undefined}
/>
{:else if inputCat == 'string'}
<div class="flex flex-col w-full">
<div class="flex flex-row w-full items- justify-between">
<textarea
rows="1"
bind:this={el}
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
on:blur={() => dispatch('blur')}
use:autosize
type="text"
class="col-span-10 {valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
placeholder={defaultValue ?? ''}
bind:value
on:input={() => {
dispatch('input', { rawValue: value, isRaw: false })
}}
/>
</div>
</div>
{/if}
<slot name="actions" />
</div>
</div>
</div>
@@ -0,0 +1,35 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import LightweightArgInput from './LightweightArgInput.svelte'
export let schema: Schema
export let args: Record<string, any> | undefined = undefined
$: if (args === undefined) {
args = {}
}
</script>
<div class="w-full">
{#each Object.keys(schema.properties ?? {}) as argName, i (argName)}
<div>
{#if typeof args == 'object' && schema?.properties[argName] && args}
<LightweightArgInput
label={argName}
description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
pattern={schema.properties[argName].pattern}
defaultValue={schema.properties[argName].default}
enum_={schema.properties[argName].enum}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
itemsType={schema.properties[argName].items}
extra={schema.properties[argName]}
/>
{/if}
</div>
{/each}
</div>
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { VariableService, type InputTransform } from '$lib/gen'
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
@@ -10,7 +10,7 @@
import VariableEditor from './VariableEditor.svelte'
export let schema: Schema
export let args: Record<string, InputTransform | any> = {}
export let args: Record<string, any> = {}
export let disabledArgs: string[] = []
export let disabled = false
@@ -137,7 +137,6 @@
<InputValue {id} input={configuration.gotoNewTab} bind:value={gotoNewTab} />
<RunnableWrapper
flexWrap
{recomputeIds}
bind:runnableComponent
{componentInput}
@@ -95,7 +95,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.line} bind:value={lineChart} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
{#if lineChart}
@@ -27,7 +27,7 @@
})
</script>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<div
class={twMerge(
'w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm',
@@ -34,15 +34,7 @@
bind:clientHeight={h}
bind:clientWidth={w}
>
<RunnableWrapper
{render}
autoRefresh
flexWrap
{componentInput}
{id}
bind:initializing
bind:result
>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
{#key result}
<iframe
frameborder="0"
@@ -78,7 +78,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.doughnutStyle} bind:value={doughnut} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
{#if doughnut}
@@ -84,7 +84,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
<Scatter {data} {options} />
@@ -72,7 +72,7 @@
<InputValue {id} input={configuration.copyButton} bind:value={copyButton} />
<InputValue {id} input={configuration.fitContent} bind:value={fitContent} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<ResizeWrapper {id} shouldWrap={fitContent}>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if !result || result === ''}
@@ -99,7 +99,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
<Scatter {data} {options} />
@@ -47,7 +47,7 @@
</script>
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
@@ -60,7 +60,7 @@
<InputValue {id} input={configuration.canvas} bind:value={canvas} />
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
@@ -81,7 +81,7 @@
<InputValue {id} input={configuration.pagination} bind:value={pagination} />
<InputValue {id} input={configuration.pageSize} bind:value={pageSize} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
{#if Array.isArray(result) && result.every(isObject)}
<div
class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full"
@@ -142,7 +142,7 @@
<InputValue {id} input={configuration.search} bind:value={search} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
{#if Array.isArray(result) && result.every(isObject)}
<div
class={twMerge(
@@ -2,6 +2,7 @@
import { goto } from '$app/navigation'
import type { Schema } from '$lib/common'
import Alert from '$lib/components/common/alert/Alert.svelte'
import LightweightSchemaForm from '$lib/components/LightweightSchemaForm.svelte'
import Popover from '$lib/components/Popover.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
@@ -24,7 +25,6 @@
export let autoRefresh: boolean = true
export let result: any = undefined
export let forceSchemaDisplay: boolean = false
export let flexWrap = false
export let wrapperClass = ''
export let wrapperStyle = ''
export let initializing: boolean | undefined = undefined
@@ -143,16 +143,6 @@
return schemaStripped as Schema
}
$: disabledArgs = Object.keys(fields ?? {}).reduce(
(disabledArgsAccumulator: string[], inputName: string) => {
if (fields[inputName].type === 'static') {
disabledArgsAccumulator = [...disabledArgsAccumulator, inputName]
}
return disabledArgsAccumulator
},
[]
)
async function executeComponent(noToast = false) {
if (runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend') {
outputs?.loading?.set(true)
@@ -300,14 +290,7 @@
<div class="h-full flex relative flex-row flex-wrap {wrapperClass}" style={wrapperStyle}>
{#if schemaStripped && Object.keys(schemaStripped?.properties ?? {}).length > 0 && (autoRefresh || forceSchemaDisplay)}
<div class="px-2 h-fit min-h-0">
<SchemaForm
{flexWrap}
schema={schemaStripped}
bind:args
{disabledArgs}
shouldHideNoInputs
noVariablePicker
/>
<LightweightSchemaForm schema={schemaStripped} bind:args />
</div>
{/if}
@@ -15,7 +15,6 @@
export let autoRefresh: boolean = true
export let runnableComponent: RunnableComponent | undefined = undefined
export let forceSchemaDisplay: boolean = false
export let flexWrap = false
export let runnableClass = ''
export let runnableStyle = ''
export let goto: string | undefined = undefined
@@ -48,7 +47,6 @@
{recomputeIds}
gotoUrl={goto}
{gotoNewTab}
{flexWrap}
bind:this={runnableComponent}
fields={componentInput.fields}
bind:result
@@ -27,7 +27,8 @@
id={component.id}
shouldCapitalize={false}
bind:inputSpecs={component.componentInput.fields}
userInputEnabled={component.type !== 'buttoncomponent'}
userInputEnabled={component.type === 'formcomponent' ||
component.type === 'formbuttoncomponent'}
{resourceOnly}
/>
{/if}
@@ -1,18 +0,0 @@
<script lang="ts">
import type { ConnectedAppInput, RowAppInput, StaticAppInput, UserAppInput } from '../inputType'
import type { AppComponent } from './component'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let component: AppComponent
export let resourceOnly: boolean = false
$: fields = resourceOnly ? fields : fields
</script>
<InputsSpecsEditor
id={component.id}
shouldCapitalize={false}
bind:inputSpecs={fields}
userInputEnabled={component.type !== 'buttoncomponent'}
/>
@@ -33,15 +33,8 @@
export let noGrid = false
export let duplicateMoveAllowed = true
const {
app,
runnableComponents,
selectedComponent,
worldStore,
focusedGrid,
stateId,
state
} = getContext<AppViewerContext>('AppViewerContext')
const { app, runnableComponents, selectedComponent, worldStore, focusedGrid, stateId, state } =
getContext<AppViewerContext>('AppViewerContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
@@ -149,7 +142,8 @@
id={component.id}
shouldCapitalize={false}
bind:inputSpecs={component.componentInput.fields}
userInputEnabled={component.type !== 'buttoncomponent'}
userInputEnabled={component.type === 'formcomponent' ||
component.type === 'formbuttoncomponent'}
{rowColumns}
/>
</PanelSection>
@@ -11,7 +11,7 @@
export let id: string
export let inputSpecs: BaseAppComponent['configuration']
export let userInputEnabled: boolean = true
export let userInputEnabled: boolean = false
export let shouldCapitalize: boolean = true
export let rowColumns = false
export let resourceOnly = false
@@ -67,21 +67,6 @@
/>
<svelte:fragment slot="text">Static</svelte:fragment>
</Popover>
<!-- {#if rowColumns}
<Popover placement="bottom" notClickable disapperTimoout={0}>
<ToggleButton
position="center"
value="row"
startIcon={{ icon: faTableCells }}
size="xs"
>
<Tooltip scale={0.6} placement="top-end" wrapperClass="center-center">
Use the column name to have the value of the cell be passed to the action
</Tooltip>
</ToggleButton>
<svelte:fragment slot="text">Column</svelte:fragment>
</Popover>
{/if} -->
{#if userInputEnabled && !input.format?.startsWith('resource-')}
<Popover placement="bottom" notClickable disapperTimoout={0}>
<ToggleButton