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:
GotthardG 2025-01-06 14:40:02 +01:00
parent 9cb6ffbfb4
commit 54975b5919
12 changed files with 436 additions and 134 deletions

View File

@ -139,10 +139,14 @@ class Sample(Base):
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
sample_name = Column(String(255), index=True)
position = Column(Integer) # Matches `position` in data creation script
proteinname = Column(String(255), index=True)
position = Column(Integer)
priority = Column(Integer)
comments = Column(String(255))
data_collection_parameters = Column(JSON, nullable=True)
# Foreign keys and relationships
dewar_id = Column(Integer, ForeignKey("dewars.id"))
puck_id = Column(Integer, ForeignKey("pucks.id"))
puck = relationship("Puck", back_populates="samples")
events = relationship("SampleEvent", back_populates="sample")

View File

@ -91,7 +91,10 @@ async def create_dewar(
sample = SampleModel(
puck_id=puck.id,
sample_name=sample_data.sample_name,
proteinname=sample_data.proteinname,
position=sample_data.position,
priority=sample_data.priority,
comments=sample_data.comments,
data_collection_parameters=sample_data.data_collection_parameters,
)
db.add(sample)
@ -285,6 +288,66 @@ async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)):
)
@router.get("/dewars/{dewar_id}/samples", response_model=dict)
async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
# Fetch Dewar, associated Pucks, and Samples
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
pucks = db.query(PuckModel).filter(PuckModel.dewar_id == dewar.id).all()
data = {"dewar": {"id": dewar.id, "dewar_name": dewar.dewar_name}, "pucks": []}
for puck in pucks:
samples = db.query(SampleModel).filter(SampleModel.puck_id == puck.id).all()
data["pucks"].append(
{
"id": puck.id,
"name": puck.puck_name,
"type": puck.puck_type,
"samples": [
{
"id": sample.id,
"position": sample.position,
"dewar_name": dewar.dewar_name, # Add Dewar name here
"sample_name": sample.sample_name,
"priority": sample.priority,
"comments": sample.comments,
# "directory":sample.directory,
"proteinname": sample.proteinname,
# "oscillation": datacollection.oscillation,
# "aperture": 10,
# "exposure": 11,
# "totalrange": 12,
# "transmission": 13,
# "dose": 14,
# "targetresolution": 15,
# "datacollectiontype": 16,
# "processingpipeline": 17,
# "spacegroupnumber": 18,
# "cellparameters": 19,
# "rescutkey": 20,
# "rescutvalue": 21,
# "userresolution": 22,
# "pdbid": 23,
# "autoprocfull": 24,
# "procfull": 25,
# "adpenabled": 26,
# "noano": 27,
# "ffcscampaign": 28,
# "trustedhigh": 29,
# "autoprocextraparams": 30,
# "chiphiangles": 31,
}
for sample in samples
],
}
)
return data
@router.get("/", response_model=List[DewarSchema])
async def get_dewars(db: Session = Depends(get_db)):
try:

View File

