Merge pull request #8600 from scinote-eln/features/link-steps-results

Features/link steps results
This commit is contained in:
Martin Artnik 2025-06-23 15:55:23 +02:00 committed by GitHub
commit 75fa364594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 100 additions and 58 deletions

View file

@ -9,14 +9,14 @@ class StepResultsController < ApplicationController
def link_results
ActiveRecord::Base.transaction do
@step_results.where.not(result: @results).each do |step_result|
log_activity(:step_and_result_unlinked, @steps.first.my_module, step_result.step, step_result.result)
log_activity(:step_and_result_unlinked, step_result.step.my_module, step_result.step, step_result.result)
step_result.destroy!
end
@results.where.not(id: @step_results.select(:result_id)).each do |result|
StepResult.create!(step: @steps.first, result: result, created_by: current_user)
log_activity(:step_and_result_linked, @steps.first.my_module, @steps.first, result)
end
render json: { results: @steps.first.results.map { |r| { id: r.id, name: r.name } } }, status: :created
render json: { results: @steps.first.results.map { |r| { id: r.id, name: r.name, archived: r.archived? } } }, status: :created
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
render json: { message: :error }, status: :unprocessable_entity
@ -27,7 +27,7 @@ class StepResultsController < ApplicationController
def link_steps
ActiveRecord::Base.transaction do
@step_results.where.not(step: @steps).each do |step_result|
log_activity(:step_and_result_unlinked, @steps.first.my_module, step_result.step, step_result.result)
log_activity(:step_and_result_unlinked, step_result.result.my_module, step_result.step, step_result.result)
step_result.destroy!
end
@steps.where.not(id: @step_results.select(:step_id)).each do |step|

View file

@ -102,7 +102,7 @@ module GlobalActivitiesHelper
when Result
return current_value unless obj.navigable?
path = obj.archived? ? archive_my_module_path(obj.my_module) : my_module_results_path(obj.my_module, result_id: obj.id)
path = obj.archived? ? my_module_results_path(obj.my_module, result_id: obj.id, view_mode: :archived) : my_module_results_path(obj.my_module, result_id: obj.id)
when Step
return current_value unless obj.navigable?

View file

@ -20,6 +20,7 @@
<SelectDropdown
:options="results"
:value="selectedResults"
:searchable="true"
@change="changeResults"
:multiple="true"
:withCheckboxes="true"

View file

@ -83,12 +83,14 @@
:data-object-type="step.attributes.type"
tabindex="0"
></span> <!-- Hidden element to support legacy code -->
<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>
<template v-if="step.attributes.results.length == 0">
<button v-if="urls.update_url" :title="i18n.t('protocols.steps.link_results')" class="btn btn-light icon-btn" @click="this.openLinkResultsModal = true">
<i class="sn-icon sn-icon-results"></i>
</button>
</template>
<GeneralDropdown v-else ref="linkedResultsDropdown" position="right">
<template v-slot:field>
<button class="btn btn-light icon-btn">
<button class="btn btn-light icon-btn" :title="i18n.t('protocols.steps.linked_results')">
<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 }}
@ -96,19 +98,23 @@
</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 class="overflow-y-auto max-h-[calc(50vh_-_6rem)]">
<a v-for="result in step.attributes.results"
:key="result.id"
:title="result.name"
:href="resultUrl(result.id, result.archived)"
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>
</div>
<template v-if="urls.update_url">
<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>
</template>
</GeneralDropdown>
<a href=" #"
@ -800,8 +806,8 @@
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})
resultUrl(result_id, archived) {
return my_module_results_path({my_module_id: this.step.attributes.my_module_id, result_id: result_id, view_mode: (archived ? 'archived' : 'active') });
},
}
}

View file

@ -20,6 +20,7 @@
<SelectDropdown
:options="steps"
:value="selectedSteps"
:searchable="true"
@change="changeSteps"
:multiple="true"
:withCheckboxes="true"

View file

