Skip to content

Commit

Permalink
feat (UI): add mixture components table with calculations
Browse files Browse the repository at this point in the history
- add select sample type dropdown
- add component table header
- add liquid components section
- add liquid components calculations
- add solid components section
- add solid components calculations
- drag and drop samples in mixture comps table
- do not render structure editor & cas fast input for mixtures
- move component between tables
- option to merge components
- calculate total MW for mixtures & use in reaction table
- mixture samples in reactions scheme
- solvent volume column only for mixture samples
- add the preview image to component in mixtures
- modified the calculations related to various fields
- added the field for required total volume
- modified the structure of the solid components table
- import the molarity value from sample in drag-n-drop into component
- when Target Concentration is updated, then Amount and Volume gets recalculated
- re-calculate amount_mol and related attributes, when Purity is updated
- add the functionality to lock concentration for multiple components
- when there is Total Conc. and amount, then the total volume is calculated
- fix the amount_mol calculations to use the density or stock
- disable the lock concentration button, if it is 0
- lock the Ratio when total conc. is locked
- fix the tooltip buttons
- show indication in the sample list if the sample is a mixture
- split sample with components

fix (UI): cannot create a single molecule
fix: issue with the Amount field not being set for mixture components
fix: fetching of components after fetching the sample causing the sample appear as edited
fix: input fields CSS
fix: sample type selection input field

test: added test codes

refactor: code
refactor: eslint warnings
refactor: moved packs/src files to javascript directory
  • Loading branch information
Tasnim Mehzabin committed Feb 12, 2025
1 parent ed323fd commit 0188cbb
Show file tree
Hide file tree
Showing 25 changed files with 2,449 additions and 273 deletions.
2 changes: 1 addition & 1 deletion app/api/chemotion/component_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class ComponentAPI < Grape::API
)
end
# Delete components
molecule_ids_to_keep = components_params.map { |cp| cp[:component_properties][:molecule_id] }.compact
molecule_ids_to_keep = components_params.filter_map { |cp| cp[:component_properties][:molecule_id] }
Component.where(sample_id: sample_id)
.where.not("CAST(component_properties ->> 'molecule_id' AS INTEGER) IN (?)", molecule_ids_to_keep)
&.destroy_all
Expand Down
2 changes: 0 additions & 2 deletions app/api/chemotion/sample_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,6 @@ class SampleAPI < Grape::API
molecular_mass: params[:molecular_mass],
sum_formula: params[:sum_formula],
sample_type: params[:sample_type],
molfile: params[:molfile],
stereo: params[:stereo],
sample_details: params[:sample_details],
}
micro_att = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { isEqual } from 'lodash';
import { Form, InputGroup, Button } from 'react-bootstrap';
import {
Form, InputGroup, Button, OverlayTrigger, Tooltip,
} from 'react-bootstrap';
import { metPreConv, metPrefSymbols } from 'src/utilities/metricPrefix';

export default class NumeralInputWithUnitsCompo extends Component {
Expand Down Expand Up @@ -107,15 +109,6 @@ export default class NumeralInputWithUnitsCompo extends Component {
}, () => this._onChangeCallback());
}

handleInputDoubleClick() {
if (this.state.block) {
this.setState({
block: false,
value: 0,
});
}
}

