mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
feat(frontend): add an eval input component for flow (#1494)
* fix sqlx * all
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
export let itemPicker: ItemPicker | undefined = undefined
|
||||
export let noMargin = false
|
||||
export let extra: Record<string, any> = {}
|
||||
export let minW = true
|
||||
|
||||
let seeEditable: boolean = enum_ != undefined || pattern != undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -141,6 +142,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key == 'Enter') {
|
||||
return
|
||||
}
|
||||
e.stopPropagation()
|
||||
}
|
||||
$: {
|
||||
if (value == undefined || value == null) {
|
||||
value = defaultValue
|
||||
@@ -165,7 +172,7 @@
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<div class="flex flex-col w-full min-w-[250px]">
|
||||
<div class="flex flex-col w-full {minW ? 'min-w-[250px]' : ''}">
|
||||
<div>
|
||||
{#if displayHeader}
|
||||
<FieldHeader {label} {required} {type} {contentEncoding} {format} />
|
||||
@@ -192,7 +199,7 @@
|
||||
use:autosize
|
||||
rows="1"
|
||||
bind:value={description}
|
||||
on:keydown|stopPropagation
|
||||
on:keydown={onKeyDown}
|
||||
placeholder="Field description"
|
||||
/>
|
||||
{#if type == 'string' && format != 'date-time'}
|
||||
@@ -408,7 +415,7 @@
|
||||
dispatch('focus')
|
||||
}}
|
||||
use:autosize
|
||||
on:keydown|stopPropagation
|
||||
on:keydown={onKeyDown}
|
||||
type="text"
|
||||
{disabled}
|
||||
class="col-span-10 {valid
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
</div>
|
||||
{:else if !forceJson && resultKind == 'error'}<div>
|
||||
<span class="text-red-500 font-semibold text-sm whitespace-pre-wrap"
|
||||
>{result.error.name}: {result.error.message}</span
|
||||
>{#if result.error.name || result.error.message}{result.error.name}: {result.error
|
||||
.message}{:else}{JSON.stringify(result.error, null, 4)}{/if}</span
|
||||
>
|
||||
<pre class="text-sm whitespace-pre-wrap text-gray-900">{result.error.stack ?? ''}</pre>
|
||||
</div>
|
||||
@@ -261,7 +262,15 @@
|
||||
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
|
||||
{@const str = JSON.stringify(result, null, 4).replace(/\\n/g, '\n')}
|
||||
{#if str.length > 100000}
|
||||
JSON too large. <a
|
||||
download="{filename ?? 'result'}.json"
|
||||
href="data:text/json;charset=utf-8,{encodeURIComponent(str)}">Download</a
|
||||
>
|
||||
{:else}
|
||||
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Portal>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { ScriptService, type FlowModule, type InputTransform, type Job } from '$lib/gen'
|
||||
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getModifierKey, getScriptByPath } from '$lib/utils'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
@@ -10,11 +10,15 @@
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import RunForm from './RunForm.svelte'
|
||||
import TestJobLoader from './TestJobLoader.svelte'
|
||||
import ModulePreviewForm from './ModulePreviewForm.svelte'
|
||||
import { Kbd } from './common'
|
||||
import { evalValue } from './flows/utils'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
|
||||
export let mod: FlowModule
|
||||
export let schema: Schema
|
||||
export let pickableProperties: PickableProperties | undefined
|
||||
|
||||
const { flowStore, flowStateStore, testStepStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -24,16 +28,12 @@
|
||||
let testIsLoading = false
|
||||
let testJob: Job | undefined = undefined
|
||||
|
||||
let stepArgs: Record<string, any> | undefined =
|
||||
$testStepStore[mod.id] ??
|
||||
Object.entries(mod.value['input_transforms'] ?? {}).reduce((acc, [k, v]) => {
|
||||
let t = v as InputTransform
|
||||
if (t.type == 'static') {
|
||||
acc[k] = t.value
|
||||
return acc
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
let stepArgs: Record<string, any> | undefined = Object.fromEntries(
|
||||
Object.keys(schema.properties).map((k) => [
|
||||
k,
|
||||
evalValue(k, mod, $testStepStore, pickableProperties, false)
|
||||
])
|
||||
)
|
||||
|
||||
$: $testStepStore[mod.id] = stepArgs
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
function jobDone() {
|
||||
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob' && `result` in testJob) {
|
||||
if ($flowStateStore[mod.id]?.previewResult) {
|
||||
if ($flowStateStore[mod.id]) {
|
||||
$flowStateStore[mod.id].previewResult = testJob.result
|
||||
$flowStateStore = $flowStateStore
|
||||
}
|
||||
@@ -80,24 +80,18 @@
|
||||
>
|
||||
{/if}
|
||||
|
||||
<RunForm
|
||||
noVariablePicker
|
||||
loading={testIsLoading}
|
||||
runnable={{ summary: mod.summary ?? '', schema, description: '' }}
|
||||
runAction={(_, args) => runTest(args)}
|
||||
schedulable={false}
|
||||
buttonText={`Test (${getModifierKey()}+Enter)`}
|
||||
detailed={false}
|
||||
topButton
|
||||
bind:args={stepArgs}
|
||||
isFlow={false}
|
||||
/>
|
||||
{#if testIsLoading}
|
||||
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full mt-4" color="red" size="sm">
|
||||
<Loader2 class="animate-spin mr-1" />
|
||||
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="sm">
|
||||
<Loader2 size={16} class="animate-spin mr-1" />
|
||||
Cancel
|
||||
</Button>
|
||||
{:else}
|
||||
<Button btnClasses="w-full truncate" size="sm" on:click={() => runTest(stepArgs)}
|
||||
>Run <Kbd>{getModifierKey()}</Kbd>+<Kbd>Enter</Kbd></Button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<ModulePreviewForm {pickableProperties} {mod} {schema} bind:args={stepArgs} />
|
||||
</Pane>
|
||||
<Pane size={50} minSize={20}>
|
||||
<Splitpanes horizontal>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { Plug } from 'lucide-svelte'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import { evalValue } from './flows/utils'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
|
||||
export let schema: Schema
|
||||
export let args: Record<string, any> = {}
|
||||
export let mod: FlowModule
|
||||
export let pickableProperties: PickableProperties | undefined
|
||||
|
||||
export let isValid: boolean = true
|
||||
export let autofocus = false
|
||||
|
||||
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
$: isValid = allTrue(inputCheck) ?? false
|
||||
|
||||
$: if (args == undefined || typeof args !== 'object') {
|
||||
args = {}
|
||||
}
|
||||
|
||||
function removeExtraKey() {
|
||||
const nargs = {}
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
nargs[key] = args[key]
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
}
|
||||
|
||||
let keys: string[] = []
|
||||
$: {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
|
||||
keys = lkeys
|
||||
removeExtraKey()
|
||||
}
|
||||
}
|
||||
|
||||
function plugIt(argName: string) {
|
||||
args[argName] = evalValue(argName, mod, testStepStore, pickableProperties, true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full pt-4">
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if Object.keys(schema.properties ?? {}).includes(argName)}
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
<ArgInput
|
||||
minW={false}
|
||||
autofocus={i == 0 && autofocus}
|
||||
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}
|
||||
bind:valid={inputCheck[argName]}
|
||||
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>
|
||||
<Button
|
||||
on:click={() => plugIt(argName)}
|
||||
size="sm"
|
||||
variant="border"
|
||||
color="light"
|
||||
title="Eval input component"><Plug size={14} /></Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2,10 +2,12 @@
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let options: {
|
||||
left?: string
|
||||
right?: string
|
||||
rightTooltip?: string
|
||||
} = {}
|
||||
export let checked: boolean = false
|
||||
export let disabled = false
|
||||
@@ -74,6 +76,9 @@
|
||||
style={textStyle}
|
||||
>
|
||||
{options?.right}
|
||||
{#if options?.rightTooltip}
|
||||
<Tooltip>{options?.rightTooltip}</Tooltip>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
@@ -63,6 +63,9 @@
|
||||
if (onDeleteComponentControl) {
|
||||
onDeleteComponentControl()
|
||||
}
|
||||
if (onDelete) {
|
||||
onDelete()
|
||||
}
|
||||
|
||||
if (componentSettings?.item.id) {
|
||||
delete $worldStore.outputsById[componentSettings?.item.id]
|
||||
|
||||
+12
-7
@@ -5,6 +5,7 @@
|
||||
export let error = ''
|
||||
export let editor: SimpleEditor | undefined = undefined
|
||||
|
||||
$: tooBig = code && code?.length > 1000000
|
||||
function parseJson() {
|
||||
try {
|
||||
value = JSON.parse(code ?? '')
|
||||
@@ -16,11 +17,15 @@
|
||||
$: code && parseJson()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="border border-gray-300 w-full">
|
||||
<SimpleEditor on:focus bind:this={editor} on:change autoHeight lang="json" bind:code />
|
||||
{#if tooBig}
|
||||
<span class="text-gray-600">JSON to edit is too big</span>
|
||||
{:else}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="border border-gray-300 w-full">
|
||||
<SimpleEditor on:focus bind:this={editor} on:change autoHeight lang="json" bind:code />
|
||||
</div>
|
||||
{#if error != ''}
|
||||
<span class="text-red-600 text-xs">{error}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if error != ''}
|
||||
<span class="text-red-600 text-xs">{error}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -242,6 +242,7 @@
|
||||
</div>
|
||||
{:else if selected === 'test'}
|
||||
<ModulePreview
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
bind:this={modulePreview}
|
||||
mod={flowModule}
|
||||
schema={$flowStateStore[$selectedId]?.schema ?? {}}
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
export let initialPath: string
|
||||
|
||||
$: url = `${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run/f/${$flowStore?.path}`
|
||||
$: syncedUrl = `${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run_wait_result/f/${$flowStore?.path}`
|
||||
</script>
|
||||
|
||||
<div class="h-full overflow-hidden">
|
||||
@@ -69,52 +71,87 @@
|
||||
On-demand:
|
||||
<ul class="pt-4">
|
||||
<li>
|
||||
1. <a href="https://docs.windmill.dev/docs/core_concepts/auto_generated_uis" target="_blank">Auto-generated UIs</a>
|
||||
</li>
|
||||
1. <a
|
||||
href="https://docs.windmill.dev/docs/core_concepts/auto_generated_uis"
|
||||
target="_blank">Auto-generated UIs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
3. <a href="/apps/add?nodraft=true" target="_blank"> App Editor</a> for customized-UIs
|
||||
2. <a href="/apps/add?nodraft=true" target="_blank"> App Editor</a> for customized-UIs
|
||||
</li>
|
||||
<li>
|
||||
3. <a href="/schedules" target="_blank">Scheduling</a>
|
||||
</li>
|
||||
<li>
|
||||
4. <a href="https://docs.windmill.dev/docs/advanced/cli" target="_blank">Windmill CLI</a>
|
||||
4. <a href="https://docs.windmill.dev/docs/advanced/cli" target="_blank"
|
||||
>Windmill CLI</a
|
||||
>
|
||||
</li>
|
||||
<br>
|
||||
<br />
|
||||
<li class="mt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
From external events:
|
||||
</p>
|
||||
<p> From external events: </p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
5. Send a <a href="https://docs.windmill.dev/docs/core_concepts/webhooks" target="_blank">webhook</a> after each event: <a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(url)
|
||||
}}
|
||||
href={$page.url.protocol + '//' + url}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
5. Send a <a
|
||||
href="https://docs.windmill.dev/docs/core_concepts/webhooks"
|
||||
target="_blank">webhook</a
|
||||
>
|
||||
after each event:
|
||||
<ul class="list-disc pl-4"
|
||||
><li
|
||||
>Async <Tooltip
|
||||
>Return an uuid instantly that you can use to fetch status and result</Tooltip
|
||||
>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(url)
|
||||
}}
|
||||
href={$page.url.protocol + '//' + url}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{url}
|
||||
<span class="text-gray-700 ml-2">
|
||||
<Icon data={faClipboard} />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
>Sync <Tooltip>Wait for result within a timeout of 20s</Tooltip>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(syncedUrl)
|
||||
}}
|
||||
href={$page.url.protocol + '//' + syncedUrl}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{syncedUrl}
|
||||
<span class="text-gray-700 ml-2">
|
||||
<Icon data={faClipboard} />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul></li
|
||||
>
|
||||
{url}
|
||||
<span class="text-gray-700 ml-2">
|
||||
<Icon data={faClipboard} />
|
||||
</span>
|
||||
</a></li>
|
||||
<br>
|
||||
<br />
|
||||
<li>
|
||||
6. Use a <a href="https://docs.windmill.dev/docs/flows/flow_trigger" target="_blank">trigger script</a> and schedule this flow to run as frequently as
|
||||
needed and compare a state persisted in Windmill to the state of the
|
||||
external system. If a difference is detected, then the rest of the flow is
|
||||
triggered. Oftentimes, the second step of a flow is a for-loop that will
|
||||
iterate over every elements. When using a trigger, a default schedule will
|
||||
be created.
|
||||
6. Use a <a
|
||||
href="https://docs.windmill.dev/docs/flows/flow_trigger"
|
||||
target="_blank">trigger script</a
|
||||
>
|
||||
and schedule this flow to run as frequently as needed and compare a state persisted
|
||||
in Windmill to the state of the external system. If a difference is detected, then
|
||||
the rest of the flow is triggered. Oftentimes, the second step of a flow is a for-loop
|
||||
that will iterate over every elements. When using a trigger, a default schedule
|
||||
will be created.
|
||||
<img
|
||||
class="shadow-lg border rounded"
|
||||
alt="static button"
|
||||
src="/trigger_button.png"
|
||||
/>
|
||||
class="shadow-lg border rounded"
|
||||
alt="static button"
|
||||
src="/trigger_button.png"
|
||||
/>
|
||||
</li></ul
|
||||
>
|
||||
</div>
|
||||
@@ -122,7 +159,11 @@
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="settings-schedule" class="p-4">
|
||||
<Alert type="info" title="Primary Schedule" documentationLink="https://docs.windmill.dev/docs/core_concepts/scheduling">
|
||||
<Alert
|
||||
type="info"
|
||||
title="Primary Schedule"
|
||||
documentationLink="https://docs.windmill.dev/docs/core_concepts/scheduling"
|
||||
>
|
||||
Flows can be triggered by any schedules, their webhooks or their UI but they only have
|
||||
only one primary schedules with which they share the same path. The primary schedule
|
||||
can be set here.
|
||||
|
||||
@@ -10,9 +10,60 @@ import {
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { loadSchema, loadSchemaFlow } from '$lib/scripts'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { get } from 'svelte/store'
|
||||
import type { FlowModuleState } from './flowState'
|
||||
import type { PickableProperties } from './previousResults'
|
||||
|
||||
function create_context_function_template(eval_string: string, context: Record<string, any>) {
|
||||
return `
|
||||
return function (context) {
|
||||
"use strict";
|
||||
${
|
||||
Object.keys(context).length > 0
|
||||
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
|
||||
: ``
|
||||
}
|
||||
return ${eval_string}
|
||||
}`
|
||||
}
|
||||
|
||||
function make_context_evaluator(eval_string, context): (context) => any {
|
||||
let template = create_context_function_template(eval_string, context)
|
||||
let functor = Function(template)
|
||||
return functor()
|
||||
}
|
||||
|
||||
export function evalValue(
|
||||
k: string,
|
||||
mod: FlowModule,
|
||||
testStepStore: Record<string, any>,
|
||||
pickableProperties: PickableProperties | undefined,
|
||||
showError: boolean
|
||||
) {
|
||||
let inputTransforms = (mod.value['input_transforms'] ?? {}) as Record<string, InputTransform>
|
||||
let v = testStepStore[mod.id]?.[k]
|
||||
let t = inputTransforms?.[k]
|
||||
if (!v) {
|
||||
if (t.type == 'static') {
|
||||
v = t.value
|
||||
} else {
|
||||
try {
|
||||
let context = {
|
||||
flow_input: pickableProperties?.flow_input,
|
||||
results: pickableProperties?.priorIds
|
||||
}
|
||||
v = make_context_evaluator(t.expr, context)(context)
|
||||
} catch (e) {
|
||||
if (showError) {
|
||||
sendUserToast(`Error evaluating ${k}: ${e.message}`, true)
|
||||
}
|
||||
v = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
export function cleanInputs(flow: Flow | any): Flow {
|
||||
const newFlow: Flow = JSON.parse(JSON.stringify(flow))
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
{:else if topBrackets}
|
||||
<span class="text-black">{openBracket}{closeBracket}</span>
|
||||
{:else}
|
||||
<span class="text-gray-400 text-xs ml-2">No items</span>
|
||||
<span class="text-gray-400 text-xs ml-2">No items ([])</span>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
@@ -172,14 +172,16 @@
|
||||
<span class="font-bold text-sm">Variables </span>
|
||||
<div class="overflow-y-auto mb-2">
|
||||
{#if displayVariable}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
displayVariable = false
|
||||
}}>-</Button
|
||||
>
|
||||
<div class="flex">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
displayVariable = false
|
||||
}}>-</Button
|
||||
>
|
||||
</div>
|
||||
<ObjectViewer
|
||||
allowCopy={false}
|
||||
pureViewer={!$propPickerConfig}
|
||||
|
||||
Reference in New Issue
Block a user