@ -65,32 +65,38 @@
: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>
<tempplate v-if="result.attributes.steps.length == 0">
<button v-if="urls.update_url" :title="i18n.t('my_modules.results.link_steps')" class="btn btn-light icon-btn" @click="this.openLinkStepsModal = true">
{{ i18n.t('my_modules.results.link_to_step') }}
</button>
</tempplate>
<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') }}
<button class="btn btn-light icon-btn" :title="i18n.t('my_modules.results.linked_steps')">
<i class="sn-icon sn-icon-steps"></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]">
{{ 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 class="overflow-y-auto max-h-[calc(50vh_-_6rem)]">
<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>
</div>
<template v-if="urls.update_url">
<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>
</template>
</GeneralDropdown>
<a href="#"

View file

@ -225,7 +225,7 @@ export default {
if (this.newValue.length === 0) {
return false;
}
if (this.newValue.length === 1) {
if (this.newValue.length === 1 && this.rawOptions.length > 1) {
this.selectAllState = 'indeterminate';
return this.renderLabel(this.rawOptions.find((option) => option[0] === this.newValue[0]));
}

View file

@ -23,7 +23,7 @@ class StepSerializer < ActiveModel::Serializer
def results
object.results.map do |result|
{ id: result.id, name: result.name }
{ id: result.id, name: result.name, archived: result.archived? }
end
end

View file

@ -4,7 +4,7 @@
<span class="<%=result_icon_class(subject)%>"></span>
<% if subject&.navigable? %>
<% path = subject.archived? ? archive_my_module_path(subject.my_module) : my_module_results_path(subject.my_module) %>
<% path = subject.archived? ? my_module_results_path(subject.my_module, result_id: subject.id, view_mode: :archived) : my_module_results_path(subject.my_module, result_id: subject.id) %>
<%= route_to_other_team(path,
team,
subject.name&.truncate(Constants::NAME_TRUNCATION_LENGTH),

View file

@ -710,7 +710,7 @@ class Extends
^CF0,15
^FO0,5^FD{{ITEM_ID}}^FS
^FO0,13^BQN,2,3^FDMA,{{ITEM_ID}}^FS
^FO70,27^FB100,2,0,L^FD{{NAME}}^FS^FS
^FO70,27^FB100,4,0,L^FD{{NAME}}^FS^FS
^XZ
HEREDOC
}

View file

@ -1644,10 +1644,12 @@ en:
comments_tab: "Comments"
comment_title: "%{user} at %{time}:"
link_to_step: "Link to step"
link_steps: "Link steps to this result"
linked_steps: "Linked steps"
modals:
link_steps:
title: 'Link result to protocol steps'
description: 'You can link result to multipple steps'
description: 'You can link a result to multiple steps.'
empty_description: 'No step found. Add a step first to enable linking with a result.'
steps_label: 'Protocol steps'
placeholder: 'Select steps'
@ -4055,6 +4057,8 @@ en:
timestamp: "Created on %{date} by %{user}"
timestamp_iso_html: "Created on <span class='iso-formatted-date'>%{date}</span> by %{user}"
manage_links: "Manage links"
link_results: "Link results to this step"
linked_results: "Linked results"
status:
complete: "Mark as done"
uncomplete: "Unmark as done"

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class FixDefault201dpiLabelTemplates < ActiveRecord::Migration[7.0]
class TempLabelTemplate < ApplicationRecord
self.table_name = 'label_templates'
end
def up
TempLabelTemplate.where(
content: "^XA\n^MTT\n^MUD,200,200\n^PR2\n^MD30\n^LH0,8\n^PW180\n^CF0,15\n^FO0,5^FD{{ITEM_ID}}^FS\n^FO0,13^BQN,2,3^FDMA,{{ITEM_ID}}^FS\n^FO70,27^FB100,2,0,L^FD{{NAME}}^FS^FS\n^XZ\n"
).update!(
content: "^XA\n^MTT\n^MUD,200,200\n^PR2\n^MD30\n^LH0,8\n^PW180\n^CF0,15\n^FO0,5^FD{{ITEM_ID}}^FS\n^FO0,13^BQN,2,3^FDMA,{{ITEM_ID}}^FS\n^FO70,27^FB100,4,0,L^FD{{NAME}}^FS^FS\n^XZ\n"
)
end
def down
TempLabelTemplate.where(
content: "^XA\n^MTT\n^MUD,200,200\n^PR2\n^MD30\n^LH0,8\n^PW180\n^CF0,15\n^FO0,5^FD{{ITEM_ID}}^FS\n^FO0,13^BQN,2,3^FDMA,{{ITEM_ID}}^FS\n^FO70,27^FB100,4,0,L^FD{{NAME}}^FS^FS\n^XZ\n"
).update!(
content: "^XA\n^MTT\n^MUD,200,200\n^PR2\n^MD30\n^LH0,8\n^PW180\n^CF0,15\n^FO0,5^FD{{ITEM_ID}}^FS\n^FO0,13^BQN,2,3^FDMA,{{ITEM_ID}}^FS\n^FO70,27^FB100,2,0,L^FD{{NAME}}^FS^FS\n^XZ\n"
)
end
end

View file

@ -15,12 +15,12 @@ describe StepResultsController, type: :controller do
let(:result_second) { create :result, my_module: my_modules.first, user: user }
let(:step_result) { create :step_result, step: step_second, result: result_second }
describe 'POST create link step result succesfully' do
let(:action) { post :create, params: params }
describe 'POST create link result succesfully' do
let(:action) { post :link_results, params: params }
let(:params) do
{
step_id: step.id,
result_id: result_text.result.id
step_ids:[step.id],
result_ids: [result_text.result.id]
}
end
@ -30,22 +30,24 @@ describe StepResultsController, type: :controller do
end
it 'calls create activity service' do
params[:step_id] = step_second.id
params[:step_ids] = [step_second.id]
action
expect(response).to have_http_status(:forbidden)
end
end
describe 'DELETE destroy' do
let(:action) { delete :destroy, params: params, format: :json }
describe 'POST create link step succesfully' do
let(:action) { delete :link_steps, params: params, format: :json }
let(:params) do
{
step_ids:[step.id],
result_ids: [result_text.result.id]
}
end
context 'when in protocol repository' do
let(:params) { { id: step_result.id } }
it 'calls create activity for deleting step in protocol repository' do
action
expect(response).to have_http_status(:success)
end
it 'calls create activity service' do
action
expect(response).to have_http_status(:success)
end
end
end