diff --git a/src/.gitignore b/src/.gitignore index ee401f18..a10818a8 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -31,7 +31,7 @@ src/ingest-ui/.pnp.js # testing src/react-app/coverage thunder-tests/ -cypress/screenshots +cypress/* # env files src/.env.* diff --git a/src/src/App.js b/src/src/App.js index c66eb054..548c24c1 100644 --- a/src/src/App.js +++ b/src/src/App.js @@ -28,7 +28,7 @@ import Forms from "./components/uuid/forms"; import {BuildError} from "./utils/error_helper"; import {Navigation} from "./Nav"; import Result from "./components/ui/result"; -import SpeedDialTooltipOpen from './components/ui/formParts'; +import {SpeedDialTooltipOpen} from './components/ui/formParts'; import {sortGroupsByDisplay,adminStatusValidation} from "./service/user_service"; import {api_validate_token} from './service/search_api'; import {ubkg_api_get_dataset_type_set,ubkg_api_get_organ_type_set} from "./service/ubkg_api"; @@ -50,6 +50,9 @@ import {DonorForm} from "./components/newDonor"; import {UploadForm} from "./components/newUpload"; import {SampleForm} from "./components/newSample"; import {PublicationForm} from "./components/newPublication"; +import {DatasetForm} from "./components/newDataset"; + +import NotFound from "./components/404"; export function App(props){ let navigate = useNavigate(); @@ -67,8 +70,8 @@ export function App(props){ var[timerStatus, setTimerStatus] = useState(true); // Data to fill in UI Elements - var[dataTypeList, setDataTypeList] = useState({}); //@TODO: Remove & use Local in forms - var[dataTypeListAll, setDataTypeListAll] = useState({}); //@TODO: Remove & use Local in forms + // var[dataTypeList, setDataTypeList] = useState({}); //@TODO: Remove & use Local in forms + // var[dataTypeListAll, setDataTypeListAll] = useState({}); //@TODO: Remove & use Local in forms var[organList, setOrganList] = useState(); //@TODO: Remove & use Local in Search // var [userDataGroups, setUserDataGroups] = useState({}); //@TODO: Remove & use Local in forms @@ -131,8 +134,8 @@ export function App(props){ if(res !== undefined){ localStorage.setItem("datasetTypes",JSON.stringify(res)); // TODO: Eventually remove these & use localstorage - setDataTypeList(res); - setDataTypeListAll(res); + // setDataTypeList(res); + // setDataTypeListAll(res); }else{ setAPIErr(["UBKG API : Dataset Types",'No local DATASET TYPE definitions were found and none could be fetched Please try again later, or contact help@hubmapconsortium.org',res]) reportError(res) @@ -527,10 +530,10 @@ export function App(props){ creationSuccess(response)}/>}/> creationSuccess(response)} /> }/> creationSuccess(response)}/>} /> - creationSuccess(response)} onReturn={() => onClose()} handleCancel={() => handleCancel()} /> }/> - creationSuccess(response)} onReturn={() => onClose()} handleCancel={() => handleCancel()} /> }/> + creationSuccess(response)} onReturn={() => onClose()} handleCancel={() => handleCancel()} /> }/> + creationSuccess(response)} onReturn={() => onClose()} handleCancel={() => handleCancel()} /> }/> urlChange(event, params, details)} routingMessage={routingMessage.Datasets} />} > - }/> + creationSuccess(response)}/>}/> creationSuccess(response)}/>}/> {/* In Develpment here */} @@ -543,14 +546,16 @@ export function App(props){ updateSuccess(response)}/>} /> updateSuccess(response)}/>} /> - } /> - {/* } /> */} + updateSuccess(response)}/>} /> + updateSuccess(response)}/>} /> + updateSuccess(response)} />} /> - updateSuccess(response)} reportError={reportError} handleCancel={handleCancel} status="view" />} /> - updateSuccess(response)} reportError={reportError} handleCancel={handleCancel} status="view" />} /> + updateSuccess(response)} reportError={reportError} handleCancel={handleCancel} status="view" />} /> + updateSuccess(response)} reportError={reportError} handleCancel={handleCancel} status="view" />} /> } /> } /> + } /> }/> @@ -558,8 +563,8 @@ export function App(props){ }/> - {/* In Develpment here */} - updateSuccess(response)}/>} /> + {/* 404 */} + } /> diff --git a/src/src/components/404.jsx b/src/src/components/404.jsx new file mode 100644 index 00000000..f549c791 --- /dev/null +++ b/src/src/components/404.jsx @@ -0,0 +1,81 @@ +import React from "react"; +import { Box, Typography, Button } from "@mui/material"; +import WarningIcon from '@mui/icons-material/Warning'; +import { useNavigate } from "react-router-dom"; +import HealingIcon from '@mui/icons-material/Healing'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; +import HomeIcon from '@mui/icons-material/Home'; + +export default function NotFound() { + const navigate = useNavigate(); + const params = new URLSearchParams(window.location.search); + const entityID = params.get('entityID'); + return ( + + + + 404 + + + Entity Not Found + + + + + Sorry, the Entity you are looking for {entityID ? (({entityID})) : ""} was not found in neo4j. Please check the ID value again, return to the homepage, or try searching for it. + + + + {/* {entityID && ( + + )} */} + + + + + ); +} diff --git a/src/src/components/DatasetFormFields.jsx b/src/src/components/DatasetFormFields.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/src/components/collections.jsx b/src/src/components/collections.jsx index 3b8e2924..01d3eef2 100644 --- a/src/src/components/collections.jsx +++ b/src/src/components/collections.jsx @@ -107,7 +107,7 @@ export const RenderCollection = (props) => { editingCollection={entity_data} // writeable={true} dataGroups={dataGroups} - dtl_all={props.dtl_all} + dtl_all={localStorage.getItem("datasetTypes") ? JSON.parse(localStorage.getItem("datasetTypes")) : []} newForm={ isNew ? true : null} /> diff --git a/src/src/components/collections/collections.jsx b/src/src/components/collections/collections.jsx index 44e5ff24..d4729e82 100644 --- a/src/src/components/collections/collections.jsx +++ b/src/src/components/collections/collections.jsx @@ -104,7 +104,7 @@ export function CollectionForm (props){ // Props var [isNew] = useState(props.newForm); var [dataGroups] = useState(props.dataGroups); - var [datatypeList] = useState(props.dtl_all); + // var [datatypeList] = useState(props.dtl_all); var [editingCollection] = useState(props.editingCollection); diff --git a/src/src/components/epicollections.jsx b/src/src/components/epicollections.jsx index b894ea5a..7e43f227 100644 --- a/src/src/components/epicollections.jsx +++ b/src/src/components/epicollections.jsx @@ -107,7 +107,7 @@ export const RenderEPICollection = (props) => { editingCollection={entity_data} // writeable={true} dataGroups={dataGroups} - dtl_all={props.dtl_all} + dtl_all={localStorage.getItem("datasetTypes") ? JSON.parse(localStorage.getItem("datasetTypes")) : []} newForm={ isNew ? true : null} /> diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx new file mode 100644 index 00000000..4428975b --- /dev/null +++ b/src/src/components/newDataset.jsx @@ -0,0 +1,614 @@ + +import React, { useEffect, useState, useMemo, useCallback } from "react"; +import LoadingButton from "@mui/lab/LoadingButton"; +import { Typography } from "@mui/material"; +import Alert from "@mui/material/Alert"; +import AlertTitle from '@mui/material/AlertTitle'; +import Box from "@mui/material/Box"; +import Grid from '@mui/material/Grid'; +import LinearProgress from "@mui/material/LinearProgress"; +import Snackbar from '@mui/material/Snackbar'; +import { useNavigate, useParams } from "react-router-dom"; +import { BulkSelector } from "./ui/bulkSelector"; +import { FormHeader, TaskAssignment } from "./ui/formParts"; +import { DatasetFormFields } from "./ui/fields/DatasetFormFields"; +import {RevertFeature} from "../utils/revertModal"; +import { humanize } from "../utils/string_helper"; +import { validateRequired } from "../utils/validators"; +import { entity_api_get_entity, entity_api_update_entity } from "../service/entity_api"; +import { + ingest_api_allowable_edit_states, + ingest_api_create_dataset, + ingest_api_validate_entity, + ingest_api_pipeline_test_submit, + ingest_api_dataset_submit, + ingest_api_notify_slack} from "../service/ingest_api"; +import { prefillFormValuesFromUrl, EntityValidationMessage } from "./ui/formParts"; +export const DatasetForm = (props) => { + let navigate = useNavigate(); + + let [entityData, setEntityData] = useState(); + let [loading, setLoading] = useState({ + page: true, + processing: false, + bulk: false, + button: { process: false, save: false, submit: false, validate: false } + }); + let [form, setForm] = useState({ + lab_dataset_id: "", + description: "", + dataset_info: "", + contains_human_genetic_sequences: "", + dt_select: "", + direct_ancestor_uuids: [], + group_uuid: "", + ingest_task: "", + assigned_to_group_name: "" + }); + let [formErrors, setFormErrors] = useState({}); + let [errorMessages, setErrorMessages] = useState([]); + let [pageErrors, setPageErrors] = useState(null); + let [readOnlySources, setReadOnlySources] = useState(false); + let [entityValidation, setEntityValidation] = useState({ + open: false, + message: null + }); + let [permissions, setPermissions] = useState({ + has_admin_priv: false, + has_publish_priv: false, + has_submit_priv: false, + has_write_priv: false + }); + let [bulkSelection, setBulkSelection] = useState({ uuids: [], data: [] }); + let [snackbarController, setSnackbarController] = useState({ + open: false, + message: "", + status: "info" + }); + const allGroups = localStorage.getItem("allGroups") ? JSON.parse(localStorage.getItem("allGroups")) : []; + const { uuid } = useParams(); + const formFields = useMemo(() => [{ + id: "lab_dataset_id", + label: "Lab Name or ID", + helperText: "An identifier used locally by the data provider.", + required: true, + type: "text" + }, + { + id: "description", + label: "Description", + helperText: "", + required: true, + type: "textarea" + }, + { + id: "dataset_info", + label: "Additional Information", + helperText: "Add information here which can be used to find this data, including lab specific (non-PHI) identifiers.", + required: false, + type: "textarea" + }, + { + id: "contains_human_genetic_sequences", + label: "Gene Sequences", + helperText: "", + required: true, + type: "radio" + }, + { + id: "dt_select", + label: "Dataset Type", + helperText: "", + required: true, + type: "select", + writeEnabled: (uuid?.length<=0 || uuid === undefined || uuid === null) ? true : false, + values: localStorage.getItem("datasetTypes") ? JSON.parse(localStorage.getItem("datasetTypes")).map(dt => ({ value: dt.dataset_type, label: dt.dataset_type })) : [] + }, + { + id: "group_uuid", + label: "Group", + helperText: (uuid?.length<=0 || uuid === undefined || uuid === null) ? "" : `Select the group for this dataset.`, + required: true, + type: "select", + writeEnabled: (uuid?.length<=0 || uuid === undefined || uuid === null) ? true : false, + values: localStorage.getItem("userGroups") ? JSON.parse(localStorage.getItem("userGroups")).map(g => ({ value: g.uuid, label: g.displayname })) : [] + } + ], []); + + const memoizedFormHeader = useMemo( + () => , [uuid, entityData, permissions] + ); + + useEffect(() => { + if (uuid && uuid !== "") { + entity_api_get_entity(uuid) + .then((response) => { + console.debug('%c◉ RESP ', 'color:#00ff7b', response); + if(response.status === 404 || response.status === 400){ + console.debug('%c◉ ERRRRR ', 'color:#FFFFFF;background: #2200FF;padding:200' , ); + navigate("/notFound?entityID="+uuid); + } + if (response.status === 200) { + const entityType = response.results.entity_type; + if (entityType !== "Dataset") { + window.location.replace( + `${process.env.REACT_APP_URL}/${entityType}/${uuid}` + ); + } else { + const entityData = response.results; + setEntityData(entityData); + setForm({ + lab_dataset_id: entityData.lab_dataset_id, + description: entityData.description, + dataset_info: entityData.dataset_info, + contains_human_genetic_sequences: entityData.contains_human_genetic_sequences, + dt_select: entityData.dataset_type, + group_uuid: entityData.group_uuid, + direct_ancestor_uuids: entityData.direct_ancestors.map(obj => obj.uuid), + ingest_task: entityData.ingest_task || "", + assigned_to_group_name: entityData.assigned_to_group_name || "" + }); + setBulkSelection({ + uuids: entityData.direct_ancestors.map(obj => obj.uuid), + data: entityData.direct_ancestors + }); + // Set the Bulk Table to read only if the Dataset is not in a modifiable state + if (entityData.creation_action === "Multi-Assay Split" || entityData.creation_action === "Central Process"){ + setReadOnlySources(true); + } + + ingest_api_allowable_edit_states(entityData.uuid) + .then((response) => { + if (entityData.data_access_level === "public") { + setReadOnlySources(true); + setPermissions({ has_write_priv: false }); + } + if(response.results.has_write_priv === false){ + setReadOnlySources(true); + } + setPermissions(response.results); + }) + .catch((error) => { + console.error(error); + + setPageErrors(error); + }); + } + } else { + setPageErrors(response); + } + }) + .catch((error) => { + console.debug('%c◉ ingest_api_allowable_edit_states ERR Catch ', 'color:#FFFFFF;background: #2200FF;padding:200' ,error ); + if(error.status === 404){ + navigate("/notFound?entityID="+uuid); + } + setPageErrors(error); + }); + } else { + // Pre-fill form values from URL parameters + prefillFormValuesFromUrl(setForm, setSnackbarController); + setPermissions({ has_write_priv: true }); + } + setLoading(prevVals => ({ ...prevVals, page: false })); + }, [uuid]); + + const handleInputChange = useCallback((e) => { + const { name, value } = e.target; + setForm(prev => { + if (prev[name] === value) return prev; + return { ...prev, [name]: value }; + }); + console.debug('%c◉ handleInputChange', 'color:#00ff7b', name, value); + }, []); + + // Callback for BulkSelector + const handleBulkSelectionChange = (uuids, hids, string, data) => { + setForm(prev => ({ + ...prev, + direct_ancestor_uuids: uuids + })); + setBulkSelection({ uuids, data }); + }; + + const validateForm = () => { + setErrorMessages([]); + let errors = 0; + let e_messages = []; + let requiredFields = ["lab_dataset_id", "description", "contains_human_genetic_sequences", "dt_select", "group_uuid"]; + let newFormErrors = {}; + for (let field of requiredFields) { + if (!validateRequired(form[field])) { + let fieldName = formFields.find(f => f.id === field)?.label || humanize(field); + e_messages.push(fieldName + " is a required field"); + newFormErrors[field] = " Required"; + errors++; + } else { + newFormErrors[field] = ""; + } + } + if (!bulkSelection.data || bulkSelection.data.length <= 0) { + e_messages.push("Please select at least one Source"); + errors++; + newFormErrors["direct_ancestor_uuids"] = "Required"; + } else if (bulkSelection.data.length > 0 && form['direct_ancestor_uuids'].length <= 0) { + setForm(prev => ({ + ...prev, + 'direct_ancestor_uuids': bulkSelection.data.map(obj => obj.uuid), + })); + } + setFormErrors(newFormErrors); + setErrorMessages(errors > 0 ? e_messages : []); + return errors === 0; + }; + + function buildCleanForm() { + let selectedUUIDs = bulkSelection.data.map(obj => obj.uuid); + let cleanForm = { + lab_dataset_id: form.lab_dataset_id, + contains_human_genetic_sequences: form.contains_human_genetic_sequences === "yes", + description: form.description, + dataset_info: form.dataset_info, + direct_ancestor_uuids: selectedUUIDs, + ...(((form.assigned_to_group_name && form.assigned_to_group_name !== entityData?.assigned_to_group_name) && permissions.has_admin_priv) && { assigned_to_group_name: form.assigned_to_group_name }), + ...(((form.ingest_task && form.ingest_task !== entityData?.ingest_task) && permissions.has_admin_priv) && { ingest_task: form.ingest_task }) + }; + if (!uuid) { + cleanForm.group_uuid = form.group_uuid || (localStorage.getItem("userGroups") ? JSON.parse(localStorage.getItem("userGroups"))[0].uuid : ""); + cleanForm.dataset_type = form.dt_select; + } + return cleanForm; + } + + const handleSave = (e) => { + e.preventDefault(); + if (validateForm()) { + setLoading(prevVals => ({ ...prevVals, processing: true })); + let selectedUUIDs = bulkSelection.data.map((obj) => obj.uuid); + let cleanForm = { + lab_dataset_id: form.lab_dataset_id, + contains_human_genetic_sequences: form.contains_human_genetic_sequences === "yes" ? true : false, + description: form.description, + dataset_info: form.dataset_info, + direct_ancestor_uuids: selectedUUIDs, + ...(((form.assigned_to_group_name && form.assigned_to_group_name !== entityData.assigned_to_group_name) && permissions.has_admin_priv) && {assigned_to_group_name: form.assigned_to_group_name}), + ...(((form.ingest_task && form.ingest_task !== entityData.ingest_task) && permissions.has_admin_priv) && {ingest_task: form.ingest_task}) + }; + console.debug('%c⭗ Data', 'color:#00ff7b', cleanForm); + if (uuid) { + let target = e.target.name; + setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, [target]: true } })); + console.log("handleSave", target); + entity_api_update_entity(uuid, JSON.stringify(cleanForm)) + .then((response) => { + if (response.status < 300) { + props.onUpdated(response.results); + } else { + setPageErrors(response); + setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, [target]: false } })); + } + }) + .catch((error) => { + setPageErrors(error); + setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, [target]: false } })); + }); + } else { + // If group_uuid is not set, default to first user group + let group_uuid = form.group_uuid || (localStorage.getItem("userGroups") ? JSON.parse(localStorage.getItem("userGroups"))[0].uuid : ""); + cleanForm.group_uuid = group_uuid; + cleanForm.dataset_type = form.dt_select + cleanForm.group_uuid = form.group_uuid + // console.log(form, form.contains_human_genetic_sequences); + console.debug('%c◉ cleanForm ', 'color:#00ff7b', cleanForm); + ingest_api_create_dataset(JSON.stringify(cleanForm)) + .then((response) => { + if (response.status === 200) { + props.onCreated(response.results); + } else { + setPageErrors(response.error ? response.error : response); + } + }) + .catch((error) => { + setPageErrors(error); + setLoading(prevVals => ({ ...prevVals, button: { process: false, save: false, submit: false }, processing: false })); + }); + } + } else { + setLoading(prevVals => ({ ...prevVals, button: { process: false, save: false, submit: false }, processing: false })); + } + }; + + const handleValidateEntity = (e) => { + e.preventDefault(); + ingest_api_validate_entity(entityData.uuid, "datasets") + .then((response) => { + console.debug('%c◉ res ', 'color:#00ff7b', response); + setEntityValidation({ + open: true, + message: response + }); + }) + .catch((error) => { + console.debug('%c◉ error ', 'color:#ff007b', error); + setEntityValidation({ + open: true, + message: error + }); + }); + }; + + const handleSubmitForTesting = () => { + console.debug('%c◉ Submitting for Testing ', 'color:#00ff7b', ); + // NOTE: CannotBe Derived! @TODO? + ingest_api_pipeline_test_submit({"uuid": entityData.uuid}) + .then((response) => { + console.debug('%c◉ SUBMITTED', 'color:#00ff7b', response); + let results = ""; + let title = ""; + if(response.status === 200){ + title = "Submitted for Testing"; + results = "Dataset submitted for test processing"; + }else if(response.status === 500){ + title = "Error"; + results = "Unexpected error occurred, please ask an admin to check the ingest-api logs."; + }else{ + title = "Submission Response: "+response.status; + results = response.results; + } + setSnackbarController({ + open: true, + message: title+" - "+results, + status: "success" + }); + }) + .catch((error) => { + setSnackbarController({ + open: true, + message: "SubmitForTesting Error - "+error.toString(), + status: "error" + }); + }) + } + + const handleSubmit = (e) => { + e.preventDefault(); + var dataSubmit = {"status":"Submitted"} + entity_api_update_entity(entityData.uuid, JSON.stringify(dataSubmit)) + .then((response) => { + console.debug("entity_api_update_entity response", response); + // @TODO: Move slackness call into entity_api_update_entity + var ingestURL= process.env.REACT_APP_URL+"/dataset/"+this.props.editingDataset.uuid + var slackMessage = {"message":"Dataset has been submitted ("+ingestURL+")"} + ingest_api_notify_slack(slackMessage) + .then((slackRes) => { + console.debug("slackRes", slackRes); + if (response.status < 300) { + props.onUpdated(response.results); + } else { + setSnackbarController({ + open: true, + message: "Slack Notification Error - "+response.toString(), + status: "error" + }); + } + }) + .catch((error) => { + setSnackbarController({ + open: true, + message: "Submit Error - "+error.toString(), + status: "error" + }); + }); + }) + .catch((error) => { + setSnackbarController({ + open: true, + message: "Submit Error - "+error.toString(), + status: "error" + }); + }); + } + + const handleProcess = (e) => { + e.preventDefault(); + setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, process: true } })); + let data = buildCleanForm(); + ingest_api_dataset_submit(entityData.uuid, JSON.stringify(data)) + .then((response) => { + if (response.status < 300) { + props.onUpdated(response.results); + } else { + // @TODO: Update on the API's end to hand us a Real error back, not an error wrapped in a 200 + var statusText = ""; + console.debug("err", response, response.error); + if(response.err){ + statusText = response.err.response.status+" "+response.err.response.statusText; + }else if(response.error){ + statusText = response.error.response.status+" "+response.error.response.statusText; + } + var submitErrorResponse="Uncaptured Error"; + if(response.err && response.err.response.data ){ + submitErrorResponse = response.err.response.data + } + if(response.error && response.error.response.data ){ + submitErrorResponse = response.error.response.data + } + setSnackbarController({ + open: true, + message: "Process Error - "+statusText+" "+submitErrorResponse, + status: "error" + }); + console.debug("entity_api_get_entity RESP NOT 200", response.status, response); + } + }) + .catch((error) => { + setSnackbarController({ + open: true, + message: "Process Error - "+error.toString(), + status: "error" + }); + }); + + } + + const buttonEngine = () => { + return (<> + + navigate("/")}> + Cancel + + {/* NEW, INVALID, REOPENED, ERROR, SUBMITTED */} + {!uuid && ( + handleSave(e)} + type="submit"> + Save + + )} + {uuid && uuid.length > 0 && permissions.has_admin_priv &&( + + )} + {/* NEW, SUBMITTED */} + {uuid && uuid.length > 0 && permissions.has_admin_priv && ["new", "submitted"].includes(entityData.status.toLowerCase()) && ( + handleProcess(e)} + variant="contained" + className="m-2"> + Process + + )} + {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status === "new" && ( + handleSubmitForTesting(e)} + name="submit" + variant="contained" + className="m-2"> + Submit for Testing + + )} + {uuid && uuid.length > 0 && permissions.has_admin_priv && (!["published", "processing"].includes(entityData.status.toLowerCase())) && ( + handleValidateEntity(e)} + name="validate" + variant="contained" + className="m-2"> + Validate + + )} + {uuid && uuid.length > 0 && ((permissions.has_write_priv && (!["published", "QA"].includes(entityData.status))) || (permissions.has_admin_priv && entityData.status === "QA")) && ( + handleSave(e)} + variant="contained" + className="m-2"> + Save + + )} + + ); + }; + + if (loading.page || ((!entityData || !form) && uuid)) { + return (); + } else { + return ( + + + {memoizedFormHeader} + +
handleSubmit(e)}> + + + {/* TASK ASSIGNMENT */} + {uuid && ( + + )} + {errorMessages && errorMessages.length > 0 && ( + + Please Review the following problems: + {errorMessages.map(error => ( + + {error} + + ))} + + )} + {buttonEngine()} + + {pageErrors && ( + + Error: {JSON.stringify(pageErrors)} + + )} + {/* Snackbar Feedback*/} + setSnackbarController(prev => ({...prev, open: false}))}> + setSnackbarController(prev => ({...prev, open: false}))} + severity={snackbarController.status} + variant="filled" + sx={{ + width: '100%', + backgroundColor: snackbarController.status === "error" ? "#f44336" : "#4caf50", + }}> + {snackbarController.message} + + + {entityValidation?.message && ( + setEntityValidation(prev => ({ ...prev, open }))} + /> + )} +
+ ); + } +}; \ No newline at end of file diff --git a/src/src/components/newPublication.jsx b/src/src/components/newPublication.jsx index 9d3a882e..173d26da 100644 --- a/src/src/components/newPublication.jsx +++ b/src/src/components/newPublication.jsx @@ -1,156 +1,138 @@ +import React, { useEffect, useState } from "react"; import LoadingButton from "@mui/lab/LoadingButton"; -import {Typography} from "@mui/material"; +import { Typography } from "@mui/material"; import Alert from "@mui/material/Alert"; import AlertTitle from '@mui/material/AlertTitle'; import Box from "@mui/material/Box"; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import FormLabel from '@mui/material/FormLabel'; import Grid from '@mui/material/Grid'; import InputLabel from "@mui/material/InputLabel"; import LinearProgress from "@mui/material/LinearProgress"; import NativeSelect from '@mui/material/NativeSelect'; -import Radio from '@mui/material/Radio'; -import RadioGroup from '@mui/material/RadioGroup'; -import TextField from "@mui/material/TextField"; -import React,{useEffect,useState} from "react"; - -import {useNavigate,useParams} from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { - entity_api_get_entity, - entity_api_get_globus_url, - entity_api_update_entity + entity_api_get_entity, + entity_api_get_globus_url, + entity_api_update_entity } from "../service/entity_api"; import { ingest_api_allowable_edit_states, ingest_api_create_publication, ingest_api_dataset_submit, - ingest_api_notify_slack} from "../service/ingest_api"; -import {search_api_es_query_ids} from "../service/search_api"; -import {humanize} from "../utils/string_helper"; + ingest_api_notify_slack +} from "../service/ingest_api"; +import { humanize } from "../utils/string_helper"; import { - validateProtocolIODOI, - validateRequired, - validateSingleProtocolIODOI + validateProtocolIODOI, + validateRequired, + validateSingleProtocolIODOI } from "../utils/validators"; -import {BulkSelector} from "./ui/bulkSelector"; -import {FormHeader,UserGroupSelectMenuPatch} from "./ui/formParts"; +import { BulkSelector } from "./ui/bulkSelector"; +import { FormHeader, UserGroupSelectMenu } from "./ui/formParts"; +import { PublicationFormFields } from "./ui/fields/PublicationFormFields"; export const PublicationForm = (props) => { let navigate = useNavigate(); - let[entityData, setEntityData] = useState(); - let[isLoading, setLoading] = useState(true); - let[isProcessing, setIsProcessing] = useState(false); - let[valErrorMessages, setValErrorMessages] = useState([]); - let[pageErrors, setPageErrors] = useState(null); - let[globusPath, setGlobusPath] = useState(null); - - - let [bulkError, setBulkError] = useState(false); - let [bulkWarning, setBulkWarning] = useState(false); - let [showBulkError, setShowBulkError] = useState(false); - let [showBulkWarning, setShowBulkWarning] = useState(false); - - let [showSearchDialog, setShowSearchDialog] = useState(false); - let [sourceBulkStatus, setSourceBulkStatus] = useState("idle"); - let [showHIDList, setShowHIDList] = useState(false); - - let [selected_HIDs, setSelectedHIDs] = useState([]); - let [selected_UUIDs, setSelectedUUIDs] = useState([]); - let [selected_string, setSelectedString] = useState(""); - let [sourcesData, setSourcesData] = useState([]); - let [sourceTableError, setSourceTableError] = useState(false); - - let[permissions,setPermissions] = useState( { + let [entityData, setEntityData] = useState(); + let [isLoading, setLoading] = useState(true); + let [isProcessing, setIsProcessing] = useState(false); + let [valErrorMessages, setValErrorMessages] = useState([]); + let [pageErrors, setPageErrors] = useState(null); + let [globusPath, setGlobusPath] = useState(null); + + let [permissions, setPermissions] = useState({ has_admin_priv: false, has_publish_priv: false, has_submit_priv: false, has_write_priv: false - } ); - let [buttonLoading, setButtonLoading] = useState( { + }); + let [buttonLoading, setButtonLoading] = useState({ process: false, save: false, submit: false, - } ) - var[formValues, setFormValues] = useState( { + }); + var [formValues, setFormValues] = useState({ title: "", - publication_venue: "", - publication_date: "", - publication_status: "", - publication_url: "", - publication_doi: "", - omap_doi: "", - issue: "", - volume: "", - pages_or_article_num: "", + publication_venue: "", + publication_date: "", + publication_status: "", + publication_url: "", + publication_doi: "", + omap_doi: "", + issue: "", + volume: "", + pages_or_article_num: "", description: "", direct_ancestor_uuids: [], - } ); - let[formErrors, setFormErrors] = useState( {...formValues} ); // Null out the unused vs "" + }); + let [formErrors, setFormErrors] = useState({ ...formValues }); // Null out the unused vs "" + + // Only track selected UUIDs from BulkSelector + let [selectedBulkUUIDs, setSelectedBulkUUIDs] = useState([]); + let [selectedBulkData, setSelectedBulkData] = useState([]); + const formFields = React.useMemo(() => [ - { + { id: "title", label: "Title", helperText: "The title of the publication", required: true, type: "text", - },{ + }, { id: "publication_venue", label: "Publication Venue", helperText: "The venue of the publication, journal, conference, preprint server, etc...", required: true, type: "text", - },{ + }, { id: "publication_date", label: "Publication Date", helperText: "The date of publication", required: true, type: "date", - },{ + }, { id: "publication_status", label: "Publication Status ", helperText: "Has this Publication been Published?", required: true, type: "radio", - values: ["true","false"] - },{ + values: ["true", "false"] + }, { id: "publication_url", label: "Publication URL", helperText: "The URL at the publishers server for print/pre-print (http(s)://[alpha-numeric-string].[alpha-numeric-string].[...]", required: true, type: "text", - },{ + }, { id: "publication_doi", label: "Publication DOI", helperText: "The DOI of the publication. (##.####/[alpha-numeric-string])", required: false, type: "text", - },{ + }, { id: "OMAP_doi", label: "OMAP DOI", helperText: "A DOI pointing to an Organ Mapping Antibody Panel relevant to this publication", required: false, type: "text", - },{ + }, { id: "issue", label: "Issue", helperText: "The issue number of the journal that it was published in.", required: false, type: "text", - },{ + }, { id: "volume", label: "Volume", helperText: "The volume number of a journal that it was published in.", required: false, type: "text", - },{ + }, { id: "pages_or_article_num", label: "Pages Or Article Number", helperText: 'The pages or the article number in the publication journal e.g., "23", "23-49", "e1003424.', required: false, type: "text", - },{ + }, { id: "description", label: "Abstract", helperText: "Free text description of the publication", @@ -161,34 +143,31 @@ export const PublicationForm = (props) => { } ], []); - const{uuid} = useParams(); + const { uuid } = useParams(); + + const memoizedUserGroupSelectMenu = React.useMemo( + () => , + [] + ); - const memoizedUserGroupSelectMenuPatch = React.useMemo( - () => , - [] - ); - useEffect(() => { - if(uuid && uuid !== ""){ + if (uuid && uuid !== "") { entity_api_get_entity(uuid) .then((response) => { - if(response.status === 200){ + if (response.status === 200) { const entityType = response.results.entity_type; - if(entityType !== "Publication"){ - // Are we sure we're loading a Publication? - // @TODO: Move this sort of handling/detection to the outer app, or into component + if (entityType !== "Publication") { window.location.replace( `${process.env.REACT_APP_URL}/${entityType}/${uuid}` ); - }else{ + } else { const entityData = response.results; setEntityData(entityData); - console.debug('%c◉ entityData ', 'color:#00ff7b', entityData); - setFormValues( { + setFormValues({ title: entityData.title || "", publication_venue: entityData.publication_venue || "", publication_date: entityData.publication_date || "", - publication_status: entityData.publication_status ? entityData.publication_status.toString() : "false", + publication_status: entityData.publication_status ? entityData.publication_status.toString() : "false", publication_url: entityData.publication_url || "", publication_doi: entityData.publication_doi || "", omap_doi: entityData.omap_doi || "", @@ -197,350 +176,164 @@ export const PublicationForm = (props) => { pages_or_article_num: entityData.pages_or_article_num || "", description: entityData.description || "", direct_ancestor_uuids: entityData.direct_ancestors.map(obj => obj.uuid) || [], - } ); - - entity_api_get_globus_url(uuid) - .then((response) => { - setGlobusPath(response.results); - }) //Nothing's wrong if this fails; no need to catch - - // Populate values for Source Management - let string = entityData.direct_ancestors.map(obj => obj.hubmap_id).join(", "); - setSelectedString(string); - setSelectedHIDs(entityData.direct_ancestors.map(obj => obj.hubmap_id)); - setSourcesData(entityData.direct_ancestors) + }); + entity_api_get_globus_url(entityData.uuid) + .then((res) => { + console.debug('%c◉ entity_api_get_globus_url: ', 'color:#E7EEFF;background: #9359FF;padding:200',res); + if(res && res.status === 200){ + setGlobusPath(res.results); + } + }) + setSelectedBulkUUIDs(entityData.direct_ancestors.map(obj => obj.uuid)); + setSelectedBulkData(entityData.direct_ancestors); ingest_api_allowable_edit_states(uuid) .then((response) => { - if(entityData.data_access_level === "public"){ - setPermissions( { + if (entityData.data_access_level === "public") { + setPermissions({ has_write_priv: false, - } ); + }); } setPermissions(response.results); - } ) + }) .catch((error) => { - console.error("ingest_api_allowable_edit_states ERROR", error); setPageErrors(error); - } ); + }); } - }else{ - console.error("entity_api_get_entity RESP NOT 200",response.status,response); + } else { setPageErrors(response); } - } ) + }) .catch((error) => { - console.debug("entity_api_get_entity ERROR", error); setPageErrors(error); - } ); - }else{ - setPermissions( { + }); + } else { + setPermissions({ has_write_priv: true, - } ); + }); } setLoading(false); }, [uuid]); - const handleInputChange = (e) => { - console.log('%c◉ handleInputChange ', 'color:#00ff7b', e); - const {id, value} = e.target; - - if(e.target.type === "radio"){ - console.log(e.target.checked); - setFormValues((prevValues) => ( { + const handleInputChange = React.useCallback((e) => { + const { id, value } = e.target; + if (e.target.type === "radio") { + setFormValues((prevValues) => ({ ...prevValues, publication_status: value, - } )); - - }else{ + })); + } else { setFormValues(prev => { - if (prev[id] === value) return prev; - return {...prev, [id]: value}; - } ); - } - if(id === "dataset_uuids_string"){ - console.debug('%c◉ dataset_uuids_string', 'color:#00ff7b', value); - setSelectedString(value); + if (prev[id] === value) return prev; + return { ...prev, [id]: value }; + }); } + }, []); + + // Callback for BulkSelector + const handleBulkSelectionChange = (uuids, hids, string, data) => { + setFormValues(prev => ({ + ...prev, + direct_ancestor_uuids: uuids + })); + setSelectedBulkUUIDs(uuids); + setSelectedBulkData(data); + }; + const validateDOI = (protocolDOI) => { + if (!validateProtocolIODOI(protocolDOI)) { + setFormErrors((prevValues) => ({ + ...prevValues, + 'protocol_url': "Please enter a valid protocols.io URL" + })); + return 1 + } else if (!validateSingleProtocolIODOI(protocolDOI)) { + setFormErrors((prevValues) => ({ + ...prevValues, + 'protocol_url': "Please enter only one valid protocols.io URL" + })); + return 1 + } else { + setFormErrors((prevValues) => ({ + ...prevValues, + 'protocol_url': "" + })); + return 0 + } } - const validateDOI = (protocolDOI) => { - if (!validateProtocolIODOI(protocolDOI)){ - setFormErrors((prevValues) => ( { - ...prevValues, - 'protocol_url': "Please enter a valid protocols.io URL" - } )); - return 1 - } else if (!validateSingleProtocolIODOI(protocolDOI)){ - setFormErrors((prevValues) => ( { - ...prevValues, - 'protocol_url': "Please enter only one valid protocols.io URL" - } )); - return 1 - }else{ - setFormErrors((prevValues) => ( { - ...prevValues, - 'protocol_url': "" - } )); - return 0 - } - } - - const validateForm = ()=> { + const validateForm = () => { setValErrorMessages(null); - setSourceTableError(false); let errors = 0; - let e_messages=[] + let e_messages = [] let requiredFields = ["title", "publication_venue", "publication_date", "publication_status", "publication_url", "description"]; - - for(let field of requiredFields){ - console.debug(`%c◉ formValues[${field}] `, 'color:#00ff7b', formValues[field]); - if(!validateRequired(formValues[field])){ - console.debug("%c◉ Required Field Error ", "color:#00ff7b", field, formValues[field]); + + for (let field of requiredFields) { + if (!validateRequired(formValues[field])) { let fieldName = formFields.find(f => f.id === field)?.label || humanize(field); - if(field !== "direct_ancestor_uuids"){ - e_messages.push(fieldName+" is a required field"); + if (field !== "direct_ancestor_uuids") { + e_messages.push(fieldName + " is a required field"); } - setSourceTableError(true) - setFormErrors((prevValues) => ( { + setFormErrors((prevValues) => ({ ...prevValues, [field]: " Required", - } )); - errors++; - console.debug("%c◉ Required Field Error ", "color:#00ff7b", field, formValues[field]); - }else{ - setFormErrors((prevValues) => ( { + })); + errors++; + } else { + setFormErrors((prevValues) => ({ ...prevValues, [field]: "", - } )); + })); } } - function validatePositiveIntegerField(fieldName, label){ - if (formValues[fieldName] && formValues[fieldName].length > 0){ - if (isNaN(formValues[fieldName]) || parseInt(formValues[fieldName]) < 0){ + function validatePositiveIntegerField(fieldName, label) { + if (formValues[fieldName] && formValues[fieldName].length > 0) { + if (isNaN(formValues[fieldName]) || parseInt(formValues[fieldName]) < 0) { e_messages.push(`${label} must be a positive integer`); - setFormErrors((prevValues) => ( { + setFormErrors((prevValues) => ({ ...prevValues, [fieldName]: " Must be a positive integer", - } )); + })); errors++; } else { - setFormErrors((prevValues) => ( { + setFormErrors((prevValues) => ({ ...prevValues, [fieldName]: "", - } )); + })); } } } validatePositiveIntegerField('issue', 'Issue'); validatePositiveIntegerField('volume', 'Volume'); - console.log(formValues['direct_ancestor_uuids'],sourcesData) - - if(!sourcesData || sourcesData.length <= 0){ + if (!selectedBulkData || selectedBulkData.length <= 0) { e_messages.push("Please select at least one Source"); - errors++; - setFormErrors((prevValues) => ( { + errors++; + setFormErrors((prevValues) => ({ ...prevValues, ["direct_ancestor_uuids"]: "Required", - } )); - setSourceTableError(true); - }else if(sourcesData.length > 0 && formValues['direct_ancestor_uuids'].length <= 0){ - console.log("source table has data, but no uuids, so we'll sync back to formVals"); - setFormValues((prevValues) => ( { + })); + } else if (selectedBulkData.length > 0 && formValues['direct_ancestor_uuids'].length <= 0) { + setFormValues((prevValues) => ({ ...prevValues, - 'direct_ancestor_uuids': sourcesData.map(obj => obj.uuid), - } )); - }else{ - setSourceTableError(false); + 'direct_ancestor_uuids': selectedBulkData.map(obj => obj.uuid), + })); } // Formatting Validation - errors += validateDOI(formValues['protocol_url']); - console.debug('%c◉ ERROR COUNTER: ', 'color:#00ff7b',errors); - setValErrorMessages(errors>0?e_messages:null); + errors += validateDOI(formValues['protocol_url']); + setValErrorMessages(errors > 0 ? e_messages : null); return errors === 0; } - const preValidateSources = (results) => { - - // Clean up the old - setBulkError([]); - setBulkWarning([]); - setShowBulkError(false) - setShowBulkWarning(false) - - // Prep the new - let errorArray = []; - let warnArray = []; - let goodArray = []; - let typeArray = []; - let originalString = selected_string.split(",").map(s => s.trim()); - - - // Warnings - // Checking for duplicated strings in the provided list - let seen = new Set(); - let duplicates = new Set(); - for (let id of originalString) { - if (seen.has(id)) { - duplicates.add(id); - } else { - seen.add(id); - } - } - duplicates = Array.from(duplicates); - - // Check for entities that were requested by both UUID and Hubmap ID - const entitiesWithBoth = results.filter( - entity => - originalString.includes(entity.uuid) && originalString.includes(entity.hubmap_id) - ); - let dupeEntList = entitiesWithBoth.map(entity => `${entity.hubmap_id} (${entity.uuid})`) - - // Combine these results, if there are any then raise the warning - let combined = [...dupeEntList, ...duplicates]; - if(combined.length > 0 ){ - warnArray.push([`The following ${combined.length} Entit${combined.length>1?'ies':'y'} ${combined.length>1?'were':'was'} referenced more than once:`,combined]) - setBulkWarning(warnArray); - setShowBulkWarning(true) - } - - // Errors - // Checks whatever values were missed from those provided - const missingIds = originalString - .filter(id =>!results - .some(entity => entity.uuid === id || entity.hubmap_id === id) - ); - if (missingIds.length > 0){ - errorArray.push([`The following Entit${missingIds.length>1?'ies':'y'} ${missingIds.length>1?'were':'was'} not found, either because ${missingIds.length>1?'they do':'it does'} not exist or ${missingIds.length>1?'their':'its'} ${missingIds.length>1?'IDs are':'ID is'} not formatted correctly:`,missingIds]); - } - - // Check against type/filter requirements - for(let entity of results){ - if (entity.entity_type !== "Dataset"){ - typeArray.push(`${entity.hubmap_id} (Invalid Type: ${entity.entity_type})`); - }else{goodArray.push(entity);} - } - if(typeArray.length > 0){ - errorArray.push([`The following ${typeArray.length} ID${typeArray.length>1?'s':''} ${typeArray.length>1?'are':'is'} of the wrong Type:`, typeArray]); - } - // Launch the Errors dialog if there are any errors - if (errorArray.length > 0){ - setBulkError(errorArray); - setShowBulkError(true) - console.warn("Bulk Error: ", errorArray); - } - - // Return the ones that are good - return goodArray; - } - - const handleInputUUIDs = (e) => { - console.debug('%c◉ e ', 'color:#00ff7b', e); - e.preventDefault(); - setSourceTableError(false); - if(!showHIDList){ - setShowHIDList(true); - setSelectedString(selected_HIDs.join(", ")) - setSourceBulkStatus("Waiting for Input..."); - }else{ - // Lets clear out the previous errors first - setFormErrors() - setShowHIDList(false); - setSourceBulkStatus("loading"); - setFormErrors((prevValues) => ( { - ...prevValues, - 'direct_ancestor_uuids': "" - } )); - - let cleanList = Array.from(new Set( - selected_string - .split(",") - .map(s => s.trim()) - .filter(s => s.length > 0) - )); - - // If We just Cleared out the whole thing, dump the whole table - // & errors/warnings - if(selected_string.length<=0){ - setSourcesData([]) - setSelectedHIDs([]); - setSelectedString(""); - setBulkError([]); - setBulkWarning([]); - setSourceBulkStatus("complete"); - setFormValues((prevValues) => ( { // Form Field Data - ...prevValues, - 'direct_ancestor_uuids': null, - } )); - - }else{ - let cols=["hubmap_id","uuid","entity_type","subtype","group_name","status","dataset_type","display_subtype"]; - search_api_es_query_ids(cleanList,['datasets'],cols) - .then((response) => { - console.debug('%c◉ response ', 'color:#00ff7b', response); - if(response.status >= 300){ - console.error("search_api_es_query_ids ERROR", response); - setPageErrors(response); - setSourceBulkStatus("error"); - return; - }else if(response.results.length <= 0){ - setBulkError("No Datasets Found for the provided IDs"); - }else{ - let validatedSources = preValidateSources(response.results,cleanList); - setSourcesData(validatedSources) - let entityHIDs = validatedSources.map(obj => obj.hubmap_id) - setSelectedHIDs(entityHIDs); - setSelectedString(entityHIDs.join(", ")); - setShowHIDList(false); - setSourceBulkStatus("complete"); - setFormValues((prevValues) => ( { // Form Field Data - ...prevValues, - 'direct_ancestor_uuids': (validatedSources.map(obj => obj.uuid)), - } )); - } - } ) - .catch((error) => { - console.debug('%c◉ error ', 'color:#00ff7b', error); - } ) - - } - - } - } - - const sourceRemover = (row_uuid,hubmap_id) => { - console.debug('%c◉ Deleting: ', 'color:#00ff7b', hubmap_id); - if(formValues['direct_ancestor_uuids'] && formValues['direct_ancestor_uuids'].length >= 1){ - let newUUIDs = formValues['direct_ancestor_uuids'].filter((uuid) => uuid !== row_uuid); - setFormValues((prev) => ( { - ...prev, - 'direct_ancestor_uuids': newUUIDs - })); - } - setSelectedHIDs((prev) => prev.filter((id) => id !== hubmap_id)); - setSourcesData((prev) => prev.filter((item) => item.hubmap_id !== hubmap_id)); - setSelectedString((prev) => { - const filtered = prev - .split(",") - .map((s) => s.trim()) - .filter((id) => id && id !== hubmap_id); - return filtered.join(", "); - } ); - - }; - const handleSubmit = (e) => { e.preventDefault() - if(validateForm()){ + if (validateForm()) { setIsProcessing(true); - let selectedUUIDs = sourcesData.map((obj) => obj.uuid); - console.debug('%c◉ selected_UUIDs ', 'color:#00ff7b', selectedUUIDs); - let cleanForm ={ + let selectedUUIDs = selectedBulkData.map((obj) => obj.uuid); + let cleanForm = { title: formValues.title, publication_venue: formValues.publication_venue, publication_date: formValues.publication_date, @@ -548,124 +341,121 @@ export const PublicationForm = (props) => { publication_url: formValues.publication_url, publication_doi: formValues.publication_doi, omap_doi: formValues.omap_doi, - ...((formValues.issue) && {issue: parseInt(formValues.issue)} ), - ...((formValues.volume) && {volume: parseInt(formValues.volume)} ), + ...((formValues.issue) && { issue: parseInt(formValues.issue) }), + ...((formValues.volume) && { volume: parseInt(formValues.volume) }), pages_or_article_num: formValues.pages_or_article_num, description: formValues.description, direct_ancestor_uuids: selectedUUIDs, contains_human_genetic_sequences: false // Holdover From Dataset Days } - if(uuid){ // We're in Edit Mode + if (uuid) { // We're in Edit Mode let target = e.target.name; - console.debug('%c◉ VALPASS ', 'color:#00ff7b',); - setButtonLoading((prev) => ( { + setButtonLoading((prev) => ({ ...prev, [target]: true, - } )); - console.log("buttonLoading",buttonLoading,target, buttonLoading[target]); - if(e.target.name === "process"){ // Process + })); + if (e.target.name === "process") { // Process ingest_api_dataset_submit(uuid, JSON.stringify(cleanForm)) .then((response) => { - if (response.status < 300){ + if (response.status < 300) { props.onUpdated(response.results); } else { setPageErrors(response); - setButtonLoading((prev) => ( { + setButtonLoading((prev) => ({ ...prev, process: false, - } )); + })); } - } ) - .catch((error) => { - props.reportError(error); - setPageErrors(error); - } ); - }else if(e.target.name === "submit"){ // Submit + }) + .catch((error) => { + props.reportError(error); + setPageErrors(error); + }); + } else if (e.target.name === "submit") { // Submit entity_api_update_entity(uuid, JSON.stringify(cleanForm)) .then((response) => { - if (response.status < 300){ - var ingestURL= process.env.REACT_APP_URL+"/publication/"+uuid - var slackMessage = {"message": "Publication has been submitted ("+ingestURL+")"} + if (response.status < 300) { + var ingestURL = process.env.REACT_APP_URL + "/publication/" + uuid + var slackMessage = { "message": "Publication has been submitted (" + ingestURL + ")" } ingest_api_notify_slack(slackMessage) .then(() => { - if (response.status < 300){ - props.onUpdated(response.results); + if (response.status < 300) { + props.onUpdated(response.results); } else { wrapUp(response) props.reportError(response); } - } ) - } else { + }) + } else { wrapUp(response) setPageErrors(response); } - } ) - }else if(e.target.name === "save"){ // Save - entity_api_update_entity(uuid,JSON.stringify(cleanForm)) + }) + } else if (e.target.name === "save") { // Save + entity_api_update_entity(uuid, JSON.stringify(cleanForm)) .then((response) => { - if(response.status === 200){ + if (response.status === 200) { props.onUpdated(response.results); - }else{ + } else { wrapUp(response) } - } ) + }) .catch((error) => { wrapUp(error) - } ); + }); } - }else{ // We're in Create mode + } else { // We're in Create mode // They might not have changed the Group Selector, so lets check for the value let selectedGroup = document.getElementById("group_uuid"); - if(selectedGroup?.value){ - cleanForm = {...cleanForm, group_uuid: selectedGroup.value}; + if (selectedGroup?.value) { + cleanForm = { ...cleanForm, group_uuid: selectedGroup.value }; } ingest_api_create_publication(JSON.stringify(cleanForm)) .then((response) => { - if(response.status === 200){ + if (response.status === 200) { entity_api_get_globus_url(response.results.uuid) .then((res) => { - let fullResult = {...response.results, globus_path: res.results}; + console.debug('%c◉ entity_api_get_globus_url: ', 'color:#E7EEFF;background: #9359FF;padding:200',res); + let fullResult = { ...response.results, globus_path: res.results }; props.onCreated(fullResult); - } ) - }else{ + }) + } else { wrapUp(response.error ? response.error : response) } - } ) + }) .catch((error) => { wrapUp(error) setPageErrors(error); - } ); + }); } - }else{ - console.debug("%c◉ Invalid ", "color:#00ff7b"); - setButtonLoading(() => ( { + } else { + setButtonLoading(() => ({ process: false, save: false, submit: false, - } )); + })); } } const wrapUp = (error) => { setPageErrors(error.error ? error.error : error); - setButtonLoading(() => ( { + setButtonLoading(() => ({ process: false, save: false, submit: false, - } )); + })); } const buttonEngine = () => { - return( - - + navigate("/")}> Cancel - {/* @TODO use next form to help work this in to its own UI component? */} {!uuid && ( { className="m-2" onClick={(e) => handleSubmit(e)} type="submit"> - Save + Save )} - {/* Process */} {uuid && uuid.length > 0 && permissions.has_admin_priv && ( - handleSubmit(e)} - variant="contained" + variant="contained" className="m-2"> Process )} - {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status!=="new" && ( - 0 && permissions.has_write_priv && entityData.status !== "new" && ( + handleSubmit(e)} name="submit" - variant="contained" + variant="contained" className="m-2"> Submit )} - {/* Save */} - {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status!=="published" && ( - 0 && permissions.has_write_priv && entityData.status !== "published" && ( + handleSubmit(e)} - variant="contained" + variant="contained" className="m-2"> Save )} - {/* Submit */} ); } - //Click on row from Search - const handleSelectClick = (event) => { - setSourceTableError(false) - console.debug('%c◉ !selected_HIDs.includes(event.row.hubmap_id ', 'color:#00ff7b', !selected_HIDs.includes(event.row.hubmap_id)); - if (!selected_HIDs.includes(event.row.hubmap_id)){ - setSelectedUUIDs((rows) => [...rows, event.row.uuid]); - setSelectedHIDs((ids) => [...ids, event.row.hubmap_id]); - setSelectedString((str) => str + (str ? ", " : "") + event.row.hubmap_id); - setSourcesData((rows) => [...rows, event.row]); - setFormValues((prevValues) => ( { - ...prevValues, - 'direct_ancestor_uuids': selected_UUIDs, - } )) - setFormErrors((prevValues) => ( { //Clear Errors - ...prevValues, - 'direct_ancestor_uuids': "", - } )); - setShowSearchDialog(false); - } else { - // maybe alert them theyre selecting one they already picked? - } - }; - const renderForum = () => { + const memoizedFormFields = React.useMemo( + () => ( + + ), + [formFields, formValues, formErrors, permissions, handleInputChange] + ); + + // MAIN RENDER + if (isLoading || ((!entityData || !formValues) && uuid)) { + return (); + } else { return ( - <> - {formFields.map((field,index) => { - if (["text", "date"].includes(field.type)){ - return ( - 0 - ? field.helperText + " " + formErrors[field.id] - : field.helperText - } +
+ + + +
handleSubmit(e)}> + + + {!uuid && ( + + + Group + + 0 ? true : false} - onChange={(e) => handleInputChange(e)} - disabled={!permissions.has_write_priv} - fullWidth = {field.type === "date" ? false : true } - size={field.type === "date" ? "small" : "medium" } - multiline={field.multiline || false} - rows={field.rows || 1} - className={"my-3 "+(formErrors[field.id] && formErrors[field.id].length > 0 ? "error" : "")}/> - ); - } - if (field.type === "radio"){ - return ( - 0 ? true : false} - className="mb-3" - fullWidth> - {field.label} - - {formErrors[field.id] ? field.helperText + " " + formErrors[field.id] : field.helperText} - - - {field.values && field.values.map((val) => ( - handleInputChange(e)} - // error={this.state.validationStatus.publication_status} - disabled={!permissions.has_write_priv} - checked={formValues[field.id] === val ? true : false} - control={} - // inputProps={{ 'aria-label': toTitleCase(val), id: field.id + "_" + val }} - label={val==="true" ? "Yes" : "No"} /> - ))} - - - ); - } - return ( -
- {field.label}: {field.value} -
- ); - } )} - - ); - } - const memoizedForum = React.useMemo( - () => renderForum(), - [formFields, formValues, formErrors, permissions,] - ); - - // MAIN RENDER - if(isLoading ||((!entityData || !formValues) && uuid)){ - return(); - }else{ - return(
- - - - handleSubmit(e)}> - handleInputUUIDs(e)} - handleSelectClick={handleSelectClick} - handleInputChange={handleInputChange} - sourceRemover={sourceRemover} - sourceTableError={sourceTableError} - showBulkError={showBulkError} - setShowBulkError={setShowBulkError} - showBulkWarning={showBulkWarning} - setShowBulkWarning={setShowBulkWarning} /> - {memoizedForum} - {/* Group */} - {/* Data is viewable in form header & cannot be changed, so only show on Creation */} - {!uuid && ( - - - Group - - handleInputChange(e)} - fullWidth - className="p-2" - sx={{ - borderTopLeftRadius: "4px", - borderTopRightRadius: "4px", - }} - disabled={uuid?true:false} - value={formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid}> - {memoizedUserGroupSelectMenuPatch} - - - )} - - {valErrorMessages && valErrorMessages.length > 0 && ( - - Please Review the following problems: - {valErrorMessages.map(error => ( - - {error} - - ))} + disabled={uuid ? true : false} + value={formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid}> + {memoizedUserGroupSelectMenu} + + + )} + + {valErrorMessages && valErrorMessages.length > 0 && ( + + Please Review the following problems: + {valErrorMessages.map(error => ( + + {error} + + ))} + + )} + + {buttonEngine()} + + + {pageErrors && ( + + Error: {JSON.stringify(pageErrors)} - )} - - {buttonEngine()} - - - {pageErrors && ( - - Error: {JSON.stringify(pageErrors)} - - )} -
); + )} +
+ ); } -} +}; diff --git a/src/src/components/newUpload.jsx b/src/src/components/newUpload.jsx index 45a1c8bf..a37e1bdd 100644 --- a/src/src/components/newUpload.jsx +++ b/src/src/components/newUpload.jsx @@ -56,7 +56,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; export const UploadForm = (props) => { - const [eValopen, setEValopen] = useState(true); + const [eValOpen, setEValOpen] = useState(true); let [snackbarController, setSnackbarController] = useState({ open: false, message: "", @@ -83,7 +83,7 @@ export const UploadForm = (props) => { }); let[formErrors, setFormErrors] = useState({}); let[valErrorMessages, setValErrorMessages] = useState([]); //form validation - let[valMessage, setValMessage] = useState([]); //Entity Validation + // let[valMessage, setValMessage] = useState([]); //Entity Validation let[permissions,setPermissions] = useState({ has_admin_priv: false, has_publish_priv: false, @@ -97,6 +97,10 @@ export const UploadForm = (props) => { let[globusPath, setGlobusPath] = useState(null); let[datasetMenu, setDatasetMenu] = useState(); let[expandVMessage, setExpandVMessage] = useState(false); + let [entityValidation, setEntityValidation] = useState({ + open:false, + message:null + }); const allGroups = JSON.parse(localStorage.getItem("allGroups")); let saveStatuses = ["submitted", "valid", "invalid", "error", "new"] // let validateStatuses = ["valid", "invalid", "error", "new", "incomplete"] @@ -421,15 +425,18 @@ export const UploadForm = (props) => { ingest_api_validate_entity(uuid, "uploads") .then((response) => { console.debug("Response from validate", response); - setValMessage(response); - setEValopen(true) + setEntityValidation({ + open:true, + message:response + }) setProcessingButton(false); }) .catch((error) => { - console.debug('%c◉ error', 'color:#ff005d', error ); - console.debug('%c◉ error long', 'color:#ff005d', error?.data?.error ); - setValMessage(error?.data?.error || error); - setProcessingButton(false); + setEntityValidation({ + open:true, + message: error?.data?.error || error?.toString() || "An unknown error occurred during validation." + }) + setProcessingButton(false); }); break; @@ -911,14 +918,13 @@ export const UploadForm = (props) => { {renderSubmitDialog()} - {valMessage?.status && ( + {entityValidation?.message && ( setEntityValidation(prev => ({ ...prev, open }))} /> )} - {pageErrors && ( <>{RenderPageError(pageErrors)} )} diff --git a/src/src/components/ui/bulkSelector.jsx b/src/src/components/ui/bulkSelector.jsx index ea6df6d0..71dee4a4 100644 --- a/src/src/components/ui/bulkSelector.jsx +++ b/src/src/components/ui/bulkSelector.jsx @@ -1,4 +1,5 @@ -import React,{useState} from "react"; + +import React, { useState, useEffect, useCallback } from "react"; import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; @@ -7,7 +8,7 @@ import ClearIcon from "@mui/icons-material/Clear"; import FormControl from '@mui/material/FormControl'; import DialogTitle from '@mui/material/DialogTitle'; import FormHelperText from '@mui/material/FormHelperText'; -import {Typography} from "@mui/material"; +import { Typography } from "@mui/material"; import TextField from "@mui/material/TextField"; import Box from "@mui/material/Box"; import TableContainer from "@mui/material/TableContainer"; @@ -19,380 +20,616 @@ import TableCell from "@mui/material/TableCell"; import TableBody from "@mui/material/TableBody"; import TableChartIcon from '@mui/icons-material/TableChart'; import PublishIcon from '@mui/icons-material/Publish'; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faPlus, faPenToSquare, faFolderTree,faTrash,faCircleExclamation,faTriangleExclamation} from "@fortawesome/free-solid-svg-icons"; +import { ubkg_api_generate_display_subtype } from "../../service/ubkg_api"; +import { toTitleCase } from "../../utils/string_helper"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faPenToSquare, faFolderTree, faTrash, faCircleExclamation, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import GridLoader from "react-spinners/GridLoader"; import SearchComponent from "../search/SearchComponent"; -import {getPublishStatusColor} from "../../utils/badgeClasses"; -import {FeedbackDialog} from "./formParts"; +import { getPublishStatusColor } from "../../utils/badgeClasses"; +import { FeedbackDialog } from "./formParts"; +import { search_api_es_query_ids } from "../../service/search_api"; + +export function BulkSelector({ + dialogTitle, + dialogSubtitle, + permissions, + initialSelectedHIDs = [], + initialSelectedUUIDs = [], + initialSelectedString = "", + initialSourcesData = [], + onBulkSelectionChange, + searchFilters, + readOnly, + preLoad, +}) { + // Bulk selection state + const [showSearchDialog, setShowSearchDialog] = useState(false); + const [showHIDList, setShowHIDList] = useState(false); + const [bulkError, setBulkError] = useState([]); + const [bulkWarning, setBulkWarning] = useState([]); + const [showBulkError, setShowBulkError] = useState(false); + const [showBulkWarning, setShowBulkWarning] = useState(false); + const [sourceBulkStatus, setSourceBulkStatus] = useState("idle"); + const [sourceTableError, setSourceTableError] = useState(false); + const [selected_HIDs, setSelectedHIDs] = useState(initialSelectedHIDs); + const [selected_UUIDs, setSelectedUUIDs] = useState(initialSelectedUUIDs); + const [selected_string, setSelectedString] = useState(initialSelectedString); + const [sourcesData, setSourcesData] = useState(initialSourcesData); + const title = dialogTitle || "Associated Dataset IDs"; + const subtitle = dialogSubtitle || "Datasets that are associated with this Publication"; + + let readOnlyState = readOnly || (permissions && permissions.has_write_priv === false); + let [loadingState, setLoadingState] = useState(preLoad) + + // Sync sourcesData with prop changes + useEffect(() => { + let sources = assembleSourceAncestorData(initialSourcesData); + setSourcesData(sources); + }, [initialSourcesData]); + + console.log("BulkSelector SOurces: ",initialSourcesData, sourcesData) + // Keep parent in sync + useEffect(() => { + if (onBulkSelectionChange) { + onBulkSelectionChange(selected_UUIDs, selected_HIDs, selected_string, sourcesData); + } + }, [selected_UUIDs, selected_HIDs, selected_string, sourcesData]); + + // Bulk dialog input + let [stringIDs, setStringIDs] = useState(selected_string ? selected_string : ""); + useEffect(() => { + setStringIDs(selected_string); + }, [selected_string]); + + // Check URL for source_list param on mount + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const urlSourceList = params.get('source_list'); + if (urlSourceList && urlSourceList.length > 0) { + // Only run if not already loaded + setStringIDs(urlSourceList); + // Directly trigger handleInputUUIDs with the url value + handleInputUUIDs(undefined, urlSourceList); + } + // eslint-disable-next-line + }, []); + + // Validation helpers + function preValidateSources(results, originalStringArr) { + let errorArray = []; + let warnArray = []; + let goodArray = []; + let typeArray = []; + // Always use the original string array as provided, or split selected_string + let originalString = originalStringArr || selected_string.split(",").map(s => s.trim()); + + // Detect duplicates: count occurrences + let idCounts = {}; + for (let id of originalString) { + if (!id) continue; + idCounts[id] = (idCounts[id] || 0) + 1; + } + // Duplicates are those with count > 1 + let duplicates = Object.keys(idCounts).filter(id => idCounts[id] > 1); + + // Entities requested by both UUID and HuBMAP ID + const entitiesWithBoth = results.filter( + entity => + originalString.includes(entity.uuid) && originalString.includes(entity.hubmap_id) + ); + let dupeEntList = entitiesWithBoth.map(entity => `${entity.hubmap_id} (${entity.uuid})`); + let combined = [...dupeEntList, ...duplicates]; + if (combined.length > 0) { + warnArray.push([`The following ${combined.length} Entit${combined.length > 1 ? 'ies' : 'y'} ${combined.length > 1 ? 'were' : 'was'} referenced more than once:`, combined]); + setBulkWarning(warnArray); + setShowBulkWarning(true); + } + + // Errors: missing IDs + const missingIds = originalString + .filter(id => !results + .some(entity => entity.uuid === id || entity.hubmap_id === id) + ); + if (missingIds.length > 0) { + errorArray.push([`The following Entit${missingIds.length > 1 ? 'ies' : 'y'} ${missingIds.length > 1 ? 'were' : 'was'} not found, either because ${missingIds.length > 1 ? 'they do' : 'it does'} not exist or ${missingIds.length > 1 ? 'their' : 'its'} ${missingIds.length > 1 ? 'IDs are' : 'ID is'} not formatted correctly:`, missingIds]); + } + + // Type check and only add unique entities to goodArray + let addedIds = new Set(); + for (let entity of results) { + // Only add the first occurrence of each entity (by uuid or hubmap_id) + let entityId = entity.hubmap_id || entity.uuid; + if (addedIds.has(entityId)) continue; + let restrictCheck = false; + if(searchFilters?.restrictions?.entityType && entity.entity_type.toLowerCase() !== searchFilters.restrictions.entityType.toLowerCase()){ + restrictCheck = true; + } + if ( + (searchFilters.blacklist && searchFilters.blacklist.includes(entity.entity_type.toLowerCase())) || + (searchFilters.whitelist && !searchFilters.whitelist.includes(entity.entity_type.toLowerCase())) || + (restrictCheck === true) + ) { + typeArray.push(`${entity.hubmap_id} (Invalid Type: ${entity.entity_type})`); + } else { + goodArray.push(entity); + addedIds.add(entityId); + } + } + + if (typeArray.length > 0) { + errorArray.push([`The following ${typeArray.length} ID${typeArray.length > 1 ? 's' : ''} ${typeArray.length > 1 ? 'are' : 'is'} of the wrong Type:`, typeArray]); + } + if (errorArray.length > 0) { + setBulkError(errorArray); + setShowBulkError(true); + } + return goodArray; + } + + // Helper to format display_subtype for sources + function assembleSourceAncestorData(sources) { + var dst = ""; + sources.forEach(function(row, index) { + dst = ubkg_api_generate_display_subtype(row); + console.debug("dst", dst); + if (row.entity_type !== "Dataset") { + dst = toTitleCase(dst); + } + sources[index].display_subtype = toTitleCase(dst); + }); + return sources; + } + + // Handle bulk input dialog update + // Modified handleInputUUIDs to accept an optional overrideString (e.g. from URL) + const handleInputUUIDs = useCallback((e, overrideString) => { + if (e) e.preventDefault(); + setSourceTableError(false); + // If triggered by URL, treat as if showHIDList is false (i.e. go straight to else branch) + const triggeredByUrl = typeof overrideString === 'string'; + if (!showHIDList || triggeredByUrl) { + if (!triggeredByUrl) { + setShowHIDList(true); + setStringIDs(selected_HIDs.join(", ")); + setSourceBulkStatus("Waiting for Input..."); + return; + } + // else, fall through to process the overrideString + } + setShowHIDList(false); + setSourceBulkStatus("loading"); + let idsToProcess = (typeof overrideString === 'string') ? overrideString : stringIDs; + // Split and trim, but do NOT dedupe here; pass all for duplicate detection + let allIds = idsToProcess + .split(",") + .map(s => s.trim()) + .filter(s => s.length > 0); + // For search, only use unique IDs (first occurrence) + let seen = new Set(); + let cleanList = []; + for (let id of allIds) { + if (!seen.has(id)) { + cleanList.push(id); + seen.add(id); + } + } + if (allIds.length <= 0) { + setSourcesData([]); + setSelectedHIDs([]); + setSelectedString(""); + setBulkError([]); + setBulkWarning([]); + setSourceBulkStatus("complete"); + setSelectedUUIDs([]); + setLoadingState(false); + } else { + let cols = ["hubmap_id", "uuid", "entity_type", "subtype", "group_name", "status", "dataset_type", "display_subtype"]; + search_api_es_query_ids(cleanList, ['datasets'], cols) + .then((response) => { + if (response.status >= 300) { + setSourceBulkStatus("error"); + setBulkError([["Search error", [response.statusText || "Unknown error"]]]); + return; + } else if (response.results.length <= 0) { + setBulkError([["No Datasets Found for the provided IDs", []]]); + } else { + // Pass allIds (with possible duplicates) to preValidateSources for warning + let validatedSources = preValidateSources(response.results, allIds); + let entityHIDs = validatedSources.map(obj => obj.hubmap_id); + let entityUUIDs = validatedSources.map(obj => obj.uuid); + setSourcesData(validatedSources); + setSelectedHIDs(entityHIDs); + setSelectedUUIDs(entityUUIDs); + setSelectedString(entityHIDs.join(", ")); + setShowHIDList(false); + setSourceBulkStatus("complete"); + } + }) + .catch((error) => { + setBulkError([["Error", [error?.message || "Unknown error"]]]); + setSourceBulkStatus("error"); + }); + } + // eslint-disable-next-line + }, [showHIDList, stringIDs, selected_HIDs]); + + // Remove a source from the table + const sourceRemover = (row_uuid, hubmap_id) => { + setSelectedUUIDs((prev) => prev.filter((uuid) => uuid !== row_uuid)); + setSelectedHIDs((prev) => prev.filter((id) => id !== hubmap_id)); + setSourcesData((prev) => prev.filter((item) => item.hubmap_id !== hubmap_id)); + setSelectedString((prev) => { + const filtered = prev + .split(",") + .map((s) => s.trim()) + .filter((id) => id && id !== hubmap_id); + return filtered.join(", "); + }); + }; + + // Handle row selection from search dialog + const handleSelectClick = (event) => { + setSourceTableError(false); + if (!selected_HIDs.includes(event.row.hubmap_id)) { + setSelectedUUIDs((rows) => [...rows, event.row.uuid]); + setSelectedHIDs((ids) => [...ids, event.row.hubmap_id]); + setSelectedString((str) => str + (str ? ", " : "") + event.row.hubmap_id); + setSourcesData((rows) => [...rows, event.row]); + setShowSearchDialog(false); + } + }; + + // Bulk dialog + function renderBulkDialog() { + return ( + + + {title} + + + + setStringIDs(event.target.value)} + value={stringIDs} /> + + {"List of Dataset HuBMAP IDs or UUIDs, Comma Separated "} + + + + + + + + + ); + } -export function BulkSelector( { - dialogTitle, - dialogSubtitle, - setShowSearchDialog, - showSearchDialog, - bulkError, - // setBulkError, - bulkWarning, - // setBulkWarning, - sourceBulkStatus, - showHIDList, - setShowHIDList, - selected_string, - sourcesData, - permissions, - handleInputUUIDs, - handleSelectClick, - handleInputChange, - sourceRemover, - sourceTableError, - showBulkError, - setShowBulkError, - showBulkWarning, - setShowBulkWarning, -} ){ + function renderFeedbackDialog() { + return (<> + 0 ? "" : "There are no errors at this time")} + note={"Acceptable results have already been attached to the table, and no further action is needed for them."} + icon={faCircleExclamation} /> + 0 ? "" : "There are no warnings at this time")} + color={"#D3C52F"} + icon={faTriangleExclamation} /> + ); + } - let [stringIDs, setStringIDs] = useState(selected_string ? selected_string : "") - if(stringIDs !== selected_string){ - setStringIDs(selected_string); - } - - function renderBulkDialog(){ - return ( - - - Providing {dialogTitle} - - - handleInputChange(event)} - value={stringIDs}/> - - {"List of Dataset HuBMAP IDs or UUIDs, Comma Seperated " } - - - - - - - - - ) - } - - function renderFeedbackDialog(){ - return (<> - 0 ? "" : "There are no errors at this time" )} - note={"Acceptable results have already been attached to the table, and no further action is needed for them." } - icon={faCircleExclamation}/> - 0 ? "" : "There are no warnings at this time" )} - color={"#D3C52F"} - icon={faTriangleExclamation}/> - ) - } - let totalWarnings = 0; - if (bulkWarning && bulkWarning.length > 0){ - for(let warningSets of bulkWarning){ - console.log("warningSets", warningSets[1].length); - totalWarnings += warningSets[1].length; - } - } - let totalErrors = 0; - if (bulkError && bulkError.length > 0){ - for(let errorSets of bulkError){ - // console.log("errorSets", errorSets[1].length); - totalErrors += errorSets[1]?errorSets[1].length:0; - } - } - let totalRejected = totalWarnings + totalErrors; - - return (<> - {/* Search Dialog */} - showSearchDialog(false)} - aria-labelledby="source-lookup-dialog" - open={showSearchDialog === true ? true : false}> - - handleSelectClick(e)} - custom_title="Search for a Source ID for your Publication" - modecheck="Source" - restrictions={{ - entityType: "dataset" - }} - /> - - - - - - {/* Bulk Input Field Dialog */} - {renderBulkDialog()} - {/* Feedback Dialogs */} - {renderFeedbackDialog()} - - - {dialogTitle} - {dialogSubtitle} - - - Loading ... - + function handleOpenPage(e,row) { + console.log("row",row) + e.preventDefault() + let url = `${process.env.REACT_APP_URL}/${row.entity_type}/${row.uuid}/` + window.open(url, "_blank"); + } - - - - - - Source ID - Subtype - Group Name - Status - {permissions.has_write_priv && ( - - Action - - )} - - - - {(!sourcesData || sourcesData.length === 0) && ( - - - No Data Loaded - - - )} - {(sourceBulkStatus ==="loading") && ( - - - - )} - {sourcesData.map((row, index) => ( - handleSourceCellSelection(row)} - className="row-selection"> - - {row.hubmap_id} - - - {row.dataset_type ? row.dataset_type : row.display_subtype} - - - {row.group_name} - - - {row.status && ( - - {" "}{row.status} - - )} - - {permissions.has_write_priv && ( - - - sourceRemover(row.uuid, row.hubmap_id)} - /> - - - )} - - ))} - -
-
+ let totalWarnings = 0; + if (bulkWarning && bulkWarning.length > 0) { + for (let warningSets of bulkWarning) { + totalWarnings += warningSets[1].length; + } + } + let totalErrors = 0; + if (bulkError && bulkError.length > 0) { + for (let errorSets of bulkError) { + totalErrors += errorSets[1] ? errorSets[1].length : 0; + } + } + let totalRejected = totalWarnings + totalErrors; - -
- - Total Selected: {sourcesData.length} - {(permissions.has_write_priv && totalRejected >0) && ( - - {totalRejected} Rejected - {"Explore the Warning and Error details for more information"} - }> -  | Total Rejected: {totalRejected} - - )} - - - - {totalWarnings} Warning{bulkWarning.length>1?"s":""} - {"Click to view Details"} - - }> - setShowBulkWarning(true)} - style={ - bulkWarning && bulkWarning.length>0 ? { - textDecoration: "underline #D3C52F", - marginLeft: "10px", - cursor: "pointer" - }:{marginLeft: "10px"} - }> - 0 ? "#D3C52F " : "rgb(68, 74, 101)"}/> -  {totalWarnings} - - -   - - {totalErrors} Error{bulkError.length>1?"s":""} - {"Click to view Details"} - }> - setShowBulkError(true)} - style={ - bulkError && bulkError.length>0 ? { - textDecoration: "underline #ff3028", - marginLeft: "15px", - cursor: "pointer" - }:{marginLeft: "10px"}}> - 0 ? "red " : "rgb(68, 74, 101)"}/> -  {totalErrors} - - - - -
+ console.debug('%c◉ searchFilters.restrictions ', 'color:#00ff7b', searchFilters, searchFilters.blacklist); + return (<> + {/* Search Dialog */} + setShowSearchDialog(false)} + aria-labelledby="source-lookup-dialog" + open={showSearchDialog === true}> + + + + + + + + {/* Bulk Input Field Dialog */} + {renderBulkDialog()} + {/* Feedback Dialogs */} + {renderFeedbackDialog()} + + + {dialogTitle} + {subtitle} + + + Loading ... + - - - - - - + + + + + + Source ID + Subtype + Group Name + Status + {permissions.has_write_priv && ( + + Action + + )} + + + + {(!sourcesData || sourcesData.length === 0) && ( + + + No Data Loaded + {loadingState === true && (<>
Loading...)} +
+
+ )} + {(sourceBulkStatus === "loading") && ( + + + + )} + {sourcesData.map((row, index) => ( + + + handleOpenPage(e,row)} style={{cursor:"pointer"}} > + {row.hubmap_id} + + + + {row.dataset_type ? row.dataset_type : row.display_subtype} + + + {row.group_name} + + + {row.status && ( + + {" "}{row.status} + + )} + + {permissions.has_write_priv && !readOnlyState && ( + + + sourceRemover(row.uuid, row.hubmap_id)} + /> + + + )} + + ))} +
+
+
+
+ + Total Selected: {sourcesData.length} + {(permissions.has_write_priv && totalRejected > 0) && ( + + {totalRejected} Rejected + {"Explore the Warning and Error details for more information"} + }> +  | Total Rejected: {totalRejected} + + )} + + + + {totalWarnings} Warning{bulkWarning.length > 1 ? "s" : ""} + {"Click to view Details"} + + }> + setShowBulkWarning(true)} + style={ + bulkWarning && bulkWarning.length > 0 ? { + textDecoration: "underline #D3C52F", + marginLeft: "10px", + cursor: "pointer" + } : { marginLeft: "10px" } + }> + 0 ? "#D3C52F " : "rgb(68, 74, 101)"} /> +  {totalWarnings} + + +   + + {totalErrors} Error{bulkError.length > 1 ? "s" : ""} + {"Click to view Details"} + }> + setShowBulkError(true)} + style={ + bulkError && bulkError.length > 0 ? { + textDecoration: "underline #ff3028", + marginLeft: "15px", + cursor: "pointer" + } : { marginLeft: "10px" }}> + 0 ? "red " : "rgb(68, 74, 101)"} /> +  {totalErrors} + + + + +
-
-
- ) + + + + + + + + + ); } \ No newline at end of file diff --git a/src/src/components/ui/fields/DatasetFormFields.jsx b/src/src/components/ui/fields/DatasetFormFields.jsx new file mode 100644 index 00000000..3fc29e05 --- /dev/null +++ b/src/src/components/ui/fields/DatasetFormFields.jsx @@ -0,0 +1,126 @@ +import React from "react"; +import { + Box, + FormControl, + FormControlLabel, + FormHelperText, + FormLabel, + MenuItem, + Radio, + RadioGroup, + Select, + TextField, + Typography +} from "@mui/material"; + +export const DatasetFormFields = ({ formFields, formValues, formErrors, permissions, handleInputChange, errorHighlight }) => { + return ( + + {formFields.map((field) => { + const error = formErrors && formErrors[field.id]; + const errorStyle = errorHighlight && error ? { borderColor: '#f44336', background: '#fff0f0' } : {}; + if (field.type === "text" || field.type === "textarea") { + return ( + + ); + } + if (field.type === "radio") { + return ( + + {field.label} + + } label="Yes" /> + } label="No" /> + + {error ? error : field.helperText} + + ); + } + + if (field.type === "select") { + if (field.id === "dt_select") { + let datasetTypes = localStorage.getItem("datasetTypes") ? JSON.parse(localStorage.getItem("datasetTypes")).map(dt => dt.dataset_type) : []; + let dtvalues = datasetTypes ? datasetTypes.map(dt => ({ value: dt, label: dt })) : [] + let found = dtvalues.some(item => item.label === formValues[field.id]); + console.debug('%c◉ dtvalues', 'color:#00ff7b',found ); + if(!found && formValues[field.id] && formValues[field.id] !== ""){ + field.values.push({label: formValues[field.id], value: formValues[field.id]}); + console.debug('%c◉ updated field.values', 'color:#00ff7b',field.values ); + } + } + let selectedGroup = null; + if (field.id === "group_uuid" && !field.writeEnabled) { + selectedGroup = field.values.find(v => v.value === formValues[field.id]); + } + return ( + + {field.label} + {!field.writeEnabled && ( + + {selectedGroup ? selectedGroup.label : formValues[field.id]} + + )} + {field.writeEnabled && ( + + )} + {error ? error : field.helperText} + + ); + + } + return null; + } + )} + + ); +}; diff --git a/src/src/components/ui/fields/PublicationFormFields.jsx b/src/src/components/ui/fields/PublicationFormFields.jsx new file mode 100644 index 00000000..9e990470 --- /dev/null +++ b/src/src/components/ui/fields/PublicationFormFields.jsx @@ -0,0 +1,95 @@ +import React from "react"; +import TextField from "@mui/material/TextField"; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import FormLabel from '@mui/material/FormLabel'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; + +export const PublicationFormFields = ({ + formFields, + formValues, + formErrors, + permissions, + handleInputChange +}) => ( + <> + {formFields.map((field, index) => { + if (["text", "date"].includes(field.type)) { + return ( + 0 + ? field.helperText + " " + formErrors[field.id] + : field.helperText + } + sx={{ + width: field.type === "date" ? "250px" : "100%", + }} + value={formValues[field.id] ? formValues[field.id] : ""} + error={formErrors[field.id] && formErrors[field.id].length > 0 ? true : false} + onChange={handleInputChange} + disabled={!permissions.has_write_priv} + fullWidth={field.type === "date" ? false : true} + size={field.type === "date" ? "small" : "medium"} + multiline={field.multiline || false} + rows={field.rows || 1} + className={ + "my-3 " + + (formErrors[field.id] && formErrors[field.id].length > 0 ? "error" : "") + } + /> + ); + } + if (field.type === "radio") { + return ( + 0 ? true : false} + className="mb-3" + fullWidth + > + {field.label} + + {formErrors[field.id] + ? field.helperText + " " + formErrors[field.id] + : field.helperText} + + + {field.values && + field.values.map((val) => ( + } + label={val === "true" ? "Yes" : "No"} + /> + ))} + + + ); + } + return ( +
+ {field.label}: {field.value} +
+ ); + })} + +); \ No newline at end of file diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 2b3f463d..e1674689 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -1,3 +1,4 @@ +import {useNavigate} from "react-router-dom"; import ArticleIcon from '@mui/icons-material/Article'; import BubbleChartIcon from '@mui/icons-material/BubbleChart'; import ClearIcon from "@mui/icons-material/Clear"; @@ -18,30 +19,29 @@ import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faBell, faHeadset, faCircleExclamation} from "@fortawesome/free-solid-svg-icons"; -import CircleNotificationsIcon from '@mui/icons-material/CircleNotifications'; import Grid from '@mui/material/Grid'; import InputLabel from "@mui/material/InputLabel"; import NativeSelect from '@mui/material/NativeSelect'; import Snackbar from '@mui/material/Snackbar'; +import Tooltip from '@mui/material/Tooltip'; import React from "react"; import {SAMPLE_CATEGORIES} from "../../constants"; import {tsToDate} from "../../utils/string_helper"; import HIPPA from "./HIPPA"; - import SpeedDial from '@mui/material/SpeedDial'; import SpeedDialAction from '@mui/material/SpeedDialAction'; import OfflineBoltIcon from '@mui/icons-material/OfflineBolt'; import DynamicFormIcon from '@mui/icons-material/DynamicForm'; +import TextField from "@mui/material/TextField"; +import FormHelperText from "@mui/material/FormHelperText"; -// import {ingest_api_allowable_edit_states} from "../../service/ingest_api"; -// import {entity_api_get_entity} from "../../service/entity_api"; -// const globalToken = localStorage.getItem("info") ? JSON.parse(localStorage.getItem("info")).groups_token : null; - +// The header on all of the Forms (The top bit) export const FormHeader = (props) => { let entityData = props.entityData; let details = (props.entityData[0]!=="new") ? `${entityData.entity_type}: ${entityData.hubmap_id}` : `New ${props.entityData[1]}`; let permissions = props.permissions; let globusURL = props.globusURL; + console.debug('%c◉ FormHeader ', 'color:#00ff7b', entityData,permissions,globusURL); document.title = `HuBMAP Ingest Portal | ${details}`; //@TODO - somehow handle this detection in App return ( @@ -51,6 +51,7 @@ export const FormHeader = (props) => { ) } +// Returns a styalized Icon based on the Entity Type & Status export function IconSelection(entity_type,status){ console.debug('%c◉ status ', 'color:#00ff7b', entity_type, status); console.debug('%c◉ test.. ', 'color:#00ff7b', status? "true" : "false"); @@ -77,6 +78,7 @@ export function IconSelection(entity_type,status){ } } +// Returns the badge class associated with provided status export function badgeClass(status){ var badge_class = ""; if(status=== undefined || !status){ @@ -135,7 +137,155 @@ export function badgeClass(status){ } } +// Admin Tool for Assigning Tasks to Groups to Entities +export function TaskAssignment({ + uuid, + permissions, + entityData, + formValues, + formErrors, + handleInputChange, + allGroups + }) { + return ( + + + + Ingest Task + + + + The next task in the data ingest process. + + + + + Assigned to Group + + + + {allGroups && allGroups.map(group => ( + + ))} + + + The group responsible for the next step in the data ingest process. + + + + ); +} +// Returns a styalized Globus Link Button +export function renderUploadLink(entityData){ + function handleUploadSelect(e, uuid){ + window.location.assign(`/upload/${uuid}`,); + } + return ( + + + + This {entityData.entityType} is contained in the data Upload{" "} + + + + + ) +} + +// Reusable helper to pre-fill form values from URL parameters +// NOTE: source_list is specifically handled inside the BulkSelector component itself +export function prefillFormValuesFromUrl(setFormValues, setSnackbarController) { + const url = new URL(window.location.href); + const params = Object.fromEntries(url.searchParams.entries()); + if (Object.keys(params).length > 0) { + setFormValues((prevValues) => ({ + ...prevValues, + ...params + })); + if (setSnackbarController) { + setSnackbarController({ + open: true, + message: "Passing Form values from URL parameters", + status: "success" + }); + } + } + return params; +} + +// Not yet in use Modal similar to the one in the legacy forms that prompts for your group if you have multiple groups +// Considering switching back to this, saving for now +export function GroupModal ({ + submitWithGroup, + showGroupSelect, + closeGroupModal + }){ + let userGroups = JSON.parse(localStorage.getItem("userGroups")) || []; + return ( + + + You currently have multiple group assignments, Please select a primary group for submission + + + + + + + + + + + ); +} + +// Styalized snackbar component rendering Error Notes for FeedbackDialog function errorNote(){ return (<> @@ -143,6 +293,8 @@ function errorNote(){ ) } + +// Styalized Footnote component for FeedbackDialog function noteWrap(note){ return ( @@ -150,11 +302,15 @@ function noteWrap(note){ ); } + +// Returns a Chip / Badge with status text and color based on status (using badgeClass for class) function statusBadge(status){ return ( ) } + +// Returns Special a Chip / Badge with NEW text and color (Purple) function newBadge(type){ console.debug('%c◉ newBadge ', 'color:#00ff7b', type); let newBadgeStyle = { @@ -171,6 +327,8 @@ function newBadge(type){ ) } + +// SWAT / MOSDAP Helper to build a pretty list of priority projects function buildPriorityProjectList(list){ if(list.length>1){ return list.join(", "); @@ -178,22 +336,40 @@ function buildPriorityProjectList(list){ return list[0] } } + +// The TopLeftmost part of the Form Header function topHeader(entityData){ if(entityData[0] !== "new"){ return ( -

