mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 08:07:15 +00:00
feat: add form validation for apps
This commit is contained in:
@@ -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
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user