@ -3,6 +3,10 @@ from typing import Any, Optional, List, Dict
from pydantic import BaseModel, Field, field_validator
from typing_extensions import Annotated
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class SpreadsheetModel(BaseModel):
@ -98,34 +102,45 @@ class SpreadsheetModel(BaseModel):
@field_validator("directory", mode="before")
@classmethod
def directory_characters(cls, v):
if v:
v = str(v).strip("/").replace(" ", "_")
if re.search("\n", v):
raise ValueError(f" '{v}' is not valid. newline character detected.")
logger.debug(f"Validating 'directory' field with value: {repr(v)}")
valid_macros = [
"{date}",
"{prefix}",
"{sgpuck}",
"{puck}",
"{beamline}",
"{sgprefix}",
"{sgpriority}",
"{sgposition}",
"{protein}",
"{method}",
]
pattern = re.compile("|".join(re.escape(macro) for macro in valid_macros))
v = pattern.sub("macro", v)
allowed_chars = "[a-z0-9_.+-]"
directory_re = re.compile(
f"^(({allowed_chars}*|{allowed_chars}+)*/*)*$", re.IGNORECASE
# Assign default value if v is None or empty
if not v:
default_value = "{sgPuck}/{sgPosition}"
logger.warning(
f"'directory' field is empty or None. Assigning default value: "
f"{default_value}"
)
return default_value
v = str(v).strip("/").replace(" ", "_")
if "\n" in v:
raise ValueError(f"'{v}' is not valid. Newline character detected.")
# Replace valid macros for consistency
valid_macros = [
"{date}",
"{prefix}",
"{sgPuck}",
"{sgPosition}",
"{beamline}",
"{sgPrefix}",
"{sgPriority}",
"{protein}",
"{method}",
]
pattern = re.compile("|".join(re.escape(macro) for macro in valid_macros))
v = pattern.sub("macro", v)
# Ensure only allowed characters are in the directory value
allowed_chars = "[a-z0-9_.+-]"
directory_re = re.compile(
f"^(({allowed_chars}*|{allowed_chars}+)*/*)*$", re.IGNORECASE
)
if not directory_re.match(v):
raise ValueError(
f"'{v}' is not valid. Value must be a valid path or macro."
)
if not directory_re.match(v):
raise ValueError(
f" '{v}' is not valid. Value must be a valid path or macro."
)
return v
@field_validator("positioninpuck", mode="before")
@ -251,110 +266,109 @@ class SpreadsheetModel(BaseModel):
raise ValueError(f" '{v}' is not valid." f"Value must be one of {allowed}.")
return v
@field_validator("spacegroupnumber", mode="before")
@classmethod
def spacegroupnumber_allowed(cls, v):
if v is not None:
try:
v = int(v)
if not (1 <= v <= 230):
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 1 and 230."
)
except (ValueError, TypeError) as e:
@field_validator("spacegroupnumber", mode="before")
@classmethod
def spacegroupnumber_allowed(cls, v):
if v is not None:
try:
v = int(v)
if not (1 <= v <= 230):
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 1 and 230."
) from e
return v
@field_validator("cellparameters", mode="before")
@classmethod
def cellparameters_format(cls, v):
if v:
values = [float(i) for i in v.split(",")]
if len(values) != 6 or any(val <= 0 for val in values):
raise ValueError(
f" '{v}' is not valid."
f"Value must be a set of six positive floats or integers."
)
return v
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 1 and 230."
) from e
return v
@field_validator("rescutkey", "rescutvalue", mode="before")
@classmethod
def rescutkey_value_pair(cls, values):
rescutkey = values.get("rescutkey")
rescutvalue = values.get("rescutvalue")
if rescutkey and rescutvalue:
if rescutkey not in {"is", "cchalf"}:
raise ValueError("Rescutkey must be either 'is' or 'cchalf'")
if not isinstance(rescutvalue, float) or rescutvalue <= 0:
raise ValueError(
"Rescutvalue must be a positive float if rescutkey is provided"
)
return values
@field_validator("cellparameters", mode="before")
@classmethod
def cellparameters_format(cls, v):
if v:
values = [float(i) for i in v.split(",")]
if len(values) != 6 or any(val <= 0 for val in values):
raise ValueError(
f" '{v}' is not valid."
f"Value must be a set of six positive floats or integers."
)
return v
@field_validator("trustedhigh", mode="before")
@classmethod
def trustedhigh_allowed(cls, v):
if v is not None:
try:
v = float(v)
if not (0 <= v <= 2.0):
raise ValueError(
f" '{v}' is not valid."
f"Value must be a float between 0 and 2.0."
)
except (ValueError, TypeError) as e:
# @field_validator("rescutkey", "rescutvalue", mode="before")
# @classmethod
# def rescutkey_value_pair(cls, values):
# rescutkey = values.get("rescutkey")
# rescutvalue = values.get("rescutvalue")
# if rescutkey and rescutvalue:
# if rescutkey not in {"is", "cchalf"}:
# raise ValueError("Rescutkey must be either 'is' or 'cchalf'")
# if not isinstance(rescutvalue, float) or rescutvalue <= 0:
# raise ValueError(
# "Rescutvalue must be a positive float if rescutkey is provided"
# )
# return values
@field_validator("trustedhigh", mode="before")
@classmethod
def trustedhigh_allowed(cls, v):
if v is not None:
try:
v = float(v)
if not (0 <= v <= 2.0):
raise ValueError(
f" '{v}' is not valid."
f"Value must be a float between 0 and 2.0."
) from e
return v
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid." f"Value must be a float between 0 and 2.0."
) from e
return v
@field_validator("chiphiangles", mode="before")
@classmethod
def chiphiangles_allowed(cls, v):
if v is not None:
try:
v = float(v)
if not (0 <= v <= 30):
raise ValueError(
f" '{v}' is not valid."
f"Value must be a float between 0 and 30."
)
except (ValueError, TypeError) as e:
@field_validator("chiphiangles", mode="before")
@classmethod
def chiphiangles_allowed(cls, v):
if v is not None:
try:
v = float(v)
if not (0 <= v <= 30):
raise ValueError(
f" '{v}' is not valid. Value must be a float between 0 and 30."
) from e
return v
f" '{v}' is not valid."
f"Value must be a float between 0 and 30."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid. Value must be a float between 0 and 30."
) from e
return v
@field_validator("dose", mode="before")
@classmethod
def dose_positive(cls, v):
if v is not None:
try:
v = float(v)
if v <= 0:
raise ValueError(
f" '{v}' is not valid. Value must be a positive float."
)
except (ValueError, TypeError) as e:
@field_validator("dose", mode="before")
@classmethod
def dose_positive(cls, v):
if v is not None:
try:
v = float(v)
if v <= 0:
raise ValueError(
f" '{v}' is not valid. Value must be a positive float."
) from e
return v
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid. Value must be a positive float."
) from e
return v
class TELLModel(SpreadsheetModel):
input_order: int
samplemountcount: int = 0
samplestatus: str = "not present"
puckaddress: str = "---"
username: str
puck_number: int
prefix: Optional[str]
folder: Optional[str]
# class TELLModel(SpreadsheetModel):
# input_order: int
# samplemountcount: int = 0
# samplestatus: str = "not present"
# puckaddress: str = "---"
# username: str
# puck_number: int
# prefix: Optional[str]
# folder: Optional[str]
class SpreadsheetResponse(BaseModel):