_onChangeCallback() {
if (this.props.onChange) {
this.props.onChange({ ...this.state, unit: this.props.unit });
Expand Down Expand Up @@ -151,7 +144,7 @@ export default class NumeralInputWithUnitsCompo extends Component {

render() {
const {
size, variant, disabled, label, unit, name
size, variant, disabled, label, unit, name, showInfoTooltipTotalVol
} = this.props;
const {
showString, value, metricPrefix,
Expand Down Expand Up @@ -189,9 +182,25 @@ export default class NumeralInputWithUnitsCompo extends Component {
return (
<div>
{label && <Form.Label className="me-2">{label}</Form.Label>}
{showInfoTooltipTotalVol && (
<OverlayTrigger
placement="top"
overlay={(
<Tooltip id="info-total-volume">
<p>
It is only a value given manually, i.e. volume by definition - not (re)calculated.
<br/>
Recalculation occurs only when the attributes of a component with a locked total concentration are
modified.
</p>
</Tooltip>
)}
>
<i className="ms-1 fa fa-info-circle"/>
</OverlayTrigger>
)}
<InputGroup
className="d-flex flex-nowrap align-items-center w-100"
onDoubleClick={event => this.handleInputDoubleClick(event)}
>
<Form.Control
type="text"
Expand All @@ -213,7 +222,7 @@ export default class NumeralInputWithUnitsCompo extends Component {
return (
<div>
{label && <Form.Label className="me-2">{label}</Form.Label>}
<div onDoubleClick={event => this.handleInputDoubleClick(event)}>
<div>
<Form.Control
type="text"
disabled={inputDisabled}
Expand All @@ -223,7 +232,6 @@ export default class NumeralInputWithUnitsCompo extends Component {
onChange={event => this._handleInputValueChange(event)}
onFocus={event => this._handleInputValueFocus(event)}
onBlur={event => this._handleInputValueBlur(event)}
onDoubleClick={event => this.handleInputDoubleClick(event)}
name={name}
className="flex-grow-1"
/>
Expand All @@ -245,7 +253,8 @@ NumeralInputWithUnitsCompo.propTypes = {
label: PropTypes.node,
variant: PropTypes.string,
size: PropTypes.string,
name: PropTypes.string
name: PropTypes.string,
showInfoTooltipTotalVol: PropTypes.bool,
};

NumeralInputWithUnitsCompo.defaultProps = {
Expand All @@ -255,5 +264,6 @@ NumeralInputWithUnitsCompo.defaultProps = {
disabled: false,
block: false,
variant: 'light',
name: ''
name: '',
showInfoTooltipTotalVol: false,
};
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ class Material extends Component {
);
}


materialLoading(material, showLoadingColumn) {
if (!showLoadingColumn) {
return false;
Expand Down Expand Up @@ -682,7 +681,7 @@ class Material extends Component {
}
onChange={(e) => this.debounceHandleAmountUnitChange(e, material.amount_g)}
onMetricsChange={this.handleMetricsChange}
variant={material.error_mass ? 'error' : massBsStyle}
variant={material.error_mass ? 'danger' : massBsStyle}
size="sm"
name="molecular-weight"
/>
Expand Down Expand Up @@ -752,10 +751,13 @@ class Material extends Component {
</div>
</OverlayTrigger>
</td>

<td>
{this.amountField(material, metricPrefixes, reaction, massBsStyle, metric)}
</td>

{this.materialVolume(material)}

<td>
<NumeralInputWithUnitsCompo
key={material.id}
Expand Down Expand Up @@ -812,9 +814,13 @@ class Material extends Component {
}

generateMolecularWeightTooltipText(sample, reaction) {
const isProduct = reaction.products.includes(sample);
const molecularWeight = sample.decoupled ?
const isProduct = reaction.products.includes(sample)
let molecularWeight = sample.decoupled ?
(sample.molecular_mass) : (sample.molecule && sample.molecule.molecular_weight);

if (sample.sample_type === 'Mixture' && sample.reference_molecular_weight) {
molecularWeight = sample.reference_molecular_weight.toFixed(4);
}
let theoreticalMassPart = "";
if (isProduct && sample.maxAmount) {
theoreticalMassPart = `, max theoretical mass: ${Math.round(sample.maxAmount * 10000) / 10} mg`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable react/sort-comp */
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
Accordion, Form, Row, Col, Button, InputGroup
Expand Down Expand Up @@ -34,9 +34,11 @@ import {
} from 'src/utilities/UnitsConversion';
import GasPhaseReactionActions from 'src/stores/alt/actions/GasPhaseReactionActions';
import GasPhaseReactionStore from 'src/stores/alt/stores/GasPhaseReactionStore';
import ComponentsFetcher from 'src/fetchers/ComponentsFetcher';
import Component from 'src/models/Component';
import { parseNumericString } from 'src/utilities/MathUtils';

export default class ReactionDetailsScheme extends Component {
export default class ReactionDetailsScheme extends React.Component {
constructor(props) {
super(props);

Expand Down Expand Up @@ -106,9 +108,34 @@ export default class ReactionDetailsScheme extends Component {
splitSample.reference = false;
}

this.insertSolventExtLabel(splitSample, tagGroup, extLabel);
reaction.addMaterialAt(splitSample, null, tagMaterial, tagGroup);
this.onReactionChange(reaction, { schemaChanged: true });
if (splitSample.sample_type === 'Mixture') {
ComponentsFetcher.fetchComponentsBySampleId(srcSample.id)
.then(async components => {
const sampleComponents = components.map(component => {
const { component_properties, ...rest } = component;
const sampleData = {
...rest,
...component_properties
};
return new Component(sampleData);
});
await splitSample.initialComponents(sampleComponents);
const comp = sampleComponents.find(component => component.amount_mol > 0 && component.molarity_value > 0);
if (comp) {
splitSample.target_amount_value = comp.amount_mol / comp.molarity_value;
splitSample.target_amount_unit = 'l';
}
reaction.addMaterialAt(splitSample, null, tagMaterial, tagGroup);
this.onReactionChange(reaction, { schemaChanged: true });
})
.catch((errorMessage) => {
console.log(errorMessage);
});
} else {
this.insertSolventExtLabel(splitSample, tagGroup, extLabel);
reaction.addMaterialAt(splitSample, null, tagMaterial, tagGroup);
this.onReactionChange(reaction, { schemaChanged: true });
}
}

insertSolventExtLabel(splitSample, materialGroup, external_label) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import { copyToClipboard } from 'src/utilities/clipboard';
const MWPrecision = 6;

const decoupleCheck = (sample) => {
if (!sample.decoupled && sample.molecule && sample.molecule.id === '_none_') {
if (!sample.decoupled && sample.molecule && sample.molecule.id === '_none_' && !sample.sample_type == 'Mixture') {
NotificationActions.add({
title: 'Error on Sample creation', message: 'The molecule structure is required!', level: 'error', position: 'tc'
});
Expand Down Expand Up @@ -164,6 +164,7 @@ export default class SampleDetails extends React.Component {

this.handleStructureEditorSave = this.handleStructureEditorSave.bind(this);
this.handleStructureEditorCancel = this.handleStructureEditorCancel.bind(this);
this.splitSmiles = this.splitSmiles.bind(this);
}

componentDidMount() {
Expand Down Expand Up @@ -311,6 +312,7 @@ export default class SampleDetails extends React.Component {
const fetchMolecule = (fetchFunction) => {
fetchFunction()
.then(fetchSuccess).catch(fetchError).finally(() => {
this.splitSmiles(editor, svgFile)
this.hideStructureEditor();
});
};
Expand All @@ -322,7 +324,7 @@ export default class SampleDetails extends React.Component {
} else {
fetchMolecule(() => MoleculesFetcher.fetchBySmi(smiles, svgFile, molfile, editor));
}
}
}

handleStructureEditorCancel() {
this.hideStructureEditor();
Expand Down Expand Up @@ -454,6 +456,7 @@ export default class SampleDetails extends React.Component {
molfile={molfile}
hasParent={hasParent}
hasChildren={hasChildren}
sample={sample}
/>
);
}
Expand Down Expand Up @@ -661,7 +664,9 @@ export default class SampleDetails extends React.Component {

saveBtn(sample, closeView = false) {
let submitLabel = (sample && sample.isNew) ? 'Create' : 'Save';
const isDisabled = !sample.can_update;
const hasComponents = sample.sample_type !== 'Mixture'
|| (sample.components && sample.components.length > 0);
const isDisabled = !sample.can_update || !hasComponents;
if (closeView) submitLabel += ' and close';

return (
Expand All @@ -677,6 +682,11 @@ export default class SampleDetails extends React.Component {
}

elementalPropertiesItem(sample) {
// avoid empty ListGroupItem
if (!sample.molecule_formula || sample.sample_type === 'Mixture') {
return false;
}

const label = sample.contains_residues
? 'Polymer section / Elemental composition'
: 'Elemental composition';
Expand Down Expand Up @@ -874,7 +884,9 @@ export default class SampleDetails extends React.Component {
const timesTag = (
<i className="fa fa-times" />
);
const sampleUpdateCondition = !this.sampleIsValid() || !sample.can_update;
const hasComponents = sample.sample_type !== 'Mixture'
|| (sample.components && sample.components.length > 0);
const sampleUpdateCondition = !this.sampleIsValid() || !sample.can_update || !hasComponents;

const elementToSave = activeTab === 'inventory' ? 'Chemical' : 'Sample';
const saveAndClose = (saveBtnDisplay &&
Expand Down Expand Up @@ -952,6 +964,7 @@ export default class SampleDetails extends React.Component {
) : null;

const inventoryLabel = sample.inventory_sample && sample.inventory_label ? sample.inventory_label : null;
const isMixture = sample.sample_type === 'Mixture';

return (
<div className="d-flex align-items-center justify-content-between">
Expand All @@ -968,7 +981,7 @@ export default class SampleDetails extends React.Component {
<ElementReactionLabels element={sample} key={`${sample.id}_reactions`} />
<PubchemLabels element={sample} />
<HeaderCommentSection element={sample} />
{sample.isNew && <FastInput fnHandle={this.handleFastInput} />}
{sample.isNew && !isMixture && <FastInput fnHandle={this.handleFastInput} />}
</div>
<div className="d-flex align-items-center gap-1">
{decoupleCb}
Expand All @@ -995,6 +1008,7 @@ export default class SampleDetails extends React.Component {
}

sampleInfo(sample) {
const isMixture = sample.sample_type === 'Mixture';
const style = { height: 'auto', marginBottom: '20px' };
let pubchemLcss = (sample.pubchem_tag && sample.pubchem_tag.pubchem_lcss
&& sample.pubchem_tag.pubchem_lcss.Record) || null;
Expand All @@ -1018,7 +1032,7 @@ export default class SampleDetails extends React.Component {
<h4><SampleName sample={sample} /></h4>
<h5>{this.sampleAverageMW(sample)}</h5>
<h5>{this.sampleExactMW(sample)}</h5>
{sample.isNew ? null : <h6>{this.moleculeCas()}</h6>}
{sample.isNew || isMixture ? null : <h6>{this.moleculeCas()}</h6>}
{lcssSign}
</Col>
<Col md={8} className="position-relative">
Expand Down Expand Up @@ -1184,12 +1198,21 @@ export default class SampleDetails extends React.Component {
}

sampleAverageMW(sample) {
const mw = sample.molecule_molecular_weight;
let mw;

if (sample.sample_type === 'Mixture' && sample.sample_details) {
mw = sample.total_molecular_weight;
} else {
mw = sample.molecule_molecular_weight;
}

if (mw) return <ClipboardCopyText text={`${mw.toFixed(MWPrecision)} g/mol`} />;
return '';
}

sampleExactMW(sample) {
if (sample.sample_type === 'Mixture' && sample.sample_details) { return }

const mw = sample.molecule_exact_molecular_weight;
if (mw) return <ClipboardCopyText text={`Exact mass: ${mw.toFixed(MWPrecision)} g/mol`} />;
return '';
Expand Down Expand Up @@ -1218,6 +1241,19 @@ export default class SampleDetails extends React.Component {
});
}

splitSmiles(editor, svgFile) {
const { sample } = this.state;
if (sample.sample_type !== 'Mixture' || !sample.molecule_cano_smiles || sample.molecule_cano_smiles === '') { return }

const mixtureSmiles = sample.molecule_cano_smiles.split('.')
if (mixtureSmiles) {
sample.splitSmilesToMolecule(mixtureSmiles, editor)
.then(() => {
this.setState({ sample });
});
}
}

toggleInchi() {
const { showInchikey } = this.state;
this.setState({ showInchikey: !showInchikey });
Expand Down
Loading

0 comments on commit 0188cbb

Please sign in to comment.