Skip to content

Commit

Permalink
Integrate with yup schemas
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
ohltyler committed Oct 18, 2023
1 parent 541a2f8 commit 1546298
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 12 deletions.
6 changes: 6 additions & 0 deletions public/component_types/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { FormikValues } from 'formik';
import { ObjectSchema } from 'yup';
import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../utils';

/**
Expand All @@ -18,6 +19,11 @@ export type ComponentFormValues = FormikValues;
export type WorkspaceFormValues = {
[componentId: string]: ComponentFormValues;
};
export type WorkspaceSchemaObj = {
[componentId: string]: ObjectSchema<any, any, any>;
};

export type WorkspaceSchema = ObjectSchema<WorkspaceSchemaObj>;

/**
* Represents a single base class as an input handle for a component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<WorkspaceFormValues>();

return (
<Field name={formField}>
Expand All @@ -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
)}
/>
</EuiFormRow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<WorkspaceFormValues>();

return (
<Field name={`${props.componentId}.${props.field.name}`}>
<Field name={formField}>
{({ field, form }: FieldProps) => {
return (
<EuiFormRow label={props.field.label}>
<EuiFormRow
label={props.field.label}
error={getFieldError(props.componentId, props.field.name, errors)}
isInvalid={isFieldInvalid(
props.componentId,
props.field.name,
errors,
touched
)}
>
<EuiFieldText
{...field}
placeholder={props.field.placeholder || ''}
Expand Down
16 changes: 14 additions & 2 deletions public/pages/workflow_detail/workspace/resizable_workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import React, { useRef, useState, useEffect } from 'react';
import { ReactFlowProvider } from 'reactflow';
import { Form, Formik } from 'formik';
import * as yup from 'yup';
import { EuiButton, EuiResizableContainer } from '@elastic/eui';
import {
Workflow,
WorkspaceFormValues,
WorkspaceSchemaObj,
WorkspaceSchema,
componentDataToFormik,
getComponentSchema,
} from '../../../../common';
import { Workspace } from './workspace';
import { ComponentDetails } from '../component_details';
Expand Down Expand Up @@ -38,26 +42,34 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {

// Formik form state
const [formValues, setFormValues] = useState<WorkspaceFormValues>({});
const [formSchema, setFormSchema] = useState<WorkspaceSchema>(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]);

return (
<Formik
enableReinitialize={true}
initialValues={formValues}
validationSchema={formSchema}
onSubmit={(values) => {
console.log('values on submit: ', values);
}}
validate={(values) => {
console.log('values on validate: ', values);
}}
>
{(formikProps) => (
<Form>
Expand Down
61 changes: 59 additions & 2 deletions public/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -67,3 +74,53 @@ export function getInitialValue(fieldType: FieldType): FieldValue {
}
}
}

export function isFieldInvalid(
componentId: string,
fieldName: string,
errors: FormikErrors<WorkspaceFormValues>,
touched: FormikTouched<WorkspaceFormValues>
): boolean {
return (
errors[componentId]?.[fieldName] !== undefined &&
touched[componentId]?.[fieldName] !== undefined
);
}

export function getFieldError(
componentId: string,
fieldName: string,
errors: FormikErrors<WorkspaceFormValues>
): string | undefined {
return errors[componentId]?.[fieldName] as string | undefined;
}

/*
**************** Yup (validation) utils **********************
*/

export function getComponentSchema(data: IComponentData): ObjectSchema<any> {
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');
}

0 comments on commit 1546298

Please sign in to comment.