{IconSelection(entityData.entity_type)}{entityData.entity_type} Information

+

{IconSelection(entityData.entity_type)} {entityData.entity_type} Information

HuBMAP ID: {entityData.hubmap_id} {entityData.status && ( - Status: {entityData.status ? statusBadge(entityData.status) : ""} - )} + Status: + + + {entityData?.pipeline_message || "" } +
+ }> + {entityData.status ? statusBadge(entityData.status) : ""} +
+
+ )} {entityData.priority_project_list && ( - Priority Projects: {buildPriorityProjectList(entityData.priority_project_list)} + + Priority Projects: {entityData.priority_project_list?.length > 1 + ? entityData.priority_project_list.join(", ") + : entityData.priority_project_list?.[0]} + )} Entered by: {entityData.created_by_user_email} + Group: {entityData.group_name} {(entityData.entity_type === "Donor" || entityData.entity_type ==="Sample") && ( Submission ID: {entityData.submission_id} )} @@ -221,6 +397,8 @@ function topHeader(entityData){ ) } } + +// The Rightmost part of the Form Header function infoPanels(entityData,permissions,globusURL){ return ( @@ -257,6 +435,9 @@ function infoPanels(entityData,permissions,globusURL){ acessible when data associated with it was published. )} + {entityData && (entityData.upload) &&( + renderUploadLink(entityData) + )} {!permissions.has_write_priv && !permissions.has_admin_priv && ( elements in table rows + const idLinks = table.querySelectorAll('tbody tr td:first-child a'); + console.log("idLinks",idLinks); + return Array.from(idLinks).map(a => a.textContent.trim()); +} +// Returns a select menu of the User's dataprovider groups +// Possibly Deprecating with move of GroupsSelector into modal or Field managers export function UserGroupSelectMenu(formValues){ let userGroups = JSON.parse(localStorage.getItem("userGroups")); if(formValues.group_name){ @@ -300,28 +495,8 @@ export function UserGroupSelectMenu(formValues){ } } -export function UserGroupSelectMenuPatch(formValues){ - console.debug('%c◉ UserGroupSelectMenuPatch ', 'color:#0026FF', formValues); - let userGroups = JSON.parse(localStorage.getItem("userGroups")); - if(formValues.group_name){ - return( - - ) - }else{ - let menuArray = []; - for(let group of userGroups){ - menuArray.push( - - ); - } - return menuArray; - } -} - +// Checks if the entityType in the URL matches the type of entity requested +// if it's not, redirects you on over to the proper form export function FormCheckRedirect(uuid,entityType,form){ console.debug('%c◉ FormCheckRedirect ', 'color:#ff0073', uuid,entityType,form); if(entityType !== form){ @@ -332,12 +507,11 @@ export function FormCheckRedirect(uuid,entityType,form){ } } +// Prevents the Search Filter Restrictions from lingering & effecting the main Search View export function combineTypeOptionsComplete(){ // Removes the Whitelist / Blacklist stuff, // mostly for use for resetting the main Search Page View - var combinedList = []; - // FIRST: Main Entity Types combinedList.push( { // @TODO: Find out why Importing Warps this donor: "Donor" , @@ -350,7 +524,6 @@ export function combineTypeOptionsComplete(){ // NEXT: Sample Categories combinedList.push(SAMPLE_CATEGORIES); // @TODO: Switch these to UBKG too? - // LAST: Organs let organs = []; let organList = handleSortOrgans(JSON.parse(localStorage.getItem("organs"))) @@ -371,8 +544,8 @@ export function combineTypeOptionsComplete(){ } }; +// Returns a sorted Map of Organs (accounting for L/R) for use in Search Filters export function handleSortOrgans(organList){ - // console.debug('%c⊙', 'color:#00ff7b', "handleSortOrgans", organList ); let sortedDataProp = {}; let sortedDataArray = []; var sortedMap = new Map(); @@ -388,59 +561,41 @@ export function handleSortOrgans(organList){ return sortedMap; }; -export function GroupSelector( {formValues, handleInputChange, memoizedUserGroupSelectMenuPatch, uuid} ){ - if (uuid) return null; - return ( - - - Group - - - {memoizedUserGroupSelectMenuPatch} - - - ); -} - +// Gathers all of the Input fields on the page Plus some other data to generate a pre-fill URL export function HandleCopyFormUrl(e) { - const url = new URL(window.location.origin + window.location.pathname); - let formValues = document.querySelectorAll("input, textarea, select"); - Object.entries(formValues).forEach(([key, value]) => { - console.debug('%c◉ formValues ', 'color:#00ff7b', value.id, value.type, value.value); - if (value !== undefined && value !== null && value !== "" && value.type !== "checkbox" && value.id && value.value && !value.disabled) { - url.searchParams.set(value.id, value.value); - } - else if (value.type === "checkbox" && value.checked ) { - url.searchParams.set(value.id, value.checked === true ? "true" : "false"); - } - }); - navigator.clipboard.writeText(url.toString()) - .then(() => { - // setSnackMessage("Form URL copied to clipboard!"); - // setShowSnack(true) - }) - .catch(() => { - // setSnackMessage("Form URL Failed to copy to clipboard!"); - // setShowSnack(true) - }); + const url = new URL(window.location.origin + window.location.pathname); + let formValues = document.querySelectorAll("input, textarea, select"); + console.debug('%c◉ Found Inputs: ', 'color:#00ff7b',formValues ); + Object.entries(formValues).forEach(([key, value]) => { + console.debug('%c◉ formValues ', 'color:#00ff7b', value.id, value.type, value.value); + if (value !== undefined && value !== null && value !== "" && value.type !== "checkbox" && value.id && value.value && !value.disabled) { + url.searchParams.set(value.id, value.value); + } + else if (value.type === "checkbox" && value.checked ) { + url.searchParams.set(value.id, value.checked === true ? "true" : "false"); + } + }); + let sourceTable = getHubmapIDsFromBulkTable(); + if (sourceTable.length > 0) { + url.searchParams.set("source_list", sourceTable.join(",")); } -export default function SpeedDialTooltipOpen() { + navigator.clipboard.writeText(url.toString()) + .then(() => { + // setSnackMessage("Form URL copied to clipboard!"); + // setShowSnack(true) + }) + .catch(() => { + // setSnackMessage("Form URL Failed to copy to clipboard!"); + // setShowSnack(true) + }); +} + +// The SpeedDial tool being used for quick actions like Copy Form URL & Create Dataset (Admin quick access) +export function SpeedDialTooltipOpen() { + let navigate = useNavigate(); const actions = [ - // { icon: , name: 'Copy' }, - // { icon: , name: 'Save' }, { icon: , name: 'Copy Form Prefil URL', action: (e) => HandleCopyFormUrl(e) }, - // { icon: , name: 'Share' }, + { icon: , name: 'Create Dataset', action: (e) => navigate(`/new/datasetAdmin`) }, ]; const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(true); @@ -449,7 +604,7 @@ export default function SpeedDialTooltipOpen() { } direction={"down"}> {actions.map((action) => ( @@ -465,16 +620,11 @@ export default function SpeedDialTooltipOpen() { /> ))} - {/* e.setShowSnack(false)} - message={e.snackMessage} - /> */} ); } +// Returns a Feedback Dialog Modal for displaying Warnings, Errors, etc export function FeedbackDialog( { showMessage, setShowMessage, @@ -484,7 +634,7 @@ export function FeedbackDialog( { note, color, icon -} ){ + } ){ let messageColor = color ? color : "#444A65"; let altColorLight = LightenHex(messageColor, 20); let altColorDark = DarkenHex(messageColor, 20); @@ -561,11 +711,9 @@ export function FeedbackDialog( { borderTop:"none", borderBottomLeftRadius: "4px", borderBottomRightRadius: "4px"}}> - {note && ( noteWrap(note) )} - {((!message || message.length <= 0) && (!summary || summary.length<=0)) && (!note || note.length<=0) && ( errorNote(errorNote) )} @@ -591,9 +739,9 @@ export function FeedbackDialog( { ) } +// Returns a Snackbar on Entity Validation messages export function EntityValidationMessage(props) { const {response, eValopen, setEValopen} = props - console.debug('%c◉ EntityValidationMessage Inner Response ', 'color:#00ff7b', response); let message = response?.results ?? response?.data ?? "No Response"; let severity = message?.error ? "error" : "info"; if (message?.error) message = message.error; @@ -620,7 +768,7 @@ export function EntityValidationMessage(props) { ); } -// @TODO: Eventually unify the Snackbar Feedback across forms into one +// Universal Snackbar for messages export function SnackbarFeedback(props){ const {snackbarController, setSnackbarController, } = props function closeSnack(){ @@ -650,8 +798,8 @@ export function SnackbarFeedback(props){ ); } - // TODO: Move this into.... idk a Value/Calculation helper service/thing? -export function HexToHsl(hex){ +// Color manipullation (Right now namely for Feedback Dialog Colors) +function HexToHsl(hex){ hex = hex.replace(/^#/, ''); if (hex.length === 3) hex = hex.split('').map(x => x + x).join(''); const num = parseInt(hex, 16); @@ -673,7 +821,7 @@ export function HexToHsl(hex){ } return {h: h * 360, s: s * 100, l: l * 100}; } -export function HslToHex(h, s, l){ +function HslToHex(h, s, l){ s /= 100; l /= 100; let c = (1 - Math.abs(2 * l - 1)) * s; let x = c * (1 - Math.abs((h / 60) % 2 - 1)); @@ -690,12 +838,12 @@ export function HslToHex(h, s, l){ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b) .toString(16).slice(1).toUpperCase(); } -export function LightenHex(hex, amount = 15){ +function LightenHex(hex, amount = 15){ let {h, s, l} = HexToHsl(hex); l = Math.min(100, l + amount); // Increase lightness by 'amount' return HslToHex(h, s, l); } -export function DarkenHex(hex, amount = 15){ +function DarkenHex(hex, amount = 15){ let {h, s, l} = HexToHsl(hex); l = Math.max(0, l - amount); // Decrease lightness by 'amount', but not below 0 return HslToHex(h, s, l); diff --git a/src/src/service/entity_api.js b/src/src/service/entity_api.js index 7213cb31..c5c03aef 100644 --- a/src/src/service/entity_api.js +++ b/src/src/service/entity_api.js @@ -37,12 +37,14 @@ export function entity_api_get_entity(uuid){ * */ export function entity_api_update_entity(uuid, data){ + // https://github.com/hubmapconsortium/entity-api/blob/08f2ab3b9ba258c1c08bf42138c042f23e8a4d87/src/app.py#L1335 let url = `${process.env.REACT_APP_ENTITY_API_URL}/entities/${uuid}`; return axios .put(url, data, options) .then(res => { // console.debug("entity_api_update_entity", res); let results = res.data; + // TODO: Move Slack Messaging handling out from UI to direct service calls here? return{status: res.status, results: results} } ) .catch(error => { diff --git a/src/src/service/ingest_api.js b/src/src/service/ingest_api.js index a73c71a7..1857f3ed 100644 --- a/src/src/service/ingest_api.js +++ b/src/src/service/ingest_api.js @@ -185,10 +185,8 @@ export function ingest_api_create_publication(data) { * */ export function ingest_api_dataset_submit(uuid, data) { - // console.debug("ingest_api_dataset_submit", data); - - let url = `${process.env.REACT_APP_DATAINGEST_API_URL}/datasets/${uuid}/submit`; - + // https://github.com/hubmapconsortium/ingest-api/blob/b0c472d14fe9b0c89cfc2c843a6bc58072bbb1a6/src/app.py#L1547 + let url = `${process.env.REACT_APP_DATAINGEST_API_URL}/datasets/${uuid}/submit`; return axios .put(url, data, options) .then(res => { @@ -527,10 +525,9 @@ export function ingest_api_pipeline_test_privs(auth) { * Pipeline Testing Submit * */ -export function ingest_api_pipeline_test_submit(auth, data) { - const options = {headers: {Authorization: "Bearer " + globalToken,"Content-Type":"application/json"}}; +export function ingest_api_pipeline_test_submit(data) { + // https://github.com/hubmapconsortium/ingest-api/blob/b0c472d14fe9b0c89cfc2c843a6bc58072bbb1a6/src/app.py#L2384 let url = `${process.env.REACT_APP_DATAINGEST_API_URL}/datasets/${data['uuid']}/submit-for-pipeline-testing`; - console.debug('%c◉ url ', 'color:#00ff7b', url); return axios .post(url, {}, options) .then(res => { diff --git a/src/src/utils/validators.jsx b/src/src/utils/validators.jsx index a962acb6..c96a93ad 100644 --- a/src/src/utils/validators.jsx +++ b/src/src/utils/validators.jsx @@ -1,6 +1,6 @@ export function validateRequired(value){ - // console.debug(typeof value); - // console.debug("VALUE",value); + console.debug(typeof value); + console.debug("VALUE",value); if(typeof value === "string"){ // console.debug("trim", (value.trim()!=="")); return value.trim() !== ""; @@ -17,6 +17,13 @@ export function validateRequired(value){ // console.debug(value.name); return value.name.trim() !== ""; } + }else if(typeof value === "boolean"){ + // console.debug("value length: ",value.length); + if(value === true || value === false){ + return true; + }else{ + return false; + } } }