Add spreadsheet enhancements and default handling
Implemented a toggleable spreadsheet UI component for sample data, added fields such as priority and comments, and improved backend validation. Default values for "directory" are now assigned when missing, with feedback highlighted in green on the front end.
This commit is contained in:
@ -13,6 +13,9 @@ import {
|
||||
Tooltip,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import SampleSpreadsheet from '../components/SampleSpreadsheetGrid';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
|
||||
import QRCode from 'react-qr-code';
|
||||
import {
|
||||
@ -101,7 +104,10 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
|
||||
const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false);
|
||||
const [qrCodeValue, setQrCodeValue] = useState(dewar.unique_id || '');
|
||||
const qrCodeRef = useRef<HTMLCanvasElement>(null); //
|
||||
|
||||
const [showSpreadsheet, setShowSpreadsheet] = useState(false);
|
||||
const toggleSpreadsheet = () => {
|
||||
setShowSpreadsheet((prev) => !prev);
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchDewarTypes = async () => {
|
||||
try {
|
||||
@ -705,8 +711,25 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
{/* Toggle Button for Spreadsheet */}
|
||||
<Tooltip title={showSpreadsheet ? "Hide Sample Spreadsheet" : "Show Sample Spreadsheet"}>
|
||||
<IconButton
|
||||
onClick={toggleSpreadsheet}
|
||||
sx={{ marginLeft: 2, backgroundColor: 'lightgray' }}
|
||||
>
|
||||
{showSpreadsheet ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Conditionally Render the SampleSpreadsheet component */}
|
||||
{showSpreadsheet && (
|
||||
<Box sx={{ marginTop: 4 }}>
|
||||
<Typography variant="h6" sx={{ marginBottom: 2 }}>Sample Spreadsheet</Typography>
|
||||
<SampleSpreadsheet dewarId={dewar.id} /> {/* Ensure dewar.id is passed */}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Snackbar
|
||||
open={openSnackbar}
|
||||
autoHideDuration={6000}
|
||||
|
88
frontend/src/components/SampleSpreadsheetGrid.tsx
Normal file
88
frontend/src/components/SampleSpreadsheetGrid.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { DataGrid, GridColDef, GridCellEditStopParams } from "@mui/x-data-grid";
|
||||
import { DewarsService } from "../../openapi";
|
||||
|
||||
interface SampleSpreadsheetProps {
|
||||
dewarId: number; // Selected Dewar's ID
|
||||
}
|
||||
|
||||
const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
||||
const [rows, setRows] = useState<any[]>([]); // Rows for the grid
|
||||
const [columns, setColumns] = useState<GridColDef[]>([]); // Columns for the grid
|
||||
|
||||
// Fetch rows and columns related to the given Dewar ID
|
||||
useEffect(() => {
|
||||
if (!dewarId) {
|
||||
console.error("Invalid or missing Dewar ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchSamples = async () => {
|
||||
try {
|
||||
const response = await DewarsService.getDewarSamplesDewarsDewarsDewarIdSamplesGet(dewarId);
|
||||
const dewarData = response; // Assume response is already in correct structure
|
||||
|
||||
// Transform pucks and samples into rows for the grid
|
||||
const allRows = [];
|
||||
dewarData.pucks.forEach((puck: any) => {
|
||||
puck.samples.forEach((sample: any) => {
|
||||
allRows.push({
|
||||
id: sample.id,
|
||||
puckId: puck.id,
|
||||
puckName: puck.name,
|
||||
puckType: puck.type,
|
||||
dewarName: dewarData.dewar.dewar_name,
|
||||
position: sample.position,
|
||||
crystalName: sample.sample_name,
|
||||
proteinName: sample.proteinname,
|
||||
priority: sample.priority,
|
||||
comments: sample.comments,
|
||||
});
|
||||
});
|
||||
});
|
||||
setRows(allRows);
|
||||
|
||||
// Define table columns if not already set
|
||||
setColumns([
|
||||
{ field: "dewarName", headerName: "Dewar Name", width: 150, editable: false }, // Display Dewar Name
|
||||
{ field: "puckName", headerName: "Puck Name", width: 150 },
|
||||
{ field: "puckType", headerName: "Puck Type", width: 150 },
|
||||
{ field: "crystalName", headerName: "Crystal Name", width: 200, editable: true },
|
||||
{ field: "proteinName", headerName: "Protein Name", width: 200, editable: true },
|
||||
{ field: "position", headerName: "Position", width: 100, editable: true , type: "number"},
|
||||
{ field: "priority", headerName: "Priority", width: 100, editable: true, type: "number" },
|
||||
{ field: "comments", headerName: "Comments", width: 300, editable: true },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dewar samples:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSamples();
|
||||
}, [dewarId]);
|
||||
|
||||
// Handle cell editing to persist changes to the backend
|
||||
const handleCellEditStop = async (params: GridCellEditStopParams) => {
|
||||
const { id, field, value } = params;
|
||||
try {
|
||||
// Example: Replace with a proper OpenAPI call if available
|
||||
await DewarsService.updateSampleData(id as number, { [field]: value }); // Assuming this exists
|
||||
console.log("Updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Error saving data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: 500, width: "100%" }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
pageSize={10}
|
||||
onCellEditStop={handleCellEditStop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SampleSpreadsheet;
|
@ -206,7 +206,10 @@ const SpreadsheetTable = ({
|
||||
const puckNameIdx = fieldToCol['puckname'];
|
||||
const puckTypeIdx = fieldToCol['pucktype'];
|
||||
const sampleNameIdx = fieldToCol['crystalname'];
|
||||
const proteinNameIdx = fieldToCol['proteinname'];
|
||||
const samplePositionIdx = fieldToCol['positioninpuck'];
|
||||
const priorityIdx = fieldToCol['priority'];
|
||||
const commentsIdx = fieldToCol['comments'];
|
||||
|
||||
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||
const row = data[rowIndex];
|
||||
@ -220,7 +223,10 @@ const SpreadsheetTable = ({
|
||||
const puckName = row.data[puckNameIdx] !== undefined && row.data[puckNameIdx] !== null ? String(row.data[puckNameIdx]).trim() : null;
|
||||
const puckType = typeof row.data[puckTypeIdx] === 'string' ? row.data[puckTypeIdx] : 'Unipuck';
|
||||
const sampleName = typeof row.data[sampleNameIdx] === 'string' ? row.data[sampleNameIdx].trim() : null;
|
||||
const proteinName = typeof row.data[proteinNameIdx] === 'string' ? row.data[proteinNameIdx].trim() : null;
|
||||
const samplePosition = row.data[samplePositionIdx] !== undefined && row.data[samplePositionIdx] !== null ? Number(row.data[samplePositionIdx]) : null;
|
||||
const priority = row?.data?.[priorityIdx] ? Number(row.data[priorityIdx]) : null;
|
||||
const comments = typeof row.data[commentsIdx] === 'string' ? row.data[commentsIdx].trim() : null;
|
||||
|
||||
if (dewarName && puckName) {
|
||||
let dewar;
|
||||
@ -263,7 +269,11 @@ const SpreadsheetTable = ({
|
||||
|
||||
const sample = {
|
||||
sample_name: sampleName,
|
||||
proteinname: proteinName,
|
||||
position: samplePosition,
|
||||
priority: priority,
|
||||
comments: comments,
|
||||
data_collection_parameters: null,
|
||||
results: null // Placeholder for results field
|
||||
};
|
||||
|
||||
@ -425,14 +435,28 @@ const SpreadsheetTable = ({
|
||||
const cellValue = (row.data && row.data[colIndex]) || "";
|
||||
const editingValue = editingCell[`${rowIndex}-${colIndex}`];
|
||||
const isReadonly = !isInvalid;
|
||||
|
||||
|
||||
const isDefaultAssigned = colIndex === 7 && row.default_set; // Directory column (index 7) and marked as default_set
|
||||
|
||||
return (
|
||||
<TableCell key={colIndex} align="center">
|
||||
<TableCell
|
||||
key={colIndex}
|
||||
align="center"
|
||||
style={{
|
||||
backgroundColor: isDefaultAssigned ? "#e6fbe6" : "transparent", // Light green for default
|
||||
color: isDefaultAssigned ? "#1b5e20" : "inherit", // Dark green text for default
|
||||
}}
|
||||
>
|
||||
<Tooltip title={errorMessage || ""} arrow disableHoverListener={!isInvalid}>
|
||||
{isInvalid ? (
|
||||
<TextField
|
||||
value={editingValue !== undefined ? editingValue : cellValue}
|
||||
onChange={(e) => setEditingCell({ ...editingCell, [`${rowIndex}-${colIndex}`]: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setEditingCell({
|
||||
...editingCell,
|
||||
[`${rowIndex}-${colIndex}`]: e.target.value,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleCellEdit(rowIndex, colIndex);
|
||||
|
@ -98,6 +98,11 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShip
|
||||
}
|
||||
};
|
||||
|
||||
// Count rows with directory defaulted
|
||||
const defaultDirectoryCount = fileSummary?.raw_data
|
||||
? fileSummary.raw_data.filter((row) => row.default_set).length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
|
||||
@ -155,6 +160,11 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShip
|
||||
The file is validated successfully with no errors.
|
||||
</Typography>
|
||||
)}
|
||||
<Typography color="success.main" sx={{ mt: 2 }}>
|
||||
{defaultDirectoryCount > 0
|
||||
? `${defaultDirectoryCount} rows had the "directory" field auto-assigned to the default value "{sgPuck}/{sgPosition}". These rows are highlighted in green.`
|
||||
: "No rows had default values assigned to the 'directory' field."}
|
||||
</Typography>
|
||||
<Typography>Dewars: {fileSummary.dewars_count}</Typography>
|
||||
<Typography>Pucks: {fileSummary.pucks_count}</Typography>
|
||||
<Typography>Samples: {fileSummary.samples_count}</Typography>
|
||||
|
Reference in New Issue
Block a user