feat: custom fields component to registration form (#29202)
Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com>pull/29529/head
parent
bc115050ae
commit
e14ec50816
@ -0,0 +1,10 @@ |
||||
--- |
||||
"@rocket.chat/meteor": patch |
||||
"@rocket.chat/core-typings": patch |
||||
"@rocket.chat/rest-typings": patch |
||||
"@rocket.chat/ui-client": patch |
||||
"@rocket.chat/ui-contexts": patch |
||||
"@rocket.chat/web-ui-registration": patch |
||||
--- |
||||
|
||||
Added and Improved Custom Fields form to Registration Flow |
||||
@ -1,71 +0,0 @@ |
||||
import { TextInput, Field, Select } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
import type { FieldError } from 'react-hook-form'; |
||||
import { useFormContext } from 'react-hook-form'; |
||||
|
||||
import { useAccountsCustomFields } from '../../hooks/useAccountsCustomFields'; |
||||
|
||||
const AccountsCustomFieldsAssembler = () => { |
||||
const t = useTranslation(); |
||||
const customFields = useAccountsCustomFields(); |
||||
|
||||
const { register, getFieldState, setValue } = useFormContext(); |
||||
|
||||
return ( |
||||
<> |
||||
{customFields?.map((customField, index) => { |
||||
const getErrorMessage = (error: FieldError | undefined) => { |
||||
switch (error?.type) { |
||||
case 'required': |
||||
return t('The_field_is_required', customField.name); |
||||
case 'minLength': |
||||
return t('Min_length_is', customField.minLength); |
||||
case 'maxLength': |
||||
return t('Max_length_is', customField.maxLength); |
||||
} |
||||
}; |
||||
|
||||
const { onChange, ...handlers } = register(customField.name, { |
||||
required: customField.required, |
||||
minLength: customField.minLength, |
||||
maxLength: customField.maxLength, |
||||
}); |
||||
|
||||
const error = getErrorMessage(getFieldState(customField.name).error); |
||||
return ( |
||||
<Field key={index}> |
||||
<Field.Label> |
||||
{t.has(customField.name) ? t(customField.name) : customField.name} |
||||
{customField.required && '*'} |
||||
</Field.Label> |
||||
<Field.Row> |
||||
{customField.type === 'select' && ( |
||||
/* |
||||
the Select component is a controlled component, |
||||
the onchange handler are not compatible among them, |
||||
so we need to setValue on the onChange handler |
||||
|
||||
Select also doesn't follow the ideal implementation, but is what we have for now |
||||
*/ |
||||
<Select |
||||
onChange={(value) => { |
||||
setValue(customField.name, value); |
||||
}} |
||||
{...handlers} |
||||
options={customField.options.map((option) => [option, option, customField.defaultValue === option])} |
||||
error={error} |
||||
value={customField.defaultValue} |
||||
/> |
||||
)} |
||||
{customField.type === 'text' && <TextInput onChange={onChange} {...handlers} error={error} />} |
||||
</Field.Row> |
||||
<Field.Error>{error}</Field.Error> |
||||
</Field> |
||||
); |
||||
})} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default AccountsCustomFieldsAssembler; |
||||
@ -1 +0,0 @@ |
||||
export { default } from './AccountsCustomFieldsAssembler'; |
||||
@ -1,152 +0,0 @@ |
||||
import { TextInput, Select, Field } from '@rocket.chat/fuselage'; |
||||
import { capitalize } from '@rocket.chat/string-helpers'; |
||||
import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo, useEffect, useState } from 'react'; |
||||
|
||||
import { useComponentDidUpdate } from '../hooks/useComponentDidUpdate'; |
||||
import { useForm } from '../hooks/useForm'; |
||||
|
||||
const CustomTextInput = ({ label, name, required, minLength, maxLength, setState, state, className, setCustomFieldsError = () => [] }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const [inputError, setInputError] = useState(''); |
||||
|
||||
const verify = useMemo(() => { |
||||
const errors = []; |
||||
if (!state && required) { |
||||
errors.push(t('The_field_is_required', label || name)); |
||||
} |
||||
|
||||
if (state.length < minLength && state.length > 0) { |
||||
errors.push(t('Min_length_is', minLength)); |
||||
} |
||||
|
||||
return errors.join(', '); |
||||
}, [state, required, minLength, t, label, name]); |
||||
|
||||
useEffect(() => { |
||||
setCustomFieldsError((oldErrors) => (verify ? [...oldErrors, { name }] : oldErrors.filter((item) => item.name !== name))); |
||||
}, [name, setCustomFieldsError, verify]); |
||||
|
||||
useComponentDidUpdate(() => { |
||||
setInputError(verify); |
||||
}, [verify]); |
||||
|
||||
return useMemo( |
||||
() => ( |
||||
<Field className={className}> |
||||
<Field.Label> |
||||
{label || t(name)} |
||||
{required && '*'} |
||||
</Field.Label> |
||||
<Field.Row> |
||||
<TextInput |
||||
name={name} |
||||
error={inputError} |
||||
maxLength={maxLength} |
||||
flexGrow={1} |
||||
value={state} |
||||
onChange={(e) => setState(e.currentTarget.value)} |
||||
/> |
||||
</Field.Row> |
||||
<Field.Error>{inputError}</Field.Error> |
||||
</Field> |
||||
), |
||||
[className, label, t, name, required, inputError, maxLength, state, setState], |
||||
); |
||||
}; |
||||
|
||||
const CustomSelect = ({ label, name, required, options = {}, setState, state, className, setCustomFieldsError = () => [] }) => { |
||||
const t = useTranslation(); |
||||
const [selectError, setSelectError] = useState(''); |
||||
|
||||
const mappedOptions = useMemo(() => Object.values(options).map((value) => [value, value]), [options]); |
||||
const verify = useMemo( |
||||
() => (!state.length && required ? t('The_field_is_required', label || name) : ''), |
||||
[name, label, required, state.length, t], |
||||
); |
||||
|
||||
useEffect(() => { |
||||
setCustomFieldsError((oldErrors) => (verify ? [...oldErrors, { name }] : oldErrors.filter((item) => item.name !== name))); |
||||
}, [name, setCustomFieldsError, verify]); |
||||
|
||||
useComponentDidUpdate(() => { |
||||
setSelectError(verify); |
||||
}, [verify]); |
||||
|
||||
return useMemo( |
||||
() => ( |
||||
<Field className={className}> |
||||
<Field.Label> |
||||
{label || t(name)} |
||||
{required && '*'} |
||||
</Field.Label> |
||||
<Field.Row> |
||||
<Select name={name} error={selectError} flexGrow={1} value={state} options={mappedOptions} onChange={(val) => setState(val)} /> |
||||
</Field.Row> |
||||
<Field.Error>{selectError}</Field.Error> |
||||
</Field> |
||||
), |
||||
[className, label, t, name, required, selectError, state, mappedOptions, setState], |
||||
); |
||||
}; |
||||
|
||||
const CustomFieldsAssembler = ({ formValues, formHandlers, customFields, ...props }) => |
||||
Object.entries(customFields).map(([key, value]) => { |
||||
const extraProps = { |
||||
name: key, |
||||
setState: formHandlers[`handle${capitalize(key)}`], |
||||
state: formValues[key], |
||||
...value, |
||||
}; |
||||
|
||||
if (value.type === 'select') { |
||||
return <CustomSelect {...extraProps} {...props} key={key} />; |
||||
} |
||||
|
||||
if (value.type === 'text') { |
||||
return <CustomTextInput {...extraProps} {...props} key={key} />; |
||||
} |
||||
|
||||
return null; |
||||
}); |
||||
|
||||
export default function CustomFieldsForm({ jsonCustomFields, customFieldsData, setCustomFieldsData, onLoadFields = () => {}, ...props }) { |
||||
const accountsCustomFieldsJson = useSetting('Accounts_CustomFields'); |
||||
|
||||
const [customFields] = useState(() => { |
||||
try { |
||||
return jsonCustomFields || JSON.parse(accountsCustomFieldsJson || '{}'); |
||||
} catch { |
||||
return {}; |
||||
} |
||||
}); |
||||
|
||||
const hasCustomFields = useMemo(() => Object.values(customFields).length > 0, [customFields]); |
||||
const defaultFields = useMemo( |
||||
() => |
||||
Object.entries(customFields).reduce((data, [key, value]) => { |
||||
data[key] = value.defaultValue ?? ''; |
||||
return data; |
||||
}, {}), |
||||
[customFields], |
||||
); |
||||
|
||||
const { values, handlers } = useForm({ ...defaultFields, ...customFieldsData }); |
||||
|
||||
useEffect(() => { |
||||
onLoadFields?.(hasCustomFields); |
||||
}, [onLoadFields, hasCustomFields]); |
||||
|
||||
useEffect(() => { |
||||
if (hasCustomFields) { |
||||
setCustomFieldsData(values); |
||||
} |
||||
}, [hasCustomFields, setCustomFieldsData, values]); |
||||
|
||||
if (!hasCustomFields) { |
||||
return null; |
||||
} |
||||
|
||||
return <CustomFieldsAssembler formValues={values} formHandlers={handlers} customFields={customFields} {...props} />; |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
import type { SelectOption } from '@rocket.chat/fuselage'; |
||||
|
||||
export type CustomFieldMetadata = { |
||||
name: string; |
||||
label?: string; |
||||
type: 'select' | 'text'; |
||||
required?: boolean; |
||||
defaultValue?: any; |
||||
minLength?: number; |
||||
maxLength?: number; |
||||
options?: SelectOption[]; |
||||
}; |
||||
@ -1,17 +1,9 @@ |
||||
import { useSetting } from '@rocket.chat/ui-contexts'; |
||||
import { useMemo } from 'react'; |
||||
import type { CustomFieldMetadata } from '@rocket.chat/core-typings'; |
||||
|
||||
type AccountsCustomField = { |
||||
type: 'text' | 'select'; |
||||
required: boolean; |
||||
defaultValue: string; |
||||
minLength: number; |
||||
maxLength: number; |
||||
options: string[]; |
||||
name: string; |
||||
}; |
||||
import { useSetting } from './useSetting'; |
||||
|
||||
export const useAccountsCustomFields = (): AccountsCustomField[] => { |
||||
export const useAccountsCustomFields = (): CustomFieldMetadata[] => { |
||||
const accountsCustomFieldsJSON = useSetting('Accounts_CustomFields'); |
||||
|
||||
return useMemo(() => { |
||||
Loading…
Reference in new issue