View File

@ -150,14 +150,20 @@ class Sample(BaseModel):
position: int # Position within the puck
puck_id: int
crystalname: Optional[str] = Field(None)
proteinname: Optional[str] = None
positioninpuck: Optional[int] = Field(None)
priority: Optional[int] = None
comments: Optional[str] = None
events: List[SampleEventCreate] = []
class SampleCreate(BaseModel):
sample_name: str = Field(..., alias="crystalname")
proteinname: Optional[str] = None
position: int = Field(..., alias="positioninpuck")
data_collection_parameters: Optional[DataCollectionParameters] = None
priority: Optional[int] = None
comments: Optional[str] = None
results: Optional[Results] = None
events: Optional[List[str]] = None

View File

@ -1,5 +1,4 @@
# Adjusting the ShipmentProcessor for better error handling and alignment
from sqlalchemy.orm import Session
from app.models import Shipment, Dewar, Puck, Sample, DataCollectionParameters
from app.schemas import ShipmentCreate, ShipmentResponse
@ -47,14 +46,17 @@ class ShipmentProcessor:
self.db.refresh(puck)
for sample_data in puck_data.samples:
data_collection_params = DataCollectionParameters(
data_collection_parameters = DataCollectionParameters(
**sample_data.data_collection_parameters.dict(by_alias=True)
)
sample = Sample(
puck_id=puck.id,
sample_name=sample_data.sample_name,
proteinname=sample_data.proteinname,
position=sample_data.position,
data_collection_parameters=data_collection_params,
priority=sample_data.priority,
comments=sample_data.comments,
data_collection_parameters=data_collection_parameters,
)
self.db.add(sample)
self.db.commit()

View File

@ -48,17 +48,6 @@ class SampleSpreadsheetImporter:
def import_spreadsheet(self, file):
return self.import_spreadsheet_with_errors(file)
def get_expected_type(self, col_name):
type_mapping = {
"dewarname": str,
"puckname": str,
"positioninpuck": int,
"priority": int,
"oscillation": float,
# Add all other mappings based on model requirements
}
return type_mapping.get(col_name, str) # Default to `str`
def import_spreadsheet_with_errors(
self, file
) -> Tuple[List[SpreadsheetModel], List[dict], List[dict], List[str]]:
@ -194,6 +183,20 @@ class SampleSpreadsheetImporter:
try:
validated_record = SpreadsheetModel(**record)
# Update the raw data with assigned default values
if (
validated_record.directory == "{sgPuck}/{sgPosition}"
and row[7] is None
):
row_list = list(row)
row_list[
7
] = validated_record.directory # Set the field to the default value
raw_data[-1]["data"] = row_list
raw_data[-1][
"default_set"
] = True # Mark this row as having a default value assigned
model.append(validated_record)
logger.debug(f"Row {index + 4} processed and validated successfully")
except ValidationError as e:

View File

@ -21,6 +21,7 @@
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.6",
"@mui/system": "^6.1.6",
"@mui/x-data-grid": "^7.23.3",
"axios": "^1.7.7",
"chokidar": "^4.0.1",
"dayjs": "^1.11.13",
@ -1632,6 +1633,63 @@
}
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.3.tgz",
"integrity": "sha512-EiM5kut6N/0o0iEYx8A7M3fJqknAa1kcPvGhlX3hH50ERLDeuJaqoKzvRYLBbYKWydHIc+0hHIFcK5oQTXLenw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.23.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0",
"@mui/system": "^5.15.14 || ^6.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.0.tgz",
"integrity": "sha512-bPclKpqUiJYIHqmTxSzMVZi6MH51cQsn5U+8jskaTlo3J4QiMeCYJn/gn7YbeR9GOZFp8hetyHjoQoVHKRXCig==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@mui/x-date-pickers": {
"version": "7.22.2",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.2.tgz",
@ -5829,6 +5887,12 @@
"node": ">=0.10.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",

View File

@ -27,6 +27,7 @@
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.6",
"@mui/system": "^6.1.6",
"@mui/x-data-grid": "^7.23.3",
"axios": "^1.7.7",
"chokidar": "^4.0.1",
"dayjs": "^1.11.13",

View File

@ -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}

View 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;

View File

@ -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);

View File

@ -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>