diff --git a/app/assets/images/icon_small/bio_eddie.png b/app/assets/images/icon_small/bio_eddie.png new file mode 100644 index 000000000..6e418c234 Binary files /dev/null and b/app/assets/images/icon_small/bio_eddie.png differ diff --git a/app/assets/images/icon_small/bio_eddie_white.png b/app/assets/images/icon_small/bio_eddie_white.png new file mode 100644 index 000000000..4ca4e65d8 Binary files /dev/null and b/app/assets/images/icon_small/bio_eddie_white.png differ diff --git a/app/assets/javascripts/sitewide/bio_eddie.js b/app/assets/javascripts/sitewide/bio_eddie.js new file mode 100644 index 000000000..0f4106f77 --- /dev/null +++ b/app/assets/javascripts/sitewide/bio_eddie.js @@ -0,0 +1,152 @@ +/* global HelperModule I18n */ +var bioEddieEditor = (function() { + var BIO_EDDIE; + var CHEMAXON; + var bioEddieIframe; + var bioEddieModal; + + function importMolecule() { + var monomerModel = BIO_EDDIE.getMonomerModel(); + var monomerImporter = new CHEMAXON.HelmImportModule(); + var molecule = bioEddieModal.data('molecule') || ''; + monomerImporter.import(molecule, monomerModel) + .then(builder => BIO_EDDIE.setModel(builder.graphStoreData)); + } + + function loadBioEddie() { + BIO_EDDIE = bioEddieIframe.contentWindow.bioEddieEditor; + CHEMAXON = bioEddieIframe.contentWindow.chemaxon; + + if (typeof BIO_EDDIE === 'undefined' || typeof CHEMAXON === 'undefined') { + setTimeout(function() { + loadBioEddie(); + }, 2000); + } else { + importMolecule(); + } + } + + function initIframe() { + if (typeof BIO_EDDIE === 'undefined' || typeof CHEMAXON === 'undefined') { + bioEddieIframe.src = bioEddieIframe.dataset.src; + loadBioEddie(); + } else { + importMolecule(); + } + } + + function saveMolecule(svg, structure) { + var moleculeName = bioEddieModal.find('.file-name input').val(); + $.post(bioEddieModal.data('create-url'), { + description: structure, + object_id: bioEddieModal.data('object_id'), + object_type: bioEddieModal.data('object_type'), + name: moleculeName, + image: svg + }, function(result) { + var newAsset = $(result.html); + if (bioEddieModal.data('object_type') === 'Step') { + newAsset.find('.file-preview-link').css('top', '-300px'); + newAsset.addClass('new').prependTo($(bioEddieModal.data('assets_container'))); + setTimeout(function() { + newAsset.find('.file-preview-link').css('top', '0px'); + }, 200); + bioEddieModal.modal('hide'); + } else if (bioEddieModal.data('object_type') === 'Result') { + window.location.reload(); + } + }); + } + + function updateMolecule(svg, structure) { + var moleculeName = bioEddieModal.find('.file-name input').val(); + $.ajax({ + url: bioEddieModal.data('update-url'), + data: { + description: structure, + name: moleculeName, + image: svg + }, + dataType: 'json', + type: 'PUT', + success: function(json) { + $('#modal_link' + json.id + ' img').attr('src', json.url); + $('#modal_link' + json.id + ' .attachment-label').html(json.file_name); + bioEddieModal.modal('hide'); + }, + error: function(response) { + if (response.status === 403) { + HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger'); + } + } + }); + } + + function generateImage(structure) { + var imageGenerator = new CHEMAXON.ImageGenerator(); + var emptySVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + imageGenerator.generateSVGFromHelm(emptySVG, structure) + .then(svg => { + if (bioEddieModal.data('edit-mode')) { + updateMolecule(svg, structure); + } else { + saveMolecule(svg, structure); + } + }); + } + + $(document).on('turbolinks:load', function() { + bioEddieIframe = document.getElementById('bioEddieIframe'); + bioEddieModal = $('#bioEddieModal'); + + bioEddieModal.on('shown.bs.modal', function() { + initIframe(); + }); + + bioEddieModal.on('click', '.file-save-link', function() { + var model = BIO_EDDIE.getModel(); + var monomerModel = BIO_EDDIE.getMonomerModel(); + var monomerExporter = new CHEMAXON.Helm2ExportModule(); + + monomerExporter.export(model, monomerModel) + .then(structure => generateImage(structure)); + }); + }); + + return { + open_new: (objectId, objectType, container) => { + bioEddieModal.data('object_id', objectId); + bioEddieModal.data('object_type', objectType); + bioEddieModal.data('assets_container', container); + bioEddieModal.find('.file-name input').val(''); + bioEddieModal.modal('show'); + }, + + open_edit: (name, molecule, updateUrl) => { + bioEddieModal.data('edit-mode', true); + bioEddieModal.data('molecule', molecule); + bioEddieModal.data('update-url', updateUrl); + bioEddieModal.find('.file-name input').val(name); + bioEddieModal.modal('show'); + } + }; +}()); + +(function() { + $(document).on('click', '.new-bio-eddie-upload-button', function() { + bioEddieEditor.open_new( + this.dataset.objectId, + this.dataset.objectType, + this.dataset.assetsContainer + ); + }); + + $(document).on('click', '.bio-eddie-edit-button', function() { + $('#filePreviewModal').modal('hide'); + bioEddieEditor.open_edit( + this.dataset.moleculeName, + this.dataset.moleculeDescription, + this.dataset.updateUrl + ); + }); +}()); diff --git a/app/assets/stylesheets/bio_eddie.scss b/app/assets/stylesheets/bio_eddie.scss new file mode 100644 index 000000000..5d25131a6 --- /dev/null +++ b/app/assets/stylesheets/bio_eddie.scss @@ -0,0 +1,75 @@ +// scss-lint:disable SelectorDepth +// scss-lint:disable NestingDepth +// scss-lint:disable SelectorFormat +// scss-lint:disable ImportantRule +// scss-lint:disable IdSelector + +@import "constants"; +@import "mixins"; + +// MarvinJs modal +.modal-bio-eddie { + background: transparent; + font-size: $font-size-base; + padding: 0 !important; + + .modal-dialog { + height: 100%; + margin: 0; + padding: 0; + width: auto; + } + + .modal-content { + background: transparent; + border: 0; + box-shadow: none; + height: 100%; + width: auto; + } + + .modal-header { + background: $color-white; + display: flex; + height: 60px; + line-height: 40px; + padding: 10px 15px; + text-align: center; + + .file-save-link { + flex-shrink: 0; + margin: 0 20px 0 0; + } + + .file-name { + align-items: center; + display: flex; + flex-shrink: 0; + float: left; + margin-right: auto; + + input { + border-radius: 5px; + box-shadow: none; + color: $color-black; + height: 40px; + margin-left: 5px; + outline: 0; + padding: 5px 10px; + position: relative; + width: 350px; + } + } + } + + .modal-body { + height: calc(100% - 60px); + padding: 0; + + iframe { + height: 100%; + position: relative; + width: 100%; + } + } +} diff --git a/app/assets/stylesheets/shared/assets.scss b/app/assets/stylesheets/shared/assets.scss index bb9c955c8..2979e3f86 100644 --- a/app/assets/stylesheets/shared/assets.scss +++ b/app/assets/stylesheets/shared/assets.scss @@ -61,6 +61,34 @@ } } + &.bio_eddie { + + &::before, + &::after { + border-radius: 1em 0 0 1em; + bottom: 1em; + content: ""; + display: block; + height: 2em; + line-height: 2em; + position: absolute; + right: -1em; + width: 2.25em; + } + + &::before { + background: $bio-eddie-color; + } + + &::after { + background-image: url("/images/icon_small/bio_eddie_white.png"); + background-repeat: no-repeat; + height: 1.85em; + right: -1.15em; + width: 2em; + } + } + .fas { font-size: 100px; line-height: 160px; diff --git a/app/assets/stylesheets/shared_styles/constants/colors.scss b/app/assets/stylesheets/shared_styles/constants/colors.scss index 3a4012f98..49aebe0ac 100644 --- a/app/assets/stylesheets/shared_styles/constants/colors.scss +++ b/app/assets/stylesheets/shared_styles/constants/colors.scss @@ -41,6 +41,9 @@ $office-ms-powerpoint: #d24726; // MarvinJS color: $marvinjs-color: #29999c; +// BioEddie color: +$bio-eddie-color: #ffa000; + $pdf-color: #f40f02; // Don't use them diff --git a/app/controllers/bio_eddie_assets_controller.rb b/app/controllers/bio_eddie_assets_controller.rb new file mode 100644 index 000000000..d282031c0 --- /dev/null +++ b/app/controllers/bio_eddie_assets_controller.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +class BioEddieAssetsController < ApplicationController + include ActiveStorage::SetCurrent + + before_action :load_vars, except: :create + before_action :load_create_vars, only: :create + + before_action :check_read_permission + before_action :check_edit_permission, only: %i(update create start_editing) + + def create + asset = BioEddieService.create_molecule(bio_eddie_params, current_user, current_team) + + if asset && bio_eddie_params[:object_type] == 'Step' + render json: { + html: render_to_string(partial: 'assets/asset.html.erb', locals: { + asset: asset, + gallery_view_id: bio_eddie_params[:object_id] + }) + } + elsif asset && bio_eddie_params[:object_type] == 'Result' + render json: { status: 'created' }, status: :ok + else + render json: asset.errors, status: :unprocessable_entity + end + end + + def update + asset = BioEddieService.update_molecule(bio_eddie_params, current_user, current_team) + + if asset + render json: { url: rails_representation_url(asset.medium_preview), + id: asset.id, + file_name: asset.blob.metadata['name'] } + else + render json: { error: t('bio_eddie.no_molecules_found') }, status: :unprocessable_entity + end + end + + def start_editing + # Activity here + end + + private + + def load_vars + @asset = current_team.assets.find_by(id: params[:id]) + return render_404 unless @asset + + @assoc = @asset.step || @asset.result + + case @assoc + when Step + @protocol = @assoc.protocol + when Result + @my_module = @assoc.my_module + end + end + + def load_create_vars + case bio_eddie_params[:object_type] + when 'Step' + @assoc = Step.find_by(id: bio_eddie_params[:object_id]) + @protocol = @assoc.protocol + when 'Result' + @assoc = MyModule.find_by(id: bio_eddie_params[:object_id]) + @my_module = @assoc + end + end + + def check_read_permission + case @assoc + when Step + return render_403 unless can_read_protocol_in_module?(@protocol) || + can_read_protocol_in_repository?(@protocol) + when Result, MyModule + return render_403 unless can_read_experiment?(@my_module.experiment) + else + render_403 + end + end + + def check_edit_permission + case @assoc + when Step + return render_403 unless can_manage_protocol_in_module?(@protocol) || + can_manage_protocol_in_repository?(@protocol) + when Result, MyModule + return render_403 unless can_manage_module?(@my_module) + else + render_403 + end + end + + def bio_eddie_params + params.permit(:id, :description, :object_id, :object_type, :name, :image) + end +end diff --git a/app/services/bio_eddie_service.rb b/app/services/bio_eddie_service.rb new file mode 100644 index 000000000..5f1de71de --- /dev/null +++ b/app/services/bio_eddie_service.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +class BioEddieService + class << self + def url + ApplicationSettings.instance.values['bio_eddie_url'] + end + + def enabled? + url.present? + end + + def create_molecule(params, current_user, current_team) + file = image_io(params) + + asset = Asset.new(created_by: current_user, + last_modified_by: current_user, + team_id: current_team.id) + attach_file(asset.file, file, params) + asset.save! + asset.post_process_file(current_team) + connect_asset(asset, params, current_user) + end + + def update_molecule(params, _current_user, current_team) + asset = current_team.assets.find(params[:id]) + attachment = asset&.file + + return unless attachment + + file = image_io(params) + attach_file(attachment, file, params) + asset + end + + private + + def connect_asset(asset, params, current_user) + case params[:object_type] + when 'Step' + object = params[:object_type].constantize.find(params[:object_id]) + asset.update!(view_mode: object.assets_view_mode) + object.assets << asset + when 'Result' + my_module = MyModule.find_by(id: params[:object_id]) + return unless my_module + + Result.create(user: current_user, + my_module: my_module, + name: prepare_name(params[:name]), + asset: asset, + last_modified_by: current_user) + end + asset + end + + def image_io(params) + StringIO.new(params[:image]) + end + + def attach_file(attachment, file, params) + attachment.attach( + io: file, + filename: "#{prepare_name(params[:name])}.svg", + content_type: 'image/svg+xml', + metadata: { + name: prepare_name(params[:name]), + description: params[:description], + asset_type: 'bio_eddie' + } + ) + end + + def prepare_name(sketch_name) + sketch_name.presence || I18n.t('bio_eddie.new_molecule') + end + end +end diff --git a/app/views/assets/bio_eddie/_create_bio_eddie_li.html.erb b/app/views/assets/bio_eddie/_create_bio_eddie_li.html.erb new file mode 100644 index 000000000..9cf91bd64 --- /dev/null +++ b/app/views/assets/bio_eddie/_create_bio_eddie_li.html.erb @@ -0,0 +1,11 @@ +
  • + + <%= image_tag 'icon_small/bio_eddie.png' %> + <%= t('bio_eddie.new_button') %> + +
  • diff --git a/app/views/my_modules/results.html.erb b/app/views/my_modules/results.html.erb index 6475dc4cf..6abb42b9c 100644 --- a/app/views/my_modules/results.html.erb +++ b/app/views/my_modules/results.html.erb @@ -29,6 +29,8 @@ <%= 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/bio_eddie/create_bio_eddie_li.html.erb', + locals: { element_id: @my_module.id, element_type: 'Result', assets_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' } %> @@ -69,6 +71,9 @@ +<%= render partial: "shared/bio_eddie_modal.html.erb" %> + + <%= javascript_include_tag "handsontable.full" %> diff --git a/app/views/protocols/_steps.html.erb b/app/views/protocols/_steps.html.erb index d954e6827..84ba148c7 100644 --- a/app/views/protocols/_steps.html.erb +++ b/app/views/protocols/_steps.html.erb @@ -34,3 +34,5 @@ <%= javascript_include_tag "assets/wopi/create_wopi_file" %> <%= javascript_include_tag "protocols/steps" %> + +<%= render partial: "shared/bio_eddie_modal.html.erb" %> diff --git a/app/views/shared/_bio_eddie_modal.html.erb b/app/views/shared/_bio_eddie_modal.html.erb new file mode 100644 index 000000000..4ab9d12e6 --- /dev/null +++ b/app/views/shared/_bio_eddie_modal.html.erb @@ -0,0 +1,33 @@ +<% if BioEddieService.enabled? %> + +<% end %> + diff --git a/app/views/shared/file_preview/_content.html.erb b/app/views/shared/file_preview/_content.html.erb index 933e448ae..dfd3acb5f 100644 --- a/app/views/shared/file_preview/_content.html.erb +++ b/app/views/shared/file_preview/_content.html.erb @@ -21,6 +21,16 @@ <%= t('assets.file_preview.edit_in_marvinjs') %> + <% elsif asset.file.metadata[:asset_type] == 'bio_eddie' %> + <% elsif asset.editable_image? %>