From 1696a5f6374986e64ebdc29cb4bd73875de30710 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Thu, 18 Sep 2025 14:57:50 -0400 Subject: [PATCH 01/26] Adds Cypress directory to gitignore (Can be removed when ready) --- src/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.* From 889a7cb67f5b754fc8ef28e2d07d2af788c306b6 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Tue, 23 Sep 2025 14:21:47 -0400 Subject: [PATCH 02/26] Merge Confict Resolution with popped Stash --- src/src/App.js | 5 +- src/src/components/newPublication.jsx | 918 ++++++++----------------- src/src/components/ui/bulkSelector.jsx | 895 ++++++++++++++---------- src/src/components/ui/formParts.jsx | 65 +- 4 files changed, 876 insertions(+), 1007 deletions(-) diff --git a/src/src/App.js b/src/src/App.js index c66eb054..79a0323d 100644 --- a/src/src/App.js +++ b/src/src/App.js @@ -50,6 +50,7 @@ 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"; export function App(props){ let navigate = useNavigate(); @@ -530,7 +531,7 @@ export function App(props){ 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,7 +544,7 @@ export function App(props){ updateSuccess(response)}/>} /> updateSuccess(response)}/>} /> - } /> + } /> {/* } /> */} updateSuccess(response)} />} /> updateSuccess(response)} reportError={reportError} handleCancel={handleCancel} status="view" />} /> diff --git a/src/src/components/newPublication.jsx b/src/src/components/newPublication.jsx index 9d3a882e..dd38229f 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, UserGroupSelectMenuPatch } 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 memoizedUserGroupSelectMenuPatch = 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}> + {memoizedUserGroupSelectMenuPatch} + + + )} + + {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/ui/bulkSelector.jsx b/src/src/components/ui/bulkSelector.jsx index ea6df6d0..29585abe 100644 --- a/src/src/components/ui/bulkSelector.jsx +++ b/src/src/components/ui/bulkSelector.jsx @@ -1,4 +1,4 @@ -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 +7,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 +19,537 @@ 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 { 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, - setShowSearchDialog, - showSearchDialog, - bulkError, - // setBulkError, - bulkWarning, - // setBulkWarning, - sourceBulkStatus, - showHIDList, - setShowHIDList, - selected_string, - sourcesData, - permissions, - handleInputUUIDs, - handleSelectClick, - handleInputChange, - sourceRemover, - sourceTableError, - showBulkError, - setShowBulkError, - showBulkWarning, - setShowBulkWarning, -} ){ +export function BulkSelector({ + dialogTitle = "Associated Dataset IDs", + dialogSubtitle = "Datasets that are associated with this Publication", + permissions, + initialSelectedHIDs = [], + initialSelectedUUIDs = [], + initialSelectedString = "", + initialSourcesData = [], + onBulkSelectionChange, + searchFilters, +}) { + // 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); - 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}/> - ) - } + const [selected_HIDs, setSelectedHIDs] = useState(initialSelectedHIDs); + const [selected_UUIDs, setSelectedUUIDs] = useState(initialSelectedUUIDs); + const [selected_string, setSelectedString] = useState(initialSelectedString); + const [sourcesData, setSourcesData] = useState(initialSourcesData); - 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 ... - + // Sync sourcesData with prop changes + useEffect(() => { + setSourcesData(initialSourcesData); + }, [initialSourcesData]); - - - - - - 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)} - /> - - - )} - - ))} - -
-
+ 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]); - -
- - 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} - - - - -
+ // Bulk dialog input + let [stringIDs, setStringIDs] = useState(selected_string ? selected_string : ""); + useEffect(() => { + setStringIDs(selected_string); + }, [selected_string]); - - - - - - + // Validation helpers + function preValidateSources(results, originalStringArr) { + let errorArray = []; + let warnArray = []; + let goodArray = []; + let typeArray = []; + let originalString = originalStringArr || selected_string.split(",").map(s => s.trim()); - - - ) + // Warnings: duplicated strings + 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); + + // 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 + 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]); + } + if (errorArray.length > 0) { + setBulkError(errorArray); + setShowBulkError(true); + } + return goodArray; + } + + // Handle bulk input dialog update + const handleInputUUIDs = useCallback((e) => { + if (e) e.preventDefault(); + setSourceTableError(false); + if (!showHIDList) { + setShowHIDList(true); + setStringIDs(selected_HIDs.join(", ")); + setSourceBulkStatus("Waiting for Input..."); + } else { + setShowHIDList(false); + setSourceBulkStatus("loading"); + let cleanList = Array.from(new Set( + stringIDs + .split(",") + .map(s => s.trim()) + .filter(s => s.length > 0) + )); + if (stringIDs.length <= 0) { + setSourcesData([]); + setSelectedHIDs([]); + setSelectedString(""); + setBulkError([]); + setBulkWarning([]); + setSourceBulkStatus("complete"); + setSelectedUUIDs([]); + } 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 { + let validatedSources = preValidateSources(response.results, cleanList); + 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 ( + + + Providing {dialogTitle} + + + + setStringIDs(event.target.value)} + value={stringIDs} /> + + {"List of Dataset HuBMAP IDs or UUIDs, Comma Separated "} + + + + + + + + + ); + } + + 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) { + 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; + + return (<> + {/* Search Dialog */} + setShowSearchDialog(false)} + aria-labelledby="source-lookup-dialog" + open={showSearchDialog === true}> + + + + + + + + {/* Bulk Input Field Dialog */} + {renderBulkDialog()} + {/* Feedback Dialogs */} + {renderFeedbackDialog()} + + + {dialogTitle} + {dialogSubtitle} + + + Loading ... + + + + + + + + Source ID + Subtype + Group Name + Status + {permissions.has_write_priv && ( + + Action + + )} + + + + {(!sourcesData || sourcesData.length === 0) && ( + + + No Data Loaded + + + )} + {(sourceBulkStatus === "loading") && ( + + + + )} + {sourcesData.map((row, index) => ( + + + {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)} + /> + + + )} + + ))} + +
+
+
+ + 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/formParts.jsx b/src/src/components/ui/formParts.jsx index 2b3f463d..1832def6 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"; @@ -42,6 +43,7 @@ export const FormHeader = (props) => { 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 ( @@ -135,6 +137,22 @@ export function badgeClass(status){ } } +function renderUploadLink(entityData){ + return ( + + + + This {entityData.entityType} is contained in the data Upload{" "} + + + + + ) +} function errorNote(){ return (<> @@ -257,6 +275,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 && ( { - 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"); - } + 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) }); - 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) - }); - } +} export default 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: 'Create Dataset', action: (e) => navigate(`/new/datasetAdmin`) }, // { icon: , name: 'Share' }, ]; const [open, setOpen] = React.useState(false); @@ -449,7 +472,7 @@ export default function SpeedDialTooltipOpen() { } direction={"down"}> {actions.map((action) => ( From af3c313fd8f0adf59a8cfcad142263b53099a420 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Tue, 23 Sep 2025 14:22:10 -0400 Subject: [PATCH 03/26] Additional Files from stash pop merge resolution --- src/src/components/DatasetFormFields.jsx | 0 src/src/components/newDataset.jsx | 389 ++++++++++++++++++ .../ui/fields/DatasetFormFields.jsx | 100 +++++ .../ui/fields/PublicationFormFields.jsx | 95 +++++ 4 files changed, 584 insertions(+) create mode 100644 src/src/components/DatasetFormFields.jsx create mode 100644 src/src/components/newDataset.jsx create mode 100644 src/src/components/ui/fields/DatasetFormFields.jsx create mode 100644 src/src/components/ui/fields/PublicationFormFields.jsx 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/newDataset.jsx b/src/src/components/newDataset.jsx new file mode 100644 index 00000000..c36ce03a --- /dev/null +++ b/src/src/components/newDataset.jsx @@ -0,0 +1,389 @@ +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 InputLabel from "@mui/material/InputLabel"; +import LinearProgress from "@mui/material/LinearProgress"; +import NativeSelect from '@mui/material/NativeSelect'; +import { useNavigate, useParams } from "react-router-dom"; +import { BulkSelector } from "./ui/bulkSelector"; +import { FormHeader, UserGroupSelectMenuPatch } from "./ui/formParts"; +import { DatasetFormFields } from "./ui/fields/DatasetFormFields"; +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 } from "../service/ingest_api"; + +export const DatasetForm = (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 [permissions, setPermissions] = useState({ + has_admin_priv: false, + has_publish_priv: false, + has_submit_priv: false, + has_write_priv: false + }); + let [buttonLoading, setButtonLoading] = useState({ + process: false, + save: false, + submit: false, + }); + let [formValues, setFormValues] = useState({ + lab_dataset_id: "", + description: "", + dataset_info: "", + contains_human_genetic_sequences: "", + dt_select: "", + direct_ancestor_uuids: [], + }); + let [formErrors, setFormErrors] = useState({ ...formValues }); + let [selectedBulkUUIDs, setSelectedBulkUUIDs] = useState([]); + let [selectedBulkData, setSelectedBulkData] = useState([]); + + const formFields = useMemo(() => [ + { + id: "lab_dataset_id", + label: "Lab Name or ID", + helperText: "Lab Name or ID", + required: true, + type: "text" + }, + { + id: "description", + label: "Description", + helperText: "Description Tips", + 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: !entityData || !entityData.uuid, + values: localStorage.getItem("datasetTypes") ? JSON.parse(localStorage.getItem("datasetTypes")).map(dt => ({ value: dt.dataset_type, label: dt.dataset_type })) : [] + } + ], []); + + const { uuid } = useParams(); + + const memoizedFormHeader = useMemo( + () => , [uuid, entityData, permissions] + ); + + + useEffect(() => { + if (uuid && uuid !== "") { + entity_api_get_entity(uuid) + .then((response) => { + 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); + setFormValues({ + 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, + }); + setSelectedBulkUUIDs(entityData.direct_ancestors.map(obj => obj.uuid)); + setSelectedBulkData(entityData.direct_ancestors); + console.log("Entity Data:", entityData.direct_ancestors, selectedBulkData); + ingest_api_allowable_edit_states(uuid) + .then((response) => { + if (entityData.data_access_level === "public") { + setPermissions({ + has_write_priv: false, + }); + } + setPermissions(response.results); + }) + .catch((error) => { + setPageErrors(error); + }); + } + } else { + setPageErrors(response); + } + }) + .catch((error) => { + setPageErrors(error); + }); + } else { + setPermissions({ + has_write_priv: true, + }); + } + setLoading(false); + // eslint-disable-next-line + }, [uuid]); + + const handleInputChange = useCallback((e) => { + const { name, value } = e.target; + setFormValues(prev => { + if (prev[name] === value) return prev; + return { ...prev, [name]: value }; + }); + }, []); + + // Callback for BulkSelector + const handleBulkSelectionChange = (uuids, hids, string, data) => { + setFormValues(prev => ({ + ...prev, + direct_ancestor_uuids: uuids + })); + setSelectedBulkUUIDs(uuids); + setSelectedBulkData(data); + }; + + const validateForm = () => { + setValErrorMessages(null); + let errors = 0; + let e_messages = []; + let requiredFields = ["lab_dataset_id", "description", "contains_human_genetic_sequences", "dt_select"]; + for (let field of requiredFields) { + if (!validateRequired(formValues[field])) { + let fieldName = formFields.find(f => f.id === field)?.label || humanize(field); + e_messages.push(fieldName + " is a required field"); + setFormErrors((prevValues) => ({ + ...prevValues, + [field]: " Required", + })); + errors++; + } else { + setFormErrors((prevValues) => ({ + ...prevValues, + [field]: "", + })); + } + } + if (!selectedBulkData || selectedBulkData.length <= 0) { + e_messages.push("Please select at least one Source"); + errors++; + setFormErrors((prevValues) => ({ + ...prevValues, + ["direct_ancestor_uuids"]: "Required", + })); + } else if (selectedBulkData.length > 0 && formValues['direct_ancestor_uuids'].length <= 0) { + setFormValues((prevValues) => ({ + ...prevValues, + 'direct_ancestor_uuids': selectedBulkData.map(obj => obj.uuid), + })); + } + setValErrorMessages(errors > 0 ? e_messages : null); + return errors === 0; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (validateForm()) { + setIsProcessing(true); + let selectedUUIDs = selectedBulkData.map((obj) => obj.uuid); + let cleanForm = { + ...formValues, + direct_ancestor_uuids: selectedUUIDs + }; + if (uuid) { + let target = e.target.name; + setButtonLoading((prev) => ({ + ...prev, + [target]: true, + })); + entity_api_update_entity(uuid, JSON.stringify(cleanForm)) + .then((response) => { + if (response.status < 300) { + props.onUpdated(response.results); + } else { + setPageErrors(response); + setButtonLoading((prev) => ({ + ...prev, + [target]: false, + })); + } + }) + .catch((error) => { + setPageErrors(error); + setButtonLoading((prev) => ({ + ...prev, + [target]: false, + })); + }); + } else { + 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); + }); + } + } else { + setButtonLoading(() => ({ + process: false, + save: false, + submit: false, + })); + } + }; + + const buttonEngine = () => { + return ( + + navigate("/")} + > + Cancel + + {!uuid && ( + handleSubmit(e)} + type="submit" + > + Save + + )} + {uuid && uuid.length > 0 && permissions.has_admin_priv && ( + handleSubmit(e)} + variant="contained" + className="m-2" + > + Process + + )} + {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status !== "new" && ( + handleSubmit(e)} + name="submit" + variant="contained" + className="m-2" + > + Submit + + )} + {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status !== "published" && ( + handleSubmit(e)} + variant="contained" + className="m-2" + > + Save + + )} + + ); + }; + + if (isLoading || ((!entityData || !formValues) && uuid)) { + return (); + } else { + return ( +
+ + {memoizedFormHeader} + +
handleSubmit(e)}> + + + {!uuid && ( + + + Group + + + + + + )} + {valErrorMessages && valErrorMessages.length > 0 && ( + + Please Review the following problems: + {valErrorMessages.map(error => ( + + {error} + + ))} + + )} + {buttonEngine()} + + {pageErrors && ( + + Error: {JSON.stringify(pageErrors)} + + )} +
+ ); + } +}; \ 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..5f824f1f --- /dev/null +++ b/src/src/components/ui/fields/DatasetFormFields.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { TextField, FormControl, FormLabel, RadioGroup, FormControlLabel, Radio, Select, MenuItem, FormHelperText, Box, Typography } from "@mui/material"; + +export const DatasetFormFields = ({ formFields, formValues, formErrors, permissions, handleInputChange }) => { + + return ( + + {formFields.map((field) => { + if (field.type === "text" || field.type === "textarea") { + return ( + + ); + } + if (field.type === "radio") { + return ( + + {field.label} + + } label="Yes" /> + } label="No" /> + + {formErrors[field.id] || field.helperText} + + ); + } + 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.dataset_type, label: dt.dataset_type })) : [] + console.debug('%c◉ dtvalues', 'color:#00ff7b',dtvalues ); + if(!field.values.includes(formValues[field.id])){ + field.values.push(formValues[field.id]); + } + console.debug('%c◉ providedType ', 'color:#00ff7b', field.values.includes(formValues[field.id]),formValues[field.id]); + return ( + + {field.label} + {!field.writeEnabled && ( + + {formValues[field.id]} + + )} + {field.writeEnabled && ( + + )} + {formErrors[field.id] || 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 From 61454b390d49174e9cc980d87fa86299618d80f3 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Tue, 23 Sep 2025 17:00:16 -0400 Subject: [PATCH 04/26] Include new componentized Admin Task Asignment interface, fix Group value association on save, adaptations for Dataset Type Values not present in Select on Load --- src/src/components/newDataset.jsx | 39 +++++++++++++++++-- .../ui/fields/DatasetFormFields.jsx | 36 +++++++++++------ src/src/components/ui/formParts.jsx | 4 +- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index c36ce03a..46675e19 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -8,9 +8,11 @@ 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 TextField from "@mui/material/TextField"; +import FormHelperText from '@mui/material/FormHelperText'; import { useNavigate, useParams } from "react-router-dom"; import { BulkSelector } from "./ui/bulkSelector"; -import { FormHeader, UserGroupSelectMenuPatch } from "./ui/formParts"; +import { FormHeader, UserGroupSelectMenuPatch,TaskAssignment } from "./ui/formParts"; import { DatasetFormFields } from "./ui/fields/DatasetFormFields"; import { humanize } from "../utils/string_helper"; import { validateRequired } from "../utils/validators"; @@ -47,6 +49,7 @@ export const DatasetForm = (props) => { let [formErrors, setFormErrors] = useState({ ...formValues }); let [selectedBulkUUIDs, setSelectedBulkUUIDs] = useState([]); let [selectedBulkData, setSelectedBulkData] = useState([]); + const allGroups = localStorage.getItem("allGroups") ? JSON.parse(localStorage.getItem("allGroups")) : []; const formFields = useMemo(() => [ { @@ -153,6 +156,7 @@ export const DatasetForm = (props) => { if (prev[name] === value) return prev; return { ...prev, [name]: value }; }); + console.debug('%c◉ handleInputChange', 'color:#00ff7b',name, value ); }, []); // Callback for BulkSelector @@ -209,9 +213,23 @@ export const DatasetForm = (props) => { setIsProcessing(true); let selectedUUIDs = selectedBulkData.map((obj) => obj.uuid); let cleanForm = { - ...formValues, - direct_ancestor_uuids: selectedUUIDs + lab_dataset_id:formValues.lab_dataset_id, + contains_human_genetic_sequences:formValues.contains_human_genetic_sequences, + description:formValues.description, + dataset_info:formValues.dataset_info, + direct_ancestor_uuids: selectedUUIDs, }; + console.debug('%c⭗ Data', 'color:#00ff7b',cleanForm ); + // if(this.state.has_admin_priv){ + // console.debug('%c⊙', 'color:#8b1fff', this.state.assigned_to_group_name, this.state.ingest_task ); + // if (this.state.assigned_to_group_name && this.state.assigned_to_group_name.length > 0){ + // data["assigned_to_group_name"]=this.state.assigned_to_group_name; + // } + // if (this.state.ingest_task && this.state.ingest_task.length > 0){ + // data["ingest_task"]=this.state.ingest_task; + // } + // } + console.debug('%c⭗ Data', 'color:#00ff7b',cleanForm); if (uuid) { let target = e.target.name; setButtonLoading((prev) => ({ @@ -238,6 +256,9 @@ export const DatasetForm = (props) => { })); }); } else { + let group_uuid = formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid; + cleanForm.dataset_type = this.state.dataset_type; + cleanForm.group_uuid = group_uuid; ingest_api_create_dataset(JSON.stringify(cleanForm)) .then((response) => { if (response.status === 200) { @@ -345,6 +366,18 @@ export const DatasetForm = (props) => { permissions={permissions} handleInputChange={handleInputChange} /> + {/* TASK ASSIGNMENT */} + {uuid && ( + + )} {!uuid && ( diff --git a/src/src/components/ui/fields/DatasetFormFields.jsx b/src/src/components/ui/fields/DatasetFormFields.jsx index 5f824f1f..ab32bb61 100644 --- a/src/src/components/ui/fields/DatasetFormFields.jsx +++ b/src/src/components/ui/fields/DatasetFormFields.jsx @@ -1,5 +1,17 @@ import React from "react"; -import { TextField, FormControl, FormLabel, RadioGroup, FormControlLabel, Radio, Select, MenuItem, FormHelperText, Box, Typography } from "@mui/material"; +import { + Box, + FormControl, + FormControlLabel, + FormHelperText, + FormLabel, + MenuItem, + Radio, + RadioGroup, + Select, + TextField, + Typography +} from "@mui/material"; export const DatasetFormFields = ({ formFields, formValues, formErrors, permissions, handleInputChange }) => { @@ -53,12 +65,13 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi } 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.dataset_type, label: dt.dataset_type })) : [] - console.debug('%c◉ dtvalues', 'color:#00ff7b',dtvalues ); - if(!field.values.includes(formValues[field.id])){ - field.values.push(formValues[field.id]); + 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){ + field.values.push({label: formValues[field.id], value: formValues[field.id]}); + console.debug('%c◉ updated field.values', 'color:#00ff7b',field.values ); } - console.debug('%c◉ providedType ', 'color:#00ff7b', field.values.includes(formValues[field.id]),formValues[field.id]); return ( {field.label} {!field.writeEnabled && ( - + {formValues[field.id]} )} @@ -79,13 +92,12 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi id={field.id} name={field.id} value={formValues[field.id] || ""} - onChange={handleInputChange} - displayEmpty> + onChange={handleInputChange} > {!field.values.includes(formValues[field.id]) && ( - {formValues[field.id]} + {formValues[field.id]} )} - {field.values && field.values.map((val) => ( - {val.label} + {field.values && field.values.map((val, index) => ( + {val.label} ))} )} diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 1832def6..cb243a3e 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -28,11 +28,12 @@ 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"; @@ -299,7 +300,6 @@ function infoPanels(entityData,permissions,globusURL){ ) } - export function UserGroupSelectMenu(formValues){ let userGroups = JSON.parse(localStorage.getItem("userGroups")); if(formValues.group_name){ From 70e7a6589871a7858d91a7afd862bf39a10ed6d9 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 24 Sep 2025 10:39:27 -0400 Subject: [PATCH 05/26] Adds Admin Task Assignment to formParts, Updates BulkSelector for handling of Display Subtypes / types , adds ReadOnly Support for BulkSelector --- src/src/App.js | 2 +- src/src/components/newDataset.jsx | 55 ++++++++++++++++---- src/src/components/ui/bulkSelector.jsx | 21 ++++++-- src/src/components/ui/formParts.jsx | 69 ++++++++++++++++++++++++-- 4 files changed, 130 insertions(+), 17 deletions(-) diff --git a/src/src/App.js b/src/src/App.js index 79a0323d..40e8078b 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"; diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 46675e19..827f0f96 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -14,10 +14,12 @@ import { useNavigate, useParams } from "react-router-dom"; import { BulkSelector } from "./ui/bulkSelector"; import { FormHeader, UserGroupSelectMenuPatch,TaskAssignment } from "./ui/formParts"; import { DatasetFormFields } from "./ui/fields/DatasetFormFields"; -import { humanize } from "../utils/string_helper"; +import {RevertFeature} from "../utils/revertModal"; +import { humanize, toTitleCase } 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 } from "../service/ingest_api"; +import {ubkg_api_generate_display_subtype} from "../service/ubkg_api"; export const DatasetForm = (props) => { let navigate = useNavigate(); @@ -26,6 +28,7 @@ export const DatasetForm = (props) => { let [isProcessing, setIsProcessing] = useState(false); let [valErrorMessages, setValErrorMessages] = useState([]); let [pageErrors, setPageErrors] = useState(null); + let [readOnlySources, setReadOnlySources] = useState(false); let [permissions, setPermissions] = useState({ has_admin_priv: false, @@ -49,6 +52,8 @@ export const DatasetForm = (props) => { let [formErrors, setFormErrors] = useState({ ...formValues }); let [selectedBulkUUIDs, setSelectedBulkUUIDs] = useState([]); let [selectedBulkData, setSelectedBulkData] = useState([]); + let [selectedBulkDataFormatted, setSelectedBulkDataFormatted] = useState([]); + const allGroups = localStorage.getItem("allGroups") ? JSON.parse(localStorage.getItem("allGroups")) : []; const formFields = useMemo(() => [ @@ -118,17 +123,31 @@ export const DatasetForm = (props) => { contains_human_genetic_sequences: entityData.contains_human_genetic_sequences, dt_select: entityData.dataset_type, }); + let formattedAncestors = assembleSourceAncestorData(entityData.direct_ancestors); + setSelectedBulkUUIDs(entityData.direct_ancestors.map(obj => obj.uuid)); - setSelectedBulkData(entityData.direct_ancestors); + setSelectedBulkData(formattedAncestors); console.log("Entity Data:", entityData.direct_ancestors, selectedBulkData); + + // 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(uuid) .then((response) => { + console.debug('%c◉ ingest_api_allowable_edit_states Permissions', 'color:#00ff7b', response.results); 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) => { setPageErrors(error); @@ -150,6 +169,17 @@ export const DatasetForm = (props) => { // eslint-disable-next-line }, [uuid]); + const assembleSourceAncestorData = (source_uuids) =>{ + var dst=""; + source_uuids.forEach(function(row, index) { + dst=ubkg_api_generate_display_subtype(row); + console.debug("dst", dst); + source_uuids[index].display_subtype=toTitleCase(dst); + }); + console.debug('%c◉ selectedBulkDataFormatted', 'color:#00ff7b',selectedBulkDataFormatted ); + return (source_uuids) + } + const handleInputChange = useCallback((e) => { const { name, value } = e.target; setFormValues(prev => { @@ -281,13 +311,17 @@ export const DatasetForm = (props) => { }; const buttonEngine = () => { - return ( + return (<> + + {uuid && uuid.length > 0 && permissions.has_admin_priv &&( + + )} + navigate("/")} - > + onClick={() => navigate("/")}> Cancel {!uuid && ( @@ -297,19 +331,18 @@ export const DatasetForm = (props) => { loading={isProcessing} className="m-2" onClick={(e) => handleSubmit(e)} - type="submit" - > + type="submit"> Save )} + {uuid && uuid.length > 0 && permissions.has_admin_priv && ( handleSubmit(e)} variant="contained" - className="m-2" - > + className="m-2"> Process )} @@ -336,7 +369,7 @@ export const DatasetForm = (props) => { )} - ); + ); }; if (isLoading || ((!entityData || !formValues) && uuid)) { @@ -358,6 +391,7 @@ export const DatasetForm = (props) => { custom_subtitle: "Collections may not be selected for Dataset sources", blacklist: ['collection'] }} + readOnly={readOnlySources} /> { formErrors={formErrors} permissions={permissions} handleInputChange={handleInputChange} + readOnly = {uuid && true} /> {/* TASK ASSIGNMENT */} {uuid && ( diff --git a/src/src/components/ui/bulkSelector.jsx b/src/src/components/ui/bulkSelector.jsx index 29585abe..37d7163c 100644 --- a/src/src/components/ui/bulkSelector.jsx +++ b/src/src/components/ui/bulkSelector.jsx @@ -26,6 +26,7 @@ import SearchComponent from "../search/SearchComponent"; import { getPublishStatusColor } from "../../utils/badgeClasses"; import { FeedbackDialog } from "./formParts"; import { search_api_es_query_ids } from "../../service/search_api"; +import {ubkg_api_generate_display_subtype} from "../../service/ubkg_api"; export function BulkSelector({ dialogTitle = "Associated Dataset IDs", @@ -37,6 +38,8 @@ export function BulkSelector({ initialSourcesData = [], onBulkSelectionChange, searchFilters, + readOnly + }) { // Bulk selection state const [showSearchDialog, setShowSearchDialog] = useState(false); @@ -52,6 +55,8 @@ export function BulkSelector({ const [selected_UUIDs, setSelectedUUIDs] = useState(initialSelectedUUIDs); const [selected_string, setSelectedString] = useState(initialSelectedString); const [sourcesData, setSourcesData] = useState(initialSourcesData); + + let readOnlyState = readOnly || (permissions && permissions.has_write_priv === false); // Sync sourcesData with prop changes useEffect(() => { @@ -299,6 +304,14 @@ export function BulkSelector({ ); } + + 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"); + } + let totalWarnings = 0; if (bulkWarning && bulkWarning.length > 0) { for (let warningSets of bulkWarning) { @@ -424,7 +437,9 @@ export function BulkSelector({ key={row.hubmap_id + "" + index} className="row-selection"> - {row.hubmap_id} + handleOpenPage(e,row)} style={{cursor:"pointer"}} > + {row.hubmap_id} + {row.dataset_type ? row.dataset_type : row.display_subtype} @@ -439,7 +454,7 @@ export function BulkSelector({ )} - {permissions.has_write_priv && ( + {permissions.has_write_priv && !readOnlyState && ( - + @@ -300,6 +362,7 @@ function infoPanels(entityData,permissions,globusURL){ ) } + export function UserGroupSelectMenu(formValues){ let userGroups = JSON.parse(localStorage.getItem("userGroups")); if(formValues.group_name){ @@ -456,7 +519,7 @@ export function HandleCopyFormUrl(e) { // setShowSnack(true) }); } -export default function SpeedDialTooltipOpen() { +export function SpeedDialTooltipOpen() { let navigate = useNavigate(); const actions = [ // { icon: , name: 'Copy' }, From 14bfeadbb0fe6f6aac1c48a7c214a79e20a31258 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 24 Sep 2025 11:26:40 -0400 Subject: [PATCH 06/26] Fix Duplicating entries in Dataset Type Dropdown, Expand Form Pre-fill (& Value scraping) to Handle Multiple Sources for the Bulk Selector --- src/src/components/newDataset.jsx | 102 ++++++++++++++++-- .../ui/fields/DatasetFormFields.jsx | 7 +- src/src/components/ui/formParts.jsx | 16 +++ 3 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 827f0f96..549a81cb 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -10,6 +10,7 @@ import LinearProgress from "@mui/material/LinearProgress"; import NativeSelect from '@mui/material/NativeSelect'; import TextField from "@mui/material/TextField"; import FormHelperText from '@mui/material/FormHelperText'; +import Snackbar from '@mui/material/Snackbar'; import { useNavigate, useParams } from "react-router-dom"; import { BulkSelector } from "./ui/bulkSelector"; import { FormHeader, UserGroupSelectMenuPatch,TaskAssignment } from "./ui/formParts"; @@ -23,6 +24,7 @@ import {ubkg_api_generate_display_subtype} from "../service/ubkg_api"; export const DatasetForm = (props) => { let navigate = useNavigate(); + let [entityData, setEntityData] = useState(); let [isLoading, setLoading] = useState(true); let [isProcessing, setIsProcessing] = useState(false); @@ -52,7 +54,11 @@ export const DatasetForm = (props) => { let [formErrors, setFormErrors] = useState({ ...formValues }); let [selectedBulkUUIDs, setSelectedBulkUUIDs] = useState([]); let [selectedBulkData, setSelectedBulkData] = useState([]); - let [selectedBulkDataFormatted, setSelectedBulkDataFormatted] = useState([]); + let [snackbarController, setSnackbarController] = useState({ + open: false, + message: "", + status: "info" + }); const allGroups = localStorage.getItem("allGroups") ? JSON.parse(localStorage.getItem("allGroups")) : []; @@ -124,7 +130,6 @@ export const DatasetForm = (props) => { dt_select: entityData.dataset_type, }); let formattedAncestors = assembleSourceAncestorData(entityData.direct_ancestors); - setSelectedBulkUUIDs(entityData.direct_ancestors.map(obj => obj.uuid)); setSelectedBulkData(formattedAncestors); console.log("Entity Data:", entityData.direct_ancestors, selectedBulkData); @@ -161,9 +166,68 @@ export const DatasetForm = (props) => { setPageErrors(error); }); } else { + let url = new URL(window.location.href); + let params = Object.fromEntries(url.searchParams.entries()); + if(Object.keys(params).length > 0){ + console.debug('%c◉ URL params ', 'color:#00ff7b', params); + setFormValues((prevValues) => ({ + ...prevValues, + ...params + })); + setSnackbarController({ + open: true, + message: "Passing Form values from URL parameters", + status: "success" + }); + } setPermissions({ has_write_priv: true, }); + // Set the Source if Passed from URL + if(params.source_list){ + // Support comma-separated list of UUIDs + const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); + let ancestorData = []; + ancestorUUIDs.forEach((uuidItem) => { + entity_api_get_entity(uuidItem) + .then((response) => { + let error = response?.data?.error ?? false; + console.debug('%c◉ entity_api_get_entity response ', 'color:#00ff7b', response, error); + if(!error && (response?.results?.entity_type !== "Collection")){ + console.debug('%c◉ error ', 'color:#00ff7b', error); + let passSource = {row: response?.results ? response.results : null}; + console.log("passSource",passSource) + ancestorData.push(passSource.row); + } + else if(!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample"){ + setSnackbarController({ + open: true, + message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, + status: "error" + }); + }else if(error){ + setSnackbarController({ + open: true, + message: `Sorry, There was an error selecting your source: ${error}`, + status: "error" + }); + }else{ + throw new Error(response) + } + }) + .catch((error) => { + console.debug("entity_api_get_entity ERROR", error); + setPageErrors(error); + }); + }); + setSelectedBulkUUIDs(ancestorUUIDs); + setSelectedBulkData(assembleSourceAncestorData(ancestorData)); + handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); + setFormValues((prevValues) => ({ + ...prevValues, + direct_ancestor_uuids: ancestorUUIDs + })); + } } setLoading(false); // eslint-disable-next-line @@ -176,7 +240,6 @@ export const DatasetForm = (props) => { console.debug("dst", dst); source_uuids[index].display_subtype=toTitleCase(dst); }); - console.debug('%c◉ selectedBulkDataFormatted', 'color:#00ff7b',selectedBulkDataFormatted ); return (source_uuids) } @@ -287,7 +350,7 @@ export const DatasetForm = (props) => { }); } else { let group_uuid = formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid; - cleanForm.dataset_type = this.state.dataset_type; + cleanForm.dataset_type = formValues.dataset_type; cleanForm.group_uuid = group_uuid; ingest_api_create_dataset(JSON.stringify(cleanForm)) .then((response) => { @@ -299,8 +362,14 @@ export const DatasetForm = (props) => { }) .catch((error) => { setPageErrors(error); + setButtonLoading(() => ({ + process: false, + save: false, + submit: false, + })); }); } + } else { setButtonLoading(() => ({ process: false, @@ -376,7 +445,7 @@ export const DatasetForm = (props) => { return (); } else { return ( -
+ {memoizedFormHeader} @@ -451,7 +520,28 @@ export const DatasetForm = (props) => { 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} + + +
); } }; \ No newline at end of file diff --git a/src/src/components/ui/fields/DatasetFormFields.jsx b/src/src/components/ui/fields/DatasetFormFields.jsx index ab32bb61..15c25a9a 100644 --- a/src/src/components/ui/fields/DatasetFormFields.jsx +++ b/src/src/components/ui/fields/DatasetFormFields.jsx @@ -68,10 +68,11 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi 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){ + 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 ); } + console.debug('%c◉ field.values ', 'color:#00ff7b',field.values); return ( - {!field.values.includes(formValues[field.id]) && ( + {/* {!field.values.includes(formValues[field.id]) && ( {formValues[field.id]} - )} + )} */} {field.values && field.values.map((val, index) => ( {val.label} ))} diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 6de24897..abe86948 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -361,6 +361,16 @@ function infoPanels(entityData,permissions,globusURL){ ) } +function getHubmapIDsFromBulkTable() { + const wrapper = document.getElementById('bulkTableWrapper'); + if (!wrapper) return []; + const table = wrapper.querySelector('table'); + if (!table) return []; + // Select all first-column 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()); +} export function UserGroupSelectMenu(formValues){ @@ -497,6 +507,7 @@ export function GroupSelector( {formValues, handleInputChange, memoizedUserGroup ); } + export function HandleCopyFormUrl(e) { const url = new URL(window.location.origin + window.location.pathname); let formValues = document.querySelectorAll("input, textarea, select"); @@ -509,6 +520,10 @@ export function HandleCopyFormUrl(e) { url.searchParams.set(value.id, value.checked === true ? "true" : "false"); } }); + let sourceTable = getHubmapIDsFromBulkTable(); + if (sourceTable.length > 0) { + url.searchParams.set("source_list", sourceTable.join(",")); + } navigator.clipboard.writeText(url.toString()) .then(() => { // setSnackMessage("Form URL copied to clipboard!"); @@ -519,6 +534,7 @@ export function HandleCopyFormUrl(e) { // setShowSnack(true) }); } + export function SpeedDialTooltipOpen() { let navigate = useNavigate(); const actions = [ From 28e06f1ee5c0886517cc3d23819688c6bc39bdec Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 24 Sep 2025 12:06:23 -0400 Subject: [PATCH 07/26] Adds loading note feedback when BulkSelector is Being Populated at Load, adds prop field Values to dt_select input, --- src/src/components/newDataset.jsx | 49 ++++++++++++------- src/src/components/ui/bulkSelector.jsx | 7 ++- .../ui/fields/DatasetFormFields.jsx | 9 ++-- src/src/components/ui/formParts.jsx | 1 + 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 549a81cb..e2389f93 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -31,6 +31,7 @@ export const DatasetForm = (props) => { let [valErrorMessages, setValErrorMessages] = useState([]); let [pageErrors, setPageErrors] = useState(null); let [readOnlySources, setReadOnlySources] = useState(false); + let [preLoadingBulk, setPreLoadingBulk] = useState(false); let [permissions, setPermissions] = useState({ has_admin_priv: false, @@ -185,9 +186,12 @@ export const DatasetForm = (props) => { }); // Set the Source if Passed from URL if(params.source_list){ + setPreLoadingBulk(true); + console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); // Support comma-separated list of UUIDs const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); let ancestorData = []; + let fetchCount = 0; ancestorUUIDs.forEach((uuidItem) => { entity_api_get_entity(uuidItem) .then((response) => { @@ -218,15 +222,21 @@ export const DatasetForm = (props) => { .catch((error) => { console.debug("entity_api_get_entity ERROR", error); setPageErrors(error); + }) + .finally(() => { + fetchCount++; + if (fetchCount === ancestorUUIDs.length) { + setSelectedBulkUUIDs(ancestorUUIDs); + setSelectedBulkData(assembleSourceAncestorData(ancestorData)); + handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); + setFormValues((prevValues) => ({ + ...prevValues, + direct_ancestor_uuids: ancestorUUIDs + })); + setPreLoadingBulk(false); + } }); }); - setSelectedBulkUUIDs(ancestorUUIDs); - setSelectedBulkData(assembleSourceAncestorData(ancestorData)); - handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); - setFormValues((prevValues) => ({ - ...prevValues, - direct_ancestor_uuids: ancestorUUIDs - })); } } setLoading(false); @@ -450,18 +460,19 @@ export const DatasetForm = (props) => { {memoizedFormHeader}
handleSubmit(e)}> - + { @@ -160,6 +161,7 @@ export function BulkSelector({ 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) @@ -424,6 +426,7 @@ export function BulkSelector({ No Data Loaded + {loadingState === true && (<>
Loading...)}
)} diff --git a/src/src/components/ui/fields/DatasetFormFields.jsx b/src/src/components/ui/fields/DatasetFormFields.jsx index 15c25a9a..8cce4d31 100644 --- a/src/src/components/ui/fields/DatasetFormFields.jsx +++ b/src/src/components/ui/fields/DatasetFormFields.jsx @@ -93,10 +93,11 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi id={field.id} name={field.id} value={formValues[field.id] || ""} - onChange={handleInputChange} > - {/* {!field.values.includes(formValues[field.id]) && ( - {formValues[field.id]} - )} */} + onChange={handleInputChange} + inputProps={{ + name: field.id, + id: field.id, + }} > {field.values && field.values.map((val, index) => ( {val.label} ))} diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index abe86948..255219d5 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -511,6 +511,7 @@ export function GroupSelector( {formValues, handleInputChange, memoizedUserGroup export function HandleCopyFormUrl(e) { 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) { From 5da25602e514e922505a36e2ca68bc89f8a4a307 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 24 Sep 2025 12:22:20 -0400 Subject: [PATCH 08/26] Fix Writability of Editable Dataset Data Type --- src/src/components/newDataset.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index e2389f93..a96de023 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -98,7 +98,7 @@ export const DatasetForm = (props) => { helperText: "", required: true, type: "select", - writeEnabled: !entityData || !entityData.uuid, + writeEnabled: entityData?.uuid ? true : false, values: localStorage.getItem("datasetTypes") ? JSON.parse(localStorage.getItem("datasetTypes")).map(dt => ({ value: dt.dataset_type, label: dt.dataset_type })) : [] } ], []); @@ -317,7 +317,7 @@ export const DatasetForm = (props) => { let selectedUUIDs = selectedBulkData.map((obj) => obj.uuid); let cleanForm = { lab_dataset_id:formValues.lab_dataset_id, - contains_human_genetic_sequences:formValues.contains_human_genetic_sequences, + contains_human_genetic_sequences:formValues.contains_human_genetic_sequences === "yes" ? true : false, description:formValues.description, dataset_info:formValues.dataset_info, direct_ancestor_uuids: selectedUUIDs, @@ -360,8 +360,10 @@ export const DatasetForm = (props) => { }); } else { let group_uuid = formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid; - cleanForm.dataset_type = formValues.dataset_type; - cleanForm.group_uuid = group_uuid; + cleanForm.dataset_type = formValues.dt_select; + cleanForm.group_uuid = formValues.group_uuid; + console.log(formValues, formValues.contains_human_genetic_sequences ) + console.debug('%c◉ cleanForm ', 'color:#00ff7b', cleanForm); ingest_api_create_dataset(JSON.stringify(cleanForm)) .then((response) => { if (response.status === 200) { From 9c970ebfe0bed856872819a002d5645cbb4d364a Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 24 Sep 2025 14:22:43 -0400 Subject: [PATCH 09/26] Adaps casing for display subtypes on non-Dataset rows for Bulk Selector, adds multi-source URL parameter fill on Dataset form, --- src/src/components/newDataset.jsx | 125 ++++++++++++------------- src/src/components/ui/bulkSelector.jsx | 21 ++++- 2 files changed, 77 insertions(+), 69 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index a96de023..deb239f3 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -16,11 +16,10 @@ import { BulkSelector } from "./ui/bulkSelector"; import { FormHeader, UserGroupSelectMenuPatch,TaskAssignment } from "./ui/formParts"; import { DatasetFormFields } from "./ui/fields/DatasetFormFields"; import {RevertFeature} from "../utils/revertModal"; -import { humanize, toTitleCase } from "../utils/string_helper"; +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 } from "../service/ingest_api"; -import {ubkg_api_generate_display_subtype} from "../service/ubkg_api"; export const DatasetForm = (props) => { let navigate = useNavigate(); @@ -130,9 +129,9 @@ export const DatasetForm = (props) => { contains_human_genetic_sequences: entityData.contains_human_genetic_sequences, dt_select: entityData.dataset_type, }); - let formattedAncestors = assembleSourceAncestorData(entityData.direct_ancestors); + // let formattedAncestors = assembleSourceAncestorData(entityData.direct_ancestors); setSelectedBulkUUIDs(entityData.direct_ancestors.map(obj => obj.uuid)); - setSelectedBulkData(formattedAncestors); + setSelectedBulkData(entityData.direct_ancestors); console.log("Entity Data:", entityData.direct_ancestors, selectedBulkData); // Set the Bulk Table to read only if the Dataset is not in a modifiable state @@ -181,77 +180,69 @@ export const DatasetForm = (props) => { status: "success" }); } + // Set the Source if Passed from URL + if(params.source_list){ + setPreLoadingBulk(true); + console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); + // Support comma-separated list of UUIDs + const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); + let ancestorData = []; + let fetchCount = 0; + ancestorUUIDs.forEach((uuidItem) => { + entity_api_get_entity(uuidItem) + .then((response) => { + let error = response?.data?.error ?? false; + console.debug('%c◉ entity_api_get_entity response ', 'color:#00ff7b', response, error); + if(!error && (response?.results?.entity_type !== "Collection")){ + console.debug('%c◉ error ', 'color:#00ff7b', error); + let passSource = {row: response?.results ? response.results : null}; + console.log("passSource",passSource) + ancestorData.push(passSource.row); + } + else if(!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample"){ + setSnackbarController({ + open: true, + message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, + status: "error" + }); + }else if(error){ + setSnackbarController({ + open: true, + message: `Sorry, There was an error selecting your source: ${error}`, + status: "error" + }); + }else{ + throw new Error(response) + } + }) + .catch((error) => { + console.debug("entity_api_get_entity ERROR", error); + setPageErrors(error); + }) + .finally(() => { + fetchCount++; + if (fetchCount === ancestorUUIDs.length) { + setSelectedBulkUUIDs(ancestorUUIDs); + setSelectedBulkData(ancestorData); + handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); + setFormValues((prevValues) => ({ + ...prevValues, + direct_ancestor_uuids: ancestorUUIDs + })); + setPreLoadingBulk(false); + } + }); + }); + } setPermissions({ has_write_priv: true, }); - // Set the Source if Passed from URL - if(params.source_list){ - setPreLoadingBulk(true); - console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); - // Support comma-separated list of UUIDs - const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); - let ancestorData = []; - let fetchCount = 0; - ancestorUUIDs.forEach((uuidItem) => { - entity_api_get_entity(uuidItem) - .then((response) => { - let error = response?.data?.error ?? false; - console.debug('%c◉ entity_api_get_entity response ', 'color:#00ff7b', response, error); - if(!error && (response?.results?.entity_type !== "Collection")){ - console.debug('%c◉ error ', 'color:#00ff7b', error); - let passSource = {row: response?.results ? response.results : null}; - console.log("passSource",passSource) - ancestorData.push(passSource.row); - } - else if(!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample"){ - setSnackbarController({ - open: true, - message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, - status: "error" - }); - }else if(error){ - setSnackbarController({ - open: true, - message: `Sorry, There was an error selecting your source: ${error}`, - status: "error" - }); - }else{ - throw new Error(response) - } - }) - .catch((error) => { - console.debug("entity_api_get_entity ERROR", error); - setPageErrors(error); - }) - .finally(() => { - fetchCount++; - if (fetchCount === ancestorUUIDs.length) { - setSelectedBulkUUIDs(ancestorUUIDs); - setSelectedBulkData(assembleSourceAncestorData(ancestorData)); - handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); - setFormValues((prevValues) => ({ - ...prevValues, - direct_ancestor_uuids: ancestorUUIDs - })); - setPreLoadingBulk(false); - } - }); - }); - } } setLoading(false); // eslint-disable-next-line }, [uuid]); - const assembleSourceAncestorData = (source_uuids) =>{ - var dst=""; - source_uuids.forEach(function(row, index) { - dst=ubkg_api_generate_display_subtype(row); - console.debug("dst", dst); - source_uuids[index].display_subtype=toTitleCase(dst); - }); - return (source_uuids) - } + // assembleSourceAncestorData moved to BulkSelector const handleInputChange = useCallback((e) => { const { name, value } = e.target; diff --git a/src/src/components/ui/bulkSelector.jsx b/src/src/components/ui/bulkSelector.jsx index 02ff344c..2e4a0030 100644 --- a/src/src/components/ui/bulkSelector.jsx +++ b/src/src/components/ui/bulkSelector.jsx @@ -1,3 +1,4 @@ + import React, { useState, useEffect, useCallback } from "react"; import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; @@ -19,6 +20,8 @@ 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 { 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"; @@ -26,7 +29,6 @@ import SearchComponent from "../search/SearchComponent"; import { getPublishStatusColor } from "../../utils/badgeClasses"; import { FeedbackDialog } from "./formParts"; import { search_api_es_query_ids } from "../../service/search_api"; -import {ubkg_api_generate_display_subtype} from "../../service/ubkg_api"; export function BulkSelector({ dialogTitle = "Associated Dataset IDs", @@ -61,7 +63,8 @@ export function BulkSelector({ // Sync sourcesData with prop changes useEffect(() => { - setSourcesData(initialSourcesData); + let sources = assembleSourceAncestorData(initialSourcesData); + setSourcesData(sources); }, [initialSourcesData]); console.log("BulkSelector SOurces: ",initialSourcesData, sourcesData) @@ -136,6 +139,20 @@ export function BulkSelector({ 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 const handleInputUUIDs = useCallback((e) => { if (e) e.preventDefault(); From af6f84ea6efb1621d2270d240a8c0f104c7f86a6 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 24 Sep 2025 14:31:45 -0400 Subject: [PATCH 10/26] Move form field & source Prefil management into formParts --- src/src/components/newDataset.jsx | 83 ++++++----------------------- src/src/components/ui/formParts.jsx | 82 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 68 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index deb239f3..64795444 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -1,3 +1,4 @@ + import React, { useEffect, useState, useMemo, useCallback } from "react"; import LoadingButton from "@mui/lab/LoadingButton"; import { Typography } from "@mui/material"; @@ -20,6 +21,8 @@ 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 } from "../service/ingest_api"; +import { handleSourceListFromParams } from "./ui/formParts"; +import { prefillFormValuesFromUrl } from "./ui/formParts"; export const DatasetForm = (props) => { let navigate = useNavigate(); @@ -166,74 +169,18 @@ export const DatasetForm = (props) => { setPageErrors(error); }); } else { - let url = new URL(window.location.href); - let params = Object.fromEntries(url.searchParams.entries()); - if(Object.keys(params).length > 0){ - console.debug('%c◉ URL params ', 'color:#00ff7b', params); - setFormValues((prevValues) => ({ - ...prevValues, - ...params - })); - setSnackbarController({ - open: true, - message: "Passing Form values from URL parameters", - status: "success" - }); - } - // Set the Source if Passed from URL - if(params.source_list){ - setPreLoadingBulk(true); - console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); - // Support comma-separated list of UUIDs - const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); - let ancestorData = []; - let fetchCount = 0; - ancestorUUIDs.forEach((uuidItem) => { - entity_api_get_entity(uuidItem) - .then((response) => { - let error = response?.data?.error ?? false; - console.debug('%c◉ entity_api_get_entity response ', 'color:#00ff7b', response, error); - if(!error && (response?.results?.entity_type !== "Collection")){ - console.debug('%c◉ error ', 'color:#00ff7b', error); - let passSource = {row: response?.results ? response.results : null}; - console.log("passSource",passSource) - ancestorData.push(passSource.row); - } - else if(!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample"){ - setSnackbarController({ - open: true, - message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, - status: "error" - }); - }else if(error){ - setSnackbarController({ - open: true, - message: `Sorry, There was an error selecting your source: ${error}`, - status: "error" - }); - }else{ - throw new Error(response) - } - }) - .catch((error) => { - console.debug("entity_api_get_entity ERROR", error); - setPageErrors(error); - }) - .finally(() => { - fetchCount++; - if (fetchCount === ancestorUUIDs.length) { - setSelectedBulkUUIDs(ancestorUUIDs); - setSelectedBulkData(ancestorData); - handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); - setFormValues((prevValues) => ({ - ...prevValues, - direct_ancestor_uuids: ancestorUUIDs - })); - setPreLoadingBulk(false); - } - }); - }); - } + // Pre-fill form values from URL parameters + const params = prefillFormValuesFromUrl(setFormValues, setSnackbarController); + // Handle source_list from URL params + handleSourceListFromParams(params, { + setPreLoadingBulk, + setSnackbarController, + setSelectedBulkUUIDs, + setSelectedBulkData, + handleBulkSelectionChange, + setFormValues, + setPageErrors + }); setPermissions({ has_write_priv: true, }); diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 255219d5..fc930f47 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -34,6 +34,7 @@ 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 { entity_api_get_entity } from "../../service/entity_api"; // import {ingest_api_allowable_edit_states} from "../../service/ingest_api"; // import {entity_api_get_entity} from "../../service/entity_api"; @@ -138,6 +139,67 @@ export function badgeClass(status){ } } +export function handleSourceListFromParams(params, { + setPreLoadingBulk, + setSnackbarController, + setSelectedBulkUUIDs, + setSelectedBulkData, + handleBulkSelectionChange, + setFormValues, + setPageErrors + }) { + if (!params.source_list) return; + setPreLoadingBulk(true); + console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); + const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); + let ancestorData = []; + let fetchCount = 0; + ancestorUUIDs.forEach((uuidItem) => { + entity_api_get_entity(uuidItem) + .then((response) => { + let error = response?.data?.error ?? false; + console.debug('%c◉ entity_api_get_entity response ', 'color:#00ff7b', response, error); + if (!error && (response?.results?.entity_type !== "Collection")) { + console.debug('%c◉ error ', 'color:#00ff7b', error); + let passSource = { row: response?.results ? response.results : null }; + console.log("passSource", passSource); + ancestorData.push(passSource.row); + } else if (!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample") { + setSnackbarController({ + open: true, + message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, + status: "error" + }); + } else if (error) { + setSnackbarController({ + open: true, + message: `Sorry, There was an error selecting your source: ${error}`, + status: "error" + }); + } else { + throw new Error(response); + } + }) + .catch((error) => { + console.debug("entity_api_get_entity ERROR", error); + setPageErrors(error); + }) + .finally(() => { + fetchCount++; + if (fetchCount === ancestorUUIDs.length) { + setSelectedBulkUUIDs(ancestorUUIDs); + setSelectedBulkData(ancestorData); + handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); + setFormValues((prevValues) => ({ + ...prevValues, + direct_ancestor_uuids: ancestorUUIDs + })); + setPreLoadingBulk(false); + } + }); + }); +} + export function TaskAssignment({ uuid, permissions, @@ -217,6 +279,26 @@ export function renderUploadLink(entityData){ ) } +// Reusable helper to pre-fill form values from URL parameters +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; +} + function errorNote(){ return (<> From b65ec44dd4179a28e92fbe391741af38e6ee779a Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 24 Sep 2025 15:25:20 -0400 Subject: [PATCH 11/26] adds group selector to Dataset Field Management, Managed Group field rendering on edit/new --- src/src/components/newDataset.jsx | 50 +++--- .../ui/fields/DatasetFormFields.jsx | 33 ++-- src/src/components/ui/formParts.jsx | 146 ++++++++++-------- 3 files changed, 118 insertions(+), 111 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 64795444..573f49ff 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -9,8 +9,6 @@ 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 TextField from "@mui/material/TextField"; -import FormHelperText from '@mui/material/FormHelperText'; import Snackbar from '@mui/material/Snackbar'; import { useNavigate, useParams } from "react-router-dom"; import { BulkSelector } from "./ui/bulkSelector"; @@ -65,6 +63,7 @@ export const DatasetForm = (props) => { const allGroups = localStorage.getItem("allGroups") ? JSON.parse(localStorage.getItem("allGroups")) : []; + const { uuid } = useParams(); const formFields = useMemo(() => [ { id: "lab_dataset_id", @@ -100,12 +99,20 @@ export const DatasetForm = (props) => { helperText: "", required: true, type: "select", - writeEnabled: entityData?.uuid ? true : false, + 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 { uuid } = useParams(); const memoizedFormHeader = useMemo( () => , [uuid, entityData, permissions] @@ -131,12 +138,12 @@ export const DatasetForm = (props) => { dataset_info: entityData.dataset_info, contains_human_genetic_sequences: entityData.contains_human_genetic_sequences, dt_select: entityData.dataset_type, + group_uuid: entityData.group_uuid, }); // let formattedAncestors = assembleSourceAncestorData(entityData.direct_ancestors); setSelectedBulkUUIDs(entityData.direct_ancestors.map(obj => obj.uuid)); setSelectedBulkData(entityData.direct_ancestors); console.log("Entity Data:", entityData.direct_ancestors, selectedBulkData); - // 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); @@ -179,7 +186,7 @@ export const DatasetForm = (props) => { setSelectedBulkData, handleBulkSelectionChange, setFormValues, - setPageErrors + setPageErrors, }); setPermissions({ has_write_priv: true, @@ -254,10 +261,10 @@ export const DatasetForm = (props) => { setIsProcessing(true); let selectedUUIDs = selectedBulkData.map((obj) => obj.uuid); let cleanForm = { - lab_dataset_id:formValues.lab_dataset_id, - contains_human_genetic_sequences:formValues.contains_human_genetic_sequences === "yes" ? true : false, - description:formValues.description, - dataset_info:formValues.dataset_info, + lab_dataset_id: formValues.lab_dataset_id, + contains_human_genetic_sequences: formValues.contains_human_genetic_sequences === "yes" ? true : false, + description: formValues.description, + dataset_info: formValues.dataset_info, direct_ancestor_uuids: selectedUUIDs, }; console.debug('%c⭗ Data', 'color:#00ff7b',cleanForm ); @@ -299,7 +306,7 @@ export const DatasetForm = (props) => { } else { let group_uuid = formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid; cleanForm.dataset_type = formValues.dt_select; - cleanForm.group_uuid = formValues.group_uuid; + cleanForm.group_uuid = group_uuid; console.log(formValues, formValues.contains_human_genetic_sequences ) console.debug('%c◉ cleanForm ', 'color:#00ff7b', cleanForm); ingest_api_create_dataset(JSON.stringify(cleanForm)) @@ -433,27 +440,6 @@ export const DatasetForm = (props) => { allGroups={allGroups} /> )} - {!uuid && ( - - - Group - - - - - - )} {valErrorMessages && valErrorMessages.length > 0 && ( Please Review the following problems: diff --git a/src/src/components/ui/fields/DatasetFormFields.jsx b/src/src/components/ui/fields/DatasetFormFields.jsx index 8cce4d31..f65d288e 100644 --- a/src/src/components/ui/fields/DatasetFormFields.jsx +++ b/src/src/components/ui/fields/DatasetFormFields.jsx @@ -34,8 +34,7 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi multiline={field.type === "textarea" || field.multiline} minRows={field.rows || (field.type === "textarea" ? 4 : undefined)} disabled={permissions.has_write_priv === false} - required={field.required} - /> + required={field.required}/> ); } if (field.type === "radio") { @@ -63,14 +62,21 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi ); } - 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 ); + + 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]); + console.debug('%c◉ selectedGroup ', 'color:#00ff7b', selectedGroup, field.writeEnabled); } console.debug('%c◉ field.values ', 'color:#00ff7b',field.values); return ( @@ -80,12 +86,11 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi margin="normal" error={!!formErrors[field.id]} required={field.required} - disabled={permissions.has_write_priv === false} - > + disabled={permissions.has_write_priv === false}> {field.label} {!field.writeEnabled && ( - {formValues[field.id]} + {selectedGroup ? selectedGroup.label : formValues[field.id]} )} {field.writeEnabled && ( @@ -106,7 +111,7 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi {formErrors[field.id] || field.helperText} ); - } + return null; })} diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index fc930f47..7c51505f 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -36,10 +36,6 @@ import TextField from "@mui/material/TextField"; import FormHelperText from "@mui/material/FormHelperText"; import { entity_api_get_entity } from "../../service/entity_api"; -// 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; - export const FormHeader = (props) => { let entityData = props.entityData; let details = (props.entityData[0]!=="new") ? `${entityData.entity_type}: ${entityData.hubmap_id}` : `New ${props.entityData[1]}`; @@ -139,67 +135,6 @@ export function badgeClass(status){ } } -export function handleSourceListFromParams(params, { - setPreLoadingBulk, - setSnackbarController, - setSelectedBulkUUIDs, - setSelectedBulkData, - handleBulkSelectionChange, - setFormValues, - setPageErrors - }) { - if (!params.source_list) return; - setPreLoadingBulk(true); - console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); - const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); - let ancestorData = []; - let fetchCount = 0; - ancestorUUIDs.forEach((uuidItem) => { - entity_api_get_entity(uuidItem) - .then((response) => { - let error = response?.data?.error ?? false; - console.debug('%c◉ entity_api_get_entity response ', 'color:#00ff7b', response, error); - if (!error && (response?.results?.entity_type !== "Collection")) { - console.debug('%c◉ error ', 'color:#00ff7b', error); - let passSource = { row: response?.results ? response.results : null }; - console.log("passSource", passSource); - ancestorData.push(passSource.row); - } else if (!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample") { - setSnackbarController({ - open: true, - message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, - status: "error" - }); - } else if (error) { - setSnackbarController({ - open: true, - message: `Sorry, There was an error selecting your source: ${error}`, - status: "error" - }); - } else { - throw new Error(response); - } - }) - .catch((error) => { - console.debug("entity_api_get_entity ERROR", error); - setPageErrors(error); - }) - .finally(() => { - fetchCount++; - if (fetchCount === ancestorUUIDs.length) { - setSelectedBulkUUIDs(ancestorUUIDs); - setSelectedBulkData(ancestorData); - handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); - setFormValues((prevValues) => ({ - ...prevValues, - direct_ancestor_uuids: ancestorUUIDs - })); - setPreLoadingBulk(false); - } - }); - }); -} - export function TaskAssignment({ uuid, permissions, @@ -299,6 +234,87 @@ export function prefillFormValuesFromUrl(setFormValues, setSnackbarController) { return params; } +export function handleSourceListFromParams(params, { + setPreLoadingBulk, + setSnackbarController, + setSelectedBulkUUIDs, + setSelectedBulkData, + handleBulkSelectionChange, + setFormValues, + setPageErrors, + restrictions + }) { + if (!params.source_list) return; + setPreLoadingBulk(true); + console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); + const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); + let ancestorData = []; + let fetchCount = 0; + let errorSet = [] + ancestorUUIDs.forEach((uuidItem) => { + entity_api_get_entity(uuidItem) + .then((response) => { + let error = response?.data?.error ?? false; + console.debug('%c◉ Ancestor Prepop entity_api_get_entity response ', 'color:#00ff7b', response, error); + // Restriction time, + if(restrictions){ + console.log("Rest Al",restrictions.allowedTypes.includes(response?.results?.entity_type)) + let blockedType = restrictions.blockedTypes.includes(response?.results?.entity_type) ? true : false; // blocked includes Result type + let allowedType = restrictions.allowedTypes.includes(response?.results?.entity_type) ? true : false; // allowed includes Result type + console.debug('%c◉ REST SET ', 'color:#00ff7b', blockedType, allowedType); + if(!allowedType || blockedType){ + errorSet.push(`The entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source type`); + } + } + if (!error && (response?.results?.entity_type !== "Collection")) { + console.debug('%c◉ error ', 'color:#00ff7b', error); + let passSource = { row: response?.results ? response.results : null }; + console.log("passSource", passSource); + ancestorData.push(passSource.row); + } else if (!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample") { + setSnackbarController({ + open: true, + message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, + status: "error" + }); + } else if (error) { + setSnackbarController({ + open: true, + message: `Sorry, There was an error selecting your source: ${error}`, + status: "error" + }); + } else { + throw new Error(response); + } + if(errorSet.length>0){ + setSnackbarController({ + open: true, + message: errorSet.join(" ; "), + status: "error" + }); + } + }) + .catch((error) => { + console.debug("entity_api_get_entity ERROR", error); + setPageErrors(error); + }) + .finally(() => { + fetchCount++; + if (fetchCount === ancestorUUIDs.length) { + setSelectedBulkUUIDs(ancestorUUIDs); + setSelectedBulkData(ancestorData); + handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); + setFormValues((prevValues) => ({ + ...prevValues, + direct_ancestor_uuids: ancestorUUIDs + })); + setPreLoadingBulk(false); + } + }); + }); +} + + function errorNote(){ return (<> From 1c68d1cc34f9ff22ebcecbbcb885bd330a38772b Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 26 Sep 2025 10:40:37 -0400 Subject: [PATCH 12/26] Add Group Name to Form Header --- src/src/components/ui/formParts.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 7c51505f..9a888c46 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -373,6 +373,7 @@ function topHeader(entityData){ Priority Projects: {buildPriorityProjectList(entityData.priority_project_list)} )} Entered by: {entityData.created_by_user_email} + Group: {entityData.group_name} {(entityData.entity_type === "Donor" || entityData.entity_type ==="Sample") && ( Submission ID: {entityData.submission_id} )} From 5be0801e3b6a05be1052d1ec4da8cd75bd5e399c Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 26 Sep 2025 11:11:12 -0400 Subject: [PATCH 13/26] Simplify & Centralize state objects --- src/src/components/newDataset.jsx | 223 +++++++----------- .../ui/fields/DatasetFormFields.jsx | 40 ++-- 2 files changed, 109 insertions(+), 154 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 573f49ff..7f32b687 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -25,44 +25,39 @@ import { prefillFormValuesFromUrl } from "./ui/formParts"; export const DatasetForm = (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 [readOnlySources, setReadOnlySources] = useState(false); - let [preLoadingBulk, setPreLoadingBulk] = useState(false); - - let [permissions, setPermissions] = useState({ - has_admin_priv: false, - has_publish_priv: false, - has_submit_priv: false, - has_write_priv: false - }); - let [buttonLoading, setButtonLoading] = useState({ - process: false, - save: false, - submit: false, + const [entityData, setEntityData] = useState(); + const [loading, setLoading] = useState({ + page: true, + processing: false, + bulk: false, + button: { process: false, save: false, submit: false } }); - let [formValues, setFormValues] = useState({ + const [form, setForm] = useState({ lab_dataset_id: "", description: "", dataset_info: "", contains_human_genetic_sequences: "", dt_select: "", direct_ancestor_uuids: [], + group_uuid: "" + }); + const [formErrors, setFormErrors] = useState({}); + const [errorMessages, setErrorMessages] = useState([]); + const [pageErrors, setPageErrors] = useState(null); + const [readOnlySources, setReadOnlySources] = useState(false); + const [permissions, setPermissions] = useState({ + has_admin_priv: false, + has_publish_priv: false, + has_submit_priv: false, + has_write_priv: false }); - let [formErrors, setFormErrors] = useState({ ...formValues }); - let [selectedBulkUUIDs, setSelectedBulkUUIDs] = useState([]); - let [selectedBulkData, setSelectedBulkData] = useState([]); - let [snackbarController, setSnackbarController] = useState({ + const [bulkSelection, setBulkSelection] = useState({ uuids: [], data: [] }); + const [snackbarController, setSnackbarController] = useState({ open: false, - message: "", + message: "", status: "info" }); - const allGroups = localStorage.getItem("allGroups") ? JSON.parse(localStorage.getItem("allGroups")) : []; - const { uuid } = useParams(); const formFields = useMemo(() => [ { @@ -113,12 +108,10 @@ export const DatasetForm = (props) => { } ], []); - const memoizedFormHeader = useMemo( () => , [uuid, entityData, permissions] ); - useEffect(() => { if (uuid && uuid !== "") { entity_api_get_entity(uuid) @@ -132,37 +125,33 @@ export const DatasetForm = (props) => { } else { const entityData = response.results; setEntityData(entityData); - setFormValues({ + 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) + }); + setBulkSelection({ + uuids: entityData.direct_ancestors.map(obj => obj.uuid), + data: entityData.direct_ancestors }); - // let formattedAncestors = assembleSourceAncestorData(entityData.direct_ancestors); - setSelectedBulkUUIDs(entityData.direct_ancestors.map(obj => obj.uuid)); - setSelectedBulkData(entityData.direct_ancestors); - console.log("Entity Data:", entityData.direct_ancestors, selectedBulkData); // 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(uuid) .then((response) => { - console.debug('%c◉ ingest_api_allowable_edit_states Permissions', 'color:#00ff7b', response.results); if (entityData.data_access_level === "public") { setReadOnlySources(true); - setPermissions({ - has_write_priv: false, - }); + setPermissions({ has_write_priv: false }); } if(response.results.has_write_priv === false){ setReadOnlySources(true); } setPermissions(response.results); - }) .catch((error) => { setPageErrors(error); @@ -176,138 +165,108 @@ export const DatasetForm = (props) => { setPageErrors(error); }); } else { - // Pre-fill form values from URL parameters - const params = prefillFormValuesFromUrl(setFormValues, setSnackbarController); + // Pre-fill form values from URL parameters + const params = prefillFormValuesFromUrl(setForm, setSnackbarController); // Handle source_list from URL params handleSourceListFromParams(params, { - setPreLoadingBulk, + setPreLoadingBulk: (v) => setLoading(l => ({ ...l, bulk: v })), setSnackbarController, - setSelectedBulkUUIDs, - setSelectedBulkData, - handleBulkSelectionChange, - setFormValues, - setPageErrors, - }); - setPermissions({ - has_write_priv: true, + setSelectedBulkUUIDs: (uuids) => setBulkSelection(sel => ({ ...sel, uuids })), + setSelectedBulkData: (data) => setBulkSelection(sel => ({ ...sel, data })), + handleBulkSelectionChange: (uuids, hids, string, data) => setBulkSelection({ uuids, data }), + setFormValues: setForm, + setPageErrors }); + setPermissions({ has_write_priv: true }); } - setLoading(false); - // eslint-disable-next-line + setLoading(l => ({ ...l, page: false })); }, [uuid]); - // assembleSourceAncestorData moved to BulkSelector - const handleInputChange = useCallback((e) => { const { name, value } = e.target; - setFormValues(prev => { + setForm(prev => { if (prev[name] === value) return prev; return { ...prev, [name]: value }; }); - console.debug('%c◉ handleInputChange', 'color:#00ff7b',name, value ); + console.debug('%c◉ handleInputChange', 'color:#00ff7b', name, value); }, []); // Callback for BulkSelector const handleBulkSelectionChange = (uuids, hids, string, data) => { - setFormValues(prev => ({ + setForm(prev => ({ ...prev, direct_ancestor_uuids: uuids })); - setSelectedBulkUUIDs(uuids); - setSelectedBulkData(data); + setBulkSelection({ uuids, data }); }; const validateForm = () => { - setValErrorMessages(null); + setErrorMessages([]); let errors = 0; let e_messages = []; - let requiredFields = ["lab_dataset_id", "description", "contains_human_genetic_sequences", "dt_select"]; + let requiredFields = ["lab_dataset_id", "description", "contains_human_genetic_sequences", "dt_select", "group_uuid"]; + let newFormErrors = {}; for (let field of requiredFields) { - if (!validateRequired(formValues[field])) { + if (!validateRequired(form[field])) { let fieldName = formFields.find(f => f.id === field)?.label || humanize(field); e_messages.push(fieldName + " is a required field"); - setFormErrors((prevValues) => ({ - ...prevValues, - [field]: " Required", - })); + newFormErrors[field] = " Required"; errors++; } else { - setFormErrors((prevValues) => ({ - ...prevValues, - [field]: "", - })); + newFormErrors[field] = ""; } } - if (!selectedBulkData || selectedBulkData.length <= 0) { + if (!bulkSelection.data || bulkSelection.data.length <= 0) { e_messages.push("Please select at least one Source"); errors++; - setFormErrors((prevValues) => ({ - ...prevValues, - ["direct_ancestor_uuids"]: "Required", - })); - } else if (selectedBulkData.length > 0 && formValues['direct_ancestor_uuids'].length <= 0) { - setFormValues((prevValues) => ({ - ...prevValues, - 'direct_ancestor_uuids': selectedBulkData.map(obj => obj.uuid), + 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), })); } - setValErrorMessages(errors > 0 ? e_messages : null); + setFormErrors(newFormErrors); + setErrorMessages(errors > 0 ? e_messages : []); return errors === 0; }; const handleSubmit = (e) => { e.preventDefault(); if (validateForm()) { - setIsProcessing(true); - let selectedUUIDs = selectedBulkData.map((obj) => obj.uuid); + setLoading(l => ({ ...l, processing: true })); + let selectedUUIDs = bulkSelection.data.map((obj) => obj.uuid); let cleanForm = { - lab_dataset_id: formValues.lab_dataset_id, - contains_human_genetic_sequences: formValues.contains_human_genetic_sequences === "yes" ? true : false, - description: formValues.description, - dataset_info: formValues.dataset_info, + 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, + dataset_type: form.dt_select, + group_uuid: form.group_uuid }; - console.debug('%c⭗ Data', 'color:#00ff7b',cleanForm ); - // if(this.state.has_admin_priv){ - // console.debug('%c⊙', 'color:#8b1fff', this.state.assigned_to_group_name, this.state.ingest_task ); - // if (this.state.assigned_to_group_name && this.state.assigned_to_group_name.length > 0){ - // data["assigned_to_group_name"]=this.state.assigned_to_group_name; - // } - // if (this.state.ingest_task && this.state.ingest_task.length > 0){ - // data["ingest_task"]=this.state.ingest_task; - // } - // } - console.debug('%c⭗ Data', 'color:#00ff7b',cleanForm); + console.debug('%c⭗ Data', 'color:#00ff7b', cleanForm); if (uuid) { let target = e.target.name; - setButtonLoading((prev) => ({ - ...prev, - [target]: true, - })); + setLoading(l => ({ ...l, button: { ...l.button, [target]: true } })); entity_api_update_entity(uuid, JSON.stringify(cleanForm)) .then((response) => { if (response.status < 300) { props.onUpdated(response.results); } else { setPageErrors(response); - setButtonLoading((prev) => ({ - ...prev, - [target]: false, - })); + setLoading(l => ({ ...l, button: { ...l.button, [target]: false } })); } }) .catch((error) => { setPageErrors(error); - setButtonLoading((prev) => ({ - ...prev, - [target]: false, - })); + setLoading(l => ({ ...l, button: { ...l.button, [target]: false } })); }); } else { - let group_uuid = formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid; - cleanForm.dataset_type = formValues.dt_select; + // 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; - console.log(formValues, formValues.contains_human_genetic_sequences ) + console.log(form, form.contains_human_genetic_sequences); console.debug('%c◉ cleanForm ', 'color:#00ff7b', cleanForm); ingest_api_create_dataset(JSON.stringify(cleanForm)) .then((response) => { @@ -319,20 +278,11 @@ export const DatasetForm = (props) => { }) .catch((error) => { setPageErrors(error); - setButtonLoading(() => ({ - process: false, - save: false, - submit: false, - })); + setLoading(l => ({ ...l, button: { process: false, save: false, submit: false }, processing: false })); }); } - } else { - setButtonLoading(() => ({ - process: false, - save: false, - submit: false, - })); + setLoading(l => ({ ...l, button: { process: false, save: false, submit: false }, processing: false })); } }; @@ -354,7 +304,7 @@ export const DatasetForm = (props) => { handleSubmit(e)} type="submit"> @@ -364,7 +314,7 @@ export const DatasetForm = (props) => { {uuid && uuid.length > 0 && permissions.has_admin_priv && ( handleSubmit(e)} variant="contained" @@ -374,7 +324,7 @@ export const DatasetForm = (props) => { )} {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status !== "new" && ( handleSubmit(e)} name="submit" variant="contained" @@ -385,7 +335,7 @@ export const DatasetForm = (props) => { )} {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status !== "published" && ( handleSubmit(e)} variant="contained" @@ -398,7 +348,7 @@ export const DatasetForm = (props) => { ); }; - if (isLoading || ((!entityData || !formValues) && uuid)) { + if (loading.page || ((!entityData || !form) && uuid)) { return (); } else { return ( @@ -409,8 +359,8 @@ export const DatasetForm = (props) => { handleSubmit(e)}> { blacklist: ['collection'] }} readOnly={readOnlySources} - preLoad = {preLoadingBulk} + preLoad={loading.bulk} /> {/* TASK ASSIGNMENT */} {uuid && ( @@ -434,16 +384,16 @@ export const DatasetForm = (props) => { uuid={uuid} permissions={permissions} entityData={entityData} - formValues={formValues} + formValues={form} formErrors={formErrors} handleInputChange={handleInputChange} allGroups={allGroups} /> )} - {valErrorMessages && valErrorMessages.length > 0 && ( + {errorMessages && errorMessages.length > 0 && ( Please Review the following problems: - {valErrorMessages.map(error => ( + {errorMessages.map(error => ( {error} @@ -457,7 +407,6 @@ export const DatasetForm = (props) => { Error: {JSON.stringify(pageErrors)} )} - {/* Snackbar Feedback*/} { - +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 ( + required={field.required} + sx={errorStyle} + /> ); } if (field.type === "radio") { @@ -43,10 +46,11 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi key={field.id} component="fieldset" margin="normal" - error={!!formErrors[field.id]} + error={!!error} required={field.required} disabled={permissions.has_write_priv === false} fullWidth + sx={errorStyle} > {field.label} } label="Yes" /> } label="No" /> - {formErrors[field.id] || field.helperText} + {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 })) : [] @@ -76,21 +81,20 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi let selectedGroup = null; if (field.id === "group_uuid" && !field.writeEnabled) { selectedGroup = field.values.find(v => v.value === formValues[field.id]); - console.debug('%c◉ selectedGroup ', 'color:#00ff7b', selectedGroup, field.writeEnabled); } - console.debug('%c◉ field.values ', 'color:#00ff7b',field.values); return ( + disabled={permissions.has_write_priv === false} + sx={errorStyle}> {field.label} {!field.writeEnabled && ( - {selectedGroup ? selectedGroup.label : formValues[field.id]} + {selectedGroup ? selectedGroup.label : formValues[field.id]} )} {field.writeEnabled && ( @@ -108,12 +112,14 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi ))} )} - {formErrors[field.id] || field.helperText} + {error ? error : field.helperText} ); - + + } return null; - })} + } + )} ); }; From 6ed96022e5d8c6836964230d5834b1e9452fa6ee Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 26 Sep 2025 12:27:50 -0400 Subject: [PATCH 14/26] Bulk Selector now honors URL Source List --- src/src/components/ui/bulkSelector.jsx | 162 +++++++++++++++---------- 1 file changed, 101 insertions(+), 61 deletions(-) diff --git a/src/src/components/ui/bulkSelector.jsx b/src/src/components/ui/bulkSelector.jsx index 2e4a0030..fb8cbb2d 100644 --- a/src/src/components/ui/bulkSelector.jsx +++ b/src/src/components/ui/bulkSelector.jsx @@ -39,7 +39,7 @@ export function BulkSelector({ initialSelectedString = "", initialSourcesData = [], onBulkSelectionChange, - searchFilters, + searchFilters, readOnly, preLoad, }) { @@ -81,25 +81,36 @@ export function BulkSelector({ 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()); - // Warnings: duplicated strings - let seen = new Set(); - let duplicates = new Set(); + // Detect duplicates: count occurrences + let idCounts = {}; for (let id of originalString) { - if (seen.has(id)) { - duplicates.add(id); - } else { - seen.add(id); - } + if (!id) continue; + idCounts[id] = (idCounts[id] || 0) + 1; } - duplicates = Array.from(duplicates); + // 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( @@ -123,12 +134,24 @@ export function BulkSelector({ 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 + // Type check and only add unique entities to goodArray + let addedIds = new Set(); for (let entity of results) { - if (entity.entity_type !== "Dataset") { + // 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; + if ( + (searchFilters.blacklist && searchFilters.blacklist.includes(entity.entity_type)) || + (searchFilters.whitelist && !searchFilters.whitelist.includes(entity.entity_type)) || + (searchFilters.restrictions && !searchFilters.restrictions.includes(entity.entity_type)) + ) { typeArray.push(`${entity.hubmap_id} (Invalid Type: ${entity.entity_type})`); - } else { goodArray.push(entity); } + } 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]); } @@ -154,58 +177,74 @@ export function BulkSelector({ } // Handle bulk input dialog update - const handleInputUUIDs = useCallback((e) => { + // Modified handleInputUUIDs to accept an optional overrideString (e.g. from URL) + const handleInputUUIDs = useCallback((e, overrideString) => { if (e) e.preventDefault(); setSourceTableError(false); - if (!showHIDList) { - setShowHIDList(true); - setStringIDs(selected_HIDs.join(", ")); - setSourceBulkStatus("Waiting for Input..."); + // 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 { - setShowHIDList(false); - setSourceBulkStatus("loading"); - let cleanList = Array.from(new Set( - stringIDs - .split(",") - .map(s => s.trim()) - .filter(s => s.length > 0) - )); - if (stringIDs.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 { - let validatedSources = preValidateSources(response.results, cleanList); - 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"]]]); + 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]); @@ -345,6 +384,7 @@ export function BulkSelector({ } let totalRejected = totalWarnings + totalErrors; + console.debug('%c◉ searchFilters.restrictions ', 'color:#00ff7b', searchFilters, searchFilters.blacklist); return (<> {/* Search Dialog */} Date: Fri, 26 Sep 2025 15:15:48 -0400 Subject: [PATCH 15/26] Cleans out redundant fucntions on formHelpers, Adds documenting comment for what's what --- src/src/components/ui/formParts.jsx | 241 +++++++++++----------------- 1 file changed, 90 insertions(+), 151 deletions(-) diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 9a888c46..1e5558c7 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -36,6 +36,7 @@ import TextField from "@mui/material/TextField"; import FormHelperText from "@mui/material/FormHelperText"; import { entity_api_get_entity } from "../../service/entity_api"; +// 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]}`; @@ -51,6 +52,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 +79,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,6 +138,7 @@ export function badgeClass(status){ } } +// Admin Tool for Assigning Tasks to Groups to Entities export function TaskAssignment({ uuid, permissions, @@ -194,6 +198,7 @@ export function TaskAssignment({ ); } +// Returns a styalized Globus Link Button export function renderUploadLink(entityData){ function handleUploadSelect(e, uuid){ window.location.assign(`/upload/${uuid}`,); @@ -215,6 +220,7 @@ export function renderUploadLink(entityData){ } // 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()); @@ -234,87 +240,56 @@ export function prefillFormValuesFromUrl(setFormValues, setSnackbarController) { return params; } -export function handleSourceListFromParams(params, { - setPreLoadingBulk, - setSnackbarController, - setSelectedBulkUUIDs, - setSelectedBulkData, - handleBulkSelectionChange, - setFormValues, - setPageErrors, - restrictions - }) { - if (!params.source_list) return; - setPreLoadingBulk(true); - console.debug('%c◉ params.source_list setPreLoadingBulk TRUEW', 'color:#00ff7b', params.source_list); - const ancestorUUIDs = params.source_list.split(',').map(s => s.trim()).filter(Boolean); - let ancestorData = []; - let fetchCount = 0; - let errorSet = [] - ancestorUUIDs.forEach((uuidItem) => { - entity_api_get_entity(uuidItem) - .then((response) => { - let error = response?.data?.error ?? false; - console.debug('%c◉ Ancestor Prepop entity_api_get_entity response ', 'color:#00ff7b', response, error); - // Restriction time, - if(restrictions){ - console.log("Rest Al",restrictions.allowedTypes.includes(response?.results?.entity_type)) - let blockedType = restrictions.blockedTypes.includes(response?.results?.entity_type) ? true : false; // blocked includes Result type - let allowedType = restrictions.allowedTypes.includes(response?.results?.entity_type) ? true : false; // allowed includes Result type - console.debug('%c◉ REST SET ', 'color:#00ff7b', blockedType, allowedType); - if(!allowedType || blockedType){ - errorSet.push(`The entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source type`); - } - } - if (!error && (response?.results?.entity_type !== "Collection")) { - console.debug('%c◉ error ', 'color:#00ff7b', error); - let passSource = { row: response?.results ? response.results : null }; - console.log("passSource", passSource); - ancestorData.push(passSource.row); - } else if (!error && response?.results?.entity_type === "Donor" && response.results.entity_type !== "Sample") { - setSnackbarController({ - open: true, - message: `Sorry, the entity ${response.results.hubmap_id} (${response.results.entity_type}) is not a valid Source (Must not be a Collection) `, - status: "error" - }); - } else if (error) { - setSnackbarController({ - open: true, - message: `Sorry, There was an error selecting your source: ${error}`, - status: "error" - }); - } else { - throw new Error(response); - } - if(errorSet.length>0){ - setSnackbarController({ - open: true, - message: errorSet.join(" ; "), - status: "error" - }); - } - }) - .catch((error) => { - console.debug("entity_api_get_entity ERROR", error); - setPageErrors(error); - }) - .finally(() => { - fetchCount++; - if (fetchCount === ancestorUUIDs.length) { - setSelectedBulkUUIDs(ancestorUUIDs); - setSelectedBulkData(ancestorData); - handleBulkSelectionChange(ancestorUUIDs, [], "", ancestorData); - setFormValues((prevValues) => ({ - ...prevValues, - direct_ancestor_uuids: ancestorUUIDs - })); - setPreLoadingBulk(false); - } - }); - }); +// 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 (<> @@ -322,6 +297,8 @@ function errorNote(){ ) } + +// Styalized Footnote component for FeedbackDialog function noteWrap(note){ return ( @@ -329,11 +306,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 = { @@ -350,6 +331,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(", "); @@ -357,6 +340,8 @@ function buildPriorityProjectList(list){ return list[0] } } + +// The TopLeftmost part of the Form Header function topHeader(entityData){ if(entityData[0] !== "new"){ return ( @@ -370,7 +355,11 @@ function topHeader(entityData){ Status: {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} @@ -401,6 +390,8 @@ function topHeader(entityData){ ) } } + +// The Rightmost part of the Form Header function infoPanels(entityData,permissions,globusURL){ return ( @@ -460,6 +451,9 @@ function infoPanels(entityData,permissions,globusURL){ ) } + +// Looks at the Bulk Selector Table and returns an array of all Hubmap IDs +// Used in HandleCopyFormUrl to populate source_list function getHubmapIDsFromBulkTable() { const wrapper = document.getElementById('bulkTableWrapper'); if (!wrapper) return []; @@ -471,7 +465,8 @@ function getHubmapIDsFromBulkTable() { 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){ @@ -493,28 +488,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){ @@ -525,12 +500,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" , @@ -543,7 +517,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"))) @@ -564,8 +537,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(); @@ -581,32 +554,7 @@ 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"); @@ -635,14 +583,12 @@ export function HandleCopyFormUrl(e) { }); } +// 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: 'Create Dataset', action: (e) => navigate(`/new/datasetAdmin`) }, - // { icon: , name: 'Share' }, ]; const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(true); @@ -667,16 +613,11 @@ export function SpeedDialTooltipOpen() { /> ))} - {/* e.setShowSnack(false)} - message={e.snackMessage} - /> */} ); } +// Returns a Feedback Dialog Modal for displaying Warnings, Errors, etc export function FeedbackDialog( { showMessage, setShowMessage, @@ -686,7 +627,7 @@ export function FeedbackDialog( { note, color, icon -} ){ + } ){ let messageColor = color ? color : "#444A65"; let altColorLight = LightenHex(messageColor, 20); let altColorDark = DarkenHex(messageColor, 20); @@ -763,11 +704,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) )} @@ -793,9 +732,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; @@ -822,7 +761,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(){ @@ -852,8 +791,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); @@ -875,7 +814,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)); @@ -892,12 +831,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); From 6ab363078ea474d08d5612f59324291cc1777925 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 26 Sep 2025 15:25:21 -0400 Subject: [PATCH 16/26] Remove unused imports, fix spacing in new form header text --- src/src/components/ui/formParts.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 1e5558c7..539524b3 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -19,7 +19,6 @@ 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'; @@ -34,7 +33,6 @@ 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 { entity_api_get_entity } from "../../service/entity_api"; // The header on all of the Forms (The top bit) export const FormHeader = (props) => { @@ -286,9 +284,6 @@ export function GroupModal ({ ); } - - - // Styalized snackbar component rendering Error Notes for FeedbackDialog function errorNote(){ return (<> @@ -347,7 +342,7 @@ function topHeader(entityData){ return ( -

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

+

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

HuBMAP ID: {entityData.hubmap_id} From d8889794e4aa34e3201b3cab32dff2679bbbcc62 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 26 Sep 2025 15:26:36 -0400 Subject: [PATCH 17/26] Remove Source List URL Handling (Moved into BulkSelector itself), fixes casing on bulk blacklist --- src/src/components/newDataset.jsx | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 7f32b687..e0f24451 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -6,20 +6,17 @@ 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 InputLabel from "@mui/material/InputLabel"; import LinearProgress from "@mui/material/LinearProgress"; -import NativeSelect from '@mui/material/NativeSelect'; import Snackbar from '@mui/material/Snackbar'; import { useNavigate, useParams } from "react-router-dom"; import { BulkSelector } from "./ui/bulkSelector"; -import { FormHeader, UserGroupSelectMenuPatch,TaskAssignment } from "./ui/formParts"; +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 } from "../service/ingest_api"; -import { handleSourceListFromParams } from "./ui/formParts"; import { prefillFormValuesFromUrl } from "./ui/formParts"; export const DatasetForm = (props) => { @@ -59,8 +56,7 @@ export const DatasetForm = (props) => { }); const allGroups = localStorage.getItem("allGroups") ? JSON.parse(localStorage.getItem("allGroups")) : []; const { uuid } = useParams(); - const formFields = useMemo(() => [ - { + const formFields = useMemo(() => [{ id: "lab_dataset_id", label: "Lab Name or ID", helperText: "Lab Name or ID", @@ -166,17 +162,7 @@ export const DatasetForm = (props) => { }); } else { // Pre-fill form values from URL parameters - const params = prefillFormValuesFromUrl(setForm, setSnackbarController); - // Handle source_list from URL params - handleSourceListFromParams(params, { - setPreLoadingBulk: (v) => setLoading(l => ({ ...l, bulk: v })), - setSnackbarController, - setSelectedBulkUUIDs: (uuids) => setBulkSelection(sel => ({ ...sel, uuids })), - setSelectedBulkData: (data) => setBulkSelection(sel => ({ ...sel, data })), - handleBulkSelectionChange: (uuids, hids, string, data) => setBulkSelection({ uuids, data }), - setFormValues: setForm, - setPageErrors - }); + prefillFormValuesFromUrl(setForm, setSnackbarController); setPermissions({ has_write_priv: true }); } setLoading(l => ({ ...l, page: false })); @@ -365,7 +351,7 @@ export const DatasetForm = (props) => { searchFilters={{ custom_title: "Search for a Source ID for your Dataset", custom_subtitle: "Collections may not be selected for Dataset sources", - blacklist: ['collection'] + blacklist: ['Collection'] }} readOnly={readOnlySources} preLoad={loading.bulk} From 9fbc12b6cae28fad3905dfcbab498ea1656db2af Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 26 Sep 2025 15:33:59 -0400 Subject: [PATCH 18/26] Moves Publications onto updates userGroup Select (Was using duplicated function), switches Collection and Epicollection Preload to finally use localStorage so we can end Re-rendering from App props --- src/src/App.js | 18 +++++++++--------- src/src/components/collections.jsx | 2 +- src/src/components/collections/collections.jsx | 2 +- src/src/components/epicollections.jsx | 2 +- src/src/components/newPublication.jsx | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/src/App.js b/src/src/App.js index 40e8078b..25718aeb 100644 --- a/src/src/App.js +++ b/src/src/App.js @@ -68,8 +68,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 @@ -132,8 +132,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) @@ -528,8 +528,8 @@ 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)}/>}/> @@ -544,11 +544,11 @@ export function App(props){ 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" />} /> } /> } /> 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/newPublication.jsx b/src/src/components/newPublication.jsx index dd38229f..f8eeaed4 100644 --- a/src/src/components/newPublication.jsx +++ b/src/src/components/newPublication.jsx @@ -27,7 +27,7 @@ import { validateSingleProtocolIODOI } from "../utils/validators"; import { BulkSelector } from "./ui/bulkSelector"; -import { FormHeader, UserGroupSelectMenuPatch } from "./ui/formParts"; +import { FormHeader, UserGroupSelectMenu } from "./ui/formParts"; import { PublicationFormFields } from "./ui/fields/PublicationFormFields"; export const PublicationForm = (props) => { @@ -145,8 +145,8 @@ export const PublicationForm = (props) => { const { uuid } = useParams(); - const memoizedUserGroupSelectMenuPatch = React.useMemo( - () => , + const memoizedUserGroupSelectMenu = React.useMemo( + () => , [] ); @@ -561,7 +561,7 @@ export const PublicationForm = (props) => { }} disabled={uuid ? true : false} value={formValues["group_uuid"] ? formValues["group_uuid"].value : JSON.parse(localStorage.getItem("userGroups"))[0].uuid}> - {memoizedUserGroupSelectMenuPatch} + {memoizedUserGroupSelectMenu} )} From ad12d144a98248427eab2d5711beb2d6ca292334 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Mon, 29 Sep 2025 14:05:08 -0400 Subject: [PATCH 19/26] Fix Value setting of gene sequence radio, Properly capture error on Entity Validation api call, sorts out actions for each action/button in the Button Engine, adds 404 page on trying to load an entity that's not found in neo4j, clean up routes, update axios options config for ingest_api_pipeline_test_submit, fix language around validation message/status responses for Uploads (to stay in line with datasets), fix required validation on boolean fields --- src/src/App.js | 10 +- src/src/components/404.jsx | 81 ++++++ src/src/components/newDataset.jsx | 234 +++++++++++++++--- src/src/components/newUpload.jsx | 32 ++- .../ui/fields/DatasetFormFields.jsx | 7 +- src/src/service/ingest_api.js | 5 +- src/src/utils/validators.jsx | 11 +- 7 files changed, 322 insertions(+), 58 deletions(-) create mode 100644 src/src/components/404.jsx diff --git a/src/src/App.js b/src/src/App.js index 25718aeb..548c24c1 100644 --- a/src/src/App.js +++ b/src/src/App.js @@ -52,6 +52,8 @@ 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(); // @todo: trim how many need to actually be hooks / work with the state @@ -545,13 +547,15 @@ 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" />} /> } /> } /> + } /> }/> @@ -559,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/newDataset.jsx b/src/src/components/newDataset.jsx index e0f24451..619dc953 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -16,20 +16,27 @@ 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 } from "../service/ingest_api"; -import { prefillFormValuesFromUrl } from "./ui/formParts"; +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"; +import {LensTwoTone} from "@mui/icons-material"; export const DatasetForm = (props) => { let navigate = useNavigate(); - const [entityData, setEntityData] = useState(); - const [loading, setLoading] = useState({ + let [entityData, setEntityData] = useState(); + let [loading, setLoading] = useState({ page: true, processing: false, bulk: false, - button: { process: false, save: false, submit: false } + button: { process: false, save: false, submit: false, validate: false } }); - const [form, setForm] = useState({ + let [form, setForm] = useState({ lab_dataset_id: "", description: "", dataset_info: "", @@ -38,18 +45,22 @@ export const DatasetForm = (props) => { direct_ancestor_uuids: [], group_uuid: "" }); - const [formErrors, setFormErrors] = useState({}); - const [errorMessages, setErrorMessages] = useState([]); - const [pageErrors, setPageErrors] = useState(null); - const [readOnlySources, setReadOnlySources] = useState(false); - const [permissions, setPermissions] = useState({ + 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 }); - const [bulkSelection, setBulkSelection] = useState({ uuids: [], data: [] }); - const [snackbarController, setSnackbarController] = useState({ + let [bulkSelection, setBulkSelection] = useState({ uuids: [], data: [] }); + let [snackbarController, setSnackbarController] = useState({ open: false, message: "", status: "info" @@ -112,6 +123,11 @@ export const DatasetForm = (props) => { 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 ', 'background:#2200FF, color:#fff', ); + navigate("/notFound?entityID="+uuid); + } if (response.status === 200) { const entityType = response.results.entity_type; if (entityType !== "Dataset") { @@ -158,6 +174,10 @@ export const DatasetForm = (props) => { } }) .catch((error) => { + console.debug('%c◉ ERRRRR ', 'background:#2200FF, color:#fff', ); + if(error.status === 404){ + navigate("/notFound?entityID="+uuid); + } setPageErrors(error); }); } else { @@ -165,7 +185,7 @@ export const DatasetForm = (props) => { prefillFormValuesFromUrl(setForm, setSnackbarController); setPermissions({ has_write_priv: true }); } - setLoading(l => ({ ...l, page: false })); + setLoading(prevVals => ({ ...prevVals, page: false })); }, [uuid]); const handleInputChange = useCallback((e) => { @@ -217,10 +237,10 @@ export const DatasetForm = (props) => { return errors === 0; }; - const handleSubmit = (e) => { + const handleSave = (e) => { e.preventDefault(); if (validateForm()) { - setLoading(l => ({ ...l, processing: true })); + setLoading(prevVals => ({ ...prevVals, processing: true })); let selectedUUIDs = bulkSelection.data.map((obj) => obj.uuid); let cleanForm = { lab_dataset_id: form.lab_dataset_id, @@ -234,19 +254,20 @@ export const DatasetForm = (props) => { console.debug('%c⭗ Data', 'color:#00ff7b', cleanForm); if (uuid) { let target = e.target.name; - setLoading(l => ({ ...l, button: { ...l.button, [target]: true } })); + 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(l => ({ ...l, button: { ...l.button, [target]: false } })); + setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, [target]: false } })); } }) .catch((error) => { setPageErrors(error); - setLoading(l => ({ ...l, button: { ...l.button, [target]: false } })); + setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, [target]: false } })); }); } else { // If group_uuid is not set, default to first user group @@ -264,14 +285,143 @@ export const DatasetForm = (props) => { }) .catch((error) => { setPageErrors(error); - setLoading(l => ({ ...l, button: { process: false, save: false, submit: false }, processing: false })); + setLoading(prevVals => ({ ...prevVals, button: { process: false, save: false, submit: false }, processing: false })); }); } } else { - setLoading(l => ({ ...l, button: { process: false, save: false, submit: false }, processing: false })); + setLoading(prevVals => ({ ...prevVals, button: { process: false, save: false, submit: false }, processing: false })); } }; + const handleValidateEntity = (e) => { + e.preventDefault(); + ingest_api_validate_entity(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', ); + ingest_api_pipeline_test_submit({"uuid": 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(uuid, JSON.stringify(dataSubmit)) + .then((response) => { + console.debug("entity_api_update_entity response", response); + 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(); + ingest_api_dataset_submit(uuid, JSON.stringify(entityData)) + .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 (<> @@ -286,47 +436,56 @@ export const DatasetForm = (props) => { onClick={() => navigate("/")}> Cancel
+ {/* NEW, INVALID, REOPENED, ERROR, SUBMITTED */} {!uuid && ( handleSubmit(e)} + onClick={(e) => 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()) && ( handleSubmit(e)} + onClick={(e) => handleProcess(e)} variant="contained" className="m-2"> Process )} - {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status !== "new" && ( + {uuid && uuid.length > 0 && permissions.has_write_priv && entityData.status === "new" && ( handleSubmit(e)} + onClick={(e) => handleSubmitForTesting(e)} name="submit" variant="contained" - className="m-2" - > - Submit + 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 && entityData.status !== "published" && ( + {uuid && uuid.length > 0 && ((permissions.has_write_priv && entityData.status !== "published") || (permissions.has_admin_priv && entityData.status === "QA")) && ( handleSubmit(e)} + onClick={(e) => handleSave(e)} variant="contained" - className="m-2" - > + className="m-2"> Save )} @@ -413,6 +572,13 @@ export const DatasetForm = (props) => { {snackbarController.message}
+ {entityValidation?.message && ( + setEntityValidation(prev => ({ ...prev, open }))} + /> + )} ); } 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/fields/DatasetFormFields.jsx b/src/src/components/ui/fields/DatasetFormFields.jsx index 742d8568..3fc29e05 100644 --- a/src/src/components/ui/fields/DatasetFormFields.jsx +++ b/src/src/components/ui/fields/DatasetFormFields.jsx @@ -56,11 +56,12 @@ export const DatasetFormFields = ({ formFields, formValues, formErrors, permissi - } label="Yes" /> - } label="No" /> + } label="Yes" /> + } label="No" /> {error ? error : field.helperText} diff --git a/src/src/service/ingest_api.js b/src/src/service/ingest_api.js index a73c71a7..66fce0d2 100644 --- a/src/src/service/ingest_api.js +++ b/src/src/service/ingest_api.js @@ -527,10 +527,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) { + // const options = {headers: {Authorization: "Bearer " + globalTokauthen,"Content-Type":"application/json"}}; 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; + } } } From a7588367cd1e4fabdd9197819ba8a7a6f28b05ba Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Mon, 29 Sep 2025 17:15:48 -0400 Subject: [PATCH 20/26] Allow for Custom Title and Subtitle on Bulk Selector, move Revert button, --- src/src/components/newDataset.jsx | 28 ++++++++++++++++---------- src/src/components/newPublication.jsx | 2 ++ src/src/components/ui/bulkSelector.jsx | 11 +++++----- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 619dc953..707411ca 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -43,7 +43,9 @@ export const DatasetForm = (props) => { contains_human_genetic_sequences: "", dt_select: "", direct_ancestor_uuids: [], - group_uuid: "" + group_uuid: "", + ingest_task: "", + assigned_to_group_name: "" }); let [formErrors, setFormErrors] = useState({}); let [errorMessages, setErrorMessages] = useState([]); @@ -144,7 +146,9 @@ export const DatasetForm = (props) => { 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) + 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), @@ -248,8 +252,8 @@ export const DatasetForm = (props) => { description: form.description, dataset_info: form.dataset_info, direct_ancestor_uuids: selectedUUIDs, - dataset_type: form.dt_select, - group_uuid: form.group_uuid + ...(((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) { @@ -273,7 +277,9 @@ export const DatasetForm = (props) => { // 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; - console.log(form, form.contains_human_genetic_sequences); + 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) => { @@ -424,11 +430,6 @@ export const DatasetForm = (props) => { const buttonEngine = () => { return (<> - - {uuid && uuid.length > 0 && permissions.has_admin_priv &&( - - )} - { 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()) && ( { Validate )} - {uuid && uuid.length > 0 && ((permissions.has_write_priv && entityData.status !== "published") || (permissions.has_admin_priv && entityData.status === "QA")) && ( + {uuid && uuid.length > 0 && ((permissions.has_write_priv && (!["published", "QA"].includes(entityData.status))) || (permissions.has_admin_priv && entityData.status === "QA")) && ( { handleSubmit(e)}> { handleSubmit(e)}> - Providing {dialogTitle} + {title} @@ -425,7 +426,7 @@ export function BulkSelector({ }}> {dialogTitle} - {dialogSubtitle} + {subtitle} Date: Tue, 30 Sep 2025 11:51:22 -0400 Subject: [PATCH 21/26] Fix Casing issue for Search Blacklist --- src/src/components/newDataset.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 707411ca..7f2752d4 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -516,7 +516,7 @@ export const DatasetForm = (props) => { searchFilters={{ custom_title: "Search for a Source ID for your Dataset", custom_subtitle: "Collections may not be selected for Dataset sources", - blacklist: ['Collection'] + blacklist: ['collection'] }} readOnly={readOnlySources} preLoad={loading.bulk} From 745551b45c5cbce8bd16ac33022b9cc0e05c7312 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Tue, 30 Sep 2025 11:55:24 -0400 Subject: [PATCH 22/26] Fix Casing for BulkSelector url-fill type validation of IDs --- src/src/components/ui/bulkSelector.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/src/components/ui/bulkSelector.jsx b/src/src/components/ui/bulkSelector.jsx index b4b11acb..2b40e270 100644 --- a/src/src/components/ui/bulkSelector.jsx +++ b/src/src/components/ui/bulkSelector.jsx @@ -141,10 +141,11 @@ export function BulkSelector({ // 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; + if ( - (searchFilters.blacklist && searchFilters.blacklist.includes(entity.entity_type)) || - (searchFilters.whitelist && !searchFilters.whitelist.includes(entity.entity_type)) || - (searchFilters.restrictions && !searchFilters.restrictions.includes(entity.entity_type)) + (searchFilters.blacklist && searchFilters.blacklist.includes(entity.entity_type.toLowerCase())) || + (searchFilters.whitelist && !searchFilters.whitelist.includes(entity.entity_type.toLowerCase())) || + (searchFilters.restrictions && !searchFilters.restrictions.includes(entity.entity_type.toLowerCase())) ) { typeArray.push(`${entity.hubmap_id} (Invalid Type: ${entity.entity_type})`); } else { From 4f122c62fb0d96202de251238b704fc38e311084 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 1 Oct 2025 10:18:25 -0400 Subject: [PATCH 23/26] Restrictions fixes --- src/src/components/ui/bulkSelector.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/src/components/ui/bulkSelector.jsx b/src/src/components/ui/bulkSelector.jsx index 2b40e270..71dee4a4 100644 --- a/src/src/components/ui/bulkSelector.jsx +++ b/src/src/components/ui/bulkSelector.jsx @@ -141,11 +141,14 @@ export function BulkSelector({ // 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())) || - (searchFilters.restrictions && !searchFilters.restrictions.includes(entity.entity_type.toLowerCase())) + (restrictCheck === true) ) { typeArray.push(`${entity.hubmap_id} (Invalid Type: ${entity.entity_type})`); } else { From 7c38f32ca63244ec6569cba95a946b3b61d8db71 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Wed, 1 Oct 2025 17:03:29 -0400 Subject: [PATCH 24/26] Add Mouseover view of entityData.pipeline_message when mousing over status badge, adds missing helper text for Lab Name or ID --- src/src/components/newDataset.jsx | 4 ++-- src/src/components/ui/formParts.jsx | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 7f2752d4..d03515a4 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -72,14 +72,14 @@ export const DatasetForm = (props) => { const formFields = useMemo(() => [{ id: "lab_dataset_id", label: "Lab Name or ID", - helperText: "Lab Name or ID", + helperText: "An identifier used locally by the data provider.", required: true, type: "text" }, { id: "description", label: "Description", - helperText: "Description Tips", + helperText: "", required: true, type: "textarea" }, diff --git a/src/src/components/ui/formParts.jsx b/src/src/components/ui/formParts.jsx index 539524b3..e1674689 100644 --- a/src/src/components/ui/formParts.jsx +++ b/src/src/components/ui/formParts.jsx @@ -23,6 +23,7 @@ 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"; @@ -347,8 +348,19 @@ function topHeader(entityData){ 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: {entityData.priority_project_list?.length > 1 From fffd7715b4bd525d2457c0dac7f2044d9667cbc6 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 3 Oct 2025 12:31:31 -0400 Subject: [PATCH 25/26] Fix Group UUID being included with Process call, fix issue with permission API call utilizing possible HubmapID from URL --- src/src/components/newDataset.jsx | 35 +++++++++++++++++++++++++------ src/src/service/entity_api.js | 2 ++ src/src/service/ingest_api.js | 8 +++---- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index d03515a4..7d1b5b5f 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -24,8 +24,6 @@ import { ingest_api_dataset_submit, ingest_api_notify_slack} from "../service/ingest_api"; import { prefillFormValuesFromUrl, EntityValidationMessage } from "./ui/formParts"; -import {LensTwoTone} from "@mui/icons-material"; - export const DatasetForm = (props) => { let navigate = useNavigate(); @@ -127,7 +125,7 @@ export const DatasetForm = (props) => { .then((response) => { console.debug('%c◉ RESP ', 'color:#00ff7b', response); if(response.status === 404 || response.status === 400){ - console.debug('%c◉ ERRRRR ', 'background:#2200FF, color:#fff', ); + console.debug('%c◉ ERRRRR ', 'color:#FFFFFF;background: #2200FF;padding:200' , ); navigate("/notFound?entityID="+uuid); } if (response.status === 200) { @@ -158,7 +156,8 @@ export const DatasetForm = (props) => { if (entityData.creation_action === "Multi-Assay Split" || entityData.creation_action === "Central Process"){ setReadOnlySources(true); } - ingest_api_allowable_edit_states(uuid) + + ingest_api_allowable_edit_states(entityData.uuid) .then((response) => { if (entityData.data_access_level === "public") { setReadOnlySources(true); @@ -170,6 +169,8 @@ export const DatasetForm = (props) => { setPermissions(response.results); }) .catch((error) => { + console.error(error); + setPageErrors(error); }); } @@ -178,7 +179,7 @@ export const DatasetForm = (props) => { } }) .catch((error) => { - console.debug('%c◉ ERRRRR ', 'background:#2200FF, color:#fff', ); + 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); } @@ -241,6 +242,24 @@ export const DatasetForm = (props) => { 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()) { @@ -320,6 +339,7 @@ export const DatasetForm = (props) => { const handleSubmitForTesting = () => { console.debug('%c◉ Submitting for Testing ', 'color:#00ff7b', ); + // NOTE: CannotBe Derived! @TODO? ingest_api_pipeline_test_submit({"uuid": uuid}) .then((response) => { console.debug('%c◉ SUBMITTED', 'color:#00ff7b', response); @@ -356,6 +376,7 @@ export const DatasetForm = (props) => { entity_api_update_entity(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) @@ -390,7 +411,9 @@ export const DatasetForm = (props) => { const handleProcess = (e) => { e.preventDefault(); - ingest_api_dataset_submit(uuid, JSON.stringify(entityData)) + setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, process: true } })); + let data = buildCleanForm(); + ingest_api_dataset_submit(uuid, JSON.stringify(data)) .then((response) => { if (response.status < 300) { props.onUpdated(response.results); 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 66fce0d2..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 => { @@ -528,7 +526,7 @@ export function ingest_api_pipeline_test_privs(auth) { * */ export function ingest_api_pipeline_test_submit(data) { - // const options = {headers: {Authorization: "Bearer " + globalTokauthen,"Content-Type":"application/json"}}; + // 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`; return axios .post(url, {}, options) From febbcb0688ddcb13d8925c6401086898a54d6b50 Mon Sep 17 00:00:00 2001 From: Birdmachine Date: Fri, 3 Oct 2025 15:26:14 -0400 Subject: [PATCH 26/26] Switch url uuid handling with entityData at Processing action --- src/src/components/newDataset.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/src/components/newDataset.jsx b/src/src/components/newDataset.jsx index 7d1b5b5f..4428975b 100644 --- a/src/src/components/newDataset.jsx +++ b/src/src/components/newDataset.jsx @@ -320,7 +320,7 @@ export const DatasetForm = (props) => { const handleValidateEntity = (e) => { e.preventDefault(); - ingest_api_validate_entity(uuid, "datasets") + ingest_api_validate_entity(entityData.uuid, "datasets") .then((response) => { console.debug('%c◉ res ', 'color:#00ff7b', response); setEntityValidation({ @@ -340,7 +340,7 @@ export const DatasetForm = (props) => { const handleSubmitForTesting = () => { console.debug('%c◉ Submitting for Testing ', 'color:#00ff7b', ); // NOTE: CannotBe Derived! @TODO? - ingest_api_pipeline_test_submit({"uuid": uuid}) + ingest_api_pipeline_test_submit({"uuid": entityData.uuid}) .then((response) => { console.debug('%c◉ SUBMITTED', 'color:#00ff7b', response); let results = ""; @@ -373,7 +373,7 @@ export const DatasetForm = (props) => { const handleSubmit = (e) => { e.preventDefault(); var dataSubmit = {"status":"Submitted"} - entity_api_update_entity(uuid, JSON.stringify(dataSubmit)) + 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 @@ -413,7 +413,7 @@ export const DatasetForm = (props) => { e.preventDefault(); setLoading(prevVals => ({ ...prevVals, button: { ...prevVals.button, process: true } })); let data = buildCleanForm(); - ingest_api_dataset_submit(uuid, JSON.stringify(data)) + ingest_api_dataset_submit(entityData.uuid, JSON.stringify(data)) .then((response) => { if (response.status < 300) { props.onUpdated(response.results);