Files
windmill/frontend/src/lib/components/ArgInput.svelte
T
Guilhem 09c8f3b1f3 feat: redesign flow step, loop and branch settings panels (#10026)
* feat: responsive modal step panel for the flow editor in sessions

On narrow layouts the flow editor's step-details pane opens as a modal
(double-click a graph node) instead of a split pane, with a dock/float
toggle. Scoped to sessions via allowModalPanel; the full-page editor is
unchanged.

- FlowEditor: modal/docked modes gated by mount width + allowModalPanel,
  small header (step-id Badge + subtle dock/close), standing
  double-click hint, and a per-step hint in the name tooltip
- selectionManager: onSelectIntent hook so flow-level panels (settings,
  input, triggers…) open the modal on single click
- PropPickerWrapper: collapse the prop picker until connect and animate
  it in via AnimatedPane (runs-page pattern), no blue connect ring in
  modal mode
- StepInputGen: drop the TAB/Wand autocompletion button + spinner
  (feature still works via focus + Tab)
- InputTransformForm: decouple the Help dropdown from the AI suggestion
- FlowModuleHeader: move 'Save to workspace' into an ellipsis dropdown

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: loop editor rendering and nested splitpanes splitters in the sessions modal

- Loop iterator/parallelism: keep the picker split pane (forceExpanded) so
  the editor fills its box and the picker shows; the collapse-until-connect
  mode stays for the step inputs
- Remove the intrusive AI TAB/Wand autocompletion button from IteratorGen
  (generation still runs headless via focus + Tab)
- Size the iterator connect plug and restyle the loop header/labels/toggles
- Scope the global `.splitter-hidden` splitter-hiding rule to direct children
  so it no longer leaks into nested Splitpanes under the sessions preview

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: redesign flow step advanced settings as a single toggle-first column

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: taller step test pane by default and restyle advanced section titles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: show flow run-settings params disabled when a setting is toggled off

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: single-column for-loop panel reusing the run-settings accordion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: single-column while-loop panel reusing the run-settings accordion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: single-column branch panels reusing the run-settings accordion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: auto-open modal panel when creating an AI agent tool

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: redesign branch panels with card layout and shared predicate editor

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: remove per-setting status badges from flow map nodes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync package-lock after windmill-utils-internal bump

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style: polish prop-picker plug button and branch panel layouts

* fix: persist skip-if-stopped toggles in early stop settings

* fix: open the step panel modal on demand and cap its width

* fix: restore graph step setting badges, strip panel header chips instead

* feat: docked panel header with detach action and open-details step menu

* feat: width-based panel mode on every surface with inline detach action

* refactor: single source for flow step settings and their defaults

* docs: pin flow editor vocabulary in CONTEXT.md

* fix: open the trigger panel on double click or a specific trigger

* fix: keep module pickers inside their pane and dismissable

* fix: drop the misleading chevron on the MCP tool entry

* fix: resolve flow approvals against the job's workspace, not the nav one

* refactor: derive the approval workspace from the job, not from callers

* fix: restore S3 snippets and gate params while their setting is off

* fix: restore branch mock controls and address review findings

* chore: drop stray debug log from the flow map item

* feat: pinned output section for loop and branch panels

* fix: open the panel for deliberate navigation from the flow header

* perf: mount branch predicate editors on demand

* fix: skip predicate picker previews the previous step's result

* fix: flow-level graph nodes open their panel on a single click

* fix: open the step panel for AI chat selections, not for undo

* chore: drop dead console.log and duplicated modalPanel doc

* fix: re-sync expression editors and scope error-handler settings

* fix: match the failure module exactly and ignore unselectable nodes

* fix: keep concurrency editable, honour module cache_ttl, tighten panel ids

* fix: open panel from indirect selections, use presence for value-driven toggles

* fix: don't open settings on error-handler delete, flush editors on unmount

* fix: guard editor destroy flush, keep retry kind reachable

* refactor: name the run settings panel after the domain vocabulary

* fix: only write editor flushes to the step they belong to

* fix: bind step panels by id so a delete can't retarget editor writes

* fix: don't let the trigger picker's escape close the drawer beneath it

* docs: condense two comments to the constraint they record

* fix: arbitrate escape through the overlay stack instead of deferring to it

* fix: key nested step blocks by identity so anchored bindings can't go stale

* fix: untrack the overlay-stack push and drop the frozen branch binding

* chore: state the escape rationale once, key branch lists, format

* fix: let the topmost overlay own escape instead of the graph

* fix: keep the dynamic-input help box out of static template fields

* fix: restore the graph connect on the for-loop iterator

* fix: end connect mode with the modal and keep it to docked panels

* fix: never enter graph connect mode from the modal panel

* fix: reveal inserted steps, restore editor pane size, unleak the drawer stack

* fix: keep the enable-AI popover reachable in session panes

* feat: add the connect policy and its single armed slot

* refactor: one picker for every expression input

* refactor: route every connect through one armed slot

* fix: give every connect button the same footprint

* fix: keep the connect ring from showing through the button

* fix: keep flow card actions right-aligned beside the detach button

* fix: give the connect ring an opaque ground to mask against

* feat: dock the panel back without reopening it

* feat: dock the panel from the graph control bar

* style: round the graph control bar and size its glyphs

* style: customize the graph controls through their supported api

* style: build the graph control bar from lucide icons

* fix: use the graph's tooltip component in the zoom controls

* style: pad the graph controls and enlarge their glyphs

* style: pad the graph controls and put dock at the bar's end

* refactor: give settings rows the same popover picker as other expressions

* fix: pass the wrapper's pickable properties to nested inputs

* refactor: stack step settings and render every expression through the step input form

* feat: split loop panels into tabs and rework the approval form

* feat: anchor drawers to their host pane and give them a size floor

* fix: mark the loop iterator expression as required

* refactor: badge ee-only toggles instead of a warning line

* fix: flag an empty loop iterator expression as an error

* refactor: pick the early-stop flow status from one toggle group

* fix: keep parallel loops uncapped unless a limit is opted into

* fix: scope the overlay stack to its host and disarm connect on dismissal

* fix: anchor the trigger picker to its host pane

* feat: move diff into the menu when the top bar is narrow

* fix: gate the result logs toggle to the graph popover

* feat: raise the modal-panel breakpoint to 1280

* fix: anchor flow editor popovers and fullscreen to their host pane

* fix: anchor overlays to their host pane and mute them when hidden

* fix: portal hosted modals and menus into the pane they anchor to

* fix: keep non-listening dialogs off the overlay stack

* fix: drop the topmost gate from confirmation dialogs

* fix: silence overlays in a collapsed preview panel

* feat: rework the branch panels with tabs, reordering and add/delete

* refactor: fold the detached-panel chrome into the card header

* fix: give every flow panel a titled card header

* fix: stop the step panel oscillating on an auto-height editor

* feat: consolidate script panel actions and restore branch predicate AI

* fix: restore the logs toggle on the flow result popover

* fix: collapse the idle property picker in modal step panels

* fix: stop the docked pane scrolling alongside its panel

* fix: space the last settings row off the panel bottom

* revert: always show the property picker pane in step panels

* chore: keep the inline script AI button identical to main

* fix: ask for AI input suggestions on click, not on hover

* fix: keep graph connects armed and remount the parallelism input

* style: reveal the predicate AI button on row hover

* style: give branch cards a handle and delete column

* refactor: arbitrate flow overlay escape through Disposable

* fix: give the popover picker its results and re-narrow the EE badge

* docs: correct loopSubset and guard the modal width measurement

* fix: insert picked properties at the cursor in expression inputs

* fix: give the expanded-subflow panel the shared header chrome

* style: rename the suspend setting to Suspend until approval/resume

* feat: open a step's modal when clicking the step already selected

* feat: add an auto/attached/detached toggle for the step panel

* refactor: pick the step panel's placement from one named menu

* refactor: keep the panel-mode module's exports to what is consumed

* feat: show each configured setting's value on its badge

* fix: carry the suspend rename into the step settings registry

* docs: name both gestures in the step explore hint

* test: pin where the step panel goes for a given width and preference

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-05 12:25:25 +02:00

1561 lines
46 KiB
Svelte

<script lang="ts">
import { preventDefault, stopPropagation, createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import type { EnumType, SchemaProperty } from '$lib/common'
import {
setInputCat as computeInputCat,
debounce,
emptySchema,
emptyString,
getSchemaFromProperties,
type DynamicInput as DynamicInputTypes
} from '$lib/utils'
import { DollarSign, Plus, X, Check, Loader2, ExternalLink } from 'lucide-svelte'
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
import { fade } from 'svelte/transition'
import { Alert, Button, SecondsInput } from './common'
import FieldHeader from './FieldHeader.svelte'
import type ItemPicker from './ItemPicker.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import Range from './Range.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import Toggle from './Toggle.svelte'
import type VariableEditor from './VariableEditor.svelte'
import { twMerge } from 'tailwind-merge'
import ArgEnum from './ArgEnum.svelte'
import DateTimeInput from './DateTimeInput.svelte'
import DateInput from './DateInput.svelte'
import CurrencyInput from './apps/components/inputs/currency/CurrencyInput.svelte'
import PasswordArgInput from './PasswordArgInput.svelte'
import Password from './Password.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
import SchemaForm from './SchemaForm.svelte'
import { deepEqual } from 'fast-equals'
import DynamicInput from './DynamicInput.svelte'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { ComponentCustomCSS } from './apps/types'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
import S3ArgInput from './common/fileUpload/S3ArgInput.svelte'
import { base } from '$lib/base'
import { workspaceStore } from '$lib/stores'
import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte'
import AIProviderPicker from './AIProviderPicker.svelte'
import TextInput from './text_input/TextInput.svelte'
import FileInput from './common/fileInput/FileInput.svelte'
import { randomUUID } from '$lib/utils/uuid'
interface Props {
label?: string
value: any
defaultValue?: any
description?: string
format?: string
contentEncoding?: 'base64' | 'binary' | undefined
type?: string | undefined
oneOf?: SchemaProperty[] | undefined
required?: boolean
pattern?: undefined | string
valid?: boolean
enum_?: EnumType
disabled?: boolean
itemsType?:
| {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
multiselect?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
| undefined
lightHeaderFont?: boolean
displayHeader?: boolean
properties?: { [name: string]: SchemaProperty } | undefined
nestedRequired?: string[] | undefined
autofocus?: boolean | null
compact?: boolean
password?: boolean
pickForField?: string | undefined
variableEditor?: VariableEditor | undefined
itemPicker?: ItemPicker | undefined
noMargin?: boolean
extra?: Record<string, any>
minW?: boolean
prettifyHeader?: boolean
resourceTypes: string[] | undefined
disablePortal?: boolean
showSchemaExplorer?: boolean
simpleTooltip?: string | undefined
customErrorMessage?: string | undefined
onlyMaskPassword?: boolean
nullable?: boolean
title?: string | undefined
placeholder?: string | undefined
order?: string[] | undefined
editor?: any | undefined
orderEditable?: boolean
shouldDispatchChanges?: boolean
noDefaultOnSelectFirst?: boolean
helperScript?: DynamicInputTypes.HelperScript
otherArgs?: Record<string, any>
lightHeader?: boolean
diffStatus?: SchemaDiff | undefined
hideNested?: boolean
nestedParent?: { label: string; nestedParent: any | undefined } | undefined
nestedClasses?: string
displayType?: boolean
css?: ComponentCustomCSS<'schemaformcomponent'> | undefined
appPath?: string | undefined
computeS3ForceViewerPolicies?:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined
workspace?: string | undefined
s3StorageConfigured?: boolean
chatInputEnabled?: boolean
actions?: import('svelte').Snippet
innerBottomSnippet?: import('svelte').Snippet
fieldHeaderActions?: import('svelte').Snippet
}
let {
label = '',
value = $bindable(),
defaultValue = $bindable(undefined),
description = $bindable(undefined),
format = $bindable(undefined),
contentEncoding = undefined,
type = undefined,
oneOf = $bindable(undefined),
required = false,
pattern = $bindable(undefined),
valid = $bindable(undefined), // Note : this should not exist, valid and error should be one coherent state
enum_ = $bindable(undefined),
disabled = false,
itemsType = $bindable(undefined),
displayHeader = true,
properties = $bindable(undefined),
nestedRequired = undefined,
autofocus = null,
compact = false,
password = false,
pickForField = $bindable(undefined),
variableEditor = undefined,
itemPicker = undefined,
noMargin = false,
extra = {},
minW = true,
prettifyHeader = false,
resourceTypes,
disablePortal = false,
showSchemaExplorer = false,
simpleTooltip = undefined,
customErrorMessage = undefined,
onlyMaskPassword = false,
nullable = false,
title = $bindable(undefined),
placeholder = $bindable(undefined),
order = $bindable(undefined),
editor = $bindable(undefined),
orderEditable = false,
shouldDispatchChanges = false,
noDefaultOnSelectFirst = false,
helperScript = undefined,
otherArgs = {},
lightHeader = false,
diffStatus = undefined,
hideNested = false,
nestedParent = undefined,
nestedClasses = '',
displayType = true,
css = undefined,
appPath = undefined,
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
s3StorageConfigured = true,
chatInputEnabled = false,
actions,
innerBottomSnippet,
fieldHeaderActions,
lightHeaderFont = false
}: Props = $props()
$effect(() => {
if (description == undefined) {
description = ''
}
if (format == undefined) {
format = ''
}
if (valid == undefined) {
valid = true
}
})
let oneOfSelected: string | undefined = $state(undefined)
let tagKey = $derived(
oneOf?.find((o) => Object.keys(o.properties ?? {})?.includes('kind')) ? 'kind' : 'label'
)
async function updateOneOfSelected(oneOf: SchemaProperty[] | undefined) {
if (
oneOf &&
oneOf.length >= 2 &&
(!oneOfSelected || !oneOf.some((o) => o.title === oneOfSelected) || !value)
) {
if (value && value[tagKey] && oneOf.some((o) => o.title === value[tagKey])) {
const existingValue = JSON.parse(JSON.stringify(value))
oneOfSelected = value[tagKey]
await tick()
value = existingValue
} else {
const variantTitle = oneOf[0]['title']
oneOfSelected = variantTitle
value = { ...(typeof value === 'object' ? (value ?? {}) : {}), [tagKey]: variantTitle }
}
}
}
function onOneOfChange() {
const label = value?.['label']
const kind = value?.['kind']
if (label && oneOf && oneOf.some((o) => o.title == label) && oneOfSelected != label) {
oneOfSelected = label
} else if (kind && oneOf && oneOf.some((o) => o.title == kind) && oneOfSelected != kind) {
oneOfSelected = kind
}
}
const dispatch = createEventDispatcher()
let ignoreValueUndefined = $state(false)
let error: string = $state('')
let isListJson = $state(false)
let hasIsListJsonChanged = $state(false)
let el: HTMLTextAreaElement | undefined = $state(undefined)
let rawValue: string | undefined = $state(undefined)
let lastValue: any = null
function computeDefaultValue(inputCat?: string, defaultValue?: any, nnullable?: boolean) {
let nvalue: any = null
if (label == 'toString' && typeof value == 'function') {
nvalue = undefined
}
if ((value == undefined || value == null) && !ignoreValueUndefined) {
nvalue = structuredClone($state.snapshot(defaultValue))
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
nvalue = nullable
? null
: format === 'uuid' && extra?.['x-auto-generate']
? randomUUID()
: ''
} else if (inputCat == 'enum' && required) {
let firstV = enum_?.[0]
if (typeof firstV === 'string') {
nvalue = firstV
} else if (firstV && typeof firstV === 'object') {
nvalue = firstV.value
}
} else if (inputCat == 'boolean') {
nvalue = false
} else if (inputCat == 'list') {
nvalue = nullable ? null : []
}
} else if (inputCat === 'object') {
evalValueToRaw()
}
}
if (nnullable && type === 'string' && value === '' && nvalue == null) {
value = null
} else if (nvalue != null && !deepEqual(nvalue, value)) {
value = nvalue
lastValue = value
}
}
// By setting isListJson to true, we can render inputs even if the value is not an array of the correct type
// This avoids the issue of the input being rendered as a string with value: [object Object], or as a number with value: NaN
function checkArrayValueType() {
try {
if (Array.isArray(value) && value.length > 0) {
let firstItem = value?.[0]
const type = itemsType?.type
switch (type) {
case 'string':
if (typeof firstItem !== 'string') {
isListJson = true
}
break
case 'number':
if (typeof firstItem !== 'number') {
isListJson = true
}
break
}
}
} catch (e) {
console.error(e)
}
lastValue = value
}
let oldDefaultValue = structuredClone($state.snapshot(defaultValue))
function handleDefaultValueChange() {
if (
deepEqual(value, oldDefaultValue) &&
!deepEqual(value, defaultValue) &&
!deepEqual(defaultValue, oldDefaultValue)
) {
value = defaultValue
}
oldDefaultValue = structuredClone($state.snapshot(defaultValue))
}
function isObjectCat(inputCat?: string) {
return (
inputCat === 'object' || inputCat === 'resource-object' || (inputCat == 'list' && !isListJson)
)
}
function isRawStringEditor(inputCat?: string) {
return inputCat == 'sql' || inputCat == 'yaml'
}
function evalValueToRaw() {
if (setCodeDisabled) {
return
}
const newRawValue =
value == undefined || value == null
? ''
: isObjectCat(inputCat) || (inputCat == 'list' && isListJson)
? JSON.stringify(value, null, 2)
: isRawStringEditor(inputCat)
? typeof value == 'string'
? value
: JSON.stringify(value, null, 2)
: undefined
if (newRawValue != rawValue) {
rawValue = newRawValue
rawValue != undefined && editor?.getCode() != rawValue && editor?.setCode(rawValue)
}
}
let setCodeDisabled = false
let timeout: number | undefined = undefined
function setNewValueFromCode(nvalue: any) {
if (!deepEqual(nvalue, value)) {
value = nvalue
timeout && clearTimeout(timeout)
setCodeDisabled = true
timeout = setTimeout(() => {
setCodeDisabled = false
}, 1000)
}
}
onMount(() => {
evalValueToRaw()
})
function fileChangedInner(file: File | undefined, cb: (v: string | undefined) => void) {
if (!file) {
cb(undefined)
return
}
let reader = new FileReader()
reader.onload = (e: any) => {
cb(e.target.result.split('base64,')[1])
}
reader.readAsDataURL(file)
}
export function focus() {
el?.focus()
if (el) {
el.style.height = '5px'
el.style.height = el.scrollHeight + 50 + 'px'
}
}
const EMAIL_PATTERN = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
const IPV4_PATTERN =
'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
const UUID_PATTERN = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
const IPV6_PATTERN =
'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'
function validateInput(pattern: string | undefined, v: any, required: boolean): void {
if (nullable && emptyString(v)) {
error = ''
valid && (valid = true)
} else if (
typeof v === 'string' &&
(v.startsWith('$var:') || v.startsWith('$res:') || v.startsWith('$jsonvar:'))
) {
// $var/$res/$jsonvar are placeholders resolved at runtime; the literal
// string won't match format constraints (email/ipv4/uuid/custom pattern),
// so format-checking it produces a false-positive "invalid format" error.
error = ''
!valid && (valid = true)
} else if (required && (v == undefined || v == null || v === '') && inputCat != 'object') {
error = 'Required'
valid && (valid = false)
} else if (
required &&
inputCat == 'list' &&
extra?.['nonEmpty'] == true &&
Array.isArray(v) &&
v.length === 0
) {
error = 'Required'
valid && (valid = false)
} else if (inputCat == 'list' && !Array.isArray(v)) {
error = 'Expected an array, got ' + typeof v + ' instead'
valid && (valid = false)
} else {
if (inputCat == 'number' && typeof v === 'number') {
let min = extra['min']
let max = extra['max']
if (min != undefined && typeof min == 'number' && v < min) {
error = `Should be greater than or equal to ${min}`
valid && (valid = false)
} else if (max != undefined && typeof max == 'number' && v > max) {
error = `Should be less than or equal to ${max}`
valid && (valid = false)
} else {
error = ''
!valid && (valid = true)
}
} else if (type == 'string' && format == 'email' && !testRegex(EMAIL_PATTERN, v)) {
error = 'invalid email address'
} else if (type == 'string' && format == 'ipv4' && !testRegex(IPV4_PATTERN, v)) {
error = 'invalid IPv4 address'
} else if (type == 'string' && format == 'ipv6' && !testRegex(IPV6_PATTERN, v)) {
error = 'invalid IPv6 address'
} else if (type == 'string' && format == 'uuid' && !testRegex(UUID_PATTERN, v)) {
error = 'invalid UUID'
} else if (pattern && !testRegex(pattern, v)) {
if (!emptyString(customErrorMessage)) {
error = customErrorMessage ?? ''
} else if (format == 'email') {
error = 'invalid email address'
} else {
error = `Should match ${pattern}`
}
valid && (valid = false)
} else {
error = ''
!valid && (valid = true)
}
}
}
function testRegex(pattern: string, value: any): boolean {
try {
const regex = new RegExp(pattern)
return regex.test(value)
} catch (err) {
return false
}
}
function onKeyDown(e: KeyboardEvent) {
if (
(e.ctrlKey || e.metaKey) &&
(e.key == 'Enter' || e.key == 'c' || e.key == 'v' || e.key == 'x')
) {
if (e.key == 'Enter') {
dispatch('keydownCmdEnter')
}
return
}
e.stopPropagation()
}
let redraw = $state(0)
let itemsLimit = $state(50)
let oldValue = value
function compareValues(value) {
if (!deepEqual(oldValue, value)) {
oldValue = value
dispatch('change')
}
}
let { debounced, clearDebounce } = debounce(() => compareValues(value), 50)
let inputCat = $derived(computeInputCat(type, format, itemsType?.type, enum_, contentEncoding))
let isNonStringSecret = $derived((password || extra?.['password'] == true) && type === 'object')
let displayJsonToggleHeader = $derived(
displayHeader &&
inputCat === 'list' &&
!(itemsType?.resourceType === 's3_object' || itemsType?.resourceType === 's3object')
)
$effect(() => {
oneOf && untrack(() => updateOneOfSelected(oneOf))
})
$effect(() => {
oneOf && value && untrack(() => onOneOfChange())
})
$effect.pre(() => {
value
let args = [inputCat, defaultValue, nullable]
untrack(() => {
if (deepEqual(value, lastValue)) return
computeDefaultValue(...args)
})
})
$effect.pre(() => {
!isListJson &&
inputCat === 'list' &&
enum_ == undefined &&
value != lastValue &&
itemsType?.type &&
!hasIsListJsonChanged &&
untrack(() => checkArrayValueType())
})
$effect.pre(() => {
defaultValue != undefined && untrack(() => handleDefaultValueChange())
})
$effect.pre(() => {
;(inputCat &&
(isObjectCat(inputCat) || isRawStringEditor(inputCat)) &&
!oneOf &&
untrack(() => evalValueToRaw())) ||
value
})
$effect(() => {
extra?.['nonEmpty']
let args = [pattern, value, required] as const
untrack(() => validateInput(...args))
})
$effect(() => {
shouldDispatchChanges && debounced(value)
})
onDestroy(() => {
clearDebounce()
})
</script>
{#snippet variableInput()}
{#if variableEditor}
<div class="text-2xs text-hint">
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
Linked to variable <button
class="text-accent underline font-normal"
onclick={() => variableEditor?.editVariable?.(value.slice(5))}>{value.slice(5)}</button
>
{:else if value && typeof value == 'string' && value?.startsWith('$jsonvar:')}
Linked to variable <button
class="text-accent underline font-normal"
onclick={() => variableEditor?.editVariable?.(value.slice('$jsonvar:'.length))}
>{value.slice('$jsonvar:'.length)}</button
>
{/if}
</div>
{/if}
{/snippet}
{#snippet resourceInput()}
{#if variableEditor}
<div class="text-sm text-primary">
{#if value && typeof value == 'string' && value?.startsWith('$res:')}
Linked to resource <a
target="_blank"
href="{base}/resources#/resource/{value.slice(5)}"
class="text-blue-500 underline"
>{value.slice(5)} <span class="inline-block -mb-0.5"><ExternalLink size={14} /></span></a
>
{/if}
</div>
{/if}
{/snippet}
<!-- svelte-ignore a11y_autofocus -->
<div
class={twMerge(
'flex flex-col gap-1 w-full rounded-md relative group',
minW ? 'min-w-[250px]' : '',
diffStatus?.diff ? 'px-2' : '',
diffStatus?.diff == 'added'
? 'bg-green-300 dark:bg-green-800'
: diffStatus?.diff === 'removed'
? 'bg-red-300 dark:bg-red-800'
: diffStatus?.diff === 'same'
? 'bg-surface'
: diffStatus?.diff === 'modified' || typeof diffStatus?.diff === 'object'
? 'border-2 border-green-500 bg-surface'
: ''
)}
data-schema-picker
>
{#if diffStatus && typeof diffStatus === 'object' && diffStatus.diff !== 'same'}
<div
class="absolute top-0 right-2 rounded-md rounded-t-none flex flex-row overflow-hidden bg-surface"
>
<button
class="p-1 bg-green-500 text-white hover:bg-green-600"
onclick={stopPropagation(
preventDefault(() => {
dispatch('acceptChange', { label, nestedParent })
})
)}
>
<Check size={14} />
</button>
<button
class="p-1 hover:bg-red-500 hover:text-white"
onclick={stopPropagation(
preventDefault(() => {
dispatch('rejectChange', { label, nestedParent })
})
)}
>
<X size={14} />
</button>
</div>
{/if}
{#if displayHeader}
<div class="flex items-end">
<FieldHeader
prettify={prettifyHeader}
label={title && !emptyString(title) ? title : label}
{disabled}
{required}
{type}
{contentEncoding}
{format}
{simpleTooltip}
{lightHeader}
{displayType}
labelClass={twMerge(lightHeaderFont ? '!font-normal text-primary' : '', css?.label?.class)}
/>
<div class="ml-auto flex gap-2">
{#if displayJsonToggleHeader}
<Toggle
on:change={(e) => {
// Once the user has changed the input type, we should not change it back automatically
if (!hasIsListJsonChanged) {
hasIsListJsonChanged = true
}
evalValueToRaw()
isListJson = !isListJson
}}
checked={isListJson}
textClass="text-secondary"
size="xs"
options={{ left: 'json' }}
/>
{/if}
{@render fieldHeaderActions?.()}
</div>
</div>
{/if}
{#if description}
<div class={twMerge('text-xs text-secondary', css?.description?.class)}>
<pre class="font-main whitespace-normal">{description}</pre>
</div>
{/if}
<div class="flex space-x-1">
{#if inputCat == 'number'}
{#if extra['seconds'] !== undefined}
<div class="w-full">
<SecondsInput
bind:seconds={value}
onfocus={bubble('focus')}
{defaultValue}
clearable={extra['clearable'] !== false}
/>
</div>
{:else if extra['min'] != undefined && extra['max'] != undefined}
<Range bind:value min={extra['min']} max={extra['max']} {defaultValue} />
{:else if extra?.currency}
<CurrencyInput
inputClasses={{
formatted: 'px-2 w-full py-1.5 text-black dark:text-white',
wrapper: 'w-full windmillapp',
formattedZero: 'text-black dark:text-white'
}}
noColor
bind:value
currency={extra?.currency}
locale={extra?.currencyLocale ?? 'en-US'}
{disabled}
/>
{:else}
<div class="relative w-full">
<TextInput
inputProps={{
autofocus,
onfocus: bubble('focus'),
onblur: bubble('blur'),
disabled,
type: 'number',
onkeydown: () => (ignoreValueUndefined = true),
placeholder: placeholder ?? defaultValue ?? '',
min: extra['min'],
max: extra['max'],
step: extra['step']
}}
{error}
bind:value
/>
<!-- <input
{autofocus}
onfocus={bubble('focus')}
onblur={bubble('blur')}
{disabled}
type="number"
onkeydown={() => {
ignoreValueUndefined = true
}}
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
placeholder={placeholder ?? defaultValue ?? ''}
bind:value
min={extra['min']}
max={extra['max']}
/> -->
</div>
{/if}
{:else if inputCat == 'boolean'}
<div class="w-full">
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
}}
{disabled}
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}
</div>
{:else if (inputCat == 'resource-object' && format && format.split('-').length > 1 && format
.replace('resource-', '')
.replace('_', '')
.toLowerCase() == 's3object') || (inputCat == 'list' && (itemsType?.resourceType === 's3_object' || itemsType?.resourceType === 's3object'))}
<S3ArgInput
multiple={inputCat == 'list'}
bind:value
{defaultValue}
{setNewValueFromCode}
{workspace}
onFocus={() => dispatch('focus')}
onBlur={() => dispatch('blur')}
bind:editor
{appPath}
{computeS3ForceViewerPolicies}
bottom={innerBottomSnippet}
hideCatalogPicker={extra?.['hideCatalogPicker'] ?? false}
hideRawInput={extra?.['hideRawInput'] ?? false}
/>
{:else if inputCat == 'object' && format == 'json-schema'}
{#await import('$lib/components/EditableSchemaForm.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:schema={
() =>
value && typeof value === 'object' && !Array.isArray(value) ? value : emptySchema(),
(v) => {
value = v
}
}
isFlowInput
editTab="inputEditor"
noPreview
addPropertyInEditorTab
on:delete={(e) => {
// Handle property deletion
if (value && value.properties && value.properties[e.detail]) {
delete value.properties[e.detail]
// Also remove from order array if it exists
if (value.order) {
value.order = value.order.filter((key) => key !== e.detail)
}
// Update the value to trigger reactivity
value = { ...value }
dispatch('change')
}
}}
/>
{/await}
{:else if inputCat == 'object' && format?.startsWith('jsonschema-')}
{#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $workspaceStore ?? '')}
<Loader2 class="animate-spin" />
{:then schema}
{#if !schema || !schema.properties}
<div>
<div class="w-full">
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default code={JSON.stringify(value, null, 2)} bind:value />
{/await}
</div>
<div class="text-red-500 text-2xs">
Error loading json schema resource {format.substring('jsonschema-'.length)}, please
check if the resource exists and is a valid json schema.
<a
href="https://windmill.dev/docs/core_concepts/resources_and_types#json-schema-resources"
target="_blank"
class="text-blue-500 hover:text-blue-700 underline">See documentation</a
>
</div>
</div>
{:else}
<div class="px-4 pt-4 border rounded-md w-full">
<SchemaForm
lightHeaderFont
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
{schema}
bind:args={value}
/>
</div>
{/if}
{/await}
{:else if inputCat == 'list' && !isListJson}
<div class="w-full flex gap-4">
<div class="w-full">
{#if Array.isArray(itemsType?.multiselect) && Array.isArray(value)}
<div class="items-start">
<MultiSelect
{disabled}
bind:value
items={safeSelectItems(itemsType?.multiselect)}
onOpen={() => dispatch('focus')}
reorderable
/>
</div>
{:else if enum_ && (Array.isArray(value) || value == undefined)}
<div class="items-start">
<MultiSelect
{disabled}
bind:value
items={safeSelectItems(enum_)}
onOpen={() => dispatch('focus')}
reorderable
/>
</div>
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && (Array.isArray(value) || value == undefined)}
<div class="items-start">
<MultiSelect
{disabled}
bind:value
items={safeSelectItems(itemsType?.enum)}
onOpen={() => dispatch('focus')}
reorderable
/>
</div>
{:else}
<div class="w-full flex flex-col gap-y-1">
{#key redraw}
{#if Array.isArray(value)}
{#each value ?? [] as v, i}
{#if i < itemsLimit}
<div class="flex w-full items-center relative">
{#snippet deleteItemBtn()}
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
aria-label="Clear"
onclick={() => {
value = value.filter((_, index) => index !== i)
redraw += 1
}}
>
<X size={14} />
</button>
{/snippet}
{#if itemsType?.type == 'number'}
<TextInput
inputProps={{ type: 'number', id: 'arg-input-number-array' }}
class="pr-8"
bind:value={value[i]}
/>
<div class="absolute z-10 right-1.5">
{@render deleteItemBtn()}
</div>
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
<FileInput
class="w-full"
on:change={(x) =>
fileChangedInner(x.detail?.[0], (val) => (value[i] = val))}
multiple={false}
/>
{@render deleteItemBtn()}
{:else if itemsType?.type == 'object' && itemsType?.resourceType === undefined && itemsType?.properties === undefined && !(format?.startsWith('resource-') && resourceTypes?.includes(format.split('-')[1]))}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
code={JSON.stringify(v, null, 2)}
bind:value={value[i]}
/>
{/await}
{@render deleteItemBtn()}
{:else if Array.isArray(itemsType?.enum)}
<ArgEnum
create={extra['disableCreate'] != true}
on:focus={() => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
{defaultValue}
valid={valid ?? true}
{disabled}
{autofocus}
bind:value={value[i]}
enum_={itemsType?.enum ?? []}
enumLabels={extra['enumLabels']}
/>
{@render deleteItemBtn()}
{:else if (itemsType?.type == 'resource' && itemsType?.resourceType && resourceTypes?.includes(itemsType.resourceType)) || (format?.startsWith('resource-') && resourceTypes?.includes(format.split('-')[1]))}
{@const resourceFormat =
itemsType?.type == 'resource' &&
itemsType.resourceType &&
resourceTypes.includes(itemsType.resourceType)
? `resource-${itemsType.resourceType}`
: format!}
<ObjectResourceInput
bind:value={value[i]}
format={resourceFormat}
defaultValue={undefined}
/>
{@render deleteItemBtn()}
{:else if itemsType?.type == 'resource'}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(v, null, 2)}
bind:value={value[i]}
/>
{/await}
{@render deleteItemBtn()}
{:else if itemsType?.type === 'object' && itemsType?.properties}
<div class="p-8 border rounded-md w-full">
<SchemaForm
lightHeaderFont
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
schema={getSchemaFromProperties(itemsType?.properties)}
bind:args={value[i]}
/>
</div>
{@render deleteItemBtn()}
{:else}
<TextInput
inputProps={{ type: 'text', id: 'arg-input-array' }}
class="pr-8"
bind:value={value[i]}
/>
<div class="absolute z-10 right-1.5">
{@render deleteItemBtn()}
</div>
{/if}
</div>
{/if}
{/each}
{#if value.length > itemsLimit}
<button onclick={() => (itemsLimit += 50)} class="text-xs py-2 text-blue-600"
>{itemsLimit}/{value.length}: Load 50 more...</button
>
{/if}
{:else if typeof value === 'string'}
{#if value.startsWith('$res:')}
{@render resourceInput()}
{:else}
<div class="text-red-500 text-xs">
Invalid string value: "{value}", expected array. Click add item to turn it
into an array.
</div>
{/if}
{/if}
{/key}
</div>
<Button
variant="default"
color="light"
size="xs"
wrapperClasses="w-full {Array.isArray(value) && value.length > 0 ? 'mt-1.5' : ''}"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
if (itemsType?.type == 'number') {
value = value.concat(0)
} else if (
(itemsType?.type == 'object' &&
!(
format?.startsWith('resource-') &&
resourceTypes?.includes(format.split('-')[1])
)) ||
(itemsType?.type == 'resource' &&
!(itemsType?.resourceType && resourceTypes?.includes(itemsType?.resourceType)))
) {
value = value.concat({})
} else {
value = value.concat('')
}
}}
id="arg-input-add-item"
startIcon={{ icon: Plus }}
>
Add item
</Button>
{/if}
</div>
{#if !displayHeader}
<div class="block mt-2 pl-2">
<Toggle
on:change={(e) => {
// Once the user has changed the input type, we should not change it back automatically
if (!hasIsListJsonChanged) {
hasIsListJsonChanged = true
}
evalValueToRaw()
isListJson = !isListJson
}}
checked={isListJson}
textClass="text-secondary"
size="xs"
options={{ left: 'json' }}
/>
</div>
{/if}
</div>
{:else if inputCat == 'dynamic'}
<DynamicInput
name={label}
{otherArgs}
{helperScript}
{workspace}
bind:value
format={format ?? ''}
/>
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
<span class="text-2xs text-primary">Loading resource types...</span>
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))}
<!-- {JSON.stringify(value)} -->
<ObjectResourceInput
datatableAsPgResource={label === 'database'}
{disabled}
{workspace}
{defaultValue}
selectFirst={!noDefaultOnSelectFirst && required}
{disablePortal}
format={format ?? ''}
bind:value
bind:editor
onClear={() => {
defaultValue = null
}}
{showSchemaExplorer}
/>
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
{#if oneOf && oneOf.length >= 2}
<div class="flex flex-col gap-2 w-full border rounded-md p-4">
{#if oneOf && oneOf.length >= 2}
<ToggleButtonGroup
selected={oneOfSelected}
wrap
class="mb-4"
on:selected={({ detail }) => {
oneOfSelected = detail
const selectedObjProperties =
oneOf?.find((o) => o.title == detail)?.properties ?? {}
const newValueKeys = Object.keys(selectedObjProperties)
const toKeep = {}
for (const key of newValueKeys) {
// Check if there is a select (enum) in the newly selected oneOf and if the current value is not in the enum, skip it
if (
!['kind', 'label'].includes(key) &&
selectedObjProperties[key]?.enum &&
value &&
value[key] !== undefined &&
!selectedObjProperties[key].enum.includes(value[key])
) {
continue
}
toKeep[key] = value[key]
}
const tagKey = oneOf.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
value = { ...toKeep, [tagKey]: detail }
}}
>
{#snippet children({ item })}
{#each oneOf as obj}
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
{/each}
{/snippet}
</ToggleButtonGroup>
{#if oneOfSelected}
{@const objIdx = oneOf.findIndex((o) => o.title === oneOfSelected)}
{@const obj = oneOf[objIdx]}
{#if obj && obj.properties && Object.keys(obj.properties).length > 0}
{#key redraw}
{#if orderEditable}
<SchemaFormDnd
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
bind:schema={
() => ({
properties: obj.properties ?? {},
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}),
() => {
dispatch('nestedChange')
}
}
bind:args={value}
hiddenArgs={[
oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
]}
on:reorder={(e) => {
if (oneOf && oneOf[objIdx]) {
const keys = e.detail
oneOf[objIdx].order = keys
}
}}
on:nestedChange
{shouldDispatchChanges}
/>
{:else}
<SchemaForm
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
{chatInputEnabled}
hiddenArgs={['label', 'kind']}
schema={{
properties: obj.properties,
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}}
bind:args={
() => value,
(v) => {
value = { ...v, [tagKey]: oneOfSelected }
}
}
{shouldDispatchChanges}
on:change={() => {
dispatch('nestedChange')
}}
on:nestedChange
/>
{/if}
{/key}
{#if !s3StorageConfigured && obj['x-no-s3-storage-workspace-warning']}
<Alert
type="warning"
title={obj['x-no-s3-storage-workspace-warning']}
size="xs"
titleClass="text-2xs"
/>
{/if}
{:else if disabled}
<textarea disabled></textarea>
{:else}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
on:changeValue={(e) => {
setNewValueFromCode(e.detail)
}}
/>
{/await}
{/if}
{/if}
{:else if disabled}
<textarea disabled></textarea>
{:else}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
on:change={(e) => {
value = e.detail
}}
/>
{/await}
{/if}
</div>
{:else if properties && Object.keys(properties).length > 0 && inputCat !== 'list'}
<div class={hideNested ? 'hidden' : 'px-4 pt-4 border rounded-md w-full'}>
{#if orderEditable}
<SchemaFormDnd
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
schema={{
properties,
$schema: '',
required: nestedRequired ?? [],
type: 'object',
order
}}
bind:args={value}
on:reorder={(e) => {
const keys = e.detail
order = keys
}}
diff={diffStatus && typeof diffStatus.diff === 'object' ? diffStatus.diff : {}}
on:acceptChange={(e) => {
dispatch('acceptChange', e.detail)
}}
on:rejectChange={(e) => {
dispatch('rejectChange', e.detail)
}}
on:nestedChange
nestedParent={{ label, nestedParent }}
{shouldDispatchChanges}
/>
{:else}
<SchemaForm
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
schema={{
properties,
order,
$schema: '',
required: nestedRequired ?? [],
type: 'object'
}}
bind:args={value}
diff={diffStatus && typeof diffStatus.diff === 'object' ? diffStatus.diff : {}}
nestedParent={{ label, nestedParent }}
on:acceptChange={(e) => {
dispatch('acceptChange', e.detail)
}}
on:rejectChange={(e) => {
dispatch('rejectChange', e.detail)
}}
on:change={() => {
dispatch('nestedChange')
}}
on:nestedChange
{shouldDispatchChanges}
/>
{/if}
</div>
{:else if disabled}
<textarea disabled></textarea>
{:else}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
on:changeValue={(e) => {
setNewValueFromCode(e.detail)
}}
/>
{/await}
{/if}
{#if inputCat == 'list' && !displayHeader}
<div class="block mt-2.5 pl-2">
<Toggle
on:change={(e) => {
isListJson = !isListJson
}}
checked={isListJson}
textClass="text-secondary"
size="xs"
options={{ left: 'json' }}
/>
</div>
{/if}
{:else if inputCat == 'enum'}
<div class="flex flex-row w-full gap-1">
<ArgEnum
onClear={() => {
lastValue = undefined
value = undefined
}}
create={extra['disableCreate'] != true}
{defaultValue}
valid={valid ?? true}
{disabled}
bind:value
{enum_}
{autofocus}
on:focus={() => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
enumLabels={extra['enumLabels']}
/>
</div>
{:else if inputCat == 'date'}
{#if format === 'date'}
<DateInput {disabled} {autofocus} bind:value dateFormat={extra?.['dateFormat']} />
{:else if format === 'naive-date-time'}
<DateTimeInput {disabled} useDropdown {autofocus} bind:value timezone="naive" />
{:else}
<DateTimeInput {disabled} useDropdown {autofocus} bind:value />
{/if}
{:else if isRawStringEditor(inputCat)}
{#if disabled}
<textarea disabled></textarea>
{:else}
<div class="border my-1 mb-4 w-full">
{#await import('$lib/components/SimpleEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
on:change={(e) => {
setNewValueFromCode(e.detail?.code)
}}
bind:this={editor}
lang={inputCat}
code={typeof rawValue == 'string' ? rawValue : JSON.stringify(rawValue, null, 2)}
autoHeight
/>
{/await}
</div>
{/if}
{:else if inputCat == 'base64'}
<div class="flex flex-col w-full">
<FileInput
on:change={(x) => fileChangedInner(x.detail?.[0], (val) => (value = val))}
multiple={false}
/>
{#if value?.length}
<div class="text-2xs text-primary mt-1">
File length: {value.length} base64 chars ({(value.length / 1024 / 1024).toFixed(2)}MB)
</div>
{/if}
</div>
{:else if inputCat == 'resource-string'}
<ResourcePicker
selectFirst={noDefaultOnSelectFirst}
{disablePortal}
{workspace}
bind:value
initialValue={defaultValue}
resourceType={format && format.split('-').length > 1
? format.substring('resource-'.length)
: undefined}
{showSchemaExplorer}
/>
{:else if inputCat == 'ai-provider'}
<AIProviderPicker bind:value {disabled} {actions} />
{:else if inputCat == 'email'}
<input
{autofocus}
onfocus={bubble('focus')}
onblur={bubble('blur')}
{disabled}
type="email"
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'}
placeholder={placeholder ?? defaultValue ?? ''}
bind:value
/>
{:else if inputCat == 'string'}
<div class="flex flex-col w-full">
<div class="flex flex-row w-full items-center justify-between relative">
{#if password || extra?.['password'] == true}
{#if onlyMaskPassword}
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
<input type="text" bind:value />
{:else}
<Password
{disabled}
minRows={extra?.['minRows']}
bind:password={value}
placeholder={placeholder ?? defaultValue ?? ''}
/>
{/if}
{:else}
<PasswordArgInput {disabled} minRows={extra?.['minRows']} bind:value />
{/if}
{:else}
{#key extra?.['minRows']}
<TextInput
inputProps={{
autofocus,
onfocus: () => dispatch('focus'),
onblur: () => dispatch('blur'),
disabled,
onkeydown: onKeyDown,
placeholder: placeholder ?? defaultValue ?? '',
rows: extra?.['minRows'] ? extra['minRows']?.toString() : '1'
}}
bind:value
{error}
unifiedHeight={false}
underlyingInputEl="textarea"
/>
{/key}
{/if}
{#if !disabled && itemPicker && extra?.['disableVariablePicker'] != true}
<Button
iconOnly
startIcon={{ icon: DollarSign }}
unifiedSize="sm"
onClick={() => {
pickForField = label
itemPicker?.openDrawer?.()
}}
wrapperClasses={twMerge(
'opacity-0 group-hover:opacity-100 transition-opacity absolute bg-surface-input',
password || extra?.['password'] == true ? 'right-8 ' : 'right-2'
)}
variant="subtle"
title="Insert a Variable"
/>
{/if}
</div>
{@render variableInput()}
{@render resourceInput()}
</div>
{/if}
{@render actions?.()}
</div>
{#if isNonStringSecret}
{#if typeof value === 'string' && value.startsWith('$jsonvar:')}
<div class="text-2xs text-tertiary">
Sensitive — stored as secret: <code class="text-2xs">{value.slice('$jsonvar:'.length)}</code
>
</div>
{:else}
<div class="text-2xs text-tertiary italic">Sensitive — will be stored as secret on submit</div
>
{/if}
{/if}
{#if !compact || (error && error != '')}
<div class="text-right text-xs leading-3 text-red-600 dark:text-red-400 mb-2">
{#if disabled || error === ''}
&nbsp;
{:else}
{error}
{/if}
</div>
{:else if !noMargin}
<div class="mb-2"></div>
{/if}
{#if !s3StorageConfigured && extra['x-no-s3-storage-workspace-warning']}
<Alert
type="warning"
title={extra['x-no-s3-storage-workspace-warning']}
size="xs"
titleClass="text-2xs"
/>
{/if}
</div>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
</style>