diff --git a/web/src/components/common/Checkbox.tsx b/web/src/components/common/Checkbox.tsx new file mode 100644 index 000000000..2f1cb65a8 --- /dev/null +++ b/web/src/components/common/Checkbox.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { useSettings } from '../../contexts/SettingsContext'; + +interface CheckboxProps { + checked: boolean; + onChange: (e: React.ChangeEvent) => void; + disabled?: boolean; + error?: string; + className?: string; + labelClassName?: string; + + // props shared through ConfigItem + id?: string; + label?: string; + dataTestId?: string; + helpText?: string; +} + +const Checkbox: React.FC = ({ + checked, + onChange, + disabled = false, + error, + className = '', + labelClassName = '', + id, + label, + helpText, +}) => { + const { settings } = useSettings(); + const themeColor = settings.themeColor; + + return ( +
+
+ + +
+ {error &&

{error}

} + {helpText && !error &&

{helpText}

} +
+ ); +}; + +export default Checkbox; diff --git a/web/src/components/common/ConfigItem.tsx b/web/src/components/common/ConfigItem.tsx new file mode 100644 index 000000000..b0528abfe --- /dev/null +++ b/web/src/components/common/ConfigItem.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +interface ConfigItemProps { + id: string; + label: string; + dataTestId?: string; + helpText?: string; + children: React.ReactElement; +} + +/** + * A wrapper component that provides consistent styling and layout for configuration form elements + * + * Props: + * @param {string} id - Unique identifier for the form element + * @param {string} label - Label text to display above the input + * @param {string} [dataTestId] - Optional test ID for e2e testing + * @param {string} [helpText] - Optional help text displayed below the input + * @param {React.ReactElement} children - The form input component to wrap + * + * The component clones the child element and injects common props (id, label, helpText) + * to ensure consistent behavior across different input types. + * + * Example: + * ```tsx + * + * + * + * ``` + */ +const ConfigItem: React.FC = ({ + id, + label, + helpText, + children, +}) => { + // Clone the child element and inject the common props + const enhancedChild = React.cloneElement(children, { + id, + label, + helpText, + } as React.HTMLAttributes); + + return ( +
+
+ {enhancedChild} +
+
+ ); +}; + + +export default ConfigItem; diff --git a/web/src/components/common/Input.tsx b/web/src/components/common/Input.tsx index c8561dcfb..74bc47982 100644 --- a/web/src/components/common/Input.tsx +++ b/web/src/components/common/Input.tsx @@ -2,39 +2,40 @@ import React from 'react'; import { useSettings } from '../../contexts/SettingsContext'; interface InputProps { - id: string; - label: string; type?: string; value: string; - onChange: (e: React.ChangeEvent) => void; - onKeyDown?: (e: React.KeyboardEvent) => void; + icon?: React.ReactNode; placeholder?: string; required?: boolean; + onKeyDown?: (e: React.KeyboardEvent) => void; + onChange: (e: React.ChangeEvent) => void; disabled?: boolean; error?: string; - helpText?: string; className?: string; labelClassName?: string; - icon?: React.ReactNode; + + // props shared through ConfigItem + id?: string; + label?: string; dataTestId?: string; + helpText?: string; } const Input: React.FC = ({ - id, - label, type = 'text', value, - onChange, - onKeyDown, + icon, placeholder = '', required = false, + onKeyDown, + onChange, disabled = false, error, - helpText, className = '', labelClassName = '', - icon, - dataTestId, + id, + label, + helpText, }) => { const { settings } = useSettings(); const themeColor = settings.themeColor; @@ -69,7 +70,7 @@ const Input: React.FC = ({ '--tw-ring-color': themeColor, '--tw-ring-offset-color': themeColor, } as React.CSSProperties} - data-testid={dataTestId} + data-testid={`text-input-${id}`} /> {error &&

{error}

} diff --git a/web/src/components/common/Radio.tsx b/web/src/components/common/Radio.tsx new file mode 100644 index 000000000..968cb1c84 --- /dev/null +++ b/web/src/components/common/Radio.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useSettings } from '../../contexts/SettingsContext'; +import { AppConfigChildItem } from '../../types'; + +interface RadioProps { + value: string; + options: AppConfigChildItem[]; + onChange: (e: React.ChangeEvent) => void; + disabled?: boolean; + error?: string; + className?: string; + labelClassName?: string; + + // props shared through ConfigItem + id?: string; + label?: string; + dataTestId?: string; + helpText?: string; +} + +const Radio: React.FC = ({ + value, + options, + onChange, + disabled = false, + error, + className = '', + labelClassName = '', + id, + label, + helpText, +}) => { + const { settings } = useSettings(); + const themeColor = settings.themeColor; + + return ( +
+ +
+ {options.map(option => ( +
+ + +
+ ))} +
+ {error &&

{error}

} + {helpText && !error &&

{helpText}

} +
+ ); +}; + +export default Radio; diff --git a/web/src/components/common/Textarea.tsx b/web/src/components/common/Textarea.tsx index d5d244e54..5bc562d55 100644 --- a/web/src/components/common/Textarea.tsx +++ b/web/src/components/common/Textarea.tsx @@ -2,35 +2,36 @@ import { ChangeEvent, CSSProperties } from 'react'; import { useSettings } from '../../contexts/SettingsContext'; interface TextareaProps { - id: string; - label: string; value: string; - onChange: (e: ChangeEvent) => void; rows?: number; placeholder?: string; required?: boolean; + onChange: (e: ChangeEvent) => void; disabled?: boolean; error?: string; - helpText?: string; className?: string; labelClassName?: string; + + // props shared through ConfigItem + id?: string; + label?: string; dataTestId?: string; + helpText?: string; } const Textarea = ({ - id, - label, value, - onChange, rows = 4, placeholder = '', required = false, + onChange, disabled = false, error, - helpText, className = '', labelClassName = '', - dataTestId, + id, + label, + helpText, }: TextareaProps) => { const { settings } = useSettings(); const themeColor = settings.themeColor; @@ -58,7 +59,7 @@ const Textarea = ({ '--tw-ring-color': themeColor, '--tw-ring-offset-color': themeColor, } as CSSProperties} - data-testid={dataTestId} + data-testid={`textarea-input-${id}`} /> {error &&

{error}

} {helpText && !error &&

{helpText}

} diff --git a/web/src/components/wizard/config/ConfigurationStep.tsx b/web/src/components/wizard/config/ConfigurationStep.tsx index 07301883b..dd93f8c29 100644 --- a/web/src/components/wizard/config/ConfigurationStep.tsx +++ b/web/src/components/wizard/config/ConfigurationStep.tsx @@ -4,6 +4,9 @@ import Card from '../../common/Card'; import Button from '../../common/Button'; import Input from '../../common/Input'; import Textarea from '../../common/Textarea'; +import Checkbox from '../../common/Checkbox'; +import Radio from '../../common/Radio'; +import ConfigItem from '../../common/ConfigItem'; import { useWizard } from '../../../contexts/WizardModeContext'; import { useAuth } from '../../../contexts/AuthContext'; import { useSettings } from '../../../contexts/SettingsContext'; @@ -165,77 +168,35 @@ const ConfigurationStep: React.FC = ({ onNext }) => { case 'text': return ( ); case 'textarea': return (