I have a parent component:
function CCList(props) {
const [panelDisabled, setPanelDisabled] = React.useState(false);
const [responseData, setResponseData] = React.useState("");
const [selectedCountry, setSelectedCountry] = React.useState("US");
const [selectedOperation, setSelectedOperation] = React.useState("Create");
const [createdObject, setCreatedObject] = React.useState(null);
const [searchResults, setSearchResults] = React.useState([]);
const [selectedResult, setSelectedResult] = React.useState({})
const [commitMessage, setCommitMessage] = React.useState("");
const [showConfirmationModal, setShowConfirmationModal] = React.useState(false);
const [showViewChangesModal, setShowViewChangesModal] = React.useState(false);
const [clonedObject, setClonedObject] = React.useState(null);
const handleOperationChange = (event) => {
setSelectedOperation(event.target.value);
setResponseData(""); // Clear any previous response data
setCreatedObject(null);
setSearchResults([]);
setSelectedResult({});
setClonedObject(null);
};
// Handle dropdown selection change
const handleOptionChange = (event) => {
setSelectedCountry(event.target.value);
setResponseData(""); // Clear any previous response data
setShowConfirmationModal(false);
setShowViewChangesModal(false);
};
const handleClose = () => {
setCreatedObject(null);
setResponseData("");
}
const openConfirmationModal = () => setShowConfirmationModal(true);
const openViewChangesModal = () => setShowViewChangesModal(true);
// Handle button click
const handleSearch = (event) => {
let urlParams = new URLSearchParams({"countryCode": selectedCountry })
let url = props.uris['trial-plans-fetch'] + "?" + urlParams.toString();
setResponseData("Searching...");
props.handlePanelDisabledChanged(true);
event.preventDefault();
fetch(url)
.then(response => response.json())
.then(bodyJson => {
if (bodyJson.success) {
const numberOfEntries = Array.isArray(bodyJson.data) ? bodyJson.data.length : 0;
setResponseData("Successfully found " + numberOfEntries + " results.");
setSearchResults(bodyJson.data);
} else {
let errorMsg = ("data" in bodyJson && "message" in bodyJson["data"]) ?
bodyJson.data.message + (bodyJson.data.code ? ' (code: ' + bodyJson.data.code + ')' : null) : bodyJson.error;
setResponseData(errorMsg);
}
})
.catch((error) => {
console.error('Error:', error);
})
.finally(() => {
console.log("done");
props.handlePanelDisabledChanged(false);
});
}
// Handle upload CSV button click
const handleUploadCSV = () => {
// Example message display logic
setResponseData(`Upload CSV Button clicked`);
};
return (
<div>
<fieldset className="p-1 my-2">
<table width={"100%"}>
<tbody>
<tr>
<td>
<label htmlFor="selectOperationBox">Select Operation</label>
</td>
<td>
<select
id="selectOperationBox"
value={selectedOperation}
style={{width: "80%"}}
className="p-1 my-2"
onChange={handleOperationChange}
>
<option value="Create">Create</option>
<option value="Clone">Clone</option>
</select>
</td>
</tr>
{selectedOperation === "Create" && (
<tr>
<td>
<ObjectCreationForm fields={modelSpecificPlanMetadataFields} validations={validations}
setResponseData={setResponseData} setCreatedObject={setCreatedObject}/>
</td>
</tr>
)}
{selectedOperation === "Clone" && (
<tr>
<td>
<label htmlFor="selectCountryBox">Select Country Code</label>
</td>
<td>
<select
id="selectCountryBox"
value={selectedCountry}
style={{width: "80%"}}
className="p-1 my-2"
onChange={handleOptionChange}
>
{props.availableCC.map((code) => (
<option key={code} value={code}>
{code}
</option>
))}
</select>
</td>
<td style={{width: "20%"}}>
<button
className={"btn btn-primary"}
style={{width: "100%"}}
onClick={handleSearch}
>
Fetch
</button>
</td>
</tr>
)}
</tbody>
</table>
</fieldset>
{responseData ? (
<div className="p-1 alert alert-info" style={{
maxHeight: '200px',
overflowY: 'auto',
backgroundColor: '#e0f7fa',
padding: '10px',
marginTop: '20px',
border: '1px solid #b0bec5',
borderRadius: '4px',
fontSize: '0.85em',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word'
}}>{responseData}</div>
) : null}
<div>
{createdObject && (
<div style={{
display: 'flex',
gap: '5px',
overflowX: 'auto',
whiteSpace: 'nowrap',
padding: '5px 0',
}}>
{Object.entries(createdObject).map(([fieldName, value]) => (
<div key={fieldName}
style={{display: 'inline-flex', flexDirection: 'column', alignItems: 'center'}}>
<span style={{
fontSize: '12px',
color: '#555',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100px',
whiteSpace: 'nowrap',
textAlign: 'center'
}}>
{fieldName}
</span>
<div style={{
backgroundColor: '#e0e0e0',
padding: '8px',
borderRadius: '4px',
width: '100px',
textAlign: 'center',
color: '#333',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{value || "N/A"}
</div>
</div>
))}
</div>
)}
{/* Compact second container */}
{createdObject && (
<div style={{
position: 'fixed',
bottom: '0',
left: '0',
right: '0',
height: '40px', // Thin banner height
backgroundColor: '#f9f9f9',
display: 'flex',
alignItems: 'center',
justifyContent: 'right',
borderTop: '1px solid #ddd',
padding: '10px 20px',
gap: '10px',
boxShadow: '0 -1px 5px rgba(0, 0, 0, 0.1)', // Subtle shadow at the top
zIndex: '1000'
}}>
{/* Status text: "Ready for Pull Request" */}
<div style={{
fontSize: '16px',
color: '#5e6c84',
marginRight: '30px', // Space between text and buttons
}}>
Create Pull Request
</div>
{/* "What did you change?" input box */}
<input
type="text"
placeholder="What did you change?"
style={{
padding: '6px',
fontSize: '14px',
borderRadius: '4px',
border: '3px solid #ccc',
backgroundColor: '#f5f5f5',
width: '180px', // Compact width for the banner style
marginRight: '10px'
}}
/>
{/* Update button */}
<button
type="button"
onClick={openConfirmationModal}
className="btn btn-primary"
style={{
backgroundColor: '#0052cc',
color: '#fff',
border: 'none',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: '10px'
}}
>
Update
</button>
{/* Close button */}
<button
type="button"
className={"btn btn-danger"}
onClick={handleClose}
style={{
border: '1px solid #ccc',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: '5px',
}}
>
Cancel
</button>
<button
onClick={openViewChangesModal}
style={{
backgroundColor: '#f4f5f7',
border: '1px solid #dfe1e6',
borderRadius: '4px',
width: '32px',
height: '32px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0',
fontSize: '18px',
color: '#5e6c84',
}}
aria-label="Options"
>
•••
</button>
</div>
)}
</div>
{searchResults.length > 0 && (
<PaginatedResults searchResults={searchResults} setResponseData={setResponseData}
setCreatedObject={setCreatedObject} clonedObject={clonedObject}
setClonedObject={setClonedObject}/>
)}
{showConfirmationModal && (
<ConfirmationModal showModal={showConfirmationModal} setShowModal={setShowConfirmationModal}
csvName={"CountrySpecificPlanMetadata"} createdObject={JSON.parse(JSON.stringify(createdObject))}/>
)}
{showViewChangesModal && (
<ViewChangesModal showModal={showViewChangesModal} setShowModal={setShowViewChangesModal}
createdObject={JSON.parse(JSON.stringify(createdObject))} clonedObject={JSON.parse(JSON.stringify(clonedObject))}/>
)}
</div>
);
}
The Child component “ObjectCreationForm” Sets createdObject on button click:
function ObjectCreationForm({fields, validations, setResponseData, setCreatedObject, clonedObject}) {
// Maintain a state for expanded categories
const [expandedSections, setExpandedSections] = React.useState({});
const [formValues, setFormValues] = React.useState({});
const [validationErrors, setValidationErrors] = React.useState({});
React.useEffect(() => {
setExpandedSections({});
setFormValues({});
setValidationErrors({});
const initializeFormValues = () => {
const initialValues = {};
const initialErrors = {};
const getFieldsFromObject = (field) => {
for (const [key, value] of Object.entries(clonedObject || {})) {
if (value && typeof value === 'object' && value[field] !== undefined) {
return value[field]; // Return the field value if found in a non-null model
}
}
return ''; // Default to an empty string if not found
};
Object.entries(fields).forEach(([category, categoryFields]) => {
initialValues[category] = {};
Object.keys(categoryFields).forEach((field) => {
const initialValue = clonedObject ? getFieldsFromObject(field) : '';
initialValues[category][field] = initialValue;
initialErrors[field] = validateInput(field, initialValue) ? null : `Invalid value for ${field}`;
});
});
setFormValues(initialValues);
setValidationErrors(initialErrors);
};
initializeFormValues();
}, [clonedObject, fields]); // Re-run this effect whenever clonedObject or fields change
const validateInput = (fieldName, value) => {
if (validations[fieldName]) {
return (new RegExp(validations[fieldName].slice(1, -1))).test(value);
}
return true; // Return true if no validation rule is defined
};
const toggleSection = (category) => {
setExpandedSections((prevSections) => ({
...prevSections,
[category]: !prevSections[category], // Toggle the visibility
}));
};
// Handle field value changes
const handleInputChange = (category, field, value) => {
// Validate the field
const isValid = validateInput(field, value);
// Update the validation errors state
setValidationErrors((prevErrors) => ({
...prevErrors,
[field]: isValid ? null : `Invalid value for ${field}`,
}));
setFormValues((prevValues) => ({
...prevValues,
[category]: {
...prevValues[category],
[field]: value,
},
}));
};
const handleButtonClick = () => {
// Set the response data in the parent component when the button is clicked
const flatObject = {};
Object.entries(formValues).forEach(([category, fields]) => {
Object.entries(fields).forEach(([field, value]) => {
flatObject[field] = value; // Store field name as key and input value as value
});
});
setResponseData(`Object Successfully Created!`);
setCreatedObject(flatObject);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
At this point there are no issues in the application. However, if I change something in theObjectCreationForm, and then click the button to setCreateObject again with different field values, the entire app crashes when I try to open ViewChangesModal:
const ViewChangesModal = ({ showModal, setShowModal, clonedObject, createdObject }) => {
if (!showModal) return null;
const closeModal = () => setShowModal(false);
const flattenObject = (obj) => {
const flatObject = {};
for (const [key, value] of Object.entries(obj)) {
if (value && typeof value === 'object') {
// Recursively flatten nested objects
const nestedObject = flattenObject(value);
for (const [nestedKey, nestedValue] of Object.entries(nestedObject)) {
flatObject[nestedKey] = nestedValue;
}
} else {
flatObject[key] = value;
}
}
return flatObject;
};
const flatClonedObject = flattenObject(clonedObject);
const flatCreatedObject = flattenObject(createdObject);
console.log("Flattened Cloned Object:", flatClonedObject);
console.log("Flattened Created Object:", flatCreatedObject);
// Function to determine style based on differences
const getFieldStyle = (objectValue, compareValue, highlightColor) => {
// Treat both "null" (string) and undefined/empty strings as equivalent
const normalizedObjectValue = objectValue === undefined || objectValue === "" ? "null" : objectValue;
const normalizedCompareValue = compareValue === undefined || compareValue === "" ? "null" : compareValue;
return {
backgroundColor: normalizedObjectValue !== normalizedCompareValue ? highlightColor : 'transparent',
fontSize: '12px',
color: '#555',
textAlign: 'center',
padding: '3px 5px',
borderRadius: '3px',
};
};
const scrollbarCSS = `
.scrollable-banner::-webkit-scrollbar {
height: 3px; /* Make the scrollbar thinner */
}
.scrollable-banner::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
.scrollable-banner::-webkit-scrollbar-track {
background-color: transparent;
}
`;
const scrollableBannerStyle = {
display: 'flex',
gap: '5px',
overflowX: 'auto',
whiteSpace: 'nowrap',
padding: '5px 0',
border: '1px solid #ddd',
borderRadius: '4px',
maxHeight: '60px',
overflowY: 'hidden',
marginBottom: '10px',
paddingRight: '10px',
};
return (
<div style={{
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '2000'
}}>
<div style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
width: '600px',
maxWidth: '90%',
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)',
overflowY: 'auto'
}}>
<p>You are viewing the changes between the object you <strong>Cloned</strong> and the object you <strong>Created</strong>.</p>
{/* Cloned Object */}
<div style={{ marginTop: '10px' }}>
<label style={{ fontWeight: 'bold' }}>Cloned Object:</label>
<div className="scrollable-banner" style={scrollableBannerStyle}>
{Object.keys(flatCreatedObject).map((field) => (
<span
key={field}
style={getFieldStyle(flatClonedObject[field], flatCreatedObject[field], 'rgba(255, 0, 0, 0.1)')} // Red for differences
>
{flatClonedObject[field] !== undefined && flatClonedObject[field] !== "" ? flatClonedObject[field] : "null"}
</span>
))}
</div>
</div>
{/* Created Object */}
<div style={{ marginTop: '10px' }}>
<label style={{ fontWeight: 'bold' }}>Created Object:</label>
<div className="scrollable-banner" style={scrollableBannerStyle}>
{Object.keys(flatCreatedObject).map((field) => {
const createdValue = flatCreatedObject[field] !== undefined && flatCreatedObject[field] !== "" ? flatCreatedObject[field] : "null";
const clonedValue = flatClonedObject[field] !== undefined && flatClonedObject[field] !== "" ? flatClonedObject[field] : "null";
return (
<span
key={`created-${field}`}
style={getFieldStyle(createdValue, clonedValue, 'rgba(0, 255, 0, 0.1)')}
>
{createdValue}
</span>
);
})}
</div>
</div>
{/* Close Button */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
<button
type="button"
onClick={closeModal}
style={{
backgroundColor: '#f0f0f0',
color: '#333',
border: '1px solid #ccc',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Close
</button>
</div>
</div>
</div>
);
}
I don’t get any errors pretty much. The DOM seems to crash and reset, as well as the browser dev tools. Im unsure of where to begin, but Im 99% Sure the issue is in the ViewChangesModal component, since i’m unable to replicate the issue when accessing any other component. The issue only occurs when createdObject is set a second time with DIFFERENT values than the first time. If i click the button to setCreatedObject without changing any of the field values, no issues occur in ViewChangesModal. Even ConfirmationModal works with no issues. Any help is appreciated.