diff --git a/app/controllers/result_elements/base_controller.rb b/app/controllers/result_elements/base_controller.rb
new file mode 100644
index 000000000..5ed0694f8
--- /dev/null
+++ b/app/controllers/result_elements/base_controller.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module ResultElements
+ class BaseController < ApplicationController
+ before_action :load_result_and_my_module
+ before_action :check_manage_permissions
+
+ private
+
+ def load_result_and_my_module
+ @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_my_module?(@my_module)
+ end
+
+ def create_in_result!(result, new_orderable)
+ ActiveRecord::Base.transaction do
+ new_orderable.save!
+
+ result.result_orderable_elements.create!(
+ position: result.result_orderable_elements.length,
+ orderable: new_orderable
+ )
+ end
+ end
+
+ def render_result_orderable_element(orderable)
+ result_orderable_element = orderable.result_orderable_element
+ render json: result_orderable_element, serializer: ResultOrderableElementSerializer, user: current_user
+ end
+
+ def log_step_activity(element_type_of, message_items)
+ # TODO
+ #message_items[:my_module] = @protocol.my_module.id if @protocol.in_module?
+ #Activities::CreateActivityService.call(
+ # activity_type: "#{!@step.protocol.in_module? ? 'protocol_step_' : 'task_step_'}#{element_type_of}",
+ # owner: current_user,
+ # team: @protocol.team,
+ # project: @protocol.in_module? ? @protocol.my_module.project : nil,
+ # subject: @protocol,
+ # message_items: {
+ # step: @step.id,
+ # step_position: {
+ # id: @step.id,
+ # value_for: 'position_plus_one'
+ # },
+ # }.merge(message_items)
+ #)
+ end
+ end
+end
diff --git a/app/controllers/result_elements/tables_controller.rb b/app/controllers/result_elements/tables_controller.rb
new file mode 100644
index 000000000..533a135ff
--- /dev/null
+++ b/app/controllers/result_elements/tables_controller.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module ResultElements
+ class TablesController < BaseController
+ before_action :load_table, only: %i(update destroy duplicate)
+
+ def create
+ predefined_table_dimensions = create_table_params[:tableDimensions].map(&:to_i)
+ name = if predefined_table_dimensions[0] == predefined_table_dimensions[1]
+ t('protocols.steps.table.default_name',
+ position: @step.step_tables.length + 1)
+ else
+ t('protocols.steps.plate.default_name',
+ position: @step.step_tables.length + 1)
+ end
+ result_table = @result.result_tables.new(table:
+ Table.new(
+ name: name,
+ contents: { data: Array.new(predefined_table_dimensions[0],
+ Array.new(predefined_table_dimensions[1], '')) }.to_json,
+ metadata: { plateTemplate: create_table_params[:plateTemplate] == 'true' },
+ created_by: current_user,
+ team: @my_module.team
+ ))
+
+ ActiveRecord::Base.transaction do
+ create_in_step!(@step, step_table)
+ # log_step_activity(:table_added, { table_name: step_table.table.name })
+ end
+
+ render_result_orderable_element(step_table)
+ rescue ActiveRecord::RecordInvalid
+ head :unprocessable_entity
+ end
+
+ def update
+ ActiveRecord::Base.transaction do
+ @table.assign_attributes(table_params.except(:metadata))
+ begin
+ if table_params[:metadata].present?
+
+ @table.metadata = if @table.metadata
+ @table.metadata.merge(JSON.parse(table_params[:metadata]))
+ else
+ JSON.parse(table_params[:metadata])
+ end
+ end
+ rescue JSON::ParserError
+ @table.metadata = {}
+ end
+ @table.save!
+ #log_step_activity(:table_edited, { table_name: @table.name })
+ end
+
+ render json: @table, serializer: ResultTableSerializer, user: current_user
+ rescue ActiveRecord::RecordInvalid
+ head :unprocessable_entity
+ end
+
+ def destroy
+ if @table.destroy
+ #log_step_activity(:table_deleted, { table_name: @table.name })
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def duplicate
+ #ActiveRecord::Base.transaction do
+ # position = @table.step_table.step_orderable_element.position
+ # @step.step_orderable_elements.where('position > ?', position).order(position: :desc).each do |element|
+ # element.update(position: element.position + 1)
+ # end
+ # @table.name += ' (1)'
+ # new_table = @table.duplicate(@step, current_user, position + 1)
+ # log_step_activity(:table_duplicated, { table_name: new_table.name })
+ # render_step_orderable_element(new_table.step_table)
+ #end
+ rescue ActiveRecord::RecordInvalid
+ head :unprocessable_entity
+ end
+
+ private
+
+ def table_params
+ params.permit(:name, :contents, :metadata)
+ end
+
+ def create_table_params
+ params.permit(:plateTemplate, tableDimensions: [])
+ end
+
+ def load_table
+ @table = @result.tables.find_by(id: params[:id])
+ return render_404 unless @table
+ end
+ end
+end
diff --git a/app/controllers/result_elements/texts_controller.rb b/app/controllers/result_elements/texts_controller.rb
new file mode 100644
index 000000000..219f896cc
--- /dev/null
+++ b/app/controllers/result_elements/texts_controller.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module ResultElements
+ class TextsController < BaseController
+ include ActionView::Helpers::UrlHelper
+ include ApplicationHelper
+ include InputSanitizeHelper
+ include Rails.application.routes.url_helpers
+
+ before_action :load_result_text, only: %i(update destroy duplicate)
+
+ def create
+ result_text = @result.result_texts.build
+
+ ActiveRecord::Base.transaction do
+ create_in_step!(@result, result_text)
+ #log_step_activity(:text_added, { text_name: step_text.name })
+ end
+
+ render_result_orderable_element(result_text)
+ rescue ActiveRecord::RecordInvalid
+ head :unprocessable_entity
+ end
+
+ def update
+ old_text = @result_text.text
+ ActiveRecord::Base.transaction do
+ @result_text.update!(result_text_params)
+ TinyMceAsset.update_images(@result_text, params[:tiny_mce_images], current_user)
+ #log_step_activity(:text_edited, { text_name: @step_text.name })
+ result_annotation_notification(old_text)
+ end
+
+ render json: @result_text, serializer: ResultTextSerializer, user: current_user
+ rescue ActiveRecord::RecordInvalid
+ render json: @result_text.errors, status: :unprocessable_entity
+ end
+
+ def destroy
+ if @result_text.destroy
+ log_step_activity(:text_deleted, { text_name: @result_text.name })
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def duplicate
+ #ActiveRecord::Base.transaction do
+ # position = @step_text.step_orderable_element.position
+ # @step.step_orderable_elements.where('position > ?', position).order(position: :desc).each do |element|
+ # element.update(position: element.position + 1)
+ # end
+ # new_step_text = @step_text.duplicate(@step, position + 1)
+ # log_step_activity(:text_duplicated, { text_name: new_step_text.name })
+ # render_step_orderable_element(new_step_text)
+ #end
+ rescue ActiveRecord::RecordInvalid
+ head :unprocessable_entity
+ end
+
+ private
+
+ def result_text_params
+ params.require(:text_component).permit(:text)
+ end
+
+ def load_result_text
+ @result_text = @result.result_texts.find_by(id: params[:id])
+ return render_404 unless @result_text
+ end
+
+ def result_annotation_notification(old_text = nil)
+ smart_annotation_notification(
+ old_text: (old_text if old_text),
+ new_text: @result_text.text,
+ title: t('notifications.result_annotation_title',
+ result: @result.name,
+ user: current_user.full_name),
+ message: t('notifications.result_annotation_message_html',
+ project: link_to(@result.my_module.experiment.project.name,
+ project_url(@result.my_module
+ .experiment
+ .project)),
+ experiment: link_to(@result.my_module.experiment.name,
+ my_modules_experiment_url(@result.my_module
+ .experiment)),
+ my_module: link_to(@result.my_module.name,
+ protocols_my_module_url(
+ @result.my_module
+ )))
+ )
+ end
+ end
+end
diff --git a/app/javascript/packs/vue/results.js b/app/javascript/packs/vue/results.js
index 5a6909d68..4ee45cd90 100644
--- a/app/javascript/packs/vue/results.js
+++ b/app/javascript/packs/vue/results.js
@@ -4,6 +4,7 @@ import Results from '../../vue/results/results.vue';
Vue.use(TurbolinksAdapter);
Vue.prototype.i18n = window.I18n;
+Vue.prototype.ActiveStoragePreviews = window.ActiveStoragePreviews;
new Vue({
el: '#results',
diff --git a/app/javascript/vue/protocol/step.vue b/app/javascript/vue/protocol/step.vue
index a9a4717d6..d40210e3d 100644
--- a/app/javascript/vue/protocol/step.vue
+++ b/app/javascript/vue/protocol/step.vue
@@ -222,7 +222,7 @@
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
import UtilsMixin from '../mixins/utils.js'
- import AttachmentsMixin from './mixins/attachments.js'
+ import AttachmentsMixin from '../shared/content/mixins/attachments.js'
import WopiFileModal from '../shared/content/attachments/mixins/wopi_file_modal.js'
import StorageUsage from '../shared/content/attachments/storage_usage.vue'
diff --git a/app/javascript/vue/results/result.vue b/app/javascript/vue/results/result.vue
index d319bb50f..8a70e361f 100644
--- a/app/javascript/vue/results/result.vue
+++ b/app/javascript/vue/results/result.vue
@@ -5,6 +5,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
{}"
+ @attachments:viewMode="() => {}"
+ @attachment:viewMode="() => {}"/>
+
diff --git a/app/javascript/vue/protocol/mixins/attachments.js b/app/javascript/vue/shared/content/mixins/attachments.js
similarity index 75%
rename from app/javascript/vue/protocol/mixins/attachments.js
rename to app/javascript/vue/shared/content/mixins/attachments.js
index a5c9f5413..2b660ca45 100644
--- a/app/javascript/vue/protocol/mixins/attachments.js
+++ b/app/javascript/vue/shared/content/mixins/attachments.js
@@ -10,6 +10,14 @@ export default {
}
};
},
+ computed: {
+ attachmentsParent() {
+ return this.step || this.result;
+ },
+ attachmentsParentName() {
+ return this.step ? 'step' : 'result';
+ }
+ },
methods: {
dropFile(e) {
if (!this.showFileModal && e.dataTransfer && e.dataTransfer.files.length) {
@@ -30,7 +38,7 @@ export default {
button.click();
},
openWopiFileModal() {
- this.initWopiFileModal(this.step, (_e, data, status) => {
+ this.initWopiFileModal(this.attachmentsParent, (_e, data, status) => {
if (status === 'success') {
this.addAttachment(data)
} else {
@@ -43,17 +51,17 @@ export default {
let filesUploadedCntr = 0;
this.showFileModal = false;
- if (!this.step.attributes.urls.upload_attachment_url) return false;
+ if (!this.attachmentsParent.attributes.urls.upload_attachment_url) return false;
return new Promise((resolve, reject) => {
$(files).each((_, file) => {
const fileObject = {
attributes: {
progress: 0,
- view_mode: this.step.attributes.assets_view_mode,
+ view_mode: this.attachmentsParent.attributes.assets_view_mode,
file_name: file.name,
uploading: true,
- asset_order: this.viewModeOrder[this.step.attributes.assets_view_mode]
+ asset_order: this.viewModeOrder[this.attachmentsParent.attributes.assets_view_mode]
},
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener('progress', (e) => {
@@ -68,16 +76,16 @@ export default {
return;
}
- const storageLimit = this.step.attributes.storage_limit &&
- this.step.attributes.storage_limit.total > 0 &&
- this.step.attributes.storage_limit.used >= this.step.attributes.storage_limit.total;
+ const storageLimit = this.attachmentsParent.attributes.storage_limit &&
+ this.attachmentsParent.attributes.storage_limit.total > 0 &&
+ this.attachmentsParent.attributes.storage_limit.used >= this.attachmentsParent.attributes.storage_limit.total;
if (storageLimit) {
fileObject.error = I18n.t('protocols.steps.attachments.new.no_more_space');
this.attachments.push(fileObject);
return;
}
- const upload = new ActiveStorage.DirectUpload(file, this.step.attributes.urls.direct_upload_url, fileObject);
+ const upload = new ActiveStorage.DirectUpload(file, this.attachmentsParent.attributes.urls.direct_upload_url, fileObject);
fileObject.isNewUpload = true;
this.attachments.push(fileObject);
@@ -93,7 +101,7 @@ export default {
reject(error);
} else {
const signedId = blob.signed_id;
- $.post(this.step.attributes.urls.upload_attachment_url, {
+ $.post(this.attachmentsParent.attributes.urls.upload_attachment_url, {
signed_blob_id: signedId
}, (result) => {
fileObject.id = result.data.id;
@@ -108,7 +116,7 @@ export default {
filesUploadedCntr += 1;
if (filesUploadedCntr === filesToUploadCntr) {
setTimeout(() => {
- this.$emit('stepUpdated');
+ this.$emit(`${this.attachmentsParentName}Updated`);
}, 1000);
resolve('done');
}
@@ -118,18 +126,18 @@ export default {
});
},
changeAttachmentsOrder(order) {
- this.step.attributes.assets_order = order;
- $.post(this.step.attributes.urls.update_view_state_step_url, {
+ this.attachmentsParent.attributes.assets_order = order;
+ $.post(this.attachmentsParent.attributes.urls.update_view_state_url, {
assets: { order }
});
},
changeAttachmentsViewMode(viewMode) {
- this.step.attributes.assets_view_mode = viewMode;
+ this.attachmentsParent.attributes.assets_view_mode = viewMode;
this.attachments.forEach((attachment) => {
this.$set(attachment.attributes, 'view_mode', viewMode);
this.$set(attachment.attributes, 'asset_order', this.viewModeOrder[viewMode]);
});
- $.post(this.step.attributes.urls.update_asset_view_mode_url, {
+ $.post(this.attachmentsParent.attributes.urls.update_asset_view_mode_url, {
assets_view_mode: viewMode
});
},
diff --git a/app/serializers/result_orderable_element_serializer.rb b/app/serializers/result_orderable_element_serializer.rb
new file mode 100644
index 000000000..42afb21fa
--- /dev/null
+++ b/app/serializers/result_orderable_element_serializer.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ResultOrderableElementSerializer < ActiveModel::Serializer
+ attributes :position, :orderable, :orderable_type
+
+ def orderable
+ case object.orderable_type
+ when 'ResultTable'
+ ResultTableSerializer.new(object.orderable.table, scope: { user: @instance_options[:user] }).as_json
+ when 'ResultText'
+ ResultTextSerializer.new(object.orderable, scope: { user: @instance_options[:user] }).as_json
+ end
+ end
+end
diff --git a/app/serializers/result_serializer.rb b/app/serializers/result_serializer.rb
index 5350627f7..30ac424f1 100644
--- a/app/serializers/result_serializer.rb
+++ b/app/serializers/result_serializer.rb
@@ -7,7 +7,7 @@ class ResultSerializer < ActiveModel::Serializer
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
- attributes :name, :id, :urls, :updated_at, :created_at_formatted, :updated_at_formatted, :user
+ attributes :name, :id, :urls, :updated_at, :created_at_formatted, :updated_at_formatted, :user, :my_module_id
def updated_at
object.updated_at.to_i
@@ -30,7 +30,8 @@ class ResultSerializer < ActiveModel::Serializer
def urls
{
-
+ elements_url: elements_my_module_result_path(object.my_module, object),
+ attachments_url: assets_my_module_result_path(object.my_module, object)
}
end
end
diff --git a/app/serializers/result_table_serializer.rb b/app/serializers/result_table_serializer.rb
new file mode 100644
index 000000000..22b0f690f
--- /dev/null
+++ b/app/serializers/result_table_serializer.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class ResultTableSerializer < ActiveModel::Serializer
+ include Canaid::Helpers::PermissionsHelper
+ include Rails.application.routes.url_helpers
+
+ attributes :name, :contents, :urls, :icon, :metadata
+
+ def contents
+ object.contents_utf_8
+ end
+
+ def icon
+ 'fa-table'
+ end
+
+ def urls
+ return if object.destroyed?
+
+ object.reload unless object.result
+
+ p object.result
+ p scope[:user] || @instance_options[:user]
+ p can_manage_result?(scope[:user] || @instance_options[:user], object.result)
+ return {} unless can_manage_result?(scope[:user] || @instance_options[:user], object.result)
+
+ {
+ duplicate_url: duplicate_my_module_result_table_path(object.result.my_module, object.result, object),
+ delete_url: my_module_result_table_path(object.result.my_module, object.result, object),
+ update_url: my_module_result_table_path(object.result.my_module, object.result, object)
+ }
+ end
+end
diff --git a/app/serializers/result_text_serializer.rb b/app/serializers/result_text_serializer.rb
new file mode 100644
index 000000000..0e4b17514
--- /dev/null
+++ b/app/serializers/result_text_serializer.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class ResultTextSerializer < ActiveModel::Serializer
+ include Canaid::Helpers::PermissionsHelper
+ include Rails.application.routes.url_helpers
+ include ApplicationHelper
+ include ActionView::Helpers::TextHelper
+
+ attributes :id, :text, :urls, :text_view, :icon, :placeholder
+
+ def updated_at
+ object.updated_at.to_i
+ end
+
+ def placeholder
+ I18n.t('protocols.steps.text.placeholder')
+ end
+
+ def text_view
+ @user = scope[:user]
+ custom_auto_link(object.tinymce_render('text'),
+ simple_format: false,
+ tags: %w(img),
+ team: object.result.my_module.team)
+ end
+
+ def text
+ sanitize_input(object.tinymce_render('text'))
+ end
+
+ def icon
+ 'fa-font'
+ end
+
+ def urls
+ result = object.result
+
+ return {} if object.destroyed? || !can_manage_result?(scope[:user] || @instance_options[:user], result)
+
+ {
+ duplicate_url: duplicate_my_module_result_text_path(result.my_module, result, object),
+ delete_url: my_module_result_text_path(result.my_module, result, object),
+ update_url: my_module_result_text_path(result.my_module, result, object)
+ }
+ end
+end
diff --git a/app/serializers/step_serializer.rb b/app/serializers/step_serializer.rb
index c4e0b4af8..359072371 100644
--- a/app/serializers/step_serializer.rb
+++ b/app/serializers/step_serializer.rb
@@ -84,7 +84,7 @@ class StepSerializer < ActiveModel::Serializer
create_text_url: step_texts_path(object),
create_checklist_url: step_checklists_path(object),
update_asset_view_mode_url: update_asset_view_mode_step_path(object),
- update_view_state_step_url: update_view_state_step_path(object),
+ update_view_state_url: update_view_state_step_path(object),
direct_upload_url: rails_direct_uploads_url,
upload_attachment_url: upload_attachment_step_path(object),
reorder_elements_url: reorder_step_step_orderable_elements_path(step_id: object.id)
diff --git a/app/views/results/index.html.erb b/app/views/results/index.html.erb
index f6f5d891a..1521e3592 100644
--- a/app/views/results/index.html.erb
+++ b/app/views/results/index.html.erb
@@ -18,4 +18,8 @@
+<%= javascript_include_tag "handsontable.full" %>
+<%= render partial: "shared/formulas_libraries" %>
+<%= render 'shared/tiny_mce_packs' %>
<%= javascript_include_tag 'vue_results' %>
+
diff --git a/config/routes.rb b/config/routes.rb
index 6d57c62be..ad0253a69 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -535,8 +535,21 @@ Rails.application.routes.draw do
get 'users/edit', to: 'user_my_modules#index_edit'
resources :results, only: %i(index show create update destroy) do
- get :elements
- get :assets
+ member do
+ get :elements
+ get :assets
+ end
+
+ resources :tables, controller: 'result_elements/tables', only: %i(create destroy update) do
+ member do
+ post :duplicate
+ end
+ end
+ resources :texts, controller: 'result_elements/texts', only: %i(create destroy update) do
+ member do
+ post :duplicate
+ end
+ end
end
end