feat: custom error messages for forms

This commit is contained in:
Ruben Fiszel
2023-11-19 13:54:32 +01:00
parent 6cdd2bceee
commit f543aa5bf9
10 changed files with 192 additions and 92 deletions
+31
View File
@@ -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
+29 -4
View File
@@ -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}
+17 -15
View File
@@ -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">
-1
View File
@@ -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'
+3
View File
@@ -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'
}