mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 00:06:14 +00:00
feat(frontend): Added support for title and placeholder for ArgInputs (#3779)
* feat(frontend): Added support for customLabel and placeholder * feat(frontend): correctly handle labels * feat(frontend): rename customLabel into title * feat(frontend): fix layout * feat(frontend): properly implement placeholder * feat(frontend): properly implement placeholder * feat(frontend): add missing undefined check * feat(frontend): remove code duplication
This commit is contained in:
@@ -41,6 +41,8 @@ export interface SchemaProperty {
|
||||
order?: string[]
|
||||
nullable?: boolean
|
||||
dateFormat?: string
|
||||
title?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export interface ModalSchemaProperty {
|
||||
@@ -65,6 +67,8 @@ export interface ModalSchemaProperty {
|
||||
password?: boolean
|
||||
nullable?: boolean
|
||||
dateFormat?: string
|
||||
title?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
|
||||
@@ -88,7 +92,9 @@ export function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
|
||||
showExpr: schema.showExpr,
|
||||
password: schema.password,
|
||||
nullable: schema.nullable,
|
||||
dateFormat: schema.dateFormat
|
||||
dateFormat: schema.dateFormat,
|
||||
title: schema.title,
|
||||
placeholder: schema.placeholder
|
||||
}
|
||||
}
|
||||
export type Schema = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import { setInputCat as computeInputCat, emptyString } from '$lib/utils'
|
||||
import { setInputCat as computeInputCat, emptyString, shouldDisplayPlaceholder } from '$lib/utils'
|
||||
import { ChevronDown, DollarSign, Pipette, Plus, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
@@ -30,6 +30,8 @@
|
||||
import autosize from '$lib/autosize'
|
||||
import PasswordArgInput from './PasswordArgInput.svelte'
|
||||
import Password from './Password.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
@@ -74,6 +76,8 @@
|
||||
export let customErrorMessage: string | undefined = undefined
|
||||
export let onlyMaskPassword = false
|
||||
export let nullable: boolean = false
|
||||
export let title: string | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
|
||||
let seeEditable: boolean = enum_ != undefined || pattern != undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -233,7 +237,7 @@
|
||||
{#if displayHeader}
|
||||
<FieldHeader
|
||||
prettify={prettifyHeader}
|
||||
{label}
|
||||
label={title && !emptyString(title) ? title : label}
|
||||
{disabled}
|
||||
{required}
|
||||
{type}
|
||||
@@ -241,6 +245,7 @@
|
||||
{format}
|
||||
{simpleTooltip}
|
||||
/>
|
||||
|
||||
{#if editableSchema}
|
||||
<span class="mx-8" />
|
||||
{#if editableSchema.i > 0}
|
||||
@@ -260,7 +265,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{#if editableSchema}
|
||||
<label class="text-secondary">
|
||||
<Label label="Description">
|
||||
<textarea
|
||||
class="mb-1"
|
||||
use:autosize
|
||||
@@ -269,7 +274,15 @@
|
||||
on:keydown={onKeyDown}
|
||||
placeholder="Field description"
|
||||
/>
|
||||
</label>
|
||||
</Label>
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<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 class="mb-1" bind:value={title} on:keydown={onKeyDown} placeholder="Field title" />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{#if type == 'array'}
|
||||
<ArrayTypeNarrowing bind:itemsType />
|
||||
@@ -317,6 +330,11 @@
|
||||
<ObjectTypeNarrowing bind:format />
|
||||
{/if}
|
||||
</div>
|
||||
{#if shouldDisplayPlaceholder(type, format, enum_, contentEncoding, pattern)}
|
||||
<Label label="Placeholder" class="pt-2">
|
||||
<textarea placeholder="Enter a placeholder" rows="1" bind:value={placeholder} />
|
||||
</Label>
|
||||
{/if}
|
||||
{#if !required && type === 'string'}
|
||||
<div class="mt-2 border-t pt-4">
|
||||
<Toggle
|
||||
@@ -381,7 +399,7 @@
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
|
||||
placeholder={defaultValue ?? ''}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
bind:value
|
||||
min={extra['min']}
|
||||
max={extra['max']}
|
||||
@@ -680,7 +698,7 @@
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'}
|
||||
placeholder={defaultValue ?? ''}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
bind:value
|
||||
/>
|
||||
{:else if inputCat == 'string'}
|
||||
@@ -688,7 +706,11 @@
|
||||
<div class="flex flex-row w-full items-center justify-between relative">
|
||||
{#if password || extra?.['password'] == true}
|
||||
{#if onlyMaskPassword}
|
||||
<Password {disabled} bind:password={value} placeholder={defaultValue ?? ''} />
|
||||
<Password
|
||||
{disabled}
|
||||
bind:password={value}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
/>
|
||||
{:else}
|
||||
<PasswordArgInput {disabled} bind:value />
|
||||
{/if}
|
||||
@@ -713,7 +735,7 @@
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'
|
||||
)}
|
||||
placeholder={defaultValue ?? ''}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
bind:value
|
||||
/>
|
||||
{/key}
|
||||
|
||||
@@ -152,6 +152,8 @@
|
||||
bind:extra={schema.properties[argName]}
|
||||
simpleTooltip={schemaFieldTooltip[argName]}
|
||||
nullable={schema.properties[argName].nullable}
|
||||
bind:title={schema.properties[argName].title}
|
||||
bind:placeholder={schema.properties[argName].placeholder}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -407,6 +407,8 @@
|
||||
bind:pickForField
|
||||
showSchemaExplorer
|
||||
nullable={schema.properties[argName].nullable}
|
||||
bind:title={schema.properties[argName].title}
|
||||
bind:placeholder={schema.properties[argName].placeholder}
|
||||
/>
|
||||
{:else if arg.expr != undefined}
|
||||
<div class="border mt-2">
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
export let hideResourceInput: boolean = false
|
||||
export let resourceInputUnsupported: boolean = false
|
||||
export let render = true
|
||||
export let title: string | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -180,8 +182,8 @@
|
||||
<div>
|
||||
{#if displayHeader}
|
||||
<FieldHeader
|
||||
prettify
|
||||
{label}
|
||||
prettify={emptyString(title)}
|
||||
label={title && !emptyString(title) ? title : label}
|
||||
{required}
|
||||
{type}
|
||||
{contentEncoding}
|
||||
@@ -231,7 +233,7 @@
|
||||
? ''
|
||||
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'
|
||||
)}
|
||||
placeholder={defaultValue ?? ''}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
bind:value
|
||||
min={extra['min']}
|
||||
max={extra['max']}
|
||||
@@ -430,7 +432,7 @@
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'}
|
||||
placeholder={defaultValue ?? ''}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
bind:value
|
||||
/>
|
||||
{:else if inputCat == 'currency'}
|
||||
@@ -439,7 +441,7 @@
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'}
|
||||
placeholder={defaultValue ?? ''}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
bind:value
|
||||
/>
|
||||
{:else if inputCat == 'string'}
|
||||
@@ -458,7 +460,7 @@
|
||||
class="col-span-10 {valid && error == ''
|
||||
? ''
|
||||
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'}"
|
||||
placeholder={defaultValue ?? ''}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
bind:value
|
||||
on:pointerdown|stopPropagation={(e) => {
|
||||
dispatch('inputClicked', e)
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
nestedRequired={schema.properties[argName].required}
|
||||
itemsType={schema.properties[argName].items}
|
||||
extra={schema.properties[argName]}
|
||||
title={schema.properties[argName].title}
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
on:inputClicked
|
||||
{displayType}
|
||||
{css}
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
itemsType={schema.properties[argName].items}
|
||||
extra={schema.properties[argName]}
|
||||
nullable={schema.properties[argName].nullable}
|
||||
title={schema.properties[argName].title}
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
/>
|
||||
{/if}
|
||||
<div class="pt-6 mt-0.5">
|
||||
|
||||
@@ -159,6 +159,8 @@
|
||||
simpleTooltip={schemaFieldTooltip[argName]}
|
||||
{onlyMaskPassword}
|
||||
nullable={schema.properties[argName].nullable}
|
||||
title={schema.properties[argName].title}
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if linkedSecretCandidates?.includes(argName)}
|
||||
|
||||
@@ -62,7 +62,9 @@
|
||||
showExpr: schema.showExpr,
|
||||
password: schema.password,
|
||||
nullable: schema.nullable,
|
||||
dateFormat: schema.format
|
||||
dateFormat: schema.format,
|
||||
title: schema.title,
|
||||
placeholder: schema.placeholder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +90,8 @@
|
||||
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
|
||||
import NumberTypeNarrowing from './NumberTypeNarrowing.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import { shouldDisplayPlaceholder } from '$lib/utils'
|
||||
|
||||
export let error = ''
|
||||
export let editing = false
|
||||
@@ -138,6 +142,8 @@
|
||||
property.password = undefined
|
||||
property.nullable = false
|
||||
property.dateFormat = undefined
|
||||
property.title = undefined
|
||||
property.placeholder = undefined
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
|
||||
@@ -157,6 +163,16 @@
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function shouldDisplayPlaceholderForProperty(property: ModalSchemaProperty): boolean {
|
||||
return shouldDisplayPlaceholder(
|
||||
property.selectedType,
|
||||
property.format,
|
||||
property.enum_,
|
||||
property.contentEncoding,
|
||||
property.pattern
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} placement="right">
|
||||
@@ -192,6 +208,11 @@
|
||||
</div>
|
||||
<textarea placeholder="Enter a description" rows="3" bind:value={property.description} />
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 font-semibold text-secondary"> Custom Title </div>
|
||||
<textarea placeholder="Enter a custom title" rows="1" bind:value={property.title} />
|
||||
</label>
|
||||
<div>
|
||||
<div class="mb-1 font-semibold text-secondary">Type<Required required={true} /></div>
|
||||
<div class="grid sm:grid-cols-3 md:grid-cols-4 gap-x-2 gap-y-1 items-center mb-2 w-full">
|
||||
@@ -266,6 +287,8 @@
|
||||
extra={property}
|
||||
disabled={property.password}
|
||||
nullable={property.nullable}
|
||||
title={property.title}
|
||||
placeholder={property.placeholder}
|
||||
/>
|
||||
<div>
|
||||
<Toggle
|
||||
@@ -341,6 +364,16 @@
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{/if}
|
||||
{#if property && shouldDisplayPlaceholderForProperty(property)}
|
||||
<Label label="Placeholder" class="pt-2">
|
||||
<textarea
|
||||
placeholder="Enter a placeholder"
|
||||
rows="1"
|
||||
bind:value={property.placeholder}
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="pt-2">
|
||||
<Toggle
|
||||
options={{ right: 'Show this field only when conditions are met' }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { computeKind } from '$lib/utils'
|
||||
import Label from './Label.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import ResourceTypePicker from './ResourceTypePicker.svelte'
|
||||
@@ -21,7 +22,13 @@
|
||||
export let noExtra = false
|
||||
export let dateFormat: string | undefined
|
||||
|
||||
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' = computeKind()
|
||||
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' = computeKind(
|
||||
enum_,
|
||||
contentEncoding,
|
||||
pattern,
|
||||
format
|
||||
)
|
||||
|
||||
let patternStr: string = pattern ?? ''
|
||||
let resource: string | undefined
|
||||
|
||||
@@ -64,24 +71,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function computeKind(): 'base64' | 'none' | 'pattern' | 'enum' | 'resource' | 'format' {
|
||||
if (enum_ != undefined) {
|
||||
return 'enum'
|
||||
}
|
||||
if (contentEncoding == 'base64') {
|
||||
return 'base64'
|
||||
}
|
||||
if (pattern != undefined) {
|
||||
return 'pattern'
|
||||
}
|
||||
if (format != undefined && format != '') {
|
||||
if (format.startsWith('resource')) {
|
||||
return 'resource'
|
||||
}
|
||||
return 'format'
|
||||
}
|
||||
return 'none'
|
||||
}
|
||||
const presetOptions = [
|
||||
{ label: 'ISO Format', format: 'yyyy-MM-dd' },
|
||||
{ label: 'US Format', format: 'MM/dd/yyyy' },
|
||||
|
||||
@@ -52,7 +52,9 @@
|
||||
...componentInput,
|
||||
type: 'evalv2',
|
||||
expr: expr,
|
||||
connections: [{ componentId: connection.componentId, id: connection.path.split('.')[0].split('[')[0] }]
|
||||
connections: [
|
||||
{ componentId: connection.componentId, id: connection.path.split('.')[0].split('[')[0] }
|
||||
]
|
||||
}
|
||||
evalV2editor?.setCode(expr)
|
||||
$app = $app
|
||||
|
||||
@@ -851,3 +851,49 @@ export function getLocalSetting(name: string) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function computeKind(
|
||||
enum_: string[] | undefined,
|
||||
contentEncoding: 'base64' | 'binary' | undefined,
|
||||
pattern: string | undefined,
|
||||
format: string | undefined
|
||||
): 'base64' | 'none' | 'pattern' | 'enum' | 'resource' | 'format' {
|
||||
if (enum_ != undefined) {
|
||||
return 'enum'
|
||||
}
|
||||
if (contentEncoding == 'base64') {
|
||||
return 'base64'
|
||||
}
|
||||
if (pattern != undefined) {
|
||||
return 'pattern'
|
||||
}
|
||||
if (format != undefined && format != '') {
|
||||
if (format?.startsWith('resource')) {
|
||||
return 'resource'
|
||||
}
|
||||
return 'format'
|
||||
}
|
||||
return 'none'
|
||||
}
|
||||
|
||||
// Used to check whether a placeholder should be displayed in the input field, based on the schema
|
||||
export function shouldDisplayPlaceholder(
|
||||
type: string | undefined,
|
||||
format: string | undefined,
|
||||
enum_: string[] | undefined,
|
||||
contentEncoding: 'base64' | 'binary' | undefined,
|
||||
pattern: string | undefined
|
||||
): boolean {
|
||||
if (type === 'string') {
|
||||
const kind = computeKind(enum_, contentEncoding, pattern, format)
|
||||
|
||||
if (kind === 'format' && format) {
|
||||
const whiteList = ['email', 'hostname', 'ipv4', 'uri', 'uuid']
|
||||
return whiteList.includes(format)
|
||||
}
|
||||
|
||||
return kind === 'none' || kind === 'pattern'
|
||||
}
|
||||
|
||||
return type === 'number' || type === 'integer' || type === undefined
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { defaultScripts, workspaceStore } from '$lib/stores'
|
||||
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { decodeState, emptySchema } from '$lib/utils'
|
||||
import { decodeState, emptySchema, emptyString } from '$lib/utils'
|
||||
import { goto } from '$app/navigation'
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: templatePath
|
||||
})
|
||||
script.summary = `Copy of ${template.summary}`
|
||||
|
||||
// Only copy the summary if it's not empty
|
||||
script.summary = !emptyString(template.summary) ? `Copy of ${template.summary}` : ''
|
||||
script.description = template.description
|
||||
script.content = template.content
|
||||
script.schema = template.schema
|
||||
|
||||
Reference in New Issue
Block a user