diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc68ede82..3ab184d0bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Chemotion_ELN Changelog ## Latest +* Features and enhancements + * Reaction variations table can now represent gaseous materials + ## [v1.10.3] > (2024-10-02) diff --git a/app/api/entities/reaction_variation_entity.rb b/app/api/entities/reaction_variation_entity.rb index ec60a8edbc..f100b956fb 100644 --- a/app/api/entities/reaction_variation_entity.rb +++ b/app/api/entities/reaction_variation_entity.rb @@ -20,28 +20,28 @@ def properties end end - def materials(material_type) + def materials(material_type, entity) {}.tap do |materials| object[material_type]&.each do |k, v| - materials[k] = ReactionVariationMaterialEntity.represent(v) + materials[k] = entity.represent(v) end end end def starting_materials - materials(:startingMaterials) + materials(:startingMaterials, StartingMaterialEntity) end def reactants - materials(:reactants) + materials(:reactants, StartingMaterialEntity) end def products - materials(:products) + materials(:products, ProductMaterialEntity) end def solvents - materials(:solvents) + materials(:solvents, SolventMaterialEntity) end end @@ -52,10 +52,33 @@ class ReactionVariationPropertyEntity < ApplicationEntity ) end - class ReactionVariationMaterialEntity < ApplicationEntity + class SolventMaterialEntity < ApplicationEntity + expose :volume, using: 'Entities::ReactionVariationMaterialEntryEntity' + + expose :aux, using: 'Entities::ReactionVariationMaterialAuxEntity' + end + + class ProductMaterialEntity < ApplicationEntity expose :mass, using: 'Entities::ReactionVariationMaterialEntryEntity' expose :amount, using: 'Entities::ReactionVariationMaterialEntryEntity' - expose :volume, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :yield, using: 'Entities::ReactionVariationMaterialEntryEntity' + + expose :duration, if: lambda { |object, _| object[:aux][:gasType] == 'gas' }, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :temperature, if: lambda { |object, _| object[:aux][:gasType] == 'gas' }, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :concentration, if: lambda { |object, _| object[:aux][:gasType] == 'gas' }, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :turnoverNumber, if: lambda { |object, _| object[:aux][:gasType] == 'gas' }, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :turnoverFrequency, if: lambda { |object, _| object[:aux][:gasType] == 'gas' }, using: 'Entities::ReactionVariationMaterialEntryEntity' + + expose :aux, using: 'Entities::ReactionVariationMaterialAuxEntity' + end + + class StartingMaterialEntity < ApplicationEntity + expose :mass, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :amount, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :equivalent, using: 'Entities::ReactionVariationMaterialEntryEntity' + + expose :volume, if: lambda { |object, _| (object[:aux][:gasType] == 'feedstock') }, using: 'Entities::ReactionVariationMaterialEntryEntity' + expose :aux, using: 'Entities::ReactionVariationMaterialAuxEntity' end @@ -68,8 +91,9 @@ class ReactionVariationMaterialAuxEntity < ApplicationEntity :molarity, :molecularWeight, :sumFormula, - :yield, - :equivalent, + :gasType, + :vesselVolume, + :materialType, ) end diff --git a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariations.js b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariations.js index c445277fe9..5b43b52294 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariations.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariations.js @@ -1,7 +1,7 @@ /* eslint-disable react/display-name */ import { AgGridReact } from 'ag-grid-react'; import React, { - useRef, useState, useEffect, useCallback + useRef, useState, useCallback, useReducer } from 'react'; import { Button, OverlayTrigger, Tooltip, Alert @@ -10,184 +10,36 @@ import { isEqual } from 'lodash'; import PropTypes from 'prop-types'; import Reaction from 'src/models/Reaction'; import { - createVariationsRow, copyVariationsRow, updateVariationsRow, getCellDataType, - temperatureUnits, durationUnits, getStandardUnit, materialTypes, updateColumnDefinitions, + createVariationsRow, copyVariationsRow, updateVariationsRow, getCellDataType, getStandardUnits, materialTypes } from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils'; import { AnalysesCellRenderer, AnalysesCellEditor, getReactionAnalyses, updateAnalyses, getAnalysesOverlay, AnalysisOverlay } from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsAnalyses'; import { - getMaterialColumnGroupChild, updateColumnDefinitionsMaterials, - getReactionMaterials, getReactionMaterialsIDs, + getMaterialColumnGroupChild, updateVariationsGasTypes, + getReactionMaterials, getReactionMaterialsIDs, getReactionMaterialsGasTypes, removeObsoleteMaterialsFromVariations, addMissingMaterialsToVariations } from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials'; import { PropertyFormatter, PropertyParser, MaterialFormatter, MaterialParser, - EquivalentFormatter, EquivalentParser, - RowToolsCellRenderer, NoteCellRenderer, NoteCellEditor -} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents'; - -function MenuHeader({ - column, context, setSort, names, entries -}) { - const { field } = column.colDef; - const { columnDefinitions, setColumnDefinitions } = context; - const [ascendingSort, setAscendingSort] = useState('inactive'); - const [descendingSort, setDescendingSort] = useState('inactive'); - const [noSort, setNoSort] = useState('inactive'); - const [name, setName] = useState(names[0]); - const [entry, setEntry] = useState(Object.keys(entries)[0]); - const [units, setUnits] = useState(entries[entry]); - const [unit, setUnit] = useState(units[0]); - - const onSortChanged = () => { - setAscendingSort(column.isSortAscending() ? 'sort_active' : 'inactive'); - setDescendingSort(column.isSortDescending() ? 'sort_active' : 'inactive'); - setNoSort( - !column.isSortAscending() && !column.isSortDescending() - ? 'sort_active' - : 'inactive' - ); - }; - - useEffect(() => { - column.addEventListener('sortChanged', onSortChanged); - onSortChanged(); - }, []); - - const onSortRequested = (order, event) => { - setSort(order, event.shiftKey); - }; - - const onUnitChanged = () => { - const newUnit = units[(units.indexOf(unit) + 1) % units.length]; - const newColumnDefinitions = updateColumnDefinitions( - columnDefinitions, - field, - 'currentEntryWithDisplayUnit', - { entry, displayUnit: newUnit } - ); - - setUnit(newUnit); - setColumnDefinitions(newColumnDefinitions); - }; - - const unitSelection = ( - - ); - - const onEntryChanged = () => { - const entryKeys = Object.keys(entries); - const newEntry = entryKeys[(entryKeys.indexOf(entry) + 1) % entryKeys.length]; - const newUnits = entries[newEntry]; - const newUnit = newUnits[0]; - let newColumnDefinitions = updateColumnDefinitions( - columnDefinitions, - field, - 'cellDataType', - getCellDataType(newEntry) - ); - newColumnDefinitions = updateColumnDefinitions( - newColumnDefinitions, - field, - 'currentEntryWithDisplayUnit', - { entry: newEntry, displayUnit: newUnit } - ); - - setEntry(newEntry); - setUnits(newUnits); - setUnit(newUnit); - setColumnDefinitions(newColumnDefinitions); - }; - - const entrySelection = ( - - ); - - const sortMenu = ( -
-
onSortRequested('asc', event)} - onTouchEnd={(event) => onSortRequested('asc', event)} - className={`customSortDownLabel ${ascendingSort}`} - > - -
-
onSortRequested('desc', event)} - onTouchEnd={(event) => onSortRequested('desc', event)} - className={`customSortUpLabel ${descendingSort}`} - > - -
-
onSortRequested('', event)} - onTouchEnd={(event) => onSortRequested('', event)} - className={`customSortRemoveLabel ${noSort}`} - > - -
-
- ); - - return ( -
- setName(names[(names.indexOf(name) + 1) % names.length])} - > - {name} - -
- {entrySelection} - {' '} - {unitSelection} -
- {sortMenu} -
- ); -} - -MenuHeader.propTypes = { - column: PropTypes.shape({ - colDef: PropTypes.object.isRequired, - isSortAscending: PropTypes.func.isRequired, - isSortDescending: PropTypes.func.isRequired, - addEventListener: PropTypes.func.isRequired, - }).isRequired, - context: PropTypes.shape({ - columnDefinitions: PropTypes.arrayOf(PropTypes.object).isRequired, - setColumnDefinitions: PropTypes.func.isRequired, - }).isRequired, - setSort: PropTypes.func.isRequired, - names: PropTypes.arrayOf(PropTypes.string).isRequired, - entries: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)).isRequired, -}; + EquivalentParser, GasParser, FeedstockParser, + NoteCellRenderer, NoteCellEditor, + RowToolsCellRenderer, MenuHeader +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents'; +import { + columnDefinitionsReducer +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsReducers'; +import GasPhaseReactionStore from 'src/stores/alt/stores/GasPhaseReactionStore'; export default function ReactionVariations({ reaction, onReactionChange }) { const gridRef = useRef(null); - const [reactionVariations, _setReactionVariations] = useState(reaction.variations); + const reactionVariations = reaction.variations; + const reactionHasPolymers = reaction.hasPolymers(); + const [gasMode, setGasMode] = useState(reaction.gaseous); const [allReactionAnalyses, setAllReactionAnalyses] = useState(getReactionAnalyses(reaction)); const [reactionMaterials, setReactionMaterials] = useState(getReactionMaterials(reaction)); - const [columnDefinitions, setColumnDefinitions] = useState([ + const [columnDefinitions, setColumnDefinitions] = useReducer(columnDefinitionsReducer, [ { headerName: 'Tools', cellRenderer: RowToolsCellRenderer, @@ -222,27 +74,28 @@ export default function ReactionVariations({ reaction, onReactionChange }) { { field: 'properties.temperature', cellDataType: getCellDataType('temperature'), - currentEntryWithDisplayUnit: { - entry: 'temperature', - displayUnit: getStandardUnit('temperature') + entryDefs: { + currentEntry: 'temperature', + displayUnit: getStandardUnits('temperature')[0], + availableEntries: ['temperature'] }, headerComponent: MenuHeader, headerComponentParams: { names: ['Temperature'], - entries: { temperature: temperatureUnits } } }, { field: 'properties.duration', cellDataType: getCellDataType('duration'), - currentEntryWithDisplayUnit: { - entry: 'duration', - displayUnit: getStandardUnit('duration') + editable: !gasMode, + entryDefs: { + currentEntry: 'duration', + displayUnit: getStandardUnits('duration')[0], + availableEntries: ['duration'] }, headerComponent: MenuHeader, headerComponentParams: { names: ['Duration'], - entries: { duration: durationUnits } } }, ] @@ -252,7 +105,7 @@ export default function ReactionVariations({ reaction, onReactionChange }) { headerName: materialTypes[materialType].label, groupId: materialType, marryChildren: true, - children: materials.map((material) => getMaterialColumnGroupChild(material, materialType, MenuHeader)) + children: materials.map((material) => getMaterialColumnGroupChild(material, materialType, MenuHeader, gasMode)) })) )); @@ -272,9 +125,26 @@ export default function ReactionVariations({ reaction, onReactionChange }) { equivalent: { extendsDataType: 'object', baseDataType: 'object', - valueFormatter: EquivalentFormatter, + valueFormatter: (params) => `${Number(params.value.equivalent.value).toPrecision(4)}`, valueParser: EquivalentParser, - } + }, + yield: { + extendsDataType: 'object', + baseDataType: 'object', + valueFormatter: (params) => `${Number(params.value.yield.value).toPrecision(4)}`, + }, + gas: { + extendsDataType: 'object', + baseDataType: 'object', + valueFormatter: MaterialFormatter, + valueParser: GasParser, + }, + feedstock: { + extendsDataType: 'object', + baseDataType: 'object', + valueFormatter: MaterialFormatter, + valueParser: FeedstockParser, + }, }; const defaultColumnDefinitions = { @@ -288,89 +158,144 @@ export default function ReactionVariations({ reaction, onReactionChange }) { }; const setReactionVariations = (updatedReactionVariations) => { - // Set updated state here and in parent component. - _setReactionVariations(updatedReactionVariations); reaction.variations = updatedReactionVariations; onReactionChange(reaction); }; + /* + What follows is a series of imperative state updates that keep the "Variations" tab in sync with other tabs. + This pattern isn't nice, but the best I could do according to + https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes and + https://react.dev/reference/react/useState#storing-information-from-previous-renders. + It would be preferable to refactor this to a more declarative approach, using a store for example. + */ const updatedReactionMaterials = getReactionMaterials(reaction); + const updatedGasMode = reaction.gaseous; + const updatedAllReactionAnalyses = getReactionAnalyses(reaction); + + /* + Keep set of materials up-to-date. + Materials could have been added or removed in the "Scheme" tab. + These changes need to be reflected in the variations. + */ if ( !isEqual( getReactionMaterialsIDs(reactionMaterials), getReactionMaterialsIDs(updatedReactionMaterials) ) ) { - /* - Keep set of materials up-to-date. - Materials could have been added or removed in the "Scheme" tab. - These changes need to be reflected in the variations. - */ - const updatedColumnDefinitions = updateColumnDefinitionsMaterials( - columnDefinitions, + let updatedReactionVariations = removeObsoleteMaterialsFromVariations(reactionVariations, updatedReactionMaterials); + updatedReactionVariations = addMissingMaterialsToVariations( + updatedReactionVariations, updatedReactionMaterials, - MenuHeader + updatedGasMode ); - let updatedReactionVariations = removeObsoleteMaterialsFromVariations(reactionVariations, updatedReactionMaterials); - updatedReactionVariations = addMissingMaterialsToVariations(updatedReactionVariations, updatedReactionMaterials); setReactionVariations(updatedReactionVariations); - setColumnDefinitions(updatedColumnDefinitions); + setColumnDefinitions( + { + type: 'update_material_set', + gasMode: updatedGasMode, + reactionMaterials: updatedReactionMaterials + } + ); setReactionMaterials(updatedReactionMaterials); } - const updatedAllReactionAnalyses = getReactionAnalyses(reaction); - if (!isEqual(allReactionAnalyses, updatedAllReactionAnalyses)) { - /* - The "Variations" tab holds references to analyses in the "Analyses" tab. - Users can add, remove, or edit analyses in the "Analyses" tab. - Every analysis in the "Analyses" tab can be assigned to one or more rows in the "Variations" tab. - Each row in the variations table keeps references to its assigned analyses - by tracking the corresponding `analysesIDs`. - In the example below, variations row "A" keeps a reference to `analysesIDs` "1", - whereas variations row "C" keeps references to "1" and "3". - The set of all `analysesIDs` that are referenced by variations is called `referenceIDs`. + /* + Update gas mode according to "Scheme" tab. + */ + if (gasMode !== updatedGasMode) { + setColumnDefinitions( + { + type: 'toggle_gas_mode', + gasMode: updatedGasMode, + reactionMaterials: updatedReactionMaterials + } + ); + setGasMode(updatedGasMode); + setReactionVariations([]); + } + + /* + Update the materials's gas types according to the "Scheme" tab. + */ + if ( + updatedGasMode && !isEqual( + getReactionMaterialsGasTypes(reactionMaterials), + getReactionMaterialsGasTypes(updatedReactionMaterials) + ) + ) { + const updatedReactionVariations = updateVariationsGasTypes( + reactionVariations, + updatedReactionMaterials, + updatedGasMode + ); + setReactionVariations(updatedReactionVariations); - Figure 1 - Analyses tab Variations tab - .---. .---------. - | 1 |<--------| A: 1 | - |---| \ |---------| - | 2 | \ | B: | - |---| \ |---------| - | 3 |<-------\| C: 1, 3 | - |---| `---------` - | 4 | - `---` + setColumnDefinitions( + { + type: 'update_gas_type', + gasMode: updatedGasMode, + reactionMaterials: updatedReactionMaterials + } + ); - The table below shows how to keep the state consistent across the "Analyses" tab and "Variations" tab. - "X" denotes absence of ID. + setReactionMaterials(updatedReactionMaterials); + } + + /* + The "Variations" tab holds references to analyses in the "Analyses" tab. + Users can add, remove, or edit analyses in the "Analyses" tab. + Every analysis in the "Analyses" tab can be assigned to one or more rows in the "Variations" tab. + Each row in the variations table keeps references to its assigned analyses + by tracking the corresponding `analysesIDs`. + In the example below, variations row "A" keeps a reference to `analysesIDs` "1", + whereas variations row "C" keeps references to "1" and "3". + The set of all `analysesIDs` that are referenced by variations is called `referenceIDs`. + + Figure 1 + Analyses tab Variations tab + .---. .---------. + | 1 |<--------| A: 1 | + |---| \ |---------| + | 2 | \ | B: | + |---| \ |---------| + | 3 |<-------\| C: 1, 3 | + |---| `---------` + | 4 | + `---` - Table 1 - .-------------- ---------------- -------------------------------------------------. - | Analyses tab | Variations tab | action | - | (analysesIDs) | (referenceIDs) | | - |-------------- |--------------- |----------------------------------------------- | - | ID | ID | None | - |-------------- |--------------- |----------------------------------------------- | - | X | ID | Container with ID removed in "Analyses" tab. | - | | | Remove ID from `referenceIDs`. | - |-------------- |--------------- |----------------------------------------------- | - | ID | X | Row that's tracking ID removed in "Variations" | - | | | tab. No action required since "Analyses" tab | - | | | only displays associations to existing rows. | - `-------------- ---------------- -------------------------------------------------` - */ + The table below shows how to keep the state consistent across the "Analyses" tab and "Variations" tab. + "X" denotes absence of ID. + + Table 1 + .-------------- ---------------- -------------------------------------------------. + | Analyses tab | Variations tab | action | + | (analysesIDs) | (referenceIDs) | | + |-------------- |--------------- |----------------------------------------------- | + | ID | ID | None | + |-------------- |--------------- |----------------------------------------------- | + | X | ID | Container with ID removed in "Analyses" tab. | + | | | Remove ID from `referenceIDs`. | + |-------------- |--------------- |----------------------------------------------- | + | ID | X | Row that's tracking ID removed in "Variations" | + | | | tab. No action required since "Analyses" tab | + | | | only displays associations to existing rows. | + `-------------- ---------------- -------------------------------------------------` + */ + if (!isEqual(allReactionAnalyses, updatedAllReactionAnalyses)) { const updatedReactionVariations = updateAnalyses(reactionVariations, updatedAllReactionAnalyses); setReactionVariations(updatedReactionVariations); setAllReactionAnalyses(updatedAllReactionAnalyses); } const addRow = useCallback(() => { + const vesselVolume = GasPhaseReactionStore.getState().reactionVesselSizeValue; setReactionVariations( - [...reactionVariations, createVariationsRow(reaction, reactionVariations)] + [...reactionVariations, createVariationsRow(reaction, reactionVariations, gasMode, vesselVolume)] ); - }, [reaction, reactionVariations]); + }, [reaction, reactionVariations, gasMode]); const copyRow = useCallback((data) => { const copiedRow = copyVariationsRow(data, reactionVariations); @@ -385,11 +310,16 @@ export default function ReactionVariations({ reaction, onReactionChange }) { const updateRow = useCallback(({ data: oldRow, colDef, newValue }) => { const { field } = colDef; - const updatedRow = updateVariationsRow(oldRow, field, newValue, reaction.hasPolymers()); + const updatedRow = updateVariationsRow(oldRow, field, newValue, reactionHasPolymers); setReactionVariations( reactionVariations.map((row) => (row.id === oldRow.id ? updatedRow : row)) ); - }, [reactionVariations, reaction]); + }, [reactionVariations, reactionHasPolymers]); + + const fitColumnToContent = (event) => { + const { column } = event; + gridRef.current.api.autoSizeColumns([column], false); + }; if (reaction.isNew) { return ( @@ -399,11 +329,6 @@ export default function ReactionVariations({ reaction, onReactionChange }) { ); } - const fitColumnToContent = (event) => { - const { column } = event; - gridRef.current.api.autoSizeColumns([column], false); - }; - return (
@@ -439,9 +366,8 @@ export default function ReactionVariations({ reaction, onReactionChange }) { context={{ copyRow, removeRow, - columnDefinitions, setColumnDefinitions, - reactionHasPolymers: reaction.hasPolymers(), + reactionHasPolymers, reactionShortLabel: reaction.short_label, allReactionAnalyses }} diff --git a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsAnalyses.js b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsAnalyses.js index 80b494cbad..1825062ede 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsAnalyses.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsAnalyses.js @@ -1,8 +1,7 @@ import React, { useState } from 'react'; -import { AgGridReact } from 'ag-grid-react'; import PropTypes from 'prop-types'; import { - Form, Button, Modal, Badge, Tooltip + Form, Button, Modal, Badge } from 'react-bootstrap'; import cloneDeep from 'lodash/cloneDeep'; import Reaction from 'src/models/Reaction'; diff --git a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents.js b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents.js deleted file mode 100644 index 1a24e0dba1..0000000000 --- a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents.js +++ /dev/null @@ -1,215 +0,0 @@ -/* eslint-disable react/display-name */ -import React, { useState } from 'react'; -import { AgGridReact } from 'ag-grid-react'; -import { - Button, ButtonGroup, Badge, Modal, Form, OverlayTrigger, Tooltip -} from 'react-bootstrap'; -import PropTypes from 'prop-types'; -import { - getVariationsRowName, - convertUnit -} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils'; -import { - updateNonReferenceMaterialOnMassChange, - getReferenceMaterial, getMolFromGram, getGramFromMol -} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials'; -import { parseNumericString } from 'src/utilities/MathUtils'; - -function RowToolsCellRenderer({ - data: variationsRow, context -}) { - const { reactionShortLabel, copyRow, removeRow } = context; - return ( -
- {getVariationsRowName(reactionShortLabel, variationsRow.id)} - - - - -
- ); -} - -RowToolsCellRenderer.propTypes = { - data: PropTypes.shape({ - id: PropTypes.number.isRequired, - }).isRequired, - context: PropTypes.shape({ - reactionShortLabel: PropTypes.string.isRequired, - copyRow: PropTypes.func.isRequired, - removeRow: PropTypes.func.isRequired, - }).isRequired, -}; - -function EquivalentFormatter({ value: cellData }) { - const { equivalent } = cellData.aux; - - return `${Number(equivalent).toPrecision(4)}`; -} - -function EquivalentParser({ data: variationsRow, oldValue: cellData, newValue }) { - let equivalent = parseNumericString(newValue); - if (equivalent < 0) { - equivalent = 0; - } - // Adapt mass to updated equivalent. - const referenceMaterial = getReferenceMaterial(variationsRow); - const referenceMol = getMolFromGram(referenceMaterial.mass.value, referenceMaterial); - const mass = getGramFromMol(referenceMol * equivalent, cellData); - - // Adapt amount to updated equivalent. - const amount = getMolFromGram(mass, cellData); - - return { - ...cellData, - mass: { ...cellData.mass, value: mass }, - amount: { ...cellData.amount, value: amount }, - aux: { ...cellData.aux, equivalent } - }; -} - -function PropertyFormatter({ value: cellData, colDef }) { - const { displayUnit } = colDef.currentEntryWithDisplayUnit; - const valueInDisplayUnit = convertUnit(Number(cellData.value), cellData.unit, displayUnit); - - return `${Number(valueInDisplayUnit).toPrecision(4)}`; -} - -function PropertyParser({ - oldValue: cellData, newValue, colDef -}) { - const { entry, displayUnit } = colDef.currentEntryWithDisplayUnit; - let value = parseNumericString(newValue); - if (entry !== 'temperature' && value < 0) { - value = 0; - } - value = convertUnit(value, displayUnit, cellData.unit); - const updatedCellData = { ...cellData, value }; - - return updatedCellData; -} - -function MaterialFormatter({ value: cellData, colDef }) { - const { entry, displayUnit } = colDef.currentEntryWithDisplayUnit; - const valueInDisplayUnit = convertUnit(Number(cellData[entry].value), cellData[entry].unit, displayUnit); - - return `${Number(valueInDisplayUnit).toPrecision(4)}`; -} - -function MaterialParser({ - data: variationsRow, oldValue: cellData, newValue, colDef, context -}) { - const { field } = colDef; - const { entry, displayUnit } = colDef.currentEntryWithDisplayUnit; - const columnGroup = field.split('.')[0]; - let value = convertUnit(parseNumericString(newValue), displayUnit, cellData[entry].unit); - if (value < 0) { - value = 0; - } - let updatedCellData = { ...cellData, [entry]: { ...cellData[entry], value } }; - - if (entry === 'mass') { - // Adapt amount to updated mass. - const amount = getMolFromGram(value, updatedCellData); - updatedCellData = { ...updatedCellData, amount: { ...updatedCellData.amount, value: amount } }; - } - if (entry === 'amount') { - // Adapt mass to updated amount. - const mass = getGramFromMol(value, updatedCellData); - updatedCellData = { ...updatedCellData, mass: { ...updatedCellData.mass, value: mass } }; - } - // See comment in ReactionVariationsUtils.updateVariationsRow() regarding reactive updates. - if (updatedCellData.aux.isReference) { - return updatedCellData; - } - return updateNonReferenceMaterialOnMassChange( - variationsRow, - updatedCellData, - columnGroup, - context.reactionHasPolymers - ); -} - -function NoteCellRenderer(props) { - return ( - - double click to edit - - } - > - {props.value ? props.value : '_'} - - ); -} - -function NoteCellEditor({ - data: variationsRow, - value, - onValueChange, - stopEditing, - context -}) { - const [note, setNote] = useState(value); - const { reactionShortLabel } = context; - - const onClose = () => { - stopEditing(); - }; - - const onSave = () => { - onValueChange(note); - stopEditing(); - }; - - const cellContent = ( - - - {`Edit note for ${getVariationsRowName(reactionShortLabel, variationsRow.id)}`} - - - setNote(event.target.value)} - /> - - - - - - ); - - return cellContent; -} - -NoteCellEditor.propTypes = { - data: PropTypes.shape({ - id: PropTypes.number.isRequired, - }).isRequired, - value: PropTypes.string.isRequired, - onValueChange: PropTypes.func.isRequired, - stopEditing: PropTypes.func.isRequired, - context: PropTypes.shape({ - reactionShortLabel: PropTypes.string.isRequired, - }).isRequired, -}; - -export { - RowToolsCellRenderer, - EquivalentFormatter, - EquivalentParser, - PropertyFormatter, - PropertyParser, - MaterialFormatter, - MaterialParser, - NoteCellRenderer, - NoteCellEditor -}; diff --git a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents.js b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents.js new file mode 100644 index 0000000000..ceb2d789ac --- /dev/null +++ b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents.js @@ -0,0 +1,513 @@ +/* eslint-disable react/display-name */ +import React, { useState, useEffect } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { + Button, ButtonGroup, Badge, Modal, Form, OverlayTrigger, Tooltip +} from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import { + getVariationsRowName, convertUnit, getStandardUnits +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils'; +import { + getReferenceMaterial, getCatalystMaterial, getFeedstockMaterial, getMolFromGram, getGramFromMol, + computeEquivalent, computePercentYield +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials'; +import { parseNumericString } from 'src/utilities/MathUtils'; +import { + calculateGasMoles, calculateTON, calculateFeedstockMoles, calculateFeedstockVolume +} from 'src/utilities/UnitsConversion'; + +function RowToolsCellRenderer({ + data: variationsRow, context +}) { + const { reactionShortLabel, copyRow, removeRow } = context; + return ( +
+ {getVariationsRowName(reactionShortLabel, variationsRow.id)} + + + + +
+ ); +} + +RowToolsCellRenderer.propTypes = { + data: PropTypes.shape({ + id: PropTypes.number.isRequired, + }).isRequired, + context: PropTypes.shape({ + reactionShortLabel: PropTypes.string.isRequired, + copyRow: PropTypes.func.isRequired, + removeRow: PropTypes.func.isRequired, + }).isRequired, +}; + +function EquivalentParser({ data: variationsRow, oldValue: cellData, newValue }) { + let equivalent = parseNumericString(newValue); + if (equivalent < 0) { + equivalent = 0; + } + // Adapt mass to updated equivalent. + const referenceMaterial = getReferenceMaterial(variationsRow); + const referenceMol = getMolFromGram(referenceMaterial.mass.value, referenceMaterial); + const mass = getGramFromMol(referenceMol * equivalent, cellData); + + // Adapt amount to updated equivalent. + const amount = getMolFromGram(mass, cellData); + + return { + ...cellData, + mass: { ...cellData.mass, value: mass }, + amount: { ...cellData.amount, value: amount }, + equivalent: { ...cellData.equivalent, value: equivalent } + }; +} + +function PropertyFormatter({ value: cellData, colDef }) { + const { displayUnit } = colDef.entryDefs; + const valueInDisplayUnit = convertUnit(Number(cellData.value), cellData.unit, displayUnit); + + return `${Number(valueInDisplayUnit).toPrecision(4)}`; +} + +function PropertyParser({ + oldValue: cellData, newValue, colDef +}) { + const { currentEntry, displayUnit } = colDef.entryDefs; + let value = parseNumericString(newValue); + if (currentEntry !== 'temperature' && value < 0) { + value = 0; + } + value = convertUnit(value, displayUnit, cellData.unit); + const updatedCellData = { ...cellData, value }; + + return updatedCellData; +} + +function MaterialFormatter({ value: cellData, colDef }) { + const { currentEntry, displayUnit } = colDef.entryDefs; + const valueInDisplayUnit = convertUnit( + Number(cellData[currentEntry].value), + cellData[currentEntry].unit, + displayUnit + ); + + return `${Number(valueInDisplayUnit).toPrecision(4)}`; +} + +function MaterialParser({ + data: variationsRow, oldValue: cellData, newValue, colDef, context +}) { + const { currentEntry, displayUnit } = colDef.entryDefs; + let value = convertUnit(parseNumericString(newValue), displayUnit, cellData[currentEntry].unit); + if (value < 0) { + value = 0; + } + let updatedCellData = { ...cellData, [currentEntry]: { ...cellData[currentEntry], value } }; + + if (currentEntry === 'mass') { + // Adapt amount to updated mass. + const amount = getMolFromGram(value, updatedCellData); + updatedCellData = { ...updatedCellData, amount: { ...updatedCellData.amount, value: amount } }; + } + if (currentEntry === 'amount') { + // Adapt mass to updated amount. + const mass = getGramFromMol(value, updatedCellData); + updatedCellData = { ...updatedCellData, mass: { ...updatedCellData.mass, value: mass } }; + } + if (updatedCellData.aux.isReference) { + return updatedCellData; + } + + const referenceMaterial = getReferenceMaterial(variationsRow); + + // Adapt equivalent to updated mass. + if ('equivalent' in updatedCellData) { + const equivalent = computeEquivalent(updatedCellData, referenceMaterial); + updatedCellData = { ...updatedCellData, equivalent: { ...updatedCellData.equivalent, value: equivalent } }; + } + + // Adapt yield to updated mass. + if ('yield' in updatedCellData) { + const percentYield = computePercentYield(updatedCellData, referenceMaterial, context.reactionHasPolymers); + updatedCellData = { ...updatedCellData, yield: { ...updatedCellData.yield, value: percentYield } }; + } + + return updatedCellData; +} + +function GasParser({ + data: variationsRow, oldValue: cellData, newValue, colDef +}) { + const { currentEntry, displayUnit } = colDef.entryDefs; + let value = convertUnit(parseNumericString(newValue), displayUnit, cellData[currentEntry].unit); + if (currentEntry !== 'temperature' && value < 0) { + value = 0; + } + let updatedCellData = { ...cellData, [currentEntry]: { ...cellData[currentEntry], value } }; + + switch (currentEntry) { + case 'concentration': + case 'temperature': { + const temperatureInKelvin = convertUnit( + updatedCellData.temperature.value, + updatedCellData.temperature.unit, + 'K' + ); + + const concentration = updatedCellData.concentration.value; + const { vesselVolume } = updatedCellData.aux; + const amount = calculateGasMoles(vesselVolume, concentration, temperatureInKelvin); + const mass = getGramFromMol(amount, updatedCellData); + + const catalyst = getCatalystMaterial(variationsRow); + const catalystAmount = catalyst?.amount.value ?? 0; + const turnoverNumber = calculateTON(amount, catalystAmount); + + const feedstockPurity = getFeedstockMaterial(variationsRow)?.aux.purity || 1; + const feedstockAmount = calculateFeedstockMoles(vesselVolume, feedstockPurity); + const percentYield = (amount / feedstockAmount) * 100; + + updatedCellData = { + ...updatedCellData, + mass: { ...updatedCellData.mass, value: mass }, + amount: { ...updatedCellData.amount, value: amount }, + yield: { ...updatedCellData.yield, value: percentYield }, + turnoverNumber: { ...updatedCellData.turnoverNumber, value: turnoverNumber }, + }; + break; + } + default: + break; + } + + const durationInHours = convertUnit( + updatedCellData.duration.value, + updatedCellData.duration.unit, + 'Hour(s)' + ); + const turnoverNumber = updatedCellData.turnoverNumber.value; + const turnoverFrequency = turnoverNumber / (durationInHours || 1); + + return { + ...updatedCellData, + turnoverFrequency: { ...updatedCellData.turnoverFrequency, value: turnoverFrequency } + }; +} + +function FeedstockParser({ + data: variationsRow, oldValue: cellData, newValue, colDef +}) { + const { currentEntry, displayUnit } = colDef.entryDefs; + let value = convertUnit(parseNumericString(newValue), displayUnit, cellData[currentEntry].unit); + if (value < 0) { + value = 0; + } + let updatedCellData = { ...cellData, [currentEntry]: { ...cellData[currentEntry], value } }; + + switch (currentEntry) { + case 'amount': { + const amount = updatedCellData.amount.value; + const mass = getGramFromMol(amount, updatedCellData); + + const purity = updatedCellData.aux.purity || 1; + const volume = calculateFeedstockVolume(amount, purity); + + updatedCellData = { + ...updatedCellData, + mass: { ...updatedCellData.mass, value: mass }, + volume: { ...updatedCellData.volume, value: volume }, + }; + break; + } + case 'volume': { + const volume = updatedCellData.volume.value; + const purity = updatedCellData.aux.purity || 1; + const amount = calculateFeedstockMoles(volume, purity); + + const mass = getGramFromMol(amount, updatedCellData); + + updatedCellData = { + ...updatedCellData, + mass: { ...updatedCellData.mass, value: mass }, + amount: { ...updatedCellData.amount, value: amount }, + }; + break; + } + case 'equivalent': { + return updatedCellData; + } + default: + break; + } + + if (updatedCellData.aux.isReference) { + return updatedCellData; + } + + const referenceMaterial = getReferenceMaterial(variationsRow); + const equivalent = computeEquivalent(updatedCellData, referenceMaterial); + + return { ...updatedCellData, equivalent: { ...updatedCellData.equivalent, value: equivalent } }; +} + +function NoteCellRenderer(props) { + return ( + + double click to edit + + )} + > + {props.value ? props.value : '_'} + + ); +} + +function NoteCellEditor({ + data: variationsRow, + value, + onValueChange, + stopEditing, + context +}) { + const [note, setNote] = useState(value); + const { reactionShortLabel } = context; + + const onClose = () => { + stopEditing(); + }; + + const onSave = () => { + onValueChange(note); + stopEditing(); + }; + + const cellContent = ( + + + {`Edit note for ${getVariationsRowName(reactionShortLabel, variationsRow.id)}`} + + + setNote(event.target.value)} + /> + + + + + + ); + + return cellContent; +} + +NoteCellEditor.propTypes = { + data: PropTypes.shape({ + id: PropTypes.number.isRequired, + }).isRequired, + value: PropTypes.string.isRequired, + onValueChange: PropTypes.func.isRequired, + stopEditing: PropTypes.func.isRequired, + context: PropTypes.shape({ + reactionShortLabel: PropTypes.string.isRequired, + }).isRequired, +}; + +function MaterialOverlay({ value: cellData }) { + const { aux = null } = cellData; + + return ( +
+
+ {aux?.isReference && ( +
Reference
+ )} + {aux?.coefficient !== null && ( +
{`Coefficient: ${Number(aux.coefficient).toPrecision(4)}`}
+ )} + {aux?.molecularWeight !== null && ( +
{`Molar mass: ${Number(aux.molecularWeight).toPrecision(2)} g/mol`}
+ )} +
+
+ ); +} + +MaterialOverlay.propTypes = { + value: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.number.isRequired, + unit: PropTypes.string.isRequired, + })).isRequired, + colDef: PropTypes.shape({ + entryDefs: PropTypes.shape({ + currentEntry: PropTypes.number.isRequired, + displayUnit: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; + +function MenuHeader({ + column, context, setSort, names, gasType = 'off' +}) { + const { setColumnDefinitions } = context; + const [ascendingSort, setAscendingSort] = useState('inactive'); + const [descendingSort, setDescendingSort] = useState('inactive'); + const [noSort, setNoSort] = useState('inactive'); + const [name, setName] = useState(names[0]); + const { field, entryDefs } = column.colDef; + const { currentEntry, displayUnit, availableEntries } = entryDefs; + const units = getStandardUnits(currentEntry); + const currentEntryTitle = currentEntry.split(/(?=[A-Z])/).join(' ').toLowerCase(); // e.g. 'turnoverNumber' -> 'turnover number' + + const onSortChanged = () => { + setAscendingSort(column.isSortAscending() ? 'sort_active' : 'inactive'); + setDescendingSort(column.isSortDescending() ? 'sort_active' : 'inactive'); + setNoSort( + !column.isSortAscending() && !column.isSortDescending() + ? 'sort_active' + : 'inactive' + ); + }; + + useEffect(() => { + column.addEventListener('sortChanged', onSortChanged); + onSortChanged(); + }, []); + + const onSortRequested = (order, event) => { + setSort(order, event.shiftKey); + }; + + const onUnitChanged = () => { + const newDisplayUnit = units[(units.indexOf(displayUnit) + 1) % units.length]; + + setColumnDefinitions( + { + type: 'update_entry_defs', + field, + entryDefs: { currentEntry, displayUnit: newDisplayUnit, availableEntries }, + gasType + } + ); + }; + + const unitSelection = ( + + ); + + const onEntryChanged = () => { + const newCurrentEntry = availableEntries[(availableEntries.indexOf(currentEntry) + 1) % availableEntries.length]; + const newUnit = getStandardUnits(newCurrentEntry)[0]; + + setColumnDefinitions( + { + type: 'update_entry_defs', + field, + entryDefs: { currentEntry: newCurrentEntry, displayUnit: newUnit, availableEntries }, + gasType + } + ); + }; + + const entrySelection = ( + + ); + + const sortMenu = ( +
+
onSortRequested('asc', event)} + onTouchEnd={(event) => onSortRequested('asc', event)} + className={`customSortDownLabel ${ascendingSort}`} + > + +
+
onSortRequested('desc', event)} + onTouchEnd={(event) => onSortRequested('desc', event)} + className={`customSortUpLabel ${descendingSort}`} + > + +
+
onSortRequested('', event)} + onTouchEnd={(event) => onSortRequested('', event)} + className={`customSortRemoveLabel ${noSort}`} + > + +
+
+ ); + + return ( +
+ setName(names[(names.indexOf(name) + 1) % names.length])} + > + {`${name} ${gasType !== 'off' ? `(${gasType})` : ''}`} + +
+ {entrySelection} + {' '} + {unitSelection} +
+ {sortMenu} +
+ ); +} + +MenuHeader.propTypes = { + column: PropTypes.instanceOf(AgGridReact.column).isRequired, + context: PropTypes.instanceOf(AgGridReact.context).isRequired, + setSort: PropTypes.func.isRequired, + names: PropTypes.arrayOf(PropTypes.string).isRequired, + gasType: PropTypes.string, +}; + +MenuHeader.defaultProps = { + gasType: 'off', +}; + +export { + RowToolsCellRenderer, + EquivalentParser, + PropertyFormatter, + PropertyParser, + MaterialFormatter, + MaterialParser, + GasParser, + FeedstockParser, + NoteCellRenderer, + NoteCellEditor, + MaterialOverlay, + MenuHeader, +}; diff --git a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.js b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.js index b4540d122f..d59562521e 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.js @@ -1,11 +1,10 @@ -import { AgGridReact } from 'ag-grid-react'; -import React from 'react'; -import PropTypes from 'prop-types'; -import { cloneDeep } from 'lodash'; +import { get, cloneDeep } from 'lodash'; import { - convertUnit, materialTypes, volumeUnits, massUnits, amountUnits, getStandardUnit, - getCellDataType + materialTypes, getStandardUnits, getCellDataType, updateColumnDefinitions, getStandardValue } from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils'; +import { + MaterialOverlay, MenuHeader +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents'; function getMolFromGram(gram, material) { if (material.aux.loading) { @@ -34,6 +33,18 @@ function getReferenceMaterial(variationsRow) { return Object.values(potentialReferenceMaterials).find((material) => material.aux?.isReference || false); } +function getCatalystMaterial(variationsRow) { + const variationsRowCopy = cloneDeep(variationsRow); + const potentialCatalystMaterials = { ...variationsRowCopy.startingMaterials, ...variationsRowCopy.reactants }; + return Object.values(potentialCatalystMaterials).find((material) => material.aux?.gasType === 'catalyst' || false); +} + +function getFeedstockMaterial(variationsRow) { + const variationsRowCopy = cloneDeep(variationsRow); + const potentialFeedstockMaterials = { ...variationsRowCopy.startingMaterials, ...variationsRowCopy.reactants }; + return Object.values(potentialFeedstockMaterials).find((material) => material.aux?.gasType === 'feedstock' || false); +} + function computeEquivalent(material, referenceMaterial) { return getMolFromGram(material.mass.value, material) / getMolFromGram(referenceMaterial.mass.value, referenceMaterial); @@ -60,12 +71,17 @@ function getReactionMaterialsIDs(reactionMaterials) { return Object.values(reactionMaterials).flat().map((material) => material.id); } +function getReactionMaterialsGasTypes(reactionMaterials) { + return Object.values(reactionMaterials).flat().map((material) => material.gas_type); +} + function updateYields(variationsRow, reactionHasPolymers) { const updatedVariationsRow = cloneDeep(variationsRow); const referenceMaterial = getReferenceMaterial(updatedVariationsRow); - Object.entries(updatedVariationsRow.products).forEach(([productName, productMaterial]) => { - updatedVariationsRow.products[productName].aux.yield = computePercentYield( + Object.values(updatedVariationsRow.products).forEach((productMaterial) => { + if (productMaterial.aux.gasType === 'gas') { return; } + productMaterial.yield.value = computePercentYield( productMaterial, referenceMaterial, reactionHasPolymers @@ -80,22 +96,78 @@ function updateEquivalents(variationsRow) { const referenceMaterial = getReferenceMaterial(updatedVariationsRow); ['startingMaterials', 'reactants'].forEach((materialType) => { - Object.entries(updatedVariationsRow[materialType]).forEach(([materialName, material]) => { + Object.values(updatedVariationsRow[materialType]).forEach((material) => { if (material.aux.isReference) { return; } - updatedVariationsRow[materialType][materialName].aux.equivalent = computeEquivalent(material, referenceMaterial); + const updatedEquivalent = computeEquivalent(material, referenceMaterial); + material.equivalent.value = updatedEquivalent; }); }); return updatedVariationsRow; } -function getMaterialData(material) { +function getMaterialEntries(materialType, gasType) { + switch ((gasType !== 'off') ? gasType : materialType) { + case 'solvents': + return ['volume']; + case 'products': + return ['mass', 'amount', 'yield']; + case 'startingMaterials': + case 'reactants': + case 'catalyst': + return ['mass', 'amount', 'equivalent']; + case 'feedstock': + return ['mass', 'amount', 'volume', 'equivalent']; + case 'gas': + return [ + 'duration', + 'temperature', + 'concentration', + 'turnoverNumber', + 'turnoverFrequency', + 'mass', + 'amount', + 'yield' + ]; + default: + return []; + } +} + +function cellIsEditable(params) { + const entry = params.colDef.entryDefs.currentEntry; + const cellData = get(params.data, params.colDef.field); + const { isReference, gasType, materialType } = cellData.aux; + + switch (entry) { + case 'equivalent': + return !isReference; + case 'mass': + return !['feedstock', 'gas'].includes(gasType); + case 'amount': + return materialType !== 'products'; + case 'yield': + case 'turnoverNumber': + case 'turnoverFrequency': + return false; + default: + return true; + } +} + +function getMaterialData(material, materialType, gasMode = false, vesselVolume = null) { const materialCopy = cloneDeep(material); - const mass = { value: materialCopy.amount_g ?? null, unit: getStandardUnit('mass') }; - const amount = { value: material.amount_mol ?? null, unit: getStandardUnit('amount') }; - const volume = { value: materialCopy.amount_l ?? null, unit: getStandardUnit('volume') }; + let gasType = materialCopy.gas_type ?? 'off'; + gasType = gasMode ? gasType : 'off'; + + // Mutable data is represented as "entries", e.g., `foo: {value: bar, unit: baz}. + const entries = getMaterialEntries(materialType, gasType); + const materialData = entries.reduce((data, entry) => { + data[entry] = { value: getStandardValue(entry, materialCopy), unit: getStandardUnits(entry)[0] }; + return data; + }, {}); - const aux = { + materialData.aux = { coefficient: materialCopy.coefficient ?? null, isReference: materialCopy.reference ?? false, loading: (Array.isArray(materialCopy.residues) && materialCopy.residues.length) ? materialCopy.residues[0].custom_info?.loading : null, @@ -103,12 +175,49 @@ function getMaterialData(material) { molarity: materialCopy.molarity_value ?? null, molecularWeight: materialCopy.molecule_molecular_weight ?? null, sumFormula: materialCopy.molecule_formula ?? null, - yield: null, - equivalent: null + gasType, + vesselVolume, + materialType, }; + return materialData; +} + +function getMaterialColumnGroupChild(material, materialType, headerComponent, gasMode) { + const materialCopy = cloneDeep(material); + + let gasType = materialCopy.gas_type ?? 'off'; + gasType = gasMode ? gasType : 'off'; + + const entries = getMaterialEntries( + materialType, + gasType + ); + const entry = entries[0]; + + const names = [`ID: ${materialCopy.id.toString()}`]; + ['external_label', 'name', 'short_label', 'molecule_formula', 'molecule_iupac_name'].forEach((name) => { + if (materialCopy[name]) { + names.push(materialCopy[name]); + } + }); + return { - volume, mass, amount, aux + field: `${materialType}.${materialCopy.id}`, // Must be unique. + tooltipField: `${materialType}.${materialCopy.id}`, + tooltipComponent: MaterialOverlay, + entryDefs: { + currentEntry: entry, + displayUnit: getStandardUnits(entry)[0], + availableEntries: entries + }, + editable: (params) => cellIsEditable(params), + cellDataType: getCellDataType(entry, gasType), + headerComponent, + headerComponentParams: { + names, + gasType, + }, }; } @@ -126,13 +235,13 @@ function removeObsoleteMaterialsFromVariations(variations, currentMaterials) { return updatedVariations; } -function addMissingMaterialsToVariations(variations, currentMaterials) { +function addMissingMaterialsToVariations(variations, currentMaterials, gasMode) { const updatedVariations = cloneDeep(variations); updatedVariations.forEach((row) => { Object.keys(materialTypes).forEach((materialType) => { currentMaterials[materialType].forEach((material) => { if (!(material.id in row[materialType])) { - row[materialType][material.id] = getMaterialData(material); + row[materialType][material.id] = getMaterialData(material, materialType, gasMode); } }); }); @@ -140,90 +249,40 @@ function addMissingMaterialsToVariations(variations, currentMaterials) { return updatedVariations; } -function MaterialOverlay({ - value: cellData, colDef -}) { - const { aux = null } = cellData; - const { entry, displayUnit } = colDef.currentEntryWithDisplayUnit; - - return ( -
-
- {entry !== 'equivalent' && ( -
- {Number(convertUnit(cellData[entry].value, cellData[entry].unit, displayUnit)).toPrecision(4) + " " + displayUnit} -
- )} - {aux?.isReference && ( -
Reference
- )} - {aux?.equivalent !== null && ( -
{"Equivalent: " + Number(aux.equivalent).toPrecision(4)}
- )} - {aux?.coefficient !== null && ( -
{"Coefficient: " + Number(aux.coefficient).toPrecision(4)}
- )} - {aux?.yield !== null && ( -
{"Yield: " + Number(aux.yield).toPrecision(4) + "%"}
- )} - {aux?.molecularWeight !== null && ( -
{"Molar mass: " + Number(aux.molecularWeight).toPrecision(2) + " g/mol"}
- )} -
-
- ); +function updateVariationsGasTypes(variations, currentMaterials, gasMode) { + const updatedVariations = cloneDeep(variations); + updatedVariations.forEach((row) => { + Object.keys(materialTypes).forEach((materialType) => { + currentMaterials[materialType].forEach((material) => { + const currentGasType = material.gas_type ?? 'off'; + if (currentGasType !== row[materialType][material.id].aux.gasType) { + row[materialType][material.id] = getMaterialData(material, materialType, gasMode); + } + }); + }); + }); + return updatedVariations; } -MaterialOverlay.propTypes = { - value: PropTypes.arrayOf(PropTypes.shape({ - value: PropTypes.number.isRequired, - unit: PropTypes.string.isRequired, - })).isRequired, - colDef: PropTypes.shape({ - currentEntryWithDisplayUnit: PropTypes.shape({ - entry: PropTypes.number.isRequired, - displayUnit: PropTypes.string.isRequired, - }).isRequired, - }).isRequired, -}; +function updateColumnDefinitionsMaterialTypes(columnDefinitions, currentMaterials, gasMode) { + let updatedColumnDefinitions = cloneDeep(columnDefinitions); -function getMaterialColumnGroupChild(material, materialType, headerComponent) { - const materialCopy = cloneDeep(material); - let entries = {}; - if (materialType === 'solvents') { - entries = { volume: volumeUnits }; - } - if (materialType === 'products') { - entries = { mass: massUnits }; - } - if (['startingMaterials', 'reactants'].includes(materialType)) { - entries = { mass: massUnits, amount: amountUnits }; - if (!materialCopy.reference ?? false) { - entries.equivalent = []; - } - } - const names = [`ID: ${materialCopy.id.toString()}`]; - ['external_label', 'name', 'short_label', 'molecule_formula', 'molecule_iupac_name'].forEach((name) => { - if (materialCopy[name]) { - names.push(materialCopy[name]); - } + Object.entries(currentMaterials).forEach(([materialType, materials]) => { + const updatedMaterials = materials.map( + (material) => getMaterialColumnGroupChild(material, materialType, MenuHeader, gasMode) + ); + updatedColumnDefinitions = updateColumnDefinitions( + updatedColumnDefinitions, + materialType, + 'children', + updatedMaterials + ); }); - const entry = materialType === 'solvents' ? 'volume' : 'mass'; - return { - field: `${materialType}.${materialCopy.id}`, // Must be unique. - tooltipField: `${materialType}.${materialCopy.id}`, - tooltipComponent: MaterialOverlay, - currentEntryWithDisplayUnit: { entry, displayUnit: getStandardUnit(entry) }, - cellDataType: getCellDataType(entry), - headerComponent, - headerComponentParams: { - names, - entries - }, - }; + + return updatedColumnDefinitions; } -function updateColumnDefinitionsMaterials(columnDefinitions, currentMaterials, headerComponent) { +function updateColumnDefinitionsMaterials(columnDefinitions, currentMaterials, headerComponent, gasMode) { const updatedColumnDefinitions = cloneDeep(columnDefinitions); Object.entries(currentMaterials).forEach(([materialType, materials]) => { @@ -238,7 +297,14 @@ function updateColumnDefinitionsMaterials(columnDefinitions, currentMaterials, h // Add missing materials. materials.forEach((material) => { if (!materialColumnGroup.children.some((child) => child.field === `${materialType}.${material.id}`)) { - materialColumnGroup.children.push(getMaterialColumnGroupChild(material, materialType, headerComponent)); + materialColumnGroup.children.push( + getMaterialColumnGroupChild( + material, + materialType, + headerComponent, + gasMode + ) + ); } }); }); @@ -246,24 +312,6 @@ function updateColumnDefinitionsMaterials(columnDefinitions, currentMaterials, h return updatedColumnDefinitions; } -function updateNonReferenceMaterialOnMassChange(variationsRow, material, materialType, reactionHasPolymers) { - const referenceMaterial = getReferenceMaterial(variationsRow); - - // Adapt equivalent to updated mass. - const equivalent = (!material.aux.isReference && ['startingMaterials', 'reactants'].includes(materialType)) - ? computeEquivalent(material, referenceMaterial) : material.aux.equivalent; - - // Adapt yield to updated mass. - const percentYield = (materialType === 'products') - ? computePercentYield(material, referenceMaterial, reactionHasPolymers) : material.aux.yield; - - const updatedAux = { ...material.aux, equivalent, yield: percentYield }; - - return { - ...material, aux: updatedAux - }; -} - function updateVariationsRowOnReferenceMaterialChange(row, reactionHasPolymers) { let updatedRow = cloneDeep(row); updatedRow = updateEquivalents(updatedRow); @@ -273,17 +321,23 @@ function updateVariationsRowOnReferenceMaterialChange(row, reactionHasPolymers) } export { - MaterialOverlay, getMaterialColumnGroupChild, getReactionMaterials, getReactionMaterialsIDs, + getReactionMaterialsGasTypes, getMaterialData, updateColumnDefinitionsMaterials, - updateNonReferenceMaterialOnMassChange, + updateColumnDefinitionsMaterialTypes, updateVariationsRowOnReferenceMaterialChange, removeObsoleteMaterialsFromVariations, addMissingMaterialsToVariations, + updateVariationsGasTypes, getReferenceMaterial, + getCatalystMaterial, + getFeedstockMaterial, getMolFromGram, getGramFromMol, + computeEquivalent, + computePercentYield, + cellIsEditable, }; diff --git a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsReducers.js b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsReducers.js new file mode 100644 index 0000000000..da0d832f6c --- /dev/null +++ b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsReducers.js @@ -0,0 +1,65 @@ +import { MenuHeader } from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents'; +import { + updateColumnDefinitionsMaterials, updateColumnDefinitionsMaterialTypes +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials'; +import { + getCellDataType, + updateColumnDefinitions +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils'; + +function columnDefinitionsReducer(columnDefinitions, action) { + switch (action.type) { + case 'update_material_set': { + return updateColumnDefinitionsMaterials( + columnDefinitions, + action.reactionMaterials, + MenuHeader, + action.gasMode + ); + } + case 'update_entry_defs': { + let updatedColumnDefinitions = updateColumnDefinitions( + columnDefinitions, + action.field, + 'entryDefs', + action.entryDefs + ); + updatedColumnDefinitions = updateColumnDefinitions( + updatedColumnDefinitions, + action.field, + 'cellDataType', + getCellDataType(action.entryDefs.currentEntry, action.gasType) + ); + return updatedColumnDefinitions; + } + case 'toggle_gas_mode': { + let updatedColumnDefinitions = updateColumnDefinitions( + columnDefinitions, + 'properties.duration', + 'editable', + !action.gasMode + ); + updatedColumnDefinitions = updateColumnDefinitionsMaterialTypes( + updatedColumnDefinitions, + action.reactionMaterials, + action.gasMode + ); + + return updatedColumnDefinitions; + } + case 'update_gas_type': { + return updateColumnDefinitionsMaterialTypes( + columnDefinitions, + action.reactionMaterials, + action.gasMode + ); + } + default: { + return columnDefinitions; + } + } +} + +export { + columnDefinitionsReducer +}; diff --git a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.js b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.js index 209719905d..7df06b79f3 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.js @@ -10,6 +10,7 @@ const durationUnits = ['Second(s)', 'Minute(s)', 'Hour(s)', 'Day(s)', 'Week(s)'] const massUnits = ['g', 'mg', 'μg']; const volumeUnits = ['l', 'ml', 'μl']; const amountUnits = ['mol', 'mmol']; +const concentrationUnits = ['ppm']; const materialTypes = { startingMaterials: { label: 'Starting Materials', reactionAttributeName: 'starting_materials' }, reactants: { label: 'Reactants', reactionAttributeName: 'reactants' }, @@ -17,26 +18,19 @@ const materialTypes = { solvents: { label: 'Solvents', reactionAttributeName: 'solvents' } }; -function getStandardUnit(entry) { - switch (entry) { - case 'volume': - return volumeUnits[0]; - case 'mass': - return massUnits[0]; - case 'amount': - return amountUnits[0]; - case 'temperature': - return temperatureUnits[0]; - case 'duration': - return durationUnits[0]; - default: - return null; - } -} - function convertUnit(value, fromUnit, toUnit) { if (temperatureUnits.includes(fromUnit) && temperatureUnits.includes(toUnit)) { - return convertTemperature(value, fromUnit, toUnit); + const convertedValue = convertTemperature(value, fromUnit, toUnit); + if (toUnit === 'K' && convertedValue < 0) { + return 0; + } + if (toUnit === '°C' && convertedValue < -273.15) { + return -273.15; + } + if (toUnit === '°F' && convertedValue < -459.67) { + return -459.67; + } + return convertedValue; } if (durationUnits.includes(fromUnit) && durationUnits.includes(toUnit)) { return convertDuration(value, fromUnit, toUnit); @@ -57,17 +51,70 @@ function convertUnit(value, fromUnit, toUnit) { return value; } -function getCellDataType(entry) { - if (['temperature', 'duration'].includes(entry)) { - return 'property'; +function getStandardUnits(entry) { + switch (entry) { + case 'volume': + return volumeUnits; + case 'mass': + return massUnits; + case 'amount': + return amountUnits; + case 'temperature': + return temperatureUnits; + case 'duration': + return durationUnits; + case 'concentration': + return concentrationUnits; + default: + return [null]; } - if (entry === 'equivalent') { - return 'equivalent'; +} + +function getStandardValue(entry, material) { + switch (entry) { + case 'volume': + return material.amount_l ?? null; + case 'mass': + return material.amount_g ?? null; + case 'amount': + return material.amount_mol ?? null; + case 'equivalent': + return (material.reference ?? false) ? 1 : 0; + case 'temperature': { + const { value = null, unit = null } = material.gas_phase_data?.temperature ?? {}; + return convertUnit(value, unit, getStandardUnits('temperature')[0]); + } + case 'concentration': + return material.gas_phase_data?.part_per_million ?? null; + case 'turnoverNumber': + return material.gas_phase_data?.turnover_number ?? null; + case 'turnoverFrequency': + return material.gas_phase_data?.turnover_frequency?.value ?? null; + default: + return null; } - if (['mass', 'volume', 'amount'].includes(entry)) { - return 'material'; +} + +function getCellDataType(entry, gasType = 'off') { + switch (entry) { + case 'temperature': + case 'duration': + return gasType === 'off' ? 'property' : 'gas'; + case 'equivalent': + return gasType === 'feedstock' ? 'feedstock' : 'equivalent'; + case 'mass': + case 'volume': + case 'amount': + return gasType === 'feedstock' ? 'feedstock' : 'material'; + case 'concentration': + case 'turnoverNumber': + case 'turnoverFrequency': + return 'gas'; + case 'yield': + return 'yield'; + default: + return null; } - return null; } function getVariationsRowName(reactionLabel, variationsRowId) { @@ -79,20 +126,20 @@ function getSequentialId(variations) { return (ids.length === 0) ? 1 : Math.max(...ids) + 1; } -function createVariationsRow(reaction, variations) { +function createVariationsRow(reaction, variations, gasMode = false, vesselVolume = null) { const reactionCopy = cloneDeep(reaction); const { dispValue: durationValue = null, dispUnit: durationUnit = 'None' } = reactionCopy.durationDisplay ?? {}; const { userText: temperatureValue = null, valueUnit: temperatureUnit = 'None' } = reactionCopy.temperature ?? {}; - let row = { + const row = { id: getSequentialId(variations), properties: { temperature: { - value: convertUnit(temperatureValue, temperatureUnit, getStandardUnit('temperature')), - unit: getStandardUnit('temperature') + value: convertUnit(temperatureValue, temperatureUnit, getStandardUnits('temperature')[0]), + unit: getStandardUnits('temperature')[0] }, duration: { - value: convertUnit(durationValue, durationUnit, getStandardUnit('duration')), - unit: getStandardUnit('duration'), + value: convertUnit(durationValue, durationUnit, getStandardUnits('duration')[0]), + unit: getStandardUnits('duration')[0], }, }, analyses: [], @@ -100,7 +147,7 @@ function createVariationsRow(reaction, variations) { }; Object.entries(materialTypes).forEach(([materialType, { reactionAttributeName }]) => { row[materialType] = reactionCopy[reactionAttributeName].reduce((a, v) => ( - { ...a, [v.id]: getMaterialData(v) }), {}); + { ...a, [v.id]: getMaterialData(v, materialType, gasMode, vesselVolume) }), {}); }); return updateVariationsRowOnReferenceMaterialChange(row, reactionCopy.has_polymers); @@ -119,16 +166,18 @@ function updateVariationsRow(row, field, value, reactionHasPolymers) { /* Some attributes of a material need to be updated in response to changes in other attributes: - attribute | needs to be updated in response to - -----------|---------------------------------- - equivalent | own mass changes^, own amount changes^, reference material's mass changes~, reference material's amount changes~ - mass | own amount changes^, own equivalent changes^ - amount | own mass changes^, own equivalent changes^ - yield | own mass changes^, own amount changes^x, reference material's mass changes~, reference material's amount changes~ + attribute | needs to be updated in response to change in + ------------------|--------------------------------------------- + equivalent | mass^, amount^, reference material's mass~, reference material's amount~ + mass | amount^, equivalent^, concentration^, temperature^ + amount | mass^, equivalent^, concentration^, temperature^ + yield | mass^, amount^x, concentration^, temperature^, reference material's mass~, reference material's amount~ + turnoverNumber | concentration^, temperature^ + turnoverFrequency | concentration^, temperature^, duration^ - ^: handled in corresponding cell parsers (changes within single material) + ^: handled in cell parsers (changes within single material) ~: handled here (row-wide changes across materials) - x: not permitted according to business logic + ^x: not permitted according to business logic */ let updatedRow = cloneDeep(row); set(updatedRow, field, value); @@ -145,11 +194,15 @@ function updateColumnDefinitions(columnDefinitions, field, property, newValue) { updatedColumnDefinitions.forEach((columnDefinition) => { if (columnDefinition.groupId) { // Column group. - columnDefinition.children.forEach((child) => { - if (child.field === field) { - child[property] = newValue; - } - }); + if (columnDefinition.groupId === field) { + columnDefinition[property] = newValue; + } else { + columnDefinition.children.forEach((child) => { + if (child.field === field) { + child[property] = newValue; + } + }); + } } else if (columnDefinition.field === field) { // Single column. columnDefinition[property] = newValue; @@ -165,13 +218,15 @@ export { amountUnits, temperatureUnits, durationUnits, + concentrationUnits, + getStandardUnits, convertUnit, - getStandardUnit, materialTypes, getVariationsRowName, createVariationsRow, copyVariationsRow, updateVariationsRow, updateColumnDefinitions, - getCellDataType + getCellDataType, + getStandardValue, }; diff --git a/app/packs/src/models/Reaction.js b/app/packs/src/models/Reaction.js index 9390ed19b5..753e4d8a46 100644 --- a/app/packs/src/models/Reaction.js +++ b/app/packs/src/models/Reaction.js @@ -206,83 +206,7 @@ export default class Reaction extends Element { set variations(variations) { /* - variations data structure (also see Entities::ReactionVariationEntity): - [ - { - "id": , - "notes": , - "properties": { - "temperature": {"value": , "unit": }, - "duration": {"value": , "unit": } - }, - "analyses": [, , ...], - "startingMaterials": { - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - ... - }, - "reactants": { - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - ... - }, - "products": { - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - ... - }, - "solvents": { - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - : { - "mass": {"value": , "unit": }, - "amount": {"value": , "unit": }, - "volume": {"value": , "unit": }, - "aux": {...} - }, - ... - }, - }, - { - "id": "", - ... - }, - ... - ] + See Entities::ReactionVariationEntity for details on the data structure. Units are to be treated as immutable. Units and corresponding values are changed (not mutated in the present data-structure!) only for display or export diff --git a/db/migrate/20241129093956_add_gas_materials_to_reaction_variations.rb b/db/migrate/20241129093956_add_gas_materials_to_reaction_variations.rb new file mode 100644 index 0000000000..78496a56a7 --- /dev/null +++ b/db/migrate/20241129093956_add_gas_materials_to_reaction_variations.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +SAMPLES_TYPES = %w[startingMaterials products reactants solvents].freeze + +def migrate_aux(material, material_type) + material['aux'].delete('yield') + material['aux'].delete('equivalent') + + material['aux']['materialType'] ||= material_type + material['aux']['gasType'] ||= 'off' + material['aux']['vesselVolume'] ||= 0 +end + +def migrate_starting_material(material) + # Transfer `equivalent` from `aux` to `equivalent` entry. + equivalent = material['aux']['equivalent'] + material['equivalent'] ||= { 'value' => equivalent, 'unit' => nil } + + material.delete('volume') if material['aux']['gasType'] != 'feedstock' +end + +def migrate_solvent(material) + material.delete('mass') + material.delete('amount') +end + +def migrate_product(material) + # Transfer `yield` from `aux` to `yield` entry. + percent_yield = material['aux']['yield'] + material['yield'] ||= { 'value' => percent_yield, 'unit' => nil } + + material.delete('volume') +end + +class AddGasMaterialsToReactionVariations < ActiveRecord::Migration[6.1] + def up + # Prior to this migration all materials have a `mass`, `amount`, and `volume` entry, irrespective of material type. + Reaction.where.not('variations = ?', '[]').find_each do |reaction| + variations = reaction.variations + variations.each do |variation| + SAMPLES_TYPES.each do |key| + variation[key].each_value do |material| + case key + when 'startingMaterials', 'reactants' + migrate_starting_material(material) + when 'solvents' + migrate_solvent(material) + when 'products' + migrate_product(material) + end + migrate_aux(material, key) + end + end + end + reaction.update_column(:variations, variations) + end + end + + def down + # After this migration all materials have a `mass`, `amount`, and `volume` entry, irrespective of material type. + Reaction.where.not('variations = ?', '[]').find_each do |reaction| + variations = reaction.variations + variations.each do |variation| + SAMPLES_TYPES.each do |key| + variation[key].each_value do |material| + material['aux'].delete('materialType') + material['aux'].delete('gasType') + material['aux'].delete('vesselVolume') + + percent_yield = material['yield']&.[]('value') + material['aux']['yield'] ||= percent_yield + material.delete('yield') + + equivalent = material['equivalent']&.[]('value') + material['aux']['equivalent'] ||= equivalent + material.delete('equivalent') + + material.delete('duration') + material.delete('temperature') + material.delete('concentration') + material.delete('turnoverNumber') + material.delete('turnoverFrequency') + + material['mass'] ||= { 'value' => 0, 'unit' => 'g' } + material['amount'] ||= { 'value' => 0, 'unit' => 'mol' } + material['volume'] ||= { 'value' => 0, 'unit' => 'l' } + end + end + end + reaction.update_column(:variations, variations) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 12ab0a0f71..b2a1c523d6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_07_11_120833) do +ActiveRecord::Schema.define(version: 2024_11_29_093956) do # These are extensions that must be enabled in order to support this database enable_extension "hstore" diff --git a/lib/reporter/docx/detail_reaction.rb b/lib/reporter/docx/detail_reaction.rb index d060908651..ca1b309f40 100644 --- a/lib/reporter/docx/detail_reaction.rb +++ b/lib/reporter/docx/detail_reaction.rb @@ -60,23 +60,25 @@ def variations 'startingMaterials' => variation_materials(var, :startingMaterials), 'reactants' => variation_materials(var, :reactants), 'solvents' => variation_materials(var, :solvents), - 'products' => variation_products(var), + 'products' => variation_materials(var, :products), 'notes' => var[:notes], } end end def variation_materials(variation, type) - variation[type].map do |_, v| - "#{v[:aux][:sumFormula]}:\n#{v[:value]} #{v[:unit]} " \ - "; #{v[:aux].fetch(:equivalent, 'n/a')} Equiv " + - (v[:aux][:isReference] ? '; Ref' : '') - end - end + variation[type].map do |_, vi| + result = "#{vi[:aux][:sumFormula]}:\n" + + meta_data = [vi[:aux][:isReference] ? 'Ref' : '', vi[:aux][:gasType] == 'off' ? '' : vi[:aux][:gasType]] + meta_data = meta_data.reject(&:empty?).join(', ') + result += "(#{meta_data})\n" if meta_data.present? + + result + vi.map do |k, vj| + next if k == :aux - def variation_products(variation) - variation[:products].map do |_, v| - "#{v[:aux][:sumFormula]}:\n#{v[:value]} #{v[:unit]}; (#{v[:aux][:yield]} % yield)" + "#{k.to_s.gsub(/([A-Z])/, ' \1').downcase.strip}: #{vj[:value]} #{vj[:unit]};\n" + end.join end end diff --git a/spec/api/entities/reaction_entity_spec.rb b/spec/api/entities/reaction_entity_spec.rb index 7fa4226336..b5979c688d 100644 --- a/spec/api/entities/reaction_entity_spec.rb +++ b/spec/api/entities/reaction_entity_spec.rb @@ -27,158 +27,182 @@ variations: [{ id: '1', analyses: [1, 2], notes: '', - products: { '47': { aux: { yield: 0, - purity: 0.29, + products: { '47': { aux: { purity: 0.29, loading: nil, molarity: 0, sumFormula: 'C21H18N2O2', coefficient: 1, isReference: false, molecularWeight: 330.37982, - equivalent: nil }, + gasType: 'gas', + materialType: 'products', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } }, - '48': { aux: { yield: 100, - purity: 0.12, + amount: { unit: 'mol', value: nil }, + yield: { unit: nil, value: nil }, + duration: { unit: 'Second(s)', value: 42 }, + temperature: { unit: 'K', value: 42 }, + concentration: { unit: nil, value: 42 }, + turnoverNumber: { unit: nil, value: 42 }, + turnoverFrequency: { unit: nil, value: 42 } }, + '48': { aux: { purity: 0.12, loading: nil, molarity: 0, sumFormula: 'C31H30O4', coefficient: 1, isReference: false, molecularWeight: 466.56750000000005, - equivalent: nil }, + gasType: 'off', + materialType: 'products', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, + yield: { unit: nil, value: nil }, amount: { unit: 'mol', value: nil } } }, - reactants: { '45': { aux: { yield: nil, - purity: 0.78, + reactants: { '45': { aux: { purity: 0.78, loading: nil, molarity: 0, sumFormula: 'C10H17NO2S', coefficient: 1, isReference: false, molecularWeight: 215.31247999999997, - equivalent: nil }, + gasType: 'catalyst', + materialType: 'reactants', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } }, - '46': { aux: { yield: nil, - purity: 0.84, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil } }, + '46': { aux: { purity: 0.84, loading: nil, molarity: 0, sumFormula: 'C10H16O4S', coefficient: 1, isReference: false, molecularWeight: 232.29664, - equivalent: nil }, + gasType: 'off', + materialType: 'reactants', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } } }, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil } } }, properties: { duration: { foo: {}, unit: 'Hour(s)', value: '19' }, temperature: { unit: 'C', value: '1' } }, - startingMaterials: { '43': { aux: { yield: nil, - purity: 0.25, + startingMaterials: { '43': { aux: { purity: 0.25, loading: nil, molarity: 0, sumFormula: 'C10H10BF4IN2', coefficient: 1, isReference: true, molecularWeight: 371.9088828000001, - equivalent: nil }, + gasType: 'feedstock', + materialType: 'startingMaterials', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } }, - '44': { aux: { yield: nil, - purity: 0.4, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil }, + volume: { unit: 'ml', value: 42 } }, + '44': { aux: { purity: 0.4, loading: nil, molarity: 0, sumFormula: 'C32H72Cr2N2O7', coefficient: 1, isReference: false, molecularWeight: 700.9154799999998, - equivalent: nil }, + gasType: 'off', + materialType: 'startingMaterials', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } } }, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil } } }, solvents: {} }, { id: '2', analyses: [], notes: '', - products: { '47': { aux: { yield: 0, - purity: 0.29, + products: { '47': { aux: { purity: 0.29, loading: nil, molarity: 0, sumFormula: 'C21H18N2O2', coefficient: 1, isReference: false, molecularWeight: 330.37982, - equivalent: nil }, + gasType: 'gas', + materialType: 'products', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } }, - '48': { aux: { yield: 2, - purity: 0.12, + amount: { unit: 'mol', value: nil }, + yield: { unit: nil, value: nil }, + duration: { unit: 'Second(s)', value: 42 }, + temperature: { unit: 'K', value: 42 }, + concentration: { unit: nil, value: 42 }, + turnoverNumber: { unit: nil, value: 42 }, + turnoverFrequency: { unit: nil, value: 42 } }, + '48': { aux: { purity: 0.12, loading: nil, molarity: 0, sumFormula: 'C31H30O4', coefficient: 1, isReference: false, molecularWeight: 466.56750000000005, - equivalent: nil }, + gasType: 'off', + materialType: 'products', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, + yield: { unit: nil, value: nil }, amount: { unit: 'mol', value: nil } } }, - reactants: { '45': { aux: { yield: nil, - purity: 0.78, + reactants: { '45': { aux: { purity: 0.78, loading: nil, molarity: 0, sumFormula: 'C10H17NO2S', coefficient: 1, isReference: false, molecularWeight: 215.31247999999997, - equivalent: nil }, + gasType: 'catalyst', + materialType: 'reactants', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } }, - '46': { aux: { yield: nil, - purity: 0.84, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil } }, + '46': { aux: { purity: 0.84, loading: nil, molarity: 0, sumFormula: 'C10H16O4S', coefficient: 1, isReference: false, molecularWeight: 232.29664, - equivalent: nil }, + gasType: 'off', + materialType: 'reactants', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } } }, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil } } }, properties: { duration: { unit: 'Hour(s)', value: '15' }, temperature: { unit: 'C', value: '2' } }, - startingMaterials: { '43': { aux: { yield: nil, - purity: 0.25, + startingMaterials: { '43': { aux: { purity: 0.25, loading: nil, molarity: 0, sumFormula: 'C10H10BF4IN2', coefficient: 1, isReference: true, molecularWeight: 371.9088828000001, - equivalent: nil }, + gasType: 'feedstock', + materialType: 'startingMaterials', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } }, - '44': { aux: { yield: nil, - purity: 0.4, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil }, + volume: { unit: 'ml', value: 42 } }, + '44': { aux: { purity: 0.4, loading: nil, molarity: 0, sumFormula: 'C32H72Cr2N2O7', coefficient: 1, isReference: false, molecularWeight: 700.9154799999998, - equivalent: nil }, + gasType: 'off', + materialType: 'startingMaterials', + vesselVolume: 42 }, mass: { unit: 'g', value: nil }, - volume: { unit: 'l', value: nil }, - amount: { unit: 'mol', value: nil } } }, + amount: { unit: 'mol', value: nil }, + equivalent: { unit: nil, value: nil } } }, solvents: {} }], ) end diff --git a/spec/javascripts/helper/reactionVariationsHelpers.js b/spec/javascripts/helper/reactionVariationsHelpers.js index a6e5f2b9bc..25ba0a01f9 100644 --- a/spec/javascripts/helper/reactionVariationsHelpers.js +++ b/spec/javascripts/helper/reactionVariationsHelpers.js @@ -21,6 +21,30 @@ async function setUpReaction() { return reaction; } +async function setUpGaseousReaction() { + const reaction = await ReactionFactory.build('ReactionFactory.water+water=>water+water'); + reaction.starting_materials[0].reference = true; + reaction.starting_materials[0].gas_type = 'catalyst'; + reaction.reactants = [await setUpMaterial()]; + reaction.reactants[0].gas_type = 'feedstock'; + reaction.products[0].gas_type = 'gas'; + reaction.products[0].gas_phase_data = { + time: { unit: 'h', value: 1 }, + temperature: { unit: 'K', value: 1 }, + turnover_number: 1, + part_per_million: 1, + turnover_frequency: { unit: 'TON/h', value: 1 } + }; + + const variations = []; + for (let id = 0; id < 3; id++) { + variations.push(createVariationsRow(reaction, variations, true, 10)); + } + reaction.variations = variations; + + return reaction; +} + function getColumnGroupChild(columnDefinitions, groupID, fieldID) { const columnGroup = columnDefinitions.find((group) => group.groupId === groupID); const columnDefinition = columnGroup.children.find((child) => child.field === fieldID); @@ -39,5 +63,5 @@ function getColumnDefinitionsMaterialIDs(columnDefinitions, materialType) { } export { - setUpMaterial, setUpReaction, getColumnGroupChild, getColumnDefinitionsMaterialIDs + setUpMaterial, setUpReaction, setUpGaseousReaction, getColumnGroupChild, getColumnDefinitionsMaterialIDs }; diff --git a/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents.spec.js b/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents.spec.js deleted file mode 100644 index 3f490136c2..0000000000 --- a/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents.spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import expect from 'expect'; -import { - EquivalentFormatter, EquivalentParser, PropertyFormatter, PropertyParser, MaterialFormatter, MaterialParser -} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents'; -import { setUpReaction } from 'helper/reactionVariationsHelpers'; - -describe('ReactionVariationsCellComponents', async () => { - describe('FormatterComponents', () => { - it('EquivalentFormatter returns number string with correct precision', () => { - const cellData = { aux: { equivalent: 1.2345 } }; - - expect(EquivalentFormatter({ value: cellData })).toEqual('1.234'); - }); - it('PropertyFormatter returns number string with correct precision', () => { - const cellData = { value: 1.2345, unit: 'Second(s)' }; - const colDef = { currentEntryWithDisplayUnit: { displayUnit: 'Minute(s)' } }; - - expect(PropertyFormatter({ value: cellData, colDef })).toEqual('0.02057'); - }); - it('MaterialFormatter returns number string with correct precision', () => { - const cellData = { amount: { value: 1.2345, unit: 'mol' } }; - const colDef = { currentEntryWithDisplayUnit: { entry: 'amount', displayUnit: 'mmol' } }; - - expect(MaterialFormatter({ value: cellData, colDef })).toEqual('1235'); - }); - }); - describe('EquivalentParser', async () => { - let variationsRow; - let cellData; - beforeEach(async () => { - const reaction = await setUpReaction(); - variationsRow = reaction.variations[0]; - cellData = Object.values(variationsRow.reactants)[0]; - }); - it('rejects negative value', () => { - const newValue = '-1'; - const updatedCellData = EquivalentParser({ data: variationsRow, oldValue: cellData, newValue }); - - expect(updatedCellData.aux.equivalent).toEqual(0); - }); - it('updates mass and amount', () => { - const newValue = '2'; - const updatedCellData = EquivalentParser({ data: variationsRow, oldValue: cellData, newValue }); - - expect(updatedCellData.mass.value).toBeCloseTo(cellData.mass.value * 2, 0.01); - expect(updatedCellData.amount.value).toBeCloseTo(cellData.amount.value * 2, 0.01); - }); - }); - describe('PropertyParser', async () => { - it('rejects negative value for duration', () => { - const cellData = { value: 120, unit: 'Second(s)' }; - const colDef = { currentEntryWithDisplayUnit: { entry: 'duration', displayUnit: 'Minute(s)' } }; - const newValue = '-1'; - const updatedCellData = PropertyParser({ oldValue: cellData, newValue, colDef }); - - expect(updatedCellData.value).toEqual(0); - }); - it('accepts negative value for temperature', () => { - const cellData = { value: 120, unit: '°C' }; - const colDef = { currentEntryWithDisplayUnit: { entry: 'temperature', displayUnit: 'K' } }; - const newValue = '-1'; - const updatedCellData = PropertyParser({ oldValue: cellData, newValue, colDef }); - - expect(updatedCellData.value).toEqual(-274.15); - }); - }); - describe('MaterialParser', async () => { - let variationsRow; - let cellData; - let context; - beforeEach(async () => { - const reaction = await setUpReaction(); - variationsRow = reaction.variations[0]; - cellData = Object.values(variationsRow.reactants)[0]; - context = { reactionHasPolymers: false }; - }); - it('rejects negative value', () => { - const colDef = { field: 'reactants.42', currentEntryWithDisplayUnit: { entry: 'amount', displayUnit: 'mmol' } }; - const updatedCellData = MaterialParser({ - data: variationsRow, oldValue: cellData, newValue: '-1', colDef, context - }); - - expect(updatedCellData.amount.value).toEqual(0); - }); - it('adapts mass when updating amount', () => { - const colDef = { field: 'reactants.42', currentEntryWithDisplayUnit: { entry: 'amount', displayUnit: 'mmol' } }; - - expect(cellData.mass.value).toBe(100); - - const updatedCellData = MaterialParser({ - data: variationsRow, oldValue: cellData, newValue: '42', colDef, context - }); - - expect(updatedCellData.mass.value).toBeCloseTo(0.75, 0.1); - }); - it('adapts amount when updating mass', () => { - const colDef = { field: 'reactants.42', currentEntryWithDisplayUnit: { entry: 'mass', displayUnit: 'g' } }; - - expect(cellData.amount.value).toBeCloseTo(5.5, 0.1); - - const updatedCellData = MaterialParser({ - data: variationsRow, oldValue: cellData, newValue: '42', colDef, context - }); - - expect(updatedCellData.amount.value).toBeCloseTo(2.33, 0.1); - }); - }); -}); diff --git a/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents.spec.js b/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents.spec.js new file mode 100644 index 0000000000..7789512650 --- /dev/null +++ b/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents.spec.js @@ -0,0 +1,236 @@ +import expect from 'expect'; +import { + EquivalentParser, PropertyFormatter, PropertyParser, MaterialFormatter, MaterialParser, FeedstockParser, GasParser +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents'; +import { setUpReaction, setUpGaseousReaction } from 'helper/reactionVariationsHelpers'; + +describe('ReactionVariationsComponents', async () => { + describe('FormatterComponents', () => { + it('PropertyFormatter returns number string with correct precision', () => { + const cellData = { value: 1.2345, unit: 'Second(s)' }; + const colDef = { entryDefs: { displayUnit: 'Minute(s)' } }; + + expect(PropertyFormatter({ value: cellData, colDef })).toEqual('0.02057'); + }); + it('MaterialFormatter returns number string with correct precision', () => { + const cellData = { amount: { value: 1.2345, unit: 'mol' } }; + const colDef = { entryDefs: { currentEntry: 'amount', displayUnit: 'mmol' } }; + + expect(MaterialFormatter({ value: cellData, colDef })).toEqual('1235'); + }); + }); + describe('EquivalentParser', async () => { + let variationsRow; + let cellData; + beforeEach(async () => { + const reaction = await setUpReaction(); + variationsRow = reaction.variations[0]; + cellData = Object.values(variationsRow.reactants)[0]; + }); + it('rejects negative value', () => { + const newValue = '-1'; + const updatedCellData = EquivalentParser({ data: variationsRow, oldValue: cellData, newValue }); + + expect(updatedCellData.equivalent.value).toEqual(0); + }); + it('updates mass and amount', () => { + const newValue = '2'; + const updatedCellData = EquivalentParser({ data: variationsRow, oldValue: cellData, newValue }); + + expect(updatedCellData.mass.value).toBeCloseTo(cellData.mass.value * 2, 0.01); + expect(updatedCellData.amount.value).toBeCloseTo(cellData.amount.value * 2, 0.01); + }); + }); + describe('PropertyParser', async () => { + it('rejects negative value for duration', () => { + const cellData = { value: 120, unit: 'Second(s)' }; + const colDef = { entryDefs: { currentEntry: 'duration', displayUnit: 'Minute(s)' } }; + const newValue = '-1'; + const updatedCellData = PropertyParser({ oldValue: cellData, newValue, colDef }); + + expect(updatedCellData.value).toEqual(0); + }); + it('accepts negative value for temperature', () => { + const cellData = { value: 120, unit: '°C' }; + const colDef = { entryDefs: { currentEntry: 'temperature', displayUnit: 'K' } }; + const newValue = '-1'; + const updatedCellData = PropertyParser({ oldValue: cellData, newValue, colDef }); + + expect(updatedCellData.value).toEqual(-273.15); + }); + }); + describe('MaterialParser', async () => { + let variationsRow; + let cellData; + let context; + beforeEach(async () => { + const reaction = await setUpReaction(); + variationsRow = reaction.variations[0]; + cellData = Object.values(variationsRow.reactants)[0]; + context = { reactionHasPolymers: false }; + }); + it('rejects negative value', () => { + const colDef = { field: 'reactants.42', entryDefs: { currentEntry: 'amount', displayUnit: 'mmol' } }; + const updatedCellData = MaterialParser({ + data: variationsRow, oldValue: cellData, newValue: '-1', colDef, context + }); + + expect(updatedCellData.amount.value).toEqual(0); + }); + it('adapts mass when updating amount', () => { + const colDef = { field: 'reactants.42', entryDefs: { currentEntry: 'amount', displayUnit: 'mmol' } }; + + expect(cellData.mass.value).toBe(100); + + const updatedCellData = MaterialParser({ + data: variationsRow, oldValue: cellData, newValue: '42', colDef, context + }); + + expect(updatedCellData.mass.value).toBeCloseTo(0.75, 0.1); + }); + it('adapts amount when updating mass', () => { + const colDef = { field: 'reactants.42', entryDefs: { currentEntry: 'mass', displayUnit: 'g' } }; + + expect(cellData.amount.value).toBeCloseTo(5.5, 0.1); + + const updatedCellData = MaterialParser({ + data: variationsRow, oldValue: cellData, newValue: '42', colDef, context + }); + + expect(updatedCellData.amount.value).toBeCloseTo(2.33, 0.1); + }); + it("adapts non-reference materials' equivalent when updating mass", async () => { + const colDef = { field: 'reactants.42', entryDefs: { currentEntry: 'mass', displayUnit: 'g' } }; + + const updatedCellData = MaterialParser({ + data: variationsRow, oldValue: cellData, newValue: `${cellData.mass.value * 2}`, colDef, context + }); + + expect(updatedCellData.equivalent.value).toBe(cellData.equivalent.value * 2); + }); + it("adapts non-reference materials' yield when updating mass", async () => { + cellData = Object.values(variationsRow.products)[0]; + const colDef = { field: 'products.42', entryDefs: { currentEntry: 'mass', displayUnit: 'g' } }; + + const updatedCellData = MaterialParser({ + data: variationsRow, oldValue: cellData, newValue: `${cellData.mass.value * 0.1}`, colDef, context + }); + + expect(updatedCellData.yield.value).toBeLessThan(cellData.yield.value); + }); + }); + describe('FeedstockParser', async () => { + let variationsRow; + let cellData; + let context; + beforeEach(async () => { + const reaction = await setUpGaseousReaction(); + variationsRow = reaction.variations[0]; + cellData = Object.values(variationsRow.reactants)[0]; + context = { reactionHasPolymers: false }; + }); + it('rejects negative value', () => { + const colDef = { field: 'reactants.42', entryDefs: { currentEntry: 'equivalent', displayUnit: null } }; + const updatedCellData = FeedstockParser({ + data: variationsRow, oldValue: cellData, newValue: '-1', colDef, context + }); + + expect(updatedCellData.equivalent.value).toEqual(0); + }); + it('adapts nothing when updating equivalent', () => { + const colDef = { field: 'reactant.42', entryDefs: { currentEntry: 'equivalent', displayUnit: null } }; + + const updatedCellData = FeedstockParser({ + data: variationsRow, oldValue: cellData, newValue: `${cellData.equivalent.value * 2}`, colDef, context + }); + + expect(updatedCellData.equivalent.value).toBe(cellData.equivalent.value * 2); + expect(updatedCellData.mass.value).toBe(cellData.mass.value); + expect(updatedCellData.amount.value).toBe(cellData.amount.value); + expect(updatedCellData.volume.value).toBe(cellData.volume.value); + }); + it('adapts other entries when updating volume', () => { + const colDef = { field: 'reactant.42', entryDefs: { currentEntry: 'volume', displayUnit: 'l' } }; + + const updatedCellData = FeedstockParser({ + data: variationsRow, oldValue: cellData, newValue: `${cellData.volume.value * 2}`, colDef, context + }); + + expect(updatedCellData.volume.value).toBe(cellData.volume.value * 2); + expect(updatedCellData.mass.value).toBeGreaterThan(cellData.mass.value); + expect(updatedCellData.amount.value).toBeGreaterThan(cellData.amount.value); + expect(updatedCellData.equivalent.value).toBeGreaterThan(cellData.equivalent.value); + }); + it('adapts other entries when updating amount', () => { + const colDef = { field: 'reactant.42', entryDefs: { currentEntry: 'amount', displayUnit: 'mol' } }; + + const updatedCellData = FeedstockParser({ + data: variationsRow, oldValue: cellData, newValue: `${cellData.amount.value * 2}`, colDef, context + }); + + expect(updatedCellData.amount.value).toBe(cellData.amount.value * 2); + expect(updatedCellData.mass.value).toBeGreaterThan(cellData.mass.value); + expect(updatedCellData.volume.value).toBeGreaterThan(cellData.volume.value); + expect(updatedCellData.equivalent.value).toBeGreaterThan(cellData.equivalent.value); + }); + }); + describe('GasParser', async () => { + let variationsRow; + let cellData; + let context; + beforeEach(async () => { + const reaction = await setUpGaseousReaction(); + variationsRow = reaction.variations[0]; + cellData = Object.values(variationsRow.products)[0]; + context = { reactionHasPolymers: false }; + }); + it('rejects negative value', () => { + const colDef = { field: 'products.42', entryDefs: { currentEntry: 'duration', displayUnit: 'Hour(s)' } }; + const updatedCellData = GasParser({ + data: variationsRow, oldValue: cellData, newValue: '-1', colDef, context + }); + + expect(updatedCellData.duration.value).toEqual(0); + }); + it('adapts only turnover frequency when updating duration', () => { + const colDef = { field: 'products.42', entryDefs: { currentEntry: 'duration', displayUnit: 'Hour(s)' } }; + + const updatedCellData = GasParser({ + data: variationsRow, oldValue: cellData, newValue: '2', colDef, context + }); + + expect(updatedCellData.mass.value).toBe(cellData.mass.value); + expect(updatedCellData.amount.value).toBe(cellData.amount.value); + expect(updatedCellData.yield.value).toBe(cellData.yield.value); + expect(updatedCellData.turnoverNumber.value).toBe(cellData.turnoverNumber.value); + + expect(updatedCellData.turnoverFrequency.value).toBeLessThan(cellData.turnoverFrequency.value); + }); + it('adapts other entries when updating concentration', () => { + const colDef = { field: 'products.42', entryDefs: { currentEntry: 'concentration', displayUnit: 'ppm' } }; + + const updatedCellData = GasParser({ + data: variationsRow, oldValue: cellData, newValue: `${cellData.concentration.value * 2}`, colDef, context + }); + + expect(updatedCellData.mass.value).not.toBe(cellData.mass.value); + expect(updatedCellData.amount.value).not.toBe(cellData.amount.value); + expect(updatedCellData.yield.value).not.toBe(cellData.yield.value); + expect(updatedCellData.turnoverNumber.value).not.toBe(cellData.turnoverNumber.value); + expect(updatedCellData.turnoverFrequency.value).not.toBe(cellData.turnoverFrequency.value); + }); + it('adapts other entries when updating temperature', () => { + const colDef = { field: 'products.42', entryDefs: { currentEntry: 'temperature', displayUnit: 'K' } }; + + const updatedCellData = GasParser({ + data: variationsRow, oldValue: cellData, newValue: `${cellData.temperature.value / 2}`, colDef, context + }); + + expect(updatedCellData.mass.value).not.toBe(cellData.mass.value); + expect(updatedCellData.amount.value).not.toBe(cellData.amount.value); + expect(updatedCellData.yield.value).not.toBe(cellData.yield.value); + expect(updatedCellData.turnoverNumber.value).not.toBe(cellData.turnoverNumber.value); + expect(updatedCellData.turnoverFrequency.value).not.toBe(cellData.turnoverFrequency.value); + }); + }); +}); diff --git a/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.spec.js b/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.spec.js index 474174f6c3..77e80e41b3 100644 --- a/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.spec.js +++ b/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials.spec.js @@ -2,13 +2,18 @@ import expect from 'expect'; import { getReactionMaterials, updateVariationsRowOnReferenceMaterialChange, removeObsoleteMaterialsFromVariations, addMissingMaterialsToVariations, - updateNonReferenceMaterialOnMassChange, updateColumnDefinitionsMaterials, - getMaterialColumnGroupChild, getReactionMaterialsIDs + updateColumnDefinitionsMaterials, + getMaterialColumnGroupChild, getReactionMaterialsIDs, updateColumnDefinitionsMaterialTypes, + getReactionMaterialsGasTypes, updateVariationsGasTypes, cellIsEditable } from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsMaterials'; import { EquivalentParser -} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsCellComponents'; -import { setUpMaterial, setUpReaction, getColumnDefinitionsMaterialIDs } from 'helper/reactionVariationsHelpers'; +} from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsComponents'; +import { + setUpMaterial, setUpReaction, setUpGaseousReaction, getColumnDefinitionsMaterialIDs, getColumnGroupChild +} from 'helper/reactionVariationsHelpers'; +import { materialTypes } from 'src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils'; +import { cloneDeep } from 'lodash'; describe('ReactionVariationsMaterials', () => { it('removes obsolete materials', async () => { @@ -38,7 +43,7 @@ describe('ReactionVariationsMaterials', () => { reaction.starting_materials.push(material); const updatedStartingMaterialIDs = reaction.starting_materials.map((startingMaterial) => startingMaterial.id); const currentMaterials = getReactionMaterials(reaction); - const updatedVariations = addMissingMaterialsToVariations(reaction.variations, currentMaterials); + const updatedVariations = addMissingMaterialsToVariations(reaction.variations, currentMaterials, false); updatedVariations .forEach((variation) => { expect(Object.keys(variation.startingMaterials)).toEqual(updatedStartingMaterialIDs); @@ -47,49 +52,25 @@ describe('ReactionVariationsMaterials', () => { it('updates yield when product mass changes', async () => { const reaction = await setUpReaction(); const productID = reaction.products[0].id; - expect(reaction.variations[0].products[productID].aux.yield).toBe(100); + expect(reaction.variations[0].products[productID].yield.value).toBe(100); reaction.variations[0].products[productID].mass.value = 2; const updatedVariationsRow = updateVariationsRowOnReferenceMaterialChange( reaction.variations[0], reaction.hasPolymers() ); - expect(updatedVariationsRow.products[productID].aux.yield).toBe(5); + expect(updatedVariationsRow.products[productID].yield.value).toBe(5); }); it("updates non-reference materials' equivalents when reference material's mass changes", async () => { const reaction = await setUpReaction(); const reactantID = reaction.reactants[0].id; - expect(reaction.variations[0].reactants[reactantID].aux.equivalent).toBe(1); + expect(reaction.variations[0].reactants[reactantID].equivalent.value).toBe(1); Object.values(reaction.variations[0].startingMaterials).forEach((material) => { if (material.aux.isReference) { material.mass.value = 2; } }); const updatedVariationsRow = updateVariationsRowOnReferenceMaterialChange(reaction.variations[0]); - expect(updatedVariationsRow.reactants[reactantID].aux.equivalent).toBeCloseTo(50, 0.01); - }); - it("updates non-reference materials' equivalents when own mass changes", async () => { - const reaction = await setUpReaction(); - const reactant = Object.values(reaction.variations[0].reactants)[0]; - reactant.mass.value *= 0.42; - const updatedReactant = updateNonReferenceMaterialOnMassChange( - reaction.variations[0], - reactant, - 'reactants', - false - ); - expect(reactant.aux.equivalent).toBeGreaterThan(updatedReactant.aux.equivalent); - }); - it("updates non-reference materials' yields when own mass changes", async () => { - const reaction = await setUpReaction(); - const product = Object.values(reaction.variations[0].products)[0]; - product.mass.value *= 0.042; - const updatedProduct = updateNonReferenceMaterialOnMassChange( - reaction.variations[0], - product, - 'products', - true - ); - expect(product.aux.yield).toBeGreaterThan(updatedProduct.aux.yield); + expect(updatedVariationsRow.reactants[reactantID].equivalent.value).toBeCloseTo(50, 0.01); }); it("updates materials' mass when equivalent changes", async () => { const reaction = await setUpReaction(); @@ -98,7 +79,7 @@ describe('ReactionVariationsMaterials', () => { const updatedReactant = EquivalentParser({ data: variationsRow, oldValue: reactant, - newValue: Number(reactant.aux.equivalent * 0.42).toString() + newValue: Number(reactant.equivalent.value * 0.42).toString() }); expect(reactant.mass.value).toBeGreaterThan(updatedReactant.mass.value); expect(EquivalentParser({ @@ -112,7 +93,7 @@ describe('ReactionVariationsMaterials', () => { const reactionMaterials = getReactionMaterials(reaction); const columnDefinitions = Object.entries(reactionMaterials).map(([materialType, materials]) => ({ groupId: materialType, - children: materials.map((material) => getMaterialColumnGroupChild(material, materialType, null)) + children: materials.map((material) => getMaterialColumnGroupChild(material, materialType, null, false)) })); const startingMaterialIDs = reactionMaterials.startingMaterials.map((material) => material.id); @@ -120,7 +101,7 @@ describe('ReactionVariationsMaterials', () => { reactionMaterials.startingMaterials.pop(); const updatedStartingMaterialIDs = reactionMaterials.startingMaterials.map((material) => material.id); - const updatedColumnDefinitions = updateColumnDefinitionsMaterials(columnDefinitions, reactionMaterials, null); + const updatedColumnDefinitions = updateColumnDefinitionsMaterials(columnDefinitions, reactionMaterials, null, false); expect(getColumnDefinitionsMaterialIDs( updatedColumnDefinitions, 'startingMaterials' @@ -133,4 +114,108 @@ describe('ReactionVariationsMaterials', () => { expect(Array.isArray(reactionMaterialsIDs)).toBe(true); expect(new Set(reactionMaterialsIDs).size).toBe(5); }); + it('retrieves reaction material gas types', async () => { + const reaction = await setUpGaseousReaction(); + const reactionMaterials = getReactionMaterials(reaction); + const reactionMaterialsGasTypes = getReactionMaterialsGasTypes(reactionMaterials); + expect(reactionMaterialsGasTypes).toEqual(['catalyst', 'off', 'feedstock', 'gas', 'off']); + }); + it("updates materials' gas type", async () => { + const reaction = await setUpGaseousReaction(); + const currentMaterials = getReactionMaterials(reaction); + + const updatedMaterials = cloneDeep(currentMaterials); + updatedMaterials.startingMaterials[0].gas_type = 'feedstock'; + updatedMaterials.reactants[0].gas_type = 'catalyst'; + updatedMaterials.products[0].gas_type = 'off'; + + const updatedVariations = updateVariationsGasTypes(reaction.variations, updatedMaterials, false); + + const variationsRow = reaction.variations[0]; + const updatedVariationsRow = updatedVariations[0]; + + expect(variationsRow.startingMaterials[Object.keys(variationsRow.startingMaterials)[0]].aux.gasType).not.toBe( + updatedVariationsRow.startingMaterials[Object.keys(updatedVariationsRow.startingMaterials)[0]].aux.gasType + ); + expect(variationsRow.reactants[Object.keys(variationsRow.reactants)[0]].aux.gasType).not.toBe( + updatedVariationsRow.reactants[Object.keys(updatedVariationsRow.reactants)[0]].aux.gasType + ); + expect(variationsRow.products[Object.keys(variationsRow.products)[0]].aux.gasType).not.toBe( + updatedVariationsRow.products[Object.keys(updatedVariationsRow.products)[0]].aux.gasType + ); + }); + it('updates column definitions of gaseous materials', async () => { + const reaction = await setUpReaction(); + + const reactionMaterials = getReactionMaterials(reaction); + const columnDefinitions = Object.entries(reactionMaterials).map(([materialType, materials]) => ({ + groupId: materialType, + children: materials.map((material) => getMaterialColumnGroupChild(material, materialType, null, false)) + })); + + Object.keys(materialTypes).forEach((materialType) => { + reactionMaterials[materialType].forEach((material) => { + switch (materialType) { + case 'startingMaterials': + material.gas_type = 'catalyst'; + break; + case 'reactants': + material.gas_type = 'feedstock'; + break; + case 'products': + material.gas_type = 'gas'; + break; + default: + break; + } + }); + }); + + const updatedColumnDefinitions = updateColumnDefinitionsMaterialTypes( + columnDefinitions, + reactionMaterials, + true + ); + + const productIDs = getColumnDefinitionsMaterialIDs(updatedColumnDefinitions, 'products'); + const productColumnDefinition = getColumnGroupChild( + updatedColumnDefinitions, + 'products', + `products.${productIDs[0]}` + ); + expect(productColumnDefinition.cellDataType).toBe('gas'); + expect(productColumnDefinition.entryDefs.currentEntry).toBe('duration'); + expect(productColumnDefinition.entryDefs.displayUnit).toBe('Second(s)'); + + const reactantIDs = getColumnDefinitionsMaterialIDs(updatedColumnDefinitions, 'reactants'); + const reactantColumnDefinition = getColumnGroupChild( + updatedColumnDefinitions, + 'reactants', + `reactants.${reactantIDs[0]}` + ); + expect(reactantColumnDefinition.cellDataType).toBe('feedstock'); + const { currentEntry } = reactantColumnDefinition.entryDefs; + + expect(currentEntry).toBe('mass'); + }); + it('determines cell editability based on entry', async () => { + const colDef = { + field: 'foo', + entryDefs: { + currentEntry: 'equivalent', + displayUnit: null + } + }; + const data = { + foo: { + aux: { + isReference: true, + gasType: 'off', + materialType: 'startingMaterials' + } + } + }; + const params = { colDef, data }; + expect(cellIsEditable(params)).toBe(false); + }); }); diff --git a/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.spec.js b/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.spec.js index 7372dce5ca..23e4422ede 100644 --- a/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.spec.js +++ b/spec/javascripts/packs/src/apps/mydb/elements/details/reactions/variationsTab/ReactionVariationsUtils.spec.js @@ -32,9 +32,9 @@ describe('ReactionVariationsUtils', () => { temperature: { value: '', unit: '°C' }, duration: { value: NaN, unit: 'Second(s)' }, }); - expect(Object.values(row.products).map((product) => product.aux.yield)).toEqual([100, 100]); - expect(nonReferenceStartingMaterial.aux.equivalent).toBe(1); - expect(reactant.aux.equivalent).toBe(1); + expect(Object.values(row.products).map((product) => product.yield.value)).toEqual([100, 100]); + expect(nonReferenceStartingMaterial.equivalent.value).toBe(1); + expect(reactant.equivalent.value).toBe(1); }); it('copies a row in the variations table', async () => { const reaction = await setUpReaction(); @@ -61,11 +61,11 @@ describe('ReactionVariationsUtils', () => { { ...referenceMaterial, mass: { ...referenceMaterial.mass, value: referenceMaterial.mass.value * 10 } }, reaction.hasPolymers() ); - expect(Object.values(row.reactants)[0].aux.equivalent).toBeGreaterThan( - Object.values(updatedRow.reactants)[0].aux.equivalent + expect(Object.values(row.reactants)[0].equivalent.value).toBeGreaterThan( + Object.values(updatedRow.reactants)[0].equivalent.value ); - expect(Object.values(row.products)[0].aux.yield).toBeGreaterThan( - Object.values(updatedRow.products)[0].aux.yield + expect(Object.values(row.products)[0].yield.value).toBeGreaterThan( + Object.values(updatedRow.products)[0].yield.value ); }); it('updates the definition of a column', async () => { @@ -74,7 +74,7 @@ describe('ReactionVariationsUtils', () => { const field = `startingMaterials.${reactionMaterials.startingMaterials[0].id}`; const columnDefinitions = Object.entries(reactionMaterials).map(([materialType, materials]) => ({ groupId: materialType, - children: materials.map((material) => getMaterialColumnGroupChild(material, materialType, null)) + children: materials.map((material) => getMaterialColumnGroupChild(material, materialType, null, false)) })); expect(getColumnGroupChild(columnDefinitions, 'startingMaterials', field).cellDataType).toBe('material'); const updatedColumnDefinitions = updateColumnDefinitions(