mirror of
				https://github.com/scinote-eln/scinote-web.git
				synced 2025-11-04 12:07:23 +08:00 
			
		
		
		
	Add step/results linking [SCI-11883][SCI-11884]
This commit is contained in:
		
							parent
							
								
									60ae88f64e
								
							
						
					
					
						commit
						dbf76acc51
					
				
					 14 changed files with 497 additions and 40 deletions
				
			
		| 
						 | 
				
			
			@ -40,6 +40,12 @@ class ResultsController < ApplicationController
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def list
 | 
			
		||||
    @results = @my_module.results.active
 | 
			
		||||
 | 
			
		||||
    update_and_apply_user_sort_preference!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
    result = @my_module.results.create!(user: current_user)
 | 
			
		||||
    log_activity(:add_result, { result: result })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,53 +1,70 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class StepResultsController < ApplicationController
 | 
			
		||||
  before_action :load_step, only: :create
 | 
			
		||||
  before_action :load_result, only: :create
 | 
			
		||||
  before_action :load_step_result, only: :destroy
 | 
			
		||||
  before_action :check_manage_permissions, only: %i(create destroy)
 | 
			
		||||
  before_action :load_steps
 | 
			
		||||
  before_action :load_results
 | 
			
		||||
  before_action :load_step_results
 | 
			
		||||
  before_action :check_manage_permissions
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
  def link_results
 | 
			
		||||
    ActiveRecord::Base.transaction do
 | 
			
		||||
      @step_result = StepResult.create!(step: @step, result: @result, created_by: current_user)
 | 
			
		||||
      render json: { step_result: { id: @step_result.id } }
 | 
			
		||||
      @step_results.where.not(result: @results).destroy_all
 | 
			
		||||
      @results.where.not(id: @step_results.select(:result_id)).each do |result|
 | 
			
		||||
        StepResult.create!(step: @steps.first, result: result, created_by: current_user)
 | 
			
		||||
      end
 | 
			
		||||
      render json: { results: @steps.first.results.map { |r| { id: r.id, name: r.name } } }, status: :created
 | 
			
		||||
    rescue ActiveRecord::RecordInvalid => e
 | 
			
		||||
      Rails.logger.error e.message
 | 
			
		||||
      render json: { errors: @step_result.errors.full_messages }, status: :unprocessable_entity
 | 
			
		||||
      render json: { message: :error }, status: :unprocessable_entity
 | 
			
		||||
      raise ActiveRecord::Rollback
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy
 | 
			
		||||
  def link_steps
 | 
			
		||||
    ActiveRecord::Base.transaction do
 | 
			
		||||
      if @step_result.destroy
 | 
			
		||||
        render json: {}, status: :ok
 | 
			
		||||
      else
 | 
			
		||||
        render json: { errors: @step_result.errors.full_messages }, status: :unprocessable_entity
 | 
			
		||||
      @step_results.where.not(step: @steps).destroy_all
 | 
			
		||||
      @steps.where.not(id: @step_results.select(:step_id)).each do |step|
 | 
			
		||||
        StepResult.create!(step: step, result: @results.first, created_by: current_user)
 | 
			
		||||
      end
 | 
			
		||||
      render json: { steps: @results.first.steps.map { |s| { id: s.id, name: s.name } } }, status: :created
 | 
			
		||||
    rescue ActiveRecord::RecordInvalid => e
 | 
			
		||||
      Rails.logger.error e.message
 | 
			
		||||
      render json: { message: :error }, status: :unprocessable_entity
 | 
			
		||||
      raise ActiveRecord::Rollback
 | 
			
		||||
    end
 | 
			
		||||
    @step_result.destroy!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def load_step
 | 
			
		||||
    @step = Step.find_by(id: params[:step_id])
 | 
			
		||||
    render_404 unless @step
 | 
			
		||||
  def load_steps
 | 
			
		||||
    @steps = Step.where(id: params[:step_ids])
 | 
			
		||||
 | 
			
		||||
    render_404 and return if (action_name == 'link_results') && @steps.size != 1
 | 
			
		||||
 | 
			
		||||
    render_403 and return if @steps.pluck(:protocol_id).uniq.size > 1
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def load_result
 | 
			
		||||
    @result = Result.find_by(id: params[:result_id])
 | 
			
		||||
    render_404 unless @result
 | 
			
		||||
  def load_results
 | 
			
		||||
    @results = Result.where(id: params[:result_ids])
 | 
			
		||||
 | 
			
		||||
    render_404 and return if (action_name == 'link_steps') && @results.size != 1
 | 
			
		||||
 | 
			
		||||
    render_403 and return if @results.pluck(:my_module_id).uniq.size > 1
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def load_step_result
 | 
			
		||||
    @step_result = StepResult.find_by(id: params[:id])
 | 
			
		||||
    render_404 unless @step_result
 | 
			
		||||
 | 
			
		||||
    @step = @step_result.step
 | 
			
		||||
    @result = @step_result.result
 | 
			
		||||
  def load_step_results
 | 
			
		||||
    @step_results = StepResult.where(step: @steps) if action_name == 'link_results'
 | 
			
		||||
    @step_results = StepResult.where(result: @results) if action_name == 'link_steps'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def check_manage_permissions
 | 
			
		||||
    render_403 unless @step.my_module == @result.my_module && can_manage_my_module?(@step.my_module)
 | 
			
		||||
    case action_name
 | 
			
		||||
    when 'link_results'
 | 
			
		||||
      render_403 and return unless can_manage_my_module?(@steps.first.my_module)
 | 
			
		||||
    when 'link_steps'
 | 
			
		||||
      render_403 and return unless can_manage_my_module?(@results.first.my_module)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    render_403 and return unless (@results + @steps).map(&:my_module).uniq.one?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,11 +7,11 @@ class StepsController < ApplicationController
 | 
			
		|||
  before_action :load_vars, only: %i(update destroy show toggle_step_state update_view_state
 | 
			
		||||
                                     update_asset_view_mode elements
 | 
			
		||||
                                     attachments upload_attachment duplicate)
 | 
			
		||||
  before_action :load_vars_nested, only: %i(create index reorder list_protocol_steps add_protocol_steps)
 | 
			
		||||
  before_action :load_vars_nested, only: %i(create index list reorder list_protocol_steps add_protocol_steps)
 | 
			
		||||
  before_action :convert_table_contents_to_utf8, only: %i(create update)
 | 
			
		||||
 | 
			
		||||
  before_action :check_protocol_manage_permissions, only: %i(reorder add_protocol_steps)
 | 
			
		||||
  before_action :check_view_permissions, only: %i(show index attachments elements list_protocol_steps)
 | 
			
		||||
  before_action :check_view_permissions, only: %i(show index list attachments elements list_protocol_steps)
 | 
			
		||||
  before_action :check_create_permissions, only: %i(create)
 | 
			
		||||
  before_action :check_manage_permissions, only: %i(update destroy
 | 
			
		||||
                                                    update_view_state update_asset_view_mode upload_attachment)
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +21,10 @@ class StepsController < ApplicationController
 | 
			
		|||
    render json: @protocol.steps.in_order, each_serializer: StepSerializer, user: current_user
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def list
 | 
			
		||||
    @steps = @protocol.steps.in_order
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def elements
 | 
			
		||||
    render json: @step.step_orderable_elements.order(:position),
 | 
			
		||||
           each_serializer: StepOrderableElementSerializer,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										132
									
								
								app/javascript/vue/protocol/modals/link_results.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								app/javascript/vue/protocol/modals/link_results.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,132 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div ref="modal" class="modal" tabindex="-1" role="dialog">
 | 
			
		||||
    <div class="modal-dialog" role="document">
 | 
			
		||||
      <form @submit.prevent="submit">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
          <div class="modal-header">
 | 
			
		||||
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
              <i class="sn-icon sn-icon-close"></i>
 | 
			
		||||
            </button>
 | 
			
		||||
            <h4 class="modal-title truncate !block">
 | 
			
		||||
              {{ i18n.t('protocols.steps.modals.link_results.title') }}
 | 
			
		||||
            </h4>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-if="results.length > 0" class="modal-body">
 | 
			
		||||
            <p>
 | 
			
		||||
              {{ i18n.t('protocols.steps.modals.link_results.description') }}
 | 
			
		||||
            </p>
 | 
			
		||||
            <div class="mt-6">
 | 
			
		||||
              <label class="sci-label">{{ i18n.t('protocols.steps.modals.link_results.result_label') }}</label>
 | 
			
		||||
              <SelectDropdown
 | 
			
		||||
                :options="results"
 | 
			
		||||
                :value="selectedResults"
 | 
			
		||||
                @change="changeResults"
 | 
			
		||||
                :multiple="true"
 | 
			
		||||
                :withCheckboxes="true"
 | 
			
		||||
                :placeholder="i18n.t('protocols.steps.modals.link_results.placeholder')" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else class="modal-body">
 | 
			
		||||
            <p>
 | 
			
		||||
              {{ i18n.t('protocols.steps.modals.link_results.empty_description') }}
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="modal-footer">
 | 
			
		||||
            <button type="button" class="btn btn-secondary" data-dismiss="modal">
 | 
			
		||||
              {{ i18n.t('general.cancel') }}
 | 
			
		||||
            </button>
 | 
			
		||||
            <template v-if="results.length > 0">
 | 
			
		||||
              <button v-if="step.attributes.results.length == 0" type="submit" class="btn btn-primary" @click="linkResults">
 | 
			
		||||
                {{ i18n.t('protocols.steps.modals.link_results.link_results') }}
 | 
			
		||||
              </button>
 | 
			
		||||
              <button v-else type="submit" class="btn btn-primary" @click="linkResults">
 | 
			
		||||
                {{ i18n.t('general.save') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template v-else>
 | 
			
		||||
              <a :href="resultsPageUrl" class="btn btn-primary">
 | 
			
		||||
                {{ i18n.t('protocols.steps.modals.link_results.go_to_results') }}
 | 
			
		||||
              </a>
 | 
			
		||||
            </template>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
/* global HelperModule I18n */
 | 
			
		||||
 | 
			
		||||
import SelectDropdown from '../../shared/select_dropdown.vue';
 | 
			
		||||
import axios from '../../../packs/custom_axios.js';
 | 
			
		||||
import modalMixin from '../../shared/modal_mixin.js';
 | 
			
		||||
import {
 | 
			
		||||
  list_my_module_results_path,
 | 
			
		||||
  my_module_results_path,
 | 
			
		||||
  link_results_step_results_path
 | 
			
		||||
} from '../../../routes.js';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'LinksResultsModal',
 | 
			
		||||
  props: {
 | 
			
		||||
    step: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      required: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mixins: [modalMixin],
 | 
			
		||||
  components: {
 | 
			
		||||
    SelectDropdown
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.selectedResults = this.step.attributes.results.map((result) => result.id);
 | 
			
		||||
    this.loadResults();
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      results: [],
 | 
			
		||||
      selectedResults: []
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    resultsListUrl() {
 | 
			
		||||
      return list_my_module_results_path({ my_module_id: this.step.attributes.my_module_id });
 | 
			
		||||
    },
 | 
			
		||||
    resultsPageUrl() {
 | 
			
		||||
      return my_module_results_path({ my_module_id: this.step.attributes.my_module_id });
 | 
			
		||||
    },
 | 
			
		||||
    resultsLinkUrl() {
 | 
			
		||||
      return link_results_step_results_path({ format: 'json' });
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    changeResults(value) {
 | 
			
		||||
      this.selectedResults = value;
 | 
			
		||||
    },
 | 
			
		||||
    linkResults() {
 | 
			
		||||
      axios.post(
 | 
			
		||||
        this.resultsLinkUrl,
 | 
			
		||||
        {
 | 
			
		||||
          step_ids: this.step.id,
 | 
			
		||||
          result_ids: this.selectedResults
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
        .then((response) => {
 | 
			
		||||
          this.$emit('close');
 | 
			
		||||
          this.$emit('updateStep', response.data.results);
 | 
			
		||||
          HelperModule.flashAlertMsg(I18n.t('protocols.steps.modals.link_results.success'), 'success');
 | 
			
		||||
        }).catch(() => {
 | 
			
		||||
          HelperModule.flashAlertMsg(I18n.t('protocols.steps.modals.link_results.error'), 'danger');
 | 
			
		||||
          this.$emit('close');
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    loadResults() {
 | 
			
		||||
      axios.get(this.resultsListUrl)
 | 
			
		||||
        .then((response) => {
 | 
			
		||||
          this.results = response.data;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -83,8 +83,35 @@
 | 
			
		|||
          :data-object-type="step.attributes.type"
 | 
			
		||||
          tabindex="0"
 | 
			
		||||
        ></span> <!-- Hidden element to support legacy code -->
 | 
			
		||||
 | 
			
		||||
        <a href="#"
 | 
			
		||||
        <button v-if="step.attributes.results.length == 0" class="btn btn-light icon-btn" @click="this.openLinkResultsModal = true">
 | 
			
		||||
          <i class="sn-icon sn-icon-results"></i>
 | 
			
		||||
        </button>
 | 
			
		||||
        <GeneralDropdown v-else ref="linkedResultsDropdown"  position="right">
 | 
			
		||||
          <template v-slot:field>
 | 
			
		||||
            <button class="btn btn-light icon-btn">
 | 
			
		||||
              <i class="sn-icon sn-icon-results"></i>
 | 
			
		||||
              <span class="absolute top-1 right-1 h-4 min-w-4 bg-sn-science-blue text-white flex items-center justify-center rounded-full text-[10px]">
 | 
			
		||||
                {{ step.attributes.results.length }}
 | 
			
		||||
              </span>
 | 
			
		||||
            </button>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-slot:flyout>
 | 
			
		||||
            <a v-for="result in step.attributes.results"
 | 
			
		||||
               :key="result.id"
 | 
			
		||||
               :title="result.name"
 | 
			
		||||
               :href="resultUrl(result.id)"
 | 
			
		||||
               class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer block hover:no-underline text-sn-blue truncate"
 | 
			
		||||
            >
 | 
			
		||||
              {{ result.name }}
 | 
			
		||||
            </a>
 | 
			
		||||
            <hr class="my-0">
 | 
			
		||||
            <div class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer text-sn-blue"
 | 
			
		||||
                 @click="this.openLinkResultsModal = true; $refs.linkedResultsDropdown.closeMenu()">
 | 
			
		||||
              {{ i18n.t('protocols.steps.manage_links') }}
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </GeneralDropdown>
 | 
			
		||||
        <a href=" #"
 | 
			
		||||
           v-if="!inRepository"
 | 
			
		||||
           ref="comments"
 | 
			
		||||
           class="open-comments-sidebar btn icon-btn btn-light"
 | 
			
		||||
| 
						 | 
				
			
			@ -181,6 +208,12 @@
 | 
			
		|||
      @close="openFormSelectModal = false"
 | 
			
		||||
      @submit="createElement('form_response', null, null, $event); openFormSelectModal = false"
 | 
			
		||||
    />
 | 
			
		||||
    <LinkResultsModal
 | 
			
		||||
      v-if="openLinkResultsModal"
 | 
			
		||||
      :step="step"
 | 
			
		||||
      @updateStep="updateLinkedResults"
 | 
			
		||||
      @close="openLinkResultsModal = false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -197,9 +230,11 @@
 | 
			
		|||
  import Checklist from '../shared/content/checklist.vue'
 | 
			
		||||
  import FormResponse from '../shared/content/form_response.vue'
 | 
			
		||||
  import deleteStepModal from './modals/delete_step.vue'
 | 
			
		||||
  import LinkResultsModal from './modals/link_results.vue'
 | 
			
		||||
  import Attachments from '../shared/content/attachments.vue'
 | 
			
		||||
  import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
 | 
			
		||||
  import MenuDropdown from '../shared/menu_dropdown.vue'
 | 
			
		||||
  import GeneralDropdown from '../shared/general_dropdown.vue'
 | 
			
		||||
  import ContentToolbar from '../shared/content/content_toolbar.vue'
 | 
			
		||||
  import CustomWellPlateModal from '../shared/content/modal/custom_well_plate_modal.vue'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -211,6 +246,10 @@
 | 
			
		|||
  import StorageUsage from '../shared/content/attachments/storage_usage.vue'
 | 
			
		||||
  import axios from '../../packs/custom_axios';
 | 
			
		||||
 | 
			
		||||
  import {
 | 
			
		||||
    my_module_results_path,
 | 
			
		||||
  } from '../../routes.js';
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    name: 'StepContainer',
 | 
			
		||||
    props: {
 | 
			
		||||
| 
						 | 
				
			
			@ -256,6 +295,7 @@
 | 
			
		|||
        inlineEditError: null,
 | 
			
		||||
        customWellPlate: false,
 | 
			
		||||
        openFormSelectModal: false,
 | 
			
		||||
        openLinkResultsModal: false,
 | 
			
		||||
        wellPlateOptions: [
 | 
			
		||||
          { text: I18n.t('protocols.steps.insert.well_plate_options.custom'),
 | 
			
		||||
            emit: 'create:custom_well_plate',
 | 
			
		||||
| 
						 | 
				
			
			@ -305,7 +345,9 @@
 | 
			
		|||
      ContentToolbar,
 | 
			
		||||
      CustomWellPlateModal,
 | 
			
		||||
      SelectFormModal,
 | 
			
		||||
      FormResponse
 | 
			
		||||
      FormResponse,
 | 
			
		||||
      LinkResultsModal,
 | 
			
		||||
      GeneralDropdown
 | 
			
		||||
    },
 | 
			
		||||
    created() {
 | 
			
		||||
      this.loadAttachments();
 | 
			
		||||
| 
						 | 
				
			
			@ -751,7 +793,16 @@
 | 
			
		|||
        }).fail(() => {
 | 
			
		||||
          HelperModule.flashAlertMsg(this.i18n.t('protocols.steps.step_duplication_failed'), 'danger');
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
      updateLinkedResults(results) {
 | 
			
		||||
        this.$emit('step:update', {
 | 
			
		||||
          results: results,
 | 
			
		||||
          position: this.step.attributes.position
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
      resultUrl(result_id) {
 | 
			
		||||
        return my_module_results_path({my_module_id: this.step.attributes.my_module_id, result_id: result_id})
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										136
									
								
								app/javascript/vue/results/modals/link_steps.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								app/javascript/vue/results/modals/link_steps.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div ref="modal" class="modal" tabindex="-1" role="dialog">
 | 
			
		||||
    <div class="modal-dialog" role="document">
 | 
			
		||||
      <form @submit.prevent="submit">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
          <div class="modal-header">
 | 
			
		||||
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
              <i class="sn-icon sn-icon-close"></i>
 | 
			
		||||
            </button>
 | 
			
		||||
            <h4 class="modal-title truncate !block">
 | 
			
		||||
              {{ i18n.t('my_modules.results.modals.link_steps.title') }}
 | 
			
		||||
            </h4>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-if="steps.length > 0" class="modal-body">
 | 
			
		||||
            <p>
 | 
			
		||||
              {{ i18n.t('my_modules.results.modals.link_steps.description') }}
 | 
			
		||||
            </p>
 | 
			
		||||
            <div class="mt-6">
 | 
			
		||||
              <label class="sci-label">{{ i18n.t('my_modules.results.modals.link_steps.steps_label') }}</label>
 | 
			
		||||
              <SelectDropdown
 | 
			
		||||
                :options="steps"
 | 
			
		||||
                :value="selectedSteps"
 | 
			
		||||
                @change="changeSteps"
 | 
			
		||||
                :multiple="true"
 | 
			
		||||
                :withCheckboxes="true"
 | 
			
		||||
                :placeholder="i18n.t('my_modules.results.modals.link_steps.placeholder')" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else class="modal-body">
 | 
			
		||||
            <p>
 | 
			
		||||
              {{ i18n.t('my_modules.results.modals.link_steps.empty_description') }}
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="modal-footer">
 | 
			
		||||
            <button type="button" class="btn btn-secondary" data-dismiss="modal">
 | 
			
		||||
              {{ i18n.t('general.cancel') }}
 | 
			
		||||
            </button>
 | 
			
		||||
            <template v-if="steps.length > 0">
 | 
			
		||||
              <button v-if="result.attributes.steps.length == 0" type="submit" class="btn btn-primary" @click="linkSteps">
 | 
			
		||||
                {{ i18n.t('my_modules.results.modals.link_steps.link_steps') }}
 | 
			
		||||
              </button>
 | 
			
		||||
              <button v-else type="submit" class="btn btn-primary"  @click="linkSteps">
 | 
			
		||||
                {{ i18n.t('general.save') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template v-else>
 | 
			
		||||
              <a :href="protocolPageUrl" class="btn btn-primary">
 | 
			
		||||
                {{ i18n.t('my_modules.results.modals.link_steps.go_to_protocol') }}
 | 
			
		||||
              </a>
 | 
			
		||||
            </template>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
/* global HelperModule I18n */
 | 
			
		||||
 | 
			
		||||
import SelectDropdown from '../../shared/select_dropdown.vue';
 | 
			
		||||
import axios from '../../../packs/custom_axios.js';
 | 
			
		||||
import modalMixin from '../../shared/modal_mixin.js';
 | 
			
		||||
import {
 | 
			
		||||
  list_steps_path,
 | 
			
		||||
  protocols_my_module_path,
 | 
			
		||||
  link_steps_step_results_path
 | 
			
		||||
} from '../../../routes.js';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'LinkStepsModal',
 | 
			
		||||
  props: {
 | 
			
		||||
    result: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      required: true
 | 
			
		||||
    },
 | 
			
		||||
    protocolId: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      required: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mixins: [modalMixin],
 | 
			
		||||
  components: {
 | 
			
		||||
    SelectDropdown
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.selectedSteps = this.result.attributes.steps.map((step) =>  step.id);
 | 
			
		||||
    this.loadSteps();
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      steps: [],
 | 
			
		||||
      selectedSteps: []
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    stepsListUrl() {
 | 
			
		||||
      return list_steps_path({ protocol_id: this.protocolId });
 | 
			
		||||
    },
 | 
			
		||||
    protocolPageUrl() {
 | 
			
		||||
      return protocols_my_module_path({ id: this.result.attributes.my_module_id });
 | 
			
		||||
    },
 | 
			
		||||
    stepsLinkUrl() {
 | 
			
		||||
      return link_steps_step_results_path({ format: 'json' });
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    changeSteps(value) {
 | 
			
		||||
      this.selectedSteps = value;
 | 
			
		||||
    },
 | 
			
		||||
    linkSteps() {
 | 
			
		||||
      axios.post(
 | 
			
		||||
        this.stepsLinkUrl,
 | 
			
		||||
        {
 | 
			
		||||
          step_ids: this.selectedSteps,
 | 
			
		||||
          result_ids: this.result.id
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
        .then((response) => {
 | 
			
		||||
          this.$emit('close');
 | 
			
		||||
          this.$emit('updateResult', response.data.steps);
 | 
			
		||||
          HelperModule.flashAlertMsg(I18n.t('protocols.steps.modals.link_results.success'), 'success');
 | 
			
		||||
        }).catch(() => {
 | 
			
		||||
          HelperModule.flashAlertMsg(I18n.t('protocols.steps.modals.link_results.error'), 'danger');
 | 
			
		||||
          this.$emit('close');
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    loadSteps() {
 | 
			
		||||
      axios.get(this.stepsListUrl)
 | 
			
		||||
        .then((response) => {
 | 
			
		||||
          this.steps = response.data;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +65,34 @@
 | 
			
		|||
            :data-object-type="result.attributes.type"
 | 
			
		||||
            tabindex="0"
 | 
			
		||||
          ></span> <!-- Hidden element to support legacy code -->
 | 
			
		||||
 | 
			
		||||
          <button v-if="result.attributes.steps.length == 0" class="btn btn-light icon-btn" @click="this.openLinkStepsModal = true">
 | 
			
		||||
            {{ i18n.t('my_modules.results.link_to_step') }}
 | 
			
		||||
          </button>
 | 
			
		||||
          <GeneralDropdown v-else ref="linkedStepsDropdown"  position="right">
 | 
			
		||||
            <template v-slot:field>
 | 
			
		||||
              <button class="btn btn-light icon-btn">
 | 
			
		||||
                {{ i18n.t('my_modules.results.link_to_step') }}
 | 
			
		||||
                <span class="absolute top-1 -right-1 h-4 min-w-4 bg-sn-science-blue text-white flex items-center justify-center rounded-full text-[10px]">
 | 
			
		||||
                  {{ result.attributes.steps.length }}
 | 
			
		||||
                </span>
 | 
			
		||||
              </button>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template v-slot:flyout>
 | 
			
		||||
              <a v-for="step in result.attributes.steps"
 | 
			
		||||
                :key="step.id"
 | 
			
		||||
                :title="step.name"
 | 
			
		||||
                :href="protocolUrl(step.id)"
 | 
			
		||||
                class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer block hover:no-underline text-sn-blue truncate"
 | 
			
		||||
              >
 | 
			
		||||
                {{ step.name }}
 | 
			
		||||
              </a>
 | 
			
		||||
              <hr class="my-0">
 | 
			
		||||
              <div class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer text-sn-blue"
 | 
			
		||||
                  @click="this.openLinkStepsModal = true; $refs.linkedStepsDropdown.closeMenu()">
 | 
			
		||||
                {{ i18n.t('protocols.steps.manage_links') }}
 | 
			
		||||
              </div>
 | 
			
		||||
            </template>
 | 
			
		||||
          </GeneralDropdown>
 | 
			
		||||
          <a href="#"
 | 
			
		||||
            ref="comments"
 | 
			
		||||
            class="open-comments-sidebar btn icon-btn btn-light"
 | 
			
		||||
| 
						 | 
				
			
			@ -148,6 +175,13 @@
 | 
			
		|||
          @cancel="closeCustomWellPlateModal"
 | 
			
		||||
          @create:table="(...args) => this.createElement('table', ...args)"
 | 
			
		||||
        />
 | 
			
		||||
        <LinkStepsModal
 | 
			
		||||
          v-if="openLinkStepsModal"
 | 
			
		||||
          :result="result"
 | 
			
		||||
          :protocolId="protocolId"
 | 
			
		||||
          @updateResult="updateLinkedSteps"
 | 
			
		||||
          @close="openLinkStepsModal = false"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -161,7 +195,9 @@ import ResultText from '../shared/content/text.vue';
 | 
			
		|||
import Attachments from '../shared/content/attachments.vue';
 | 
			
		||||
import InlineEdit from '../shared/inline_edit.vue';
 | 
			
		||||
import MenuDropdown from '../shared/menu_dropdown.vue';
 | 
			
		||||
import GeneralDropdown from '../shared/general_dropdown.vue';
 | 
			
		||||
import deleteResultModal from './delete_result.vue';
 | 
			
		||||
import LinkStepsModal from './modals/link_steps.vue'
 | 
			
		||||
import ContentToolbar from '../shared/content/content_toolbar';
 | 
			
		||||
import CustomWellPlateModal from '../shared/content/modal/custom_well_plate_modal.vue'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -170,12 +206,16 @@ import WopiFileModal from '../shared/content/attachments/mixins/wopi_file_modal.
 | 
			
		|||
import OveMixin from '../shared/content/attachments/mixins/ove.js';
 | 
			
		||||
import UtilsMixin from '../mixins/utils.js';
 | 
			
		||||
import StorageUsage from '../shared/content/attachments/storage_usage.vue';
 | 
			
		||||
import {
 | 
			
		||||
  protocols_my_module_path,
 | 
			
		||||
} from '../../routes.js';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Results',
 | 
			
		||||
  props: {
 | 
			
		||||
    result: { type: Object, required: true },
 | 
			
		||||
    resultToReload: { type: Number, required: false },
 | 
			
		||||
    protocolId: { type: Number, required: false },
 | 
			
		||||
    activeDragResult: {
 | 
			
		||||
      required: false
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +233,7 @@ export default {
 | 
			
		|||
      showFileModal: false,
 | 
			
		||||
      dragingFile: false,
 | 
			
		||||
      customWellPlate: false,
 | 
			
		||||
      openLinkStepsModal: false,
 | 
			
		||||
      wellPlateOptions: [
 | 
			
		||||
        { text: I18n.t('protocols.steps.insert.well_plate_options.custom'), emit: 'create:custom_well_plate'},
 | 
			
		||||
        { text: I18n.t('protocols.steps.insert.well_plate_options.32_x_48'), emit: 'create:table', params: [32, 48] },
 | 
			
		||||
| 
						 | 
				
			
			@ -219,7 +260,9 @@ export default {
 | 
			
		|||
    deleteResultModal,
 | 
			
		||||
    StorageUsage,
 | 
			
		||||
    ContentToolbar,
 | 
			
		||||
    CustomWellPlateModal
 | 
			
		||||
    CustomWellPlateModal,
 | 
			
		||||
    LinkStepsModal,
 | 
			
		||||
    GeneralDropdown
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    resultToReload() {
 | 
			
		||||
| 
						 | 
				
			
			@ -576,7 +619,15 @@ export default {
 | 
			
		|||
      axios.patch(this.urls.update_url, { result: { name } }).then((_) => {
 | 
			
		||||
        this.$emit('updated');
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    updateLinkedSteps(steps) {
 | 
			
		||||
      this.$emit('result:update', this.result.id,{
 | 
			
		||||
        steps: steps
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    protocolUrl(step_id) {
 | 
			
		||||
      return protocols_my_module_path({ id: this.result.attributes.my_module_id }, { step_id: step_id })
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,8 @@
 | 
			
		|||
        :resultToReload="resultToReload"
 | 
			
		||||
        :activeDragResult="activeDragResult"
 | 
			
		||||
        :userSettingsUrl="userSettingsUrl"
 | 
			
		||||
        :protocolId="protocolId"
 | 
			
		||||
        @result:update="updateResult"
 | 
			
		||||
        @result:elements:loaded="resultToReload = null; elementsLoaded++"
 | 
			
		||||
        @result:move_element="reloadResult"
 | 
			
		||||
        @result:attachments:loaded="resultToReload = null; attachmentsLoaded++"
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +76,8 @@ export default {
 | 
			
		|||
    archived: { type: String, required: true },
 | 
			
		||||
    active_url: { type: String, required: true },
 | 
			
		||||
    archived_url: { type: String, required: true },
 | 
			
		||||
    userSettingsUrl: { type: String, required: false }
 | 
			
		||||
    userSettingsUrl: { type: String, required: false },
 | 
			
		||||
    protocolId: { type: Number, required: false }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -238,6 +241,13 @@ export default {
 | 
			
		|||
 | 
			
		||||
      axios.put(this.userSettingsUrl, { settings: [settings] });
 | 
			
		||||
    },
 | 
			
		||||
    updateResult(id, attributes) {
 | 
			
		||||
      const resultIndex = this.results.findIndex((result) => result.id === id);
 | 
			
		||||
      this.results[resultIndex].attributes = {
 | 
			
		||||
        ...this.results[resultIndex].attributes,
 | 
			
		||||
        ...attributes
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    removeResult(result_id) {
 | 
			
		||||
      this.results = this.results.filter((r) => r.id != result_id);
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ class StepSerializer < ActiveModel::Serializer
 | 
			
		|||
  attributes :name, :position, :completed, :attachments_manageble, :urls, :assets_view_mode,
 | 
			
		||||
             :marvinjs_enabled, :marvinjs_context, :created_by, :created_at, :assets_order,
 | 
			
		||||
             :wopi_enabled, :wopi_context, :comments_count, :unseen_comments, :storage_limit,
 | 
			
		||||
             :type, :open_vector_editor_context, :collapsed, :results
 | 
			
		||||
             :type, :open_vector_editor_context, :collapsed, :my_module_id, :results
 | 
			
		||||
 | 
			
		||||
  def collapsed
 | 
			
		||||
    step_states = @instance_options[:user].settings.fetch('task_step_states', {})
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +27,10 @@ class StepSerializer < ActiveModel::Serializer
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def my_module_id
 | 
			
		||||
    object.my_module&.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def type
 | 
			
		||||
    'Step'
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@
 | 
			
		|||
  <div id="results" data-behaviour="vue">
 | 
			
		||||
    <results url="<%= my_module_results_url(@my_module, view_mode: params[:view_mode]) %>"
 | 
			
		||||
             active_url="<%= my_module_results_url(@my_module) %>"
 | 
			
		||||
             protocol-id="<%= @my_module.protocol.id %>"
 | 
			
		||||
             archived_url="<%= my_module_results_url(@my_module, view_mode: :archived) %>"
 | 
			
		||||
             can-create=<%= can_create_results?(@my_module) && !(params[:view_mode] == 'archived') %>
 | 
			
		||||
             archived=<%= params[:view_mode] == 'archived' %>>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								app/views/results/list.json.jbuilder
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/results/list.json.jbuilder
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
json.array! @results do |r|
 | 
			
		||||
  json.array! [r.id, r.name]
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										11
									
								
								app/views/steps/list.json.jbuilder
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/views/steps/list.json.jbuilder
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
json.array! @steps do |r|
 | 
			
		||||
  json.array! [
 | 
			
		||||
    r.id,
 | 
			
		||||
    r.name,
 | 
			
		||||
    {
 | 
			
		||||
      position: r.position
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1603,6 +1603,17 @@ en:
 | 
			
		|||
      info_tab: "Info"
 | 
			
		||||
      comments_tab: "Comments"
 | 
			
		||||
      comment_title: "%{user} at %{time}:"
 | 
			
		||||
      link_to_step: "Link to step"
 | 
			
		||||
      modals:
 | 
			
		||||
        link_steps:
 | 
			
		||||
          title: 'Link result to protocol steps'
 | 
			
		||||
          description: 'You can link result to multipple steps'
 | 
			
		||||
          empty_description: 'No step found. Add a step first to enable linking with a result.'
 | 
			
		||||
          steps_label: 'Protocol steps'
 | 
			
		||||
          placeholder: 'Select steps'
 | 
			
		||||
          link_steps: 'Link steps'
 | 
			
		||||
          go_to_protocol: 'Go to Protocol'
 | 
			
		||||
          step_label: "Step %{position} - %{name}"
 | 
			
		||||
      insert:
 | 
			
		||||
        button: 'Insert content'
 | 
			
		||||
        title: 'Insert result content'
 | 
			
		||||
| 
						 | 
				
			
			@ -3980,6 +3991,7 @@ en:
 | 
			
		|||
      component_duplication_failed: "Step content could not be duplicated, try again later."
 | 
			
		||||
      timestamp: "Created on %{date} by %{user}"
 | 
			
		||||
      timestamp_iso_html: "Created on <span class='iso-formatted-date'>%{date}</span> by %{user}"
 | 
			
		||||
      manage_links: "Manage links"
 | 
			
		||||
      status:
 | 
			
		||||
        complete: "Mark as done"
 | 
			
		||||
        uncomplete: "Unmark as done"
 | 
			
		||||
| 
						 | 
				
			
			@ -4067,6 +4079,16 @@ en:
 | 
			
		|||
          confirm: 'Yes, delete them'
 | 
			
		||||
        reorder_elements:
 | 
			
		||||
          title: 'Rearrange step %{step_position} content'
 | 
			
		||||
        link_results:
 | 
			
		||||
          title: 'Link results to this step'
 | 
			
		||||
          description: 'You can link multiple results to a step'
 | 
			
		||||
          empty_description: 'No results found. Add a result first to enable linking with a step.'
 | 
			
		||||
          result_label: 'Result'
 | 
			
		||||
          placeholder: 'Select results'
 | 
			
		||||
          link_results: 'Link results'
 | 
			
		||||
          go_to_results: 'Go to Results'
 | 
			
		||||
          success: 'Step and result links successfully updated.'
 | 
			
		||||
          error: 'There was a problem with linking result to the step.'
 | 
			
		||||
        custom_well_plate:
 | 
			
		||||
          title: 'Insert custom well plate'
 | 
			
		||||
          name_label: 'Well plate name'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -460,9 +460,10 @@ Rails.application.routes.draw do
 | 
			
		|||
    get 'experiments/:experiment_id/table', to: 'my_modules#index'
 | 
			
		||||
    get 'experiments/:experiment_id/modules', to: 'my_modules#index', as: :my_modules
 | 
			
		||||
 | 
			
		||||
    resources :step_results, only: %i(destroy) do
 | 
			
		||||
    resources :step_results, only: [] do
 | 
			
		||||
      collection do
 | 
			
		||||
        post :create
 | 
			
		||||
        post :link_results
 | 
			
		||||
        post :link_steps
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -564,6 +565,9 @@ Rails.application.routes.draw do
 | 
			
		|||
      get 'users/edit', to: 'user_my_modules#index_edit'
 | 
			
		||||
 | 
			
		||||
      resources :results, only: %i(index show create update destroy) do
 | 
			
		||||
        collection do
 | 
			
		||||
          get :list
 | 
			
		||||
        end
 | 
			
		||||
        member do
 | 
			
		||||
          get :elements
 | 
			
		||||
          get :assets
 | 
			
		||||
| 
						 | 
				
			
			@ -642,6 +646,9 @@ Rails.application.routes.draw do
 | 
			
		|||
        post 'update_asset_view_mode'
 | 
			
		||||
        post 'duplicate'
 | 
			
		||||
      end
 | 
			
		||||
      collection do
 | 
			
		||||
        get :list
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # tinyMCE image uploader endpoint
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue