From 27fa2ea3094b2533ac597c99de56e661b6b13ebc Mon Sep 17 00:00:00 2001 From: Eileen Li Date: Fri, 15 Aug 2025 16:37:57 -0700 Subject: [PATCH 1/2] fix: add questionField to uniformsForm --- .../dynamicForm/dynamicForm.stories.tsx | 48 ++++++++-- src/components/dynamicForm/questionField.tsx | 90 +++++++++++++++++++ src/components/dynamicForm/uniformsForm.css | 11 +++ src/components/dynamicForm/uniformsForm.tsx | 32 ++++++- 4 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 src/components/dynamicForm/questionField.tsx create mode 100644 src/components/dynamicForm/uniformsForm.css diff --git a/src/components/dynamicForm/dynamicForm.stories.tsx b/src/components/dynamicForm/dynamicForm.stories.tsx index a771a6dc..22f41fb0 100644 --- a/src/components/dynamicForm/dynamicForm.stories.tsx +++ b/src/components/dynamicForm/dynamicForm.stories.tsx @@ -13,7 +13,7 @@ export default { }, } -export const Default = { +export const TextFields = { args: { title: 'Provide a delivery address', schema: { @@ -27,11 +27,15 @@ export const Default = { }, required: ['street', 'zip', 'state'], }, - ws: { send: () => {} }, + ws: { + send: (msg: any) => { + alert('Form submitted: ' + JSON.stringify(msg)) + }, + }, }, } -export const Meeting = { +export const Radio = { args: { title: 'Choose the days', schema: { @@ -47,11 +51,45 @@ export const Meeting = { sunday: { type: 'boolean' }, }, }, - ws: { send: () => {} }, + ws: { + send: (msg: any) => { + alert('Form submitted: ' + JSON.stringify(msg)) + }, + }, + }, +} + +export const ButtonGroup = { + args: { + title: 'What do you think?', + description: 'Choose one of the options below.', + schema: { + type: 'object', + properties: { + opinion: { + type: 'string', + title: 'Your Opinion', + uniforms: { + component: 'QuestionField', + options: [ + { value: 'yes', label: 'Yes' }, + { value: 'maybe', label: 'Maybe' }, + { value: 'no', label: 'No' }, + ], + description: 'Please select one option', + }, + }, + }, + }, + ws: { + send: (msg: any) => { + alert('Form submitted: ' + JSON.stringify(msg)) + }, + }, }, } -export const ReadOnly = { +export const ReadOnlyRadio = { args: { title: 'Choose the days', schema: { diff --git a/src/components/dynamicForm/questionField.tsx b/src/components/dynamicForm/questionField.tsx new file mode 100644 index 00000000..2dfb5713 --- /dev/null +++ b/src/components/dynamicForm/questionField.tsx @@ -0,0 +1,90 @@ +import { useMediaQuery, useTheme } from '@mui/material' +import Chip from '@mui/material/Chip' +import FormControl from '@mui/material/FormControl' +import FormHelperText from '@mui/material/FormHelperText' +import Stack from '@mui/material/Stack' +import React, { useState } from 'react' +import { filterDOMProps, useField } from 'uniforms' + +type Option = { + label?: string + value: T +} + +type QuestionFieldProps = { + name: string + options?: Option[] + value?: string +} + +function QuestionField(props: QuestionFieldProps) { + const [fieldProps] = useField(props.name, props) + + const { + options = [], + disabled, + onChange, + value, + error, + errorMessage, + showInlineError, + ...rest + } = fieldProps + + const [selectedOption, setSelectedOption] = useState(value || '') + + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('md')) + const buttonGroupOrientation = isMobile ? 'column' : 'row' + + const handleOptionSelect = (option: string) => { + setSelectedOption(option) + onChange?.(option) + } + + const buttonList = options.map((option, index) => { + const isSelected = selectedOption === option.value + const selectedStyles = { + '&.MuiChip-root': { + backgroundColor: 'secondary.light', + color: 'text.primary', + }, + } + + return ( + handleOptionSelect(option.value)} + color="secondary" + sx={isSelected ? selectedStyles : {}} + className="rustic-option" + disabled={!!disabled} + aria-disabled={!!disabled} + label={option.label || option.value} + /> + ) + }) + + return ( + + + {buttonList} + + + {!!(error && showInlineError) && ( + {errorMessage} + )} + + ) +} + +export default QuestionField diff --git a/src/components/dynamicForm/uniformsForm.css b/src/components/dynamicForm/uniformsForm.css new file mode 100644 index 00000000..d10b04d7 --- /dev/null +++ b/src/components/dynamicForm/uniformsForm.css @@ -0,0 +1,11 @@ +.rustic-question-field .rustic-options-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin: 16px 0; +} + +.rustic-option { + flex: 1; + min-width: fit-content; +} diff --git a/src/components/dynamicForm/uniformsForm.tsx b/src/components/dynamicForm/uniformsForm.tsx index 053eb1c9..df4a4301 100644 --- a/src/components/dynamicForm/uniformsForm.tsx +++ b/src/components/dynamicForm/uniformsForm.tsx @@ -1,3 +1,5 @@ +import './uniformsForm.css' + import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' import Ajv, { type JSONSchemaType } from 'ajv' @@ -8,6 +10,7 @@ import { v4 as getUUID } from 'uuid' import MarkedMarkdown from '../markdown/markedMarkdown' import type { DynamicFormProps, Message } from '../types' +import QuestionField from './questionField' /** * The `UniformsForm` component provides a user interface for rendering a dynamic form using [uniforms](https://uniforms.tools/) and sending the response as a message on the websocket. @@ -21,7 +24,6 @@ import type { DynamicFormProps, Message } from '../types' */ export default function UniformsForm(props: DynamicFormProps) { const [data, setData] = useState(props.data) - useEffect(() => { if (props.data) { setData(props.data) @@ -45,6 +47,32 @@ export default function UniformsForm(props: DynamicFormProps) { const schemaValidator = createValidator(props.schema) + const customComponents = { + QuestionField: QuestionField, + } + + const processSchema = (schema: any) => { + if (schema.properties) { + Object.keys(schema.properties).forEach((key) => { + const property = schema.properties[key] + + if (property.properties) { + processSchema(property) + } + + if (property.uniforms?.component) { + const componentName = property.uniforms.component + if (componentName in customComponents) { + property.uniforms.component = + customComponents[componentName as keyof typeof customComponents] + } + } + }) + } + } + + processSchema(props.schema) + const bridge = new JSONSchemaBridge({ schema: props.schema, validator: schemaValidator, @@ -66,7 +94,7 @@ export default function UniformsForm(props: DynamicFormProps) { } return ( - + {props.title && {props.title}} {props.description && } Date: Fri, 15 Aug 2025 16:46:18 -0700 Subject: [PATCH 2/2] fix: render questionField based on options --- .../dynamicForm/dynamicForm.stories.tsx | 1 - src/components/dynamicForm/uniformsForm.tsx | 20 ++++++------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/components/dynamicForm/dynamicForm.stories.tsx b/src/components/dynamicForm/dynamicForm.stories.tsx index 22f41fb0..b7fb2fc9 100644 --- a/src/components/dynamicForm/dynamicForm.stories.tsx +++ b/src/components/dynamicForm/dynamicForm.stories.tsx @@ -70,7 +70,6 @@ export const ButtonGroup = { type: 'string', title: 'Your Opinion', uniforms: { - component: 'QuestionField', options: [ { value: 'yes', label: 'Yes' }, { value: 'maybe', label: 'Maybe' }, diff --git a/src/components/dynamicForm/uniformsForm.tsx b/src/components/dynamicForm/uniformsForm.tsx index df4a4301..cd92ed46 100644 --- a/src/components/dynamicForm/uniformsForm.tsx +++ b/src/components/dynamicForm/uniformsForm.tsx @@ -12,6 +12,10 @@ import MarkedMarkdown from '../markdown/markedMarkdown' import type { DynamicFormProps, Message } from '../types' import QuestionField from './questionField' +const customComponents = { + QuestionField: QuestionField, +} + /** * The `UniformsForm` component provides a user interface for rendering a dynamic form using [uniforms](https://uniforms.tools/) and sending the response as a message on the websocket. * It is designed to facilitate interactive decision-making and response submission within a conversation or messaging context. @@ -47,25 +51,13 @@ export default function UniformsForm(props: DynamicFormProps) { const schemaValidator = createValidator(props.schema) - const customComponents = { - QuestionField: QuestionField, - } - const processSchema = (schema: any) => { if (schema.properties) { Object.keys(schema.properties).forEach((key) => { const property = schema.properties[key] - if (property.properties) { - processSchema(property) - } - - if (property.uniforms?.component) { - const componentName = property.uniforms.component - if (componentName in customComponents) { - property.uniforms.component = - customComponents[componentName as keyof typeof customComponents] - } + if (property.uniforms?.options) { + property.uniforms.component = customComponents.QuestionField } }) }