From f6e57a78b6e34f83271c00c4a2566e09d0cdefe3 Mon Sep 17 00:00:00 2001 From: PiTrem Date: Wed, 7 Aug 2024 17:31:36 +0200 Subject: [PATCH] This is the commit message #2: fix: component table header updated feat: add tooltip text for total conc. refactor: eslint warnings This is the commit message #3: feat: change component table for liquid components This is the commit message #4: feat: change component table for liquid components when volume given, calculate the amount when amount is given, calculate the volume handle the calculation when the starting conc. or density being locked reset the entire row when starting conc. or density is updated feat: Sample types extension updates #2058 Squashed commit of the following: commit 9922e1b564ff126c1795f37bc068915608d18326 Author: Tasnim Mehzabin Date: Wed Aug 7 16:37:53 2024 +0200 fix: issue with the Amount field not being set for mixture components feat: calculate other fields based on the Amount field fix: the Volume column being updated though it has been locked commit 13cdb86f09e527e74e2b7aa861b408d0b19a15b0 Author: Tasnim Mehzabin Date: Wed Aug 7 15:13:08 2024 +0200 feat: add the preview image to component in mixtures commit 36c0fae63b8274a7214f3608104870071751dafb Author: Tasnim Mehzabin Date: Tue Aug 6 16:18:52 2024 +0200 refactor: eslint warnings commit 0c9b13a2b3852536f73bd71edf827bd0d3dd34a3 Author: Tasnim Mehzabin Date: Tue Aug 6 16:18:52 2024 +0200 refactor: eslint warnings commit 8c59c3629b3fcfff3c999beb8aad572f3e465e2f Author: Tasnim Mehzabin Date: Tue Aug 6 15:45:56 2024 +0200 feat: add density field for the solid components in mixtures commit b3c2ef176b69c9e3fed981904dc322194384ea1d Author: Tasnim Mehzabin Date: Mon Jul 29 15:59:02 2024 +0200 feat: add tooltip text for the "Total volume" field for Mixture commit 52ee52427f9fcead5d4ff4e27c9874502f979d7e Author: Tasnim Mehzabin Date: Tue Jul 23 16:05:37 2024 +0200 fix: fetching of components after fetching the sample causing the sample appear as edited commit 698bcabd399bc9e7016ab0bab424696042afb8e5 Author: Tasnim Mehzabin Date: Tue Jul 23 13:42:38 2024 +0200 fix (UI): cannot create a single molecule commit 7b04874beecc9df66a6ea546337ac310414cccbe Author: fathia Date: Thu Jun 20 12:08:00 2024 +0000 disable savebtn for mixtures with 0 components commit ce423c4a9fbc9f8dbbbb13198f9a6a036283a0f8 Author: Fathia Idiris Date: Wed Jun 19 16:49:08 2024 +0200 fix: solvent volume column only for mixture samples commit 514c216228c9b5dede3332397fa3867367d7dcb2 Author: Fathia Idiris Date: Wed Jun 19 15:38:20 2024 +0200 fix: fix update ratio commit e51a82affa838d1b287c7d3943bb25f044f08d80 Author: Fathia Idiris Date: Wed Jun 19 09:24:45 2024 +0200 fix: fix ratio after setting purity commit 209d7309c4bba004b4106169960a67a206e1a336 Author: fathia Date: Tue Jun 18 14:28:08 2024 +0000 feat: comp. table calculations with purity commit 91234401ceeeccf2971607043ccbf32997e4e2b9 Author: fathia Date: Tue Jun 18 08:24:27 2024 +0000 fix: update volume of a comp with a density in reactions table. commit d273142476bb8e2f3077a4857bc47f0e70e1e16f Author: Fathia Idiris Date: Mon Jun 17 16:56:42 2024 +0200 feat: comp. table calculations handles density & add purity field commit e58dde66e33048d0cbf7bc8471d21b7503f89300 Author: fathia Date: Tue Jun 11 07:36:04 2024 +0000 refactor: save/update components api endpoint commit 96014bf4274ad6696ed41d94be3d886055af1df8 Author: fathia Date: Mon Jun 10 09:43:17 2024 +0000 fix: blank screen with get reference molarity val commit 97d3bdcc5c086010cf813aff530d0e5a55c2ae19 Author: fathia Date: Wed Jun 5 14:23:26 2024 +0000 fix: fix display svg image before saving sample commit 2a374afe8be44dc816fc4b41f409069ad66a94b0 Author: fathia Date: Wed Jun 5 12:21:07 2024 +0000 feat: mixture samples in reactions scheme commit 085c451c7f5a58ddc0de703fbe6bf34c28ea838e Author: fathia Date: Mon Jun 3 14:19:39 2024 +0000 refactor: enable chem identifiers & correct sample types dropdown commit 7cbdba8d1ffc42ae9cdfd584f256939f046a6990 Author: fathia Date: Mon Jun 3 14:13:38 2024 +0000 refactor: remove add component sample button commit d2c14cd470b811106d897e7d53d37b521dc51794 Author: fathia Date: Mon Jun 3 08:43:57 2024 +0000 feat: calculate total MW for mixtures & use in reaction table commit 2b29fab9ef52a421b8d2167e19926c08cbdc1b46 Author: fathia Date: Wed May 29 07:29:23 2024 +0000 chore: remove unused files commit c8edca711776860f8e66744a02ad4c90a6daa993 Author: fathia Date: Wed May 29 07:27:22 2024 +0000 fix rebase error commit d51c61d25a86981644264b03a32a8b200a2fd0fd Author: fathia Date: Wed May 29 06:37:43 2024 +0000 fix: fix set default reference component commit 7365ef74b4a04cb6fc91163b736c33fce205679d Author: fathia Date: Fri May 24 13:52:29 2024 +0000 feat: option to merge components commit e15e3a567733cc1cfcddc689afdfdea906134904 Author: fathia Date: Thu May 16 14:29:07 2024 +0000 fix: fix create sample for mixture & store molfile commit 489b74b656b4367af2e57ed55dd820b8fdac31a3 Author: fathia Date: Thu May 16 13:46:50 2024 +0000 feat: allow users to adjust comp. ratios commit 5971234830f4f39beba1f68e78579fc6d2645156 Author: fathia Date: Wed May 15 14:19:54 2024 +0000 refactor: rename labels and fix alignments in UI commit 886988a07e760efa31a48cfb894b16469ec7b636 Author: fathia Date: Mon May 13 09:24:26 2024 +0000 fix: move component between tables commit 75afa87d1b8e13b9713adeaa0f9794026de04e91 Author: fathia Date: Fri May 10 13:56:21 2024 +0000 feat: two tables for mixture components commit b52399953b3003de743001b7b526b5767f3d6d15 Author: fathia Date: Tue May 7 12:56:42 2024 +0000 feat: add component label/name text field commit 047300ddea2a5a7c2fb83852c00b277db4c20e65 Author: fathia Date: Tue May 7 07:36:49 2024 +0000 refactor: change class name to avoid conflicts with react component commit 1d310e365800f26728790cdbeb18884d9a8bb514 Author: fathia Date: Mon May 6 15:00:11 2024 +0000 refactor: handle components in sample subclass commit 568f0bfb3439aff7d4f54db0b37f5b8a2fa68470 Author: fathia Date: Fri May 3 14:17:06 2024 +0000 refactor: separate reaction materials & components commit 9adb2c267007364528c92aa71e8322788e4c55eb Author: Fathia Idiris Date: Tue Apr 30 17:11:17 2024 +0200 feat: switch between set amount & set conc in comp table commit d9fef4763bc7bed66a70d290f2c0a964bcc29210 Author: Fathia Idiris Date: Tue Apr 30 14:48:30 2024 +0200 refactor: stricter validation in save/update components endpoint commit e607ecc3a178078b502771433507ef96d5c55492 Author: Fathia Idiris Date: Fri Apr 26 16:13:35 2024 +0200 feat: swap positions of comps in table commit 5f7c482ada9a5751fec214780c147998a409a048 Author: Fathia Idiris Date: Thu Apr 25 17:18:21 2024 +0200 feat: drag n drop mixture into another mixture commit eee87e81194d9722be13d310dbe2ee4e34762f0e Author: fathia Date: Wed Apr 24 13:59:14 2024 +0000 chore: remove old migrations commit 163f7f18e5a62c0b7863e724dbf22c3d7578e206 Author: fathia Date: Wed Apr 24 13:39:04 2024 +0000 feat: fetch and update components commit 5cac26ce91b93651b42c7deaa8f9eb6bf634b976 Author: fathia Date: Fri Apr 19 14:30:36 2024 +0000 refactor: remove redundant code & change names commit 2af79558359cc03d4d4c1c413ec42380a493d9f2 Author: fathia Date: Thu Apr 18 14:23:44 2024 +0000 feat: add components fetcher and api commit 5e2e7ca5d24a9a2b433ab78c56c46ae45da808ff Author: fathia Date: Thu Apr 18 09:16:51 2024 +0000 refactor: store components in a separate table commit f88ab269c4292fb70fac5c48776ea030190b53da Author: Fathia Idiris Date: Tue Apr 16 15:39:20 2024 +0200 feat: update smiles when a new comp is added via dragndrop commit d244a6108c21b0ed5267b7c738da0685b2ef45f4 Author: fathia Date: Mon Apr 8 14:22:29 2024 +0000 wip: split image by SMILES and store ket file commit 532335ce1a94f1ea020367777cf4bcd347a85931 Author: Fathia Idiris Date: Tue Apr 2 15:22:28 2024 +0200 fix: update comp volumes when total vol is updated & fix solv ratio commit 85fbeca7871d8929583d7ae4bf8fc20f057aef97 Author: Fathia Idiris Date: Tue Apr 2 10:48:04 2024 +0200 fix: store stock molarity of each component commit 81e1065b078bc5addd3bcb501472acbda8103a1e Author: Fathia Idiris Date: Thu Mar 28 16:21:49 2024 +0100 fix: component ratio calculation commit 8de85e77457a69d2898f7bcac96b3cbea69b5b71 Author: Fathia Idiris Date: Thu Mar 28 12:31:31 2024 +0100 Feat: calculate amount of component required from target conc & volume commit 2f5ed9badbf414a05817810ec8f05eae73aeae4d Author: Fathia Idiris Date: Thu Mar 28 11:53:49 2024 +0100 feat: add solvent volume commit 2629781b1cf12ef1788f93a4c84f2f623ad6622c Author: Fathia Idiris Date: Fri Mar 22 16:49:40 2024 +0100 fix: fix update mixture samples and re-enable inventory label commit 99100c70b7ceafe9420011301530b1289c1ba01a Author: Fathia Idiris Date: Thu Mar 21 17:46:09 2024 +0100 feat: reconstruct RXN when the components list is updated commit 6985bb75571e6082fb20c8b72bde6eb856c49e74 Author: fathia Date: Tue Mar 19 15:29:17 2024 +0000 WIP: new sample from img instead of subsample commit e79a71df063c8e58cdd60bad2db3a2cd0a264f78 Author: fathia Date: Mon Mar 18 14:31:08 2024 +0000 refactor: splitmolfile and combine svg fetch calls commit 6eb1227a165bb9373e7fd55912efa96df7ddb444 Author: fathia Date: Mon Mar 18 10:43:04 2024 +0000 fix: fix missing C-bonds and order of fetch calls commit f27bb28dac8399fce9881ad8df8d489a3229bd83 Author: fathia Date: Thu Mar 14 15:19:03 2024 +0000 WIP: render combined sample svg w/o reaction arrow commit 4b10f2a9143e71d0116694b4f8c2afefb8055eb5 Author: fathia Date: Thu Mar 14 10:11:34 2024 +0000 split Ketcher 2 rxn file into subsamples commit 2b38280dd15d555c92607868bf08a50df9742fbe Author: fathia Date: Tue Mar 12 15:01:21 2024 +0000 wip: split rxn file into mixture subsamples commit d52d94f1a11a7331937a2a3183e0fa00f2c0036d Author: fathia Date: Thu Mar 7 13:45:29 2024 +0000 refactor: separate show reaction & show mixture material commit c4dae183f88f7aabbee9c7cecc7bff85cd2d319c Author: fathia Date: Wed Mar 6 13:22:28 2024 +0000 fix: fix url when clicking on a mixture component commit 5bd89b94c130da8c4cdacab81311f96e8ed9af40 Author: fathia Date: Tue Mar 5 14:30:54 2024 +0000 refactor: Hide unnecessary sections, improve table alignment, display parent sample short label commit d663196c90f1f67147ea7e23228c95f36ac9b39f Author: fathia Date: Mon Mar 4 14:40:51 2024 +0000 hide subsamples in elements list & show stock sample on click material commit 2265fca3d08e9ea507c91630bca3d49c410e8656 Author: fathia Date: Mon Mar 4 12:26:41 2024 +0000 refactor: add sample to mixture via add sample btn commit 38fc91a484fb370b5f4b5bc8f56128e1155543bd Author: fathia Date: Fri Mar 1 13:33:31 2024 +0000 fix: update sample endpoint commit d8f9ee6db6be5b064c0d5bddf8fd0b044561ba58 Author: fathia Date: Fri Mar 1 10:02:05 2024 +0000 fix: reopen mixture sample after saving commit f1c32a1016425a0c4911879509a1a324df3b60ba Author: fathia Date: Thu Feb 29 08:29:52 2024 +0000 fix: fix create mixture comp subsamples commit 733173c99f0fa1031deb35666400bbb0d77d7ae1 Author: fathia Date: Wed Feb 28 10:08:03 2024 +0000 fix: fix save stock & add sample type name attr. commit b1e36c4b11d5913156640efad1ae3a69852b76c7 Author: fathia Date: Tue Feb 27 09:39:20 2024 +0000 fix: create button enabled for mixtures with comps commit fcd184d5895f764424c1ae4a584bde031abca299 Author: fathia Date: Tue Feb 27 09:29:52 2024 +0000 fix: new comp short label same as mixture label commit a86f9ce4507f51f8c12d50188f5927195982f05b Author: Fathia Idiris Date: Mon Feb 26 17:01:26 2024 +0100 do not render structure editor & cas fast input for mixtures commit f17169f67381a39b607c68b671fb7a43217f927b Author: Fathia Idiris Date: Mon Feb 26 14:56:59 2024 +0100 fix: disable unnecessary 'dropmaterial' commit c9ab74b6cff415163b3c956d950677839cedf5c9 Author: fathia Date: Fri Feb 23 13:12:34 2024 +0000 fix: add mixture component via add sample button commit c8e2598f4304317ea696069b4193e8195c4497be Author: fathia Date: Fri Feb 23 08:31:24 2024 +0000 fix: remove molarity and density tooltip commit 101c04e5164384bcbdd43a24e9a9ae7d3d66f799 Author: fathia Date: Thu Feb 22 15:01:51 2024 +0000 feat: component table calculations commit 39fd089fd8b154c339e4079aedaba94c0bab896c Author: fathia Date: Wed Feb 21 15:16:04 2024 +0000 fix: drag and drop samples in mixture comps table commit 8bf29bcbda91cb9a63ac5557892c0d22d93f02f4 Author: fathia Date: Wed Feb 21 10:32:17 2024 +0000 handle sample type changes commit 87769e6d75ac1c36033fcd905ce87a6dbc8e5b59 Author: Fathia Idiris Date: Mon Feb 19 16:42:49 2024 +0100 feat: add mixture components table in UI commit 22d229c3c6926efd32b4e259042cf7c94c641b2d Author: Fathia Idiris Date: Mon Feb 19 11:17:09 2024 +0100 feat: add select sample type dropdown commit 2847ecb1c141bca158e276d233a894399006d82e Author: fathia Date: Thu Feb 15 14:40:49 2024 +0000 save mixture comps as subsample of stock solution commit c07885df881c3e68f9f98d7579a967374b81d70e Author: fathia Date: Wed Feb 14 09:41:10 2024 +0000 update sample types after create sample commit 34ffaeab5cb8c0d9594ac705aaa3924be5aa9301 Author: fathia Date: Tue Feb 13 09:48:37 2024 +0000 remove outdated migration files commit 05599d9cc2ed627ed95fa6135446e732ad130f0c Author: fathia Date: Mon Jan 29 15:29:19 2024 +0000 refactor: refactor sample type models commit 93825c6690f40cc9c4135535df84fe8a50623426 Author: fathia Date: Tue Jan 23 14:32:05 2024 +0000 create sample mixture and sample components commit 47bf220a0a561d2731f555657c96bdfcf9220815 Author: fathia Date: Tue Jan 23 10:04:59 2024 +0000 refactor: mixtures and components model commit 6afa120f8b03ca3d3396a1bba517a8c5b12c895d Author: fathia Date: Mon Jan 22 14:10:58 2024 +0000 create micromolecule by default & move fields commit 77832415ed42c82081e87abe6a2a1817a3f102e9 Author: fathia Date: Fri Jan 19 10:03:13 2024 +0000 refactor: refactor mixture associations commit 06766c927f70696fda6a7a4c3fabebc98c4d5a00 Author: fathia Date: Thu Jan 18 15:23:10 2024 +0000 split sample model into micromolecule and mixture --- app/api/api.rb | 1 + app/api/chemotion/component_api.rb | 75 +++ app/api/chemotion/sample_api.rb | 32 +- app/api/entities/sample_entity.rb | 2 + app/models/component.rb | 5 + app/models/micromolecule.rb | 5 + app/models/sample.rb | 7 +- .../details/NumeralInputWithUnitsCompo.js | 26 +- .../details/reactions/schemeTab/Material.js | 11 +- .../schemeTab/ReactionDetailsScheme.js | 37 +- .../elements/details/samples/SampleDetails.js | 49 +- .../samples/propertiesTab/SampleComponent.js | 619 ++++++++++++++++++ .../propertiesTab/SampleComponentsGroup.js | 187 ++++++ .../propertiesTab/SampleDetailsComponents.js | 397 +++++++++++ .../SampleDetailsComponentsDnd.js | 130 ++++ .../samples/propertiesTab/SampleForm.js | 361 ++++++---- .../propertiesTab/SampleSolventGroup.js | 33 +- app/packs/src/components/common/SampleName.js | 11 + .../staticDropdownOptions/options.js | 5 + .../structureEditor/StructureEditorSet.js | 2 + app/packs/src/fetchers/ComponentsFetcher.js | 42 ++ app/packs/src/models/Component.js | 274 ++++++++ app/packs/src/models/Sample.js | 331 +++++++++- .../src/stores/alt/actions/ElementActions.js | 20 +- .../src/stores/alt/stores/ElementStore.js | 89 ++- app/usecases/reactions/update_materials.rb | 19 + .../20240118130824_create_micromolecules.rb | 8 + ...119122000_add_molfile_to_micromolecules.rb | 7 + ...6145309_add_micromolecule_id_to_samples.rb | 5 + .../20240418084207_create_components.rb | 12 + ...240418084814_add_sample_type_to_samples.rb | 5 + ...603073714_add_sample_details_to_samples.rb | 5 + db/schema.rb | 220 ++++--- .../propertiesTab/SampleSolventGroup.spec.js | 4 + 34 files changed, 2780 insertions(+), 256 deletions(-) create mode 100644 app/api/chemotion/component_api.rb create mode 100644 app/models/component.rb create mode 100644 app/models/micromolecule.rb create mode 100644 app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleComponent.js create mode 100644 app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleComponentsGroup.js create mode 100644 app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponents.js create mode 100644 app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponentsDnd.js create mode 100644 app/packs/src/fetchers/ComponentsFetcher.js create mode 100644 app/packs/src/models/Component.js create mode 100644 db/migrate/20240118130824_create_micromolecules.rb create mode 100644 db/migrate/20240119122000_add_molfile_to_micromolecules.rb create mode 100644 db/migrate/20240126145309_add_micromolecule_id_to_samples.rb create mode 100644 db/migrate/20240418084207_create_components.rb create mode 100644 db/migrate/20240418084814_add_sample_type_to_samples.rb create mode 100644 db/migrate/20240603073714_add_sample_details_to_samples.rb diff --git a/app/api/api.rb b/app/api/api.rb index 2bbf016ce8..2e07de457b 100644 --- a/app/api/api.rb +++ b/app/api/api.rb @@ -206,6 +206,7 @@ def to_json_camel_case(val) mount Chemotion::InventoryAPI mount Chemotion::AdminDeviceAPI mount Chemotion::AdminDeviceMetadataAPI + mount Chemotion::ComponentAPI if Rails.env.development? add_swagger_documentation(info: { diff --git a/app/api/chemotion/component_api.rb b/app/api/chemotion/component_api.rb new file mode 100644 index 0000000000..b0e03d835e --- /dev/null +++ b/app/api/chemotion/component_api.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Chemotion + class ComponentAPI < Grape::API + resource :components do + desc 'Return components by sample_id' + params do + requires :sample_id, type: Integer, desc: 'sample id' + end + + route_param :sample_id do + get do + components = Component.where(sample_id: params[:sample_id]) + components_with_molecule_data = components.map do |component| + molecule_id = component.component_properties['molecule_id'] + molecule = Molecule.find_by(id: molecule_id) + component.component_properties['molecule'] = molecule + component + end + present components_with_molecule_data + end + end + + desc 'Save or update components for a given sample' + params do + requires :sample_id, type: Integer, desc: 'sample id' + requires :components, type: Array, desc: 'components' do + requires :id, types: [Integer, String], desc: 'Component ID' + optional :name, type: String, desc: 'Component name' + optional :position, type: Integer, desc: 'Component position in the table' + requires :component_properties, type: Hash, desc: 'Component properties' do + optional :amount_mol, type: Float, desc: 'Component moles' + optional :amount_l, type: Float, desc: 'Component volume' + optional :amount_g, type: Float, desc: 'Component mass' + optional :density, type: Float, desc: 'Density in g/ml' + optional :molarity_unit, type: String, desc: 'Molarity unit' + optional :molarity_value, type: Float, desc: 'Molarity value' + optional :starting_molarity_value, type: Float, desc: 'Starting molarity value' + optional :starting_molarity_unit, type: String, desc: 'Starting molarity unit' + requires :molecule_id, type: Integer, desc: 'Molecule ID' + optional :equivalent, types: [Float, String], desc: 'Equivalent' + optional :parent_id, type: Integer, desc: 'Parent ID' + optional :material_group, type: String, desc: 'type of component e.g. liquid' + optional :reference, type: Boolean, desc: 'reference comp. for ratio calculations' + optional :purity, type: Float, desc: 'Component purity' + end + end + end + + put do + sample_id = params[:sample_id] + components_params = params[:components] + + components_params.each do |component_params| + molecule_id = component_params[:component_properties][:molecule_id] + + component = Component.where("sample_id = ? AND CAST(component_properties ->> 'molecule_id' AS INTEGER) = ?", + sample_id, molecule_id) + .first_or_initialize(sample_id: sample_id) + + component.update( + name: component_params[:name], + position: component_params[:position], + component_properties: component_params[:component_properties], + ) + end + # Delete components + molecule_ids_to_keep = components_params.map { |cp| cp[:component_properties][:molecule_id] }.compact + Component.where(sample_id: sample_id) + .where.not("CAST(component_properties ->> 'molecule_id' AS INTEGER) IN (?)", molecule_ids_to_keep) + &.destroy_all + end + end + end +end diff --git a/app/api/chemotion/sample_api.rb b/app/api/chemotion/sample_api.rb index 2ab411638e..496594b8b8 100644 --- a/app/api/chemotion/sample_api.rb +++ b/app/api/chemotion/sample_api.rb @@ -347,6 +347,8 @@ class SampleAPI < Grape::API optional :molecular_mass, type: Float optional :sum_formula, type: String # use :root_container_params + optional :sample_type, type: String, default: 'Micromolecule' + optional :sample_details, type: Hash, desc: 'extra params for mixtures or polymers' end route_param :id do @@ -393,7 +395,14 @@ class SampleAPI < Grape::API attributes.delete(:melting_point_lowerbound) attributes.delete(:melting_point_upperbound) + micro_att = { + name: params[:name], + molfile: params[:molfile], + stereo: params[:stereo], + } + @sample.update!(attributes) + @sample.micromolecule&.update(micro_att) @sample.save_segments(segments: params[:segments], current_user_id: current_user.id) # save to profile @@ -453,6 +462,8 @@ class SampleAPI < Grape::API optional :inventory_sample, type: Boolean, default: false optional :molecular_mass, type: Float optional :sum_formula, type: String + optional :sample_type, type: String, default: 'Micromolecule' + optional :sample_details, type: Hash, desc: 'extra params for mixtures or polymers' end post do molecule_id = if params[:decoupled] && params[:molfile].blank? @@ -475,7 +486,6 @@ class SampleAPI < Grape::API dry_solvent: params[:dry_solvent], solvent: params[:solvent], location: params[:location], - molfile: params[:molfile], molecule_id: molecule_id, sample_svg_file: params[:sample_svg_file], is_top_secret: params[:is_top_secret], @@ -484,12 +494,20 @@ class SampleAPI < Grape::API elemental_compositions: params[:elemental_compositions], created_by: current_user.id, xref: params[:xref], - stereo: params[:stereo], molecule_name_id: params[:molecule_name_id], decoupled: params[:decoupled], inventory_sample: params[:inventory_sample], molecular_mass: params[:molecular_mass], sum_formula: params[:sum_formula], + sample_type: params[:sample_type], + molfile: params[:molfile], + stereo: params[:stereo], + sample_details: params[:sample_details], + } + micro_att = { + name: params[:name], + molfile: params[:molfile], + stereo: params[:stereo], } boiling_point_lowerbound = (params['boiling_point_lowerbound'].presence || -Float::INFINITY) @@ -543,9 +561,15 @@ class SampleAPI < Grape::API sample.collections << all_coll end + case params[:sample_type] + when 'Micromolecule' + micromolecule = Micromolecule.new(micro_att) + micromolecule.samples << sample + micromolecule.save! + end + sample.container = update_datamodel(params[:container]) sample.save! - sample.save_segments(segments: params[:segments], current_user_id: current_user.id) # save to profile @@ -566,7 +590,9 @@ class SampleAPI < Grape::API delete do sample = Sample.find(params[:id]) + micromolecule = Micromolecule.find_by(id: sample.micromolecule_id) sample.destroy + micromolecule&.destroy end end end diff --git a/app/api/entities/sample_entity.rb b/app/api/entities/sample_entity.rb index 1e07bb0b5d..0f1c727a33 100644 --- a/app/api/entities/sample_entity.rb +++ b/app/api/entities/sample_entity.rb @@ -71,6 +71,8 @@ class SampleEntity < ApplicationEntity expose! :target_amount_value, unless: :displayed_in_list expose! :user_labels expose! :xref + expose! :sample_type + expose! :sample_details end # rubocop:enable Layout/LineLength, Layout/ExtraSpacing, Metrics/BlockLength diff --git a/app/models/component.rb b/app/models/component.rb new file mode 100644 index 0000000000..fa1145edee --- /dev/null +++ b/app/models/component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Component < ApplicationRecord + belongs_to :sample +end diff --git a/app/models/micromolecule.rb b/app/models/micromolecule.rb new file mode 100644 index 0000000000..fe7b1b2f84 --- /dev/null +++ b/app/models/micromolecule.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Micromolecule < ApplicationRecord + has_many :samples +end diff --git a/app/models/sample.rb b/app/models/sample.rb index a0b71ced1d..42a1873e38 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -160,7 +160,6 @@ class Sample < ApplicationRecord Sample.where(id: samples.map(&:id)) } - before_save :auto_set_molfile_to_molecules_molfile before_save :find_or_create_molecule_based_on_inchikey before_save :update_molecule_name @@ -201,6 +200,9 @@ class Sample < ApplicationRecord has_many :private_notes, as: :noteable, dependent: :destroy has_many :comments, as: :commentable, dependent: :destroy + belongs_to :micromolecule, optional: true + has_many :components, dependent: :destroy + belongs_to :fingerprint, optional: true belongs_to :user, optional: true belongs_to :molecule_name, optional: true @@ -233,6 +235,7 @@ class Sample < ApplicationRecord delegate :computed_props, to: :molecule, prefix: true delegate :inchikey, to: :molecule, prefix: true, allow_nil: true + delegate :molfile, :molfile_version, :stereo, to: :micromolecule, prefix: true, allow_nil: true attr_writer :skip_reaction_svg_update @@ -583,7 +586,7 @@ def set_elem_composition_data d_type, d_values, loading = nil end def check_molfile_polymer_section - return if decoupled + return if decoupled || sample_type == 'Mixture' return unless self.molfile.include? 'R#' lines = self.molfile.lines diff --git a/app/packs/src/apps/mydb/elements/details/NumeralInputWithUnitsCompo.js b/app/packs/src/apps/mydb/elements/details/NumeralInputWithUnitsCompo.js index a147a5998c..317ff8fd12 100644 --- a/app/packs/src/apps/mydb/elements/details/NumeralInputWithUnitsCompo.js +++ b/app/packs/src/apps/mydb/elements/details/NumeralInputWithUnitsCompo.js @@ -1,6 +1,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { FormControl, ControlLabel, InputGroup, Button } from 'react-bootstrap'; +import { + FormControl, ControlLabel, InputGroup, Button, OverlayTrigger, Tooltip +} from 'react-bootstrap'; import { metPreConv, metPrefSymbols } from 'src/utilities/metricPrefix'; export default class NumeralInputWithUnitsCompo extends Component { @@ -126,7 +128,7 @@ export default class NumeralInputWithUnitsCompo extends Component { render() { const { - bsSize, bsStyle, disabled, label, unit, name + bsSize, bsStyle, disabled, label, unit, name, showInfoTooltipTotalVol } = this.props; const { showString, value, metricPrefix, @@ -164,6 +166,20 @@ export default class NumeralInputWithUnitsCompo extends Component { return (
{labelWrap} + {showInfoTooltipTotalVol && ( + + It is only a value given manually, i.e. volume by definition - not (re)calculated + + )} + > + + + + + )} - + { + const sampleComponents = components.map(component => { + const { component_properties, ...rest } = component; + const sampleData = { + ...rest, + ...component_properties + }; + return new Component(sampleData); + }); + await splitSample.initialComponents(sampleComponents); + const comp = sampleComponents.find(component => component.amount_mol > 0 && component.molarity_value > 0); + if (comp) { + splitSample.target_amount_value = comp.amount_mol / comp.molarity_value; + splitSample.target_amount_unit = 'l'; + } + reaction.addMaterialAt(splitSample, null, tagMaterial, tagGroup); + this.onReactionChange(reaction, { schemaChanged: true }); + }) + .catch((errorMessage) => { + console.log(errorMessage); + }); + } else { + this.insertSolventExtLabel(splitSample, tagGroup, extLabel); + reaction.addMaterialAt(splitSample, null, tagMaterial, tagGroup); + this.onReactionChange(reaction, { schemaChanged: true }); + } } insertSolventExtLabel(splitSample, materialGroup, external_label) { diff --git a/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js b/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js index 54f9591292..88a7e16701 100644 --- a/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js +++ b/app/packs/src/apps/mydb/elements/details/samples/SampleDetails.js @@ -80,7 +80,7 @@ import { commentActivation } from 'src/utilities/CommentHelper'; const MWPrecision = 6; const decoupleCheck = (sample) => { - if (!sample.decoupled && sample.molecule && sample.molecule.id === '_none_') { + if (!sample.decoupled && sample.molecule && sample.molecule.id === '_none_' && !sample.sample_type == 'Mixture') { NotificationActions.add({ title: 'Error on Sample creation', message: 'The molecule structure is required!', level: 'error', position: 'tc' }); @@ -164,6 +164,7 @@ export default class SampleDetails extends React.Component { this.handleStructureEditorSave = this.handleStructureEditorSave.bind(this); this.handleStructureEditorCancel = this.handleStructureEditorCancel.bind(this); + this.splitSmiles = this.splitSmiles.bind(this); } componentDidMount() { @@ -325,6 +326,7 @@ export default class SampleDetails extends React.Component { const fetchMolecule = (fetchFunction) => { fetchFunction() .then(fetchSuccess).catch(fetchError).finally(() => { + this.splitSmiles(editor, svgFile) this.hideStructureEditor(); }); }; @@ -336,7 +338,7 @@ export default class SampleDetails extends React.Component { } else { fetchMolecule(() => MoleculesFetcher.fetchBySmi(smiles, svgFile, molfile, editor)); } - } + } handleStructureEditorCancel() { this.hideStructureEditor(); @@ -477,6 +479,7 @@ export default class SampleDetails extends React.Component { molfile={molfile} hasParent={hasParent} hasChildren={hasChildren} + sample={sample} /> ); } @@ -685,7 +688,9 @@ export default class SampleDetails extends React.Component { saveBtn(sample, closeView = false) { let submitLabel = (sample && sample.isNew) ? 'Create' : 'Save'; - const isDisabled = !sample.can_update; + const hasComponents = sample.sample_type !== 'Mixture' + || (sample.components && sample.components.length > 0); + const isDisabled = !sample.can_update || !hasComponents; if (closeView) submitLabel += ' and close'; return ( @@ -755,7 +760,7 @@ export default class SampleDetails extends React.Component { elementalPropertiesItem(sample) { // avoid empty ListGroupItem - if (!sample.molecule_formula) { + if (!sample.molecule_formula || sample.sample_type === 'Mixture') { return false; } @@ -964,7 +969,9 @@ export default class SampleDetails extends React.Component { const timesTag = ( ); - const sampleUpdateCondition = !this.sampleIsValid() || !sample.can_update; + const hasComponents = sample.sample_type !== 'Mixture' + || (sample.components && sample.components.length > 0); + const sampleUpdateCondition = !this.sampleIsValid() || !sample.can_update || !hasComponents; const elementToSave = activeTab === 'inventory' ? 'Chemical' : 'Sample'; const saveAndClose = ( @@ -1038,6 +1045,8 @@ export default class SampleDetails extends React.Component { ) : null; + const isMixture = sample.sample_type === 'Mixture'; + return (
@@ -1055,7 +1064,7 @@ export default class SampleDetails extends React.Component { - {sample.isNew + {sample.isNew && !isMixture ? : null}
@@ -1105,6 +1114,7 @@ export default class SampleDetails extends React.Component { } sampleInfo(sample) { + const isMixture = sample.sample_type === 'Mixture'; const style = { height: 'auto', marginBottom: '20px' }; let pubchemLcss = (sample.pubchem_tag && sample.pubchem_tag.pubchem_lcss && sample.pubchem_tag.pubchem_lcss.Record) || null; @@ -1128,7 +1138,7 @@ export default class SampleDetails extends React.Component {

{this.sampleAverageMW(sample)}
{this.sampleExactMW(sample)}
- {sample.isNew ? null :
{this.moleculeCas()}
} + {sample.isNew || isMixture ? null :
{this.moleculeCas()}
} {lcssSign} @@ -1411,12 +1421,20 @@ export default class SampleDetails extends React.Component { } sampleAverageMW(sample) { - const mw = sample.molecule_molecular_weight; + let mw; + + if (sample.sample_type === 'Mixture' && sample.sample_details) { + mw = sample.total_molecular_weight; + } else { + mw = sample.molecule_molecular_weight; + } + if (mw) return ; return ''; } sampleExactMW(sample) { + if (sample.sample_type === 'Mixture' && sample.sample_details) { return } const mw = sample.molecule_exact_molecular_weight; if (mw) return ; return ''; @@ -1445,6 +1463,19 @@ export default class SampleDetails extends React.Component { }); } + splitSmiles(editor, svgFile) { + const { sample } = this.state; + if (sample.sample_type !== 'Mixture' || !sample.molecule_cano_smiles || sample.molecule_cano_smiles === '') { return } + + const mixtureSmiles = sample.molecule_cano_smiles.split('.') + if (mixtureSmiles) { + sample.splitSmilesToMolecule(mixtureSmiles, editor) + .then(() => { + this.setState({ sample }); + }); + } + } + toggleInchi() { const { showInchikey } = this.state; this.setState({ showInchikey: !showInchikey }); @@ -1613,6 +1644,8 @@ export default class SampleDetails extends React.Component { const activeTab = (this.state.activeTab !== 0 && stb.indexOf(this.state.activeTab) > -1 && this.state.activeTab) || visible.get(0); + const isMixture = sample.sample_type === 'Mixture'; + return ( ({ + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), +}); + +const matTagCollect = (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), +}); + +class SampleComponent extends Component { + constructor(props) { + super(props); + + this.handleAmountChange = this.handleAmountChange.bind(this); + this.handleMetricsChange = this.handleMetricsChange.bind(this); + this.handleDensityChange = this.handleDensityChange.bind(this); + this.handlePurityChange = this.handlePurityChange.bind(this); + this.handleNameChange = this.handleNameChange.bind(this); + this.handleRatioChange = this.handleRatioChange.bind(this); + this.handleReferenceChange = this.handleReferenceChange.bind(this); + } + + handleAmountChange(e, value, concType, lockColumn) { + if (e.value === value) return; + const { materialGroup } = this.props; + + if (this.props.onChange && e) { + const event = { + amount: e, + type: 'amountChanged', + materialGroup, + sampleID: this.componentId(), + concType, + lockColumn, + }; + this.props.onChange(event); + } + } + + handleDensityChange(e, value, lockColumn) { + if (e.value === value) return; + + if (this.props.onChange && e) { + const event = { + amount: e, + type: 'densityChanged', + materialGroup: this.props.materialGroup, + sampleID: this.componentId(), + lockColumn, + }; + this.props.onChange(event); + } + } + + handlePurityChange(e, value) { + if (e.value === value) return; + + if (this.props.onChange && e) { + const event = { + amount: e, + type: 'purityChanged', + materialGroup: this.props.materialGroup, + sampleID: this.componentId(), + }; + this.props.onChange(event); + } + } + + handleMetricsChange(e) { + if (this.props.onChange && e) { + const event = { + metricUnit: e.metricUnit, + metricPrefix: e.metricPrefix, + type: 'MetricsChanged', + materialGroup: this.props.materialGroup, + sampleID: this.componentId(), + + }; + this.props.onChange(event); + } + } + + componentId() { + return this.component().id; + } + + component() { + return this.props.material; + } + + materialNameWithIupac(material) { + let moleculeIupacName = ''; + const iupacStyle = { + display: 'block', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '100%' + }; + + moleculeIupacName = material.molecule_iupac_name; + + if (moleculeIupacName === '' || !moleculeIupacName) { + moleculeIupacName = material.molecule.sum_formular; + } + + return ( +
+ + {this.svgPreview(material, moleculeIupacName)} + +
+ ); + } + + nameInput(material) { + return ( + + { this.handleNameChange(e, material.name); }} + disabled={!permitOn(this.props.sample)} + /> + + ); + } + + handleNameChange(e, value) { + if (e.value === value) return; + + if (this.props.onChange && e) { + const event = { + newName: e.target.value, + type: 'nameChanged', + sampleID: this.componentId(), + + }; + this.props.onChange(event); + } + } + + handleRatioChange(e, value) { + if (e.value === value) return; + + const { materialGroup } = this.props; + + if (this.props.onChange && e) { + const event = { + newRatio: e.value, + type: 'ratioChanged', + sampleID: this.componentId(), + materialGroup, + }; + this.props.onChange(event); + } + } + + handleReferenceChange(e) { + const { value } = e.target; + if (this.props.onChange) { + const event = { + type: 'referenceChanged', + materialGroup: this.props.materialGroup, + sampleID: this.componentId(), + value + }; + this.props.onChange(event); + } + } + + generateMolecularWeightTooltipText(sample) { + const molecularWeight = sample.decoupled + ? (sample.molecular_mass) : (sample.molecule && sample.molecule.molecular_weight); + return `molar mass: ${molecularWeight} g/mol`; + } + + materialVolume(material) { + if (material.contains_residues) { return notApplicableInput(); } + + const { + sample, enableComponentLabel, enableComponentPurity + } = this.props; + const metricPrefixes = ['m', 'n', 'u']; + const metric = (material.metrics && material.metrics.length > 2 && metricPrefixes.indexOf(material.metrics[1]) > -1) ? material.metrics[1] : 'm'; + + return ( + + this.handleAmountChange(e, material.amount_l)} + onMetricsChange={this.handleMetricsChange} + bsStyle={material.amount_unit === 'l' ? 'success' : 'default'} + /> + + ); + } + + componentMass(material, metric, metricPrefixes, massBsStyle) { + return ( + {this.generateMolecularWeightTooltipText(material)} + } + > +
+ this.handleAmountChange(e, material.amount_g)} + onMetricsChange={this.handleMetricsChange} + bsStyle={material.error_mass ? 'error' : massBsStyle} + name="molecular-weight" + /> +
+
+ ); + } + + componentMol(material, metricMol, metricPrefixesMol) { + const { + sample, enableComponentLabel, enableComponentPurity + } = this.props; + + return ( + + this.handleAmountChange(e, material.amount_mol)} + onMetricsChange={this.handleMetricsChange} + bsStyle={material.amount_unit === 'mol' ? 'success' : 'default'} + /> + + ); + } + + componentConc(material, metricMolConc, metricPrefixesMolConc) { + const { sample } = this.props; + return ( + + this.handleAmountChange(e, material.concn, 'targetConc')} + onMetricsChange={this.handleMetricsChange} + /> + + ); + } + + componentStartingConc(material, metricMolConc, metricPrefixesMolConc) { + const { sample, lockAmountColumn } = this.props; + + return ( + + this.handleAmountChange(e, material.startingConc, 'startingConc', lockAmountColumn)} + onMetricsChange={this.handleMetricsChange} + /> + + ); + } + + materialRef(material) { + return ( + + this.handleReferenceChange(e)} + bsSize="xsmall" + style={{ margin: 0 }} + /> + + ); + } + + componentDensity(material) { + const { + sample, materialGroup, lockAmountColumn, lockAmountColumnSolids + } = this.props; + const lockColumn = materialGroup === 'liquid' ? lockAmountColumn : lockAmountColumnSolids; + + return ( + + this.handleDensityChange(e, material.density, lockColumn)} + /> + + ); + } + + mixtureComponent(props, style) { + const { + sample, material, deleteMaterial, connectDragSource, connectDropTarget, activeTab, + enableComponentLabel, enableComponentPurity + } = props; + const metricPrefixes = ['m', 'n', 'u']; + const metricPrefixesMol = ['m', 'n']; + const metricMol = (material.metrics && material.metrics.length > 2 && metricPrefixes.indexOf(material.metrics[2]) > -1) ? material.metrics[2] : 'm'; + const metricPrefixesMolConc = ['m', 'n']; + const metricMolConc = (material.metrics && material.metrics.length > 3 && metricPrefixes.indexOf(material.metrics[3]) > -1) ? material.metrics[3] : 'm'; + + return ( + + {compose(connectDragSource, connectDropTarget)( + + + , + { dropEffect: 'copy' } + )} + + + {this.materialNameWithIupac(material)} + + + + + + + {activeTab === 'concentration' && this.componentStartingConc(material, metricMolConc, metricPrefixesMolConc)} + {activeTab === 'density' && this.componentDensity(material)} + + {this.materialVolume(material)} + + {this.componentMol(material, metricMol, metricPrefixesMol)} + + + this.handleRatioChange(e, material.equivalent)} + /> + + + {this.materialRef(material)} + + {this.componentConc(material, metricMolConc, metricPrefixesMolConc)} + + { + enableComponentLabel && ( + + {this.nameInput(material)} + + ) + } + + { + enableComponentPurity && ( + + this.handlePurityChange(e, material.purity)} + /> + + ) + } + + ); + } + + solidComponent(props, style) { + const { + sample, material, deleteMaterial, connectDragSource, connectDropTarget + } = props; + const metricPrefixes = ['m', 'n', 'u']; + const metric = (material.metrics && material.metrics.length > 2 && metricPrefixes.indexOf(material.metrics[0]) > -1) ? material.metrics[0] : 'm'; + const metricPrefixesMol = ['m', 'n']; + const metricMol = (material.metrics && material.metrics.length > 2 && metricPrefixes.indexOf(material.metrics[2]) > -1) ? material.metrics[2] : 'm'; + const massBsStyle = material.amount_unit === 'g' ? 'success' : 'default'; + const metricPrefixesMolConc = ['m', 'n']; + const metricMolConc = (material.metrics && material.metrics.length > 3 && metricPrefixes.indexOf(material.metrics[3]) > -1) ? material.metrics[3] : 'm'; + + return ( + + {compose(connectDragSource, connectDropTarget)( + + + , + { dropEffect: 'copy' } + )} + + + {this.materialNameWithIupac(material)} + + + {this.materialRef(material)} + + + {this.componentMass(material, metric, metricPrefixes, massBsStyle)} + + + + {this.componentMol(material, metricMol, metricPrefixesMol)} + + {this.componentDensity(material, metricMol, metricPrefixesMol)} + {this.componentConc(material, metricMolConc, metricPrefixesMolConc)} + + + this.handlePurityChange(e, material.purity)} + /> + + + + this.handleRatioChange(e, material.equivalent)} + /> + + + + + + + ); + } + + svgPreview(material, moleculeIupacName) { + return ( + + ); + } + + render() { + const { + isDragging, canDrop, isOver, material + } = this.props; + const style = { padding: '0' }; + if (isDragging) { style.opacity = 0.3; } + if (canDrop) { + style.borderStyle = 'dashed'; + style.borderWidth = 2; + } + if (isOver) { + style.borderColor = '#337ab7'; + style.opacity = 0.6; + style.backgroundColor = '#337ab7'; + } + + if (material.material_group === 'liquid') { + return this.mixtureComponent(this.props, style); + } + return this.solidComponent(this.props, style); + } +} + +export default compose( + DragSource( + DragDropItemTypes.MATERIAL, + matSource, + matSrcCollect, + ), + DropTarget( + [DragDropItemTypes.SAMPLE, DragDropItemTypes.MOLECULE, DragDropItemTypes.MATERIAL], + matTarget, + matTagCollect, + ), +)(SampleComponent); + +SampleComponent.propTypes = { + sample: PropTypes.object.isRequired, + material: PropTypes.instanceOf(Sample).isRequired, + materialGroup: PropTypes.string.isRequired, + deleteMaterial: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + index: PropTypes.number, + isDragging: PropTypes.bool, + canDrop: PropTypes.bool, + isOver: PropTypes.bool, + lockAmountColumn: PropTypes.bool.isRequired, + lockAmountColumnSolids: PropTypes.bool.isRequired, + enableComponentLabel: PropTypes.bool.isRequired, + enableComponentPurity: PropTypes.bool.isRequired, +}; diff --git a/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleComponentsGroup.js b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleComponentsGroup.js new file mode 100644 index 0000000000..62c97c7563 --- /dev/null +++ b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleComponentsGroup.js @@ -0,0 +1,187 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Button, OverlayTrigger, Tooltip, Tab, Tabs, ControlLabel +} from 'react-bootstrap'; +import Sample from 'src/models/Sample'; +import Component from 'src/models/Component'; +import SampleComponent from 'src/apps/mydb/elements/details/samples/propertiesTab/SampleComponent'; + +const SampleComponentsGroup = ({ + materialGroup, deleteMixtureComponent, onChange, sample, + headIndex, dropSample, dropMaterial, lockAmountColumn, lockAmountColumnSolids, switchAmount, sampleComponents, + showModalWithMaterial, activeTab, handleTabSelect, enableComponentLabel, enableComponentPurity +}) => { + const contents = []; + if (sampleComponents && sampleComponents.length > 0) { + sampleComponents = sampleComponents.map((component) => { + if (!(component instanceof Component)) { + return new Component(component); + } + return component; + }); + let index = headIndex; + sampleComponents.forEach((sampleComponent) => { + index += 1; + contents.push(( + deleteMixtureComponent(sc, materialGroup)} + index={index} + dropMaterial={dropMaterial} + dropSample={dropSample} + lockAmountColumn={lockAmountColumn} + lockAmountColumnSolids={lockAmountColumnSolids} + showModalWithMaterial={showModalWithMaterial} + activeTab={activeTab} + handleTabSelect={handleTabSelect} + enableComponentLabel={enableComponentLabel} + enableComponentPurity={enableComponentPurity} + /> + )); + }); + } + + const headers = { + name: 'Label', + amount: 'Amount', + mass: 'Mass', + volume: 'Volume', + startingConc: 'Stock', + concn: 'Total Conc.', + eq: 'Ratio', + ref: 'Ref', + purity: 'Purity', + density: 'Density', + }; + + if (materialGroup === 'solid') { + headers.group = 'Solids'; + } else { + headers.group = 'Liquids'; + } + + const switchAmountTooltip = () => ( + + + Lock/unlock amounts + + + (mass/stock/density) + + + ); + + const SwitchAmountButton = (lockAmountColumn, switchAmount, materialGroup) => ( + + + + ); + + return ( +
+ + + + + + + + + + + + {enableComponentLabel && } + {enableComponentPurity && } + + + + + + )} + + {materialGroup === 'liquid' && ( + + )} + + {materialGroup === 'liquid' && ( + + )} + + + {materialGroup === 'solid' && } + + + + {enableComponentLabel && } + {enableComponentPurity && } + + + + {contents.map((item) => item)} + +
+ {headers.group} + {materialGroup === 'solid' && ( + + {SwitchAmountButton(lockAmountColumnSolids, switchAmount, materialGroup)} {headers.mass} + +
+ + + + + {SwitchAmountButton(lockAmountColumn, switchAmount, materialGroup)} +
+
+ {headers.volume} + {headers.amount}{headers.density}{headers.eq}{headers.ref} + {headers.concn} + + Total Conc. will only be calculated when we have a Total volume + + )} + > + + + + + {headers.name}{headers.purity}
+
+ ); +}; + +SampleComponentsGroup.propTypes = { + materialGroup: PropTypes.string.isRequired, + headIndex: PropTypes.number.isRequired, + deleteMixtureComponent: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + sample: PropTypes.instanceOf(Sample).isRequired, + dropSample: PropTypes.func.isRequired, + dropMaterial: PropTypes.func.isRequired, + switchAmount: PropTypes.func.isRequired, + lockAmountColumn: PropTypes.bool.isRequired, + lockAmountColumnSolids: PropTypes.bool.isRequired, + enableComponentLabel: PropTypes.bool.isRequired, + enableComponentPurity: PropTypes.bool.isRequired, +}; + +export default SampleComponentsGroup; diff --git a/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponents.js b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponents.js new file mode 100644 index 0000000000..0fd307f108 --- /dev/null +++ b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponents.js @@ -0,0 +1,397 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Sample from 'src/models/Sample'; +import Molecule from 'src/models/Molecule'; +import SampleDetailsComponentsDnd from 'src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponentsDnd'; // Import the appropriate Dnd component +import UIStore from 'src/stores/alt/stores/UIStore'; +import ComponentsFetcher from 'src/fetchers/ComponentsFetcher'; +import Component from 'src/models/Component'; +import { + ListGroup, ListGroupItem, Button, Modal +} from 'react-bootstrap'; + +export default class SampleDetailsComponents extends React.Component { + constructor(props) { + super(props); + + const { sample } = props; + this.state = { + sample, + showModal: false, + droppedMaterial: null, + activeTab: 'concentration', + lockAmountColumn: true, + lockAmountColumnSolids: false, + }; + + this.dropSample = this.dropSample.bind(this); + this.dropMaterial = this.dropMaterial.bind(this); + this.deleteMixtureComponent = this.deleteMixtureComponent.bind(this); + this.onChangeComponent = this.onChangeComponent.bind(this); + this.updatedSampleForAmountUnitChange = this.updatedSampleForAmountUnitChange.bind(this); + this.updatedSampleForMetricsChange = this.updatedSampleForMetricsChange.bind(this); + this.switchAmount = this.switchAmount.bind(this); + this.updateComponentName = this.updateComponentName.bind(this); + this.updateRatio = this.updateRatio.bind(this); + this.updateSampleForReferenceChanged = this.updateSampleForReferenceChanged.bind(this); + this.showModalWithMaterial = this.showModalWithMaterial.bind(this); + this.handleModalClose = this.handleModalClose.bind(this); + this.handleModalAction = this.handleModalAction.bind(this); + this.handleTabSelect = this.handleTabSelect.bind(this); + this.updatePurity = this.updatePurity.bind(this); + } + + handleModalClose() { + this.setState({ showModal: false, droppedMaterial: null }); + } + + handleModalAction(action) { + const { droppedMaterial, sample } = this.state; + + if (droppedMaterial) { + const { + srcMat, srcGroup, tagMat, tagGroup + } = droppedMaterial; + this.dropMaterial(srcMat, srcGroup, tagMat, tagGroup, action); + } + this.handleModalClose(); + this.props.onChange(sample); + } + + handleTabSelect(tab) { + this.setState({ activeTab: tab }); + } + + onChangeComponent(changeEvent) { + const { sample } = this.state; + + sample.components = sample.components.map((component) => { + if (!(component instanceof Component)) { + return new Component(component); + } + return component; + }); + + switch (changeEvent.type) { + case 'amountChanged': + this.updatedSampleForAmountUnitChange(changeEvent); + break; + case 'MetricsChanged': + this.updatedSampleForMetricsChange(changeEvent); + break; + case 'nameChanged': + this.updateComponentName(changeEvent); + break; + case 'ratioChanged': + this.updateRatio(changeEvent); + break; + case 'referenceChanged': + this.updateSampleForReferenceChanged(changeEvent); + break; + case 'purityChanged': + this.updatePurity(changeEvent); + break; + case 'densityChanged': + this.updateDensity(changeEvent); + break; + default: + break; + } + this.props.onChange(sample); + } + + updatedSampleForAmountUnitChange(changeEvent) { + const { sample } = this.props; + const { sampleID, amount } = changeEvent; + const componentIndex = sample.components.findIndex( + (component) => component.id === sampleID + ); + + const totalVolume = sample.amount_l; + + if (amount.unit === 'g' || amount.unit === 'l') { + sample.components[componentIndex].setAmount(amount, totalVolume); // volume given, update amount + } else if (amount.unit === 'mol') { + sample.components[componentIndex].setMol(amount, totalVolume); // amount given, update volume + } else if (amount.unit === 'mol/l') { + sample.components[componentIndex].setConc(amount); // starting conc. given, + } + + // update components ratio + sample.updateMixtureComponentEquivalent(); + } + + updateDensity(changeEvent) { + const { sample } = this.props; + const { sampleID, amount, lockColumn } = changeEvent; + const componentIndex = sample.components.findIndex( + (component) => component.id === sampleID + ); + + const totalVolume = sample.amount_l; + + sample.components[componentIndex].setDensity(amount, lockColumn, totalVolume); + // update components ratio + sample.updateMixtureComponentEquivalent(); + } + + updatePurity(changeEvent) { + const { sample } = this.props; + const { sampleID, amount } = changeEvent; + const purity = amount.value; + const componentIndex = this.props.sample.components.findIndex( + (component) => component.id === sampleID + ); + sample.components[componentIndex].setPurity(purity, sample.amount_l); + sample.updateMixtureComponentEquivalent(); + } + + updatedSampleForMetricsChange(changeEvent) { + const { sample } = this.props; + const { sampleID, metricUnit, metricPrefix } = changeEvent; + const componentIndex = this.props.sample.components.findIndex( + (component) => (component.parent_id === sampleID || component.id === sampleID) + ); + sample.components[componentIndex].setUnitMetrics(metricUnit, metricPrefix); + } + + dropSample(srcSample, tagMaterial, tagGroup, extLabel, isNewSample = false) { + const { sample } = this.state; + const { currentCollection } = UIStore.getState(); + let splitSample; + + if (srcSample instanceof Molecule || isNewSample) { + splitSample = Sample.buildNew(srcSample, currentCollection.id); + splitSample = new Component(splitSample); + } else if (srcSample instanceof Sample) { + splitSample = srcSample.buildChildWithoutCounter(); + splitSample = new Component(splitSample); + } + + splitSample.material_group = tagGroup; + + if (splitSample.sample_type === 'Mixture') { + ComponentsFetcher.fetchComponentsBySampleId(srcSample.id) + .then(async (components) => { + for (const component of components) { + const { component_properties, ...rest } = component; + const sampleData = { + ...rest, + ...component_properties + }; + const sampleComponent = new Component(sampleData); + sampleComponent.parent_id = splitSample.parent_id; + sampleComponent.material_group = tagGroup; + sampleComponent.reference = false; + if (tagGroup === 'solid') { + sampleComponent.setMolarity({ value: 0, unit: 'M' }, sample.amount_l, 'startingConc'); + sampleComponent.setAmount({ value: sampleComponent.amount_g, unit: 'g' }, sample.amount_l); + } else if (tagGroup === 'liquid') { + sampleComponent.setAmount({ value: sampleComponent.amount_l, unit: 'l' }, sample.amount_l); + } + sampleComponent.id = `comp_${Math.random().toString(36).substr(2, 9)}`; + await sample.addMixtureComponent(sampleComponent); + sample.updateMixtureComponentEquivalent(); + } + this.props.onChange(sample); + }) + .catch((errorMessage) => { + console.error(errorMessage); + }); + } else { + sample.addMixtureComponent(splitSample); + sample.updateMixtureComponentEquivalent(); + this.props.onChange(sample); + } + } + + updateComponentName(changeEvent) { + const { sample } = this.props; + const { sampleID, newName } = changeEvent; + const componentIndex = this.props.sample.components.findIndex( + (component) => component.id === sampleID + ); + sample.components[componentIndex].name = newName; + + this.props.onChange(sample); + } + + dropMaterial(srcMat, srcGroup, tagMat, tagGroup, action) { + const { sample } = this.state; + sample.components = sample.components.map((component) => { + if (!(component instanceof Component)) { + return new Component(component); + } + return component; + }); + + if (action === 'move') { + sample.moveMaterial(srcMat, srcGroup, tagMat, tagGroup); + this.props.onChange(sample); + } else if (action === 'merge') { + sample.mergeComponents(srcMat, srcGroup, tagMat, tagGroup) + .then(() => { + this.props.onChange(sample); + }) + .catch((error) => { + console.error('Error merging components:', error); + }); + } + } + + deleteMixtureComponent(component) { + const { sample } = this.state; + sample.deleteMixtureComponent(component); + this.props.onChange(sample); + } + + switchAmount(materialGroup) { + const { lockAmountColumn, lockAmountColumnSolids } = this.state; + if (materialGroup === 'liquid') { + this.setState({ lockAmountColumn: !lockAmountColumn }); + } else if (materialGroup === 'solid') { + this.setState({ lockAmountColumnSolids: !lockAmountColumnSolids }); + } + } + + updateRatio(changeEvent) { + const { sample } = this.props; + const { + sampleID, newRatio, materialGroup + } = changeEvent; + const componentIndex = this.props.sample.components.findIndex( + (component) => component.id === sampleID + ); + const refIndex = this.props.sample.components.findIndex( + (component) => component.reference === true + ); + const referenceMoles = sample.components[refIndex].amount_mol; + const totalVolume = sample.amount_l; + + sample.components[componentIndex].updateRatio(newRatio, materialGroup, totalVolume, referenceMoles); + + sample.updateMixtureMolecularWeight(); + + this.props.onChange(sample); + } + + updateSampleForReferenceChanged(changeEvent) { + const { sample } = this.props; + const { sampleID } = changeEvent; + const componentIndex = this.props.sample.components.findIndex( + (component) => component.id === sampleID + ); + + sample.setReferenceComponent(componentIndex); + } + + showModalWithMaterial(srcMat, srcGroup, tagMat, tagGroup) { + if (!tagMat && srcGroup !== tagGroup) { + this.setState({ + showModal: false, + droppedMaterial: null, + }); + return this.dropMaterial(srcMat, srcGroup, tagMat, tagGroup, 'move'); + } + this.setState({ + showModal: true, + droppedMaterial: { + srcMat, srcGroup, tagMat, tagGroup + }, + }); + } + + renderModal() { + return ( + + + +

