From 54975b59196448eea9c332eed4cb398ec5093cf5 Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:40:02 +0100 Subject: [PATCH] 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. --- backend/app/models.py | 6 +- backend/app/routers/dewar.py | 63 +++++ backend/app/sample_models.py | 244 +++++++++--------- backend/app/schemas.py | 6 + backend/app/services/shipment_processor.py | 8 +- backend/app/services/spreadsheet_service.py | 25 +- frontend/package-lock.json | 64 +++++ frontend/package.json | 1 + frontend/src/components/DewarDetails.tsx | 25 +- .../src/components/SampleSpreadsheetGrid.tsx | 88 +++++++ frontend/src/components/SpreadsheetTable.tsx | 30 ++- frontend/src/components/UploadDialog.tsx | 10 + 12 files changed, 436 insertions(+), 134 deletions(-) create mode 100644 frontend/src/components/SampleSpreadsheetGrid.tsx diff --git a/backend/app/models.py b/backend/app/models.py index 20772c8..b692f3c 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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") diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index 25ee92f..318f122 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -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: diff --git a/backend/app/sample_models.py b/backend/app/sample_models.py index b29b2c6..4dc5497 100644 --- a/backend/app/sample_models.py +++ b/backend/app/sample_models.py @@ -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): diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 0e38c70..c4da527 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -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 diff --git a/backend/app/services/shipment_processor.py b/backend/app/services/shipment_processor.py index 93c3e7c..4a26b61 100644 --- a/backend/app/services/shipment_processor.py +++ b/backend/app/services/shipment_processor.py @@ -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() diff --git a/backend/app/services/spreadsheet_service.py b/backend/app/services/spreadsheet_service.py index 7df4c97..3cc6936 100644 --- a/backend/app/services/spreadsheet_service.py +++ b/backend/app/services/spreadsheet_service.py @@ -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: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7454672..81d1558 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index ac0fa28..2d5a8f2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index c02b3bb..2ed3359 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -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 = ({ const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false); const [qrCodeValue, setQrCodeValue] = useState(dewar.unique_id || ''); const qrCodeRef = useRef(null); // - + const [showSpreadsheet, setShowSpreadsheet] = useState(false); + const toggleSpreadsheet = () => { + setShowSpreadsheet((prev) => !prev); + }; useEffect(() => { const fetchDewarTypes = async () => { try { @@ -705,8 +711,25 @@ const DewarDetails: React.FC = ({ > Save Changes + {/* Toggle Button for Spreadsheet */} + + + {showSpreadsheet ? : } + + + {/* Conditionally Render the SampleSpreadsheet component */} + {showSpreadsheet && ( + + Sample Spreadsheet + {/* Ensure dewar.id is passed */} + + )} + = ({ dewarId }) => { + const [rows, setRows] = useState([]); // Rows for the grid + const [columns, setColumns] = useState([]); // 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 ( +
+ +
+ ); +}; + +export default SampleSpreadsheet; \ No newline at end of file diff --git a/frontend/src/components/SpreadsheetTable.tsx b/frontend/src/components/SpreadsheetTable.tsx index 017dbc9..7d57e48 100644 --- a/frontend/src/components/SpreadsheetTable.tsx +++ b/frontend/src/components/SpreadsheetTable.tsx @@ -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 ( - + {isInvalid ? ( 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); diff --git a/frontend/src/components/UploadDialog.tsx b/frontend/src/components/UploadDialog.tsx index b80628c..8e8690e 100644 --- a/frontend/src/components/UploadDialog.tsx +++ b/frontend/src/components/UploadDialog.tsx @@ -98,6 +98,11 @@ const UploadDialog: React.FC = ({ open, onClose, selectedShip } }; + // Count rows with directory defaulted + const defaultDirectoryCount = fileSummary?.raw_data + ? fileSummary.raw_data.filter((row) => row.default_set).length + : 0; + return ( <> @@ -155,6 +160,11 @@ const UploadDialog: React.FC = ({ open, onClose, selectedShip The file is validated successfully with no errors. )} + + {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."} + Dewars: {fileSummary.dewars_count} Pucks: {fileSummary.pucks_count} Samples: {fileSummary.samples_count}