Add elements and attachments to new result [SCI-8953]

This commit is contained in:
Anton 2023-08-10 16:48:08 +02:00
parent ac37885885
commit f6191dbb67
14 changed files with 531 additions and 23 deletions

View file

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

View file

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

View file

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

View file

@ -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',

View file

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

View file

@ -5,6 +5,68 @@
<button @click="openReorderModal">
Open Rearrange Modal
</button>
<div>
<input type="file" class="hidden" ref="fileSelector" @change="loadFromComputer" multiple />
<div ref="elementsDropdownButton" v-if="urls.update_url" class="dropdown">
<button class="btn btn-light dropdown-toggle insert-button" type="button" :id="'stepInserMenu_' + step.id" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{ i18n.t('protocols.steps.insert.button') }}
<span class="sn-icon sn-icon-down"></span>
</button>
<ul ref="elementsDropdown" class="dropdown-menu insert-element-dropdown dropdown-menu-right" :aria-labelledby="'stepInserMenu_' + step.id">
<li class="title">
{{ i18n.t('protocols.steps.insert.title') }}
</li>
<li class="action" @click="createElement('table')">
<i class="sn-icon sn-icon-tables"></i>
{{ i18n.t('protocols.steps.insert.table') }}
</li>
<li class="action dropdown-submenu-item">
<i class="sn-icon sn-icon-tables"></i>
{{ i18n.t('protocols.steps.insert.well_plate') }}
<span class="caret"></span>
<ul class="dropdown-submenu">
<li v-for="option in wellPlateOptions" :key="option.dimensions.toString()" class="action" @click="createElement('table', option.dimensions, true)">
{{ i18n.t(option.label) }}
</li>
</ul>
</li>
<li class="action" @click="createElement('checklist')">
<i class="sn-icon sn-icon-activities"></i>
{{ i18n.t('protocols.steps.insert.checklist') }}
</li>
<li class="action" @click="createElement('text')">
<i class="sn-icon sn-icon-result-text"></i>
{{ i18n.t('protocols.steps.insert.text') }}
</li>
<li class="action dropdown-submenu-item">
<i class="sn-icon sn-icon-files"></i>
{{ i18n.t('protocols.steps.insert.attachment') }}
<span class="caret"></span>
<ul class="dropdown-submenu">
<li class="action" @click="openLoadFromComputer">
{{ i18n.t('protocols.steps.insert.add_file') }}
</li>
<li class="action" v-if="step.attributes.wopi_enabled" @click="openWopiFileModal">
{{ i18n.t('assets.create_wopi_file.button_text') }}
</li>
<li class="action" v-if="step.attributes.marvinjs_enabled" @click="openMarvinJsModal($refs.marvinJsButton)">
<span
class="new-marvinjs-upload-button text-sn-black text-decoration-none"
:data-object-id="step.id"
ref="marvinJsButton"
:data-marvin-url="step.attributes.marvinjs_context.marvin_js_asset_url"
:data-object-type="step.attributes.type"
tabindex="0"
>
{{ i18n.t('marvinjs.new_button') }}
</span>
</li>
</ul>
</li>
</ul>
</div>
</div>
<hr>
<ReorderableItemsModal v-if="reordering"
title="Placeholder title for this modal"
@ -12,12 +74,46 @@
@reorder="updateElementOrder"
@close="closeReorderModal"
/>
<div>
<template v-for="(element, index) in orderedElements">
<component
:is="elements[index].attributes.orderable_type"
:key="index"
:element.sync="elements[index]"
:inRepository="false"
:reorderElementUrl="elements.length > 1 ? urls.reorder_elements_url : ''"
:assignableMyModuleId="result.attributes.my_module_id"
:isNew="element.isNew"
@component:delete="deleteElement"
@update="updateElement"
@reorder="openReorderModal"
@component:insert="insertElement"
/>
</template>
<Attachments v-if="attachments.length"
:parent="result"
:attachments="attachments"
:attachmentsReady="attachmentsReady"
@attachments:openFileModal="showFileModal = true"
@attachment:deleted="attachmentDeleted"
@attachment:uploaded="loadAttachments"
@attachments:order="() => {}"
@attachments:viewMode="() => {}"
@attachment:viewMode="() => {}"/>
</div>
</div>
</template>
<script>
import axios from 'axios';
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue';
import ResultTable from '../shared/content/table.vue';
import ResultText from '../shared/content/text.vue';
import Attachments from '../shared/content/attachments.vue';
import AttachmentsMixin from '../shared/content/mixins/attachments.js'
import WopiFileModal from '../shared/content/attachments/mixins/wopi_file_modal.js'
import UtilsMixin from '../mixins/utils.js'
export default {
name: 'Results',
@ -27,11 +123,18 @@
data() {
return {
reordering: false,
elements: []
elements: [],
attachments: [],
attachmentsReady: false,
showFileModal: false
}
},
mixins: [UtilsMixin, AttachmentsMixin, WopiFileModal],
components: {
ReorderableItemsModal
ReorderableItemsModal,
ResultTable,
ResultText,
Attachments
},
computed: {
reorderableElements() {
@ -44,6 +147,10 @@
return this.result.attributes.urls || {}
}
},
created() {
this.loadAttachments();
this.loadElements();
},
methods: {
openReorderModal() {
this.reordering = true;
@ -76,6 +183,37 @@
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
});
},
deleteElement(element) {
},
updateElement(element) {
},
insertElement(element) {
},
loadElements() {
$.get(this.urls.elements_url, (result) => {
this.elements = result.data
});
},
loadAttachments() {
this.attachmentsReady = false
$.get(this.urls.attachments_url, (result) => {
this.attachments = result.data
if (this.attachments.findIndex((e) => e.attributes.attached === false) >= 0) {
setTimeout(() => {
this.loadAttachments()
}, 10000)
} else {
this.attachmentsReady = true
}
});
this.showFileModal = false;
},
attachmentDeleted(id) {
this.attachments = this.attachments.filter((a) => a.id !== id );
this.$emit('resultUpdated');
},
}
}
</script>

View file

@ -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
});
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,4 +18,8 @@
<results url="<%= my_module_results_url(@my_module) %>">
</div>
<%= javascript_include_tag "handsontable.full" %>
<%= render partial: "shared/formulas_libraries" %>
<%= render 'shared/tiny_mce_packs' %>
<%= javascript_include_tag 'vue_results' %>

View file

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