Do you want to merge or move this component?

+
+ + + + + +
+ ); + } + + render() { + const { + sample, isOver, canDrop, enableComponentLabel, enableComponentPurity + } = this.props; + const style = { + padding: '2px 5px', + }; + if (isOver && canDrop) { + style.borderStyle = 'dashed'; + style.borderColor = '#337ab7'; + } else if (canDrop) { + style.borderStyle = 'dashed'; + } + const minPadding = { padding: '1px 2px 2px 0px' }; + + if (sample && sample.components) { + sample.components = sample.components.map((component) => (component instanceof Component ? component : new Component(component))); + } + + const liquids = sample.components + ? sample.components.filter((component) => component.material_group === 'liquid') + : []; + const solids = sample.components + ? sample.components.filter((component) => component.material_group === 'solid') + : []; + + return ( + + {this.renderModal()} + + this.onChangeComponent(changeEvent)} + switchAmount={this.switchAmount} + lockAmountColumn={this.state.lockAmountColumn} + lockAmountColumnSolids={this.state.lockAmountColumnSolids} + materialGroup="liquid" + showModalWithMaterial={this.showModalWithMaterial} + handleTabSelect={this.handleTabSelect} + activeTab={this.state.activeTab} + enableComponentLabel={enableComponentLabel} + enableComponentPurity={enableComponentPurity} + /> + + + this.onChangeComponent(changeEvent)} + switchAmount={this.switchAmount} + lockAmountColumn={this.state.lockAmountColumn} + lockAmountColumnSolids={this.state.lockAmountColumnSolids} + materialGroup="solid" + showModalWithMaterial={this.showModalWithMaterial} + handleTabSelect={this.handleTabSelect} + activeTab={this.state.activeTab} + /> + + + ); + } +} + +SampleDetailsComponents.propTypes = { + sample: PropTypes.instanceOf(Sample).isRequired, + onChange: PropTypes.func.isRequired, + isOver: PropTypes.bool.isRequired, + canDrop: PropTypes.bool.isRequired, + enableComponentLabel: PropTypes.bool.isRequired, + enableComponentPurity: PropTypes.bool.isRequired, +}; diff --git a/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponentsDnd.js b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponentsDnd.js new file mode 100644 index 0000000000..050b4e933c --- /dev/null +++ b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponentsDnd.js @@ -0,0 +1,130 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DropTarget } from 'react-dnd'; +import { DragDropItemTypes } from 'src/utilities/DndConst'; +import Sample from 'src/models/Sample'; +import SampleComponentsGroup from 'src/apps/mydb/elements/details/samples/propertiesTab/SampleComponentsGroup'; + +const target = { + drop(tagProps, monitor) { + const { dropSample, dropMaterial } = tagProps; + const srcItem = monitor.getItem(); + const srcType = monitor.getItemType(); + if (srcType === DragDropItemTypes.SAMPLE) { + dropSample( + srcItem.element, + tagProps.material, + tagProps.materialGroup, + ); + } else if (srcType === DragDropItemTypes.MOLECULE) { + dropSample( + srcItem.element, + tagProps.material, + tagProps.materialGroup, + null, + true, + ); + } else if (srcType === DragDropItemTypes.MATERIAL) { + dropMaterial( + srcItem.material, + srcItem.materialGroup, + tagProps.material, + tagProps.materialGroup, + 'move', + ); + } + }, + canDrop(tagProps, monitor) { + const srcType = monitor.getItemType(); + const isCorrectType = srcType === DragDropItemTypes.MATERIAL + || srcType === DragDropItemTypes.SAMPLE + || srcType === DragDropItemTypes.MOLECULE; + return isCorrectType; + }, +}; + +const collect = (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop() +}); + +class SampleDetailsComponentsDnd extends React.Component { + render() { + const { + sample, + sampleComponents, + headIndex, + onChangeComponent, + dropSample, + dropMaterial, + deleteMixtureComponent, + isOver, + canDrop, + connectDropTarget, + lockAmountColumn, + lockAmountColumnSolids, + switchAmount, + materialGroup, + showModalWithMaterial, + activeTab, + handleTabSelect, + enableComponentLabel, + enableComponentPurity, + } = this.props; + const style = { + padding: '0px 0px', + }; + if (isOver && canDrop) { + style.borderStyle = 'dashed'; + style.borderColor = '#337ab7'; + } else if (canDrop) { + style.borderStyle = 'dashed'; + } + return connectDropTarget( +
+ +
+ ); + } +} + +export default DropTarget( + [DragDropItemTypes.SAMPLE, DragDropItemTypes.MOLECULE, DragDropItemTypes.MATERIAL], + target, + collect, +)(SampleDetailsComponentsDnd); + +SampleDetailsComponentsDnd.propTypes = { + sample: PropTypes.instanceOf(Sample).isRequired, + headIndex: PropTypes.number, + onChangeComponent: PropTypes.func.isRequired, + dropSample: PropTypes.func.isRequired, + dropMaterial: PropTypes.func.isRequired, + showModalWithMaterial: PropTypes.func.isRequired, + deleteMixtureComponent: PropTypes.func.isRequired, + isOver: PropTypes.bool.isRequired, + canDrop: PropTypes.bool.isRequired, + connectDropTarget: PropTypes.func.isRequired, + enableComponentLabel: PropTypes.bool.isRequired, + enableComponentPurity: PropTypes.bool.isRequired, +}; diff --git a/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleForm.js b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleForm.js index 2001639f88..5f2adab251 100644 --- a/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleForm.js +++ b/app/packs/src/apps/mydb/elements/details/samples/propertiesTab/SampleForm.js @@ -11,10 +11,11 @@ import DetailActions from 'src/stores/alt/actions/DetailActions'; import NumeralInputWithUnitsCompo from 'src/apps/mydb/elements/details/NumeralInputWithUnitsCompo'; import NumericInputUnit from 'src/apps/mydb/elements/details/NumericInputUnit'; import TextRangeWithAddon from 'src/apps/mydb/elements/details/samples/propertiesTab/TextRangeWithAddon'; -import { solventOptions } from 'src/components/staticDropdownOptions/options'; +import { solventOptions, SampleTypesOptions } from 'src/components/staticDropdownOptions/options'; import SampleDetailsSolvents from 'src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsSolvents'; import PrivateNoteElement from 'src/apps/mydb/elements/details/PrivateNoteElement'; import NotificationActions from 'src/stores/alt/actions/NotificationActions'; +import SampleDetailsComponents from 'src/apps/mydb/elements/details/samples/propertiesTab/SampleDetailsComponents'; export default class SampleForm extends React.Component { constructor(props) { @@ -23,6 +24,9 @@ export default class SampleForm extends React.Component { molarityBlocked: (props.sample.molarity_value || 0) <= 0, isMolNameLoading: false, moleculeFormulaWas: props.sample.molecule_formula, + selectedSampleType: props.sample.sample_type ? props.sample.sample_type : SampleTypesOptions[0], + enableComponentLabel: false, + enableComponentPurity: false, }; this.handleFieldChanged = this.handleFieldChanged.bind(this); @@ -34,6 +38,8 @@ export default class SampleForm extends React.Component { this.handleRangeChanged = this.handleRangeChanged.bind(this); this.handleSolventChanged = this.handleSolventChanged.bind(this); this.handleMetricsChange = this.handleMetricsChange.bind(this); + this.handleMixtureComponentChanged = this.handleMixtureComponentChanged.bind(this); + this.handleSampleTypeChanged = this.handleSampleTypeChanged.bind(this); } // eslint-disable-next-line camelcase @@ -41,6 +47,27 @@ export default class SampleForm extends React.Component { this.setState({ isMolNameLoading: false }); } + handleToggle = (key) => { + this.setState((prevState) => ({ + [key]: !prevState[key], + })); + }; + + renderCheckbox = (key, label, className) => { + const isChecked = this.state[key]; + + return ( + this.handleToggle(key)} + > + {label} + + ); + }; + formulaChanged() { return this.props.sample.molecule_formula !== this.state.moleculeFormulaWas; } @@ -54,6 +81,13 @@ export default class SampleForm extends React.Component { this.setState({ molarityBlocked: false }); } + handleSampleTypeChanged(sampleType) { + const { sample } = this.props; + sample.updateSampleType(sampleType.value); + this.setState({ selectedSampleType: sampleType.value }); + this.props.parent.setState({ sample }); + } + handleDensityChanged(density) { this.props.sample.setDensity(density); this.setState({ molarityBlocked: true }); @@ -67,6 +101,10 @@ export default class SampleForm extends React.Component { this.props.parent.setState({ sample }); } + handleMixtureComponentChanged(sample) { + this.props.parent.setState({ sample }); + } + showStructureEditor() { this.props.parent.setState({ showStructureEditor: true, @@ -425,7 +463,8 @@ export default class SampleForm extends React.Component { disabled = false, title = '', block = false, - notApplicable = false + notApplicable = false, + showInfoTooltipTotalVol = false ) { if (sample.contains_residues && unit === 'l') return false; const value = !isNaN(sample[field]) ? sample[field] : null; @@ -482,6 +521,7 @@ export default class SampleForm extends React.Component { onChange={(e) => this.handleFieldChanged(field, e)} onMetricsChange={(e) => this.handleMetricsChange(e)} id={`numInput_${field}`} + showInfoTooltipTotalVol={showInfoTooltipTotalVol} /> ); @@ -530,6 +570,34 @@ export default class SampleForm extends React.Component { ); } + totalAmount(sample) { + const isDisabled = !sample.can_update; + + if (!sample.isMethodDisabled('amount_value') && !sample.contains_residues) { + return this.numInput( + sample, + 'amount_l', + 'l', + ['m', 'u', 'n'], + 5, + 'Total volume', + 'l', + isDisabled, + '', + false, + false, + true + ); + } + + return ( + + Target Total Volume + + + ); + } + sampleAmount(sample) { const content = []; const isDisabled = !sample.can_update; @@ -683,14 +751,48 @@ export default class SampleForm extends React.Component { // eslint-disable-next-line class-methods-use-this assignAmountType(reaction, sample) { - // eslint-disable-next-line no-underscore-dangle - reaction._products.map((s) => { - if (s.id === sample.id) { - // eslint-disable-next-line no-param-reassign - sample.amountType = 'real'; - } - return sample; - }); + if (reaction._products && reaction._products.length > 0) { + // eslint-disable-next-line no-underscore-dangle + reaction._products.map((s) => { + if (s.id === sample.id) { + // eslint-disable-next-line no-param-reassign + sample.amountType = 'real'; + } + return sample; + }); + } + } + + sampleTypeInput() { + const { sample } = this.props; + return ( + + Sample Type +