From e10774b56e1304998e921245915d052a38e2f7aa Mon Sep 17 00:00:00 2001 From: PiTrem Date: Tue, 21 Jan 2025 12:28:53 +0100 Subject: [PATCH] feat: element device description #2281 Squashed commit of the following: commit 8ee755d9fa95be922ffc7a1c9a68af6a0f90bb4e Author: Beate Quednau Date: Mon Jan 20 18:58:25 2025 +0100 Fix some fields and translations for device descriptions commit 088f2a60cb70b610fdc3ae397f46247c3b12c627 Author: Beate Quednau Date: Mon Jan 13 16:46:58 2025 +0100 Fix deleted cell line for copy cell line commit 88c7022bb9d5f8002687d4bfb6a7f015034067d6 Author: Beate Quednau Date: Fri Jan 10 19:09:14 2025 +0100 Fix attachment spec commit 17fea2ac75684c5bd4c1ebce8db8f9edf74a31fe Author: Beate Quednau Date: Fri Jan 10 17:23:40 2025 +0100 Fix rubocop commit c109522524d7ead878580bbf68843da031e178ae Author: Beate Quednau Date: Fri Jan 10 13:52:03 2025 +0100 Fix double getReactionId commit 10565d2c80d407a2392eabd3be5f1738983fbf19 Author: Beate Quednau Date: Tue Jan 7 19:25:16 2025 +0100 Fix some errors commit ed4b1828bfd8cb84622c21fb8a8495822f53e021 Author: Beate Quednau Date: Tue Jan 7 18:36:34 2025 +0100 Fix sortable analyses list commit 476bbe4579dc251d133a78b552b0c26d242d6e29 Author: Beate Quednau Date: Mon Jan 6 17:42:15 2025 +0100 Fix chem-generic-ui buttons and interface at device description details commit 7a6df0351756fa81aa1f269fad985767e7253334 Author: Beate Quednau Date: Mon Jan 6 17:23:03 2025 +0100 Fix sortable ontologies list commit 94994b06dcdb7fdc2879995e0415015052634176 Author: Beate Quednau Date: Fri Jan 3 14:16:17 2025 +0100 Move device description details and list commit fe112db91832b86254d60c6556762136ca25d092 Author: Beate Quednau Date: Wed Nov 6 14:48:59 2024 +0100 Fix user label warning commit 7009e47a6587a3a1b1a155e1451b7f5dc44b4d99 Author: Beate Quednau Date: Tue Nov 5 19:42:01 2024 +0100 Fix rubocop commit ffcace36ad4e65f7e6d71689bfd0d18ea8954090 Author: Beate Quednau Date: Tue Nov 5 19:17:34 2024 +0100 Fixes for attachments, image modal and textareas commit 3ae0d408546b0c17277d7802d7898b8d0d62cb43 Author: Beate Quednau Date: Tue Nov 5 15:39:16 2024 +0100 Remove unused scss files, Fix calendar commit 52ccccbaf9c965e245d4fefada83b132082ae6f2 Author: Beate Quednau Date: Mon Nov 4 15:42:42 2024 +0100 Fix for text template toolbar commit 5374d5aa31ff68a51d32299466ea2ae3d65c2b51 Author: Beate Quednau Date: Wed Oct 30 18:19:16 2024 +0100 Fix device description detail commit e4fa1393f09ed01fab0ca175503191e8db2a4420 Author: Beate Quednau Date: Mon Oct 21 17:12:03 2024 +0200 Fix device description list commit fc20c087b99ef816c7c30b4f5c5e15523bea2d74 Author: Beate Quednau Date: Mon Oct 21 17:09:31 2024 +0200 Fix device description icon commit b7f8fa4b3b30ae75f77bed6bc155d512cb6eb3dd Author: Beate Quednau Date: Thu Sep 26 11:46:20 2024 +0200 Fix rubocop for profile.api commit 486cc5d0552249c809cbd48439c54fa86ddbb34d Author: Beate Quednau Date: Thu Sep 26 11:36:16 2024 +0200 Fixes after rebase commit 2f333d0a5c11d75b0a79ce5d5aa756b61651bc34 Author: Beate Quednau Date: Thu Jun 6 12:41:07 2024 +0200 Fix styles for staging commit 7128808dd624a05724a94ac387a29151d9fe415c Author: Beate Quednau Date: Wed Jun 5 19:00:39 2024 +0200 Fix styles for select in addon commit fa1ee891e65935ad5962dd47f375ea624489ec40 Author: Beate Quednau Date: Mon Jun 3 14:41:09 2024 +0200 Add maintenance tab fields commit ebb1f29df28029c3087cea47f1137bed6e7ef995 Author: Beate Quednau Date: Fri May 31 14:07:56 2024 +0200 Fix barcode function for device description, rubocop for code logs commit a5fb0c1d57a2d64100ca83f262a363b6394ecd13 Author: Beate Quednau Date: Thu May 30 16:45:37 2024 +0200 Optimize some fields commit fa88cbe32fd2aa91e7cf6614c641e291fad2a701 Author: Beate Quednau Date: Thu May 30 11:44:24 2024 +0200 Fix for adding new device description commit 24d73bb78b6dd155eb65a620eb2d1c959229078b Author: Beate Quednau Date: Fri May 24 18:47:10 2024 +0200 Add missing key commit 7b1ba667bc29932cd685569211c8525ffb52108b Author: Beate Quednau Date: Fri May 24 18:13:25 2024 +0200 Add set description with draggable components commit 1cf9eaa06ab491815ff8f9acf22fb19d20df05ca Author: Beate Quednau Date: Thu May 16 20:39:07 2024 +0200 Add new field at properties tab commit 5ced50a30213deb327f591ef51d7311863aa2456 Author: Beate Quednau Date: Tue May 14 17:40:23 2024 +0200 Add styles for grouped device description list commit 1aa6a7a529c3db31dccd24b3329f10548dbd9112 Author: Beate Quednau Date: Tue Apr 30 19:51:56 2024 +0200 Add list view for device description commit 18aef07b63e7c1f98ae615bd2d6c3307cb524377 Author: Beate Quednau Date: Mon Apr 29 17:25:25 2024 +0200 Fix rubocop for each commit 47488c852be8962edf84280f8b0e25fa50fa5537 Author: Beate Quednau Date: Fri Apr 26 15:38:01 2024 +0200 Add split for device description commit f9c67872a108ea50a8d61db42ef670577be9ccc5 Author: Beate Quednau Date: Wed Apr 24 16:40:02 2024 +0200 Add copy device description functionality commit 4b204180a8ddf914d2b4b8b41adef2690d659407 Author: Beate Quednau Date: Tue Apr 23 19:23:32 2024 +0200 Fix rubocop error for inverse_of at device description commit b635781a923ed5243c6d3c75bd9a8fbc5000fcd5 Author: Beate Quednau Date: Tue Apr 23 12:25:24 2024 +0200 Add copy button commit 7750ce860b7292f062d6d5bcc73a83a25bd6b227 Author: Beate Quednau Date: Fri Apr 19 13:34:45 2024 +0200 Fix for taggable data of element tag commit 38aa58cd59eeb17f336dd5af1e360583c6100d11 Author: Beate Quednau Date: Thu Apr 18 19:31:21 2024 +0200 Add calendar button to device description detail header commit 15f3f33d56f240dc32933c380ce823b01620ff03 Author: Beate Quednau Date: Wed Apr 17 20:09:29 2024 +0200 Add buttons to device description header, Rename conflict fields commit 390a4e9fcdc2ae330b53ff5df380002d3eec7128 Author: Beate Quednau Date: Tue Apr 16 18:04:40 2024 +0200 Fix some javascript warnings commit d365bd66fdf1fc6819c1d7b0a94586dbfaaa01a5 Author: Beate Quednau Date: Fri Apr 12 19:45:51 2024 +0200 Fix save for unchanged segements commit 9f53c85dc894d2b1e2785f5f896f161053a1fe33 Author: Beate Quednau Date: Fri Apr 12 19:44:45 2024 +0200 Add disabled tabs for new device description commit 575c243f56e24dc496501b7971a8a1087ddae1f5 Author: Beate Quednau Date: Fri Apr 12 14:28:34 2024 +0200 Add test for finding segment klasses by ontology commit 6e5dfdd97e8ffaa4b065fc09504704103e92f7e0 Author: Beate Quednau Date: Thu Apr 11 19:47:55 2024 +0200 Fix rubocop errors commit bb7a5f1551209405d798b10b2bb0717274ba27cb Author: Beate Quednau Date: Thu Apr 11 17:29:12 2024 +0200 Add more functions for adding ontologies to device descriptions commit 6202b8bba9bcd6f09a42741ef5c7dfb8eae28280 Author: Beate Quednau Date: Fri Apr 5 14:43:29 2024 +0200 Add ontology details tab commit 7b0969f45fac738dceba98f3f6e5be3adf680a7a Author: Beate Quednau Date: Fri Mar 15 14:22:18 2024 +0100 Add button functionality for attachments and analyses commit 36131733c223e9e3b784fa145eea00281f42242f Author: Beate Quednau Date: Wed Mar 13 19:55:11 2024 +0100 Add basic ontology requests commit 493baedc08631d6017e96dceeed7f0433177634f Author: Beate Quednau Date: Wed Mar 13 19:54:01 2024 +0100 Fix update, delete attachments commit a621ef772e92888f61455033ad7ad5eb4aeb9a94 Author: Beate Quednau Date: Fri Mar 8 16:03:40 2024 +0100 Add attachment form and handling commit 79158e0a18ec2478ab3125bb8f4f04186df889aa Author: Beate Quednau Date: Fri Mar 1 21:06:23 2024 +0100 Add analyses container to device description, Fix detail level commit 8e8532dd72d4ac9d88da7e3cca66eb1c671fbe33 Author: Beate Quednau Date: Thu Feb 22 19:29:49 2024 +0100 Add operators at device description properties tab commit 9c698f24218832cbcd7387db19f3004106c3cafe Author: Beate Quednau Date: Wed Feb 21 13:52:27 2024 +0100 Fix for deleting device descriptions commit f6c46d6009864a2e214bea831ae89e7a48d7952a Author: Beate Quednau Date: Tue Feb 20 19:46:04 2024 +0100 Add more fields to properties form of device description commit f1b73cfbff85e82f905fdf9a07df36723bdb05d3 Author: Beate Quednau Date: Tue Feb 20 19:45:09 2024 +0100 Add device description store commit 1357f30d650b1242cbfa3f69f2aeed51897216c8 Author: Beate Quednau Date: Tue Feb 20 19:42:43 2024 +0100 Add device description to alt store and apis commit c1d904d6a204f2659276420492a4b949f8094320 Author: Beate Quednau Date: Tue Feb 20 19:39:38 2024 +0100 Add and change some fields at device description commit 4c598aee4ef7114913f7e4d74a4e8722dd8b2e88 Author: Beate Quednau Date: Mon Dec 18 15:56:08 2023 +0100 Add basic form with tabs for device description commit 3288efab9c8a3027b31806cb281ce73265059fcf Author: Beate Quednau Date: Thu Dec 14 15:08:17 2023 +0100 Add device description fetcher, js model, text template, to collection commit d94fc5430bed28974380323334c574876631bf13 Author: Beate Quednau Date: Thu Dec 14 14:50:28 2023 +0100 Add new element device description to eln commit b1382f46da4a406c8c32283a922bced383abd63e Author: Beate Quednau Date: Tue Dec 12 12:39:49 2023 +0100 Add model collections_device_description commit fd56b5b79a6e5eec2d05bde2d23dc26c0e7f0880 Author: Beate Quednau Date: Mon Dec 11 17:25:09 2023 +0100 Add device description api endpoint commit 319ef626f5cf7310a822afa331949208e23dd479 Author: Beate Quednau Date: Mon Dec 11 13:00:38 2023 +0100 Add model device description --- app/api/api.rb | 4 +- app/api/chemotion/attachable_api.rb | 4 +- app/api/chemotion/attachment_api.rb | 36 +- app/api/chemotion/code_log_api.rb | 25 +- app/api/chemotion/collection_api.rb | 10 + app/api/chemotion/comment_api.rb | 1 + app/api/chemotion/device_description_api.rb | 312 ++++++++ app/api/chemotion/element_api.rb | 5 +- app/api/chemotion/literature_api.rb | 6 +- app/api/chemotion/profile_api.rb | 4 + app/api/chemotion/text_template_api.rb | 1 + app/api/entities/attachment_entity.rb | 6 +- app/api/entities/device_description_entity.rb | 82 ++ app/api/entities/user_entity.rb | 5 + app/api/helpers/collection_helpers.rb | 6 +- .../package-mods/react-datepicker.scss | 10 + .../package-mods/react-select.scss | 69 +- .../stylesheets/global-styles/utilities.scss | 1 + .../global-styles/utilities/detail-card.scss | 2 +- .../global-styles/utilities/dnd.scss | 9 + .../global-styles/utilities/form.scss | 20 + .../stylesheets/global-styles/vendor.scss | 1 + .../stylesheets/legacy/miscellaneous.scss | 11 + app/javascript/src/apps/generic/Utils.js | 2 +- .../apps/mydb/collections/CollectionTabs.js | 3 +- .../src/apps/mydb/elements/Elements.js | 6 + .../mydb/elements/details/ElementDetails.js | 7 + .../DeviceDescriptionDetails.js | 245 ++++++ .../details/deviceDescriptions/FormFields.js | 751 ++++++++++++++++++ .../deviceDescriptions/OntologiesList.js | 231 ++++++ .../OntologySegmentsList.js | 105 +++ .../deviceDescriptions/OntologySelect.js | 135 ++++ .../OntologySortableList.js | 83 ++ .../analysesTab/AnalysesContainer.js | 222 ++++++ .../analysesTab/AnalysesSortableContainer.js | 77 ++ .../analysesTab/AnalysisHeader.js | 214 +++++ .../attachmentsTab/AttachmentForm.js | 333 ++++++++ .../detailsTab/DetailsForm.js | 68 ++ .../maintenanceTab/MaintenanceForm.js | 250 ++++++ .../propertiesTab/PropertiesForm.js | 469 +++++++++++ .../ImageAnnotationEditButton.js | 3 +- .../apps/mydb/elements/list/AttachmentList.js | 16 +- .../mydb/elements/list/ElementContainer.js | 1 + .../apps/mydb/elements/list/ElementsList.js | 9 +- .../apps/mydb/elements/list/ElementsTable.js | 16 +- .../DeviceDescriptionList.js | 183 +++++ .../DeviceDescriptionListHeader.js | 54 ++ .../mydb/elements/tabLayout/TabLayoutCell.js | 9 +- app/javascript/src/apps/mydb/routes.js | 6 + app/javascript/src/components/UserLabels.js | 40 +- .../src/components/calendar/Calendar.js | 2 + .../src/components/common/CopyElementModal.js | 22 +- .../src/components/common/ImageModal.js | 2 + .../components/container/AnalysisEditor.js | 8 +- .../components/contextActions/CreateButton.js | 35 +- .../contextActions/SplitElementButton.js | 23 + .../components/generic/EditorAnalysisBtn.js | 3 +- .../managingActions/ManagingActions.js | 1 - .../ToolbarTemplateCreator.js | 17 +- .../src/fetchers/AttachmentFetcher.js | 47 +- .../src/fetchers/DeviceDescriptionFetcher.js | 150 ++++ .../src/models/DeviceDescription.js | 105 +++ .../stores/alt/actions/ClipboardActions.js | 21 + .../src/stores/alt/actions/ElementActions.js | 68 +- .../src/stores/alt/stores/ClipboardStore.js | 23 +- .../src/stores/alt/stores/ElementStore.js | 59 +- .../src/stores/alt/stores/LoadingStore.js | 2 + .../stores/alt/stores/TextTemplateStore.js | 1 + .../src/stores/alt/stores/UIStore.js | 41 +- .../src/stores/mobx/CalendarStore.jsx | 1 + .../stores/mobx/DeviceDescriptionsStore.jsx | 285 +++++++ app/javascript/src/stores/mobx/RootStore.jsx | 3 + app/javascript/src/utilities/DndConst.js | 7 +- app/javascript/src/utilities/routesUtils.js | 46 +- app/jobs/download_analyses_job.rb | 60 +- app/models/collection.rb | 2 + app/models/collections_device_description.rb | 47 ++ app/models/comment.rb | 11 +- app/models/concerns/tagging.rb | 2 +- app/models/device.rb | 1 + app/models/device_description.rb | 129 +++ .../device_description_text_template.rb | 26 + app/models/profile.rb | 1 + app/models/sync_collections_user.rb | 2 + app/models/text_template.rb | 24 +- app/models/user.rb | 2 + app/proxies/element_permission_proxy.rb | 2 + .../element_detail_level_calculator.rb | 8 +- app/usecases/device_descriptions/create.rb | 68 ++ app/usecases/device_descriptions/update.rb | 146 ++++ app/usecases/sharing/share_with_users.rb | 1 + .../initializers/content_security_policy.rb | 2 +- config/profile_default.yml.example | 36 + ...240206164554_create_device_descriptions.rb | 38 + ..._create_collections_device_descriptions.rb | 12 + ...t_device_description_to_element_klasses.rb | 49 ++ ...description_detail_level_to_collections.rb | 5 + ...ange_some_fields_at_device_descriptions.rb | 71 ++ ...tail_level_field_for_device_description.rb | 6 + ...8_add_ontologies_to_device_descriptions.rb | 5 + ...33_rename_fields_at_device_descriptions.rb | 6 + ...556_add_ancestry_to_device_descriptions.rb | 6 + ..._add_more_fields_to_device_descriptions.rb | 15 + ...intenance_fields_to_device_descriptions.rb | 14 + ..._add_weight_unit_to_device_descriptions.rb | 5 + db/schema.rb | 72 +- spec/api/chemotion/attachment_api_spec.rb | 10 +- .../chemotion/device_description_api_spec.rb | 173 ++++ .../collections_device_descriptions.rb | 8 + spec/factories/device_descriptions.rb | 99 +++ spec/factories/setment_klasses.rb | 64 ++ spec/factories/text_template.rb | 17 +- spec/factories/users.rb | 2 + .../collections_device_description_spec.rb | 6 + spec/models/device_description_spec.rb | 6 + .../element_detail_level_calculator_spec.rb | 1 + 116 files changed, 6217 insertions(+), 183 deletions(-) create mode 100644 app/api/chemotion/device_description_api.rb create mode 100644 app/api/entities/device_description_entity.rb create mode 100644 app/assets/stylesheets/global-styles/package-mods/react-datepicker.scss create mode 100644 app/assets/stylesheets/global-styles/utilities/form.scss create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/DeviceDescriptionDetails.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/FormFields.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/OntologiesList.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/OntologySegmentsList.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/OntologySelect.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/OntologySortableList.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/analysesTab/AnalysesContainer.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/analysesTab/AnalysesSortableContainer.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/analysesTab/AnalysisHeader.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/attachmentsTab/AttachmentForm.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/detailsTab/DetailsForm.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/maintenanceTab/MaintenanceForm.js create mode 100644 app/javascript/src/apps/mydb/elements/details/deviceDescriptions/propertiesTab/PropertiesForm.js create mode 100644 app/javascript/src/apps/mydb/elements/list/deviceDescriptions/DeviceDescriptionList.js create mode 100644 app/javascript/src/apps/mydb/elements/list/deviceDescriptions/DeviceDescriptionListHeader.js create mode 100644 app/javascript/src/fetchers/DeviceDescriptionFetcher.js create mode 100644 app/javascript/src/models/DeviceDescription.js create mode 100644 app/javascript/src/stores/mobx/DeviceDescriptionsStore.jsx create mode 100644 app/models/collections_device_description.rb create mode 100644 app/models/device_description.rb create mode 100644 app/models/device_description_text_template.rb create mode 100644 app/usecases/device_descriptions/create.rb create mode 100644 app/usecases/device_descriptions/update.rb create mode 100644 db/migrate/20240206164554_create_device_descriptions.rb create mode 100644 db/migrate/20240206171038_create_collections_device_descriptions.rb create mode 100644 db/migrate/20240207121720_init_device_description_to_element_klasses.rb create mode 100644 db/migrate/20240207123444_add_device_description_detail_level_to_collections.rb create mode 100644 db/migrate/20240209120817_add_and_change_some_fields_at_device_descriptions.rb create mode 100644 db/migrate/20240229180204_change_detail_level_field_for_device_description.rb create mode 100644 db/migrate/20240312151708_add_ontologies_to_device_descriptions.rb create mode 100644 db/migrate/20240417092033_rename_fields_at_device_descriptions.rb create mode 100644 db/migrate/20240424181556_add_ancestry_to_device_descriptions.rb create mode 100644 db/migrate/20240515090140_add_more_fields_to_device_descriptions.rb create mode 100644 db/migrate/20240531122129_add_maintenance_fields_to_device_descriptions.rb create mode 100644 db/migrate/20250120162008_add_weight_unit_to_device_descriptions.rb create mode 100644 spec/api/chemotion/device_description_api_spec.rb create mode 100644 spec/factories/collections_device_descriptions.rb create mode 100644 spec/factories/device_descriptions.rb create mode 100644 spec/models/collections_device_description_spec.rb create mode 100644 spec/models/device_description_spec.rb diff --git a/app/api/api.rb b/app/api/api.rb index 219124793e..bb234a587e 100644 --- a/app/api/api.rb +++ b/app/api/api.rb @@ -138,7 +138,7 @@ def to_json_camel_case(val) TARGET = Rails.env.production? ? 'https://www.chemotion-repository.net/' : 'http://localhost:3000/' - ELEMENTS = %w[research_plan screen wellplate reaction sample cell_line].freeze + ELEMENTS = %w[research_plan screen wellplate reaction sample cell_line device_description].freeze ELEMENT_CLASS = { 'research_plan' => ResearchPlan, @@ -147,6 +147,7 @@ def to_json_camel_case(val) 'reaction' => Reaction, 'sample' => Sample, 'cell_line' => CelllineSample, + 'device_description' => DeviceDescription, }.freeze mount Chemotion::LiteratureAPI @@ -203,6 +204,7 @@ def to_json_camel_case(val) mount Chemotion::AdminDeviceAPI mount Chemotion::AdminDeviceMetadataAPI mount Chemotion::ChemicalAPI + mount Chemotion::DeviceDescriptionAPI add_swagger_documentation(info: { title: 'Chemotion ELN', diff --git a/app/api/chemotion/attachable_api.rb b/app/api/chemotion/attachable_api.rb index facfeb907c..1fdc0f9821 100644 --- a/app/api/chemotion/attachable_api.rb +++ b/app/api/chemotion/attachable_api.rb @@ -48,7 +48,9 @@ class AttachableAPI < Grape::API begin a.save! attach_ary.push(a.id) - rp_attach_ary.push(a.id) if a.attachable_type.in?(%w[ResearchPlan Wellplate Labimotion::Element]) + if a.attachable_type.in?(%w[ResearchPlan Wellplate DeviceDescription Labimotion::Element]) + rp_attach_ary.push(a.id) + end ensure tempfile.close tempfile.unlink diff --git a/app/api/chemotion/attachment_api.rb b/app/api/chemotion/attachment_api.rb index c7c1c4d16a..2456910af2 100644 --- a/app/api/chemotion/attachment_api.rb +++ b/app/api/chemotion/attachment_api.rb @@ -82,6 +82,7 @@ def remove_duplicated(att) @attachment = Attachment.find_by(identifier: params[:identifier]) if @attachment.nil? && params[:identifier] + # rubocop:disable Performance/StringInclude case request.env['REQUEST_METHOD'] when /delete/i error!('401 Unauthorized', 401) unless writable?(@attachment) @@ -101,6 +102,13 @@ def remove_duplicated(att) can_dwnld = can_read && ElementPermissionProxy.new(current_user, element, user_ids).read_dataset? end + elsif /device_description_analyses/.match?(request.url) + @device_description = DeviceDescription.find(params[:device_description_id]) + if (element = @device_description) + can_read = ElementPolicy.new(current_user, element).read? + can_dwnld = can_read && + ElementPermissionProxy.new(current_user, element, user_ids).read_dataset? + end elsif @attachment can_dwnld = @attachment.container_id.nil? && @attachment.created_for == current_user.id @@ -119,6 +127,7 @@ def remove_duplicated(att) end error!('401 Unauthorized', 401) unless can_dwnld end + # rubocop:enable Performance/StringInclude end desc 'Bulk Delete Attachments' @@ -347,20 +356,43 @@ def remove_duplicated(att) end end&.flatten&.reduce(:+) || 0 if tts > 300_000_000 - DownloadAnalysesJob.perform_later(@sample.id, current_user.id, false) + DownloadAnalysesJob.perform_later(@sample.id, current_user.id, false, 'sample') nil else env['api.format'] = :binary content_type('application/zip, application/octet-stream') filename = CGI.escape("#{@sample.short_label}-analytical-files.zip") header('Content-Disposition', "attachment; filename=\"#{filename}\"") - zip = DownloadAnalysesJob.perform_now(@sample.id, current_user.id, true) + zip = DownloadAnalysesJob.perform_now(@sample.id, current_user.id, true, 'sample') zip.rewind zip.read end end + desc 'Download the zip attachment file by device_description_id' + get 'device_description_analyses/:device_description_id' do + # rubocop:disable Performance/Sum + tts = @device_description.analyses&.map do |a| + a.children&.map do |d| + d.attachments&.map(&:filesize) + end + end&.flatten&.reduce(:+) || 0 + # rubocop:enable Performance/Sum + if tts > 300_000_000 + DownloadAnalysesJob.perform_later(@device_description.id, current_user.id, false, 'device_description') + nil + else + env['api.format'] = :binary + content_type('application/zip, application/octet-stream') + filename = CGI.escape("#{@device_description.short_label}-analytical-files.zip") + header('Content-Disposition', "attachment; filename=\"#{filename}\"") + zip = DownloadAnalysesJob.perform_now(@device_description.id, current_user.id, true, 'device_description') + zip.rewind + zip.read + end + end + desc 'Return image attachment' params do diff --git a/app/api/chemotion/code_log_api.rb b/app/api/chemotion/code_log_api.rb index 79a8c99a13..42b51cccb8 100644 --- a/app/api/chemotion/code_log_api.rb +++ b/app/api/chemotion/code_log_api.rb @@ -27,15 +27,16 @@ class CodeLogAPI < Grape::API get do code = params[:code] s = code&.size || 0 - code_log = if s >= 39 - CodeLog.find(CodeCreator.digit_to_uuid(code)) - elsif s >= 8 - # TODO: use where instead of find_by ? - CodeLog.where('value ~ ?', "\\A0#{code}").first - elsif s == 6 - # TODO: use where instead of find_by ? - CodeLog.find_by(value_xs: code.to_i) - end + code_log = + if s >= 39 + CodeLog.find(CodeCreator.digit_to_uuid(code)) + elsif s >= 8 + # TODO: use where instead of find_by ? + CodeLog.where('value ~ ?', "\\A0#{code}").first + elsif s == 6 + # TODO: use where instead of find_by ? + CodeLog.find_by(value_xs: code.to_i) + end if code_log.nil? error!("Element with #{code.size}-digit code #{params[:code]} not found", 404) @@ -48,7 +49,7 @@ class CodeLogAPI < Grape::API namespace :print_codes do desc 'Build PDF with element bar & qr code' params do - requires :element_type, type: String, values: %w[sample reaction wellplate screen] + requires :element_type, type: String, values: %w[sample reaction wellplate screen device_description] # TODO: check coerce with type Array[Integer] not working with before do requires :ids, type: Array # , coerce_with: ->(val) { val.split(/,/).map(&:to_i) } requires :width, type: Integer @@ -97,7 +98,7 @@ class CodeLogAPI < Grape::API namespace :print_analyses_codes do desc 'Build PDF with analyses codes of one analysis type' params do - requires :element_type, type: String, values: %w[sample reaction wellplate screen] + requires :element_type, type: String, values: %w[sample reaction wellplate screen device_description] requires :id, type: Integer, desc: 'Element id' requires :analyses_ids, type: Array[String] requires :size, type: String, values: %w[small big] @@ -118,7 +119,7 @@ class CodeLogAPI < Grape::API content_type('application/pdf') header 'Content-Disposition', "attachment; filename*=UTF-8''analysis_codes_#{params[:size]}.pdf" env['api.format'] = :binary - # TODO: check container type/info instead + # TODO: check container type/info instead # case params[:type] # when "nmr_analysis" # body AnalysisNmrPdf.new(elements).render diff --git a/app/api/chemotion/collection_api.rb b/app/api/chemotion/collection_api.rb index e1e95ff4b8..6d712a972c 100644 --- a/app/api/chemotion/collection_api.rb +++ b/app/api/chemotion/collection_api.rb @@ -193,6 +193,9 @@ class CollectionAPI < Grape::API optional :cell_line, type: Hash do use :ui_state_params end + optional :device_description, type: Hash do + use :ui_state_params + end end requires :collection_attributes, type: Hash do requires :permission_level, type: Integer @@ -221,6 +224,9 @@ class CollectionAPI < Grape::API cell_lines = CelllineSample.by_collection_id(@cid) .by_ui_state(params[:elements_filter][:cell_line]) .for_user_n_groups(user_ids) + device_descriptions = DeviceDescription.by_collection_id(@cid) + .by_ui_state(params[:elements_filter][:device_description]) + .for_user_n_groups(user_ids) elements = {} Labimotion::ElementKlass.find_each do |klass| elements[klass.name] = Labimotion::Element.by_collection_id(@cid).by_ui_state(params[:elements_filter][klass.name]).for_user_n_groups(user_ids) @@ -237,6 +243,7 @@ class CollectionAPI < Grape::API share_screens = ElementsPolicy.new(current_user, screens).share? share_research_plans = ElementsPolicy.new(current_user, research_plans).share? share_cell_lines = ElementsPolicy.new(current_user, cell_lines).share? + share_device_descriptions = ElementsPolicy.new(current_user, device_descriptions).share? share_elements = !(elements&.length > 0) elements.each do |k, v| share_elements = ElementsPolicy.new(current_user, v).share? @@ -249,6 +256,7 @@ class CollectionAPI < Grape::API share_screens && share_research_plans && share_cell_lines && + share_device_descriptions && share_elements error!('401 Unauthorized', 401) if (!sharing_allowed || is_top_secret) @@ -258,6 +266,7 @@ class CollectionAPI < Grape::API @screen_ids = screens.pluck(:id) @research_plan_ids = research_plans.pluck(:id) @cell_line_ids = cell_lines.pluck(:id) + @device_description_ids = device_descriptions.pluck(:id) @element_ids = elements&.transform_values { |v| v && v.pluck(:id) } end @@ -280,6 +289,7 @@ class CollectionAPI < Grape::API screen_ids: @screen_ids, research_plan_ids: @research_plan_ids, cell_line_ids: @cell_line_ids, + device_description_ids: @device_description_ids, element_ids: @element_ids, collection_attributes: params[:collection_attributes].merge(shared_by_id: current_user.id) ).execute! diff --git a/app/api/chemotion/comment_api.rb b/app/api/chemotion/comment_api.rb index 85660467b9..74e6a27313 100644 --- a/app/api/chemotion/comment_api.rb +++ b/app/api/chemotion/comment_api.rb @@ -100,6 +100,7 @@ def find_commentable(commentable_type, commentable_id) Comment.wellplate_sections.values + Comment.screen_sections.values + Comment.research_plan_sections.values + + Comment.device_description_sections.values + Comment.header_sections.values end diff --git a/app/api/chemotion/device_description_api.rb b/app/api/chemotion/device_description_api.rb new file mode 100644 index 0000000000..270e523a6d --- /dev/null +++ b/app/api/chemotion/device_description_api.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/ClassLength +module Chemotion + class DeviceDescriptionAPI < Grape::API + include Grape::Kaminari + helpers ContainerHelpers + helpers ParamsHelpers + helpers CollectionHelpers + + helpers do + params :create_params do + requires :collection_id, type: Integer + end + + params :update_params do + requires :id, type: Integer + optional :device_id, type: Integer + end + + params :default_params do + optional :name, type: String + optional :short_label, type: String + optional :device_type, type: String + optional :device_type_detail, type: String + optional :operation_mode, type: String + optional :vendor_device_name, type: String + optional :vendor_device_id, type: String + optional :serial_number, type: String + optional :vendor_company_name, type: String + optional :vendor_id, type: String + optional :description, type: String + optional :general_tags, type: Array + optional :version_number, type: String + optional :version_installation_start_date, type: DateTime, allow_blank: true + optional :version_installation_end_date, type: DateTime, allow_blank: true + optional :version_identifier_type, type: String + optional :version_doi, type: String + optional :version_doi_url, type: String + optional :version_characterization, type: String + optional :operators, type: Array do + optional :name, type: String + optional :phone, type: String + optional :email, type: String + optional :type, type: String + optional :comment, type: String + end + optional :university_campus, type: String + optional :institute, type: String + optional :building, type: String + optional :room, type: String + optional :infrastructure_assignment, type: String + optional :access_options, type: String + optional :access_comments, type: String + optional :size, type: String + optional :weight, type: String + optional :weight_unit, type: String + optional :application_name, type: String + optional :application_version, type: String + optional :vendor_url, type: String + optional :helpers_uploaded, type: Boolean + optional :policies_and_user_information, type: String + optional :description_for_methods_part, type: String + optional :container, type: Hash + optional :ontologies, type: Array do + optional :data, type: Hash + optional :paths, type: Array + optional :segments, type: Array + optional :index, type: Integer + end + optional :segments, type: Array + optional :setup_descriptions, type: Hash + optional :maintenance_contract_available, type: String + optional :maintenance_scheduling, type: String + optional :contact_for_maintenance, type: Array do + optional :company, type: String + optional :contact, type: String + optional :email, type: String + optional :phone, type: String + optional :comment, type: String + end + optional :planned_maintenance, type: Array do + optional :date, type: Date + optional :type, type: String + optional :details, type: String + optional :status, type: String + optional :costs, type: Float + optional :time, type: String + optional :changes, type: String + end + optional :consumables_needed_for_maintenance, type: Array do + optional :name, type: String + optional :type, type: String + optional :number, type: Integer + optional :status, type: String + optional :costs, type: Float + optional :details, type: String + end + optional :unexpected_maintenance, type: Array do + optional :date, type: Date + optional :type, type: String + optional :details, type: String + optional :status, type: String + optional :costs, type: Float + optional :time, type: String + optional :changes, type: String + end + optional :measures_after_full_shut_down, type: String + optional :measures_after_short_shut_down, type: String + optional :measures_to_plan_offline_period, type: String + optional :restart_after_planned_offline_period, type: String + end + + def device_description_with_entity(device_description) + @element_policy = ElementPolicy.new(current_user, device_description) + present( + device_description, + with: Entities::DeviceDescriptionEntity, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: device_description) + .detail_levels, + policy: @element_policy, + root: :device_description, + ) + end + end + + resource :device_descriptions do + # Return serialized device description by collection id + params do + optional :collection_id, type: Integer + optional :sync_collection_id, type: Integer + optional :filter_created_at, type: Boolean, desc: 'filter by created at or updated at' + optional :from_date, type: Integer, desc: 'created_date from in ms' + optional :to_date, type: Integer, desc: 'created_date to in ms' + end + paginate per_page: 5, offset: 0 + before do + params[:per_page].to_i > 50 && (params[:per_page] = 50) + end + get do + scope = + if params[:collection_id] + begin + Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids) + .find(params[:collection_id]).device_descriptions + rescue ActiveRecord::RecordNotFound + DeviceDescription.none + end + elsif params[:sync_collection_id] + begin + current_user.all_sync_in_collections_users.find(params[:sync_collection_id]) + .collection.device_descriptions + rescue ActiveRecord::RecordNotFound + DeviceDescription.none + end + else + # All collection of current_user + DeviceDescription.joins(:collections) + .where(collections: { user_id: current_user.id }).distinct + end + scope.order('created_at DESC') + + from = params[:from_date] + to = params[:to_date] + by_created_at = params[:filter_created_at] || false + + scope = scope.includes_for_list_display + scope = scope.created_time_from(Time.zone.at(from)) if from && by_created_at + scope = scope.created_time_to(Time.zone.at(to) + 1.day) if to && by_created_at + scope = scope.updated_time_from(Time.zone.at(from)) if from && !by_created_at + scope = scope.updated_time_to(Time.zone.at(to) + 1.day) if to && !by_created_at + + reset_pagination_page(scope) + + device_descriptions = paginate(scope).map do |device_description| + Entities::DeviceDescriptionEntity.represent( + device_description, + displayed_in_list: true, + detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: device_description) + .detail_levels, + ) + end + + { device_descriptions: device_descriptions } + end + + # create a device description + params do + use :default_params + use :create_params + end + post do + attributes = declared(params.except(:container), include_missing: false) + attributes[:created_by] = current_user.id + device_description = Usecases::DeviceDescriptions::Create.new(attributes, current_user).execute + device_description.container = update_datamodel(params[:container]) if params[:container].present? + device_description_with_entity(device_description) + rescue ActiveRecord::RecordInvalid + { errors: device_description.errors.messages } + end + + # get segment klass ids by new ontology + namespace :byontology do + params do + requires :id, type: Integer + requires :ontology, type: Hash do + optional :data, type: Hash + optional :paths, type: Array + end + end + put ':id' do + device_description = DeviceDescription.find(params[:id]) + attributes = declared(params, include_missing: false) + segment_klass_ids = + Usecases::DeviceDescriptions::Update.new(attributes, device_description, current_user) + .segment_klass_ids_by_new_ontology + segment_klass_ids + end + end + + # get device descriptions by UI state + namespace :ui_state do + params do + requires :ui_state, type: Hash, desc: 'Selected device descriptions from the UI' do + optional :all, type: Boolean + optional :included_ids, type: Array + optional :excluded_ids, type: Array + optional :from_date, type: Date + optional :to_date, type: Date + optional :collection_id, type: Integer + optional :is_sync_to_me, type: Boolean, default: false + end + optional :limit, type: Integer, desc: 'Limit number of device descriptions' + end + + before do + cid = fetch_collection_id_w_current_user(params[:ui_state][:collection_id], params[:ui_state][:is_sync_to_me]) + @device_descriptions = + DeviceDescription.by_collection_id(cid).by_ui_state(params[:ui_state]).for_user(current_user.id) + error!('401 Unauthorized', 401) unless ElementsPolicy.new(current_user, @device_descriptions).read? + end + + post do + @device_descriptions = @device_descriptions.limit(params[:limit]) if params[:limit] + + present @device_descriptions, with: Entities::DeviceDescriptionEntity, root: :device_descriptions + end + end + + # split device description into sub device description + namespace :sub_device_descriptions do + params do + requires :ui_state, type: Hash, desc: 'Selected device descriptions from the UI' + end + post do + ui_state = params[:ui_state] + col_id = ui_state[:currentCollectionId] + element_params = ui_state[:device_description] + device_description_ids = + DeviceDescription.for_user(current_user.id) + .for_ui_state_with_collection(element_params, CollectionsDeviceDescription, col_id) + DeviceDescription.where(id: device_description_ids).find_each do |device_description| + device_description.create_sub_device_description(current_user, col_id) + end + + {} # JS layer does not use the reply + end + end + + # return serialized device description by id + params do + requires :id, type: Integer + end + route_param :id do + get do + device_description = DeviceDescription.find(params[:id]) + error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, device_description).read? + + device_description_with_entity(device_description) + end + end + + # update a device description + params do + use :update_params + use :default_params + end + put ':id' do + device_description = DeviceDescription.find(params[:id]) + error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, device_description).update? + attributes = declared(params.except(:container), include_missing: false) + device_description = + Usecases::DeviceDescriptions::Update.new(attributes, device_description, current_user).execute + device_description.container = update_datamodel(params[:container]) if params[:container].present? + device_description_with_entity(device_description) + rescue ActiveRecord::RecordInvalid + { errors: device_description.errors.messages } + end + + # delete a device description + delete ':id' do + device_description = DeviceDescription.find(params[:id]) + + error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, device_description).destroy? + error!('Device could not be deleted', 400) unless device_description.present? && device_description.destroy + + { deleted: device_description.id } + end + end + end +end +# rubocop:enable Metrics/ClassLength diff --git a/app/api/chemotion/element_api.rb b/app/api/chemotion/element_api.rb index a5fa586371..8868fbcc57 100644 --- a/app/api/chemotion/element_api.rb +++ b/app/api/chemotion/element_api.rb @@ -38,6 +38,9 @@ class ElementAPI < Grape::API optional :cell_line, type: Hash do use :ui_state_params end + optional :device_description, type: Hash do + use :ui_state_params + end optional :selecteds, desc: 'Elements currently opened in detail tabs', type: Array do optional :type, type: String optional :id, type: Integer @@ -77,7 +80,7 @@ class ElementAPI < Grape::API desc "delete element from ui state selection." delete do deleted = { 'sample' => [] } - %w[sample reaction wellplate screen research_plan cell_line].each do |element| + %w[sample reaction wellplate screen research_plan cell_line device_description].each do |element| next unless params[element] next unless params[element][:checkedAll] || params[element][:checkedIds].present? diff --git a/app/api/chemotion/literature_api.rb b/app/api/chemotion/literature_api.rb index ebeff5daf0..6839f7f6e4 100644 --- a/app/api/chemotion/literature_api.rb +++ b/app/api/chemotion/literature_api.rb @@ -62,7 +62,7 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = desc 'Return the literature list for the given element' params do requires :element_id, type: Integer - requires :element_type, type: String, values: %w[sample reaction research_plan cell_line] + requires :element_type, type: String, values: %w[sample reaction research_plan cell_line device_description] end get do @@ -78,7 +78,7 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = desc 'create a literature entry' params do requires :element_id, type: Integer - requires :element_type, type: String, values: %w[sample reaction research_plan cell_line] + requires :element_type, type: String, values: %w[sample reaction research_plan cell_line device_description] requires :ref, type: Hash do optional :is_new, type: Boolean optional :id, types: [Integer, String] @@ -131,7 +131,7 @@ def citation_for_elements(id = params[:element_id], type = @element_klass, cat = params do requires :element_id, type: Integer - requires :element_type, type: String, values: %w[sample reaction research_plan cell_line] + requires :element_type, type: String, values: %w[sample reaction research_plan cell_line device_description] requires :id, type: Integer end diff --git a/app/api/chemotion/profile_api.rb b/app/api/chemotion/profile_api.rb index a3d905d685..06d2009187 100644 --- a/app/api/chemotion/profile_api.rb +++ b/app/api/chemotion/profile_api.rb @@ -16,6 +16,7 @@ def validate_param!(attr_name, params) end end + # rubocop: disable Metrics/ClassLength class ProfileAPI < Grape::API resource :profiles do desc 'Return the profile of the current_user' @@ -97,6 +98,7 @@ class ProfileAPI < Grape::API optional :layout_detail_sample, type: Hash, profile_layout_hash: true optional :layout_detail_wellplate, type: Hash, profile_layout_hash: true optional :layout_detail_screen, type: Hash, profile_layout_hash: true + optional :layout_detail_device_description, type: Hash, profile_layout_hash: true optional :export_selection, type: Hash do optional :sample, type: [Boolean] optional :reaction, type: [Boolean] @@ -134,6 +136,7 @@ class ProfileAPI < Grape::API 'screen' => 4, 'research_plan' => 5, 'cell_line' => -1000, + 'device_description' => -1100, } if data['layout'].nil? layout = data['layout'].select { |e| available_ements.include?(e) } @@ -252,5 +255,6 @@ class ProfileAPI < Grape::API end end end + # rubocop: enable Metrics/ClassLength end # rubocop: enable Style/MultilineIfModifier diff --git a/app/api/chemotion/text_template_api.rb b/app/api/chemotion/text_template_api.rb index 085ea5a6b1..0c5b3a62be 100644 --- a/app/api/chemotion/text_template_api.rb +++ b/app/api/chemotion/text_template_api.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Chemotion class TextTemplateAPI < Grape::API resource :text_templates do diff --git a/app/api/entities/attachment_entity.rb b/app/api/entities/attachment_entity.rb index 7166d02dc2..b3c1ca24d1 100644 --- a/app/api/entities/attachment_entity.rb +++ b/app/api/entities/attachment_entity.rb @@ -3,7 +3,11 @@ module Entities class AttachmentEntity < ApplicationEntity expose :id, documentation: { type: 'Integer', desc: "Attachment's unique id" } - expose :filename, :identifier, :content_type, :thumb, :aasm_state, :filesize + expose :filename, :identifier, :content_type, :thumb, :aasm_state, :filesize, :thumbnail expose_timestamps + + def thumbnail + object.thumb ? Base64.encode64(object.read_thumbnail) : nil + end end end diff --git a/app/api/entities/device_description_entity.rb b/app/api/entities/device_description_entity.rb new file mode 100644 index 0000000000..9973cdae91 --- /dev/null +++ b/app/api/entities/device_description_entity.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Entities + class DeviceDescriptionEntity < ApplicationEntity + expose :id + expose :device_id + expose :name + expose :short_label + expose :device_type + expose :device_type_detail + expose :operation_mode + expose :vendor_device_name + expose :vendor_device_id + expose :serial_number + expose :vendor_company_name + expose :vendor_id + expose :description + expose :general_tags + expose :version_number + expose :version_installation_start_date + expose :version_installation_end_date + expose :version_identifier_type + expose :version_doi + expose :version_doi_url + expose :version_characterization + expose :operators + expose :university_campus + expose :institute + expose :building + expose :room + expose :infrastructure_assignment + expose :access_options + expose :access_comments + expose :size + expose :weight + expose :weight_unit + expose :application_name + expose :application_version + expose :vendor_url + expose :helpers_uploaded + expose :policies_and_user_information + expose :description_for_methods_part + expose :type + expose :changed + expose :container, using: 'Entities::ContainerEntity' + expose :attachments, using: 'Entities::AttachmentEntity' + expose :ontologies + expose :segments, using: 'Labimotion::SegmentEntity' + expose :comments, using: 'Entities::CommentEntity' + expose :comment_count + expose :tag, using: 'Entities::ElementTagEntity' + expose! :can_copy, unless: :displayed_in_list + expose! :ancestor_ids + expose :setup_descriptions + expose :maintenance_contract_available + expose :maintenance_scheduling + expose :contact_for_maintenance + expose :planned_maintenance + expose :consumables_needed_for_maintenance + expose :unexpected_maintenance + expose :measures_after_full_shut_down + expose :measures_after_short_shut_down + expose :measures_to_plan_offline_period + expose :restart_after_planned_offline_period + + expose_timestamps + + private + + def type + 'device_description' + end + + def changed + false + end + + def comment_count + object.comments.count + end + end +end diff --git a/app/api/entities/user_entity.rb b/app/api/entities/user_entity.rb index 3a15ef41b4..cf1cbc3b46 100644 --- a/app/api/entities/user_entity.rb +++ b/app/api/entities/user_entity.rb @@ -10,6 +10,7 @@ class UserEntity < Grape::Entity expose :samples_count, documentation: { type: 'Integer', desc: 'Sample count' } expose :reactions_count, documentation: { type: 'Integer', desc: 'Reactions count' } expose :cell_lines_count, documentation: { type: 'Integer', desc: 'Cellline Samples count' } + expose :device_descriptions_count, documentation: { type: 'Integer', desc: 'Device Descriptions count' } expose :type, if: ->(obj, _opts) { obj.respond_to? :type } expose :reaction_name_prefix, if: ->(obj, _opts) { obj.respond_to? :reaction_name_prefix } expose :layout, if: ->(obj, _opts) { obj.respond_to? :layout } @@ -38,6 +39,10 @@ def cell_lines_count object.counters['celllines'].to_i end + def device_descriptions_count + object.counters['device_descriptions'].to_i + end + expose :current_sign_in_at do |obj| return nil unless obj.respond_to? :current_sign_in_at diff --git a/app/api/helpers/collection_helpers.rb b/app/api/helpers/collection_helpers.rb index cd840ef6fd..d849ae9830 100644 --- a/app/api/helpers/collection_helpers.rb +++ b/app/api/helpers/collection_helpers.rb @@ -38,7 +38,8 @@ def detail_level_for_collection(id, is_sync = false) :sample_detail_level, :reaction_detail_level, :wellplate_detail_level, :screen_detail_level, :researchplan_detail_level, :element_detail_level, - :celllinesample_detail_level + :celllinesample_detail_level, + :devicedescription_detail_level )&.symbolize_keys { permission_level: 0, @@ -49,6 +50,7 @@ def detail_level_for_collection(id, is_sync = false) researchplan_detail_level: 0, element_detail_level: 0, celllinesample_detail_level: 0, + devicedescription_detail_level: 0, }.merge(dl || {}) end @@ -123,6 +125,7 @@ def set_var(c_id = params[:collection_id], is_sync = params[:is_sync]) researchplan_detail_level: 10, element_detail_level: 10, celllinesample_detail_level: 10, + devicedescription_detail_level: 10, } @dl = detail_level_for_collection(c_id, is_sync) unless @is_owned @@ -134,6 +137,7 @@ def set_var(c_id = params[:collection_id], is_sync = params[:is_sync]) @dl_rp = @dl[:researchplan_detail_level] @dl_e = @dl[:element_detail_level] @dl_cl = @dl[:celllinesample_detail_level] + @dl_dd = @dl[:devicedescription_detail_level] end end # rubocop:enable Metrics/ModuleLength, Style/OptionalBooleanParameter, Naming/MethodParameterName, Layout/LineLength diff --git a/app/assets/stylesheets/global-styles/package-mods/react-datepicker.scss b/app/assets/stylesheets/global-styles/package-mods/react-datepicker.scss new file mode 100644 index 0000000000..f6634cd560 --- /dev/null +++ b/app/assets/stylesheets/global-styles/package-mods/react-datepicker.scss @@ -0,0 +1,10 @@ +.react-datepicker-wrapper { + width: 100%; + .react-datepicker__input-container input { + width: 100%; + border-radius: 4px; + box-shadow: inset 0 2px 2px $gray-100; + border: 1px solid $gray-200; + padding: 6px 10px 5px; + } +} diff --git a/app/assets/stylesheets/global-styles/package-mods/react-select.scss b/app/assets/stylesheets/global-styles/package-mods/react-select.scss index f7ab72b564..84eb0cb879 100644 --- a/app/assets/stylesheets/global-styles/package-mods/react-select.scss +++ b/app/assets/stylesheets/global-styles/package-mods/react-select.scss @@ -82,8 +82,8 @@ } &__value-container { - @extend .flex-nowrap; @extend .text-nowrap; + @extend .gap-1; } &__value-container.chemotion-select__value-container--is-multi { @@ -96,11 +96,6 @@ @extend .m-0; } - &__value-container { - @extend .gap-1; - @extend .d-flex; - } - &__single-value .badge { @extend .d-block } @@ -159,3 +154,65 @@ @include fa-based-remove-icon; } } + +.select-in-inputgroup-text { + &__control { + @extend .d-flex; + @extend .justify-content-between; + @extend .align-items-center; + @extend .gap-1; + min-height: auto !important; + + &:hover { + border-color: $form-select-border-color; + } + + &--menu-is-open { + border-color: color("primary"); + } + + &:focus, &--is-focused { + border-color: $input-focus-border-color; + outline: 0; + @if $enable-shadows { + @include box-shadow($form-select-box-shadow, $form-select-focus-box-shadow); + } @else { + // Avoid using mixin so we can pass custom focus shadow properly + box-shadow: $form-select-focus-box-shadow; + } + } + } + + &__menu { + padding: $form-select-padding-y 0; + box-shadow: none; + z-index: 10; + } + + &__option { + padding: $form-select-padding-y $form-select-padding-x; + + &--is-selected, &--is-selected.chemotion-select__option--is-focused { + background-color: color("primary"); + } + + &--is-focused { + background-color: $input-focus-border-color; + } + } +} + +.select-in-inputgroup-text__control { + min-height: 30px !important; +} + +.select-in-inputgroup-text__menu-list { + @extend .bg-white; +} + +.select-in-inputgroup-text__option, .select-in-inputgroup-text__menu-list { + width: 160px; + white-space: normal; + text-align: left; + overflow-x: hidden; +} diff --git a/app/assets/stylesheets/global-styles/utilities.scss b/app/assets/stylesheets/global-styles/utilities.scss index 9db265ab08..802efaf513 100644 --- a/app/assets/stylesheets/global-styles/utilities.scss +++ b/app/assets/stylesheets/global-styles/utilities.scss @@ -5,4 +5,5 @@ @import "utilities/detail-card"; @import "utilities/tabs-container-with-borders"; @import "utilities/modal"; +@import "utilities/form"; @import "utilities/draggable-modal"; diff --git a/app/assets/stylesheets/global-styles/utilities/detail-card.scss b/app/assets/stylesheets/global-styles/utilities/detail-card.scss index 2fb648420e..a432929754 100644 --- a/app/assets/stylesheets/global-styles/utilities/detail-card.scss +++ b/app/assets/stylesheets/global-styles/utilities/detail-card.scss @@ -2,7 +2,7 @@ > .card-header { @extend .text-bg-primary; } - &.detail-card--unsaved .card-header { + &.detail-card--unsaved > .card-header { @extend .text-bg-info; } } diff --git a/app/assets/stylesheets/global-styles/utilities/dnd.scss b/app/assets/stylesheets/global-styles/utilities/dnd.scss index 89d810caed..80aa893dd9 100644 --- a/app/assets/stylesheets/global-styles/utilities/dnd.scss +++ b/app/assets/stylesheets/global-styles/utilities/dnd.scss @@ -7,6 +7,15 @@ @extend .text-center; } +.dnd-zone-list-item { + @extend .w-100; + @extend .border; + @extend .border-dashed; + @extend .border-3; + @extend .p-3; + @extend .text-start; +} + .dnd-zone-over { @extend .border-primary; } diff --git a/app/assets/stylesheets/global-styles/utilities/form.scss b/app/assets/stylesheets/global-styles/utilities/form.scss new file mode 100644 index 0000000000..3f756b1f4c --- /dev/null +++ b/app/assets/stylesheets/global-styles/utilities/form.scss @@ -0,0 +1,20 @@ +.multiple-row-fields { + @extend .d-flex; + @extend .justify-content-between; + @extend .align-items-end; + @extend .flex-nowrap; + @extend .gap-2; + @extend .mb-4; +} + +.multiple-row-fields.components:last-child { + @extend .mb-3; +} + +.multiple-row-fields > div { + flex: 1; +} + +.multiple-row-fields .dnd-zone + button { + @extend .my-auto; +} diff --git a/app/assets/stylesheets/global-styles/vendor.scss b/app/assets/stylesheets/global-styles/vendor.scss index 3ad5496d67..62d278a08a 100644 --- a/app/assets/stylesheets/global-styles/vendor.scss +++ b/app/assets/stylesheets/global-styles/vendor.scss @@ -18,3 +18,4 @@ @import "package-mods/react-svg-file-zoom-pan"; @import "package-mods/react-select"; @import "package-mods/ag-grid"; +@import "package-mods/react-datepicker"; diff --git a/app/assets/stylesheets/legacy/miscellaneous.scss b/app/assets/stylesheets/legacy/miscellaneous.scss index dd80a210fc..bce032fa97 100644 --- a/app/assets/stylesheets/legacy/miscellaneous.scss +++ b/app/assets/stylesheets/legacy/miscellaneous.scss @@ -104,11 +104,22 @@ $font-icons-research_plan: "\f0f6"; .icon-cell_line { font-style: inherit; } + .icon-cell_line:before { font-family: FontAwesome; content: "\f0a3"; } +.icon-device_description { + font-style: inherit; +} + +.icon-device_description:before { + font-family: FontAwesome; + content: "\f1b2"; +} + + .importChemDrawModal { width: 98%; height: 97%; diff --git a/app/javascript/src/apps/generic/Utils.js b/app/javascript/src/apps/generic/Utils.js index 195325f497..56a706a870 100644 --- a/app/javascript/src/apps/generic/Utils.js +++ b/app/javascript/src/apps/generic/Utils.js @@ -93,7 +93,7 @@ export const segmentsByKlass = name => { export const elementNames = async (all = true, generics = null) => { const elnElements = all - ? ['sample', 'reaction', 'screen', 'wellplate', 'research_plan', 'cell_line'] + ? ['sample', 'reaction', 'screen', 'wellplate', 'research_plan', 'cell_line', 'device_description'] : []; try { if (generics?.length > 0) return elnElements.concat(generics?.map((el) => el.name)); diff --git a/app/javascript/src/apps/mydb/collections/CollectionTabs.js b/app/javascript/src/apps/mydb/collections/CollectionTabs.js index c37ced4e19..6c6920dd6a 100644 --- a/app/javascript/src/apps/mydb/collections/CollectionTabs.js +++ b/app/javascript/src/apps/mydb/collections/CollectionTabs.js @@ -16,7 +16,8 @@ const elements = [ { name: 'reaction', label: 'Reaction' }, { name: 'wellplate', label: 'Wellplate' }, { name: 'screen', label: 'Screen' }, - { name: 'research_plan', label: 'Research Plan' } + { name: 'research_plan', label: 'Research Plan' }, + { name: 'device_description', label: 'Device description' } ]; export default class CollectionTabs extends React.Component { diff --git a/app/javascript/src/apps/mydb/elements/Elements.js b/app/javascript/src/apps/mydb/elements/Elements.js index 9f9550d53d..1d41d6d522 100644 --- a/app/javascript/src/apps/mydb/elements/Elements.js +++ b/app/javascript/src/apps/mydb/elements/Elements.js @@ -4,8 +4,11 @@ import { Row, Col } from 'react-bootstrap'; import ElementsList from 'src/apps/mydb/elements/list/ElementsList'; import ElementDetails from 'src/apps/mydb/elements/details/ElementDetails'; import ElementStore from 'src/stores/alt/stores/ElementStore'; +import { StoreContext } from 'src/stores/mobx/RootStore'; export default class Elements extends Component { + static contextType = StoreContext; + constructor(props) { super(props); this.state = { @@ -25,6 +28,9 @@ export default class Elements extends Component { handleOnChange(state) { const { currentElement } = state; + if (currentElement && currentElement.type == 'device_description') { + this.context.deviceDescriptions.setDeviceDescription(currentElement, true); + } this.setState({ currentElement }); } diff --git a/app/javascript/src/apps/mydb/elements/details/ElementDetails.js b/app/javascript/src/apps/mydb/elements/details/ElementDetails.js index 69335c1b05..91cd9d7d10 100644 --- a/app/javascript/src/apps/mydb/elements/details/ElementDetails.js +++ b/app/javascript/src/apps/mydb/elements/details/ElementDetails.js @@ -11,6 +11,7 @@ import React, { Component } from 'react'; import ReactionDetails from 'src/apps/mydb/elements/details/reactions/ReactionDetails'; import ReportContainer from 'src/apps/mydb/elements/details/reports/ReportContainer'; import ResearchPlanDetails from 'src/apps/mydb/elements/details/researchPlans/ResearchPlanDetails'; +import DeviceDescriptionDetails from 'src/apps/mydb/elements/details/deviceDescriptions/DeviceDescriptionDetails'; import SampleDetails from 'src/apps/mydb/elements/details/samples/SampleDetails'; import ScreenDetails from 'src/apps/mydb/elements/details/screens/ScreenDetails'; import UserStore from 'src/stores/alt/stores/UserStore'; @@ -175,6 +176,12 @@ export default class ElementDetails extends Component { toggleFullScreen={this.toggleFullScreen} /> ); + case 'device_description': + return ( + + ); case 'metadata': return ; case 'report': diff --git a/app/javascript/src/apps/mydb/elements/details/deviceDescriptions/DeviceDescriptionDetails.js b/app/javascript/src/apps/mydb/elements/details/deviceDescriptions/DeviceDescriptionDetails.js new file mode 100644 index 0000000000..f28ff2b896 --- /dev/null +++ b/app/javascript/src/apps/mydb/elements/details/deviceDescriptions/DeviceDescriptionDetails.js @@ -0,0 +1,245 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { + ButtonToolbar, Button, Tabs, Tab, Tooltip, OverlayTrigger, Card +} from 'react-bootstrap'; + +import PropertiesForm from './propertiesTab/PropertiesForm'; +import DetailsForm from './detailsTab/DetailsForm'; +import AnalysesContainer from './analysesTab/AnalysesContainer'; +import AttachmentForm from './attachmentsTab/AttachmentForm'; +import MaintenanceForm from './maintenanceTab/MaintenanceForm'; + +import ElementCollectionLabels from 'src/apps/mydb/elements/labels/ElementCollectionLabels'; +import HeaderCommentSection from 'src/components/comments/HeaderCommentSection'; +import CommentSection from 'src/components/comments/CommentSection'; +import CommentActions from 'src/stores/alt/actions/CommentActions'; +import CommentModal from 'src/components/common/CommentModal'; +import { commentActivation } from 'src/utilities/CommentHelper'; +import MatrixCheck from 'src/components/common/MatrixCheck'; +import ConfirmClose from 'src/components/common/ConfirmClose'; +import PrintCodeButton from 'src/components/common/PrintCodeButton'; +import ElementDetailSortTab from 'src/apps/mydb/elements/details/ElementDetailSortTab'; +import OpenCalendarButton from 'src/components/calendar/OpenCalendarButton'; +import CopyElementModal from 'src/components/common/CopyElementModal'; +import Immutable from 'immutable'; +import { formatTimeStampsOfElement } from 'src/utilities/timezoneHelper'; + +import AttachmentFetcher from 'src/fetchers/AttachmentFetcher'; + +import { observer } from 'mobx-react'; +import { StoreContext } from 'src/stores/mobx/RootStore'; +import ElementActions from 'src/stores/alt/actions/ElementActions'; +import DetailActions from 'src/stores/alt/actions/DetailActions'; +import LoadingActions from 'src/stores/alt/actions/LoadingActions'; +import UIStore from 'src/stores/alt/stores/UIStore'; +import UserStore from 'src/stores/alt/stores/UserStore'; +import CollectionUtils from 'src/models/collection/CollectionUtils'; + +const DeviceDescriptionDetails = ({ toggleFullScreen }) => { + const deviceDescriptionsStore = useContext(StoreContext).deviceDescriptions; + let deviceDescription = deviceDescriptionsStore.device_description; + deviceDescriptionsStore.setKeyPrefix('deviceDescription'); + + const { currentCollection, isSync } = UIStore.getState(); + const { currentUser } = UserStore.getState(); + + const [activeTab, setActiveTab] = useState('properties'); // state from store + const [visibleTabs, setVisibleTabs] = useState(Immutable.List()); + + const submitLabel = deviceDescription.isNew ? 'Create' : 'Save'; + let tabContents = []; + + useEffect(() => { + if (MatrixCheck(currentUser.matrix, commentActivation) && !deviceDescription.isNew) { + CommentActions.fetchComments(deviceDescription); + } + }, []); + + const tabContentComponents = { + properties: PropertiesForm, + detail: DetailsForm, + analyses: AnalysesContainer, + attachments: AttachmentForm, + maintenance: MaintenanceForm, + }; + + const tabTitles = { + properties: 'Properties', + detail: 'Details', + analyses: 'Analyses', + attachments: 'Attachment', + maintenance: 'Maintenance', + }; + + const isReadOnly = () => { + return CollectionUtils.isReadOnly( + currentCollection, + currentUser.id, + isSync + ); + } + + const disabled = (index) => { + return deviceDescription.id.toString().length < 30 || index === 0 ? false : true; + } + + visibleTabs.forEach((key, i) => { + tabContents.push( + + { + !deviceDescription.isNew && + + } + {React.createElement(tabContentComponents[key], { + key: `${deviceDescription.id}-${key}`, + readonly: isReadOnly() + })} + + ); + }); + + const onTabPositionChanged = (visible) => { + setVisibleTabs(visible); + } + + const handleTabChange = (key) => { + setActiveTab(key); + } + + const handleSubmit = () => { + LoadingActions.start(); + if (deviceDescription.is_new) { + DetailActions.close(deviceDescription, true); + ElementActions.createDeviceDescription(deviceDescription); + } else { + ElementActions.updateDeviceDescription(deviceDescription); + } + } + + const deviceDescriptionIsValid = () => { + // TODO: validation + return true; + } + + const handleExportAnalyses = () => { + deviceDescriptionsStore.toggleAnalysisStartExport(); + AttachmentFetcher.downloadZipByDeviceDescription(deviceDescription.id) + .then(() => { deviceDescriptionsStore.toggleAnalysisStartExport(); }) + .catch((errorMessage) => { console.log(errorMessage); }); + } + + const downloadAnalysisButton = () => { + const hasNoAnalysis = deviceDescription.analyses?.length === 0 || deviceDescription.analyses?.length === undefined; + if (deviceDescription.isNew || hasNoAnalysis) { return null; } + + return ( + + ); + } + + const deviceDescriptionHeader = () => { + const titleTooltip = formatTimeStampsOfElement(deviceDescription || {}); + const defCol = currentCollection && currentCollection.is_shared === false + && currentCollection.is_locked === false && currentCollection.label !== 'All' ? currentCollection.id : null; + + return ( +
+
+ {titleTooltip}}> + + + {deviceDescription.name} + + + + +
+
+ + {!deviceDescription.isNew && + } + FullScreen} + > + + + {deviceDescription.can_copy && !deviceDescription.isNew && ( + + )} + {deviceDescription.isEdited && ( + Save device description} + > + + + )} + +
+
+ ); + } + + return ( + + + {deviceDescriptionHeader()} + + + +
+ handleTabChange(key)} + id="deviceDescriptionDetailsTab" + unmountOnExit + > + {tabContents} + +
+ +
+ + + + + {downloadAnalysisButton()} + + +
+ ); +} + +export default observer(DeviceDescriptionDetails); diff --git a/app/javascript/src/apps/mydb/elements/details/deviceDescriptions/FormFields.js b/app/javascript/src/apps/mydb/elements/details/deviceDescriptions/FormFields.js new file mode 100644 index 0000000000..09ef70c35a --- /dev/null +++ b/app/javascript/src/apps/mydb/elements/details/deviceDescriptions/FormFields.js @@ -0,0 +1,751 @@ +import React from 'react'; +import { + InputGroup, OverlayTrigger, Tooltip, Button, Form, +} from 'react-bootstrap'; +import { Select } from 'src/components/common/Select'; +import DatePicker from 'react-datepicker'; +import { useDrop } from 'react-dnd'; +import { DragDropItemTypes } from 'src/utilities/DndConst'; +import ChevronIcon from 'src/components/common/ChevronIcon'; +import { handleFloatNumbers } from 'src/utilities/UnitsConversion'; +import moment from 'moment'; +import { v4 as uuid } from 'uuid'; + +import { elementShowOrNew } from 'src/utilities/routesUtils'; +import UIStore from 'src/stores/alt/stores/UIStore'; + +const valueByType = (type, event) => { + let value = []; + switch (type) { + case 'text': + case 'textarea': + case 'textWithAddOn': + case 'system-defined': + case 'formula-field': + case 'subGroupWithAddOn': + case 'numeric': + return event.target.value; + case 'checkbox': + return event.target.checked; + case 'select': + return event.value ? event.value : event.label; + case 'multiselect': + event.forEach((element) => { + element?.value ? value.push(element.value) : value.push(element) + }); + return value; + case 'datetime': + return moment(event, 'YYYY-MM-DD HH:mm:ss').toISOString(); + case 'date': + return moment(event, 'YYYY-MM-DD').toISOString(); + case 'time': + return moment(event, 'HH:mm').toISOString(); + default: + return event; + } +} + +const fieldByType = (option, field, fields, element, store, info) => { + switch (option.type) { + case 'text': + fields.push(textInput(element, store, field, option.label, info)); + break; + case 'textarea': + fields.push(textareaInput(element, store, field, option.label, option.rows, info)); + break; + case 'checkbox': + fields.push(checkboxInput(element, option.label, field, store)); + break; + case 'select': + fields.push(selectInput(element, store, field, option.label, option.options, info)); + break; + case 'numeric': + fields.push(numericInput(element, store, field, option.label, option.type, info)); + break; + case 'time': + fields.push(timePickerInput(element, store, field, option.label, info)); + break; + case 'date': + fields.push(datePickerInput(element, store, field, option.label, info)) + break; + } + return fields; +} + +const handleFieldChanged = (store, field, type, element_type) => (event) => { + let value = event === null ? '' : valueByType(type, event); + + if (element_type == 'device_description') { + store.changeDeviceDescription(field, value); + } +} + +const weightConversion = (value, multiplier) => value * multiplier; + +const weightConversionMap = { + t: { convertedUnit: 'kg', conversionFactor: 1000 }, + kg: { convertedUnit: 'g', conversionFactor: 1000 }, + g: { convertedUnit: 't', conversionFactor: 0.000001 }, +}; + +const convertByUnit = (valueToFormat, currentUnit) => { + const { convertedUnit, conversionFactor } = weightConversionMap[currentUnit]; + const decimalPlaces = 7; + const formattedValue = weightConversion(valueToFormat, conversionFactor); + const convertedValue = handleFloatNumbers(formattedValue, decimalPlaces); + return [convertedValue, convertedUnit]; +}; + +const changeWeightUnit = (element, store, field, field_unit, element_type) => { + const oldValue = element[field]; + const oldUnitValue = element[field_unit] || 'kg'; + const [newValue, newUnitValue] = convertByUnit(oldValue, oldUnitValue); + + if (element_type == 'device_description') { + if (newValue !== 0) { + store.changeDeviceDescription(field, newValue); + } + store.changeDeviceDescription(field_unit, newUnitValue); + } +} + +const toggleContent = (store, content) => { + store.toggleContent(content); +} + +const allowedAnnotationFileTypes = ['png', 'jpg', 'bmp', 'tif', 'svg', 'jpeg', 'tiff']; + +const annotationButton = (store, attachment) => { + if (!attachment || !attachment.filename) { return null; } + + const extension = attachment.filename.split('.').pop(); + const isAllowedFileType = allowedAnnotationFileTypes.includes(extension); + const isActive = isAllowedFileType && !attachment.isNew; + const className = !isAllowedFileType ? 'attachment-gray-button' : ''; + const tooltipText = isActive + ? 'Annotate image' + : 'Cannot annotate - invalid file type or the image is new'; + + return ( + {tooltipText}} + > + +