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 @@ +
+ + +

+
+ + +
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