diff --git a/public/component_types/interfaces.ts b/public/component_types/interfaces.ts index bff6d22d..ee8bf0e3 100644 --- a/public/component_types/interfaces.ts +++ b/public/component_types/interfaces.ts @@ -4,6 +4,7 @@ */ import { FormikValues } from 'formik'; +import { ObjectSchema } from 'yup'; import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../utils'; /** @@ -18,6 +19,11 @@ export type ComponentFormValues = FormikValues; export type WorkspaceFormValues = { [componentId: string]: ComponentFormValues; }; +export type WorkspaceSchemaObj = { + [componentId: string]: ObjectSchema; +}; + +export type WorkspaceSchema = ObjectSchema; /** * Represents a single base class as an input handle for a component. diff --git a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx index b9772855..95d83772 100644 --- a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx @@ -10,8 +10,13 @@ import { EuiSuperSelectOption, EuiText, } from '@elastic/eui'; -import { Field, FieldProps } from 'formik'; -import { IComponentField, getInitialValue } from '../../../../../common'; +import { Field, FieldProps, useFormikContext } from 'formik'; +import { + IComponentField, + WorkspaceFormValues, + getInitialValue, + isFieldInvalid, +} from '../../../../../common'; // TODO: Should be fetched from global state. // Need to have a way to determine where to fetch this dynamic data. @@ -39,9 +44,8 @@ interface SelectFieldProps { */ export function SelectField(props: SelectFieldProps) { const options = existingIndices; - - // The derived form field based on the nested structure of WorkspaceFormValues. const formField = `${props.componentId}.${props.field.name}`; + const { errors, touched } = useFormikContext(); return ( @@ -55,6 +59,12 @@ export function SelectField(props: SelectFieldProps) { field.onChange(option); form.setFieldValue(formField, option); }} + isInvalid={isFieldInvalid( + props.componentId, + props.field.name, + errors, + touched + )} /> ); diff --git a/public/pages/workflow_detail/component_details/input_fields/text_field.tsx b/public/pages/workflow_detail/component_details/input_fields/text_field.tsx index f312c019..eb236574 100644 --- a/public/pages/workflow_detail/component_details/input_fields/text_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/text_field.tsx @@ -4,9 +4,15 @@ */ import React from 'react'; -import { Field, FieldProps } from 'formik'; +import { Field, FieldProps, useFormikContext } from 'formik'; import { EuiFieldText, EuiFormRow } from '@elastic/eui'; -import { IComponentField, getInitialValue } from '../../../../../common'; +import { + IComponentField, + WorkspaceFormValues, + getFieldError, + getInitialValue, + isFieldInvalid, +} from '../../../../../common'; interface TextFieldProps { field: IComponentField; @@ -17,11 +23,23 @@ interface TextFieldProps { * An input field for a component where users input plaintext */ export function TextField(props: TextFieldProps) { + const formField = `${props.componentId}.${props.field.name}`; + const { errors, touched } = useFormikContext(); + return ( - + {({ field, form }: FieldProps) => { return ( - + ({}); + const [formSchema, setFormSchema] = useState(yup.object({})); // Initialize the form state to an existing workflow, if applicable. useEffect(() => { if (props.workflow?.workspaceFlowState) { - const nodes = props.workflow.workspaceFlowState.nodes; const initFormValues = {} as WorkspaceFormValues; - nodes.forEach((node) => { + const initSchemaObj = {} as WorkspaceSchemaObj; + props.workflow.workspaceFlowState.nodes.forEach((node) => { initFormValues[node.id] = componentDataToFormik(node.data); + initSchemaObj[node.id] = getComponentSchema(node.data); }); + const initFormSchema = yup.object(initSchemaObj) as WorkspaceSchema; setFormValues(initFormValues); + setFormSchema(initFormSchema); } }, [props.workflow]); @@ -55,9 +63,13 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { { console.log('values on submit: ', values); }} + validate={(values) => { + console.log('values on validate: ', values); + }} > {(formikProps) => (
diff --git a/public/utils/utils.ts b/public/utils/utils.ts index 5ccd6d9d..fc75248a 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -3,12 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { FormikValues } from 'formik'; +import { FormikErrors, FormikTouched, FormikValues } from 'formik'; +import { Schema, ObjectSchema } from 'yup'; +import * as yup from 'yup'; import { FieldType, FieldValue, IComponent, IComponentData, + IComponentField, + WorkspaceFormValues, } from '../../common'; // Append 16 random characters @@ -22,7 +26,6 @@ export function generateId(prefix: string): string { // Adding any instance metadata. Converting the base IComponent obj into // an instance-specific IComponentData obj. -// TODO: initialize values too? export function initComponentData( data: IComponent, componentId: string @@ -33,6 +36,10 @@ export function initComponentData( } as IComponentData; } +/* + **************** Formik (form) utils ********************** + */ + // Converting stored values in component data to initial formik values export function componentDataToFormik(data: IComponentData): FormikValues { const formikValues = {} as FormikValues; @@ -67,3 +74,53 @@ export function getInitialValue(fieldType: FieldType): FieldValue { } } } + +export function isFieldInvalid( + componentId: string, + fieldName: string, + errors: FormikErrors, + touched: FormikTouched +): boolean { + return ( + errors[componentId]?.[fieldName] !== undefined && + touched[componentId]?.[fieldName] !== undefined + ); +} + +export function getFieldError( + componentId: string, + fieldName: string, + errors: FormikErrors +): string | undefined { + return errors[componentId]?.[fieldName] as string | undefined; +} + +/* + **************** Yup (validation) utils ********************** + */ + +export function getComponentSchema(data: IComponentData): ObjectSchema { + const schemaObj = {} as { [key: string]: Schema }; + data.fields?.forEach((field) => { + schemaObj[field.name] = getFieldSchema(field); + }); + return yup.object(schemaObj); +} + +function getFieldSchema(field: IComponentField): Schema { + let baseSchema: Schema; + switch (field.type) { + case 'string': + case 'select': { + baseSchema = yup.string().min(1, 'Too short').max(70, 'Too long'); + break; + } + case 'json': { + baseSchema = yup.object().json(); + break; + } + } + return field.optional + ? baseSchema.optional() + : baseSchema.required('Required'); +}