diff --git a/app/api/chemotion/reaction_api.rb b/app/api/chemotion/reaction_api.rb index da6a7096e3..5c70814359 100644 --- a/app/api/chemotion/reaction_api.rb +++ b/app/api/chemotion/reaction_api.rb @@ -91,6 +91,7 @@ class ReactionAPI < Grape::API get do reaction = Reaction.find(params[:id]) + class_name = reaction&.class&.name { reaction: Entities::ReactionEntity.represent( @@ -98,7 +99,7 @@ class ReactionAPI < Grape::API policy: @element_policy, detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: reaction).detail_levels, ), - literatures: Entities::LiteratureEntity.represent(citation_for_elements(params[:id], 'Reaction')), + literatures: Entities::LiteratureEntity.represent(citation_for_elements(params[:id], class_name)), } end end diff --git a/app/api/chemotion/research_plan_api.rb b/app/api/chemotion/research_plan_api.rb index 9609f0b592..740482bb3f 100644 --- a/app/api/chemotion/research_plan_api.rb +++ b/app/api/chemotion/research_plan_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Chemotion # rubocop: disable Metrics/ClassLength @@ -20,8 +22,8 @@ class ResearchPlanAPI < Grape::API 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]).research_plans + Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids) + .find(params[:collection_id]).research_plans rescue ActiveRecord::RecordNotFound ResearchPlan.none end @@ -138,6 +140,17 @@ class ResearchPlanAPI < Grape::API end end + desc 'Return element linked to research plan' + params do + requires :id, type: Integer, desc: 'Research plan id' + requires :element, type: String, desc: 'Sample or Reaction' + end + + get 'linked' do + type = "#{params[:element]}_id" + ResearchPlan.where('body @> ?', [{ value: { type => params[:id] } }].to_json).select(:id, :name) + end + desc 'Return serialized research plan by id' params do requires :id, type: Integer, desc: 'Research plan id' diff --git a/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js b/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js index 307dd2c504..58dc6edb61 100644 --- a/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js +++ b/app/packs/src/apps/mydb/elements/details/reactions/ReactionDetails.js @@ -7,6 +7,7 @@ import { import SvgFileZoomPan from 'react-svg-file-zoom-pan-latest'; import { findIndex } from 'lodash'; import ElementCollectionLabels from 'src/apps/mydb/elements/labels/ElementCollectionLabels'; +import ElementResearchPlanLabels from 'src/apps/mydb/elements/labels/ElementResearchPlanLabels'; import ElementAnalysesLabels from 'src/apps/mydb/elements/labels/ElementAnalysesLabels'; import ElementActions from 'src/stores/alt/actions/ElementActions'; import DetailActions from 'src/stores/alt/actions/DetailActions'; @@ -279,6 +280,10 @@ export default class ReactionDetails extends Component { ); + const rsPlanLabel = (reaction.isNew || _.isEmpty(reaction.research_plans)) ? null : ( + + ); + return (
{titleTooltip}}> @@ -344,6 +349,7 @@ export default class ReactionDetails extends Component {
{colLabel} + {rsPlanLabel}
@@ -474,7 +480,6 @@ export default class ReactionDetails extends Component { green_chemistry: 'Green Chemistry' } - addSegmentTabs(reaction, this.handleSegmentsChange, tabContentsMap); const tabContents = []; diff --git a/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsField.js b/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsField.js index 2ce0b7fc90..f63bb6668e 100644 --- a/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsField.js +++ b/app/packs/src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsField.js @@ -10,7 +10,6 @@ import ResearchPlanDetailsFieldImage from 'src/apps/mydb/elements/details/resear import ResearchPlanDetailsFieldTable from 'src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsFieldTable'; import ResearchPlanDetailsFieldSample from 'src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsFieldSample'; import ResearchPlanDetailsFieldReaction from 'src/apps/mydb/elements/details/researchPlans/researchPlanTab/ResearchPlanDetailsFieldReaction'; -import AttachmentFetcher from 'src/fetchers/AttachmentFetcher'; export default class ResearchPlanDetailsField extends Component { render() { diff --git a/app/packs/src/apps/mydb/elements/labels/ElementResearchPlanLabels.js b/app/packs/src/apps/mydb/elements/labels/ElementResearchPlanLabels.js new file mode 100644 index 0000000000..b0a1356202 --- /dev/null +++ b/app/packs/src/apps/mydb/elements/labels/ElementResearchPlanLabels.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { Label, Button, OverlayTrigger, Popover } from 'react-bootstrap'; + +export default class ElementResearchPlanLabels extends React.Component { + constructor(props) { + + super(props); + this.state = { + research_plans: props.plans + }; + + this.handleOnClick = this.handleOnClick.bind(this); + this.preventOnClick = this.preventOnClick.bind(this); + } + + handleOnClick(label, e) { + e.stopPropagation(); + + let url = "/research_plan/" + label.id; + Aviator.navigate(url); + } + + preventOnClick(e) { + e.stopPropagation(); + } + + formatLabels(labels) { + return labels.map((label, index) => { + return ( + + +   + + ); + }); + } + + renderCollectionsLabels(research_plans) { + if (research_plans == undefined) return ; + + return ( +
+

Research Plans

+
+ {this.formatLabels(research_plans)} +
+
+ ); + } + + render() { + const { research_plans } = this.state; + + let placement = 'right'; + let researchPlanOverlay = ( + + {this.renderCollectionsLabels(research_plans)} + + ); + + return ( +
+ + + + + +
+ ); + } +} diff --git a/app/packs/src/fetchers/ReactionsFetcher.js b/app/packs/src/fetchers/ReactionsFetcher.js index ac1fb5455f..42b0b3fc2c 100644 --- a/app/packs/src/fetchers/ReactionsFetcher.js +++ b/app/packs/src/fetchers/ReactionsFetcher.js @@ -6,32 +6,43 @@ import Reaction from 'src/models/Reaction'; import AttachmentFetcher from 'src/fetchers/AttachmentFetcher'; import Literature from 'src/models/Literature'; import GenericElsFetcher from 'src/fetchers/GenericElsFetcher'; +import ResearchPlansFetcher from './ResearchPlansFetcher'; // TODO: Extract common base functionality into BaseFetcher export default class ReactionsFetcher { - static fetchById(id) { - return fetch(`/api/v1/reactions/${id}.json`, { - credentials: 'same-origin' - }).then(response => response.json()) - .then((json) => { - if (json.hasOwnProperty("reaction")) { - const reaction = new Reaction(json.reaction); - if (json.literatures && json.literatures.length > 0) { - const tliteratures = json.literatures.map(literature => new Literature(literature)); - const lits = tliteratures.reduce((acc, l) => acc.set(l.literal_id, l), new Immutable.Map()); - reaction.literatures = lits; + static async fetchById(id) { + try { + const response = await fetch(`/api/v1/reactions/${id}.json`, { + credentials: 'same-origin' + }).then(response => response.json()) + .then((json) => { + if (json.hasOwnProperty("reaction")) { + const reaction = new Reaction(json.reaction); + if (json.literatures && json.literatures.length > 0) { + const tliteratures = json.literatures.map(literature => new Literature(literature)); + const lits = tliteratures.reduce((acc, l) => acc.set(l.literal_id, l), new Immutable.Map()); + reaction.literatures = lits; + } + if (json.research_plans && json.research_plans.length > 0) { + reaction.research_plans = json.research_plans; + } + reaction.updateMaxAmountOfProducts(); + return reaction; } - reaction.updateMaxAmountOfProducts(); - return reaction; - } - const rReaction = new Reaction(json.reaction); - if (json.error) { - rReaction.id = `${id}:error:Reaction ${id} is not accessible!`; - } - return rReaction; - }).catch((errorMessage) => { - console.log(errorMessage); - }); + const rReaction = new Reaction(json.reaction); + if (json.error) { + rReaction.id = `${id}:error:Reaction ${id} is not accessible!`; + } + return rReaction; + }).catch((errorMessage) => { + console.log(errorMessage); + }); + const researchPlans = await ResearchPlansFetcher.fetchResearchPlansForElements(id, response.type); + response['research_plans'] = researchPlans; + return response; + } catch (error) { + console.error(error); + } } static fetchByCollectionId(id, queryParams = {}, isSync = false) { @@ -78,10 +89,10 @@ export default class ReactionsFetcher { return Promise.all(tasks).then(() => { return promise(); }); - } - + } + return promise(); - } + } static updateAnnotationsInReaction(reaction){ const tasks=[]; @@ -89,7 +100,7 @@ export default class ReactionsFetcher { return Promise.all(tasks); } - static update(reaction) { + static update(reaction) { return ReactionsFetcher.create(reaction, 'put'); } } diff --git a/app/packs/src/fetchers/ResearchPlansFetcher.js b/app/packs/src/fetchers/ResearchPlansFetcher.js index 768ea17057..752826049b 100644 --- a/app/packs/src/fetchers/ResearchPlansFetcher.js +++ b/app/packs/src/fetchers/ResearchPlansFetcher.js @@ -295,4 +295,17 @@ export default class ResearchPlansFetcher { return Promise.all(updateTasks); } + + static fetchResearchPlansForElements(id, element) { + return fetch(`/api/v1/research_plans/linked?id=${id}&element=${element}`, { + credentials: 'same-origin', + method: 'get', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }).then((response) => response.json()) + .then((json) => json) + .catch((errorMessage) => { console.log(errorMessage); }); + } } diff --git a/app/packs/src/models/Reaction.js b/app/packs/src/models/Reaction.js index 917e44991c..b82e1225aa 100644 --- a/app/packs/src/models/Reaction.js +++ b/app/packs/src/models/Reaction.js @@ -114,6 +114,7 @@ export default class Reaction extends Element { duration: '', durationDisplay: DurationDefault, literatures: {}, + research_plans: {}, name: '', observation: Reaction.quillDefault(), products: [], @@ -175,6 +176,7 @@ export default class Reaction extends Element { durationCalc: this.durationCalc(), id: this.id, literatures: this.literatures, + research_plans: this.research_plans, materials: { starting_materials: this.starting_materials.map(s => s.serializeMaterial()), reactants: this.reactants.map(s => s.serializeMaterial()), @@ -864,10 +866,18 @@ export default class Reaction extends Element { return this._literatures || {}; } + get research_plans() { + return this._research_plans || {}; + } + set literatures(literatures) { this._literatures = literatures; } + set research_plans(research_plans) { + this._research_plans = research_plans; + } + get totalVolume() { let totalVolume = 0.0; const materials = [...this.starting_materials, diff --git a/spec/api/research_plan_api_spec.rb b/spec/api/research_plan_api_spec.rb index f37c410af8..9361870357 100644 --- a/spec/api/research_plan_api_spec.rb +++ b/spec/api/research_plan_api_spec.rb @@ -31,31 +31,6 @@ end end - describe 'GET /api/v1/research_plans' do - let!(:c) { create(:collection, label: 'C1', user: user, is_shared: false) } - let(:rp) { create(:research_plan) } - let!(:research_plan_metadata) { create(:research_plan_metadata) } - - before do - rp.research_plan_metadata = research_plan_metadata - CollectionsResearchPlan.create!(research_plan: rp, collection: c) - end - - it 'returns serialized research_plans of logged in user' do - get '/api/v1/research_plans' - first_rp = JSON.parse(response.body)['research_plans'].first - expect(response.status).to eq 200 - expect(first_rp).to include( - 'type' => 'research_plan', - 'name' => rp.name - ) - expect(first_rp['research_plan_metadata']).to include( - 'id' => research_plan_metadata.id, - 'doi' => research_plan_metadata.doi - ) - end - end - describe 'POST /api/v1/research_plans' do context 'with valid parameters' do let(:params) do @@ -135,5 +110,23 @@ expect(first_row['readout_1_unit']).to eq first_readout['unit'] end end + + describe 'GET /api/v1/research_plans/linked' do + let!(:c) { create(:collection, label: 'C1', user: user) } + let!(:research_plan) { create(:research_plan, :with_linked) } + + before do + get '/api/v1/research_plans/linked', params: { id: 100, element: 'reaction' } + end + + it 'returns 200 status code' do + expect(response.status).to eq 200 + end + + it 'returns research_plans linked to an element' do + response_body = JSON.parse(response.body) + expect(response_body[0]['name']).to eq research_plan.name + end + end end end diff --git a/spec/factories/research_plans.rb b/spec/factories/research_plans.rb index c7f64c6bbc..8e2287277d 100644 --- a/spec/factories/research_plans.rb +++ b/spec/factories/research_plans.rb @@ -7,7 +7,7 @@ { "id"=>SecureRandom.uuid, "type"=>"richtext", "value"=>{ "ops"=>[{ "insert"=>"some text here\n" }] } }, - + ] end @@ -24,6 +24,20 @@ end end + trait :with_linked do + name { 'Linked Research Plan' } + + body do + [ + { + 'id' => SecureRandom.uuid, + 'type' => 'reaction', + 'value' => { 'reaction_id' => 100 }, + }, + ] + end + end + callback(:before_create) do |research_plan| research_plan.creator = FactoryBot.build(:user) unless research_plan.creator end