mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 00:01:55 +00:00
feat: custom error messages for forms
This commit is contained in:
@@ -28,10 +28,41 @@ export interface SchemaProperty {
|
||||
contentEncoding?: 'base64'
|
||||
enum?: string[]
|
||||
}
|
||||
customErrorMessage?: string
|
||||
properties?: { [name: string]: SchemaProperty }
|
||||
required?: string[]
|
||||
}
|
||||
|
||||
export interface ModalSchemaProperty {
|
||||
selectedType?: string
|
||||
description: string
|
||||
name: string
|
||||
required: boolean
|
||||
format?: string
|
||||
pattern?: string
|
||||
enum_?: string[]
|
||||
default?: any
|
||||
items?: { type?: 'string' | 'number' }
|
||||
contentEncoding?: 'base64' | 'binary'
|
||||
schema?: Schema
|
||||
customErrorMessage?: string
|
||||
}
|
||||
|
||||
export function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
|
||||
return {
|
||||
type: schema.selectedType,
|
||||
description: schema.description,
|
||||
pattern: schema.pattern,
|
||||
default: schema.default,
|
||||
enum: schema.enum_,
|
||||
items: schema.items,
|
||||
contentEncoding: schema.contentEncoding,
|
||||
format: schema.format,
|
||||
customErrorMessage: schema.customErrorMessage,
|
||||
properties: schema.schema?.properties,
|
||||
required: schema.schema?.required
|
||||
}
|
||||
}
|
||||
export type Schema = {
|
||||
$schema: string | undefined
|
||||
type: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import { setInputCat as computeInputCat } from '$lib/utils'
|
||||
import { setInputCat as computeInputCat, emptyString } from '$lib/utils'
|
||||
import { ChevronDown, DollarSign, Plus, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import autosize from 'svelte-autosize'
|
||||
@@ -66,6 +66,7 @@
|
||||
export let disablePortal = false
|
||||
export let showSchemaExplorer = false
|
||||
export let simpleTooltip: string | undefined = undefined
|
||||
export let customErrorMessage: string | undefined = undefined
|
||||
|
||||
let seeEditable: boolean = enum_ != undefined || pattern != undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -150,7 +151,13 @@
|
||||
valid && (valid = false)
|
||||
} else {
|
||||
if (pattern && !testRegex(pattern, v)) {
|
||||
error = `Should match ${pattern}`
|
||||
if (!emptyString(customErrorMessage)) {
|
||||
error = customErrorMessage ?? ''
|
||||
} else if (format == 'email') {
|
||||
error = 'invalid email address'
|
||||
} else {
|
||||
error = `Should match ${pattern}`
|
||||
}
|
||||
valid && (valid = false)
|
||||
} else {
|
||||
error = ''
|
||||
@@ -234,7 +241,13 @@
|
||||
{#if seeEditable}
|
||||
<div class="mt-2">
|
||||
{#if type == 'string' && format != 'date-time'}
|
||||
<StringTypeNarrowing bind:format bind:pattern bind:enum_ bind:contentEncoding />
|
||||
<StringTypeNarrowing
|
||||
bind:customErrorMessage
|
||||
bind:format
|
||||
bind:pattern
|
||||
bind:enum_
|
||||
bind:contentEncoding
|
||||
/>
|
||||
{:else if type == 'number'}
|
||||
<NumberTypeNarrowing bind:min={extra['min']} bind:max={extra['max']} />
|
||||
{:else if type == 'object'}
|
||||
@@ -446,6 +459,18 @@
|
||||
: undefined}
|
||||
{showSchemaExplorer}
|
||||
/>
|
||||
{:else if inputCat == 'email'}
|
||||
<input
|
||||
{autofocus}
|
||||
on:focus
|
||||
{disabled}
|
||||
type="email"
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'}
|
||||
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">
|
||||
@@ -467,7 +492,7 @@
|
||||
'w-full',
|
||||
valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'
|
||||
)}
|
||||
placeholder={defaultValue ?? ''}
|
||||
bind:value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { setInputCat as computeInputCat } from '$lib/utils'
|
||||
import { setInputCat as computeInputCat, emptyString } from '$lib/utils'
|
||||
import { Badge, Button } from './common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
@@ -43,13 +43,12 @@
|
||||
export let nestedRequired: string[] | undefined = undefined
|
||||
export let extra: Record<string, any> = {}
|
||||
export let displayType: boolean = true
|
||||
export let customErrorMessage: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
|
||||
|
||||
$: validateInput(pattern, value)
|
||||
|
||||
export let error: string = ''
|
||||
|
||||
let el: HTMLTextAreaElement | undefined = undefined
|
||||
@@ -68,10 +67,8 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
error = ''
|
||||
if (inputCat === 'object') {
|
||||
evalValueToRaw()
|
||||
validateInput(pattern, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,17 +99,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function validateInput(pattern: string | undefined, v: any): void {
|
||||
function validateInput(pattern: string | undefined, v: any, required: boolean): void {
|
||||
if (required && (v == undefined || v == null || v === '')) {
|
||||
error = 'Required'
|
||||
valid = false
|
||||
valid && (valid = false)
|
||||
} else {
|
||||
if (pattern && !testRegex(pattern, v)) {
|
||||
error = `Should match ${pattern}`
|
||||
valid = false
|
||||
if (!emptyString(customErrorMessage)) {
|
||||
error = customErrorMessage ?? ''
|
||||
} else if (format == 'email') {
|
||||
error = 'invalid email address'
|
||||
} else {
|
||||
error = `should match ${pattern}`
|
||||
}
|
||||
valid && (valid = false)
|
||||
} else {
|
||||
error = ''
|
||||
valid = true
|
||||
!valid && (valid = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +144,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: validateInput(pattern, value, required)
|
||||
|
||||
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
|
||||
</script>
|
||||
|
||||
@@ -209,47 +214,51 @@
|
||||
{:else if inputCat == 'list'}
|
||||
<div class="w-full">
|
||||
<div class="w-full">
|
||||
{#each value ?? [] as v, i}
|
||||
<div class="flex flex-row max-w-md mt-1 w-full">
|
||||
{#if itemsType?.type == 'number'}
|
||||
<input type="number" bind:value={v} />
|
||||
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
|
||||
<input
|
||||
type="file"
|
||||
class="my-6"
|
||||
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
{:else if Array.isArray(itemsType?.enum)}
|
||||
<select
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
{#if Array.isArray(value)}
|
||||
{#each value ?? [] as v, i}
|
||||
<div class="flex flex-row max-w-md mt-1 w-full">
|
||||
{#if itemsType?.type == 'number'}
|
||||
<input type="number" bind:value={v} />
|
||||
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
|
||||
<input
|
||||
type="file"
|
||||
class="my-6"
|
||||
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
{:else if Array.isArray(itemsType?.enum)}
|
||||
<select
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
class="px-6"
|
||||
bind:value={v}
|
||||
>
|
||||
{#each itemsType?.enum ?? [] as e}
|
||||
<option>{e}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<input type="text" bind:value={v} />
|
||||
{/if}
|
||||
<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"
|
||||
on:click={() => {
|
||||
value = value.filter((el) => el != v)
|
||||
if (value.length == 0) {
|
||||
value = undefined
|
||||
}
|
||||
}}
|
||||
class="px-6"
|
||||
bind:value={v}
|
||||
>
|
||||
{#each itemsType?.enum ?? [] as e}
|
||||
<option>{e}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<input type="text" bind:value={v} />
|
||||
{/if}
|
||||
<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"
|
||||
on:click={() => {
|
||||
value = value.filter((el) => el != v)
|
||||
if (value.length == 0) {
|
||||
value = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
List is not an array
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex my-2">
|
||||
<Button
|
||||
@@ -269,7 +278,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<span class="ml-2">
|
||||
{(value ?? []).length} item{(value ?? []).length > 1 ? 's' : ''}
|
||||
{(value ?? []).length} item{(value ?? []).length != 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
{:else if inputCat == 'resource-object'}
|
||||
@@ -312,12 +321,16 @@
|
||||
{:else if inputCat == 'date'}
|
||||
<DateTimeInput bind:value />
|
||||
{:else if inputCat == 'base64'}
|
||||
<input
|
||||
type="file"
|
||||
class="my-6"
|
||||
on:change={(x) => fileChanged(x, (val) => (value = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
<div class="flex flex-col my-6 w-full">
|
||||
<input
|
||||
type="file"
|
||||
on:change={(x) => fileChanged(x, (val) => (value = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
{#if value?.length}
|
||||
<div class="text-2xs text-tertiary mt-1">File length: {value.length} base64 chars</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if inputCat == 'resource-string'}
|
||||
<div class="flex flex-row gap-x-1 w-full">
|
||||
<LightweightResourcePicker
|
||||
@@ -327,6 +340,16 @@
|
||||
: undefined}
|
||||
/>
|
||||
</div>
|
||||
{:else if inputCat == 'email'}
|
||||
<input
|
||||
on:focus
|
||||
type="email"
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'}
|
||||
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">
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
enum_={schema.properties[argName].enum}
|
||||
format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
properties={schema.properties[argName].properties}
|
||||
nestedRequired={schema.properties[argName].required}
|
||||
itemsType={schema.properties[argName].items}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { Schema, SchemaProperty, PropertyDisplayInfo } from '$lib/common'
|
||||
import {
|
||||
type Schema,
|
||||
type SchemaProperty,
|
||||
type PropertyDisplayInfo,
|
||||
modalToSchema,
|
||||
type ModalSchemaProperty
|
||||
} from '$lib/common'
|
||||
import { emptySchema, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { Button } from './common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { ModalSchemaProperty } from './SchemaModal.svelte'
|
||||
import SchemaModal, { DEFAULT_PROPERTY, schemaToModal } from './SchemaModal.svelte'
|
||||
import PropertyRow from './PropertyRow.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
@@ -79,20 +84,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
|
||||
return {
|
||||
type: schema.selectedType,
|
||||
description: schema.description,
|
||||
pattern: schema.pattern,
|
||||
default: schema.default,
|
||||
enum: schema.enum_,
|
||||
items: schema.items,
|
||||
contentEncoding: schema.contentEncoding,
|
||||
format: schema.format,
|
||||
properties: schema.schema?.properties,
|
||||
required: schema.schema?.required
|
||||
}
|
||||
}
|
||||
function handleAddOrEditArgument(modalProperty: ModalSchemaProperty): void {
|
||||
// If editing the arg's name, oldName containing the old argument name must be provided
|
||||
argError = ''
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
bind:contentEncoding={schema.properties[argName].contentEncoding}
|
||||
bind:customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
properties={schema.properties[argName].properties}
|
||||
nestedRequired={schema.properties[argName].required}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
@@ -131,6 +132,7 @@
|
||||
enum_={schema.properties[argName].enum}
|
||||
format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
properties={schema.properties[argName].properties}
|
||||
nestedRequired={schema.properties[argName].required}
|
||||
itemsType={schema.properties[argName].items}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { SchemaProperty, Schema } from '$lib/common'
|
||||
import { type SchemaProperty, type ModalSchemaProperty, modalToSchema } from '$lib/common'
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
@@ -8,20 +8,6 @@
|
||||
export const ARG_TYPES = ['integer', 'number', 'string', 'boolean', 'object', 'array'] as const
|
||||
export type ArgType = (typeof ARG_TYPES)[number]
|
||||
|
||||
export interface ModalSchemaProperty {
|
||||
selectedType?: string
|
||||
description: string
|
||||
name: string
|
||||
required: boolean
|
||||
format?: string
|
||||
pattern?: string
|
||||
enum_?: string[]
|
||||
default?: any
|
||||
items?: { type?: 'string' | 'number' }
|
||||
contentEncoding?: 'base64' | 'binary'
|
||||
schema?: Schema
|
||||
}
|
||||
|
||||
export function schemaToModal(
|
||||
schema: SchemaProperty,
|
||||
name: string,
|
||||
@@ -68,6 +54,7 @@
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
import ArrayTypeNarrowing from './ArrayTypeNarrowing.svelte'
|
||||
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
|
||||
|
||||
export let error = ''
|
||||
export let editing = false
|
||||
@@ -202,7 +189,10 @@
|
||||
bind:value={property.default}
|
||||
type={property.selectedType}
|
||||
pattern={property.pattern}
|
||||
customErrorMessage={property.customErrorMessage}
|
||||
itemsType={property.items}
|
||||
contentEncoding={property.contentEncoding}
|
||||
format={property.format}
|
||||
/>
|
||||
<Toggle
|
||||
options={{ right: 'Required' }}
|
||||
@@ -224,6 +214,7 @@
|
||||
<div class="font-semibold text-secondary mb-1">Advanced</div>
|
||||
{#if property.selectedType == 'string'}
|
||||
<StringTypeNarrowing
|
||||
bind:customErrorMessage={property.customErrorMessage}
|
||||
bind:format={property.format}
|
||||
bind:pattern={property.pattern}
|
||||
bind:enum_={property.enum_}
|
||||
@@ -254,7 +245,18 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="font-semibold text-secondary mb-1 pt-4">Preview</div>
|
||||
<LightweightSchemaForm
|
||||
displayType={false}
|
||||
schema={{
|
||||
properties: {
|
||||
[property.name]: modalToSchema(property)
|
||||
},
|
||||
required: property.required ? [property.name] : []
|
||||
}}
|
||||
/>
|
||||
<svelte:fragment slot="actions">
|
||||
<div class="h-10" />
|
||||
<Button
|
||||
color="dark"
|
||||
disabled={!property.name || error != ''}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts">
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import ResourceTypePicker from './ResourceTypePicker.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { Button } from './common'
|
||||
|
||||
export let pattern: string | undefined
|
||||
export let enum_: string[] | undefined
|
||||
export let format: string | undefined
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined
|
||||
export let customErrorMessage: string | undefined
|
||||
|
||||
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' = computeKind()
|
||||
let patternStr: string = pattern ?? ''
|
||||
@@ -32,6 +34,11 @@
|
||||
$: pattern = patternStr == '' ? undefined : patternStr
|
||||
$: contentEncoding = kind == 'base64' ? 'base64' : undefined
|
||||
|
||||
$: {
|
||||
if (format == 'email') {
|
||||
pattern = '^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$'
|
||||
}
|
||||
}
|
||||
function add() {
|
||||
enum_ = enum_ ? enum_.concat('') : ['']
|
||||
}
|
||||
@@ -103,6 +110,22 @@
|
||||
clear
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Custom error message' }}
|
||||
checked={customErrorMessage != undefined && customErrorMessage != ''}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
customErrorMessage = 'Custom error message'
|
||||
} else {
|
||||
customErrorMessage = undefined
|
||||
}
|
||||
}}
|
||||
>Custom error message
|
||||
</Toggle>
|
||||
<input type="text" bind:value={customErrorMessage} />
|
||||
</div>
|
||||
</label>
|
||||
{:else if kind == 'enum'}
|
||||
<label for="input" class="mb-2 text-secondary text-xs">
|
||||
|
||||
@@ -123,7 +123,6 @@ function argSigToJsonSchemaType(
|
||||
| { object: { key: string; typ: any }[] },
|
||||
oldS: SchemaProperty
|
||||
): void {
|
||||
console.log(JSON.st)
|
||||
const newS: SchemaProperty = { type: '' }
|
||||
if (t === 'int') {
|
||||
newS.type = 'integer'
|
||||
|
||||
@@ -311,6 +311,7 @@ export function isString(value: any) {
|
||||
|
||||
export type InputCat =
|
||||
| 'string'
|
||||
| 'email'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'list'
|
||||
@@ -350,6 +351,8 @@ export function setInputCat(
|
||||
return 'yaml'
|
||||
} else if (type == 'string' && contentEncoding == 'base64') {
|
||||
return 'base64'
|
||||
} else if (type == 'string' && format == 'email') {
|
||||
return 'email'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user