From a0ccc346123a2308a26f97b555816a027f9a38a0 Mon Sep 17 00:00:00 2001 From: west270 Date: Wed, 21 Jun 2023 15:18:27 +0000 Subject: [PATCH] Reworked command form elements --- .../FormComponents/FormAnyOrDict.tsx | 60 ++++++ src/components/FormComponents/FormArray.tsx | 6 +- src/components/FormComponents/FormFile.tsx | 43 ++++ .../FormComponents/FormTextField.tsx | 26 ++- src/components/FormComponents/index.ts | 3 +- src/pages/CommandView/CommandFormFields.tsx | 60 +++--- src/pages/CommandView/CommandView.tsx | 15 +- .../ParameterElements/ParamArray.tsx | 79 ++----- .../ParameterElements/ParamCheckbox.tsx | 29 +-- .../ParameterElements/ParamTextField.tsx | 201 ++++++++---------- 10 files changed, 270 insertions(+), 252 deletions(-) create mode 100644 src/components/FormComponents/FormAnyOrDict.tsx create mode 100644 src/components/FormComponents/FormFile.tsx diff --git a/src/components/FormComponents/FormAnyOrDict.tsx b/src/components/FormComponents/FormAnyOrDict.tsx new file mode 100644 index 00000000..5b2fdcb7 --- /dev/null +++ b/src/components/FormComponents/FormAnyOrDict.tsx @@ -0,0 +1,60 @@ +import { TextField, TextFieldProps } from '@mui/material' +import { defaultTextFieldProps } from 'components/FormComponents' +import { ChangeEvent } from 'react' +import { RegisterOptions, useFormContext } from 'react-hook-form' + +export type FormAnyOrDictProps = { + registerKey: string + registerOptions?: RegisterOptions +} & TextFieldProps + +const FormAnyOrDict = ({ registerKey, registerOptions, ...textFieldProps }: FormAnyOrDictProps) => { + const { getFieldState, watch, setValue, register, setError, clearErrors } = useFormContext() + + register(registerKey, {...registerOptions}) + + const currentValue = watch(registerKey) + + const { error, invalid } = getFieldState(registerKey) + + if(error){ + textFieldProps.error = true + if(error.message) textFieldProps.helperText = error.message + } + + return ( + ) => { + if(event.target.value === '') { + clearErrors(registerKey) + setValue(registerKey, undefined) + } + else try { + const value = JSON.parse(event.target.value) + if( textFieldProps.type === 'any' || typeof value === 'object' ) { + setValue(registerKey, value) + clearErrors(registerKey) + } else throw new Error('value is not valid') + } catch(e) { + if(event.target.value !== ''){ + setError(registerKey, { + type: 'parseError', + message: textFieldProps.type === 'object' ? + 'Invalid object' + : + 'Unknown Type. Remember, variant fields must be enclosed with {} (object), [] (array), or "" (string).' + }) + } else { + clearErrors(registerKey) + } + setValue(registerKey, event.target.value) + } + }} + /> + ) +} + +export { FormAnyOrDict } diff --git a/src/components/FormComponents/FormArray.tsx b/src/components/FormComponents/FormArray.tsx index bba98fa3..2c09daa5 100644 --- a/src/components/FormComponents/FormArray.tsx +++ b/src/components/FormComponents/FormArray.tsx @@ -26,12 +26,10 @@ const FormArray = ({ registerKey, disabled, helperText, label, minimum, maximum, const addItem = () => { const tempData = getValues(registerKey) || [] if(!maximum || tempData.length < maximum) - setValue(`${registerKey}.${tempData.length}`, addValue ? addValue : null) + setValue(`${registerKey}.${tempData.length}`, addValue) } - watch(registerKey) - - const currentValue = getValues(registerKey) || [] + const currentValue = watch(registerKey) || [] const error = getFieldState(registerKey).error diff --git a/src/components/FormComponents/FormFile.tsx b/src/components/FormComponents/FormFile.tsx new file mode 100644 index 00000000..1dc44312 --- /dev/null +++ b/src/components/FormComponents/FormFile.tsx @@ -0,0 +1,43 @@ +import { TextField, TextFieldProps } from '@mui/material' +import { defaultTextFieldProps } from 'components/FormComponents' +import { ChangeEvent } from 'react' +import { RegisterOptions, useFormContext } from 'react-hook-form' + +export type FormFileProps = { + registerKey: string + registerOptions?: RegisterOptions +} & TextFieldProps + +const FormFile = ({ registerKey, registerOptions, ...textFieldProps }: FormFileProps) => { + const { getFieldState, watch, setValue, register, trigger } = useFormContext() + + const currentValue = watch(registerKey) + + register(registerKey, {...registerOptions}) + + const { error } = getFieldState(registerKey) + if(error){ + textFieldProps.error = true + if(error.message) textFieldProps.helperText = error.message + } + + return ( + ) => { + if(event.target.files) { + setValue(registerKey, event.target.files[0]) + } + else { + setValue(registerKey, '') + } + trigger(registerKey) + }} + /> + ) +} + +export { FormFile } diff --git a/src/components/FormComponents/FormTextField.tsx b/src/components/FormComponents/FormTextField.tsx index d19ab241..2f0787c3 100644 --- a/src/components/FormComponents/FormTextField.tsx +++ b/src/components/FormComponents/FormTextField.tsx @@ -10,17 +10,18 @@ export type FormTextFieldProps = { menuOptions?: (string | number)[] } & TextFieldProps +const defaultTextFieldProps: TextFieldProps = { + FormHelperTextProps: { + sx: {ml: 0} + }, + size:'small', + fullWidth: true, + InputLabelProps: {shrink: true} +} + const FormTextField = ({ registerKey, registerOptions, menuOptions, ...textFieldProps }: FormTextFieldProps) => { const { register, getFieldState, watch, } = useFormContext() const [showPassword, setShowPassword] = useMountedState(false) - const defaultTextFieldProps: TextFieldProps = { - FormHelperTextProps: { - sx: {ml: 0} - }, - size:'small', - fullWidth: true, - InputLabelProps: {shrink: true} - } const currentValue = watch(registerKey) @@ -61,8 +62,13 @@ const FormTextField = ({ registerKey, registerOptions, menuOptions, ...textField {menuOptions.map((value) => {value})} ) : ( - + ) } -export { FormTextField } +export { defaultTextFieldProps, FormTextField } diff --git a/src/components/FormComponents/index.ts b/src/components/FormComponents/index.ts index 7247cf2c..a08a012d 100644 --- a/src/components/FormComponents/index.ts +++ b/src/components/FormComponents/index.ts @@ -1,4 +1,5 @@ +export * from './FormAnyOrDict' export * from './FormArray' export * from './FormCheckbox' +export * from './FormFile' export * from './FormTextField' - diff --git a/src/pages/CommandView/CommandFormFields.tsx b/src/pages/CommandView/CommandFormFields.tsx index 5b62d856..02d43aeb 100644 --- a/src/pages/CommandView/CommandFormFields.tsx +++ b/src/pages/CommandView/CommandFormFields.tsx @@ -1,6 +1,7 @@ import { TabContext, TabList } from '@mui/lab' -import { Alert, Box, Grid, MenuItem, Tab, TextField, TextFieldProps } from '@mui/material' +import { Alert, Box, Grid, Tab, TextFieldProps } from '@mui/material' import { Divider } from 'components/Divider' +import { FormTextField } from 'components/FormComponents' import { useMountedState } from 'hooks/useMountedState' import { ParameterElement } from 'pages/CommandView' import { useFormContext } from 'react-hook-form' @@ -16,11 +17,15 @@ const CommandFormFields = ({ parameters, instances, parentKey }: { parameters: P fullWidth: true, } - const { register, formState: { errors } } = useFormContext() + const { formState: { errors } } = useFormContext() const getKey = (key: string) => ( parentKey ? [parentKey, key].join('.') : key ) - + + const instancesNames: (string | number)[] | undefined = instances.length === 1 ? undefined : [] + if(instancesNames) instances.sort((a, b) => + (a.name < b.name ? -1 : 1) + ).forEach(instance => instancesNames.push(instance.name)) const requiredParams = parameters.filter((param: Parameter) => (!param.optional)) || [] const optionalParams = parameters.filter((param: Parameter) => (param.optional)) || [] @@ -52,42 +57,26 @@ const CommandFormFields = ({ parameters, instances, parentKey }: { parameters: P rowSpacing={2} > - + - + - + - { - instances.length === 1 ? - - : - - {(instances.sort((a, b) => - (a.name < b.name ? -1 : 1) - ).map((instance, index) => ( - - {instance.name} - ) - ))} - - } + - ) diff --git a/src/pages/CommandView/CommandView.tsx b/src/pages/CommandView/CommandView.tsx index a4619ac6..bbb8b445 100644 --- a/src/pages/CommandView/CommandView.tsx +++ b/src/pages/CommandView/CommandView.tsx @@ -98,14 +98,15 @@ const CommandView = ({isJob} : {isJob?: boolean}) => { job, isJob ]) - - const getCommandFromSystem = (sys: System) => { - let tempCommand: Command | undefined = undefined - tempCommand = sys.commands.find((cmd: Command) => (cmd.name===commandName)) - setCommand && setCommand(tempCommand as AugmentedCommand | undefined) - } - if(system && Object.hasOwn(system, 'commands') && !command) getCommandFromSystem(system as System) + useEffect(() => { + const getCommandFromSystem = (sys: System) => { + let tempCommand: Command | undefined = undefined + tempCommand = sys.commands.find((cmd: Command) => (cmd.name===commandName)) + setCommand && setCommand(tempCommand as AugmentedCommand | undefined) + } + if(system && Object.hasOwn(system, 'commands') && !command) getCommandFromSystem(system as System) + }, [command, commandName, setCommand, system]) if (!system || !command || error) { diff --git a/src/pages/CommandView/ParameterElements/ParamArray.tsx b/src/pages/CommandView/ParameterElements/ParamArray.tsx index 165d5ab3..c8f4ba6a 100644 --- a/src/pages/CommandView/ParameterElements/ParamArray.tsx +++ b/src/pages/CommandView/ParameterElements/ParamArray.tsx @@ -1,4 +1,4 @@ -import { Alert, Box, Button, FormHelperText, Stack, Typography, } from '@mui/material' +import { FormArray } from 'components/FormComponents' import { ParameterElement } from 'pages/CommandView/ParameterElements' import { useFormContext } from 'react-hook-form' import { Parameter } from 'types/backend-types' @@ -9,78 +9,35 @@ interface ParamTextFieldProps { } const ParamArray = ({ parameter, registerKey }: ParamTextFieldProps) => { - const { getValues, setValue, watch, setError, clearErrors, getFieldState } = useFormContext() - - const addItem = () => { - const tempData = getValues(registerKey) || [] - if(!parameter.maximum || tempData.length < parameter.maximum) - setValue(`${registerKey}.${tempData.length}`, null) - } - - const removeItem = (index: number) => { - const tempData = getValues(registerKey) || [] - if(!parameter.minimum || tempData.length > parameter.minimum) tempData.splice(index, 1) - setValue(registerKey, tempData) - } + const { watch, setError, clearErrors, getFieldState } = useFormContext() // triggers rerender when adding or removing - watch(registerKey) + const currentValue = watch(registerKey) const {error} = getFieldState(registerKey) - const currentValue = getValues(registerKey) || [] - if(parameter.maximum && currentValue.length > parameter.maximum) { if(error?.type !== 'arrayLength') setError(registerKey, { type: 'arrayLength', message: `remove items max length is: ${parameter.maximum}` }) } else if(parameter.minimum && currentValue.length < parameter.minimum) { if(error?.type !== 'arrayLength') setError(registerKey, { type: 'arrayLength', message: `add items min length is: ${parameter.minimum}` }) } else if(error?.type === 'arrayLength') clearErrors(registerKey) + const getFieldJsx = (index: number, registerKey: string,): JSX.Element => { + return ( + + ) + } + return ( - - {parameter.display_name && {parameter.display_name}} - - {currentValue.map((value: unknown, index: number) => { - return ( - - - - - ) - })} - - {parameter.description && - - { error?.type === 'arrayLength' ? - {error.message} - : - parameter.description - } - - } - - + ) } diff --git a/src/pages/CommandView/ParameterElements/ParamCheckbox.tsx b/src/pages/CommandView/ParameterElements/ParamCheckbox.tsx index 6199a9f2..bae6c26a 100644 --- a/src/pages/CommandView/ParameterElements/ParamCheckbox.tsx +++ b/src/pages/CommandView/ParameterElements/ParamCheckbox.tsx @@ -1,5 +1,4 @@ -import { Checkbox, FormControlLabel, FormHelperText } from '@mui/material' -import { useFormContext } from 'react-hook-form' +import { FormCheckbox } from 'components/FormComponents' import { Parameter } from 'types/backend-types' interface ParamCheckboxProps { @@ -8,28 +7,14 @@ interface ParamCheckboxProps { } const ParamCheckbox = ({ parameter, registerKey }: ParamCheckboxProps) => { - const { register, getValues, getFieldState } = useFormContext() - const { error } = getFieldState(registerKey) return ( - <> - - } - label={ !parameter.multi && parameter.display_name } - /> - {!parameter.multi && parameter.description && - - {error? error.message : parameter.description} - - } - + ) } diff --git a/src/pages/CommandView/ParameterElements/ParamTextField.tsx b/src/pages/CommandView/ParameterElements/ParamTextField.tsx index 6bfd3394..847b3167 100644 --- a/src/pages/CommandView/ParameterElements/ParamTextField.tsx +++ b/src/pages/CommandView/ParameterElements/ParamTextField.tsx @@ -1,14 +1,14 @@ -import { Alert, Autocomplete, InputBaseComponentProps, MenuItem, TextField } from '@mui/material' +import { Alert, Autocomplete, MenuItem } from '@mui/material' import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' import useAxios from 'axios-hooks' -import { FormTextField, FormTextFieldProps } from 'components/FormComponents' +import { defaultTextFieldProps, FormAnyOrDict, FormFile, FormFileProps, FormTextField, FormTextFieldProps } from 'components/FormComponents' import { ServerConfigContainer } from 'containers/ConfigContainer' import { SocketContainer } from 'containers/SocketContainer' import { useDebounceEmptyFunction } from 'hooks/useDebounce' import { useMountedState } from 'hooks/useMountedState' import { useMyAxios } from 'hooks/useMyAxios' -import { ChangeEvent, useCallback, useEffect, useMemo } from 'react' -import { Controller, RegisterOptions, useFormContext } from 'react-hook-form' +import { useCallback, useEffect, useMemo } from 'react' +import { RegisterOptions, useFormContext } from 'react-hook-form' import { DynamicChoiceCommandDetails, DynamicChoiceDictionaryDetails, @@ -25,7 +25,8 @@ interface ParamTextFieldProps { } const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { - const { getValues, setValue, register, watch, getFieldState, setError, clearErrors, control } = useFormContext() + const { getValues, setValue, watch, getFieldState, setError } = useFormContext() + const registerKeyPrefix = registerKey.split('parameters.')[0] let type = 'string' switch(parameter.type) { case 'Float': @@ -48,7 +49,7 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { type = parameter.type.toLowerCase() break } - } + } const { authEnabled } = ServerConfigContainer.useContainer() const { axiosManualOptions } = useMyAxios() const [, execute] = useAxios({}, axiosManualOptions) @@ -56,15 +57,8 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { parameter.choices?.type === 'static' && !Object.hasOwn(parameter.choices?.details, 'key_reference') ? parameter.choices.value as (string | number)[] : [] ) - const inputProps: InputBaseComponentProps = {key: parameter.key} const registerOptions: RegisterOptions = { required: parameter.optional ? false : `${parameter.display_name} is required`, - setValueAs: value => { - return value === '' ? (parameter.nullable ? null : undefined) : value - } - } - if(type === 'file'){ - registerKey = registerKey.replace('parameters', 'multipart') } const [requestId, setRequestId] = useMountedState() const [isLoading, setIsLoading] = useMountedState(false) @@ -73,13 +67,8 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { const formTextFieldProps: FormTextFieldProps = { registerKey: registerKey, type: type, - size: 'small', - error: !!error, - fullWidth: true, - FormHelperTextProps: { - sx: {ml: 0} - } } + formTextFieldProps.inputProps = {key: parameter.key} if(parameter.regex) { registerOptions.pattern = new RegExp(parameter.regex) @@ -89,15 +78,17 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { if(parameter.choices){ const tempWatchKey: string[] = [] if(parameter.choices.type === 'command' || parameter.choices?.type === 'url'){ - (parameter.choices.details as DynamicChoiceCommandDetails).args.forEach((arg) => tempWatchKey.push(`parameters.${arg[1]}`)) + (parameter.choices.details as DynamicChoiceCommandDetails).args.forEach((arg) => + tempWatchKey.push(`${registerKeyPrefix}parameters.${arg[1]}`) + ) } if(parameter.choices.type === 'static' && Object.hasOwn(parameter.choices.details, 'key_reference')){ - tempWatchKey.push(`parameters.${(parameter.choices.details as DynamicChoiceDictionaryDetails).key_reference}`) + tempWatchKey.push(`${registerKeyPrefix}parameters.${(parameter.choices.details as DynamicChoiceDictionaryDetails).key_reference}`) } return tempWatchKey } return [] - }, [parameter]) + }, [parameter, registerKeyPrefix]) const makeRequest = useCallback(() => { if(parameter.choices?.type === 'command'){ @@ -112,18 +103,18 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { if(name) value = getValues(name) choicesParameters[arg[0]] = value }) - if(getValues('instance_name') === '' || Object.values(choicesParameters).includes(undefined)) { + if(getValues(`${registerKeyPrefix}instance_name`) === '' || Object.values(choicesParameters).includes(undefined)) { setChoiceValues([]) setIsLoading(false) return } const choicesRequest: RequestTemplate = { command: choicesDetails.name, - instance_name: getValues('instance_name'), - namespace: getValues('namespace'), + instance_name: getValues(`${registerKeyPrefix}instance_name`), + namespace: getValues(`${registerKeyPrefix}namespace`), parameters: choicesParameters, - system: getValues('system'), - system_version: getValues('system_version') + system: getValues(`${registerKeyPrefix}system`), + system_version: getValues(`${registerKeyPrefix}system_version`) } const config: AxiosRequestConfig = { @@ -156,7 +147,17 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { // todo add error handling }) } - }, [authEnabled, execute, getValues, parameter.choices?.details, parameter.choices?.type, setChoiceValues, setIsLoading, setRequestId, watchKeys]) + }, [ + authEnabled, + execute, + getValues, + parameter, + registerKeyPrefix, + setChoiceValues, + setIsLoading, + setRequestId, + watchKeys + ]) const makeRequestDebounce = useDebounceEmptyFunction(makeRequest, 500) @@ -186,7 +187,9 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { } if(parameter.choices.type === 'command' || Object.hasOwn(parameter.choices.details, 'key_reference')){ const subscription = watch((value, { name, type }) => { - if(name && (watchKeys.includes(name) || name === 'instance_name')){ + + if(name === undefined && type === undefined) setChoiceValues([]) + else if(name && (watchKeys.includes(name) || name === `${registerKeyPrefix}instance_name`)){ if(parameter.choices?.type === 'command' || parameter.choices?.type === 'url'){ if(type === 'change') makeRequestDebounce() else makeRequest() @@ -207,11 +210,36 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { } } } - }, [watch, makeRequest, setChoiceValues, getValues, parameter, addCallback, requestId, removeCallback, setIsLoading, setRequestId, isLoading, watchKeys, makeRequestDebounce, registerKey, setError]) + }, [ + watch, + makeRequest, + setChoiceValues, + getValues, + parameter, + addCallback, + requestId, + removeCallback, + setIsLoading, + setRequestId, + isLoading, + watchKeys, + makeRequestDebounce, + registerKey, + setError, + registerKeyPrefix + ]) + + if(parameter.type === 'Base64'){ + return( + + Parameter type Base64 is not supported in the UI + + ) + } if(type==='number'){ registerOptions.valueAsNumber = true - if(parameter.type === 'Float') inputProps.step = 'any' + if(parameter.type === 'Float') formTextFieldProps.inputProps.step = 'any' if(!parameter.multi){ if(parameter.maximum) { registerOptions.max = {value: parameter.maximum, message: `Maximum ${parameter.display_name} number is ${parameter.minimum}`} @@ -256,23 +284,45 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { return choiceValues.includes(value) } } - - + + if(type === 'file'){ + if(!parameter.optional && getValues(registerKey) !== undefined) + registerOptions.required = false + formTextFieldProps.registerKey = registerKey.replace('parameters', 'multipart') + return ( + + ) + } + + if(!parameter.choices && (parameter.type === 'Any' || parameter.type === 'Dictionary')){ + const currentValue = getValues(registerKey) + if(!parameter.optional) + if((currentValue === '' && parameter.type === 'Any') || (parameter.nullable && currentValue === null)) + registerOptions.required = false + return ( + + ) + } else registerOptions.setValueAs = value => { + return value === '' && type !== 'any' ? (parameter.nullable ? null : undefined) : value + } + if(parameter.choices?.display === 'typeahead') { - return ( ( - )} options={ @@ -291,81 +341,6 @@ const ParamTextField = ({ parameter, registerKey }: ParamTextFieldProps) => { />) } - if(parameter.type === 'Base64'){ - return( - - Parameter type Base64 is not supported in the UI - - ) - } - - if(!parameter.choices && (parameter.type === 'Any' || parameter.type === 'Dictionary')){ - return ( - { - return ( - ) => { - try { - const value = JSON.parse(event.target.value) - if( parameter.type === 'Any' || typeof value === 'object' ) { - onChange(value) - clearErrors(registerKey) - } else throw new Error('value is not valid') - } catch(e) { - if(event.target.value !== '' || !parameter.nullable){ - setError(registerKey, { - type: 'parseError', - message: parameter.type === 'Dictionary' ? - 'Invalid object' - : - 'Unknown Type. Remember, variant fields must be enclosed with {} (object), [] (array), or "" (string).' - }) - onChange(event.target.value === '' && parameter.nullable ? null : event.target.value) - } else { - clearErrors(registerKey) - onChange(event.target.value === '' && parameter.nullable ? null : event.target.value) - } - - } - }} - /> - ) - }} - /> - ) - } - - if(type === 'file'){ - return ( - { - return ( - ) => { - if(event.target.files) onChange(event.target.files[0]) - }} - /> - ) - }} - /> - ) - } - - if(inputProps) formTextFieldProps.inputProps = inputProps - if(parameter.choices?.display === 'select') formTextFieldProps.menuOptions = choiceValues return (