feat: add form validation for apps

This commit is contained in:
Ruben Fiszel
2023-10-22 17:55:05 +02:00
parent 8a8feb378e
commit 8ac0562a36
12 changed files with 149 additions and 28 deletions
@@ -52,7 +52,7 @@
$: validateInput(pattern, value)
let error: string = ''
export let error: string = ''
let el: HTMLTextAreaElement | undefined = undefined
@@ -185,9 +185,9 @@
}}
type="number"
class={twMerge(
valid
valid && error == ''
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'
)}
placeholder={defaultValue ?? ''}
bind:value
@@ -200,9 +200,9 @@
on:pointerdown={(e) => {
e?.stopPropagation()
}}
class={valid
class={valid && error == ''
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'}
bind:checked={value}
/>
{#if type == 'boolean' && value == undefined}
@@ -292,9 +292,9 @@
}}
use:autosize
style="max-height: {maxHeight}"
class="col-span-10 {valid
class="col-span-10 {valid && error == ''
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'}"
placeholder={defaultValue ? JSON.stringify(defaultValue, null, 4) : ''}
bind:value={rawValue}
/>
@@ -340,9 +340,9 @@
}}
use:autosize
type="text"
class="col-span-10 {valid
class="col-span-10 {valid && error == ''
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'}"
placeholder={defaultValue ?? ''}
bind:value
on:pointerdown|stopPropagation={(e) => {
@@ -14,6 +14,8 @@
export let isValid: boolean = true
let inputCheck: { [id: string]: boolean } = {}
let errors: { [id: string]: string } = {}
$: isValid = allTrue(inputCheck) ?? false
$: if (args === undefined) {
@@ -22,6 +24,21 @@
reorder()
export function invalidate(key: string, error: string) {
inputCheck[key] = false
errors[key] = error
}
export function validate(key: string) {
inputCheck[key] = true
errors[key] = ''
}
export function validateAll() {
inputCheck = Object.fromEntries(Object.entries(inputCheck).map((x) => [x[0], true]))
errors = Object.fromEntries(Object.entries(errors).map((x) => [x[0], '']))
}
function reorder() {
if (schema.order && Array.isArray(schema.order)) {
const n = {}
@@ -50,6 +67,7 @@
description={schema.properties[argName].description}
bind:value={args[argName]}
bind:valid={inputCheck[argName]}
bind:error={errors[argName]}
type={schema.properties[argName].type}
required={schema.required?.includes(argName) ?? false}
pattern={schema.properties[argName].pattern}
@@ -46,6 +46,15 @@
$componentControl[id] = {
setValue(nvalue: string) {
wrapper?.setArgs(nvalue)
},
invalidate(key: string, error: string) {
runnableComponent?.invalidate(key, error)
},
validateAll() {
runnableComponent?.validateAll()
},
validate(key: string) {
runnableComponent?.validate(key)
}
}
@@ -35,6 +35,15 @@
$componentControl[id] = {
onDelete: () => {
modal?.close()
},
invalidate(key: string, error: string) {
runnableComponent?.invalidate(key, error)
},
validateAll() {
runnableComponent?.validateAll()
},
validate(key: string) {
runnableComponent?.validate(key)
}
}
@@ -55,9 +55,20 @@
}
}
let schemaForm: LightweightSchemaForm
$componentControl[id] = {
setValue(nvalue: any) {
args = nvalue
},
invalidate(key: string, error: string) {
schemaForm?.invalidate(key, error)
},
validateAll() {
schemaForm?.validateAll()
},
validate(key: string) {
schemaForm?.validate(key)
}
}
@@ -106,6 +117,7 @@
schema={result}
bind:isValid={valid}
bind:args
bind:this={schemaForm}
displayType={Boolean(resolvedConfig.displayType)}
largeGap={Boolean(resolvedConfig.largeGap)}
{css}
@@ -131,6 +131,20 @@
}
}
let schemaForm: LightweightSchemaForm
export function invalidate(key: string, error: string) {
schemaForm?.invalidate(key, error)
}
export function validate(key: string) {
schemaForm?.validate(key)
}
export function validateAll() {
schemaForm?.validateAll()
}
// Test job internal state
let resultJobLoader: ResultJobLoader | undefined = undefined
@@ -568,6 +582,7 @@
<div class="px-2 h-fit min-h-0">
<LightweightSchemaForm
schema={schemaStripped}
bind:this={schemaForm}
bind:args
on:inputClicked={handleInputClick}
/>
@@ -20,7 +20,7 @@ export function computeGlobalContext(world: World | undefined, extraContext: any
function create_context_function_template(eval_string: string, context, noReturn: boolean) {
return `
return async function (context, state, goto, setTab, recompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close) {
return async function (context, state, goto, setTab, recompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll) {
"use strict";
${
Object.keys(context).length > 0
@@ -52,7 +52,10 @@ function make_context_evaluator(
openModal,
closeModal,
open,
close
close,
validate,
invalidate,
validateAll
) => Promise<any> {
let template = create_context_function_template(eval_string, context, noReturn)
let functor = Function(template)
@@ -108,6 +111,9 @@ export async function eval_like(
closeModal?: () => void
open?: () => void
close?: () => void
validate?: (key: string) => void
invalidate?: (key: string, error: string) => void
validateAll?: () => void
}
>,
worldStore: World | undefined,
@@ -170,6 +176,15 @@ export async function eval_like(
},
(id) => {
controlComponents[id]?.close?.()
},
(id, key) => {
controlComponents[id]?.validate?.(key)
},
(id, key, error) => {
controlComponents[id]?.invalidate?.(key, error)
},
(id) => {
controlComponents[id]?.validateAll?.()
}
)
}
@@ -4,7 +4,7 @@ export type ComponentFunction = {
title: string
description: string
example: string
documentation: string
documentation?: string
}
const setTab = {
@@ -57,6 +57,24 @@ const close = {
documentation: 'https://www.windmill.dev/docs/apps/app-runnable-panel#close'
}
const validate = {
title: 'validate',
description: 'Validate a specific field of a form',
example: 'validate(id: string, key: string)',
}
const invalidate = {
title: 'invalidate',
description: 'Invalidate a specific field of a form',
example: 'validate(id: string, key: string, error: string)',
}
const validateAll = {
title: 'validateAll',
description: 'Validate all fields of a form',
example: 'validate(id: string, key: string)',
}
export function getComponentControl(type: keyof typeof components): Array<ComponentFunction> {
switch (type) {
case 'tabscomponent':
@@ -83,16 +101,18 @@ export function getComponentControl(type: keyof typeof components): Array<Compon
case 'numberinputcomponent':
case 'currencycomponent':
case 'checkboxcomponent':
case 'formcomponent':
case 'rangecomponent':
case 'multiselectcomponent':
case 'selectcomponent':
case 'slidercomponent':
case 'schemaformcomponent':
case 'quillcomponent':
case 'textcomponent':
case 'textareainputcomponent':
return [setValue]
case 'formcomponent':
case 'schemaformcomponent':
case 'formbuttoncomponent':
return [setValue, validate, validateAll, invalidate]
case 'tablecomponent':
return [setSelectedIndex]
default:
@@ -47,20 +47,21 @@
<div class="text-xs">
{control.description}
</div>
<div class="p-1 border w-full">
<div class="p-1 border w-full overflow-x-auto">
<Highlight language={typescript} code={control.example} />
</div>
<a
href={control.documentation}
target="_blank"
class="text-frost-500 dark:text-frost-300 font-semibold text-xs"
>
<div class="flex flex-row gap-2">
See documentation
<ExternalLink size="16" />
</div>
</a>
{#if control.documentation}
<a
href={control.documentation}
target="_blank"
class="text-frost-500 dark:text-frost-300 font-semibold text-xs"
>
<div class="flex flex-row gap-2">
See documentation
<ExternalLink size="16" />
</div>
</a>
{/if}
{/each}
{/if}
</PanelSection>
@@ -21,7 +21,7 @@
<PanelSection
title="Trigger runnables on success"
tooltip="Select components to recompute after running this runnable as a success"
tooltip="Select components to recompute after this runnable has successfully run"
documentationLink="https://www.windmill.dev/docs/apps/app-runnable-panel#recompute-others"
>
{#if Object.keys($runnableComponents ?? {}).filter((id) => id !== ownId).length > 0}
@@ -240,6 +240,9 @@ export type AppViewerContext = {
closeModal?: () => void
open?: () => void
close?: () => void
validate?: (key: string) => void
invalidate?: (key: string, error: string) => void
validateAll?: () => void
}
>
>
+19
View File
@@ -217,6 +217,25 @@ declare function open(id: string): void;
* @param id component's id
*/
declare function close(id: string): void;
/** validate form field property 'key'
* @param id component's id
* @param key property's key to validate
*/
declare function validate(id: string, key: number): void;
/** validate form field property 'key'
* @param id component's id
* @param key property's key to validate
*/
declare function invalidate(id: string, key: number, error: string): void;
/** validate all form's properties
* @param id component's id
*/
declare function validateAll(id: string, key: number): void;
`
: ''
}