From f7f18983d0cc34d99c46fe5df90711fa141a8505 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Wed, 13 Mar 2024 20:21:53 -0700 Subject: [PATCH] CORE-1967 Refactor metadata template form fields Try to simplify the logic for building and validating form fields. --- .../{ => fields}/AstroThesaurusSearchField.js | 28 ++- .../fields/CheckboxStringValueField.js | 28 +++ .../metadata/templates/fields/EnumField.js | 40 ++++ .../templates/fields/GroupingField.js | 20 ++ .../metadata/templates/fields/IntegerField.js | 35 +++ .../{ => fields}/LocalContextsField.js | 41 +++- .../templates/fields/MultilineTextField.js | 37 +++ .../metadata/templates/fields/NumberField.js | 35 +++ .../OntologyLookupServiceSearchField.js | 28 ++- .../metadata/templates/fields/TextField.js | 32 +++ .../templates/fields/TimestampField.js | 41 ++++ .../metadata/templates/fields/UrlField.js | 34 +++ src/components/metadata/templates/index.js | 210 +++--------------- 13 files changed, 423 insertions(+), 186 deletions(-) rename src/components/metadata/templates/{ => fields}/AstroThesaurusSearchField.js (70%) create mode 100644 src/components/metadata/templates/fields/CheckboxStringValueField.js create mode 100644 src/components/metadata/templates/fields/EnumField.js create mode 100644 src/components/metadata/templates/fields/GroupingField.js create mode 100644 src/components/metadata/templates/fields/IntegerField.js rename src/components/metadata/templates/{ => fields}/LocalContextsField.js (86%) create mode 100644 src/components/metadata/templates/fields/MultilineTextField.js create mode 100644 src/components/metadata/templates/fields/NumberField.js rename src/components/metadata/templates/{ => fields}/OntologyLookupServiceSearchField.js (61%) create mode 100644 src/components/metadata/templates/fields/TextField.js create mode 100644 src/components/metadata/templates/fields/TimestampField.js create mode 100644 src/components/metadata/templates/fields/UrlField.js diff --git a/src/components/metadata/templates/AstroThesaurusSearchField.js b/src/components/metadata/templates/fields/AstroThesaurusSearchField.js similarity index 70% rename from src/components/metadata/templates/AstroThesaurusSearchField.js rename to src/components/metadata/templates/fields/AstroThesaurusSearchField.js index 365c43895..95607da3f 100644 --- a/src/components/metadata/templates/AstroThesaurusSearchField.js +++ b/src/components/metadata/templates/fields/AstroThesaurusSearchField.js @@ -2,8 +2,11 @@ * @author psarando sriram */ import React from "react"; + +import { FastField } from "formik"; import PropTypes from "prop-types"; +import { useTranslation } from "i18n"; import FormSearchField from "components/forms/FormSearchField"; import { ListItemText } from "@mui/material"; @@ -12,8 +15,9 @@ const AstroThesaurusOption = (option) => ( ); -const AstroThesaurusSearchField = (props) => { - const { searchAstroThesaurusTerms, ...custom } = props; +const AstroThesaurusSearchFieldComponent = (props) => { + const { searchAstroThesaurusTerms, attribute, writable, ...custom } = props; + const [options, setOptions] = React.useState([]); const handleSearch = (event, value, reason) => { @@ -59,11 +63,31 @@ const AstroThesaurusSearchField = (props) => { options={options} labelKey="label" valueKey="label" + label={attribute.name} + required={attribute.required && writable} + readOnly={!writable} {...custom} /> ); }; +const AstroThesaurusSearchField = ({ avu, avuFieldName, ...props }) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (props.attribute?.required && !value) { + return t("required"); + } + }} + {...props} + /> + ); +}; + AstroThesaurusSearchField.propTypes = { searchAstroThesaurusTerms: PropTypes.func.isRequired, }; diff --git a/src/components/metadata/templates/fields/CheckboxStringValueField.js b/src/components/metadata/templates/fields/CheckboxStringValueField.js new file mode 100644 index 000000000..982dca84c --- /dev/null +++ b/src/components/metadata/templates/fields/CheckboxStringValueField.js @@ -0,0 +1,28 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import FormCheckboxStringValue from "components/forms/FormCheckboxStringValue"; + +const CheckboxStringValueField = ({ + attribute, + avu, + avuFieldName, + writable, + ...props +}) => { + return ( + + ); +}; + +export default CheckboxStringValueField; diff --git a/src/components/metadata/templates/fields/EnumField.js b/src/components/metadata/templates/fields/EnumField.js new file mode 100644 index 000000000..75c70fc3d --- /dev/null +++ b/src/components/metadata/templates/fields/EnumField.js @@ -0,0 +1,40 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import { useTranslation } from "i18n"; +import FormTextField from "components/forms/FormTextField"; +import { MenuItem } from "@mui/material"; + +const EnumField = ({ attribute, avu, avuFieldName, writable, ...props }) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (attribute.required && !value) { + return t("required"); + } + }} + {...props} + > + {attribute.values && + attribute.values.map((enumVal, index) => ( + + {enumVal.value} + + ))} + + ); +}; + +export default EnumField; diff --git a/src/components/metadata/templates/fields/GroupingField.js b/src/components/metadata/templates/fields/GroupingField.js new file mode 100644 index 000000000..b4077dc8b --- /dev/null +++ b/src/components/metadata/templates/fields/GroupingField.js @@ -0,0 +1,20 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +const GroupingField = ({ + attribute, + avu, + avuFieldName, + writable, + ...props +}) => { + return ( + + ); +}; + +export default GroupingField; diff --git a/src/components/metadata/templates/fields/IntegerField.js b/src/components/metadata/templates/fields/IntegerField.js new file mode 100644 index 000000000..f0f07af92 --- /dev/null +++ b/src/components/metadata/templates/fields/IntegerField.js @@ -0,0 +1,35 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import { useTranslation } from "i18n"; +import FormIntegerField from "components/forms/FormIntegerField"; + +const IntegerField = ({ attribute, avu, avuFieldName, writable, ...props }) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (attribute.required && !value && value !== 0) { + return t("required"); + } + + if (isNaN(Number(value))) { + return t("templateValidationErrMsgNumber"); + } + }} + {...props} + /> + ); +}; + +export default IntegerField; diff --git a/src/components/metadata/templates/LocalContextsField.js b/src/components/metadata/templates/fields/LocalContextsField.js similarity index 86% rename from src/components/metadata/templates/LocalContextsField.js rename to src/components/metadata/templates/fields/LocalContextsField.js index de3048dcc..c99d32435 100644 --- a/src/components/metadata/templates/LocalContextsField.js +++ b/src/components/metadata/templates/fields/LocalContextsField.js @@ -7,11 +7,12 @@ */ import React from "react"; +import { FastField } from "formik"; import { useQuery } from "react-query"; import { useTranslation } from "i18n"; -import LocalContextsLabelDisplay from "../LocalContextsLabelDisplay"; +import LocalContextsLabelDisplay from "../../LocalContextsLabelDisplay"; import ErrorTypographyWithDialog from "components/error/ErrorTypographyWithDialog"; import getFormError from "components/forms/getFormError"; @@ -19,6 +20,7 @@ import { LocalContextsAttrs, parseProjectID, } from "components/models/metadata/LocalContexts"; +import { urlField } from "components/utils/validations"; import { LOCAL_CONTEXTS_QUERY_KEY, @@ -29,15 +31,18 @@ import { Skeleton, TextField } from "@mui/material"; const findAVU = (avus, attr) => avus?.find((avu) => avu.attr === attr); -const LocalContextsField = ({ +const LocalContextsFieldComponent = ({ + attribute, avu, - onUpdate, + avuFieldName, + writable, helperText, - form: { setFieldValue, ...form }, + form, field: { value, onChange, ...field }, ...props }) => { - const { t } = useTranslation("localcontexts"); + const { t } = useTranslation(["localcontexts", "metadata"]); + const { i18nUtil } = useTranslation("util"); const [rightsURIAVU, setRightsURIAVU] = React.useState( () => @@ -87,8 +92,7 @@ const LocalContextsField = ({ return schemeURI; }); - const { touched, errors } = form; - const fieldError = getFormError(field.name, touched, errors); + const fieldError = getFormError(avuFieldName, form.touched, form.errors); const projectID = parseProjectID(projectHubURI); const { data: project, isFetching } = useQuery({ @@ -99,6 +103,8 @@ const LocalContextsField = ({ }), enabled: !!projectHubURI && !fieldError, onSuccess: (project) => { + if (!writable) return; + let newValue = avu.value || ""; const projectLabels = [ @@ -163,7 +169,11 @@ const LocalContextsField = ({ })), ]; - onUpdate({ ...avu, value: newValue, avus: newAVUs }); + form.setFieldValue(avuFieldName, { + ...avu, + value: newValue, + avus: newAVUs, + }); } }, onError: (error) => { @@ -179,6 +189,10 @@ const LocalContextsField = ({ const updateProjectHubURI = (uri) => { setProjectHubURI(uri); setProjectHubError(null); + form.setFieldError( + avuFieldName, + !uri ? t("metadata:required") : urlField(uri, i18nUtil) + ); let newAVUs = avu.avus || []; @@ -204,7 +218,7 @@ const LocalContextsField = ({ rightsIDSchemeURIAVU, ]; - onUpdate({ ...avu, avus: newAVUs }); + form.setFieldValue(avuFieldName, { ...avu, avus: newAVUs }); } }; @@ -213,6 +227,7 @@ const LocalContextsField = ({ return ( <> ( + +); + export default LocalContextsField; diff --git a/src/components/metadata/templates/fields/MultilineTextField.js b/src/components/metadata/templates/fields/MultilineTextField.js new file mode 100644 index 000000000..c2ee11a54 --- /dev/null +++ b/src/components/metadata/templates/fields/MultilineTextField.js @@ -0,0 +1,37 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import { useTranslation } from "i18n"; +import FormMultilineTextField from "components/forms/FormMultilineTextField"; + +const MultilineTextField = ({ + attribute, + avu, + avuFieldName, + writable, + ...props +}) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (attribute.required && !value) { + return t("required"); + } + }} + {...props} + /> + ); +}; + +export default MultilineTextField; diff --git a/src/components/metadata/templates/fields/NumberField.js b/src/components/metadata/templates/fields/NumberField.js new file mode 100644 index 000000000..09916f9e8 --- /dev/null +++ b/src/components/metadata/templates/fields/NumberField.js @@ -0,0 +1,35 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import { useTranslation } from "i18n"; +import FormNumberField from "components/forms/FormNumberField"; + +const NumberField = ({ attribute, avu, avuFieldName, writable, ...props }) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (attribute.required && !value && value !== 0.0) { + return t("required"); + } + + if (isNaN(Number(value))) { + return t("templateValidationErrMsgNumber"); + } + }} + {...props} + /> + ); +}; + +export default NumberField; diff --git a/src/components/metadata/templates/OntologyLookupServiceSearchField.js b/src/components/metadata/templates/fields/OntologyLookupServiceSearchField.js similarity index 61% rename from src/components/metadata/templates/OntologyLookupServiceSearchField.js rename to src/components/metadata/templates/fields/OntologyLookupServiceSearchField.js index 185b0c2f3..7376d9107 100644 --- a/src/components/metadata/templates/OntologyLookupServiceSearchField.js +++ b/src/components/metadata/templates/fields/OntologyLookupServiceSearchField.js @@ -2,8 +2,11 @@ * @author psarando sriram */ import React from "react"; + +import { FastField } from "formik"; import PropTypes from "prop-types"; +import { useTranslation } from "i18n"; import FormSearchField from "components/forms/FormSearchField"; import { ListItemText } from "@mui/material"; @@ -19,8 +22,9 @@ const OLSOption = (option) => ( /> ); -const OntologyLookupServiceSearchField = (props) => { - const { attribute, searchOLSTerms, ...custom } = props; +const OntologyLookupServiceSearchFieldComponent = (props) => { + const { attribute, searchOLSTerms, writable, ...custom } = props; + const [options, setOptions] = React.useState([]); const handleSearch = (event, value, reason) => { @@ -44,11 +48,31 @@ const OntologyLookupServiceSearchField = (props) => { options={options} labelKey="label" valueKey="label" + label={attribute.name} + required={attribute.required && writable} + readOnly={!writable} {...custom} /> ); }; +const OntologyLookupServiceSearchField = ({ avu, avuFieldName, ...props }) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (props.attribute?.required && !value) { + return t("required"); + } + }} + {...props} + /> + ); +}; + OntologyLookupServiceSearchField.propTypes = { searchOLSTerms: PropTypes.func.isRequired, }; diff --git a/src/components/metadata/templates/fields/TextField.js b/src/components/metadata/templates/fields/TextField.js new file mode 100644 index 000000000..f203662bf --- /dev/null +++ b/src/components/metadata/templates/fields/TextField.js @@ -0,0 +1,32 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import { useTranslation } from "i18n"; +import FormTextField from "components/forms/FormTextField"; + +const TextField = ({ attribute, avu, avuFieldName, writable, ...props }) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (attribute.required && !value) { + return t("required"); + } + }} + {...props} + /> + ); +}; + +export default TextField; diff --git a/src/components/metadata/templates/fields/TimestampField.js b/src/components/metadata/templates/fields/TimestampField.js new file mode 100644 index 000000000..6d68b2dd3 --- /dev/null +++ b/src/components/metadata/templates/fields/TimestampField.js @@ -0,0 +1,41 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import { useTranslation } from "i18n"; +import FormTimestampField from "components/forms/FormTimestampField"; + +const TimestampField = ({ + attribute, + avu, + avuFieldName, + writable, + ...props +}) => { + const { t } = useTranslation("metadata"); + + return ( + { + if (attribute.required && !value) { + return t("required"); + } + + if (!Date.parse(value)) { + return t("templateValidationErrMsgTimestamp"); + } + }} + {...props} + /> + ); +}; + +export default TimestampField; diff --git a/src/components/metadata/templates/fields/UrlField.js b/src/components/metadata/templates/fields/UrlField.js new file mode 100644 index 000000000..1ebac0ab2 --- /dev/null +++ b/src/components/metadata/templates/fields/UrlField.js @@ -0,0 +1,34 @@ +/** + * @author psarando + */ +import React from "react"; + +import { FastField } from "formik"; + +import { useTranslation } from "i18n"; + +import FormTextField from "components/forms/FormTextField"; +import { urlField } from "components/utils/validations"; + +const UrlField = ({ attribute, avu, avuFieldName, writable, ...props }) => { + const { t } = useTranslation("metadata"); + const { i18nUtil } = useTranslation("util"); + + return ( + { + return attribute.required && !value + ? t("required") + : urlField(value, i18nUtil, attribute.required); + }} + {...props} + /> + ); +}; + +export default UrlField; diff --git a/src/components/metadata/templates/index.js b/src/components/metadata/templates/index.js index 34e8174de..731c6b214 100644 --- a/src/components/metadata/templates/index.js +++ b/src/components/metadata/templates/index.js @@ -3,7 +3,7 @@ */ import React, { Fragment } from "react"; -import { FastField, FieldArray, Formik } from "formik"; +import { FieldArray, Formik } from "formik"; import PropTypes from "prop-types"; import { useQuery } from "react-query"; @@ -11,7 +11,6 @@ import { useTranslation } from "i18n"; import ids from "../ids"; import styles from "../styles"; -import { urlField } from "components/utils/validations"; import AttributeTypes from "components/models/metadata/TemplateAttributeTypes"; import ConfirmationDialog from "components/utils/ConfirmationDialog"; @@ -29,21 +28,22 @@ import { searchUnifiedAstronomyThesaurus, } from "serviceFacades/metadata"; -import FormMultilineTextField from "components/forms/FormMultilineTextField"; -import FormTextField from "components/forms/FormTextField"; -import FormTimestampField from "components/forms/FormTimestampField"; - -import FormNumberField from "components/forms/FormNumberField"; - -import FormIntegerField from "components/forms/FormIntegerField"; import getFormError from "components/forms/getFormError"; import buildID from "components/utils/DebugIDUtil"; import { formatCurrentDate } from "components/utils/DateFormatter"; -import FormCheckboxStringValue from "components/forms/FormCheckboxStringValue"; -import AstroThesaurusSearchField from "./AstroThesaurusSearchField"; -import OntologyLookupServiceSearchField from "./OntologyLookupServiceSearchField"; -import LocalContextsField from "./LocalContextsField"; +import AstroThesaurusSearchField from "./fields/AstroThesaurusSearchField"; +import CheckboxStringValueField from "./fields/CheckboxStringValueField"; +import EnumField from "./fields/EnumField"; +import GroupingField from "./fields/GroupingField"; +import IntegerField from "./fields/IntegerField"; +import LocalContextsField from "./fields/LocalContextsField"; +import MultilineTextField from "./fields/MultilineTextField"; +import NumberField from "./fields/NumberField"; +import OntologyLookupServiceSearchField from "./fields/OntologyLookupServiceSearchField"; +import TextField from "./fields/TextField"; +import TimestampField from "./fields/TimestampField"; +import UrlField from "./fields/UrlField"; import SlideUpTransition from "../SlideUpTransition"; @@ -54,7 +54,6 @@ import { AccordionDetails, Grid, IconButton, - MenuItem, Table, Typography, } from "@mui/material"; @@ -189,80 +188,51 @@ const MetadataTemplateAttributeForm = (props) => { ); let attrErrors = false; - let canRemove = !attribute.required, - FieldComponent, - fieldProps = { - label: attribute.name, - required: attribute.required && writable, - inputProps: { readOnly: !writable }, - }; + let canRemove = !attribute.required; + let FieldComponent; + let customFieldProps = {}; switch (attribute.type) { case AttributeTypes.BOOLEAN: - FieldComponent = FormCheckboxStringValue; - fieldProps = { - ...fieldProps, - disabled: !writable, - }; + FieldComponent = CheckboxStringValueField; break; case AttributeTypes.NUMBER: - FieldComponent = FormNumberField; + FieldComponent = NumberField; break; case AttributeTypes.INTEGER: - FieldComponent = FormIntegerField; + FieldComponent = IntegerField; break; case AttributeTypes.MULTILINE_TEXT: - FieldComponent = FormMultilineTextField; + FieldComponent = MultilineTextField; break; case AttributeTypes.TIMESTAMP: - FieldComponent = FormTimestampField; + FieldComponent = TimestampField; break; case AttributeTypes.ENUM: - FieldComponent = FormTextField; - fieldProps = { - ...fieldProps, - select: true, - children: - attribute.values && - attribute.values.map((enumVal, index) => ( - - {enumVal.value} - - )), - }; + FieldComponent = EnumField; break; case AttributeTypes.ONTOLOGY_TERM_UAT: FieldComponent = AstroThesaurusSearchField; - fieldProps = { - ...fieldProps, - searchAstroThesaurusTerms, - readOnly: !writable, - }; + customFieldProps = { searchAstroThesaurusTerms }; break; case AttributeTypes.ONTOLOGY_TERM_OLS: FieldComponent = OntologyLookupServiceSearchField; - fieldProps = { - ...fieldProps, - searchOLSTerms, - attribute, - readOnly: !writable, - }; + customFieldProps = { searchOLSTerms }; break; case AttributeTypes.GROUPING: - FieldComponent = "span"; - fieldProps = {}; + FieldComponent = GroupingField; + break; + + case AttributeTypes.URL: + FieldComponent = UrlField; break; default: - FieldComponent = FormTextField; - fieldProps.multiline = true; + FieldComponent = TextField; break; } @@ -278,10 +248,6 @@ const MetadataTemplateAttributeForm = (props) => { LocalContextsAttrs.LOCAL_CONTEXTS; if (isLocalContextsAttr) { FieldComponent = LocalContextsField; - fieldProps.avu = avu; - fieldProps.onUpdate = (avu) => { - arrayHelpers.replace(index, avu); - }; } const avuFieldName = `${field}.avus[${index}]`; @@ -339,11 +305,13 @@ const MetadataTemplateAttributeForm = (props) => { alignItems="center" > - {!isGroupingAttr && ( @@ -488,7 +456,7 @@ const MetadataTemplateForm = (props) => { const [showErrorsDialog, setShowErrorsDialog] = React.useState(false); const handleSubmitWrapper = () => { - if (errors.error) { + if (errors?.metadata) { setShowErrorsDialog(true); } else { handleSubmit(); @@ -605,7 +573,6 @@ const MetadataTemplateView = (props) => { const [uatSearch, searchAstroThesaurusTerms] = React.useState(null); const { t } = useTranslation("metadata"); - const { t: i18nUtil } = useTranslation("util"); const { isFetching } = useQuery({ queryKey: [FILESYSTEM_METADATA_TEMPLATE_QUERY_KEY, templateId], @@ -709,108 +676,6 @@ const MetadataTemplateView = (props) => { return { template, attributeMap, metadata }; }; - const validateAVUs = (avus, attributeMap) => { - const avuArrayErrors = []; - avus.forEach((avu, avuIndex) => { - const avuErrors = {}; - - const attrTemplate = attributeMap[avu.attr]; - if (!attrTemplate) { - return; - } - - let attrType = attrTemplate.type; - let value = avu.value; - - const isLocalContexts = - avu.attr === LocalContextsAttrs.LOCAL_CONTEXTS; - if (isLocalContexts) { - const rightsURIAVU = avu.avus?.find( - (childAVU) => - childAVU.attr === LocalContextsAttrs.RIGHTS_URI - ); - - attrType = AttributeTypes.URL; - value = rightsURIAVU?.value; - } - - const isRequired = isLocalContexts || attrTemplate.required; - if (isRequired && value === "") { - avuErrors.value = t("required"); - avuErrors.error = true; - avuArrayErrors[avuIndex] = avuErrors; - } else if (value) { - switch (attrType) { - case AttributeTypes.NUMBER: - case AttributeTypes.INTEGER: - const numVal = Number(value); - if (isNaN(numVal)) { - avuErrors.value = t( - "templateValidationErrMsgNumber" - ); - avuErrors.error = true; - avuArrayErrors[avuIndex] = avuErrors; - } - - break; - - case AttributeTypes.TIMESTAMP: - if (!Date.parse(value)) { - avuErrors.value = t( - "templateValidationErrMsgTimestamp" - ); - avuErrors.error = true; - avuArrayErrors[avuIndex] = avuErrors; - } - - break; - - case AttributeTypes.URL: - const err = urlField(value, i18nUtil); - if (err) { - avuErrors.value = err; - avuErrors.error = true; - avuArrayErrors[avuIndex] = avuErrors; - } - - break; - - default: - break; - } - } - - if (attrTemplate.attributes && avu.avus && avu.avus.length > 0) { - const subAttrErros = validateAVUs( - avu.avus, - attrTemplate.attributes - ); - if (subAttrErros.length > 0) { - avuErrors.avus = subAttrErros; - avuErrors.error = true; - avuArrayErrors[avuIndex] = avuErrors; - } - } - }); - - return avuArrayErrors; - }; - - const validate = (values) => { - const errors = {}; - const { attributeMap, metadata } = values; - - if (metadata.avus && metadata.avus.length > 0) { - const avuArrayErrors = validateAVUs(metadata.avus, attributeMap); - if (avuArrayErrors.length > 0) { - errors.metadata = { avus: avuArrayErrors }; - errors.error = true; - } - } - - return errors; - }; - /** * Users do not fill in the values of Grouping attributes, * but duplicated attributes need unique values in order for the service to save them as separate AVUs. @@ -874,7 +739,6 @@ const MetadataTemplateView = (props) => { {(formikProps) => {