Merge pull request #5912 from artoscinote/ma_SCI_8947

Basic results component and structure [SCI-8947]
This commit is contained in:
artoscinote 2023-08-07 16:09:34 +02:00 committed by GitHub
commit a8bf290fc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 499 additions and 157 deletions

View file

@ -33,10 +33,6 @@ html {
/* Results index page */
#results {
margin-top: 20px;
}
.navigation-results-counter {
background-color: $color-concrete;
border-radius: $border-radius-circle;

View file

@ -0,0 +1,46 @@
# frozen_string_literal: true
class ResultOrderableElementsController < ApplicationController
before_action :load_vars_nested
before_action :check_manage_permissions
def reorder
@result.with_lock do
params[:result_orderable_element_positions].each do |id, position|
@result.result_orderable_elements.find(id).update_column(:position, position)
end
log_activity(:result_content_rearranged, @my_module.experiment.project, my_module: @my_module.id)
@result.touch
end
render json: params[:result_orderable_element_positions], status: :ok
end
private
def load_vars_nested
@result = Result.find_by(id: params[:result_id])
return render_404 unless @result
@my_module = @result.my_module
end
def check_manage_permissions
render_403 unless can_manage_result?(@result)
end
def log_activity(type_of, project = nil, message_items = {})
default_items = { result: @result.id }
message_items = default_items.merge(message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: @result,
team: @@my_module.team,
project: project,
message_items: message_items)
end
end

View file

@ -1,6 +1,48 @@
# frozen_string_literal: true
class ResultsController < ApplicationController
before_action :load_vars
before_action :check_destroy_permissions
skip_before_action :verify_authenticity_token, only: %i(create destroy)
before_action :load_my_module
before_action :load_vars, only: %i(destroy elements assets)
before_action :check_destroy_permissions, only: :destroy
def index
respond_to do |format|
format.json do
# API endpoint
render(
json: apply_sort(@my_module.results),
formats: :json
)
end
format.html do
# Main view
@experiment = @my_module.experiment
@project = @experiment.project
render(:index, formats: :html)
end
end
end
def create
result = @my_module.results.create!(user: current_user)
render json: result
end
def elements
render json: @result.result_orderable_elements.order(:position),
each_serializer: ResultOrderableElementSerializer,
user: current_user
end
def assets
render json: @result.assets,
each_serializer: AssetSerializer,
user: current_user
end
def destroy
result_type = if @result.is_text
@ -27,9 +69,32 @@ class ResultsController < ApplicationController
private
def apply_sort(results)
case params[:sort]
when 'updated_at_asc'
results.order(updated_at: :asc)
when 'updated_at_desc'
results.order(updated_at: :desc)
when 'created_at_asc'
results.order(created_at: :asc)
when 'created_at_desc'
results.order(created_at: :desc)
when 'name_asc'
results.order(name: :asc)
when 'name_desc'
results.order(name: :desc)
end
end
def load_my_module
@my_module = MyModule.readable_by_user(current_user).find(params[:my_module_id])
end
def load_vars
@result = Result.find_by_id(params[:id])
@result = @my_module.results.find(params[:id])
return render_403 unless @result
@my_module = @result.my_module
end

View file

@ -0,0 +1,13 @@
import TurbolinksAdapter from 'vue-turbolinks';
import Vue from 'vue/dist/vue.esm';
import Results from '../../vue/results/results.vue';
Vue.use(TurbolinksAdapter);
Vue.prototype.i18n = window.I18n;
new Vue({
el: '#results',
components: {
Results
}
});

View file

@ -0,0 +1,19 @@
<template>
<div class="result-wrapper">
{{ result.id }}
{{ result.attributes.name }}
<hr>
</div>
</template>
<script>
import Result from './result.vue';
export default {
name: 'Results',
components: { Result },
props: {
result: { type: Object, required: true }
}
}
</script>

View file

@ -0,0 +1,69 @@
<template>
<div class="results-wrapper">
<ResultsToolbar :sort="sort" @setSort="setSort" @newResult="createResult" @expandAll="expandAll" @collapseAll="collapseAll" lass="mb-3" />
<div class="results-list">
<Result v-for="result in results" :key="result.id" :result="result" />
</div>
</div>
</template>
<script>
import axios from 'axios';
import ResultsToolbar from './results_toolbar.vue';
import Result from './result.vue';
export default {
name: 'Results',
components: { ResultsToolbar, Result },
props: {
url: { type: String, required: true }
},
data() {
return {
results: [],
sort: 'created_at_desc'
}
},
created() {
this.loadResults();
},
methods: {
loadResults() {
axios.get(
`${this.url}?sort=${this.sort}`,
{
headers: {
'Accept': 'application/json'
}
}
).then((response) => this.results = response.data.data);
},
setSort(sort) {
this.sort = sort;
this.loadResults();
},
createResult() {
axios.post(
`${this.url}`,
{
headers: {
'Accept': 'application/json'
}
}
).then(
(response) => {
this.results.unshift(response.data.data)
}
);
},
expandAll() {
$('.result-wrapper .collapse').collapse('show')
},
collapseAll() {
$('.result-wrapper .collapse').collapse('hide')
}
}
}
</script>

View file

@ -0,0 +1,61 @@
<template>
<div class="result-toolbar p-3 flex justify-between rounded-md bg-sn-white">
<div class="result-toolbar__left">
<button class="btn btn-secondary" @click="$emit('newResult')">
<i class="sn-icon sn-icon-new-task"></i>
{{ i18n.t('my_modules.results.add_label') }}
</button>
</div>
<div class="result-toolbar__right flex items-center" @click="$emit('expandAll')">
<button class="btn btn-secondary mr-3">
{{ i18n.t('my_modules.results.expand_label') }}
</button>
<button class="btn btn-secondary mr-3" @click="$emit('collapseAll')">
{{ i18n.t('my_modules.results.collapse_label') }}
</button>
<button class="btn btn-light icon-btn mr-3">
<i class="sn-icon sn-icon-filter"></i>
</button>
<div class="dropdown">
<button class="dropdown-toggle btn btn-light icon-btn mr-3" id="sortDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="sn-icon sn-icon-sort"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="sortDropdown">
<li v-for="sort in sorts" :key="sort">
<a class="cursor-pointer" @click="setSort(sort)">
{{ i18n.t(`my_modules.results.sorts.${sort}`)}}
</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
const SORTS = [
'updated_at_asc',
'updated_at_desc',
'created_at_asc',
'created_at_desc',
'name_asc',
'name_desc'
];
export default {
name: 'ResultsToolbar',
props: {
sort: { type: String, required: true }
},
created() {
this.sorts = SORTS;
},
methods: {
setSort(sort) {
this.$emit('setSort', sort);
}
}
}
</script>

View file

@ -107,15 +107,16 @@ module Reports
results = my_module_element.my_module.results
order_results_for_report(results, report.settings.dig(:task, :result_order)).each do |result|
next unless result.is_asset && PREVIEW_EXTENSIONS.include?(result.asset.file.blob.filename.extension)
result.assets.each do |asset|
next unless PREVIEW_EXTENSIONS.include?(asset.file.blob.filename.extension)
asset = result.asset
if !asset.file_pdf_preview.attached? || (asset.file.created_at > asset.file_pdf_preview.created_at)
PdfPreviewJob.perform_now(asset.id)
asset.reload
end
asset.file_pdf_preview.open(tmpdir: tmp_dir) do |file|
report_file = merge_pdf_files(file, report_file)
if !asset.file_pdf_preview.attached? || (asset.file.created_at > asset.file_pdf_preview.created_at)
PdfPreviewJob.perform_now(asset.id)
asset.reload
end
asset.file_pdf_preview.open(tmpdir: tmp_dir) do |file|
report_file = merge_pdf_files(file, report_file)
end
end
end
end

View file

@ -6,25 +6,25 @@ class Result < ApplicationRecord
include SearchableByNameModel
auto_strip_attributes :name, nullify: false
validates :user, :my_module, presence: true
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
belongs_to :user, inverse_of: :results
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true
belongs_to :archived_by, foreign_key: 'archived_by_id', class_name: 'User', optional: true
belongs_to :restored_by, foreign_key: 'restored_by_id', class_name: 'User', optional: true
belongs_to :last_modified_by, class_name: 'User', optional: true
belongs_to :archived_by, class_name: 'User', optional: true
belongs_to :restored_by, class_name: 'User', optional: true
belongs_to :my_module, inverse_of: :results
has_one :result_asset, inverse_of: :result, dependent: :destroy
has_one :asset, through: :result_asset
has_one :result_table, inverse_of: :result, dependent: :destroy
has_one :table, through: :result_table
has_one :result_text, inverse_of: :result, dependent: :destroy
has_many :result_comments, foreign_key: :associated_id, dependent: :destroy
has_many :result_orderable_elements, inverse_of: :result, dependent: :destroy
has_many :result_assets, inverse_of: :result, dependent: :destroy
has_many :assets, through: :result_assets
has_many :result_tables, inverse_of: :result, dependent: :destroy
has_many :tables, through: :result_tables
has_many :result_texts, inverse_of: :result, dependent: :destroy
has_many :result_comments, inverse_of: :result, foreign_key: :associated_id, dependent: :destroy
has_many :report_elements, inverse_of: :result, dependent: :destroy
accepts_nested_attributes_for :result_text
accepts_nested_attributes_for :asset
accepts_nested_attributes_for :table
accepts_nested_attributes_for :result_texts
accepts_nested_attributes_for :assets
accepts_nested_attributes_for :tables
def self.search(user,
include_archived,
@ -37,7 +37,7 @@ class Result < ApplicationRecord
new_query =
Result
.distinct
.joins('LEFT JOIN result_texts ON results.id = result_texts.result_id')
.left_outer_joins(:result_texts)
.where(results: { my_module_id: module_ids })
.where_attributes_like(['results.name', 'result_texts.text'], query, options)
@ -66,7 +66,7 @@ class Result < ApplicationRecord
end
def space_taken
is_asset ? result_asset.space_taken : 0
result_assets.joins(asset: { file_attachment: :blob }).sum('active_storage_blobs.byte_size')
end
def last_comments(last_id = 1, per_page = Constants::COMMENTS_SEARCH_LIMIT)
@ -80,29 +80,19 @@ class Result < ApplicationRecord
end
def is_text
result_text.present?
raise StandardError, 'Deprecated method, needs to be replaced!'
end
def is_table
table.present?
raise StandardError, 'Deprecated method, needs to be replaced!'
end
def is_asset
asset.present?
raise StandardError, 'Deprecated method, needs to be replaced!'
end
def unlocked?(result)
if result.is_asset
!result.asset.locked?
else
true
end
end
def editable?
return false if is_asset && asset.file.metadata['asset_type'] == 'marvinjs'
true
result.assets.none?(&:locked?)
end
def comments

View file

@ -1,9 +1,7 @@
# frozen_string_literal: true
class ResultAsset < ApplicationRecord
validates :result, :asset, presence: true
belongs_to :result, inverse_of: :result_asset, touch: true
belongs_to :result, inverse_of: :result_assets, touch: true
belongs_to :asset, inverse_of: :result_asset, dependent: :destroy
def space_taken

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
class ResultOrderableElement < ApplicationRecord
validates :position, uniqueness: { scope: :step }
validate :check_result_relations
around_destroy :decrement_following_elements_positions
belongs_to :result, inverse_of: :result_orderable_elements, touch: true
belongs_to :orderable, polymorphic: true, inverse_of: :result_orderable_element
private
def check_step_relations
if step != orderable.step
errors.add(
:step_orderable_element,
I18n.t('activerecord.errors.models.result_orderable_element.attributes.result.wrong_result')
)
end
end
def decrement_following_elements_positions
result.with_lock do
yield
result.result_orderable_elements
.where('position > ?', position)
.order(position: :asc).each do |result_orderable_element|
# find_each ignore any ordering
result_orderable_element.position -= 1
result_orderable_element.save!
end
end
end
end

View file

@ -1,8 +1,7 @@
# frozen_string_literal: true
class ResultTable < ApplicationRecord
validates :result, :table, presence: true
belongs_to :result, inverse_of: :result_table
belongs_to :result, inverse_of: :result_tables
belongs_to :table, inverse_of: :result_table, dependent: :destroy, touch: true
has_one :result_orderable_element, as: :orderable, dependent: :destroy
end

View file

@ -4,8 +4,8 @@ class ResultText < ApplicationRecord
include TinyMceImages
auto_strip_attributes :text, nullify: false
validates :result, :text, presence: true
validates :text, length: { maximum: Constants::RICH_TEXT_MAX_LENGTH }
belongs_to :result, inverse_of: :result_text, touch: true
belongs_to :result, inverse_of: :result_texts, touch: true
has_one :result_orderable_element, as: :orderable, dependent: :destroy
end

View file

@ -1,8 +1,6 @@
# frozen_string_literal: true
class StepAsset < ApplicationRecord
validates :step, :asset, presence: true
belongs_to :step, inverse_of: :step_assets, touch: true
belongs_to :asset, inverse_of: :step_asset, dependent: :destroy
end

View file

@ -1,8 +1,6 @@
# frozen_string_literal: true
class StepTable < ApplicationRecord
validates :step, :table, presence: true
belongs_to :step, inverse_of: :step_tables, touch: true
belongs_to :table, inverse_of: :step_table
has_one :step_orderable_element, as: :orderable, dependent: :destroy

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
class ResultSerializer < ActiveModel::Serializer
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
include ApplicationHelper
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
attributes :name, :id, :urls, :updated_at, :created_at_formatted, :updated_at_formatted, :user
def updated_at
object.updated_at.to_i
end
def user
{
avatar: object.user&.avatar_url(:icon_small),
name: object.user&.full_name
}
end
def created_at_formatted
I18n.l(object.created_at, format: :full)
end
def updated_at_formatted
I18n.l(object.updated_at, format: :full)
end
def urls
{
}
end
end

View file

@ -11,7 +11,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-options pull-right">
<% if result.editable? && can_manage_my_module?(result.my_module) %>
<% if can_manage_my_module?(result.my_module) %>
<a class="btn btn-light icon-btn edit-result-button <%= edit_result_button_class(result) %>" id="<%= result.id %>_edit" href="<%= edit_result_link(result) %>" data-remote="true" title="<%= t'my_modules.results.options.edit_title' %>">
<span class="sn-icon sn-icon-edit"></span>
</a>

View file

@ -1,95 +0,0 @@
<% provide(:head_title, t("my_modules.results.head_title", project: h(@project.name), module: h(@my_module.name)).html_safe) %>
<% provide(:sidebar_title, t("sidebar.my_module.sidebar_title")) %>
<%= content_for :sidebar do %>
<%= render partial: "shared/sidebar/#{@my_module.archived_branch? ? 'archived_my_module' : 'my_module'}",
locals: {
my_modules: @experiment_my_modules,
experiment: @my_module.experiment,
current_my_module: @my_module
}
%>
<% end %>
<%= render partial: 'shared/drag_n_drop_overlay' %>
<%= render partial: 'assets/wopi/create_wopi_file_modal' %>
<% provide(:container_class, 'no-second-nav-container') %>
<div class="content-pane flexible">
<%= render partial: 'my_modules/header' %>
<%= render partial: 'my_modules/header_actions' %>
<div class="mt-5" id="results-toolbar">
<% if can_manage_my_module?(@my_module) %>
<div class="add-result-dropdown dropdown sci-dropdown">
<a href="#" id="add-result-button" class="btn btn-secondary" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="sn-icon sn-icon-new-task"></span>
<span><%= t("my_modules.results.add_label") %></span>
<span class="sn-icon sn-icon-down"></span>
</a>
<ul class="dropdown-menu add-result-dropdown" aria-labelledby="add-result-button">
<li id="new-result-text" data-href="<%= new_my_module_result_text_path(my_module_id: @my_module.id, page: params[:page], order: params[:order]) %>">
<i class="sn-icon sn-icon-result-text"></i><%= t("my_modules.results.new_text_result") %>
</li>
<li id="new-result-table" data-href="<%= new_my_module_result_table_path(my_module_id: @my_module.id, page: params[:page], order: params[:order]) %>">
<i class="sn-icon sn-icon-tables"></i><%= t("my_modules.results.new_table_result") %>
</li>
<li id="new-result-asset" data-href="<%= new_my_module_result_asset_path(my_module_id: @my_module.id, page: params[:page], order: params[:order]) %>">
<i class="sn-icon sn-icon-files"></i><%= t("my_modules.results.new_asset_result") %>
</li>
<%= render partial: '/assets/marvinjs/create_marvin_sketch_li',
locals: { element_id: @my_module.id, element_type: 'Result', sketch_container: "#results[data-module-id=#{@my_module.id}]" } %>
<%= render partial: "assets/wopi/create_wopi_file_li",
locals: { element_id: @my_module.id, element_type: 'Result' } %>
</ul>
</div>
<% end %>
<div class="sci-btn-group collapse-expand-result">
<button class="btn btn-light" id="results-collapse-btn">
<span class="sn-icon sn-icon-up"></span>
<span class="hidden-xs-custom"><%= t'my_modules.results.collapse_label' %></span>
</button>
<button class="btn btn-light" id="results-expand-btn">
<span class="sn-icon sn-icon-down"></span>
<span class="hidden-xs-custom"><%= t'my_modules.results.expand_label' %></span>
</button>
</div>
<div class="sort-result-dropdown dropdown">
<button id="sort-result-button" class="btn btn-light icon-btn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="sn-icon sn-icon-sort-up"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="sort-result-button">
<li><%= link_to t('general.sort_new.atoz'), results_my_module_path(@my_module, page: params[:page], order: 'atoz'), class: (@results_order == 'atoz' ? 'selected' : '') %></li>
<li><%= link_to t('general.sort_new.ztoa'), results_my_module_path(@my_module, page: params[:page], order: 'ztoa'), class: (@results_order == 'ztoa' ? 'selected' : '') %></li>
<li><%= link_to t('general.sort_new.old'), results_my_module_path(@my_module, page: params[:page], order: 'old'), class: (@results_order == 'old' ? 'selected' : '') %></li>
<li><%= link_to t('general.sort_new.new'), results_my_module_path(@my_module, page: params[:page], order: 'new'), class: (@results_order == 'new' ? 'selected' : '') %></li>
<li><%= link_to t('general.sort_new.old_updated'), results_my_module_path(@my_module, page: params[:page], order: 'old_updated'), class: (@results_order == 'old_updated' ? 'selected' : '') %></li>
<li><%= link_to t('general.sort_new.new_updated'), results_my_module_path(@my_module, page: params[:page], order: 'new_updated'), class: (@results_order == 'new_updated' ? 'selected' : '') %></li>
</ul>
</div>
</div>
<div style="height: 15px;"></div>
<div id="results" data-module-id="<%= @my_module.id %>" data-task-id="<%= @my_module.id %>">
<% @results.each do |result| %>
<%= render partial: "result", locals: { result: result } %>
<% end %>
</div>
<div class="kaminari-pagination">
<%= paginate @results, outer_window: 1, window: 1 %>
</div>
</div>
<%= javascript_include_tag "handsontable.full" %>
<!-- Libraries for formulas -->
<%= render partial: "shared/formulas_libraries" %>
<%= javascript_include_tag "assets/wopi/create_wopi_file" %>
<%= javascript_include_tag "my_modules/results" %>
<%= javascript_include_tag "results/result_texts" %>
<%= javascript_include_tag "results/result_tables" %>
<%= javascript_include_tag "results/result_assets" %>
<%= javascript_include_tag 'pdf_js' %>
<%= stylesheet_link_tag 'pdf_js_styles' %>
<%= render 'shared/tiny_mce_packs' %>

View file

@ -0,0 +1,21 @@
<% provide(:head_title, t("my_modules.results.head_title", project: h(@project.name), module: h(@my_module.name)).html_safe) %>
<% provide(:sidebar_title, t("sidebar.my_module.sidebar_title")) %>
<%= content_for :sidebar do %>
<%= render partial: "shared/sidebar/#{@my_module.archived_branch? ? 'archived_my_module' : 'my_module'}",
locals: {
my_modules: @experiment.my_modules,
experiment: @experiment,
current_my_module: @my_module
}
%>
<% end %>
<%= render partial: 'shared/drag_n_drop_overlay' %>
<%= render partial: 'assets/wopi/create_wopi_file_modal' %>
<% provide(:container_class, 'no-second-nav-container') %>
<div id="results" data-behaviour="vue">
<results url="<%= my_module_results_url(@my_module) %>">
</div>
<%= javascript_include_tag 'vue_results' %>

View file

@ -214,7 +214,7 @@ class Extends
ACTIVITY_MESSAGE_ITEMS_TYPES =
ACTIVITY_SUBJECT_TYPES + %w(
User Tag RepositoryColumn RepositoryRow Step Asset TinyMceAsset
User Tag RepositoryColumn RepositoryRow Step Result Asset TinyMceAsset
Repository MyModuleStatus RepositorySnapshot
).freeze
@ -448,7 +448,8 @@ class Extends
team_sharing_tasks_disabled: 245,
task_link_sharing_enabled: 246,
task_link_sharing_disabled: 247,
shared_task_message_edited: 248
shared_task_message_edited: 248,
result_content_rearranged: 244
}
ACTIVITY_GROUPS = {

View file

@ -192,6 +192,10 @@ en:
attributes:
step:
wrong_step: "connected to the wrong step"
result_orderable_element:
attributes:
result:
wrong_result: "connected to the wrong result"
repository_snapshot:
attributes:
selected:
@ -1208,7 +1212,7 @@ en:
load_from_file_protocol_general_error: "Failed to load the protocol from file. It is likely that certain fields (protocol and individual step titles and names) contain too many or too few characters.(max is %{max} and min is %{min})"
results:
head_title: "%{project} | %{module} | Results"
add_label: "Add new result"
add_label: "New result"
new_text_result: "Text"
new_table_result: "Table"
new_asset_result: "File"
@ -1224,6 +1228,13 @@ en:
info_tab: "Info"
comments_tab: "Comments"
comment_title: "%{user} at %{time}:"
sorts:
updated_at_asc: "Modified first"
updated_at_desc: "Modified last"
created_at_asc: "Created first"
created_at_desc: "Created last"
name_asc: "Name A to Z"
name_desc: "Name Z to A"
options:
comment_title: "Comments"
no_comments: "No comments"

View file

@ -214,6 +214,7 @@ en:
task_inventory_item_stock_consumed_html: "%{user} changed consumption of inventory item %{repository_row} from %{stock_consumption_was} %{unit} to %{stock_consumption} %{unit} on task %{my_module} \n%{comment}"
task_steps_rearranged_html: "%{user} rearranged protocol's steps on task %{my_module}"
task_step_content_rearranged_html: "%{user} rearranged content of protocol's step %{step_position} <strong>%{step}</strong> on task %{my_module}"
result_content_rearranged_html: "%{user} rearranged result's content on task %{my_module}"
task_step_file_added_html: "%{user} added file <strong>%{file}</strong> to protocol's step %{step_position} <strong>%{step}</strong> on task %{my_module}"
task_step_file_deleted_html: "%{user} deleted file <strong>%{file}</strong> in protocol's step %{step_position} <strong>%{step}</strong> on task %{my_module}"
protocol_steps_rearranged_html: "%{user} rearranged protocol's steps in protocol %{protocol} in Protocol repository"

View file

@ -317,6 +317,7 @@ Rails.application.routes.draw do
end
resources :my_modules, only: %i(show) do
member do
get :tree
end
@ -525,7 +526,6 @@ Rails.application.routes.draw do
get 'protocols' # Protocols view for single module
get 'protocol', to: 'my_modules#protocol', as: 'protocol'
patch 'protocol', to: 'my_modules#update_protocol', as: 'update_protocol'
get 'results' # Results view for single module
get 'archive' # Archive view for single module
end
@ -533,6 +533,11 @@ Rails.application.routes.draw do
# to preserve original id parameters in URL.
get 'tags/edit', to: 'my_module_tags#index_edit'
get 'users/edit', to: 'user_my_modules#index_edit'
resources :results, only: %i(index show create update destroy) do
get :elements
get :assets
end
end
resources :steps, only: %i(index update destroy show) do

View file

@ -29,6 +29,7 @@ const entryList = {
pdf_js_worker: './app/javascript/packs/pdfjs/pdf_js_worker.js',
vue_label_template: './app/javascript/packs/vue/label_template.js',
vue_protocol: './app/javascript/packs/vue/protocol.js',
vue_results: './app/javascript/packs/vue/results.js',
vue_repository_filter: './app/javascript/packs/vue/repository_filter.js',
vue_repository_search: './app/javascript/packs/vue/repository_search.js',
vue_repository_print_modal: './app/javascript/packs/vue/repository_print_modal.js',

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
class CreateResultOrderableElements < ActiveRecord::Migration[7.0]
def up
create_table :result_orderable_elements do |t|
t.references :result, null: false, index: false, foreign_key: true
t.integer :position, null: false
t.references :orderable, polymorphic: true
t.timestamps
t.index %i(result_id position), unique: true
end
ActiveRecord::Base.connection.execute(
'INSERT INTO ' \
'result_orderable_elements(result_id, position, orderable_type, orderable_id, created_at, updated_at) ' \
'SELECT result_id, 0, \'ResultText\', id, NOW(), NOW() FROM result_texts;'
)
ActiveRecord::Base.connection.execute(
'INSERT INTO ' \
'result_orderable_elements(result_id, position, orderable_type, orderable_id, created_at, updated_at) ' \
'SELECT result_id, 0, \'ResultTable\', id, NOW(), NOW() FROM result_tables;'
)
end
def down
ResultOrderableElement.delete_all
drop_table :result_orderable_elements
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_07_20_070830) do
ActiveRecord::Schema[7.0].define(version: 2023_07_25_120831) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gist"
enable_extension "pg_trgm"
@ -903,6 +903,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_20_070830) do
t.index ["result_id", "asset_id"], name: "index_result_assets_on_result_id_and_asset_id"
end
create_table "result_orderable_elements", force: :cascade do |t|
t.bigint "result_id", null: false
t.integer "position", null: false
t.string "orderable_type"
t.bigint "orderable_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["orderable_type", "orderable_id"], name: "index_result_orderable_elements_on_orderable"
t.index ["result_id", "position"], name: "index_result_orderable_elements_on_result_id_and_position", unique: true
end
create_table "result_tables", force: :cascade do |t|
t.bigint "result_id", null: false
t.bigint "table_id", null: false
@ -1429,6 +1440,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_20_070830) do
add_foreign_key "repository_text_values", "users", column: "last_modified_by_id"
add_foreign_key "result_assets", "assets"
add_foreign_key "result_assets", "results"
add_foreign_key "result_orderable_elements", "results"
add_foreign_key "result_tables", "results"
add_foreign_key "result_tables", "tables"
add_foreign_key "result_texts", "results"

View file

@ -49,6 +49,7 @@
"@joeattardi/emoji-button": "^4.6.2",
"ajv": "6.12.6",
"autoprefixer": "10.4.14",
"axios": "^1.4.0",
"babel-loader": "^8.2.5",
"babel-plugin-macros": "^3.1.0",
"bootstrap": "^3.4.1",

View file

@ -1900,6 +1900,15 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axios@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-0.1.0.tgz#62f59dbc59c9f9242759ca349960e7a2fe3c36c0"
@ -3606,6 +3615,11 @@ focus-trap@^5.1.0:
tabbable "^4.0.0"
xtend "^4.0.1"
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -3622,6 +3636,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@ -5805,6 +5828,11 @@ prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"