mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 08:00:45 +00:00
234 lines
7.2 KiB
Svelte
234 lines
7.2 KiB
Svelte
<script lang="ts">
|
|
import autosize from '$lib/autosize'
|
|
import { shouldDisplayPlaceholder } from '$lib/utils'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import ArrayTypeNarrowing from '../ArrayTypeNarrowing.svelte'
|
|
import Label from '../Label.svelte'
|
|
import NumberTypeNarrowing from '../NumberTypeNarrowing.svelte'
|
|
import StringTypeNarrowing from '../StringTypeNarrowing.svelte'
|
|
import Tooltip from '../Tooltip.svelte'
|
|
|
|
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
|
import { deepEqual } from 'fast-equals'
|
|
import type { EnumType } from '$lib/common'
|
|
import type { SchemaProperty } from '$lib/common'
|
|
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
|
import { createEventDispatcher, onMount } from 'svelte'
|
|
|
|
export let description: string = ''
|
|
export let format: string | undefined = undefined
|
|
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
|
export let type: string | undefined = undefined
|
|
export let oneOf: SchemaProperty[] | undefined = undefined
|
|
export let pattern: undefined | string = undefined
|
|
export let enum_: EnumType = undefined
|
|
export let extra: Record<string, any> = {}
|
|
export let minW = true
|
|
export let customErrorMessage: string | undefined = undefined
|
|
export let title: string | undefined = undefined
|
|
export let placeholder: string | undefined = undefined
|
|
export let properties: Record<string, any> | undefined = undefined
|
|
export let isFlowInput: boolean = false
|
|
export let isAppInput: boolean = false
|
|
export let order: string[] | undefined = undefined
|
|
export let itemsType:
|
|
| {
|
|
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
|
|
contentEncoding?: 'base64'
|
|
enum?: string[]
|
|
resourceType?: string
|
|
multiselect?: string[]
|
|
}
|
|
| undefined = undefined
|
|
|
|
const dispatch = createEventDispatcher()
|
|
let el: HTMLTextAreaElement | undefined = undefined
|
|
|
|
let oneOfSelected: string | undefined =
|
|
(oneOf && oneOf.length >= 2 && oneOf[0]['title']) || undefined
|
|
let oneOfSchemas: any[] | undefined = undefined
|
|
function updateOneOfSchemas(oneOf: SchemaProperty[] | undefined) {
|
|
if (oneOf && oneOf.length >= 2) {
|
|
oneOfSchemas = oneOf.map((obj) => {
|
|
return {
|
|
properties: obj.properties
|
|
? Object.fromEntries(Object.entries(obj.properties).filter(([k, v]) => k !== 'label'))
|
|
: {},
|
|
order: obj.order
|
|
}
|
|
})
|
|
}
|
|
}
|
|
$: updateOneOfSchemas(oneOf)
|
|
|
|
export function focus() {
|
|
el?.focus()
|
|
if (el) {
|
|
el.style.height = '5px'
|
|
el.style.height = el.scrollHeight + 50 + 'px'
|
|
}
|
|
}
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if ((e.ctrlKey || e.metaKey) && e.key == 'Enter') {
|
|
return
|
|
}
|
|
e.stopPropagation()
|
|
}
|
|
|
|
let schema = {
|
|
properties,
|
|
order
|
|
}
|
|
|
|
let initialExtra: any = structuredClone({ order: undefined, properties: undefined, ...extra })
|
|
|
|
let mounted = false
|
|
let firstOnContentChange = true
|
|
onMount(() => {
|
|
setTimeout(() => {
|
|
mounted = true
|
|
}, 500)
|
|
})
|
|
|
|
$: extra && mounted && onContentChange()
|
|
|
|
function onContentChange() {
|
|
if (firstOnContentChange) {
|
|
firstOnContentChange = false
|
|
return
|
|
}
|
|
if (!deepEqual(extra, initialExtra)) {
|
|
initialExtra = structuredClone(extra)
|
|
console.debug('property content updated')
|
|
dispatch('change')
|
|
}
|
|
}
|
|
|
|
$: (properties || order) && updateSchema()
|
|
|
|
function updateSchema() {
|
|
if (!deepEqual(schema.properties, properties) || !deepEqual(schema.order, order)) {
|
|
schema = {
|
|
properties,
|
|
order
|
|
}
|
|
console.debug('property schema updated')
|
|
dispatch('change')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-row items-center justify-between w-full gap-2">
|
|
<!-- svelte-ignore a11y-autofocus -->
|
|
<div class={twMerge('flex flex-col w-full', 'gap-4', minW ? 'min-w-[250px]' : '')}>
|
|
<slot name="typeeditor" />
|
|
|
|
<Label label="Description">
|
|
<textarea
|
|
use:autosize
|
|
rows="2"
|
|
bind:value={description}
|
|
on:keydown={onKeyDown}
|
|
placeholder="Field description"
|
|
/>
|
|
</Label>
|
|
|
|
<Label label="Custom Title" class="w-full">
|
|
<svelte:fragment slot="header">
|
|
<Tooltip light>Will be displayed in the UI instead of the field name.</Tooltip>
|
|
</svelte:fragment>
|
|
<input bind:value={title} on:keydown={onKeyDown} placeholder="Field title" />
|
|
</Label>
|
|
|
|
<Label label="Placeholder">
|
|
<svelte:fragment slot="header">
|
|
<Tooltip light>
|
|
Will be displayed in the input field when the field is empty. If not set, the default
|
|
value will be used. The placeholder is disabled depending on the field type, format, etc.
|
|
</Tooltip>
|
|
</svelte:fragment>
|
|
|
|
<textarea
|
|
placeholder="Enter a placeholder"
|
|
rows="1"
|
|
bind:value={placeholder}
|
|
on:change={() => dispatch('change')}
|
|
disabled={!shouldDisplayPlaceholder(type, format, enum_, contentEncoding, pattern, extra)}
|
|
/>
|
|
</Label>
|
|
|
|
{#if type == 'array'}
|
|
<ArrayTypeNarrowing bind:itemsType canEditResourceType={isFlowInput || isAppInput} />
|
|
{:else if type == 'string' || ['number', 'integer', 'object'].includes(type ?? '')}
|
|
<div>
|
|
<Label label="Field settings">
|
|
<div>
|
|
{#if type == 'string'}
|
|
<StringTypeNarrowing
|
|
bind:customErrorMessage
|
|
bind:format
|
|
bind:pattern
|
|
bind:enum_
|
|
bind:contentEncoding
|
|
bind:password={extra['password']}
|
|
bind:minRows={extra['minRows']}
|
|
bind:disableCreate={extra['disableCreate']}
|
|
bind:disableVariablePicker={extra['disableVariablePicker']}
|
|
bind:dateFormat={extra['dateFormat']}
|
|
bind:enumLabels={extra['enumLabels']}
|
|
originalType={extra['originalType']}
|
|
overrideAllowKindChange={isFlowInput || isAppInput}
|
|
/>
|
|
{:else if type == 'number' || type == 'integer'}
|
|
<NumberTypeNarrowing
|
|
bind:min={extra['min']}
|
|
bind:max={extra['max']}
|
|
bind:currency={extra['currency']}
|
|
bind:currencyLocale={extra['currencyLocale']}
|
|
/>
|
|
{:else if type == 'object' && oneOf && oneOf.length >= 2 && !isFlowInput && !isAppInput}
|
|
<ToggleButtonGroup bind:selected={oneOfSelected} class="mb-2">
|
|
{#each oneOf as obj}
|
|
<ToggleButton value={obj.title} label={obj.title} />
|
|
{/each}
|
|
</ToggleButtonGroup>
|
|
{#if oneOfSelected && oneOfSchemas}
|
|
{@const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)}
|
|
<div class="border">
|
|
<EditableSchemaForm
|
|
on:change
|
|
noPreview
|
|
bind:schema={oneOfSchemas[idx]}
|
|
uiOnly
|
|
jsonEnabled={false}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{:else if type == 'object' && format?.startsWith('dynselect-')}
|
|
<div class="text-tertiary text-xs">No settings available for Dynamic Select</div>
|
|
{:else if type == 'object' && !format?.startsWith('resource-') && !isFlowInput && !isAppInput}
|
|
<div class="border">
|
|
<EditableSchemaForm on:change noPreview bind:schema uiOnly jsonEnabled={false} />
|
|
</div>
|
|
{:else}
|
|
<div class="text-tertiary text-xs">No settings available for this field type</div>
|
|
{/if}
|
|
</div>
|
|
</Label>
|
|
</div>
|
|
{/if}
|
|
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
input::-webkit-outer-spin-button,
|
|
input::-webkit-inner-spin-button {
|
|
-webkit-appearance: none !important;
|
|
margin: 0;
|
|
}
|
|
</style>
|