From 85de98d0301f8fdd516b418bdbdb3986c7572604 Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 20 Sep 2021 15:58:18 +0300 Subject: [PATCH 01/22] Add DOI form in a popup modal --- .../WizardForms/WizardDOIForm.js | 109 ++++ .../WizardForms/WizardJSONSchemaParser.js | 14 +- .../WizardSteps/WizardShowSummaryStep.js | 58 +- src/constants/folder.json | 573 ++++++++++++++++++ src/theme.js | 1 + src/utils/JSONSchemaUtils.js | 6 + 6 files changed, 758 insertions(+), 3 deletions(-) create mode 100644 src/components/NewDraftWizard/WizardForms/WizardDOIForm.js create mode 100644 src/constants/folder.json diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js new file mode 100644 index 00000000..285607f7 --- /dev/null +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -0,0 +1,109 @@ +//@flow + +import React, { useEffect, useState } from "react" + +import { makeStyles } from "@material-ui/core/styles" +import { useForm, FormProvider } from "react-hook-form" +import { useSelector, useDispatch } from "react-redux" + +import { WizardAjvResolver } from "./WizardAjvResolver" +import JSONSchemaParser from "./WizardJSONSchemaParser" + +import folderSchema from "constants/folder.json" +import { WizardStatus } from "constants/wizardStatus" +import { updateStatus } from "features/wizardStatusMessageSlice" +import folderAPIService from "services/folderAPI" +import { dereferenceSchema } from "utils/JSONSchemaUtils" + +const useStyles = makeStyles(theme => ({ + form: { + margin: theme.spacing(3, 2), + "& .MuiTextField-root > .Mui-required": { + color: theme.palette.primary.main, + }, + "& .MuiTextField-root": { + width: "48%", + margin: theme.spacing(1), + }, + "& .MuiTypography-root": { + margin: theme.spacing(1), + ...theme.typography.subtitle1, + fontWeight: "bold", + }, + "& .MuiTypography-h2": { + width: "100%", + color: theme.palette.primary.light, + borderBottom: `2px solid ${theme.palette.secondary.main}`, + }, + "& .MuiTypography-h3": { + width: "100%", + }, + "& .array": { + margin: theme.spacing(1), + + "& .arrayRow": { + display: "flex", + alignItems: "center", + marginBottom: theme.spacing(1), + width: "100%", + "& .MuiTextField-root": { + width: "95%", + }, + }, + "& h2, h3, h4, h5, h6": { + margin: theme.spacing(1, 0), + }, + "& .MuiPaper-elevation2": { + paddingRight: theme.spacing(1), + marginBottom: theme.spacing(1), + width: "60%", + "& .array": { margin: 0 }, + "& h3, h4": { margin: theme.spacing(1) }, + "& button": { margin: theme.spacing(1) }, + }, + "& .MuiSelect-outlined": { + marginRight: 0, + }, + }, + }, +})) + +const DOIForm = ({ formId }: { formId: string }) => { + const [doiSchema, setDoiSchema] = useState({}) + + useEffect(async () => { + const loadData = await dereferenceSchema(folderSchema) + const doiData = loadData?.properties?.doiInfo + setDoiSchema(doiData) + }, []) + + const resolver = WizardAjvResolver(doiSchema) + const methods = useForm({ mode: "onBlur", resolver }) + + const currentFolder = useSelector(state => state.submissionFolder) + const dispatch = useDispatch() + const classes = useStyles() + + const onSubmit = async data => { + const changes = [{ op: "add", path: "/doiInfo", value: data }] + const response = await folderAPIService.patchFolderById(currentFolder.folderId, changes) + if (!response.ok) { + dispatch( + updateStatus({ + successStatus: WizardStatus.error, + response: response, + errorPrefix: "Can't submit the DOI information", + }) + ) + } + } + return ( + +
+
{Object.keys(doiSchema)?.length > 0 ? JSONSchemaParser.buildFields(doiSchema) : null}
+
+
+ ) +} + +export default DOIForm diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index 76b07853..19eb3ab3 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -156,6 +156,18 @@ const traverseFields = ( if (object.oneOf) return + if (typeof object["type"] === "object") { + return ( + + ) + } switch (object.type) { case "object": { const properties = object.properties @@ -946,12 +958,10 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { // Get currentObject and the values of current field const currentObject = useSelector(state => state.currentObject) || {} const fieldValues = get(currentObject, name) - const items = (traverseValues(object.items): any) // Needs to use "control" from useForm() const { control, setValue } = useForm() - const { fields, append, remove } = useFieldArray({ control, name }) // Required field array handling diff --git a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js index 9309eccd..81669be2 100644 --- a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js +++ b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js @@ -1,6 +1,12 @@ //@flow -import React from "react" +import React, { useState } from "react" +import Button from "@material-ui/core/Button" +import Dialog from "@material-ui/core/Dialog" +import DialogActions from "@material-ui/core/DialogActions" +import DialogContent from "@material-ui/core/DialogContent" +import DialogContentText from "@material-ui/core/DialogContentText" +import DialogTitle from "@material-ui/core/DialogTitle" import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction" @@ -12,6 +18,7 @@ import { useSelector } from "react-redux" import WizardHeader from "../WizardComponents/WizardHeader" import WizardSavedObjectActions from "../WizardComponents/WizardSavedObjectActions" import WizardStepper from "../WizardComponents/WizardStepper" +import WizardDOIForm from "../WizardForms/WizardDOIForm" import type { ObjectInsideFolderWithTags } from "types" import { getItemPrimaryText, formatDisplayObjectType } from "utils" @@ -42,6 +49,9 @@ const useStyles = makeStyles(theme => ({ alignItems: "flex-start", padding: theme.spacing(1.5), }, + doiButton: { + marginTop: theme.spacing(4), + }, })) type Schema = "study" | "schema" | "experiment" | "run" | "analysis" | "dac" | "policy" | "dataset" @@ -61,8 +71,45 @@ const WizardShowSummaryStep = (): React$Element => { } }) + const [openDoiDialog, setOpenDoiDialog] = useState(false) + const classes = useStyles() + const DOIDialog = () => ( + setOpenDoiDialog(false)} + aria-labelledby="doi-dialog-title" + aria-describedby="doi-dialog-description" + > + DOI information + + + Please fill in and save the information if you would like your submission to have DOI. + + + + + + + + + ) + return ( <> @@ -105,6 +152,15 @@ const WizardShowSummaryStep = (): React$Element => { ) })} + + {openDoiDialog && } ) } diff --git a/src/constants/folder.json b/src/constants/folder.json new file mode 100644 index 00000000..f52ea5bb --- /dev/null +++ b/src/constants/folder.json @@ -0,0 +1,573 @@ +{ + "type": "object", + "title": "Folder schema containing submitted metadata objects", + "required": ["name", "description"], + "properties": { + "folderId": { + "type": "string", + "title": "Folder Id" + }, + "name": { + "type": "string", + "title": "Folder Name" + }, + "description": { + "type": "string", + "title": "Folder Description" + }, + "dateCreated": { + "type": "integer", + "title": "Unix time stamp of creation, used for indexing" + }, + "published": { + "type": "boolean", + "title": "Published Folder" + }, + "doiInfo": { + "type": "object", + "title": "The DOI info schema", + "required": ["creators", "subjects"], + "properties": { + "creators": { + "type": "array", + "title": "List of creators", + "items": { + "type": "object", + "title": "Main researchers involved with data or the authors of the publication", + "properties": { + "name": { + "type": "string", + "title": "Full name of creator (format: Family, Given)" + }, + "nameType": { + "type": "string", + "title": "Type of name" + }, + "givenName": { + "type": "string", + "title": "First name" + }, + "familyName": { + "type": "string", + "title": "Last name" + }, + "nameIdentifiers": { + "type": "array", + "title": "List of name identifiers", + "items": { + "type": "object", + "title": "Name identifier object", + "properties": { + "schemeUri": { + "type": ["string", "null"], + "title": "URI (location) of the name identifier scheme" + }, + "nameIdentifier": { + "type": ["string", "null"], + "title": "URI (location) of name identifier" + }, + "nameIdentifierScheme": { + "type": ["string", "null"], + "title": "Name of name identifier scheme" + } + } + }, + "uniqueItems": true + }, + "affiliation": { + "type": "array", + "title": "List of affiliations", + "items": { + "type": "object", + "title": "Name affiliation object", + "properties": { + "name": { + "type": "string", + "title": "Name of the place of affiliation" + }, + "schemeUri": { + "type": "string", + "title": "URI (location) of the affiliation scheme" + }, + "affiliationIdentifier": { + "type": "string", + "title": "Location of affiliation identifier" + }, + "affiliationIdentifierScheme": { + "type": "string", + "title": "Name of affiliation identifier scheme" + } + } + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "uniqueItems": true + }, + "subjects": { + "type": "array", + "title": "List of subject identifiers specified by FOS", + "items": { + "type": "object", + "title": "Subject objects", + "required": ["subject"], + "properties": { + "subject": { + "type": "string", + "title": "FOS identifier" + }, + "subjectScheme": { + "type": "string", + "title": "Subject scheme name" + } + }, + "additionalProperties": true + }, + "uniqueItems": true + }, + "contributors": { + "type": "array", + "title": "List of contributors", + "items": { + "type": "object", + "title": "The institution or person responsible for contributing to the developement of the dataset", + "required": ["contributorType"], + "properties": { + "name": { + "type": "string", + "title": "Full name of contributor (format: Family, Given)" + }, + "nameType": { + "type": "string", + "title": "Type of name" + }, + "givenName": { + "type": "string", + "title": "First name" + }, + "familyName": { + "type": "string", + "title": "Last name" + }, + "contributorType": { + "type": "string", + "title": "Type of contributor" + }, + "nameIdentifiers": { + "type": "array", + "title": "List of name identifiers", + "items": { + "type": "object", + "title": "Name identifier object", + "properties": { + "schemeUri": { + "type": ["string", "null"], + "title": "URI (location) of the name identifier scheme" + }, + "nameIdentifier": { + "type": ["string", "null"], + "title": "Location of name identifier" + }, + "nameIdentifierScheme": { + "type": ["string", "null"], + "title": "Name of name identifier scheme" + } + } + } + }, + "affiliation": { + "type": "array", + "title": "List of affiliations", + "items": { + "type": "object", + "title": "Name affiliation object", + "properties": { + "name": { + "type": "string", + "title": "Name of the place of affiliation" + }, + "schemeUri": { + "type": "string", + "title": "URI (location) of the affiliation scheme" + }, + "affiliationIdentifier": { + "type": "string", + "title": "Location of affiliation identifier" + }, + "affiliationIdentifierScheme": { + "type": "string", + "title": "Name of affiliation identifier scheme" + } + } + } + } + }, + "additionalProperties": false + }, + "uniqueItems": true + }, + "dates": { + "type": "array", + "title": "List of relevant dates to publication", + "items": { + "type": "object", + "title": "Date object", + "required": ["date", "dateType"], + "properties": { + "date": { + "type": "string", + "title": "A standard format for a date value" + }, + "dateType": { + "type": "string", + "title": "Relevance of the date" + }, + "dateInformation": { + "type": "string", + "title": "Specific event of the date" + } + }, + "additionalProperties": false + }, + "uniqueItems": true + }, + "descriptions": { + "type": "array", + "title": "List of descriptions", + "items": { + "type": "object", + "title": "Description object", + "properties": { + "lang": { + "type": "string", + "title": "Language code of the description" + }, + "description": { + "type": "string", + "title": "Additional information that does not fit in any of the other categories" + }, + "descriptionType": { + "type": "string", + "title": "Type of description" + } + }, + "additionalProperties": false + }, + "uniqueItems": true + }, + "geoLocations": { + "type": "array", + "title": "List of GeoLocations", + "items": { + "type": "object", + "title": "GeoLocation object", + "properties": { + "geoLocationPlace": { + "type": "string", + "title": "Spatial region or named place where the data was gathered" + }, + "geoLocationPoint": { + "type": "object", + "title": "A point containing a single latitude-longitude pair", + "properties": { + "pointLongitude": { + "type": "string", + "title": "Longitude coordinate" + }, + "pointLatitude": { + "type": "string", + "title": "Latitude coordinate" + } + }, + "additionalProperties": false + }, + "geoLocationBox": { + "type": "object", + "title": "A box determined by two longitude and two latitude borders", + "properties": { + "westBoundLongitude": { + "type": "string", + "title": "Longitude coordinate of west bound" + }, + "eastBoundLongitude": { + "type": "string", + "title": "Longitude coordinate of east bound" + }, + "southBoundLatitude": { + "type": "string", + "title": "Latitude coordinate of south bound" + }, + "northBoundLatitude": { + "type": "string", + "title": "Latitude coordinate of north bound" + } + } + }, + "geoLocationPolygon": { + "type": "array", + "title": "A drawn polygon area, defined by a set of polygon points", + "items": { + "type": "object", + "title": "Polygon point object", + "properties": { + "pointLongitude": { + "type": "string", + "title": "Longitude coordinate" + }, + "pointLatitude": { + "type": "string", + "title": "Latitude coordinate" + } + } + } + } + }, + "additionalProperties": false + }, + "uniqueItems": true + }, + "language": { + "type": "string", + "title": "Code of the primary language of the resource" + }, + "alternateIdentifiers": { + "type": "array", + "title": "List of alternate identifiers", + "items": { + "type": "object", + "title": "An identifier or identifiers other than the primary Identifier of the resource", + "required": ["alternateIdentifier", "alternateIdentifierType"], + "properties": { + "alternateIdentifier": { + "type": "string", + "title": "Alternate identifier info" + }, + "alternateIdentifierType": { + "type": "string", + "title": "Type of alternate identifier" + } + }, + "additionalProperties": false + }, + "uniqueItems": true + }, + "relatedIdentifiers": { + "type": "array", + "title": "List of related identifiers", + "items": { + "type": "object", + "title": "Identifier of related resources", + "required": ["relatedIdentifier", "relatedIdentifierType", "relationType"], + "properties": { + "relatedIdentifier": { + "type": "string", + "title": "Related identifier info" + }, + "relatedIdentifierType": { + "type": "string", + "title": "Type of related identifier" + }, + "relationType": { + "type": "string", + "title": "Specification of the relation" + }, + "relatedMetadataScheme": { + "type": "string", + "title": "Scheme of related metadata" + }, + "schemeUri": { + "type": "string", + "title": "URI (location) of the related metadata scheme" + }, + "schemeType": { + "type": "string", + "title": "Type of the related metadata scheme" + }, + "resourceTypeGeneral": { + "type": "string", + "title": "Optional general type name" + } + }, + "additionalProperties": false + }, + "uniqueItems": true + }, + "sizes": { + "type": "array", + "title": "List of sizes", + "items": { + "type": "string", + "title": "Unstructured size information about the resource" + } + }, + "formats": { + "type": "array", + "title": "List of formats", + "items": { + "type": "string", + "title": "Technical format of the resource" + } + }, + "fundingReferences": { + "type": "array", + "title": "List of funding references", + "items": { + "type": "object", + "title": "Information about financial support for the resource", + "required": ["funderName", "funderIdentifier", "funderIdentifierType"], + "properties": { + "funderName": { + "type": "string", + "title": "Name of the funding provider" + }, + "funderIdentifier": { + "type": "string", + "title": "Unique identifier for funding entity" + }, + "funderIdentifierType": { + "type": "string", + "title": "Type of identifier for funding entity" + }, + "schemeUri": { + "type": ["string", "null"], + "title": "URI (location) of scheme for funder identifier" + }, + "awardNumber": { + "type": ["string", "null"], + "title": "The code assigned by the funder to a sponsored award" + }, + "awardTitle": { + "type": ["string", "null"], + "title": "The human readable title of the award" + }, + "awardUri": { + "type": ["string", "null"], + "title": "URI (location) of the award" + } + }, + "additionalProperties": false + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "extraInfo": { + "type": "object", + "title": "The extra DOI info schema", + "properties": { + "identifier": { + "type": "object", + "title": "identifier object", + "required": ["identifierType", "doi"], + "properties": { + "identifierType": { + "type": "string", + "title": "Type of identifier (= DOI)" + }, + "doi": { + "type": "string", + "title": "A persistent identifier for a resource" + } + } + }, + "publisher": { + "type": "string", + "title": "Full name of publisher from Research Organization Registry" + }, + "resourceType": { + "type": "object", + "title": "Type info of the resource", + "required": ["type", "resourceTypeGeneral"], + "properties": { + "type": { + "type": "string", + "title": "Name of resource type" + }, + "resourceTypeGeneral": { + "type": "string", + "title": "Mandatory general type name" + } + }, + "additionalProperties": false + }, + "url": { + "type": "string", + "title": "URL of the digital location of the object" + }, + "version": { + "type": "string", + "title": "Version number of the resource" + } + }, + "additionalProperties": false + }, + "metadataObjects": { + "type": "array", + "title": "The metadataObjects schema", + "items": { + "type": "object", + "title": "Folder objects", + "required": ["accessionId", "schema"], + "properties": { + "accessionId": { + "type": "string", + "title": "Accession Id" + }, + "schema": { + "type": "string", + "title": "Object's schema" + }, + "tags": { + "type": "object", + "title": "Different tags to describe the object.", + "additionalProperties": true, + "properties": { + "submissionType": { + "type": "string", + "title": "Type of submission", + "enum": ["XML", "Form"] + } + } + } + } + }, + "uniqueItems": true + }, + "drafts": { + "type": "array", + "title": "The drafts schema", + "items": { + "type": "object", + "title": "Folder objects", + "required": ["accessionId", "schema"], + "properties": { + "accessionId": { + "type": "string", + "title": "Accession Id" + }, + "schema": { + "type": "string", + "title": "Draft object's schema" + }, + "tags": { + "type": "object", + "title": "Different tags to describe the draft object.", + "additionalProperties": true, + "properties": { + "submissionType": { + "type": "string", + "title": "Type of submission", + "enum": ["XML", "Form"] + } + } + } + } + }, + "uniqueItems": true + } + }, + "additionalProperties": false +} diff --git a/src/theme.js b/src/theme.js index 611eb875..a551b6af 100644 --- a/src/theme.js +++ b/src/theme.js @@ -92,6 +92,7 @@ const CSCtheme = createTheme({ fontSize: defaultTheme.typography.pxToRem(14), boxShadow: defaultTheme.shadows[1], }, + form: {}, }) export default CSCtheme diff --git a/src/utils/JSONSchemaUtils.js b/src/utils/JSONSchemaUtils.js index bd304774..6736b47e 100644 --- a/src/utils/JSONSchemaUtils.js +++ b/src/utils/JSONSchemaUtils.js @@ -21,6 +21,11 @@ export const pathToName = (path: string[]): string => path.join(".") */ export const traverseValues = (object: any): any => { if (object["oneOf"]) return object + + if (typeof object["type"] === "object") { + return "" + } + switch (object["type"]) { case "object": { let values = {} @@ -50,6 +55,7 @@ export const traverseValues = (object: any): any => { case "null": { return null } + default: { console.error(` No initial value parsing support for type ${object["type"]} yet. From 7dc7cebd8936bea173a9a80d12ac38034d73b9f6 Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 20 Sep 2021 16:30:23 +0300 Subject: [PATCH 02/22] Create custom styles for form --- .../WizardForms/WizardDOIForm.js | 51 +------------------ .../WizardFillObjectDetailsForm.js | 48 +---------------- src/theme.js | 51 ++++++++++++++++++- 3 files changed, 52 insertions(+), 98 deletions(-) diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index 285607f7..f702b15f 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -16,56 +16,7 @@ import folderAPIService from "services/folderAPI" import { dereferenceSchema } from "utils/JSONSchemaUtils" const useStyles = makeStyles(theme => ({ - form: { - margin: theme.spacing(3, 2), - "& .MuiTextField-root > .Mui-required": { - color: theme.palette.primary.main, - }, - "& .MuiTextField-root": { - width: "48%", - margin: theme.spacing(1), - }, - "& .MuiTypography-root": { - margin: theme.spacing(1), - ...theme.typography.subtitle1, - fontWeight: "bold", - }, - "& .MuiTypography-h2": { - width: "100%", - color: theme.palette.primary.light, - borderBottom: `2px solid ${theme.palette.secondary.main}`, - }, - "& .MuiTypography-h3": { - width: "100%", - }, - "& .array": { - margin: theme.spacing(1), - - "& .arrayRow": { - display: "flex", - alignItems: "center", - marginBottom: theme.spacing(1), - width: "100%", - "& .MuiTextField-root": { - width: "95%", - }, - }, - "& h2, h3, h4, h5, h6": { - margin: theme.spacing(1, 0), - }, - "& .MuiPaper-elevation2": { - paddingRight: theme.spacing(1), - marginBottom: theme.spacing(1), - width: "60%", - "& .array": { margin: 0 }, - "& h3, h4": { margin: theme.spacing(1) }, - "& button": { margin: theme.spacing(1) }, - }, - "& .MuiSelect-outlined": { - marginRight: 0, - }, - }, - }, + form: theme.form, })) const DOIForm = ({ formId }: { formId: string }) => { diff --git a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js index fc967ed2..4a26c3ec 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js @@ -58,53 +58,7 @@ const useStyles = makeStyles(theme => ({ addIcon: { marginRight: theme.spacing(1), }, - formComponents: { - margin: theme.spacing(3, 2), - "& .MuiTextField-root > .Mui-required": { - color: theme.palette.primary.main, - }, - "& .MuiTextField-root": { - width: "48%", - margin: theme.spacing(1), - }, - "& .MuiTypography-root": { - margin: theme.spacing(1), - ...theme.typography.subtitle1, - fontWeight: "bold", - }, - "& .MuiTypography-h2": { - width: "100%", - color: theme.palette.primary.light, - borderBottom: `2px solid ${theme.palette.secondary.main}`, - }, - "& .MuiTypography-h3": { - width: "100%", - }, - "& .array": { - margin: theme.spacing(1), - "& .arrayRow": { - display: "flex", - alignItems: "center", - marginBottom: theme.spacing(1), - width: "100%", - "& .MuiTextField-root": { - width: "95%", - }, - }, - "& h2, h3, h4, h5, h6": { - margin: theme.spacing(1, 0), - }, - "& .MuiPaper-elevation2": { - paddingRight: theme.spacing(1), - "& .array": { margin: 0 }, - "& h3, h4": { margin: theme.spacing(1) }, - "& button": { margin: theme.spacing(0, 1) }, - }, - "& .MuiSelect-outlined": { - marginRight: 0, - }, - }, - }, + formComponents: theme.form, })) type CustomCardHeaderProps = { diff --git a/src/theme.js b/src/theme.js index a551b6af..912aef42 100644 --- a/src/theme.js +++ b/src/theme.js @@ -92,7 +92,56 @@ const CSCtheme = createTheme({ fontSize: defaultTheme.typography.pxToRem(14), boxShadow: defaultTheme.shadows[1], }, - form: {}, + form: { + margin: defaultTheme.spacing(3, 2), + "& .MuiTextField-root > .Mui-required": { + color: palette.primary.main, + }, + "& .MuiTextField-root": { + width: "48%", + margin: defaultTheme.spacing(1), + }, + "& .MuiTypography-root": { + margin: defaultTheme.spacing(1), + ...defaultTheme.typography.subtitle1, + fontWeight: "bold", + }, + "& .MuiTypography-h2": { + width: "100%", + color: palette.primary.light, + borderBottom: `2px solid ${palette.secondary.main}`, + }, + "& .MuiTypography-h3": { + width: "100%", + }, + "& .array": { + margin: defaultTheme.spacing(1), + + "& .arrayRow": { + display: "flex", + alignItems: "center", + marginBottom: defaultTheme.spacing(1), + width: "100%", + "& .MuiTextField-root": { + width: "95%", + }, + }, + "& h2, h3, h4, h5, h6": { + margin: defaultTheme.spacing(1, 0), + }, + "& .MuiPaper-elevation2": { + paddingRight: defaultTheme.spacing(1), + marginBottom: defaultTheme.spacing(1), + width: "60%", + "& .array": { margin: 0 }, + "& h3, h4": { margin: defaultTheme.spacing(1) }, + "& button": { margin: defaultTheme.spacing(1) }, + }, + "& .MuiSelect-outlined": { + marginRight: 0, + }, + }, + }, }) export default CSCtheme From 17effa47cb99db23e968745bb8244c2f5323a9c2 Mon Sep 17 00:00:00 2001 From: HangLe Date: Tue, 21 Sep 2021 08:15:09 +0300 Subject: [PATCH 03/22] Fix for wrong flow return type --- .../NewDraftWizard/WizardForms/WizardDOIForm.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index f702b15f..db8789ee 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -19,13 +19,16 @@ const useStyles = makeStyles(theme => ({ form: theme.form, })) -const DOIForm = ({ formId }: { formId: string }) => { +const DOIForm = ({ formId }: { formId: string }): React$Element => { const [doiSchema, setDoiSchema] = useState({}) - useEffect(async () => { - const loadData = await dereferenceSchema(folderSchema) - const doiData = loadData?.properties?.doiInfo - setDoiSchema(doiData) + useEffect(() => { + const getDoiSchema = async () => { + const loadData = await dereferenceSchema(folderSchema) + const doiData = loadData?.properties?.doiInfo + setDoiSchema(doiData) + } + getDoiSchema() }, []) const resolver = WizardAjvResolver(doiSchema) From f79e42d2946ef8eb32b1bcda5b0b61b4ae8bc7e5 Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 11 Oct 2021 10:30:01 +0300 Subject: [PATCH 04/22] Get dataCite schema from back-end and fix for doi form w --- src/App.js | 5 +- .../WizardForms/WizardDOIForm.js | 47 +- .../WizardForms/WizardJSONSchemaParser.js | 26 +- src/constants/folder.json | 573 ------------------ src/utils/JSONSchemaUtils.js | 4 - 5 files changed, 46 insertions(+), 609 deletions(-) delete mode 100644 src/constants/folder.json diff --git a/src/App.js b/src/App.js index 6a2c035f..6565c2db 100644 --- a/src/App.js +++ b/src/App.js @@ -90,7 +90,10 @@ const App = (): React$Element => { if (isMounted) { if (response.ok) { const schemas = response.data - .filter(schema => schema.title !== "Project" && schema.title !== "Submission") + .filter( + schema => + schema.title !== "Project" && schema.title !== "Submission" && schema.title !== "Datacite DOI schema" + ) .map(schema => schema.title.toLowerCase()) dispatch(setObjectTypesArray(schemas)) } else { diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index db8789ee..5fab9f89 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -3,16 +3,17 @@ import React, { useEffect, useState } from "react" import { makeStyles } from "@material-ui/core/styles" +import Ajv from "ajv" import { useForm, FormProvider } from "react-hook-form" import { useSelector, useDispatch } from "react-redux" import { WizardAjvResolver } from "./WizardAjvResolver" import JSONSchemaParser from "./WizardJSONSchemaParser" -import folderSchema from "constants/folder.json" import { WizardStatus } from "constants/wizardStatus" import { updateStatus } from "features/wizardStatusMessageSlice" import folderAPIService from "services/folderAPI" +import schemaAPIService from "services/schemaAPI" import { dereferenceSchema } from "utils/JSONSchemaUtils" const useStyles = makeStyles(theme => ({ @@ -20,18 +21,35 @@ const useStyles = makeStyles(theme => ({ })) const DOIForm = ({ formId }: { formId: string }): React$Element => { - const [doiSchema, setDoiSchema] = useState({}) + const [dataciteSchema, setDataciteSchema] = useState({}) useEffect(() => { - const getDoiSchema = async () => { - const loadData = await dereferenceSchema(folderSchema) - const doiData = loadData?.properties?.doiInfo - setDoiSchema(doiData) + const getDataciteSchema = async () => { + let dataciteSchema = sessionStorage.getItem(`cached_datacite_schema`) + if (!dataciteSchema || !new Ajv().validateSchema(JSON.parse(dataciteSchema))) { + try { + const response = await schemaAPIService.getSchemaByObjectType("datacite") + dataciteSchema = response.data + sessionStorage.setItem(`cached_datacite_schema`, JSON.stringify(dataciteSchema)) + } catch (err) { + dispatch( + updateStatus({ + successStatus: WizardStatus.error, + response: err, + errorPrefix: "Can't submit the DOI information", + }) + ) + } + } else { + dataciteSchema = JSON.parse(dataciteSchema) + } + const dereferencedDataciteSchema = await dereferenceSchema(dataciteSchema) + setDataciteSchema(dereferencedDataciteSchema) } - getDoiSchema() + getDataciteSchema() }, []) - const resolver = WizardAjvResolver(doiSchema) + const resolver = WizardAjvResolver(dataciteSchema) const methods = useForm({ mode: "onBlur", resolver }) const currentFolder = useSelector(state => state.submissionFolder) @@ -39,14 +57,15 @@ const DOIForm = ({ formId }: { formId: string }): React$Element { - const changes = [{ op: "add", path: "/doiInfo", value: data }] - const response = await folderAPIService.patchFolderById(currentFolder.folderId, changes) - if (!response.ok) { + try { + const changes = [{ op: "add", path: "/doiInfo", value: data }] + await folderAPIService.patchFolderById(currentFolder.folderId, changes) + } catch (err) { dispatch( updateStatus({ successStatus: WizardStatus.error, - response: response, - errorPrefix: "Can't submit the DOI information", + response: err, + errorPrefix: "Can't submit information for DOI.", }) ) } @@ -54,7 +73,7 @@ const DOIForm = ({ formId }: { formId: string }): React$Element
-
{Object.keys(doiSchema)?.length > 0 ? JSONSchemaParser.buildFields(doiSchema) : null}
+
{Object.keys(dataciteSchema)?.length > 0 ? JSONSchemaParser.buildFields(dataciteSchema) : null}
) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index 19eb3ab3..040e7be7 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -156,18 +156,6 @@ const traverseFields = ( if (object.oneOf) return - if (typeof object["type"] === "object") { - return ( - - ) - } switch (object.type) { case "object": { const properties = object.properties @@ -1040,11 +1028,15 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { return ( - {Object.keys(items).map(item => { - const pathForThisIndex = [...pathWithoutLastItem, lastPathItemWithIndex, item] - const requiredField = requiredProperties ? requiredProperties.filter(prop => prop === item) : [] - return traverseFields(properties[item], pathForThisIndex, requiredField, false, field) - })} + { + items + ? Object.keys(items).map(item => { + const pathForThisIndex = [...pathWithoutLastItem, lastPathItemWithIndex, item] + const requiredField = requiredProperties ? requiredProperties.filter(prop => prop === item) : [] + return traverseFields(properties[item], pathForThisIndex, requiredField, false, field) + }) + : traverseFields(object.items, [...pathWithoutLastItem, lastPathItemWithIndex], [], false, field) // special case for doiSchema's "sizes" and "formats" + } handleRemove(index)}> diff --git a/src/constants/folder.json b/src/constants/folder.json deleted file mode 100644 index f52ea5bb..00000000 --- a/src/constants/folder.json +++ /dev/null @@ -1,573 +0,0 @@ -{ - "type": "object", - "title": "Folder schema containing submitted metadata objects", - "required": ["name", "description"], - "properties": { - "folderId": { - "type": "string", - "title": "Folder Id" - }, - "name": { - "type": "string", - "title": "Folder Name" - }, - "description": { - "type": "string", - "title": "Folder Description" - }, - "dateCreated": { - "type": "integer", - "title": "Unix time stamp of creation, used for indexing" - }, - "published": { - "type": "boolean", - "title": "Published Folder" - }, - "doiInfo": { - "type": "object", - "title": "The DOI info schema", - "required": ["creators", "subjects"], - "properties": { - "creators": { - "type": "array", - "title": "List of creators", - "items": { - "type": "object", - "title": "Main researchers involved with data or the authors of the publication", - "properties": { - "name": { - "type": "string", - "title": "Full name of creator (format: Family, Given)" - }, - "nameType": { - "type": "string", - "title": "Type of name" - }, - "givenName": { - "type": "string", - "title": "First name" - }, - "familyName": { - "type": "string", - "title": "Last name" - }, - "nameIdentifiers": { - "type": "array", - "title": "List of name identifiers", - "items": { - "type": "object", - "title": "Name identifier object", - "properties": { - "schemeUri": { - "type": ["string", "null"], - "title": "URI (location) of the name identifier scheme" - }, - "nameIdentifier": { - "type": ["string", "null"], - "title": "URI (location) of name identifier" - }, - "nameIdentifierScheme": { - "type": ["string", "null"], - "title": "Name of name identifier scheme" - } - } - }, - "uniqueItems": true - }, - "affiliation": { - "type": "array", - "title": "List of affiliations", - "items": { - "type": "object", - "title": "Name affiliation object", - "properties": { - "name": { - "type": "string", - "title": "Name of the place of affiliation" - }, - "schemeUri": { - "type": "string", - "title": "URI (location) of the affiliation scheme" - }, - "affiliationIdentifier": { - "type": "string", - "title": "Location of affiliation identifier" - }, - "affiliationIdentifierScheme": { - "type": "string", - "title": "Name of affiliation identifier scheme" - } - } - }, - "uniqueItems": true - } - }, - "additionalProperties": false - }, - "uniqueItems": true - }, - "subjects": { - "type": "array", - "title": "List of subject identifiers specified by FOS", - "items": { - "type": "object", - "title": "Subject objects", - "required": ["subject"], - "properties": { - "subject": { - "type": "string", - "title": "FOS identifier" - }, - "subjectScheme": { - "type": "string", - "title": "Subject scheme name" - } - }, - "additionalProperties": true - }, - "uniqueItems": true - }, - "contributors": { - "type": "array", - "title": "List of contributors", - "items": { - "type": "object", - "title": "The institution or person responsible for contributing to the developement of the dataset", - "required": ["contributorType"], - "properties": { - "name": { - "type": "string", - "title": "Full name of contributor (format: Family, Given)" - }, - "nameType": { - "type": "string", - "title": "Type of name" - }, - "givenName": { - "type": "string", - "title": "First name" - }, - "familyName": { - "type": "string", - "title": "Last name" - }, - "contributorType": { - "type": "string", - "title": "Type of contributor" - }, - "nameIdentifiers": { - "type": "array", - "title": "List of name identifiers", - "items": { - "type": "object", - "title": "Name identifier object", - "properties": { - "schemeUri": { - "type": ["string", "null"], - "title": "URI (location) of the name identifier scheme" - }, - "nameIdentifier": { - "type": ["string", "null"], - "title": "Location of name identifier" - }, - "nameIdentifierScheme": { - "type": ["string", "null"], - "title": "Name of name identifier scheme" - } - } - } - }, - "affiliation": { - "type": "array", - "title": "List of affiliations", - "items": { - "type": "object", - "title": "Name affiliation object", - "properties": { - "name": { - "type": "string", - "title": "Name of the place of affiliation" - }, - "schemeUri": { - "type": "string", - "title": "URI (location) of the affiliation scheme" - }, - "affiliationIdentifier": { - "type": "string", - "title": "Location of affiliation identifier" - }, - "affiliationIdentifierScheme": { - "type": "string", - "title": "Name of affiliation identifier scheme" - } - } - } - } - }, - "additionalProperties": false - }, - "uniqueItems": true - }, - "dates": { - "type": "array", - "title": "List of relevant dates to publication", - "items": { - "type": "object", - "title": "Date object", - "required": ["date", "dateType"], - "properties": { - "date": { - "type": "string", - "title": "A standard format for a date value" - }, - "dateType": { - "type": "string", - "title": "Relevance of the date" - }, - "dateInformation": { - "type": "string", - "title": "Specific event of the date" - } - }, - "additionalProperties": false - }, - "uniqueItems": true - }, - "descriptions": { - "type": "array", - "title": "List of descriptions", - "items": { - "type": "object", - "title": "Description object", - "properties": { - "lang": { - "type": "string", - "title": "Language code of the description" - }, - "description": { - "type": "string", - "title": "Additional information that does not fit in any of the other categories" - }, - "descriptionType": { - "type": "string", - "title": "Type of description" - } - }, - "additionalProperties": false - }, - "uniqueItems": true - }, - "geoLocations": { - "type": "array", - "title": "List of GeoLocations", - "items": { - "type": "object", - "title": "GeoLocation object", - "properties": { - "geoLocationPlace": { - "type": "string", - "title": "Spatial region or named place where the data was gathered" - }, - "geoLocationPoint": { - "type": "object", - "title": "A point containing a single latitude-longitude pair", - "properties": { - "pointLongitude": { - "type": "string", - "title": "Longitude coordinate" - }, - "pointLatitude": { - "type": "string", - "title": "Latitude coordinate" - } - }, - "additionalProperties": false - }, - "geoLocationBox": { - "type": "object", - "title": "A box determined by two longitude and two latitude borders", - "properties": { - "westBoundLongitude": { - "type": "string", - "title": "Longitude coordinate of west bound" - }, - "eastBoundLongitude": { - "type": "string", - "title": "Longitude coordinate of east bound" - }, - "southBoundLatitude": { - "type": "string", - "title": "Latitude coordinate of south bound" - }, - "northBoundLatitude": { - "type": "string", - "title": "Latitude coordinate of north bound" - } - } - }, - "geoLocationPolygon": { - "type": "array", - "title": "A drawn polygon area, defined by a set of polygon points", - "items": { - "type": "object", - "title": "Polygon point object", - "properties": { - "pointLongitude": { - "type": "string", - "title": "Longitude coordinate" - }, - "pointLatitude": { - "type": "string", - "title": "Latitude coordinate" - } - } - } - } - }, - "additionalProperties": false - }, - "uniqueItems": true - }, - "language": { - "type": "string", - "title": "Code of the primary language of the resource" - }, - "alternateIdentifiers": { - "type": "array", - "title": "List of alternate identifiers", - "items": { - "type": "object", - "title": "An identifier or identifiers other than the primary Identifier of the resource", - "required": ["alternateIdentifier", "alternateIdentifierType"], - "properties": { - "alternateIdentifier": { - "type": "string", - "title": "Alternate identifier info" - }, - "alternateIdentifierType": { - "type": "string", - "title": "Type of alternate identifier" - } - }, - "additionalProperties": false - }, - "uniqueItems": true - }, - "relatedIdentifiers": { - "type": "array", - "title": "List of related identifiers", - "items": { - "type": "object", - "title": "Identifier of related resources", - "required": ["relatedIdentifier", "relatedIdentifierType", "relationType"], - "properties": { - "relatedIdentifier": { - "type": "string", - "title": "Related identifier info" - }, - "relatedIdentifierType": { - "type": "string", - "title": "Type of related identifier" - }, - "relationType": { - "type": "string", - "title": "Specification of the relation" - }, - "relatedMetadataScheme": { - "type": "string", - "title": "Scheme of related metadata" - }, - "schemeUri": { - "type": "string", - "title": "URI (location) of the related metadata scheme" - }, - "schemeType": { - "type": "string", - "title": "Type of the related metadata scheme" - }, - "resourceTypeGeneral": { - "type": "string", - "title": "Optional general type name" - } - }, - "additionalProperties": false - }, - "uniqueItems": true - }, - "sizes": { - "type": "array", - "title": "List of sizes", - "items": { - "type": "string", - "title": "Unstructured size information about the resource" - } - }, - "formats": { - "type": "array", - "title": "List of formats", - "items": { - "type": "string", - "title": "Technical format of the resource" - } - }, - "fundingReferences": { - "type": "array", - "title": "List of funding references", - "items": { - "type": "object", - "title": "Information about financial support for the resource", - "required": ["funderName", "funderIdentifier", "funderIdentifierType"], - "properties": { - "funderName": { - "type": "string", - "title": "Name of the funding provider" - }, - "funderIdentifier": { - "type": "string", - "title": "Unique identifier for funding entity" - }, - "funderIdentifierType": { - "type": "string", - "title": "Type of identifier for funding entity" - }, - "schemeUri": { - "type": ["string", "null"], - "title": "URI (location) of scheme for funder identifier" - }, - "awardNumber": { - "type": ["string", "null"], - "title": "The code assigned by the funder to a sponsored award" - }, - "awardTitle": { - "type": ["string", "null"], - "title": "The human readable title of the award" - }, - "awardUri": { - "type": ["string", "null"], - "title": "URI (location) of the award" - } - }, - "additionalProperties": false - }, - "uniqueItems": true - } - }, - "additionalProperties": false - }, - "extraInfo": { - "type": "object", - "title": "The extra DOI info schema", - "properties": { - "identifier": { - "type": "object", - "title": "identifier object", - "required": ["identifierType", "doi"], - "properties": { - "identifierType": { - "type": "string", - "title": "Type of identifier (= DOI)" - }, - "doi": { - "type": "string", - "title": "A persistent identifier for a resource" - } - } - }, - "publisher": { - "type": "string", - "title": "Full name of publisher from Research Organization Registry" - }, - "resourceType": { - "type": "object", - "title": "Type info of the resource", - "required": ["type", "resourceTypeGeneral"], - "properties": { - "type": { - "type": "string", - "title": "Name of resource type" - }, - "resourceTypeGeneral": { - "type": "string", - "title": "Mandatory general type name" - } - }, - "additionalProperties": false - }, - "url": { - "type": "string", - "title": "URL of the digital location of the object" - }, - "version": { - "type": "string", - "title": "Version number of the resource" - } - }, - "additionalProperties": false - }, - "metadataObjects": { - "type": "array", - "title": "The metadataObjects schema", - "items": { - "type": "object", - "title": "Folder objects", - "required": ["accessionId", "schema"], - "properties": { - "accessionId": { - "type": "string", - "title": "Accession Id" - }, - "schema": { - "type": "string", - "title": "Object's schema" - }, - "tags": { - "type": "object", - "title": "Different tags to describe the object.", - "additionalProperties": true, - "properties": { - "submissionType": { - "type": "string", - "title": "Type of submission", - "enum": ["XML", "Form"] - } - } - } - } - }, - "uniqueItems": true - }, - "drafts": { - "type": "array", - "title": "The drafts schema", - "items": { - "type": "object", - "title": "Folder objects", - "required": ["accessionId", "schema"], - "properties": { - "accessionId": { - "type": "string", - "title": "Accession Id" - }, - "schema": { - "type": "string", - "title": "Draft object's schema" - }, - "tags": { - "type": "object", - "title": "Different tags to describe the draft object.", - "additionalProperties": true, - "properties": { - "submissionType": { - "type": "string", - "title": "Type of submission", - "enum": ["XML", "Form"] - } - } - } - } - }, - "uniqueItems": true - } - }, - "additionalProperties": false -} diff --git a/src/utils/JSONSchemaUtils.js b/src/utils/JSONSchemaUtils.js index 6736b47e..09aea2f9 100644 --- a/src/utils/JSONSchemaUtils.js +++ b/src/utils/JSONSchemaUtils.js @@ -22,10 +22,6 @@ export const pathToName = (path: string[]): string => path.join(".") export const traverseValues = (object: any): any => { if (object["oneOf"]) return object - if (typeof object["type"] === "object") { - return "" - } - switch (object["type"]) { case "object": { let values = {} From fcef83960a2c67789438a35b29f3bd2dafb3769d Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 18 Oct 2021 09:50:27 +0300 Subject: [PATCH 05/22] Fix for FormAutocompleteField for less re-rendering --- .../WizardForms/WizardJSONSchemaParser.js | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index 040e7be7..56a23989 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -152,7 +152,7 @@ const traverseFields = ( const label = object.title ?? lastPathItem const required = !!requiredProperties?.includes(lastPathItem) || requireFirst || false const description = object.description - const autoCompleteIdentifiers = ["organisation"] + const autoCompleteIdentifiers = ["organisation", "name of the place of affiliation"] if (object.oneOf) return @@ -634,11 +634,13 @@ const FormAutocompleteField = ({ const defaultValue = getValues(name) || "" const fetchOrganisations = async searchTerm => { - const response = await rorAPIService.getOrganisations(searchTerm) + // Check if searchTerm includes non-word char, for e.g. "(", ")", "-" because the api does not work with those chars + const isContainingNonWordChar = searchTerm.match(/\W/g) + const response = isContainingNonWordChar === null ? await rorAPIService.getOrganisations(searchTerm) : null if (response) setLoading(false) - if (response.ok) { + if (response?.ok) { const mappedOrganisations = response.data.items.map(org => ({ name: org.name })) setOptions(mappedOrganisations) } @@ -647,7 +649,7 @@ const FormAutocompleteField = ({ const debouncedSearch = useCallback( _.debounce(newInput => { if (newInput.length > 0) fetchOrganisations(newInput) - }, 0), + }, 150), [] ) @@ -670,6 +672,21 @@ const FormAutocompleteField = ({ } }, [selection, inputValue, fetch]) + const handleInputChange = (event, newInputValue, reason) => { + setInputValue(newInputValue) + switch (reason) { + case "input": + case "clear": + setInputValue(newInputValue) + break + case "reset": + selection ? setInputValue(selection?.name) : null + break + default: + break + } + } + return ( ( @@ -685,7 +702,7 @@ const FormAutocompleteField = ({ setOpen(false) }} options={options} - getOptionLabel={option => option.name || defaultValue} + getOptionLabel={option => option.name || ""} data-testid={name} disableClearable={inputValue.length === 0} renderInput={params => ( @@ -712,12 +729,9 @@ const FormAutocompleteField = ({ setSelection(option) setValue(name, option?.name) }} - onInputChange={(event, newInputValue) => { - setOptions([]) - setInputValue(newInputValue) - setValue(name, newInputValue) - }} + onInputChange={handleInputChange} value={defaultValue} + inputValue={inputValue} /> {description && ( Date: Fri, 22 Oct 2021 13:48:29 +0300 Subject: [PATCH 06/22] Add dependent field value if autocomplete field is set --- .../WizardForms/WizardDOIForm.js | 2 + .../WizardForms/WizardJSONSchemaParser.js | 389 ++++++++++-------- .../WizardSteps/WizardShowSummaryStep.js | 13 +- src/features/autocompleteSlice.js | 16 + src/rootReducer.js | 2 + 5 files changed, 239 insertions(+), 183 deletions(-) create mode 100644 src/features/autocompleteSlice.js diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index 5fab9f89..db3783f1 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -11,6 +11,7 @@ import { WizardAjvResolver } from "./WizardAjvResolver" import JSONSchemaParser from "./WizardJSONSchemaParser" import { WizardStatus } from "constants/wizardStatus" +import { resetAutocompleteField } from "features/autocompleteSlice" import { updateStatus } from "features/wizardStatusMessageSlice" import folderAPIService from "services/folderAPI" import schemaAPIService from "services/schemaAPI" @@ -69,6 +70,7 @@ const DOIForm = ({ formId }: { formId: string }): React$Element diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index 56a23989..49d060e5 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -23,9 +23,10 @@ import RemoveIcon from "@material-ui/icons/Remove" import Autocomplete from "@material-ui/lab/Autocomplete" import * as _ from "lodash" import { get } from "lodash" -import { useFieldArray, useFormContext, useForm, Controller } from "react-hook-form" -import { useSelector } from "react-redux" +import { useFieldArray, useFormContext, useForm, Controller, useWatch } from "react-hook-form" +import { useSelector, useDispatch } from "react-redux" +import { setAutocompleteField } from "features/autocompleteSlice" import rorAPIService from "services/rorAPI" import { pathToName, traverseValues } from "utils/JSONSchemaUtils" @@ -149,6 +150,7 @@ const traverseFields = ( ) => { const name = pathToName(path) const [lastPathItem] = path.slice(-1) + const label = object.title ?? lastPathItem const required = !!requiredProperties?.includes(lastPathItem) || requireFirst || false const description = object.description @@ -559,57 +561,74 @@ const FormTextField = ({ description, type = "string", nestedField, -}: FormFieldBaseProps & { description: string, type?: string, nestedField?: any }) => ( - - {({ control }) => { - const classes = useStyles() - const multiLineRowIdentifiers = ["description", "abstract", "policy text"] +}: FormFieldBaseProps & { description: string, type?: string, nestedField?: any }) => { + const autocompleteField = useSelector(state => state.autocompleteField) + const path = name.split(".") + const [lastPathItem] = path.slice(-1) - const { - formState: { errors }, - } = useFormContext() + const autofilledFields = ["affiliationIdentifier", "schemeUri", "affiliationIdentifierScheme"] - return ( - { - const error = _.get(errors, name) - return ( -
- label.toLowerCase().includes(value))} - rows={5} - value={(typeof field.value !== "object" && field.value) || ""} - onChange={e => { - const val = e.target.value - field.onChange(type === "string" && !isNaN(val) ? val.toString() : val) - }} - /> - {description && ( - - - - )} -
- ) - }} - name={name} - control={control} - defaultValue={getDefaultValue(nestedField, name)} - rules={{ required: required }} - /> - ) - }} -
-) + const watchFieldName = autofilledFields.includes(lastPathItem) ? path.slice(0, -1).join(".").concat(".", "name") : "" + const watchFieldValue = watchFieldName ? useWatch(watchFieldName) : null + const check = watchFieldValue ? get(watchFieldValue, watchFieldName) : null + + return ( + + {({ control, setValue, getValues }) => { + const classes = useStyles() + const multiLineRowIdentifiers = ["description", "abstract", "policy text"] + + const val = getValues(name) + useEffect(() => { + if (check && !val) { + lastPathItem === autofilledFields[0] ? setValue(name, autocompleteField) : null + lastPathItem === autofilledFields[1] ? setValue(name, "https://ror.org") : null + lastPathItem === autofilledFields[2] ? setValue(name, "ROR") : null + } + }, [autocompleteField]) + + return ( + { + return ( +
+ label.toLowerCase().includes(value))} + rows={5} + value={val || (typeof field.value !== "object" && field.value) || ""} + onChange={e => { + const val = e.target.value + field.onChange(type === "string" && !isNaN(val) ? val.toString() : val) + }} + disabled={val?.length > 0} + /> + {description && ( + + + + )} +
+ ) + }} + name={name} + control={control} + defaultValue={getDefaultValue(nestedField, name)} + rules={{ required: required }} + /> + ) + }} +
+ ) +} /* * FormAutocompleteField uses ROR API to fetch organisations @@ -619,150 +638,159 @@ const FormAutocompleteField = ({ label, required, description, -}: FormFieldBaseProps & { type?: string, nestedField?: any, description: string }) => ( - - {({ errors, getValues, control, setValue }) => { - const error = _.get(errors, name) - const classes = useStyles() +}: FormFieldBaseProps & { type?: string, nestedField?: any, description: string }) => { + const dispatch = useDispatch() + + return ( + + {({ errors, getValues, control, setValue }) => { + const error = _.get(errors, name) + const classes = useStyles() - const [selection, setSelection] = useState(null) - const [open, setOpen] = useState(false) - const [options, setOptions] = useState([]) - const [inputValue, setInputValue] = useState("") - const [loading, setLoading] = useState(false) + const [selection, setSelection] = useState(null) + const [open, setOpen] = useState(false) + const [options, setOptions] = useState([]) + const [inputValue, setInputValue] = useState("") + const [loading, setLoading] = useState(false) - const defaultValue = getValues(name) || "" + const defaultValue = getValues(name) || "" - const fetchOrganisations = async searchTerm => { - // Check if searchTerm includes non-word char, for e.g. "(", ")", "-" because the api does not work with those chars - const isContainingNonWordChar = searchTerm.match(/\W/g) - const response = isContainingNonWordChar === null ? await rorAPIService.getOrganisations(searchTerm) : null + const fetchOrganisations = async searchTerm => { + // Check if searchTerm includes non-word char, for e.g. "(", ")", "-" because the api does not work with those chars + const isContainingNonWordChar = searchTerm.match(/\W/g) + const response = isContainingNonWordChar === null ? await rorAPIService.getOrganisations(searchTerm) : null - if (response) setLoading(false) + if (response) setLoading(false) - if (response?.ok) { - const mappedOrganisations = response.data.items.map(org => ({ name: org.name })) - setOptions(mappedOrganisations) + if (response?.ok) { + const mappedOrganisations = response.data.items.map(org => ({ name: org.name, id: org.id })) + setOptions(mappedOrganisations) + } } - } - const debouncedSearch = useCallback( - _.debounce(newInput => { - if (newInput.length > 0) fetchOrganisations(newInput) - }, 150), - [] - ) + const debouncedSearch = useCallback( + _.debounce(newInput => { + if (newInput.length > 0) fetchOrganisations(newInput) + }, 150), + [] + ) + + useEffect(() => { + let active = true - useEffect(() => { - let active = true + if (inputValue === "") { + setOptions([]) + return undefined + } - if (inputValue === "") { - setOptions([]) - return undefined - } + if (active && open) { + setLoading(true) + debouncedSearch(inputValue) + } - if (active && open) { - setLoading(true) - debouncedSearch(inputValue) - } + return () => { + active = false + setLoading(false) + } + }, [selection, inputValue, fetch]) - return () => { - active = false - setLoading(false) + // const fieldsToBePrefilled = ["schemeUri", "affiliationIdentifier", "affiliationIdentifierScheme"] + + const handleAutocompleteValueChange = (event, option) => { + setSelection(option) + setValue(name, option?.name) + dispatch(setAutocompleteField(option?.id)) } - }, [selection, inputValue, fetch]) - - const handleInputChange = (event, newInputValue, reason) => { - setInputValue(newInputValue) - switch (reason) { - case "input": - case "clear": - setInputValue(newInputValue) - break - case "reset": - selection ? setInputValue(selection?.name) : null - break - default: - break + + const handleInputChange = (event, newInputValue, reason) => { + setInputValue(newInputValue) + switch (reason) { + case "input": + case "clear": + setInputValue(newInputValue) + break + case "reset": + selection ? setInputValue(selection?.name) : null + break + default: + break + } } - } - return ( - ( -
- { - setOpen(true) - }} - onClose={() => { - setOpen(false) - }} - options={options} - getOptionLabel={option => option.name || ""} - data-testid={name} - disableClearable={inputValue.length === 0} - renderInput={params => ( - - {loading ? : null} - {params.InputProps.endAdornment} - - ), - }} - /> + return ( + ( +
+ { + setOpen(true) + }} + onClose={() => { + setOpen(false) + }} + options={options} + getOptionLabel={option => option.name || ""} + data-testid={name} + disableClearable={inputValue.length === 0} + renderInput={params => ( + + {loading ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + onChange={handleAutocompleteValueChange} + onInputChange={handleInputChange} + value={defaultValue} + inputValue={inputValue} + /> + {description && ( + + {description} +
+ {"Organisations provided by "} + + {"ror.org"} + + + {"."} + + } + placement="top" + arrow + interactive + > + +
)} - onChange={(event, option) => { - setSelection(option) - setValue(name, option?.name) - }} - onInputChange={handleInputChange} - value={defaultValue} - inputValue={inputValue} - /> - {description && ( - - {description} -
- {"Organisations provided by "} - - {"ror.org"} - - - {"."} - - } - placement="top" - arrow - interactive - > - -
- )} -
- )} - name={name} - control={control} - /> - ) - }} - -) +
+ )} + name={name} + control={control} + /> + ) + }} +
+ ) +} /* * FormSelectField is rendered for choosing one from many options @@ -1013,7 +1041,6 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { {fields.map((field, index) => { - const [lastPathItem] = path.slice(-1) const pathWithoutLastItem = path.slice(0, -1) const lastPathItemWithIndex = `${lastPathItem}.${index}` diff --git a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js index 81669be2..fbd534ef 100644 --- a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js +++ b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js @@ -13,13 +13,14 @@ import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction" import ListItemText from "@material-ui/core/ListItemText" import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" -import { useSelector } from "react-redux" +import { useSelector, useDispatch } from "react-redux" import WizardHeader from "../WizardComponents/WizardHeader" import WizardSavedObjectActions from "../WizardComponents/WizardSavedObjectActions" import WizardStepper from "../WizardComponents/WizardStepper" import WizardDOIForm from "../WizardForms/WizardDOIForm" +import { resetAutocompleteField } from "features/autocompleteSlice" import type { ObjectInsideFolderWithTags } from "types" import { getItemPrimaryText, formatDisplayObjectType } from "utils" @@ -74,6 +75,7 @@ const WizardShowSummaryStep = (): React$Element => { const [openDoiDialog, setOpenDoiDialog] = useState(false) const classes = useStyles() + const dispatch = useDispatch() const DOIDialog = () => ( => { - - {openDoiDialog && } + {openedDoiForm && } ) } diff --git a/src/features/fileTypesSlice.js b/src/features/fileTypesSlice.js new file mode 100644 index 00000000..4b51d8e8 --- /dev/null +++ b/src/features/fileTypesSlice.js @@ -0,0 +1,27 @@ +//@flow +import { createSlice } from "@reduxjs/toolkit" +import { reject } from "lodash" + +const initialState = [] + +const fileTypesSlice: any = createSlice({ + name: "fileTypes", + initialState, + reducers: { + setFileTypes: (state, action) => { + if (state.find(obj => obj.accessionId === action.payload.accessionId)) { + state = state.filter(obj => obj.accessionId !== action.payload.accessionId) + } + state.push(action.payload) + }, + deleteFileType: (state, action) => { + return (state = reject(state, function (o) { + return o.accessionId === action.payload + })) + }, + resetFileTypes: () => initialState, + }, +}) + +export const { setFileTypes, deleteFileType, resetFileTypes } = fileTypesSlice.actions +export default fileTypesSlice.reducer diff --git a/src/features/openedDoiFormSlice.js b/src/features/openedDoiFormSlice.js new file mode 100644 index 00000000..4dd8fc20 --- /dev/null +++ b/src/features/openedDoiFormSlice.js @@ -0,0 +1,16 @@ +//@flow +import { createSlice } from "@reduxjs/toolkit" + +const initialState: boolean = false + +const openedDoiFormSlice: any = createSlice({ + name: "openedDoiForm", + initialState, + reducers: { + setOpenedDoiForm: (state, action) => action.payload, + resetOpenedDoiForm: () => initialState, + }, +}) + +export const { setOpenedDoiForm, resetOpenedDoiForm } = openedDoiFormSlice.actions +export default openedDoiFormSlice.reducer diff --git a/src/rootReducer.js b/src/rootReducer.js index 0073a49f..b18001df 100644 --- a/src/rootReducer.js +++ b/src/rootReducer.js @@ -5,10 +5,12 @@ import { combineReducers } from "@reduxjs/toolkit" import autocompleteReducer from "features/autocompleteSlice" import clearFormReducer from "features/clearFormSlice" import draftStatusReducer from "features/draftStatusSlice" +import fileTypesReducer from "features/fileTypesSlice" import focusReducer from "features/focusSlice" import loadingReducer from "features/loadingSlice" import localeReducer from "features/localeSlice" import objectTypesArrayReducer from "features/objectTypesArraySlice" +import openedDoiFormReducer from "features/openedDoiFormSlice" import openedRowsReducer from "features/openedRowsSlice" import publishedFoldersReducer from "features/publishedFoldersSlice" import selectedFolderReducer from "features/selectedFolderSlice" @@ -44,6 +46,8 @@ const rootReducer: any = combineReducers({ clearForm: clearFormReducer, templateAccessionIds: templatesReducer, autocompleteField: autocompleteReducer, + fileTypes: fileTypesReducer, + openedDoiForm: openedDoiFormReducer, }) export default rootReducer diff --git a/src/utils/index.js b/src/utils/index.js index fec4bf90..48a441c0 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -10,6 +10,7 @@ import MUITablePagination from "@material-ui/core/TablePagination" import TableRow from "@material-ui/core/TableRow" import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" +import { uniqBy } from "lodash" import { useLocation } from "react-router-dom" import { Locale } from "constants/locale" @@ -221,3 +222,17 @@ export const pathWithLocale = (path: string): any => { return `/${locale}/${path}` } + +// Get unique "fileTypes" from Run form or Analysis form +export const getNewUniqueFileTypes = ( + objectAccessionId: ?string, + formData: any +): { accessionId: string, fileTypes: Array } | null => { + if (formData.files?.length > 0 && objectAccessionId) { + // Get unique fileTypes from current objectType and Add the new unique types fileTypes state in Redux + const fileTypes = uniqBy(formData.files.map(file => file.filetype)) + const objectWithFileTypes = { accessionId: objectAccessionId, fileTypes } + return objectWithFileTypes + } + return null +} From b9ffdedc8f91b45286789fd907809ee42094605c Mon Sep 17 00:00:00 2001 From: HangLe Date: Fri, 29 Oct 2021 17:01:42 +0300 Subject: [PATCH 11/22] Fix for merging errors w w --- .../WizardComponents/WizardFooter.js | 1 + .../WizardSavedObjectActions.js | 1 + .../WizardForms/WizardDOIForm.js | 8 +++---- .../WizardFillObjectDetailsForm.js | 2 +- .../WizardForms/WizardJSONSchemaParser.js | 22 +++++++++---------- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardFooter.js b/src/components/NewDraftWizard/WizardComponents/WizardFooter.js index 73e0cdc0..dd3a9219 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardFooter.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardFooter.js @@ -11,6 +11,7 @@ import WizardAlert from "./WizardAlert" import saveDraftsAsTemplates from "components/NewDraftWizard/WizardHooks/WizardSaveTemplatesHook" import { ResponseStatus } from "constants/responseStatus" +import { resetFileTypes } from "features/fileTypesSlice" import { updateStatus } from "features/statusMessageSlice" import { resetObjectType } from "features/wizardObjectTypeSlice" import { publishFolderContent, deleteFolderAndContent, resetFolder } from "features/wizardSubmissionFolderSlice" diff --git a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectActions.js b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectActions.js index c9d6db12..74162001 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectActions.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectActions.js @@ -10,6 +10,7 @@ import { useHistory } from "react-router-dom" import { ResponseStatus } from "constants/responseStatus" import { ObjectSubmissionTypes, ObjectStatus, ObjectTypes } from "constants/wizardObject" import { deleteFileType } from "features/fileTypesSlice" +import { updateStatus } from "features/statusMessageSlice" import { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice" import { setObjectType } from "features/wizardObjectTypeSlice" import { deleteObjectFromFolder } from "features/wizardSubmissionFolderSlice" diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index db3783f1..28e314b1 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -10,9 +10,9 @@ import { useSelector, useDispatch } from "react-redux" import { WizardAjvResolver } from "./WizardAjvResolver" import JSONSchemaParser from "./WizardJSONSchemaParser" -import { WizardStatus } from "constants/wizardStatus" +import { ResponseStatus } from "constants/responseStatus" import { resetAutocompleteField } from "features/autocompleteSlice" -import { updateStatus } from "features/wizardStatusMessageSlice" +import { updateStatus } from "features/statusMessageSlice" import folderAPIService from "services/folderAPI" import schemaAPIService from "services/schemaAPI" import { dereferenceSchema } from "utils/JSONSchemaUtils" @@ -35,7 +35,7 @@ const DOIForm = ({ formId }: { formId: string }): React$Element { const getDefaultValue = (nestedField?: any, name: string) => { if (nestedField) { const path = name.split(".") - if (path.length > 1) { + // E.g. Case of DOI form - Formats's fields + if (path[0] === "formats") { + const k = path[0] + if (k in nestedField) { + nestedField = nestedField[k] + } else { + return + } + } else { for (var i = 1, n = path.length; i < n; ++i) { var k = path[i] + if (k in nestedField) { nestedField = nestedField[k] } else { @@ -133,15 +142,6 @@ const getDefaultValue = (nestedField?: any, name: string) => { } } } - // E.g. Case of DOI form - Formats's fields - if (path.length === 1) { - const k = path[0].split("[")[0] - if (k in nestedField) { - nestedField = nestedField[k] - } else { - return - } - } return nestedField } else { return "" @@ -1053,7 +1053,7 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { }, [fields]) // Get unique fileTypes from submitted fileTypes - const uniqueFileTypes = uniqBy(flatten(fileTypes.map(obj => obj.fileTypes))) + const uniqueFileTypes = uniqBy(flatten(fileTypes?.map(obj => obj.fileTypes))) useEffect(() => { // Append fileType to formats' field From 6bcd7f8ac6aedb7c746fbf613eb22cdf06cdc1f6 Mon Sep 17 00:00:00 2001 From: HangLe Date: Tue, 2 Nov 2021 07:53:12 +0200 Subject: [PATCH 12/22] Add e2e test for DOI form and bug fixing w w --- cypress/integration/doiForm.spec.js | 151 ++++++++++++++++++ .../integration/objectLinksAttributes.spec.js | 7 + .../WizardForms/WizardJSONSchemaParser.js | 7 +- 3 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 cypress/integration/doiForm.spec.js diff --git a/cypress/integration/doiForm.spec.js b/cypress/integration/doiForm.spec.js new file mode 100644 index 00000000..da17751a --- /dev/null +++ b/cypress/integration/doiForm.spec.js @@ -0,0 +1,151 @@ +describe("DOI form", function () { + it("should render DOI form correctly with affiliation's autocomplete field and formats' prefilled values ", () => { + cy.login() + + cy.get("button", { timeout: 10000 }).contains("Create Submission").click() + + // Add folder name & description, navigate to submissions + cy.get("input[name='name']").type("Test name") + cy.get("textarea[name='description']").type("Test description") + cy.get("button[type=button]").contains("Next").click() + + cy.wait(500) + + // Fill in Run form + cy.clickFillForm("Run") + + const testRunData = { + title: "Test Run", + experimentRefLabel: "Test Experiment reference label", + } + + cy.get("input[data-testid='title']").type(testRunData.title) + cy.get("h2[data-testid='experimentRef']").parents().children("button").click() + cy.get("[data-testid='experimentRef.0.label']").type(testRunData.experimentRefLabel) + + const testRunFile = { + fileName: "Run file name", + fileType: "bam", + checksumMethod: "MD5", + checksum: "Run file check sum", + } + cy.get("h2[data-testid='files']").parents().children("button").click() + cy.get("input[name='files.0.filename']").type(testRunFile.fileName) + cy.get("select[name='files.0.filetype']").select(testRunFile.fileType) + cy.get("select[name='files.0.checksumMethod']").select(testRunFile.checksumMethod) + cy.get("input[name='files.0.checksum']").type(testRunFile.checksum) + + // Submit form + cy.get("button[type=submit]").contains("Submit").click() + + // Submitted objects list should have newly added item from Run form + cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 1) + + // Fill in Analysis form + cy.clickFillForm("Analysis") + + const testAnalysisData = { + title1: "Test Analysis", + title2: "Test Analysis 2", + analysisType: "Reference Alignment", + refAlignmentAssembly: "Standard", + refAlignmentAssemblyId: "Standard Accession Id", + } + + cy.get("input[name='title']").type(testAnalysisData.title1) + cy.get("select[name='analysisType']").select("Reference Alignment") + cy.get("select[name='analysisType.referenceAlignment.assembly']").select("Standard") + cy.get("input[name='analysisType.referenceAlignment.assembly.accessionId']").type("Standard Accession Id") + + const testAnalysisFile = { + fileName: "Analysis file name", + fileType1: "cram", + fileType2: "bam", + checksumMethod: "SHA-256", + checksum: "Analysis file check sum", + } + // Select fileType + cy.get("h2[data-testid='files']").parents().children("button").click() + cy.get("input[name='files.0.filename']").type(testAnalysisFile.fileName) + cy.get("select[name='files.0.filetype']").select(testAnalysisFile.fileType1) + cy.get("select[name='files.0.checksumMethod']").select(testAnalysisFile.checksumMethod) + cy.get("input[name='files.0.checksum']").type(testAnalysisFile.checksum) + + // Submit form + cy.get("button[type=submit]").contains("Submit").click() + + // Submitted objects list should have newly added item from Run form + cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 1) + + // Fill another Analysis form with the same fileType as Run form: "bam" + cy.get("button").contains("New form").click() + cy.get("input[name='title']").type(testAnalysisData.title2) + cy.get("select[name='analysisType']").select("Reference Alignment") + cy.get("select[name='analysisType.referenceAlignment.assembly']").select("Standard") + cy.get("input[name='analysisType.referenceAlignment.assembly.accessionId']").type("Standard Accession Id") + // Select fileType + cy.get("input[name='files.0.filename']").type(testAnalysisFile.fileName) + cy.get("select[name='files.0.filetype']").select(testAnalysisFile.fileType2) + cy.get("select[name='files.0.checksumMethod']").select(testAnalysisFile.checksumMethod) + cy.get("input[name='files.0.checksum']").type(testAnalysisFile.checksum) + + // Submit form + cy.get("button[type=submit]").contains("Submit").click() + + // Submitted objects list should have newly added item from Run form + cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 2) + + // Go to DOI form + cy.get("button[type=button]").contains("Next").click() + cy.get("button").contains("Add DOI information (optional)", { timeout: 10000 }).click() + cy.get("div[role='dialog']").should("be.visible") + + // Check file types from submitted Run form and Analysis form are Uniquely pre-filled in DOI form + cy.get("input[data-testid='formats.0']", { timeout: 10000 }).should("have.value", "bam") + cy.get("input[data-testid='formats.1']", { timeout: 10000 }).should("have.value", "cram") + + // Go to Creators section and Add new item + cy.get("h2[data-testid='creators']").parents().children("button").click() + cy.get("h3[data-testid='creators.0.affiliation']", { timeout: 10000 }).parent().children("button").click() + // Type search words in autocomplete field + cy.get("input[name='creators.0.affiliation.0.name']").type("csc") + // Select the first result + cy.get(".MuiAutocomplete-option") + .should("be.visible") + .then($el => $el.first().click()) + // Check the rest 3 fields are auto-filled and disabled + cy.get("input[data-testid='creators.0.affiliation.0.schemeUri']").should("have.value", "https://ror.org") + cy.get("input[data-testid='creators.0.affiliation.0.schemeUri']").should("be.disabled") + + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifier").should( + "have.value", + "https://ror.org/04m8m1253" + ) + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifier").should("be.disabled") + + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifierScheme']").should("have.value", "ROR") + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifierScheme']").should("be.disabled") + + // Go to Contributors and Add new item + cy.get("h2[data-testid='contributors']").parents().children("button").click() + cy.get("h3[data-testid='contributors.0.affiliation']", { timeout: 10000 }).parent().children("button").click() + // Type search words in autocomplete field + cy.get("input[name='contributors.0.affiliation.0.name']").type("demos") + // Select the first result + cy.get(".MuiAutocomplete-option") + .should("be.visible") + .then($el => $el.first().click()) + // Check the rest 3 fields are auto-filled and disabled + cy.get("input[data-testid='contributors.0.affiliation.0.schemeUri").should("have.value", "https://ror.org") + cy.get("input[data-testid='contributors.0.affiliation.0.schemeUri").should("be.disabled") + + cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifier").should( + "have.value", + "https://ror.org/032bmj362" + ) + cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifier").should("be.disabled") + + cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifierScheme']").should("have.value", "ROR") + cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifierScheme']").should("be.disabled") + }) +}) diff --git a/cypress/integration/objectLinksAttributes.spec.js b/cypress/integration/objectLinksAttributes.spec.js index 145a6d77..acc9c8eb 100644 --- a/cypress/integration/objectLinksAttributes.spec.js +++ b/cypress/integration/objectLinksAttributes.spec.js @@ -81,5 +81,12 @@ describe("render objects' links and attributes ", function () { cy.get("input[name='studyAttributes.0.tag']").should("have.value", "Test Attributes Tag") cy.get("textarea[name='studyAttributes.0.value']").should("have.value", "Test Attributes Value") + + // Remove URL Link and check that the rest of the Study Links render correctly + cy.get("div[data-testid='studyLinks[1]']").children("button").click() + cy.get("div[class='arrayRow']", { timeout: 10000 }).should("have.length", 2) + + cy.get("select[name='studyLinks.0']").should("have.value", "XRef Link") + cy.get("select[name='studyLinks.1']").should("have.value", "Entrez Link") }) }) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index 708a374c..b886de8d 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -1073,11 +1073,12 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { } const handleRemove = index => { + // Re-register hidden input if all field arrays are removed + if (index === 0) setValid(false) // Set the correct values according to the name path when removing a field const values = getValues(name) const filteredValues = values.filter((val, ind) => ind !== index) setValue(name, filteredValues) - if (index === 0) setValid(false) remove(index) } @@ -1103,9 +1104,9 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { const pathForThisIndex = [...pathWithoutLastItem, lastPathItemWithIndex] return ( -
+
- + handleRemove(index)}> From 75709ecbc94542001c6279bf6e513c01369e8efc Mon Sep 17 00:00:00 2001 From: HangLe Date: Tue, 2 Nov 2021 10:30:26 +0200 Subject: [PATCH 13/22] Save DoiForm info in redux --- cypress/integration/doiForm.spec.js | 1 + .../WizardForms/WizardDOIForm.js | 37 +++++++++++------- .../WizardSteps/WizardShowSummaryStep.js | 38 ++++++++----------- src/features/wizardSubmissionFolderSlice.js | 19 ++++++++++ 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/cypress/integration/doiForm.spec.js b/cypress/integration/doiForm.spec.js index da17751a..ba43b298 100644 --- a/cypress/integration/doiForm.spec.js +++ b/cypress/integration/doiForm.spec.js @@ -100,6 +100,7 @@ describe("DOI form", function () { cy.get("button").contains("Add DOI information (optional)", { timeout: 10000 }).click() cy.get("div[role='dialog']").should("be.visible") + cy.wait(500) // Check file types from submitted Run form and Analysis form are Uniquely pre-filled in DOI form cy.get("input[data-testid='formats.0']", { timeout: 10000 }).should("have.value", "bam") cy.get("input[data-testid='formats.1']", { timeout: 10000 }).should("have.value", "cram") diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index 28e314b1..b94a8385 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -12,8 +12,10 @@ import JSONSchemaParser from "./WizardJSONSchemaParser" import { ResponseStatus } from "constants/responseStatus" import { resetAutocompleteField } from "features/autocompleteSlice" +import { setOpenedDoiForm } from "features/openedDoiFormSlice" import { updateStatus } from "features/statusMessageSlice" -import folderAPIService from "services/folderAPI" +import { resetCurrentObject } from "features/wizardCurrentObjectSlice" +import { addDoiInfoToFolder } from "features/wizardSubmissionFolderSlice" import schemaAPIService from "services/schemaAPI" import { dereferenceSchema } from "utils/JSONSchemaUtils" @@ -50,27 +52,34 @@ const DOIForm = ({ formId }: { formId: string }): React$Element state.submissionFolder) const resolver = WizardAjvResolver(dataciteSchema) const methods = useForm({ mode: "onBlur", resolver }) - const currentFolder = useSelector(state => state.submissionFolder) const dispatch = useDispatch() const classes = useStyles() + // Set form default values + useEffect(() => { + methods.reset(currentFolder.doiInfo) + }, []) + const onSubmit = async data => { - try { - const changes = [{ op: "add", path: "/doiInfo", value: data }] - await folderAPIService.patchFolderById(currentFolder.folderId, changes) - } catch (err) { - dispatch( - updateStatus({ - status: ResponseStatus.error, - response: err, - errorPrefix: "Can't submit information for DOI.", - }) + dispatch(addDoiInfoToFolder(currentFolder.folderId, data)) + .then(() => { + dispatch(resetAutocompleteField()) + dispatch(setOpenedDoiForm(false)) + dispatch(resetCurrentObject()) + }) + .catch(err => + dispatch( + updateStatus({ + status: ResponseStatus.error, + response: err, + errorPrefix: "Can't submit information for DOI.", + }) + ) ) - } - dispatch(resetAutocompleteField()) } return ( diff --git a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js index f5b33863..29ea5392 100644 --- a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js +++ b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js @@ -22,6 +22,7 @@ import WizardDOIForm from "../WizardForms/WizardDOIForm" import { resetAutocompleteField } from "features/autocompleteSlice" import { setOpenedDoiForm } from "features/openedDoiFormSlice" +import { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice" import type { ObjectInsideFolderWithTags } from "types" import { getItemPrimaryText, formatDisplayObjectType } from "utils" @@ -94,31 +95,27 @@ const WizardShowSummaryStep = (): React$Element => { - -
) + const handleCancelDoiDialog = () => { + dispatch(setOpenedDoiForm(false)) + dispatch(resetAutocompleteField()) + dispatch(resetCurrentObject()) + } + + const handleOpenDoiDialog = () => { + dispatch(setOpenedDoiForm(true)) + dispatch(setCurrentObject(folder.doiInfo)) + } + return ( <> @@ -161,12 +158,7 @@ const WizardShowSummaryStep = (): React$Element => { ) })} - {openedDoiForm && } diff --git a/src/features/wizardSubmissionFolderSlice.js b/src/features/wizardSubmissionFolderSlice.js index f27e5ac4..28d43180 100644 --- a/src/features/wizardSubmissionFolderSlice.js +++ b/src/features/wizardSubmissionFolderSlice.js @@ -24,6 +24,9 @@ const wizardSubmissionFolderSlice: any = createSlice({ addDraftObject: (state, action) => { state.drafts.push(action.payload) }, + addDoiInfo: (state, action) => { + state.doiInfo = action.payload + }, deleteObject: (state, action) => { state.metadataObjects = _reject(state.metadataObjects, function (o) { return o.accessionId === action.payload @@ -48,6 +51,7 @@ export const { setFolder, addObject, addDraftObject, + addDoiInfo, deleteObject, deleteDraftObject, modifyObjectTags, @@ -234,3 +238,18 @@ export const deleteFolderAndContent = }) } } + +export const addDoiInfoToFolder = + (folderId: string, doiFormDetails: any): ((dispatch: (any) => void) => Promise) => + async (dispatch: any => void) => { + const changes = [{ op: "add", path: "/doiInfo", value: doiFormDetails }] + const response = await folderAPIService.patchFolderById(folderId, changes) + return new Promise((resolve, reject) => { + if (response.ok) { + dispatch(addDoiInfo(doiFormDetails)) + resolve(response) + } else { + reject(JSON.stringify(response)) + } + }) + } From 48e22aff3629d3728344bc62e825b5ce77690c93 Mon Sep 17 00:00:00 2001 From: HangLe Date: Wed, 3 Nov 2021 11:34:33 +0200 Subject: [PATCH 14/22] Add nameType as hidden field for DOI form before patching --- .../WizardForms/WizardJSONSchemaParser.js | 4 ++- src/features/wizardSubmissionFolderSlice.js | 29 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index b886de8d..daa9c2b1 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -1117,7 +1117,9 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { const properties = object.items.properties let requiredProperties = - index === 0 ? object.contains?.allOf?.flatMap(item => item.required) : object.items?.required + index === 0 && object.contains?.allOf + ? object.contains?.allOf?.flatMap(item => item.required) // Case: DAC - Main Contact needs at least 1 + : object.items?.required // Force first array item as required field if array is required but none of the items are required if (required && !requiredProperties) requiredProperties = [Object.keys(items)[0]] diff --git a/src/features/wizardSubmissionFolderSlice.js b/src/features/wizardSubmissionFolderSlice.js index 28d43180..95081031 100644 --- a/src/features/wizardSubmissionFolderSlice.js +++ b/src/features/wizardSubmissionFolderSlice.js @@ -1,7 +1,6 @@ //@flow import { createSlice } from "@reduxjs/toolkit" -import _extend from "lodash/extend" -import _reject from "lodash/reject" +import { extend, reject, merge } from "lodash" import draftAPIService from "../services/draftAPI" import objectAPIService from "../services/objectAPI" @@ -28,12 +27,12 @@ const wizardSubmissionFolderSlice: any = createSlice({ state.doiInfo = action.payload }, deleteObject: (state, action) => { - state.metadataObjects = _reject(state.metadataObjects, function (o) { + state.metadataObjects = reject(state.metadataObjects, function (o) { return o.accessionId === action.payload }) }, deleteDraftObject: (state, action) => { - state.drafts = _reject(state.drafts, function (o) { + state.drafts = reject(state.drafts, function (o) { return o.accessionId === action.payload }) }, @@ -98,7 +97,7 @@ export const updateNewDraftFolder = ? folderDetails.folder.drafts.concat(selectedDraftsArray) : folderDetails.folder.drafts - const updatedFolder = _extend( + const updatedFolder = extend( { ...folderDetails.folder }, { name: folderDetails.name, description: folderDetails.description, drafts: updatedDrafts } ) @@ -242,8 +241,26 @@ export const deleteFolderAndContent = export const addDoiInfoToFolder = (folderId: string, doiFormDetails: any): ((dispatch: (any) => void) => Promise) => async (dispatch: any => void) => { - const changes = [{ op: "add", path: "/doiInfo", value: doiFormDetails }] + const nameType = { nameType: "Personal" } + // Add "nameType": "Personal" to "creators" and "contributors" + const modifiedCreators = doiFormDetails.creators?.map(creator => ({ + ...creator, + ...nameType, + })) + + const modifiedContributors = doiFormDetails.contributors?.map(contributor => ({ + ...contributor, + ...nameType, + })) + + const modifiedDoiFormDetails = merge({}, doiFormDetails, { + creators: modifiedCreators, + contributors: modifiedContributors, + }) + + const changes = [{ op: "add", path: "/doiInfo", value: modifiedDoiFormDetails }] const response = await folderAPIService.patchFolderById(folderId, changes) + return new Promise((resolve, reject) => { if (response.ok) { dispatch(addDoiInfo(doiFormDetails)) From a92b96e1d4ea5ac0cfcd06457b9d3533152b6944 Mon Sep 17 00:00:00 2001 From: HangLe Date: Wed, 3 Nov 2021 13:26:51 +0200 Subject: [PATCH 15/22] Fix for changing affiliations' name affecting on other fields --- .../WizardForms/WizardJSONSchemaParser.js | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index daa9c2b1..c4d1bc92 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -583,15 +583,17 @@ const FormTextField = ({ // Case: DOI form - Affilation fields to be prefilled const prefilledFields = ["affiliationIdentifier", "schemeUri", "affiliationIdentifierScheme"] - let watchFieldName = "" + let watchAutocompleteFieldName = "" let watchFieldValue = null if (openedDoiForm) { - // useWatch to watch changes in autocomplete field - watchFieldName = + watchAutocompleteFieldName = name.includes("affiliation") && prefilledFields.includes(lastPathItem) ? path.slice(0, -1).join(".").concat(".", "name") : "" - watchFieldValue = watchFieldName ? useWatch({ name: watchFieldName }) : null + // useWatch to watch any changes in form's fields + const watchValues = useWatch() + // check changes of value of autocompleteField from watchValues + watchFieldValue = watchAutocompleteFieldName ? get(watchValues, watchAutocompleteFieldName) : null } // Conditions to disable input field: disable editing option if the field is prefilled @@ -607,19 +609,28 @@ const FormTextField = ({ // Check value of current name path const val = getValues(name) - useEffect(() => { - if (openedDoiForm && watchFieldValue && !val) { - lastPathItem === prefilledFields[0] ? setValue(name, autocompleteField) : null - lastPathItem === prefilledFields[1] ? setValue(name, "https://ror.org") : null - lastPathItem === prefilledFields[2] ? setValue(name, "ROR") : null - } - }, [autocompleteField]) + + if (openedDoiForm) { + // Set values for Affiliations' fields if autocompleteField exists + useEffect(() => { + if (watchFieldValue && !val) { + lastPathItem === prefilledFields[0] ? setValue(name, autocompleteField) : null + lastPathItem === prefilledFields[1] ? setValue(name, "https://ror.org") : null + lastPathItem === prefilledFields[2] ? setValue(name, "ROR") : null + } + }, [autocompleteField]) + + // Remove values for Affiliations' field if autocompleteField is deleted + useEffect(() => { + if (watchFieldValue === undefined && val && lastPathItem === prefilledFields[0]) setValue(name, "") + }, [watchFieldValue]) + } return ( { const inputValue = - (watchFieldName && typeof val !== "object" && val) || + (watchAutocompleteFieldName && typeof val !== "object" && val) || (typeof field.value !== "object" && field.value) || "" From be40ade8ab935b72813d7cb11d828f9a6fd5f57d Mon Sep 17 00:00:00 2001 From: HangLe Date: Wed, 3 Nov 2021 14:02:14 +0200 Subject: [PATCH 16/22] Fix for the inconsistency of rendering Affiliations' fields --- .../NewDraftWizard/WizardForms/WizardJSONSchemaParser.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index c4d1bc92..b5d249fd 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -618,7 +618,7 @@ const FormTextField = ({ lastPathItem === prefilledFields[1] ? setValue(name, "https://ror.org") : null lastPathItem === prefilledFields[2] ? setValue(name, "ROR") : null } - }, [autocompleteField]) + }, [autocompleteField, watchFieldValue]) // Remove values for Affiliations' field if autocompleteField is deleted useEffect(() => { @@ -1032,6 +1032,7 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { // Get currentObject and the values of current field const currentObject = useSelector(state => state.currentObject) || {} const fileTypes = useSelector(state => state.fileTypes) + const fieldValues = get(currentObject, name) const items = (traverseValues(object.items): any) From 15635ef300fc7da5df4e398c6a9da263231553a8 Mon Sep 17 00:00:00 2001 From: HangLe Date: Wed, 3 Nov 2021 14:47:20 +0200 Subject: [PATCH 17/22] Update e2e test for Affiliations' fields when deleting its name field --- cypress/integration/doiForm.spec.js | 45 ++++++++++++++++++- .../WizardForms/WizardJSONSchemaParser.js | 2 +- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/cypress/integration/doiForm.spec.js b/cypress/integration/doiForm.spec.js index ba43b298..68df6187 100644 --- a/cypress/integration/doiForm.spec.js +++ b/cypress/integration/doiForm.spec.js @@ -1,5 +1,5 @@ describe("DOI form", function () { - it("should render DOI form correctly with affiliation's autocomplete field and formats' prefilled values ", () => { + it("should render DOI form correctly with formats' prefilled values and affiliation's autocomplete field", () => { cy.login() cy.get("button", { timeout: 10000 }).contains("Create Submission").click() @@ -127,6 +127,30 @@ describe("DOI form", function () { cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifierScheme']").should("have.value", "ROR") cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifierScheme']").should("be.disabled") + // Remove Creators > Affiliations field and add a new field again + cy.get("div[data-testid='creators.0.affiliation']").children("button").click() + cy.get("h3[data-testid='creators.0.affiliation']").parent().children("button").click() + + // Repeat search words in autocomplete field + cy.get("input[name='creators.0.affiliation.0.name']").type("csc") + // Select the first result + cy.get(".MuiAutocomplete-option") + .should("be.visible") + .then($el => $el.first().click()) + + // Check the rest 3 fields are auto-filled and disabled as they should be + cy.get("input[data-testid='creators.0.affiliation.0.schemeUri']").should("have.value", "https://ror.org") + cy.get("input[data-testid='creators.0.affiliation.0.schemeUri']").should("be.disabled") + + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifier").should( + "have.value", + "https://ror.org/04m8m1253" + ) + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifier").should("be.disabled") + + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifierScheme']").should("have.value", "ROR") + cy.get("input[data-testid='creators.0.affiliation.0.affiliationIdentifierScheme']").should("be.disabled") + // Go to Contributors and Add new item cy.get("h2[data-testid='contributors']").parents().children("button").click() cy.get("h3[data-testid='contributors.0.affiliation']", { timeout: 10000 }).parent().children("button").click() @@ -148,5 +172,24 @@ describe("DOI form", function () { cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifierScheme']").should("have.value", "ROR") cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifierScheme']").should("be.disabled") + + // Delete the autocompleteField value and check the field also removed + cy.get("input[name='contributors.0.affiliation.0.name']") + .parent() + .children("div[class='MuiAutocomplete-endAdornment']") + .click() + cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifier']").should("have.value", "") + + // Type new search words + cy.get("input[name='contributors.0.affiliation.0.name']").type("test") + // Select the first result and check the field is filled and disabled + cy.get(".MuiAutocomplete-option") + .should("be.visible") + .then($el => $el.first().click()) + cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifier").should( + "have.value", + "https://ror.org/03fknzz27" + ) + cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifier").should("be.disabled") }) }) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index b5d249fd..9a1db8b4 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -1137,7 +1137,7 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { if (required && !requiredProperties) requiredProperties = [Object.keys(items)[0]] return ( - + { items From afd0ec037ef55228e65c7a071aab4ae4f24f7895 Mon Sep 17 00:00:00 2001 From: HangLe Date: Wed, 3 Nov 2021 15:13:32 +0200 Subject: [PATCH 18/22] Update e2e test for saving DOI form sucessfully --- cypress/integration/doiForm.spec.js | 30 ++++++++++++++++--- .../WizardForms/WizardDOIForm.js | 6 ++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/cypress/integration/doiForm.spec.js b/cypress/integration/doiForm.spec.js index 68df6187..702777a5 100644 --- a/cypress/integration/doiForm.spec.js +++ b/cypress/integration/doiForm.spec.js @@ -1,5 +1,5 @@ describe("DOI form", function () { - it("should render DOI form correctly with formats' prefilled values and affiliation's autocomplete field", () => { + beforeEach(() => { cy.login() cy.get("button", { timeout: 10000 }).contains("Create Submission").click() @@ -10,7 +10,8 @@ describe("DOI form", function () { cy.get("button[type=button]").contains("Next").click() cy.wait(500) - + }) + it("should render DOI form correctly with formats' prefilled values and affiliation's autocomplete field", () => { // Fill in Run form cy.clickFillForm("Run") @@ -100,7 +101,6 @@ describe("DOI form", function () { cy.get("button").contains("Add DOI information (optional)", { timeout: 10000 }).click() cy.get("div[role='dialog']").should("be.visible") - cy.wait(500) // Check file types from submitted Run form and Analysis form are Uniquely pre-filled in DOI form cy.get("input[data-testid='formats.0']", { timeout: 10000 }).should("have.value", "bam") cy.get("input[data-testid='formats.1']", { timeout: 10000 }).should("have.value", "cram") @@ -191,5 +191,27 @@ describe("DOI form", function () { "https://ror.org/03fknzz27" ) cy.get("input[data-testid='contributors.0.affiliation.0.affiliationIdentifier").should("be.disabled") - }) + }), + it("should fill the required fields and save DOI form successfully", () => { + // Go to DOI form + cy.get("button[type=button]").contains("Next").click() + cy.get("button").contains("Add DOI information (optional)", { timeout: 10000 }).click() + cy.get("div[role='dialog']").should("be.visible") + + // Fill in required Creators field + cy.get("h2[data-testid='creators']").parent().children("button").click() + cy.get("input[data-testid='creators.0.givenName']").type("John Smith") + + // Fill in required Subjects field + cy.get("h2[data-testid='subjects']").parent().children("button").click() + cy.get("select[name='subjects.0.subject']").select("FOS: Mathematics") + + cy.get("button[type='submit']").click() + cy.contains(".MuiAlert-message", "DOI form has been saved successfully") + + // Open the DOI form again and check the fields render correctly + cy.get("button").contains("Add DOI information (optional)", { timeout: 10000 }).click() + cy.get("input[data-testid='creators.0.givenName']").should("have.value", "John Smith") + cy.get("select[name='subjects.0.subject']").should("have.value", "FOS: Mathematics") + }) }) diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index b94a8385..49520049 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -70,6 +70,12 @@ const DOIForm = ({ formId }: { formId: string }): React$Element dispatch( From b645cb747b23c153cef1c3f7f1db8c72aa18675d Mon Sep 17 00:00:00 2001 From: HangLe Date: Wed, 3 Nov 2021 15:48:29 +0200 Subject: [PATCH 19/22] Fix for clear button position --- .../NewDraftWizard/WizardForms/WizardJSONSchemaParser.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index 9a1db8b4..39c84b08 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -63,6 +63,11 @@ const useStyles = makeStyles(theme => ({ marginTop: theme.spacing(1), }, }, + autocompleteInput: { + "& .MuiAutocomplete-endAdornment": { + top: 0, + }, + }, externalLink: { fontSize: "1rem", marginBottom: theme.spacing(-0.5), @@ -782,6 +787,7 @@ const FormAutocompleteField = ({ renderInput={params => ( Date: Wed, 3 Nov 2021 15:54:17 +0200 Subject: [PATCH 20/22] Add description for FormArray --- .../NewDraftWizard/WizardForms/WizardJSONSchemaParser.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js index 39c84b08..729b5fa2 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js +++ b/src/components/NewDraftWizard/WizardForms/WizardJSONSchemaParser.js @@ -1029,7 +1029,8 @@ type FormArrayProps = { /* * FormArray is rendered for arrays of objects. User is given option to choose how many objects to add to array. */ -const FormArray = ({ object, path, required }: FormArrayProps) => { +const FormArray = ({ object, path, required, description }: FormArrayProps & { description: string }) => { + const classes = useStyles() const name = pathToName(path) const [lastPathItem] = path.slice(-1) const label = object.title ?? lastPathItem @@ -1112,6 +1113,11 @@ const FormArray = ({ object, path, required }: FormArrayProps) => { )} + {description && ( + + + + )} {fields.map((field, index) => { From a8c815e157d0b401f86d2098d3708649157a65d3 Mon Sep 17 00:00:00 2001 From: HangLe Date: Thu, 4 Nov 2021 15:00:43 +0200 Subject: [PATCH 21/22] Change errorPrefix to helperText for error messages --- src/components/Home/UserDraftTemplateActions.js | 2 +- src/components/NewDraftWizard/WizardForms/WizardDOIForm.js | 4 ++-- .../NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Home/UserDraftTemplateActions.js b/src/components/Home/UserDraftTemplateActions.js index 5f233ed7..e16dae05 100644 --- a/src/components/Home/UserDraftTemplateActions.js +++ b/src/components/Home/UserDraftTemplateActions.js @@ -38,7 +38,7 @@ const UserDraftTemplateActions = (props: { item: { schema: string, accessionId: updateStatus({ status: ResponseStatus.error, response: response, - errorPrefix: "Unable to delete template", + helperText: "Unable to delete template", }) ) } diff --git a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js index 49520049..09220419 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardDOIForm.js @@ -39,7 +39,7 @@ const DOIForm = ({ formId }: { formId: string }): React$Element Date: Thu, 4 Nov 2021 21:19:56 +0200 Subject: [PATCH 22/22] subject scheme should be fixed --- src/features/wizardSubmissionFolderSlice.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/features/wizardSubmissionFolderSlice.js b/src/features/wizardSubmissionFolderSlice.js index 95081031..c9e03bcf 100644 --- a/src/features/wizardSubmissionFolderSlice.js +++ b/src/features/wizardSubmissionFolderSlice.js @@ -253,9 +253,18 @@ export const addDoiInfoToFolder = ...nameType, })) + const subjectSchema = { subjectScheme: "Fields of Science and Technology (FOS)"} + // Add fixed subject schema as we are using FOS by default + + const modifiedSubjects = doiFormDetails.subjects?.map(subject => ({ + ...subject, + ...subjectSchema, + })) + const modifiedDoiFormDetails = merge({}, doiFormDetails, { creators: modifiedCreators, contributors: modifiedContributors, + subjects: modifiedSubjects, }) const changes = [{ op: "add", path: "/doiInfo", value: modifiedDoiFormDetails }]