-
-
-
- {this.textInput(sample, 'name', 'Name')}
-
-
- {this.textInput(sample, 'external_label', 'External label')}
-
-
- {this.textInput(sample, 'xref_inventory_label', 'Inventory label')}
+ {' '}
+ {selectedSampleType !== 'Mixture' ? (
+
+
+
+
+
+ {this.moleculeInput()}
+ {this.stereoAbsInput()}
+ {this.stereoRelInput()}
+
+ {
+ enableSampleDecoupled ? (
+
+ {this.decoupledCheckbox(sample)}
+
+ ) : null
+ }
-
- {this.drySolventCheckbox(sample)}
+
+
+
+
+
+
+ {this.textInput(sample, 'name', 'Name')}
+
+
+ {this.textInput(sample, 'external_label', 'External label')}
+
+
+ {this.textInput(sample, 'xref_inventory_label', 'Inventory label')}
+
+
+ {this.drySolventCheckbox(sample)}
+
- {/*
- Solvent
- {this.sampleSolvent(sample)}
-
*/}
-
-
-
- {sample.decoupled
- && (
-
- {
- this.numInput(sample, 'molecular_mass', 'g/mol', ['n'], 5, 'Molecular mass', '', isDisabled)
- }
-
- {
- this.textInput(sample, 'sum_formula', 'Sum formula')
- }
-
-
- )}
-
-
-
-
-
-
-
-
- {/* eslint-disable-next-line jsx-a11y/label-has-for */}
-
-
- {this.infoButton()}
-
-
-
- {this.sampleAmount(sample)}
-
-
-
-
- {
- this.numInputWithoutTable(sample, 'density', 'g/ml', ['n'], 5, '', '', polyDisabled, '', false, isPolymer)
- }
-
-
- {
- this.numInputWithoutTable(sample, 'molarity_value', 'M', ['n'], 5, '', '', polyDisabled, '', false, isPolymer)
- }
-
-
-
- {
- this.numInputWithoutTable(sample, 'purity', 'n', ['n'], 5, 'Purity/Concentration', '', isDisabled)
+
+
+ {sample.decoupled
+ && (
+
+ {
+ this.numInput(sample, 'molecular_mass', 'g/mol', ['n'], 5, 'Molecular mass', '', isDisabled)
+ }
+
+ {
+ this.textInput(sample, 'sum_formula', 'Sum formula')
}
+
+
+ )}
+
+
+
+
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-for */}
+
+
+ {this.infoButton()}
+
-
-
-
-
-
-
-
-
+
+ {this.sampleAmount(sample)}
+
+
+
+
+ {
+ this.numInputWithoutTable(sample, 'density', 'g/ml', ['n'], 5, '', '', polyDisabled, '', false, isPolymer)
+ }
+
+
+ {
+ this.numInputWithoutTable(sample, 'molarity_value', 'M', ['n'], 5, '', '', polyDisabled, '', false, isPolymer)
+ }
+
+
+
+ {
+ this.numInputWithoutTable(sample, 'purity', 'n', ['n'], 5, 'Purity/Concentration', '', isDisabled)
+ }
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+ {this.textInput(sample, 'name', 'Name')}
+
+
+ {this.textInput(sample, 'external_label', 'External label')}
+
+
+ {this.textInput(sample, 'xref_inventory_label', 'Inventory label')}
+
+
+ )}
- {this.additionalProperties(sample)}
+ Mixture Components:
+
+
+
+ {selectedSampleType === 'Mixture' ? this.totalAmount(sample) : null}
+
+
+ {selectedSampleType === 'Mixture' ? this.mixtureComponentsList(sample) : null}
+
+
+ {this.renderCheckbox('enableComponentLabel', 'Enable Label', 'enable-component-label')}
+ {this.renderCheckbox('enableComponentPurity', 'Enable Purity', 'enable-component-purity')}
+
@@ -835,6 +960,10 @@ export default class SampleForm extends React.Component {
+
+ {this.additionalProperties(sample)}
+
+
{this.sampleDescription(sample)}
diff --git a/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.js b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.js
index 2b85330459..43693fb71e 100644
--- a/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.js
+++ b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.js
@@ -9,8 +9,9 @@ import { defaultMultiSolventsSmilesOptions } from 'src/components/staticDropdown
import MoleculesFetcher from 'src/fetchers/MoleculesFetcher';
import { ionic_liquids } from 'src/components/staticDropdownOptions/ionic_liquids';
import NotificationActions from 'src/stores/alt/actions/NotificationActions';
+import NumeralInputWithUnitsCompo from 'src/apps/mydb/elements/details/NumeralInputWithUnitsCompo';
-function SolventDetails({ solvent, deleteSolvent, onChangeSolvent }) {
+function SolventDetails({ solvent, deleteSolvent, onChangeSolvent, sampleType }) {
if (!solvent) {
return null;
}
@@ -25,7 +26,14 @@ function SolventDetails({ solvent, deleteSolvent, onChangeSolvent }) {
onChangeSolvent(solvent);
};
+ const changeVolume = (event) => {
+ solvent.amount_l = event.value;
+ onChangeSolvent(solvent);
+ };
+
// onChangeRatio
+ const metricPrefixes = ['m', 'n', 'u'];
+ const metric = (solvent.metrics && solvent.metrics.length > 2 && metricPrefixes.indexOf(solvent.metrics[1]) > -1) ? solvent.metrics[1] : 'm';
return (
@@ -37,6 +45,18 @@ function SolventDetails({ solvent, deleteSolvent, onChangeSolvent }) {
disabled
/>
+ {sampleType && sampleType === 'Mixture' && (
+
+
+
+ )}
0) {
let key = -1;
@@ -81,6 +102,7 @@ function SampleSolventGroup({
solvent={solv}
deleteSolvent={deleteSolvent}
onChangeSolvent={onChangeSolvent}
+ sampleType={sampleType}
/>
));
});
@@ -133,7 +155,14 @@ function SampleSolventGroup({
menuContainerStyle={{ minHeight: '200px' }}
style={{ marginBottom: '10px' }}
/>
- { sampleSolvents && sampleSolvents.length > 0 && (
+ { sampleSolvents && sampleSolvents.length > 0 && sampleType === 'Mixture' && (
+ <>
+ Label:
+ Volume:
+ Ratio:
+ >
+ )}
+ { sampleSolvents && sampleSolvents.length > 0 && sampleType !== 'Mixture' && (
<>
Label:
Ratio:
diff --git a/app/packs/src/components/common/SampleName.js b/app/packs/src/components/common/SampleName.js
index fd57e9cc2b..6867cece41 100644
--- a/app/packs/src/components/common/SampleName.js
+++ b/app/packs/src/components/common/SampleName.js
@@ -33,6 +33,17 @@ const SampleName = ({ sample }) => {
);
}
+
+ if (sample.sample_type == 'Mixture' && sample.components) {
+ const title = sample.components.map(comp => comp.molecule.iupac_name).join(', ');
+ return (
+
+
{sample.name}
+ {title}
+
+ );
+ }
+
return (
diff --git a/app/packs/src/components/staticDropdownOptions/options.js b/app/packs/src/components/staticDropdownOptions/options.js
index 0624e627eb..b654850999 100644
--- a/app/packs/src/components/staticDropdownOptions/options.js
+++ b/app/packs/src/components/staticDropdownOptions/options.js
@@ -658,3 +658,8 @@ export const amountSearchOptions = [
{ label: 'l', value: 'l' },
{ label: 'mol', value: 'mol' },
];
+
+export const SampleTypesOptions = [
+ { label: 'Single molecule', value: 'Micromolecule' },
+ { label: 'Mixture', value: 'Mixture' },
+]
diff --git a/app/packs/src/components/structureEditor/StructureEditorSet.js b/app/packs/src/components/structureEditor/StructureEditorSet.js
index 9194343ef8..0ec70bd1a8 100644
--- a/app/packs/src/components/structureEditor/StructureEditorSet.js
+++ b/app/packs/src/components/structureEditor/StructureEditorSet.js
@@ -24,6 +24,8 @@ const EditorAttrs =
setMfFuncName: 'setMolecule',
getMfFuncName: 'getMolfile',
getMfWithCallback: false,
+ getRxnFuncName: 'getRxn',
+ getSVGFuncName: 'getSVG',
}
},
chemdraw:
diff --git a/app/packs/src/fetchers/ComponentsFetcher.js b/app/packs/src/fetchers/ComponentsFetcher.js
new file mode 100644
index 0000000000..ba538e98cd
--- /dev/null
+++ b/app/packs/src/fetchers/ComponentsFetcher.js
@@ -0,0 +1,42 @@
+import 'whatwg-fetch';
+
+export default class ComponentsFetcher {
+ static fetchComponentsBySampleId(sampleId) {
+ return fetch(`/api/v1/components/${sampleId}`, {
+ credentials: 'same-origin',
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to fetch components');
+ }
+ return response.json();
+ })
+ .catch((errorMessage) => {
+ console.error(errorMessage);
+ });
+ }
+
+ static saveOrUpdateComponents(sample, components) {
+ const serializedComponents = components.map(component => (component.serializeComponent()));
+ return fetch( '/api/v1/components', {
+ credentials: 'same-origin',
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ sample_id: sample.id,
+ components: serializedComponents,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error(`Failed to update components`);
+ }
+ return response.json();
+ })
+ .catch((error) => {
+ console.error(`Error updating components:`, error);
+ });
+ }
+}
diff --git a/app/packs/src/models/Component.js b/app/packs/src/models/Component.js
new file mode 100644
index 0000000000..6f43a03f9a
--- /dev/null
+++ b/app/packs/src/models/Component.js
@@ -0,0 +1,274 @@
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable camelcase */
+import React from 'react';
+import Sample from 'src/models/Sample';
+
+export default class Component extends Sample {
+ constructor(props) {
+ super(props);
+ }
+
+ get has_density() {
+ return this.density > 0 && this.starting_molarity_value === 0;
+ }
+
+ get amount_mol() {
+ return this._amount_mol;
+ }
+
+ set amount_mol(amount_mol) {
+ this._amount_mol = amount_mol;
+ }
+
+ get amount_g() {
+ return this._amount_g;
+ }
+
+ set amount_g(amount_g) {
+ return this._amount_g = amount_g;
+ }
+
+ get amount_l() {
+ return this._amount_l;
+ }
+
+ set amount_l(amount_l) {
+ return this._amount_l = amount_l;
+ }
+
+ get svgPath() {
+ return this.molecule && this.molecule.molecule_svg_file
+ ? `/images/molecules/${this.molecule.molecule_svg_file}` : '';
+ }
+
+ setAmount(amount, totalVolume) {
+ if (!amount.unit || Number.isNaN(amount.value)) {
+ return;
+ }
+ if (this.density && this.density > 0 && this.material_group !== 'solid') {
+ this.setAmountDensity(amount, totalVolume);
+ } else {
+ this.setAmountConc(amount, totalVolume);
+ }
+ }
+
+ // refactor this part
+ setMol(amount, totalVolume) {
+ if (Number.isNaN(amount.value) || amount.unit !== 'mol') return;
+
+ this.amount_mol = amount.value;
+ const purity = this.purity || 1.0;
+
+ if (this.material_group === 'liquid') {
+ if (this.density && this.density > 0) { // if density is given
+ this.starting_molarity_value = 0;
+ this.amount_l = (this.amount_mol * this.molecule_molecular_weight * purity) / (this.density * 1000);
+ } else { // if stock concentration is given
+ this.density = 0;
+ this.amount_l = this.amount_mol / (this.starting_molarity_value * purity);
+ }
+ if (totalVolume && totalVolume > 0) {
+ const concentration = this.amount_mol / (totalVolume * purity);
+ this.molarity_value = concentration;
+ this.concn = concentration;
+ this.molarity_unit = 'M';
+ }
+ }
+ if (this.material_group === 'solid') {
+ // update concentrations
+ if (this.amount_l === 0 || this.amount_g === 0) {
+ this.molarity_value = 0;
+ this.concn = 0;
+ } else if (totalVolume && totalVolume > 0) {
+ const concentration = this.amount_mol / (totalVolume * purity);
+
+ this.concn = concentration;
+ this.molarity_value = concentration;
+
+ this.molarity_unit = 'M';
+ }
+ }
+ }
+
+ setAmountDensity(amount, totalVolume) {
+ this.amount_l = amount.value;
+ this.starting_molarity_value = 0;
+ const purity = this.purity || 1.0;
+ if (this.material_group === 'liquid') {
+ this.amount_g = (this.amount_l * 1000) * this.density;
+ this.amount_mol = (this.amount_g * purity) / this.molecule_molecular_weight;
+
+ const concentration = this.amount_mol / (totalVolume * purity);
+ this.molarity_value = concentration;
+ this.concn = concentration;
+ }
+ }
+
+ setAmountConc(amount, totalVolume) {
+ const purity = this.purity || 1.0;
+ if (amount.unit === 'l') {
+ this.amount_l = amount.value;
+ if (this.starting_molarity_value) {
+ this.amount_mol = this.starting_molarity_value * this.amount_l * purity;
+ }
+
+ const concentration = this.amount_mol / (totalVolume * purity);
+ this.concn = concentration;
+ this.molarity_value = concentration;
+ this.molarity_unit = 'M';
+ } else if (amount.unit === 'g') {
+ this.amount_g = amount.value;
+ this.amount_mol = (this.amount_g * purity) / this.molecule_molecular_weight;
+ if (totalVolume) {
+ this.molarity_value = this.concn = this.amount_mol / (totalVolume * purity);
+ this.molarity_unit = 'M';
+ }
+
+ if (this.amount_l === 0 || this.amount_g === 0) {
+ this.molarity_value = this.concn = 0;
+ }
+ }
+
+ this.density = 0;
+ }
+
+ setConc(amount) {
+ if (!amount.unit || Number.isNaN(amount.value) || amount.unit !== 'mol/l') { return; }
+
+ this.amount_mol = 0;
+ this.amount_g = 0;
+ this.amount_l = 0;
+ this.density = 0;
+
+ // if (this.density && this.density > 0 && concType !== 'startingConc' && this.material_group !== 'solid') {
+ // this.setMolarityDensity(amount, totalVolume);
+ // } else {
+ // this.setMolarity(amount, totalVolume, concType, lockColumn);
+ // }
+ }
+
+ setMolarityDensity(amount, totalVolume) {
+ const purity = this.purity || 1.0;
+ this.molarity_value = this.concn = amount.value;
+ this.molarity_unit = amount.unit;
+ this.starting_molarity_value = 0;
+
+ this.amount_mol = this.molarity_value * totalVolume * purity;
+ this.amount_g = (this.molecule_molecular_weight * this.amount_mol) / purity;
+ this.amount_l = (this.amount_g / this.density) / 1000;
+ }
+
+ setMolarity(amount, totalVolume, concType, lockColumn) {
+ const purity = this.purity || 1.0;
+ if (concType !== 'startingConc') {
+ this.concn = amount.value;
+ this.molarity_value = amount.value;
+ this.molarity_unit = amount.unit;
+ } else {
+ this.starting_molarity_value = amount.value;
+ this.starting_molarity_unit = amount.unit;
+ }
+ if (this.material_group === 'liquid' && lockColumn) {
+ this.amount_mol = this.starting_molarity_value * this.amount_l * purity;
+ this.concn = this.molarity_value = this.amount_mol / (totalVolume * purity);
+ } else if (this.material_group === 'liquid' && !lockColumn) {
+ this.amount_mol = this.molarity_value * totalVolume * purity;
+ this.amount_l = this.amount_mol / (this.starting_molarity_value * purity);
+ } else if (this.material_group === 'solid' && this.concn && totalVolume) {
+ this.amount_mol = this.molarity_value * totalVolume * purity;
+ this.amount_g = this.molecule_molecular_weight * (this.amount_mol / purity);
+ } else if (this.concn === 0) {
+ this.amount_g = 0;
+ this.amount_l = 0;
+ }
+
+ this.density = 0;
+ }
+
+ setDensity(density, lockColumn, totalVolume) {
+ // const purity = this.purity || 1.0;
+ if (!density.unit || isNaN(density.value) || density.unit !== 'g/ml') { return; }
+
+ this.density = density.value;
+ this.starting_molarity_value = 0;
+
+ // const concentration = (this.amount_mol / (totalVolume * purity)) || 0;
+
+ // this.concn = concentration;
+ // this.molarity_value = concentration;
+
+ this.amount_g = 0;
+ this.amount_l = 0;
+ this.amount_mol = 0;
+
+ // if (lockColumn) {
+ // this.amount_g = (this.amount_l * 1000) * this.density;
+ // this.amount_mol = this.amount_g * purity / this.molecule_molecular_weight;
+ // this.concn = this.molarity_value = this.amount_mol / (totalVolume * purity);
+ // } else {
+ // this.amount_mol = this.molarity_value * totalVolume * purity;
+ // this.amount_g = this.amount_mol * this.molecule_molecular_weight / purity;
+ // this.amount_l = (this.amount_g / this.density) / 1000;
+ // }
+ }
+
+ updateRatio(newRatio, materialGroup, totalVolume, referenceMoles) {
+ if (this.equivalent === newRatio) { return; }
+
+ const purity = this.purity || 1.0;
+ this.amount_mol = newRatio * referenceMoles;
+ this.equivalent = newRatio;
+
+ if (materialGroup === 'liquid') {
+ if (!this.has_density) {
+ this.amount_l = this.amount_mol / (this.starting_molarity_value * purity);
+ this.molarity_value = this.concn = this.amount_mol / (totalVolume * purity);
+ this.molarity_unit = 'M';
+ } else if (this.has_density) {
+ this.amount_g = (this.amount_mol * this.molecule_molecular_weight) / purity;
+ this.amount_l = this.amount_g / (this.density * 1000);
+ this.molarity_value = this.concn = this.amount_mol / (totalVolume * purity);
+ this.molarity_unit = 'M';
+ }
+ } else if (materialGroup === 'solid') {
+ this.amount_g = (this.amount_mol * this.molecule_molecular_weight) / purity;
+ this.molarity_value = this.concn = this.amount_mol / (totalVolume * purity);
+ }
+ }
+
+ setPurity(purity, totalVolume) {
+ if (!isNaN(purity) && purity >= 0 && purity <= 1) {
+ this.purity = purity;
+ this.amount_mol = this.molarity_value * totalVolume * this.purity;
+ }
+ }
+
+ get svgPath() {
+ return this.molecule && this.molecule.molecule_svg_file
+ ? `/images/molecules/${this.molecule.molecule_svg_file}` : '';
+ }
+
+ serializeComponent() {
+ return {
+ id: this.id,
+ name: this.name,
+ position: this.position,
+ component_properties: {
+ amount_mol: this.amount_mol,
+ amount_l: this.amount_l,
+ amount_g: this.amount_g,
+ density: this.density,
+ molarity_unit: this.molarity_unit,
+ molarity_value: this.molarity_value,
+ starting_molarity_value: this.starting_molarity_value,
+ starting_molarity_unit: this.starting_molarity_unit,
+ molecule_id: this.molecule.id,
+ equivalent: this.equivalent,
+ parent_id: this.parent_id,
+ material_group: this.material_group,
+ reference: this.reference,
+ purity: this.purity,
+ }
+ };
+ }
+}
diff --git a/app/packs/src/models/Sample.js b/app/packs/src/models/Sample.js
index 0a1c452606..835f140ae9 100644
--- a/app/packs/src/models/Sample.js
+++ b/app/packs/src/models/Sample.js
@@ -8,6 +8,7 @@ import Molecule from 'src/models/Molecule';
import UserStore from 'src/stores/alt/stores/UserStore';
import Container from 'src/models/Container';
import Segment from 'src/models/Segment';
+import MoleculesFetcher from 'src/fetchers/MoleculesFetcher';
const prepareRangeBound = (args = {}, field) => {
const argsNew = args;
@@ -171,6 +172,7 @@ export default class Sample extends Element {
static buildEmpty(collection_id) {
const sample = new Sample({
collection_id,
+ name: '',
type: 'sample',
external_label: '',
target_amount_value: 0,
@@ -200,7 +202,9 @@ export default class Sample extends Element {
inventory_sample: false,
molecular_mass: 0,
sum_formula: '',
- xref: {}
+ xref: {},
+ sample_type: 'Micromolecule',
+ components: []
});
sample.short_label = Sample.buildNewShortLabel();
@@ -332,6 +336,8 @@ export default class Sample extends Element {
sum_formula: this.sum_formula,
inventory_sample: this.inventory_sample,
segments: this.segments.map((s) => s.serialize()),
+ sample_type: this.sample_type,
+ sample_details: this.sample_details,
});
return serialized;
@@ -488,14 +494,21 @@ export default class Sample extends Element {
}
get molarity_value() {
+ if (this.sample_type === 'Mixture' && this.reference_component) {
+ return this.reference_molarity_value;
+ }
return this._molarity_value;
}
set molarity_value(molarity_value) {
this._molarity_value = molarity_value;
+ this.concn = molarity_value
}
get molarity_unit() {
+ if (this.sample_type === 'Mixture' && this.reference_component) {
+ return this.reference_molarity_unit;
+ }
return this._molarity_unit;
}
@@ -503,6 +516,22 @@ export default class Sample extends Element {
this._molarity_unit = molarity_unit;
}
+ get starting_molarity_value() {
+ return this._starting_molarity_value;
+ }
+
+ set starting_molarity_value(starting_molarity_value) {
+ this._starting_molarity_value = starting_molarity_value;
+ }
+
+ get starting_molarity_unit() {
+ return this._starting_molarity_unit;
+ }
+
+ set starting_molarity_unit(starting_molarity_unit) {
+ this._starting_molarity_unit = starting_molarity_unit;
+ }
+
get imported_readout() {
return this._imported_readout;
}
@@ -528,10 +557,14 @@ export default class Sample extends Element {
}
setAmount(amount) {
+ const prevTotalVolume = this.amount_l;
if (amount.unit && !isNaN(amount.value)) {
this.amount_value = amount.value;
this.amount_unit = amount.unit;
}
+ if (this.sample_type === 'Mixture' && this.components) {
+ this.updateMixtureComponentVolume(prevTotalVolume);
+ }
}
setUnitMetrics(unit, metricPrefix) {
@@ -630,7 +663,7 @@ export default class Sample extends Element {
}
get has_molarity() {
- return this.molarity_value > 0 && this.density === 0;
+ return this.molarity_value > 0 && (this.density === 0 || !this.density);
}
get has_density() {
@@ -784,6 +817,9 @@ export default class Sample extends Element {
}
get molecule_molecular_weight() {
+ if (this.sample_type === 'Mixture') {
+ return this.reference_molecular_weight;
+ }
if (this.decoupled) {
return this.molecular_mass;
}
@@ -887,7 +923,8 @@ export default class Sample extends Element {
}
get isValid() {
- return (this && ((this.molfile && !this.decoupled) || this.decoupled)
+ const isValidMixture = this.sample_type === 'Mixture' && this.components?.length > 0;
+ return (this && ((this.molfile && !this.decoupled) || this.decoupled || isValidMixture)
&& !this.error_loading && !this.error_polymer_type);
}
@@ -935,6 +972,55 @@ export default class Sample extends Element {
return this._maxAmount;
}
+ set sample_details(sample_details) {
+ this._sample_details = sample_details
+ }
+
+ get sample_details() {
+ return this._sample_details;
+ }
+
+ set total_molecular_weight(total_molecular_weight) {
+ this.sample_details.total_molecular_weight = total_molecular_weight;
+ }
+
+ get total_molecular_weight() {
+ if (!this.sample_details) { return null }
+ return this.sample_details.total_molecular_weight;
+ }
+
+ get reference_component() {
+ if (!this.components || this.components.length < 1) { return null }
+ return this.components.find(
+ (component) => component.reference === true
+ );
+ }
+
+ get reference_molecular_weight() {
+ if (this.sample_details) {
+ return this.sample_details.reference_molecular_weight;
+ }
+
+ if (!this.reference_component) { return null}
+ return this.reference_component.molecule.molecular_weight
+ }
+
+ set reference_molecular_weight(reference_molecular_weight) {
+ this.sample_details.reference_molecular_weight = reference_molecular_weight;
+ }
+
+ get reference_molarity_value() {
+ if (!this.reference_component) { return null}
+
+ return this.reference_component.molarity_value
+ }
+
+ get reference_molarity_unit() {
+ if (!this.reference_component) { return null}
+
+ return this.reference_component.molarity_unit
+ }
+
serializeMaterial() {
const params = this.serialize();
const extra_params = {
@@ -944,11 +1030,15 @@ export default class Sample extends Element {
show_label: (this.decoupled && !this.molfile) ? true : (this.show_label || false),
waste: this.waste,
coefficient: this.coefficient,
+ components: this.components && this.components.length > 0
+ ? this.components.map(s => s.serializeComponent())
+ : []
};
_.merge(params, extra_params);
return params;
}
+
// Container & Analyses routines
addAnalysis(analysis) {
this.container.children.filter(
@@ -1037,9 +1127,244 @@ export default class Sample extends Element {
&& solv.inchikey && solventToUpdate.inchikey));
if (filteredIndex >= 0) {
tmpSolvents[filteredIndex] = solventToUpdate;
+
+ if (tmpSolvents.length > 1 && tmpSolvents.every(solv => solv.amount_l)) {
+ const totalVolume = tmpSolvents.reduce((acc, solv) => acc + solv.amount_l, 0);
+ const minRatio = Math.min(...tmpSolvents.map(solv => solv.amount_l / totalVolume));
+ const scale = 1 / minRatio;
+
+ tmpSolvents.forEach(solv => {
+ solv.ratio = Number((solv.amount_l / totalVolume) * scale).toFixed(1);
+ });
+ }
}
this.solvent = tmpSolvents;
}
+
+ updateSampleType(newSampleType) {
+ this.sample_type = newSampleType;
+ }
+
+ initialComponents(components) {
+ this.components = components.sort((a, b) => a.position - b.position);
+ this._checksum = this.checksum();
+ }
+
+ async addMixtureComponent(newComponent) {
+ const tmpComponents = [...(this.components || [])];
+ const isNew = !tmpComponents.some(component => component.molecule.iupac_name === newComponent.molecule.iupac_name
+ || component.molecule.inchikey === newComponent.molecule.inchikey
+ || component.molecule_cano_smiles.split('.').includes(newComponent.molecule_cano_smiles)); // check if this component is already part of a merged component (e.g. ionic compound)
+
+ if (!newComponent.material_group){
+ newComponent.material_group = 'liquid';
+ }
+
+ if (!newComponent.purity) {
+ newComponent.purity = 1;
+ }
+
+ if (isNew){
+ tmpComponents.push(newComponent);
+ this.components = tmpComponents;
+ this.setComponentPositions()
+
+ if (!this.molecule_cano_smiles
+ || !this.molecule_cano_smiles.split('.').some(smiles => smiles === newComponent.molecule_cano_smiles)) {
+ const newSmiles = this.molecule_cano_smiles ? `${this.molecule_cano_smiles}.${newComponent.molecule_cano_smiles}` : newComponent.molecule_cano_smiles;
+
+ const result = await MoleculesFetcher.fetchBySmi(newSmiles, null, this.molfile, 'ketcher2');
+ this.molecule = result;
+ this.molfile = result.molfile;
+ }
+ }
+ }
+
+ async deleteMixtureComponent(componentToDelete) {
+ const tmpComponents = [...(this.components || [])];
+ const filteredComponents = tmpComponents.filter(
+ (comp) => comp !== componentToDelete
+ );
+ this.components = filteredComponents;
+
+ if (!this.molecule_cano_smiles || this.molecule_cano_smiles === '') {
+ this.molecule = null;
+ this.molfile = '';
+ return;
+ }
+
+ const smilesToRemove = componentToDelete.molecule_cano_smiles;
+ const newSmiles = this.molecule_cano_smiles
+ .split('.')
+ .filter(smiles => smiles !== smilesToRemove && !smilesToRemove.split('.').includes(smiles))
+ .join('.');
+
+ if (newSmiles !== this.molecule_cano_smiles ){
+ const result = await MoleculesFetcher.fetchBySmi(newSmiles, null, this.molfile, 'ketcher2')
+ this.molecule = result;
+ this.molfile = result.molfile;
+ }
+ this.setComponentPositions()
+ }
+
+
+ updateMixtureComponentVolume(prevTotalVolume) {
+ if (this.components.length < 1) {
+ return;
+ }
+ const totalVolume = this.amount_l;
+
+ this.components.forEach((component) => {
+ const purity = component.purity || 1.0;
+ if (component.material_group === 'liquid') {
+ if (component.concn > 0 && component.starting_molarity_value > 0) {
+ component.amount_l = component.concn * totalVolume / component.starting_molarity_value;
+ } else if (component.density && component.density > 0) {
+ component.amount_l = component.amount_l * (totalVolume / prevTotalVolume);
+ }
+ } else if (component.material_group === 'solid') {
+ if (component.concn > 0) {
+ component.amount_mol = component.concn * totalVolume;
+ component.amount_g = component.molecule_molecular_weight * component.amount_mol;
+ }
+ }
+
+ component.amount_mol = totalVolume * component.molarity_value * purity;
+ });
+ }
+
+ setReferenceComponent(componentIndex) {
+ this.components[componentIndex].equivalent = 1
+ this.components[componentIndex].reference = true
+
+ this.components.forEach((component, index) => {
+ if (index !== componentIndex) {
+ component.reference = false;
+ }
+ });
+
+ if (!this.sample_details) {
+ this.sample_details = {};
+ }
+ this.sample_details.reference_molecular_weight = this.components[componentIndex].molecule.molecular_weight;
+
+ this.updateMixtureComponentEquivalent()
+ }
+
+ updateMixtureComponentEquivalent() {
+ let referenceIndex = this.components.findIndex(component => component.reference);
+
+ if (referenceIndex === -1) {
+ referenceIndex = this.components.findIndex(component => component.position === 0);
+ if (referenceIndex !== -1) {
+ this.setReferenceComponent(referenceIndex);
+ }
+ }
+
+ const referenceMol = this.components[referenceIndex].amount_mol;
+
+ for (let i = 0; i < this.components.length; i++) {
+ if (i === referenceIndex) continue;
+ this.components[i].equivalent = this.components[i].amount_mol / referenceMol;
+ }
+
+ this.updateMixtureMolecularWeight();
+ }
+
+ updateMixtureMolecularWeight() {
+ if (this.components && this.components.length <= 1) { return };
+
+ const totalAmount = this.components.reduce((acc, component) => acc + component.amount_mol, 0);
+ let totalMolecularWeight = 0;
+
+ if (!this.sample_details) {
+ this.sample_details = {};
+ }
+
+ if (totalAmount === 0) {
+ this.sample_details.total_molecular_weight = 0;
+ return;
+ }
+
+ for (let i = 0; i < this.components.length; i++) {
+ const moleFraction = this.components[i].amount_mol / totalAmount
+ totalMolecularWeight += this.components[i].molecule.molecular_weight * moleFraction;
+ }
+
+ this.sample_details.total_molecular_weight = totalMolecularWeight
+ }
+
+ moveMaterial(srcMat, srcGroup, tagMat, tagGroup) {
+ const srcIndex = this.components.findIndex(mat => mat === srcMat);
+ const tagIndex = this.components.findIndex(mat => mat === tagMat);
+
+ if (srcIndex === tagIndex) {
+ return;
+ }
+
+ this.components[srcIndex].material_group = tagGroup;
+
+ if (!tagMat && srcMat !== tagGroup) {
+ return this.setComponentPositions()
+ }
+
+ const movedMat = this.components.splice(srcIndex, 1)[0];
+ this.components.splice(tagIndex, 0, movedMat);
+ this.setComponentPositions()
+ }
+
+ async mergeComponents(srcMat, srcGroup, tagMat, tagGroup) {
+ const srcIndex = this.components.findIndex(mat => mat === srcMat);
+ const tagIndex = this.components.findIndex(mat => mat === tagMat);
+
+ if (srcIndex === -1 || tagIndex === -1) {
+ console.error('Source or target material not found in components.');
+ return;
+ }
+ const newSmiles = `${srcMat.molecule_cano_smiles}.${tagMat.molecule_cano_smiles}`;
+
+ try {
+ const newMolecule = await MoleculesFetcher.fetchBySmi(newSmiles, null, this.molfile, 'ketcher2');
+ const newComponent = Sample.buildNew(newMolecule, this.collection_id);
+ newComponent.material_group = tagGroup;
+
+ await this.deleteMixtureComponent(tagMat)
+ await this.deleteMixtureComponent(srcMat)
+ await this.addMixtureComponent(newComponent);
+
+ } catch (error) {
+ console.error('Error merging components:', error);
+ }
+ }
+
+ setComponentPositions() {
+ this.components.forEach((mat, index) => {
+ mat.position = index;
+ });
+ }
+
+ splitSmilesToMolecule(mixtureSmiles, editor) {
+ const promises = mixtureSmiles.map(smiles => {
+ return MoleculesFetcher.fetchBySmi(smiles, null, null, editor);
+ });
+
+ return Promise.all(promises)
+ .then(mixtureMolecules => {
+ return this.mixtureMoleculeToSubsample(mixtureMolecules);
+ })
+ .catch(errorMessage => {
+ console.log(errorMessage);
+ return [];
+ });
+ }
+
+ mixtureMoleculeToSubsample(mixtureMolecules){
+ mixtureMolecules.map(async molecule => {
+ const newSample = Sample.buildNew(molecule, this.collection_id);
+ await this.addMixtureComponent(newSample);
+ });
+ }
+
}
Sample.counter = 0;
diff --git a/app/packs/src/stores/alt/actions/ElementActions.js b/app/packs/src/stores/alt/actions/ElementActions.js
index 4541c36d64..12e6a13946 100644
--- a/app/packs/src/stores/alt/actions/ElementActions.js
+++ b/app/packs/src/stores/alt/actions/ElementActions.js
@@ -371,7 +371,7 @@ class ElementActions {
return (dispatch) => {
SamplesFetcher.create(params)
.then((result) => {
- dispatch({ element: result, closeView })
+ dispatch({ element: result, closeView, components: params.components })
});
};
}
@@ -380,7 +380,7 @@ class ElementActions {
return (dispatch) => {
SamplesFetcher.create(sample)
.then((newSample) => {
- dispatch({ newSample, reaction, materialGroup })
+ dispatch({ newSample, reaction, materialGroup, components: sample.components })
});
};
}
@@ -429,7 +429,7 @@ class ElementActions {
.then((newSample) => {
reaction.updateMaterial(newSample);
reaction.changed = true;
- dispatch({ reaction, sample: newSample, closeView })
+ dispatch({ reaction, sample: newSample, closeView, components: sample.components })
}).catch((errorMessage) => {
console.log(errorMessage);
});
@@ -440,7 +440,7 @@ class ElementActions {
return (dispatch) => {
SamplesFetcher.update(params)
.then((result) => {
- dispatch({ element: result, closeView })
+ dispatch({ element: result, closeView, components: params.components })
}).catch((errorMessage) => {
console.log(errorMessage);
});
@@ -518,6 +518,18 @@ class ElementActions {
}
}
+ showMixtureMaterial(params) {
+ return (dispatch) => {
+ SamplesFetcher.fetchById(params.sample.id)
+ .then((result) => {
+ params.sample = result;
+ dispatch(params);
+ }).catch((errorMessage) => {
+ console.log(errorMessage);
+ })
+ }
+ }
+
importSamplesFromFile(params) {
return (dispatch) => {
SamplesFetcher.importSamplesFromFile(params)
diff --git a/app/packs/src/stores/alt/stores/ElementStore.js b/app/packs/src/stores/alt/stores/ElementStore.js
index 26be61ef72..44bff0ea39 100644
--- a/app/packs/src/stores/alt/stores/ElementStore.js
+++ b/app/packs/src/stores/alt/stores/ElementStore.js
@@ -40,6 +40,8 @@ import MatrixCheck from 'src/components/common/MatrixCheck';
import GenericEl from 'src/models/GenericEl';
import MessagesFetcher from 'src/fetchers/MessagesFetcher';
+import ComponentsFetcher from 'src/fetchers/ComponentsFetcher';
+import Component from 'src/models/Component';
const fetchOls = (elementType) => {
switch (elementType) {
@@ -661,12 +663,38 @@ class ElementStore {
// -- Samples --
handleFetchSampleById(result) {
- if (!this.state.currentElement || this.state.currentElement._checksum != result._checksum) {
- this.changeCurrentElement(result);
+ if (!this.state.currentElement || this.state.currentElement._checksum != result._checksum) {
+ if (result.sample_type && result.sample_type === 'Mixture') {
+ ComponentsFetcher.fetchComponentsBySampleId(result.id)
+ .then(async components => {
+ const sampleComponents = components.map(component => {
+ const { component_properties, ...rest } = component;
+ const sampleData = {
+ ...rest,
+ ...component_properties
+ };
+ return new Component(sampleData);
+ });
+ await result.initialComponents(sampleComponents);
+ })
+ .catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ }
+ this.changeCurrentElement(result);
+ }
+ }
+
+ handleCreateSample({ element, closeView, components }) {
+ if (element.sample_type && element.sample_type === 'Mixture') {
+ ComponentsFetcher.saveOrUpdateComponents(element, components)
+ .then(async () => {
+ await element.initialComponents(components)
+ })
+ .catch((errorMessage) => {
+ console.log(errorMessage);
+ });
}
- }
-
- handleCreateSample({ element, closeView }) {
UserActions.fetchCurrentUser();
fetchOls('sample');
this.handleRefreshElements('sample');
@@ -675,8 +703,17 @@ class ElementStore {
}
}
- handleCreateSampleForReaction({ newSample, reaction, materialGroup }) {
+ handleCreateSampleForReaction({ newSample, reaction, materialGroup, components }) {
UserActions.fetchCurrentUser();
+ if (newSample.sample_type && newSample.sample_type === 'Mixture') {
+ ComponentsFetcher.saveOrUpdateComponents(newSample, components)
+ .then(async () => {
+ await newSample.initialComponents(components)
+ })
+ .catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ }
reaction.addMaterial(newSample, materialGroup);
this.handleRefreshElements('sample');
ElementActions.handleSvgReactionChange(reaction);
@@ -695,9 +732,18 @@ class ElementStore {
this.changeCurrentElement(sample);
}
- handleUpdateSampleForReaction({ reaction, sample, closeView }) {
+ handleUpdateSampleForReaction({ reaction, sample, closeView, components }) {
// UserActions.fetchCurrentUser();
ElementActions.handleSvgReactionChange(reaction);
+ if (sample.sample_type && sample.sample_type === 'Mixture') {
+ ComponentsFetcher.saveOrUpdateComponents(sample, components)
+ .then(async () => {
+ await sample.initialComponents(components)
+ })
+ .catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ }
if (closeView) {
this.changeCurrentElement(reaction);
} else {
@@ -709,7 +755,16 @@ class ElementStore {
this.handleUpdateElement(sample);
}
- handleUpdateLinkedElement({ element, closeView }) {
+ handleUpdateLinkedElement({ element, closeView, components }) {
+ if (element.sample_type && element.sample_type === 'Mixture') {
+ ComponentsFetcher.saveOrUpdateComponents(element, components)
+ .then(() => {
+ element.initialComponents(components)
+ })
+ .catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ }
if (closeView) {
this.deleteCurrentElement(element);
} else {
@@ -814,6 +869,24 @@ class ElementStore {
}
return { refreshCoefficient: updatedCoefficient };
});
+
+ if (sample.sample_type && sample.sample_type === 'Mixture') {
+ ComponentsFetcher.fetchComponentsBySampleId(sample.id)
+ .then(async components => {
+ const sampleComponents = components.map(component => {
+ const { component_properties, ...rest } = component;
+ const sampleData = {
+ ...rest,
+ ...component_properties
+ };
+ return new Component(sampleData);
+ });
+ await sample.initialComponents(sampleComponents);
+ })
+ .catch((errorMessage) => {
+ console.log(errorMessage);
+ });
+ }
this.changeCurrentElement(sample);
}
diff --git a/app/usecases/reactions/update_materials.rb b/app/usecases/reactions/update_materials.rb
index 7b544d8b11..94a434f8ae 100644
--- a/app/usecases/reactions/update_materials.rb
+++ b/app/usecases/reactions/update_materials.rb
@@ -70,6 +70,10 @@ def execute!
modified_sample = update_existing_sample(sample, fixed_label)
end
+ if sample.components.present? && sample.sample_type == 'Mixture'
+ save_components(modified_sample.id, sample.components)
+ end
+
modified_sample.save_segments(segments: sample.segments, current_user_id: @current_user.id) if sample.segments
modified_sample_ids << modified_sample.id
@@ -211,6 +215,21 @@ def rangebound(lower, upper)
Range.new(lower, upper)
end
end
+
+ def save_components(sample_id, components)
+ components.each do |component_params|
+ molecule_id = component_params[:component_properties][:molecule_id]
+
+ component = Component.where("sample_id = ? AND CAST(component_properties ->> 'molecule_id' AS INTEGER) = ?", sample_id, molecule_id)
+ .first_or_initialize(sample_id: sample_id)
+
+ component.update(
+ name: component_params[:name],
+ position: component_params[:position],
+ component_properties: component_params[:component_properties],
+ )
+ end
+ end
end
end
end
diff --git a/db/migrate/20240118130824_create_micromolecules.rb b/db/migrate/20240118130824_create_micromolecules.rb
new file mode 100644
index 0000000000..e25207aee4
--- /dev/null
+++ b/db/migrate/20240118130824_create_micromolecules.rb
@@ -0,0 +1,8 @@
+class CreateMicromolecules < ActiveRecord::Migration[6.1]
+ def change
+ create_table :micromolecules do |t|
+ t.string :name
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240119122000_add_molfile_to_micromolecules.rb b/db/migrate/20240119122000_add_molfile_to_micromolecules.rb
new file mode 100644
index 0000000000..b9dc9ed3b9
--- /dev/null
+++ b/db/migrate/20240119122000_add_molfile_to_micromolecules.rb
@@ -0,0 +1,7 @@
+class AddMolfileToMicromolecules < ActiveRecord::Migration[6.1]
+ def change
+ add_column :micromolecules, :molfile, :binary
+ add_column :micromolecules, :molfile_version, :string, limit: 20
+ add_column :micromolecules, :stereo, :jsonb
+ end
+end
diff --git a/db/migrate/20240126145309_add_micromolecule_id_to_samples.rb b/db/migrate/20240126145309_add_micromolecule_id_to_samples.rb
new file mode 100644
index 0000000000..94757d86af
--- /dev/null
+++ b/db/migrate/20240126145309_add_micromolecule_id_to_samples.rb
@@ -0,0 +1,5 @@
+class AddMicromoleculeIdToSamples < ActiveRecord::Migration[6.1]
+ def change
+ add_reference :samples, :micromolecule, foreign_key: { to_table: :samples }
+ end
+end
diff --git a/db/migrate/20240418084207_create_components.rb b/db/migrate/20240418084207_create_components.rb
new file mode 100644
index 0000000000..369c7482cc
--- /dev/null
+++ b/db/migrate/20240418084207_create_components.rb
@@ -0,0 +1,12 @@
+class CreateComponents < ActiveRecord::Migration[6.1]
+ def change
+ create_table :components do |t|
+ t.references :sample, null: false, foreign_key: true
+ t.string :name
+ t.integer :position
+ t.jsonb :component_properties
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240418084814_add_sample_type_to_samples.rb b/db/migrate/20240418084814_add_sample_type_to_samples.rb
new file mode 100644
index 0000000000..d914c8e019
--- /dev/null
+++ b/db/migrate/20240418084814_add_sample_type_to_samples.rb
@@ -0,0 +1,5 @@
+class AddSampleTypeToSamples < ActiveRecord::Migration[6.1]
+ def change
+ add_column :samples, :sample_type, :string
+ end
+end
diff --git a/db/migrate/20240603073714_add_sample_details_to_samples.rb b/db/migrate/20240603073714_add_sample_details_to_samples.rb
new file mode 100644
index 0000000000..57a4ec0353
--- /dev/null
+++ b/db/migrate/20240603073714_add_sample_details_to_samples.rb
@@ -0,0 +1,5 @@
+class AddSampleDetailsToSamples < ActiveRecord::Migration[6.1]
+ def change
+ add_column :samples, :sample_details, :jsonb
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index bd4eed0808..75853c94a3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -80,8 +80,8 @@
t.string "token", null: false
t.integer "user_id"
t.inet "ip"
- t.string "role"
t.string "fqdn"
+ t.string "role"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["user_id"], name: "index_authentication_keys_on_user_id"
@@ -212,8 +212,8 @@
create_table "collections_elements", id: :serial, force: :cascade do |t|
t.integer "collection_id"
t.integer "element_id"
- t.string "element_type"
t.datetime "deleted_at"
+ t.string "element_type"
t.index ["collection_id"], name: "index_collections_elements_on_collection_id"
t.index ["deleted_at"], name: "index_collections_elements_on_deleted_at"
t.index ["element_id", "collection_id"], name: "index_collections_elements_on_element_id_and_collection_id", unique: true
@@ -298,6 +298,16 @@
t.index ["section"], name: "index_comments_on_section"
end
+ create_table "components", force: :cascade do |t|
+ t.bigint "sample_id", null: false
+ t.string "name"
+ t.integer "position"
+ t.jsonb "component_properties"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["sample_id"], name: "index_components_on_sample_id"
+ end
+
create_table "computed_props", id: :serial, force: :cascade do |t|
t.integer "molecule_id"
t.float "max_potential", default: 0.0
@@ -486,15 +496,15 @@
t.string "label"
t.string "desc"
t.string "icon_name"
- t.boolean "is_active", default: true, null: false
- t.string "klass_prefix", default: "E", null: false
- t.boolean "is_generic", default: true, null: false
- t.integer "place", default: 100, null: false
t.jsonb "properties_template"
t.integer "created_by"
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "deleted_at"
+ t.boolean "is_active", default: true, null: false
+ t.string "klass_prefix", default: "E", null: false
+ t.boolean "is_generic", default: true, null: false
+ t.integer "place", default: 100, null: false
t.string "uuid"
t.jsonb "properties_release", default: {}
t.datetime "released_at"
@@ -544,12 +554,12 @@
create_table "elements", id: :serial, force: :cascade do |t|
t.string "name"
t.integer "element_klass_id"
- t.string "short_label"
t.jsonb "properties"
t.integer "created_by"
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "deleted_at"
+ t.string "short_label"
t.string "uuid"
t.string "klass_uuid"
t.jsonb "properties_release"
@@ -807,6 +817,15 @@
t.datetime "updated_at", null: false
end
+ create_table "micromolecules", force: :cascade do |t|
+ t.string "name"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.binary "molfile"
+ t.string "molfile_version", limit: 20
+ t.jsonb "stereo"
+ end
+
create_table "molecule_names", id: :serial, force: :cascade do |t|
t.integer "molecule_id"
t.integer "user_id"
@@ -886,16 +905,16 @@
create_table "pg_search_documents", id: :serial, force: :cascade do |t|
t.text "content"
- t.string "searchable_type"
t.integer "searchable_id"
+ t.string "searchable_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["searchable_type", "searchable_id"], name: "index_pg_search_documents_on_searchable_type_and_searchable_id"
end
create_table "predictions", id: :serial, force: :cascade do |t|
- t.string "predictable_type"
t.integer "predictable_id"
+ t.string "predictable_type"
t.jsonb "decision", default: {}, null: false
t.datetime "created_at"
t.datetime "updated_at"
@@ -1008,7 +1027,7 @@
t.datetime "updated_at", null: false
t.string "template", default: "standard"
t.text "mol_serials", default: "--- []\n"
- t.text "si_reaction_settings", default: "---\nName: true\nCAS: true\nFormula: true\nSmiles: true\nInCHI: true\nMolecular Mass: true\nExact Mass: true\nEA: true\n"
+ t.text "si_reaction_settings", default: "---\n:Name: true\n:CAS: true\n:Formula: true\n:Smiles: true\n:InCHI: true\n:Molecular Mass: true\n:Exact Mass: true\n:EA: true\n"
t.text "prd_atts", default: "--- []\n"
t.integer "report_templates_id"
t.index ["author_id"], name: "index_reports_on_author_id"
@@ -1084,8 +1103,8 @@
end
create_table "research_plans_screens", force: :cascade do |t|
- t.bigint "screen_id", null: false
- t.bigint "research_plan_id", null: false
+ t.integer "screen_id"
+ t.integer "research_plan_id"
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "deleted_at"
@@ -1094,8 +1113,8 @@
end
create_table "research_plans_wellplates", force: :cascade do |t|
- t.bigint "research_plan_id", null: false
- t.bigint "wellplate_id", null: false
+ t.integer "research_plan_id"
+ t.integer "wellplate_id"
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "deleted_at"
@@ -1160,6 +1179,7 @@
t.integer "molecule_name_id"
t.string "molfile_version", limit: 20
t.jsonb "stereo"
+ t.string "mol_rdkit"
t.string "metrics", default: "mmm"
t.boolean "decoupled", default: false, null: false
t.float "molecular_mass"
@@ -1167,9 +1187,13 @@
t.jsonb "solvent"
t.boolean "dry_solvent", default: false
t.boolean "inventory_sample", default: false
+ t.bigint "micromolecule_id"
+ t.string "sample_type"
+ t.jsonb "sample_details"
t.index ["deleted_at"], name: "index_samples_on_deleted_at"
t.index ["identifier"], name: "index_samples_on_identifier"
t.index ["inventory_sample"], name: "index_samples_on_inventory_sample"
+ t.index ["micromolecule_id"], name: "index_samples_on_micromolecule_id"
t.index ["molecule_id"], name: "index_samples_on_sample_id"
t.index ["molecule_name_id"], name: "index_samples_on_molecule_name_id"
t.index ["user_id"], name: "index_samples_on_user_id"
@@ -1370,11 +1394,11 @@
t.string "name_abbreviation", limit: 12
t.string "type", default: "Person"
t.string "reaction_name_prefix", limit: 3, default: "R"
+ t.hstore "layout", default: {"sample"=>"1", "screen"=>"4", "reaction"=>"2", "wellplate"=>"3", "research_plan"=>"5"}, null: false
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
- t.hstore "layout", default: {"sample"=>"1", "screen"=>"4", "reaction"=>"2", "wellplate"=>"3", "research_plan"=>"5"}, null: false
t.integer "selected_device_id"
t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token"
@@ -1464,119 +1488,121 @@
t.datetime "updated_at", null: false
t.string "additive"
t.datetime "deleted_at"
- t.jsonb "readouts", default: [{"unit"=>"", "value"=>""}]
t.string "label", default: "Molecular structure", null: false
t.string "color_code"
+ t.jsonb "readouts", default: [{"unit"=>"", "value"=>""}]
t.index ["deleted_at"], name: "index_wells_on_deleted_at"
t.index ["sample_id"], name: "index_wells_on_sample_id"
t.index ["wellplate_id"], name: "index_wells_on_wellplate_id"
end
add_foreign_key "collections", "inventories"
+ add_foreign_key "components", "samples"
add_foreign_key "literals", "literatures"
add_foreign_key "report_templates", "attachments"
add_foreign_key "sample_tasks", "samples"
add_foreign_key "sample_tasks", "users", column: "creator_id"
+ add_foreign_key "samples", "samples", column: "micromolecule_id"
create_function :user_instrument, sql_definition: <<-'SQL'
CREATE OR REPLACE FUNCTION public.user_instrument(user_id integer, sc text)
RETURNS TABLE(instrument text)
LANGUAGE sql
AS $function$
- select distinct extended_metadata -> 'instrument' as instrument from containers c
- where c.container_type='dataset' and c.id in
- (select ch.descendant_id from containers sc,container_hierarchies ch, samples s, users u
- where sc.containable_type in ('Sample','Reaction') and ch.ancestor_id=sc.id and sc.containable_id=s.id
- and s.created_by = u.id and u.id = $1 and ch.generations=3 group by descendant_id)
- and upper(extended_metadata -> 'instrument') like upper($2 || '%')
- order by extended_metadata -> 'instrument' limit 10
- $function$
+ select distinct extended_metadata -> 'instrument' as instrument from containers c
+ where c.container_type='dataset' and c.id in
+ (select ch.descendant_id from containers sc,container_hierarchies ch, samples s, users u
+ where sc.containable_type in ('Sample','Reaction') and ch.ancestor_id=sc.id and sc.containable_id=s.id
+ and s.created_by = u.id and u.id = $1 and ch.generations=3 group by descendant_id)
+ and upper(extended_metadata -> 'instrument') like upper($2 || '%')
+ order by extended_metadata -> 'instrument' limit 10
+ $function$
SQL
create_function :collection_shared_names, sql_definition: <<-'SQL'
CREATE OR REPLACE FUNCTION public.collection_shared_names(user_id integer, collection_id integer)
RETURNS json
LANGUAGE sql
AS $function$
- select array_to_json(array_agg(row_to_json(result))) from (
- SELECT sync_collections_users.id, users.type,users.first_name || chr(32) || users.last_name as name,sync_collections_users.permission_level,
- sync_collections_users.reaction_detail_level,sync_collections_users.sample_detail_level,sync_collections_users.screen_detail_level,sync_collections_users.wellplate_detail_level
- FROM sync_collections_users
- INNER JOIN users ON users.id = sync_collections_users.user_id AND users.deleted_at IS NULL
- WHERE sync_collections_users.shared_by_id = $1 and sync_collections_users.collection_id = $2
- group by sync_collections_users.id,users.type,users.name_abbreviation,users.first_name,users.last_name,sync_collections_users.permission_level
- ) as result
- $function$
+ select array_to_json(array_agg(row_to_json(result))) from (
+ SELECT sync_collections_users.id, users.type,users.first_name || chr(32) || users.last_name as name,sync_collections_users.permission_level,
+ sync_collections_users.reaction_detail_level,sync_collections_users.sample_detail_level,sync_collections_users.screen_detail_level,sync_collections_users.wellplate_detail_level
+ FROM sync_collections_users
+ INNER JOIN users ON users.id = sync_collections_users.user_id AND users.deleted_at IS NULL
+ WHERE sync_collections_users.shared_by_id = $1 and sync_collections_users.collection_id = $2
+ group by sync_collections_users.id,users.type,users.name_abbreviation,users.first_name,users.last_name,sync_collections_users.permission_level
+ ) as result
+ $function$
SQL
create_function :user_ids, sql_definition: <<-'SQL'
CREATE OR REPLACE FUNCTION public.user_ids(user_id integer)
RETURNS TABLE(user_ids integer)
LANGUAGE sql
AS $function$
- select $1 as id
- union
- (select users.id from users inner join users_groups ON users.id = users_groups.group_id WHERE users.deleted_at IS null
- and users.type in ('Group') and users_groups.user_id = $1)
- $function$
+ select $1 as id
+ union
+ (select users.id from users inner join users_groups ON users.id = users_groups.group_id WHERE users.deleted_at IS null
+ and users.type in ('Group') and users_groups.user_id = $1)
+ $function$
SQL
create_function :user_as_json, sql_definition: <<-'SQL'
CREATE OR REPLACE FUNCTION public.user_as_json(user_id integer)
RETURNS json
LANGUAGE sql
AS $function$
- select row_to_json(result) from (
- select users.id, users.name_abbreviation as initials ,users.type,users.first_name || chr(32) || users.last_name as name
- from users where id = $1
- ) as result
- $function$
+ select row_to_json(result) from (
+ select users.id, users.name_abbreviation as initials ,users.type,users.first_name || chr(32) || users.last_name as name
+ from users where id = $1
+ ) as result
+ $function$
SQL
create_function :shared_user_as_json, sql_definition: <<-'SQL'
CREATE OR REPLACE FUNCTION public.shared_user_as_json(in_user_id integer, in_current_user_id integer)
RETURNS json
LANGUAGE plpgsql
AS $function$
- begin
- if (in_user_id = in_current_user_id) then
- return null;
- else
- return (select row_to_json(result) from (
- select users.id, users.name_abbreviation as initials ,users.type,users.first_name || chr(32) || users.last_name as name
- from users where id = $1
- ) as result);
- end if;
- end;
- $function$
+ begin
+ if (in_user_id = in_current_user_id) then
+ return null;
+ else
+ return (select row_to_json(result) from (
+ select users.id, users.name_abbreviation as initials ,users.type,users.first_name || chr(32) || users.last_name as name
+ from users where id = $1
+ ) as result);
+ end if;
+ end;
+ $function$
SQL
create_function :detail_level_for_sample, sql_definition: <<-'SQL'
CREATE OR REPLACE FUNCTION public.detail_level_for_sample(in_user_id integer, in_sample_id integer)
RETURNS TABLE(detail_level_sample integer, detail_level_wellplate integer)
LANGUAGE plpgsql
AS $function$
- declare
- i_detail_level_wellplate integer default 0;
- i_detail_level_sample integer default 0;
- begin
- select max(all_cols.sample_detail_level), max(all_cols.wellplate_detail_level)
- into i_detail_level_sample, i_detail_level_wellplate
- from
- (
- select v_sams_cols.cols_sample_detail_level sample_detail_level, v_sams_cols.cols_wellplate_detail_level wellplate_detail_level
- from v_samples_collections v_sams_cols
- where v_sams_cols.sams_id = in_sample_id
- and v_sams_cols.cols_user_id in (select user_ids(in_user_id))
- union
- select sync_cols.sample_detail_level sample_detail_level, sync_cols.wellplate_detail_level wellplate_detail_level
- from sync_collections_users sync_cols
- inner join collections cols on cols.id = sync_cols.collection_id and cols.deleted_at is null
- where sync_cols.collection_id in
- (
- select v_sams_cols.cols_id
- from v_samples_collections v_sams_cols
- where v_sams_cols.sams_id = in_sample_id
- )
- and sync_cols.user_id in (select user_ids(in_user_id))
- ) all_cols;
-
- return query select coalesce(i_detail_level_sample,0) detail_level_sample, coalesce(i_detail_level_wellplate,0) detail_level_wellplate;
- end;$function$
+ declare
+ i_detail_level_wellplate integer default 0;
+ i_detail_level_sample integer default 0;
+ begin
+ select max(all_cols.sample_detail_level), max(all_cols.wellplate_detail_level)
+ into i_detail_level_sample, i_detail_level_wellplate
+ from
+ (
+ select v_sams_cols.cols_sample_detail_level sample_detail_level, v_sams_cols.cols_wellplate_detail_level wellplate_detail_level
+ from v_samples_collections v_sams_cols
+ where v_sams_cols.sams_id = in_sample_id
+ and v_sams_cols.cols_user_id in (select user_ids(in_user_id))
+ union
+ select sync_cols.sample_detail_level sample_detail_level, sync_cols.wellplate_detail_level wellplate_detail_level
+ from sync_collections_users sync_cols
+ inner join collections cols on cols.id = sync_cols.collection_id and cols.deleted_at is null
+ where sync_cols.collection_id in
+ (
+ select v_sams_cols.cols_id
+ from v_samples_collections v_sams_cols
+ where v_sams_cols.sams_id = in_sample_id
+ )
+ and sync_cols.user_id in (select user_ids(in_user_id))
+ ) all_cols;
+
+ return query select coalesce(i_detail_level_sample,0) detail_level_sample, coalesce(i_detail_level_wellplate,0) detail_level_wellplate;
+ end;$function$
SQL
create_function :group_user_ids, sql_definition: <<-'SQL'
CREATE OR REPLACE FUNCTION public.group_user_ids(group_id integer)
@@ -1697,8 +1723,8 @@
RETURNS TABLE(literatures text)
LANGUAGE sql
AS $function$
- select string_agg(l2.id::text, ',') as literatures from literals l , literatures l2
- where l.literature_id = l2.id
+ select string_agg(l2.id::text, ',') as literatures from literals l , literatures l2
+ where l.literature_id = l2.id
and l.element_type = $1 and l.element_id = $2
$function$
SQL
@@ -1708,20 +1734,6 @@
CREATE TRIGGER update_users_matrix_trg AFTER INSERT OR UPDATE ON public.matrices FOR EACH ROW EXECUTE FUNCTION update_users_matrix()
SQL
- create_view "v_samples_collections", sql_definition: <<-SQL
- SELECT cols.id AS cols_id,
- cols.user_id AS cols_user_id,
- cols.sample_detail_level AS cols_sample_detail_level,
- cols.wellplate_detail_level AS cols_wellplate_detail_level,
- cols.shared_by_id AS cols_shared_by_id,
- cols.is_shared AS cols_is_shared,
- samples.id AS sams_id,
- samples.name AS sams_name
- FROM ((collections cols
- JOIN collections_samples col_samples ON (((col_samples.collection_id = cols.id) AND (col_samples.deleted_at IS NULL))))
- JOIN samples ON (((samples.id = col_samples.sample_id) AND (samples.deleted_at IS NULL))))
- WHERE (cols.deleted_at IS NULL);
- SQL
create_view "literal_groups", sql_definition: <<-SQL
SELECT lits.element_type,
lits.element_id,
@@ -1765,4 +1777,18 @@
users
WHERE ((channels.id = messages.channel_id) AND (messages.id = notifications.message_id) AND (users.id = messages.created_by));
SQL
+ create_view "v_samples_collections", sql_definition: <<-SQL
+ SELECT cols.id AS cols_id,
+ cols.user_id AS cols_user_id,
+ cols.sample_detail_level AS cols_sample_detail_level,
+ cols.wellplate_detail_level AS cols_wellplate_detail_level,
+ cols.shared_by_id AS cols_shared_by_id,
+ cols.is_shared AS cols_is_shared,
+ samples.id AS sams_id,
+ samples.name AS sams_name
+ FROM ((collections cols
+ JOIN collections_samples col_samples ON (((col_samples.collection_id = cols.id) AND (col_samples.deleted_at IS NULL))))
+ JOIN samples ON (((samples.id = col_samples.sample_id) AND (samples.deleted_at IS NULL))))
+ WHERE (cols.deleted_at IS NULL);
+ SQL
end
diff --git a/spec/javascripts/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.spec.js b/spec/javascripts/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.spec.js
index 473d048dd9..9cfc9442f4 100644
--- a/spec/javascripts/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.spec.js
+++ b/spec/javascripts/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleSolventGroup.spec.js
@@ -41,6 +41,7 @@ describe('SampleSolventGroup.render()', async () => {
describe('when sample has two solvents', async () => {
const sampleWithSolvents = await SampleFactory.build('SampleFactory.water_100g');
sampleWithSolvents.solvent = [{ label: 'water', ratio: 1.0 }, { label: 'ethanol', ratio: 2.0 }];
+ sampleWithSolvents.sample_type = 'Micromolecule';
const wrapper = shallow(
{
describe('SolventDetails.render()', () => {
const deleteSolvent = () => {};
const onChangeSolvent = () => {};
+ const sampleType = 'Micromolecule';
describe('when solvent prop is null', () => {
const wrapper = shallow(
@@ -72,6 +74,7 @@ describe('SolventDetails.render()', () => {
deleteSolvent={deleteSolvent}
onChangeSolvent={onChangeSolvent}
solvent={null}
+ sampleType={sampleType}
/>
);
@@ -88,6 +91,7 @@ describe('SolventDetails.render()', () => {
deleteSolvent={deleteSolvent}
onChangeSolvent={onChangeSolvent}
solvent={solvent}
+ sampleType={sampleType}
/>
);