diff --git a/.gitignore b/.gitignore index c0f9f219b..f50dc2373 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,8 @@ spec/addons # RVM/rbenv ruby version for local development .ruby-version + +#ignore marvinJs + +public/marvinjs +public/marvin4js-license.cxl diff --git a/Gemfile b/Gemfile index b523846fa..0acba81b4 100644 --- a/Gemfile +++ b/Gemfile @@ -4,9 +4,7 @@ source 'http://rubygems.org' ruby '2.6.3' - gem 'bootsnap', require: false -gem 'webpacker', '~> 3.5' gem 'bootstrap-sass', '~> 3.3.7' gem 'bootstrap_form', '~> 2.7.0' gem 'devise', '~> 4.6.2' @@ -19,6 +17,7 @@ gem 'recaptcha', require: 'recaptcha/rails' gem 'sanitize', '~> 4.4' gem 'sassc-rails' gem 'simple_token_authentication', '~> 1.15.1' # Token authentication for Devise +gem 'webpacker', '~> 3.5' gem 'yomu' # Gems for OAuth2 subsystem @@ -29,6 +28,7 @@ gem 'omniauth-linkedin-oauth2' # Gems for API implementation gem 'active_model_serializers', '~> 0.10.7' gem 'json-jwt' +gem 'jsonapi-renderer', '= 0.2.0' gem 'jwt', '~> 1.5' gem 'kaminari' gem 'rack-attack' @@ -78,16 +78,16 @@ gem 'sneaky-save', git: 'https://github.com/einzige/sneaky-save' gem 'turbolinks', '~> 5.1.1' gem 'underscore-rails' gem 'wicked_pdf', '~> 1.1.0' -gem 'wkhtmltopdf-heroku' +gem 'wkhtmltopdf-heroku', '2.12.4' gem 'aws-sdk-rails' gem 'aws-sdk-s3' -gem 'mini_magick' -gem 'paperclip', '~> 6.1' # File attachment, image attachment library gem 'delayed_job_active_record' gem 'devise-async', git: 'https://github.com/mhfs/devise-async.git', branch: 'devise-4.x' +gem 'mini_magick' +gem 'paperclip', '~> 6.1' # File attachment, image attachment library gem 'rufus-scheduler', '~> 3.5' gem 'discard', '~> 1.0' diff --git a/Gemfile.lock b/Gemfile.lock index c2297e6d7..a006d3b98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM json_matchers (0.11.0) json_schema json_schema (0.20.6) - jsonapi-renderer (0.2.1) + jsonapi-renderer (0.2.0) jwt (1.5.6) kaminari (1.1.1) activesupport (>= 4.1.0) @@ -558,7 +558,7 @@ GEM websocket-extensions (0.1.4) whacamole (1.2.0) wicked_pdf (1.1.0) - wkhtmltopdf-heroku (2.12.5.0) + wkhtmltopdf-heroku (2.12.4.0) xpath (3.2.0) nokogiri (~> 1.8) yomu (0.2.4) @@ -616,6 +616,7 @@ DEPENDENCIES js_cookie_rails json-jwt json_matchers + jsonapi-renderer (= 0.2.0) jwt (~> 1.5) kaminari listen (~> 3.0) @@ -671,7 +672,7 @@ DEPENDENCIES webpacker (~> 3.5) whacamole wicked_pdf (~> 1.1.0) - wkhtmltopdf-heroku + wkhtmltopdf-heroku (= 2.12.4) yomu RUBY VERSION diff --git a/app/assets/images/icon_small/marvinjs.svg b/app/assets/images/icon_small/marvinjs.svg new file mode 100644 index 000000000..bec9ab199 --- /dev/null +++ b/app/assets/images/icon_small/marvinjs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 324a2408c..ee00589a1 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -42,6 +42,7 @@ //= require shared/inline_editing //= require activestorage //= require turbolinks +//= require marvinjslauncher // Initialize links for submitting forms. This is useful for submitting diff --git a/app/assets/javascripts/my_modules/protocols.js b/app/assets/javascripts/my_modules/protocols.js new file mode 100644 index 000000000..fbaa091dc --- /dev/null +++ b/app/assets/javascripts/my_modules/protocols.js @@ -0,0 +1,485 @@ +/* global TinyMCE I18n animateSpinner importProtocolFromFile truncateLongString globalConstants */ +/* global HelperModule */ +/* eslint-disable no-use-before-define, no-alert, no-restricted-globals, no-underscore-dangle */ + +//= require my_modules +//= require protocols/import_export/import + +// Currently selected row in "load from protocol" modal +var selectedRow = null; + + +function initEditMyModuleDescription() { + $('#my_module_description_view').on('click', function() { + TinyMCE.init('#my_module_description_textarea'); + }); +} + +function initEditProtocolDescription() { + $('#protocol_description_view').on('click', function() { + TinyMCE.init('#protocol_description_textarea', refreshProtocolStatusBar); + }); +} + +// Initialize edit description modal window +function initEditDescription() { + var editDescriptionModal = $('#manage-module-description-modal'); + var editDescriptionModalBody = editDescriptionModal.find('.modal-body'); + $('.description-link') + .on('ajax:success', function(ev, data) { + var descriptionLink = $('.description-refresh'); + + // Set modal body & title + editDescriptionModalBody.html(data.html); + editDescriptionModal + .find('#manage-module-description-modal-label') + .text(data.title); + + editDescriptionModalBody.find('form') + .on('ajax:success', function(ev2, data2) { + // Update module's description in the tab + descriptionLink.html(data2.description_label); + + // Close modal + editDescriptionModal.modal('hide'); + }) + .on('ajax:error', function(ev2, data2) { + // Display errors if needed + $(this).renderFormErrors('my_module', data2.responseJSON); + }); + + // Show modal + editDescriptionModal.modal('show'); + }); + + + editDescriptionModal.on('hidden.bs.modal', function() { + editDescriptionModalBody.find('form').off('ajax:success ajax:error'); + editDescriptionModalBody.html(''); + }); +} + +function initCopyToRepository() { + var link = $("[data-action='copy-to-repository']"); + var modal = $('#copy-to-repository-modal'); + var modalBody = modal.find('.modal-body'); + var submitBtn = modal.find(".modal-footer [data-action='submit']"); + + link + .on('ajax:success', function(e, data) { + modalBody.html(data.html); + + modalBody.find("[data-role='copy-to-repository']") + .on('ajax:success', function(e2, data2) { + if (data2.refresh !== null) { + // Reload page + location.reload(); + } else { + // Simply hide the modal + modal.modal('hide'); + } + }) + .on('ajax:error', function(e2, data2) { + // Display errors in form + submitBtn[0].disabled = false; + if (data2.status === 422) { + $(this).renderFormErrors('protocol', data2.responseJSON); + } else { + // Simply display global error + alert(data2.responseJSON.message); + } + }); + + modal.modal('show'); + submitBtn[0].disabled = false; + }) + .on('ajax:error', function() {}); + + submitBtn.on('click', function() { + // Submit the embedded form + submitBtn[0].disabled = true; + modalBody.find('form').submit(); + }); + + modalBody.on('click', "[data-role='link-check']", function() { + var text = $(this).closest('.modal-body').find("[data-role='link-text']"); + if ($(this).prop('checked')) { + text.show(); + } else { + text.hide(); + } + }); + + modal.on('hidden.bs.modal', function() { + modalBody.find("[data-role='copy-to-repository']") + .off('ajax:success ajax:error'); + + modalBody.html(''); + }); +} + +function initLinkUpdate() { + var modal = $('#confirm-link-update-modal'); + var modalTitle = modal.find('.modal-title'); + var modalBody = modal.find('.modal-body'); + var updateBtn = modal.find(".modal-footer [data-action='submit']"); + $("[data-action='unlink'], [data-action='revert'], [data-action='update-parent'], [data-action='update-self']") + .on('ajax:success', function(e, data) { + modalTitle.html(data.title); + modalBody.html(data.message); + updateBtn.text(data.btn_text); + modal.attr('data-url', data.url); + modal.modal('show'); + }); + modal.on('hidden.bs.modal', function() { + modalBody.html(''); + }); + + if (!$._data(updateBtn[0], 'events')) { + updateBtn.on('click', function() { + // POST via ajax + $.ajax({ + url: modal.attr('data-url'), + type: 'POST', + dataType: 'json', + success: function() { + // Simply reload page + location.reload(); + }, + error: function(ev) { + // Display error message in alert() + alert(ev.responseJSON.message); + + // Hide modal + modal.modal('hide'); + } + }); + }); + } + + $('[data-role="protocol-status-bar"] .preview-protocol').click(function(e) { + e.preventDefault(); + }); +} + +function initLoadFromRepository() { + var modal = $('#load-from-repository-modal'); + var modalBody = modal.find('.modal-body'); + var loadBtn = modal.find(".modal-footer [data-action='submit']"); + + $("[data-action='load-from-repository']") + .on('ajax:success', function(e, data) { + modalBody.html(data.html); + + // Disable load btn + loadBtn.attr('disabled', 'disabled'); + + modal.modal('show'); + + // Init Datatable on public tab + initLoadFromRepositoryTable(modalBody.find('#public-tab')); + + modalBody.find("a[data-toggle='tab']") + .on('hide.bs.tab', function(el) { + // Destroy Handsontable in to-be-hidden tab + var content = $($(el.target).attr('href')); + destroyLoadFromRepositoryTable(content); + }) + .on('shown.bs.tab', function(el) { + // Initialize Handsontable in to-be-shown tab + var content = $($(el.target).attr('href')); + initLoadFromRepositoryTable(content); + }); + + loadBtn.on('click', function() { + loadFromRepository(); + }); + }); + modal.on('hidden.bs.modal', function() { + // Destroy the current Datatable + destroyLoadFromRepositoryTable(modalBody.find('.tab-pane.active')); + + modalBody.find("a[data-toggle='tab']") + .off('hide.bs.tab shown.bs.tab'); + + loadBtn.off('click'); + + modalBody.html(''); + }); +} + +function initLoadFromRepositoryTable(content) { + var tableEl = content.find("[data-role='datatable']"); + + var datatable = tableEl.DataTable({ + order: [[1, 'asc']], + dom: 'RBfltpi', + buttons: [], + processing: true, + serverSide: true, + responsive: true, + ajax: { + url: tableEl.data('source'), + type: 'POST' + }, + colReorder: { + fixedColumnsLeft: 1000000 // Disable reordering + }, + columnDefs: [{ + targets: 0, + searchable: false, + orderable: false, + sWidth: '1%', + render: function() { + return ""; + } + }, { + targets: [1, 2, 3, 4, 5, 6], + searchable: true, + orderable: true + }], + columns: [ + { data: '0' }, + { data: '1' }, + { data: '2' }, + { data: '3' }, + { data: '4' }, + { data: '5' }, + { data: '6' } + ], + oLanguage: { + sSearch: I18n.t('general.filter') + }, + rowCallback: function(row, data) { + // Get row ID + var rowId = data.DT_RowId; + + $(row).attr('data-row-id', rowId); + + // If row ID is in the list of selected row IDs + if (rowId === selectedRow) { + $(row).find("input[type='radio']").prop('checked', true); + $(row).addClass('selected'); + } + }, + fnDrawCallback: function() { + animateSpinner(this, false); + }, + preDrawCallback: function() { + animateSpinner(this); + } + }); + + // Handle click on table cells with radio buttons + tableEl.find('tbody').on('click', 'td', function() { + $(this).parent().find("input[type='radio']").trigger('click'); + }); + + // Handle clicks on radio buttons + tableEl.find('tbody').on('click', "input[type='radio']", function(e) { + // Get row ID + var row = $(this).closest('tr'); + var data = datatable.row(row).data(); + var rowId = data.DT_RowId; + + // Uncheck all radio buttons + tableEl.find("tbody input[type='radio']") + .prop('checked', false) + .closest('tr') + .removeClass('selected'); + + // Select the current row + row.find("input[type='radio']").prop('checked', true); + selectedRow = rowId; + row.addClass('selected'); + + // Enable load btn + content.closest('.modal') + .find(".modal-footer [data-action='submit']") + .removeAttr('disabled'); + + e.stopPropagation(); + }); + + tableEl.find('tbody').on('click', "a[data-action='filter']", function(e) { + var link = $(this); + var query = link.attr('data-param'); + + // Re-search data + datatable.search(query).draw(); + + // Don't propagate this further up + e.stopPropagation(); + return false; + }); +} + +function destroyLoadFromRepositoryTable(content) { + var tableEl = content.find("[data-role='datatable']"); + + // Unbind event listeners + tableEl.find('tbody').off('click', "a[data-action='filter']"); + tableEl.find('tbody').off('click', "input[type='radio']"); + tableEl.find('tbody').off('click', 'td'); + + // Destroy datatable + tableEl.DataTable().destroy(); + tableEl.find('tbody').html(''); + + selectedRow = null; + + // Disable load btn + content.closest('.modal') + .find(".modal-footer [data-action='submit']") + .attr('disabled', 'disabled'); +} + +function loadFromRepository() { + var modal = $('#load-from-repository-modal'); + + var checkLinked = $("[data-role='protocol-status-bar']") + .text(); + + var confirmMessage = ''; + if (checkLinked.trim() !== '(unlinked)') { + confirmMessage = I18n.t('my_modules.protocols.load_from_repository_modal.import_to_linked_task_rep'); + } else { + confirmMessage = I18n.t('my_modules.protocols.load_from_repository_modal.confirm_message'); + } + + if (selectedRow !== null && confirm(confirmMessage)) { + // POST via ajax + $.ajax({ + url: modal.attr('data-url'), + type: 'POST', + dataType: 'json', + data: { source_id: selectedRow }, + success: function() { + // Simply reload page + location.reload(); + }, + error: function(ev) { + // Display error message in alert() + alert(ev.responseJSON.message); + + // Hide modal + modal.modal('hide'); + } + }); + } +} + +function refreshProtocolStatusBar() { + // Get the status bar URL + var url = $("[data-role='protocol-status-bar-url']").attr('data-url'); + + // Fetch new updated at label + $.ajax({ + url: url, + type: 'GET', + dataType: 'json', + success: function(data) { + $("[data-role='protocol-status-bar']").html(data.html); + initLinkUpdate(); + } + }); +} + +function initImport() { + var fileInput = $("[data-action='load-from-file']"); + + // Make sure multiple selections of same file + // always prompt new modal + fileInput.find("input[type='file']").on('click', function() { + this.value = null; + }); + + // Hack to hide "No file chosen" tooltip + fileInput.attr('title', window.URL ? ' ' : ''); + + fileInput.on('change', function(ev) { + var importUrl = fileInput.attr('data-import-url'); + importProtocolFromFile( + ev.target.files[0], + importUrl, + null, + true, + function(datas) { + var data = datas[0]; + if (data.status === 'ok') { + // Simply reload page + location.reload(); + } else if (data.status === 'locked') { + alert(I18n.t('my_modules.protocols.load_from_file_error_locked')); + } else { + if (data.status === 'size_too_large') { + alert(I18n.t('my_modules.protocols.load_from_file_size_error', + { size: $(document.body).data('file-max-size-mb') })); + } else { + alert(I18n.t('my_modules.protocols.load_from_file_error')); + } + animateSpinner(null, false); + } + } + ); + // Clear input on self + $(this).val(''); + }); +} + +function initRecentProtocols() { + var recentProtocolContainer = $('.my-module-recent-protocols'); + var dropDownList = recentProtocolContainer.find('.dropdown-menu'); + recentProtocolContainer.find('.dropdown-button').click(function() { + dropDownList.find('.protocol').remove(); + $.get('/protocols/recent_protocols', result => { + $.each(result, (i, protocol) => { + $('
' + + truncateLongString(protocol.name, globalConstants.name_truncation_length) + + '
').appendTo(dropDownList) + .click(() => { + $.post(recentProtocolContainer.data('updateUrl'), { source_id: protocol.id }) + .success(() => { + location.reload(); + }) + .error(ev => { + HelperModule.flashAlertMsg(ev.responseJSON.message, 'warning'); + }); + }); + }); + }); + }); + + // We use here ajax:success, because we want to check any change on this page + $(document).on('ajax:success', () => { + updateRecentProtocolsStatus(); + }); +} + +function updateRecentProtocolsStatus() { + var recentProtocolContainer = $('.my-module-recent-protocols'); + var steps = $('.step'); + var protocolDescription = $('#protocol_description_view').html(); + if (steps.length === 0 && protocolDescription.length === 0) { + recentProtocolContainer.css('display', ''); + } else { + recentProtocolContainer.css('display', 'none'); + } +} + +/** + * Initializes page + */ +function init() { + initEditMyModuleDescription(); + initEditProtocolDescription(); + initEditDescription(); + initCopyToRepository(); + initLinkUpdate(); + initLoadFromRepository(); + refreshProtocolStatusBar(); + initImport(); + initRecentProtocols(); +} + +init(); diff --git a/app/assets/javascripts/protocols/import_export/import.js b/app/assets/javascripts/protocols/import_export/import.js index 7091baa75..218976d1c 100644 --- a/app/assets/javascripts/protocols/import_export/import.js +++ b/app/assets/javascripts/protocols/import_export/import.js @@ -500,7 +500,14 @@ function importProtocolFromFile( var tinyMceAsset = {}; var fileRef = $(this).attr('fileRef'); tinyMceAsset.tokenId = $(this).attr('tokenId'); + tinyMceAsset.fileName = $(this).children('fileName').text(); tinyMceAsset.fileType = $(this).children('fileType').text(); + if ($(this).children('fileMetadata').html() !== undefined) { + tinyMceAsset.fileMetadata = $(this).children('fileMetadata').html() + .replace('', '') + .replace(']]>', ''); + } tinyMceAsset.bytes = getAssetBytes( protocolFolders[index], stepGuid, @@ -579,6 +586,12 @@ function importProtocolFromFile( stepAssetJson.id = assetId; stepAssetJson.fileName = fileName; stepAssetJson.fileType = $(this).children('fileType').text(); + if ($(this).children('fileMetadata').html() !== undefined) { + stepAssetJson.fileMetadata = $(this).children('fileMetadata').html() + .replace('', '') + .replace(']]>', ''); + } stepAssetJson.bytes = getAssetBytes( protocolFolders[index], stepGuid, diff --git a/app/assets/javascripts/protocols/steps.js.erb b/app/assets/javascripts/protocols/steps.js.erb index 7f798f779..93cf48e16 100644 --- a/app/assets/javascripts/protocols/steps.js.erb +++ b/app/assets/javascripts/protocols/steps.js.erb @@ -122,6 +122,7 @@ SmartAnnotation.preventPropagation('.atwho-user-popover'); TinyMCE.destroyAll(); DragNDropSteps.clearFiles(); + MarvinJsEditor.initNewButton('.new-marvinjs-upload-button'); }, 1000); }) @@ -374,7 +375,7 @@ } function initCallBacks() { - applyCreateWopiFileCallback(); + applyCreateWopiFileCallback() applyCheckboxCallBack(); applyStepCompletedCallBack(); applyEditCallBack(); @@ -502,10 +503,9 @@ $('#new-step-main-tab a') .on('shown.bs.tab', function() { $('#step_name').focus(); - TinyMCE.init('#step_description_textarea'); - }).on('hidden.bs.tab', function() { tinyMCE.editors.step_description_textarea.remove(); - }); + TinyMCE.init('#step_description_textarea'); + }) TinyMCE.init('#step_description_textarea'); }); @@ -536,7 +536,7 @@ var nameValid = textValidator(ev, $nameInput, 1, <%= Constants::NAME_MAX_LENGTH %>); var $descrTextarea = $form.find("#step_description_textarea"); - var $tinyMCEInput = TinyMCE.getContent(); + var $tinyMCEInput = tinyMCE.editors.step_description_textarea.getContent(); var descriptionValid = textValidator(ev, $descrTextarea, 0, <%= Constants::RICH_TEXT_MAX_LENGTH %>, false, $tinyMCEInput); var tableNamesValidArray = []; @@ -601,6 +601,7 @@ SmartAnnotation.preventPropagation('.atwho-user-popover'); tinyMCE.editors.step_description_textarea.remove(); + MarvinJsEditor.initNewButton('.new-marvinjs-upload-button'); //Rerender tables $new_step.find("div.step-result-hot-table").each(function() { @@ -611,6 +612,7 @@ FilePreviewModal.init(); $.initTooltips(); if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); + if (typeof updateRecentProtocolsStatus === 'function') updateRecentProtocolsStatus(); }, error: function(xhr) { if (xhr.responseJSON['assets.file']) { @@ -665,13 +667,20 @@ } // Reorder attachments - global.reorderAttachments = function reorderAtt(stepId, sortType) { + global.reorderAttachments = function reorderAtt(elem, stepId, sortType) { var label_value = $("#dd-att-step-" + stepId + "> .dropdown-menu > li > a[data-order=" + sortType + "]").html(); $("#dd-att-step-" + stepId + "-label").html(label_value); $('#att-' + stepId + ' a.file-preview-link').each(function(){ var elm = $(this) elm.parent().css('order', elm.attr('data-order-' + sortType)); - }) + }); + + $.post( + $(elem).closest('.dropdown-menu').data('stateSavePath'), + { assets: { order: sortType } }, + null, + 'json', + ); } // On init diff --git a/app/assets/javascripts/reports/new.js b/app/assets/javascripts/reports/new.js index ee5e9b8a9..8dcfb9326 100644 --- a/app/assets/javascripts/reports/new.js +++ b/app/assets/javascripts/reports/new.js @@ -45,7 +45,7 @@ function initializeHandsonTable(el) { formulas: true }); el.handsontable("getInstance").loadData(data); - el.handsontable("getInstance").sort(3, order); + el.handsontable('getInstance').getPlugin('columnSorting').sort(3, order); // "Hack" to disable user sorting rows by clicking on // header elements diff --git a/app/assets/javascripts/shared/inline_editing.js b/app/assets/javascripts/shared/inline_editing.js index 009a38910..650ac64cc 100644 --- a/app/assets/javascripts/shared/inline_editing.js +++ b/app/assets/javascripts/shared/inline_editing.js @@ -85,6 +85,7 @@ function initInlineEditing(title) { if (inputString.disabled) { saveAllEditFields(); editBlock.dataset.editMode = 1; + $editBlock.closest('.inline_scroll_block').scrollTop(editBlock.offsetTop); inputString.disabled = false; $inputString.removeClass('hidden'); $editBlock.find('.view-mode').addClass('hidden'); diff --git a/app/assets/javascripts/sitewide/comments.js b/app/assets/javascripts/sitewide/comments.js index 2127b94a1..ecd1f0a83 100644 --- a/app/assets/javascripts/sitewide/comments.js +++ b/app/assets/javascripts/sitewide/comments.js @@ -2,8 +2,14 @@ /* eslint-disable no-restricted-globals, no-alert */ var Comments = (function() { function changeCounter(comment, value) { - var currnetCount = $('#comment-counter-' + comment.closest('.comments-container').attr('data-object-id')); - currnetCount.html(parseInt(currnetCount.html(), 10) + value); + var currentCount = $('#comment-counter-' + comment.closest('.comments-container').attr('data-object-id')); + var newValue = parseInt(currentCount.html(), 10) + value; + currentCount.html(newValue); + if (newValue === 0) { + currentCount.addClass('hidden'); + } else { + currentCount.removeClass('hidden'); + } } function scrollBottom(container) { @@ -76,6 +82,7 @@ var Comments = (function() { $el.find('#message').val(''); $el.find('.new-comment-button').removeClass('show'); newButton.disable = false; + $el.find('textarea').focus().blur(); }) .error((error) => { errorField.html(error.responseJSON.errors.message); diff --git a/app/assets/javascripts/sitewide/file_preview.js b/app/assets/javascripts/sitewide/file_preview.js index da16d0c5b..f71203a63 100644 --- a/app/assets/javascripts/sitewide/file_preview.js +++ b/app/assets/javascripts/sitewide/file_preview.js @@ -1,12 +1,17 @@ /* eslint no-underscore-dangle: ["error", { "allowAfterThis": true }]*/ /* eslint no-use-before-define: ["error", { "functions": false }]*/ /* eslint-disable no-underscore-dangle */ -/* global Uint8Array fabric tui animateSpinner I18n PerfectScrollbar*/ +/* global Uint8Array fabric tui animateSpinner Assets + I18n PerfectScrollbar MarvinJsEditor refreshProtocolStatusBar */ + var FilePreviewModal = (function() { 'use strict'; var readOnly = false; + var CHECK_READY_DELAY = 5000; + var CHECK_READY_TRIES_LIMIT = 60; + var checkReadyCntr; function initPreviewModal(options = {}) { var name; @@ -17,10 +22,11 @@ var FilePreviewModal = (function() { $('.file-preview-link').off('click'); $('.file-preview-link').click(function(e) { e.preventDefault(); - name = $(this).find('p').text(); + name = $(this).find('.attachment-label').text(); url = $(this).data('preview-url'); downloadUrl = $(this).attr('href'); openPreviewModal(name, url, downloadUrl); + return true; }); } @@ -402,8 +408,33 @@ var FilePreviewModal = (function() { imageEditor = {}; $('#tui-image-editor').html(''); $('#fileEditModal').modal('hide'); - if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); }); + + if (data.mode === 'tinymce') { + $.ajax({ + type: 'PUT', + url: data.url, + data: dataUpload, + contentType: false, + processData: false, + success: function(res) { + data.image.src = res.url; + } + }).done(function() { closeEditor(); }); + } else { + $.ajax({ + type: 'POST', + url: '/files/' + data.id + '/update_image', + data: dataUpload, + contentType: false, + processData: false, + success: function(res) { + $('#modal_link' + data.id).parent().html(res.html); + Assets.setupAssetsLoading(); + } + }).done(function() { closeEditor(); }); + } + if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); }); window.onresize = function() { @@ -420,8 +451,7 @@ var FilePreviewModal = (function() { dataType: 'json', success: function(data) { var link = modal.find('.file-download-link'); - modal.find('.file-preview-container').empty(); - modal.find('.file-wopi-controls').empty(); + clearPrevieModal(); if (Object.prototype.hasOwnProperty.call(data, 'wopi-controls')) { modal.find('.file-wopi-controls').html(data['wopi-controls']); } @@ -443,6 +473,7 @@ var FilePreviewModal = (function() { if (!readOnly && data.editable) { modal.find('.file-edit-link').css('display', ''); modal.find('.file-edit-link').off().click(function(ev) { + $.post('/files/' + data.id + '/start_edit_image'); ev.preventDefault(); ev.stopPropagation(); modal.modal('hide'); @@ -452,6 +483,8 @@ var FilePreviewModal = (function() { modal.find('.file-edit-link').css('display', 'none'); } } + } else if (data.type === 'marvinjs') { + openMarvinEditModal(data, modal); } else { modal.find('.file-edit-link').css('display', 'none'); modal.find('.file-preview-container').html(data['preview-icon']); @@ -460,10 +493,13 @@ var FilePreviewModal = (function() { modal.find('#wopi_file_edit_button').remove(); } if (data.processing) { - checkFileReady(url, modal); + setTimeout(function() { + checkFileReady(url, modal); + }, CHECK_READY_DELAY); } modal.find('.file-name').text(name); modal.find('.preview-close').click(function() { + checkReadyCntr = CHECK_READY_TRIES_LIMIT; modal.modal('hide'); if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); }); @@ -472,6 +508,7 @@ var FilePreviewModal = (function() { ev.preventDefault(); }); $('.modal-backdrop').last().css('z-index', modal.css('z-index') - 1); + checkReadyCntr = 0; }, error: function() { // TODO @@ -492,9 +529,11 @@ var FilePreviewModal = (function() { ev.preventDefault(); ev.stopPropagation(); }); - setTimeout(function() { - checkFileReady(url, modal); - }, 10000); + if (checkReadyCntr < CHECK_READY_TRIES_LIMIT) { + setTimeout(function() { + checkFileReady(url, modal); + }, CHECK_READY_DELAY); + } } else { if (data.type === 'image') { modal.find('.file-preview-container').empty(); @@ -518,9 +557,45 @@ var FilePreviewModal = (function() { .off(); } }); + + checkReadyCntr += 1; + } + + function clearPrevieModal() { + var modal = $('#filePreviewModal'); + modal.find('.file-preview-container').empty(); + modal.find('.file-wopi-controls').empty(); + modal.find('.file-edit-link').css('display', 'none'); + } + + function openMarvinEditModal(data, modal) { + modal.find('.file-preview-container') + .append($('') + .attr('src', data['large-preview-url']) + .attr('alt', data.name) + .click(function(ev) { + ev.stopPropagation(); + })); + if (!readOnly && data.editable) { + modal.find('.file-edit-link').css('display', ''); + modal.find('.file-edit-link').off().click(function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + modal.modal('hide'); + MarvinJsEditor.open({ + mode: 'edit', + data: data.description, + name: data.name, + marvinUrl: data['update-url'] + }); + }); + } else { + modal.find('.file-edit-link').css('display', 'none'); + } } return Object.freeze({ - init: initPreviewModal + init: initPreviewModal, + imageEditor: initImageEditor }); }(window)); diff --git a/app/assets/javascripts/sitewide/marvinjs_editor.js b/app/assets/javascripts/sitewide/marvinjs_editor.js new file mode 100644 index 000000000..3fa9442e5 --- /dev/null +++ b/app/assets/javascripts/sitewide/marvinjs_editor.js @@ -0,0 +1,318 @@ +/* global TinyMCE, ChemicalizeMarvinJs, MarvinJSUtil, I18n, FilePreviewModal, tinymce */ +/* global Results, Comments */ +/* eslint-disable no-param-reassign */ +/* eslint-disable wrap-iife */ +/* eslint-disable no-use-before-define */ + + +var marvinJsRemoteLastMrv; +var marvinJsRemoteEditor; +var MarvinJsEditor; + +var MarvinJsEditorApi = (function() { + var marvinJsModal = $('#MarvinJsModal'); + var marvinJsContainer = $('#marvinjs-editor'); + var marvinJsObject = $('#marvinjs-sketch'); + var emptySketch = ''; + var sketchName = marvinJsModal.find('.file-name input'); + var marvinJsMode = marvinJsContainer.data('marvinjsMode'); + + // Facade api actions + var marvinJsExportImage = (childFuction, options = {}) => { + if (marvinJsMode === 'remote') { + remoteExportImage(childFuction, options); + } else { + localExportImage(childFuction, options); + } + }; + // /////////////// + + var loadEditor = () => { + if (marvinJsMode === 'remote') { + return marvinJsRemoteEditor; + } + return MarvinJSUtil.getEditor('#marvinjs-sketch'); + }; + var loadPackages = () => { + return MarvinJSUtil.getPackage('#marvinjs-sketch'); + }; + + // Local marvinJS installation + + var localExportImage = (childFuction, options = {}) => { + loadEditor().then(function(sketcherInstance) { + sketcherInstance.exportStructure('mrv').then(function(source) { + loadPackages().then(function(sketcherPackage) { + sketcherPackage.onReady(function() { + var exporter = createExporter(sketcherPackage, 'image/jpeg'); + exporter.render(source).then(function(image) { + childFuction(source, image, options); + }); + }); + }); + }); + }); + }; + + // Web services installation + + var remoteImage = (childFuction, source, options = {}) => { + var params = { + carbonLabelVisible: false, + implicitHydrogen: 'TERMINAL_AND_HETERO', + displayMode: 'WIREFRAME', + width: 900, + height: 900 + }; + if (typeof (marvinJsRemoteEditor) === 'undefined') { + setTimeout(() => { remoteImage(childFuction, source, options); }, 100); + return false; + } + marvinJsRemoteEditor.exportMrvToImageDataUri(source, 'image/jpeg', params).then(function(image) { + childFuction(source, image, options); + }); + return true; + }; + + var remoteExportImage = (childFuction, options = {}) => { + remoteImage(childFuction, marvinJsRemoteLastMrv, options); + }; + + // Support actions + function preloadActions(config) { + if (marvinJsMode === 'remote') { + if (config.mode === 'new' || config.mode === 'new-tinymce') { + marvinJsRemoteEditor.importStructure('mrv', emptySketch); + sketchName.val(''); + } else if (config.mode === 'edit') { + marvinJsRemoteLastMrv = config.data; + marvinJsRemoteEditor.importStructure('mrv', config.data); + sketchName.val(config.name); + } else if (config.mode === 'edit-tinymce') { + marvinJsRemoteLastMrv = config.data; + $.get(config.marvinUrl, { object_type: 'TinyMceAsset' }, function(result) { + marvinJsRemoteEditor.importStructure('mrv', result.description); + sketchName.val(result.name); + }); + } + marvinJsRemoteEditor.on('molchange', () => { + marvinJsRemoteEditor.exportStructure('mrv').then(function(source) { + marvinJsRemoteLastMrv = source; + }); + }); + } else { + loadEditor().then(function(marvin) { + if (config.mode === 'new' || config.mode === 'new-tinymce') { + marvin.importStructure('mrv', emptySketch); + sketchName.val(I18n.t('marvinjs.new_sketch')); + } else if (config.mode === 'edit') { + marvin.importStructure('mrv', config.data); + sketchName.val(config.name); + } else if (config.mode === 'edit-tinymce') { + $.get(config.marvinUrl, function(result) { + marvin.importStructure('mrv', result.description); + sketchName.val(result.name); + }); + } + }); + } + } + + function createExporter(marvin, imageType) { + var inputFormat = 'mrv'; + var settings = { + width: 900, + height: 900 + }; + + var params = { + imageType: imageType, + settings: settings, + inputFormat: inputFormat + }; + return new marvin.ImageExporter(params); + } + + function TinyMceBuildHTML(json) { + var imgstr = ""; + return imgstr; + } + + function saveFunction(source, image, config) { + $.post(config.marvinUrl, { + description: source, + object_id: config.objectId, + object_type: config.objectType, + name: sketchName.val(), + image: image + }, function(result) { + var newAsset = $(result.html); + var json; + if (config.objectType === 'Step') { + newAsset.find('.file-preview-link').css('top', '-300px'); + newAsset.addClass('new').prependTo($(config.container)); + setTimeout(function() { + newAsset.find('.file-preview-link').css('top', '0px'); + }, 200); + } else if (config.objectType === 'Result') { + newAsset.prependTo($(config.container)); + Results.expandResult(newAsset); + Comments.init(); + } else if (config.objectType === 'TinyMceAsset') { + json = tinymce.util.JSON.parse(result); + config.editor.execCommand('mceInsertContent', false, TinyMceBuildHTML(json)); + TinyMCE.updateImages(config.editor); + } + $(marvinJsModal).modal('hide'); + FilePreviewModal.init(); + }); + } + + function updateFunction(source, image, config) { + $.ajax({ + url: config.marvinUrl, + data: { + description: source, + name: sketchName.val(), + object_type: config.objectType, + image: image + }, + dataType: 'json', + type: 'PUT', + success: function(json) { + if (config.objectType === 'TinyMceAsset') { + config.image[0].src = json.url; + config.image[0].dataset.mceSrc = json.url; + $(marvinJsModal).modal('hide'); + } else { + $(marvinJsModal).modal('hide'); + $('#modal_link' + json.id + ' img').attr('src', json.url); + $('#modal_link' + json.id + ' .attachment-label').html(json.file_name); + } + } + }); + } + + // MarvinJS Methods + + return { + enabled: function() { + return ($('#MarvinJsModal').length > 0); + }, + + open: function(config) { + if (!MarvinJsEditor.enabled()) { + $('#MarvinJsPromoModal').modal('show'); + return false; + } + if (marvinJsMode === 'remote' && typeof (marvinJsRemoteEditor) === 'undefined') { + setTimeout(() => { MarvinJsEditor.open(config); }, 100); + return false; + } + preloadActions(config); + $(marvinJsModal).modal('show'); + $(marvinJsObject) + .css('width', marvinJsContainer.width() + 'px') + .css('height', marvinJsContainer.height() + 'px'); + marvinJsModal.find('.file-save-link').off('click').on('click', () => { + if (config.mode === 'new') { + MarvinJsEditor.save(config); + } else if (config.mode === 'edit') { + config.objectType = 'Asset'; + MarvinJsEditor.update(config); + } else if (config.mode === 'new-tinymce') { + config.objectType = 'TinyMceAsset'; + MarvinJsEditor.save(config); + } else if (config.mode === 'edit-tinymce') { + config.objectType = 'TinyMceAsset'; + MarvinJsEditor.update(config); + } + }); + return true; + }, + + initNewButton: function(selector) { + $(selector).off('click').on('click', function() { + var objectId = this.dataset.objectId; + var objectType = this.dataset.objectType; + var marvinUrl = this.dataset.marvinUrl; + var container = this.dataset.sketchContainer; + MarvinJsEditor.open({ + mode: 'new', + objectId: objectId, + objectType: objectType, + marvinUrl: marvinUrl, + container: container + }); + }); + }, + + save: function(config) { + marvinJsExportImage(saveFunction, config); + }, + + update: function(config) { + marvinJsExportImage(updateFunction, config); + } + }; +}); + +// TinyMCE plugin + +(function() { + 'use strict'; + + tinymce.PluginManager.requireLangPack('MarvinJsPlugin'); + + tinymce.create('tinymce.plugins.MarvinJsPlugin', { + MarvinJsPlugin: function(ed) { + var editor = ed; + + function openMarvinJs() { + MarvinJsEditor.open({ + mode: 'new-tinymce', + marvinUrl: '/tiny_mce_assets/marvinjs', + editor: editor + }); + } + // Add a button that opens a window + editor.addButton('marvinjsplugin', { + tooltip: I18n.t('marvinjs.new_button'), + icon: 'marvinjs', + onclick: openMarvinJs + }); + + // Adds a menu item to the tools menu + editor.addMenuItem('marvinjsplugin', { + text: I18n.t('marvinjs.new_button'), + icon: 'marvinjs', + context: 'insert', + onclick: openMarvinJs + }); + } + }); + + tinymce.PluginManager.add( + 'marvinjsplugin', + tinymce.plugins.MarvinJsPlugin + ); +})(); + +// Initialization + + +$(document).on('turbolinks:load', function() { + MarvinJsEditor = MarvinJsEditorApi(); + if (MarvinJsEditor.enabled()) { + if ($('#marvinjs-editor')[0].dataset.marvinjsMode === 'remote') { + ChemicalizeMarvinJs.createEditor('#marvinjs-sketch').then(function(marvin) { + marvinJsRemoteEditor = marvin; + }); + } + } + MarvinJsEditor.initNewButton('.new-marvinjs-upload-button'); +}); diff --git a/app/assets/javascripts/sitewide/tiny_mce_file_upload_plugin.js b/app/assets/javascripts/sitewide/tiny_mce_file_upload_plugin.js index 87d32cdb1..0f92445f6 100644 --- a/app/assets/javascripts/sitewide/tiny_mce_file_upload_plugin.js +++ b/app/assets/javascripts/sitewide/tiny_mce_file_upload_plugin.js @@ -88,7 +88,7 @@ form = createElement('form', { action: editor.getParam( 'customimageuploader_form_url', - '/tinymce_assets' + '/tiny_mce_assets' ), target: iframe._id, method: 'POST', diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 929c3e5af..0d2cf38f6 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -23,5 +23,6 @@ @import "select2.min"; @import "extend/perfect-scrollbar"; @import "my_modules/protocols/*"; +@import "my_modules/results/*"; @import "protocols/*"; @import "hooks/*"; diff --git a/app/assets/stylesheets/hooks/tinymce.scss b/app/assets/stylesheets/hooks/tinymce.scss index cb6ad2481..1ebe183c1 100644 --- a/app/assets/stylesheets/hooks/tinymce.scss +++ b/app/assets/stylesheets/hooks/tinymce.scss @@ -43,3 +43,22 @@ .mce-top-part { z-index: 5; } + +.mce-widget.mce-btn[aria-label="Restore last draft"] { + background: $brand-primary; + border-radius: 4px; + transition: $md-transaction; + + i { + color: $color-white; + transition: $md-transaction; + } + + &.mce-disabled { + background: transparent; + + i { + color: inherit; + } + } +} diff --git a/app/assets/stylesheets/marvinjs.scss b/app/assets/stylesheets/marvinjs.scss new file mode 100644 index 000000000..317061491 --- /dev/null +++ b/app/assets/stylesheets/marvinjs.scss @@ -0,0 +1,166 @@ +// 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-marvin-js { + background: transparent; + font-size: $font-size-large; + padding: 0 !important; + + .preview-close { + background: transparent; + border: 0; + color: $color-white; + display: inline-block; + float: right; + } + + .modal-dialog { + height: 100%; + margin: 0; + padding: 0; + width: auto; + } + + .modal-content { + background: transparent; + border: 0; + box-shadow: none; + color: $color-white; + height: 100%; + width: auto; + } + + .modal-header { + background: $color-black; + border: 0; + height: 60px; + line-height: 40px; + padding: 10px 15px; + text-align: center; + + .file-save-link { + margin: 0 20px 0 0; + } + + .file-name { + align-items: center; + display: flex; + float: left; + + 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; + + #marvinjs-editor { + height: 100%; + overflow: hidden; + position: relative; + width: 100%; + + #marvinjs-sketch { + border-right: 1px solid $color-gainsboro; + float: left; + min-height: 450px; + min-width: 500px; + overflow: hidden; + } + } + + .sketch-container { + @include md-card-style; + cursor: pointer; + margin: 10px; + overflow: hidden; + padding: 10px; + position: relative; + + .sketch-image { + height: 100%; + width: 100%; + } + + .sketch-name { + color: $brand-primary; + font-family: Lato; + font-size: 16px; + line-height: 18px; + margin: 10px auto; + overflow: hidden; + text-align: center; + width: 160px; + } + + .sketch-object { + color: $color-emperor; + font-size: 12px; + opacity: .6; + text-align: center; + } + } + } + + .file-save-link { + color: $color-white; + cursor: pointer; + display: inline-block; + float: right; + margin-right: 20px; + } +} + +#new-step-sketch { + + .sketch-container { + display: grid; + float: left; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + width: 100%; + } +} + +.new-marvinjs-upload-button { + + .new-marvinjs-upload-icon { + display: inline-block; + height: 22px; + width: 22px; + + img { + height: 100%; + width: 100%; + } + } +} + +.mce-i-marvinjs::before { + background-image: url("icon_small/marvinjs.svg"); + content: ""; + display: block; + height: 22px; + left: -3px; + line-height: 16px; + position: relative; + top: -3px; + width: 22px; +} diff --git a/app/assets/stylesheets/my_modules/protocols/index.scss b/app/assets/stylesheets/my_modules/protocols/index.scss index 5fee791f3..f9364332b 100644 --- a/app/assets/stylesheets/my_modules/protocols/index.scss +++ b/app/assets/stylesheets/my_modules/protocols/index.scss @@ -239,3 +239,49 @@ border-left: 0; border-radius: 0 5px 5px 0; } + +.my-module-recent-protocols { + flex-grow: 1; + margin-bottom: 5px; + position: relative; + + .btn-group { + align-items: center; + display: flex; + float: right; + height: 33px; + } + + .title { + font-size: 14px; + } + + .dropdown-button { + cursor: pointer; + padding: 10px 5px; + } + + .dropdown-menu { + left: auto; + padding: 0; + right: 0; + width: 402px; + + .protocol { + cursor: pointer; + display: inline-block; + float: left; + padding: 5px 10px; + transition: $md-transaction; + width: 200px; + + &:hover { + background: $color-gainsboro; + } + + .fas { + margin-right: 5px; + } + } + } +} diff --git a/app/assets/stylesheets/my_modules/results/index.scss b/app/assets/stylesheets/my_modules/results/index.scss new file mode 100644 index 000000000..9fe221dc4 --- /dev/null +++ b/app/assets/stylesheets/my_modules/results/index.scss @@ -0,0 +1,17 @@ +// scss-lint:disable SelectorDepth +// scss-lint:disable NestingDepth +// scss-lint:disable SelectorFormat +// scss-lint:disable ImportantRule + +@import "constants"; +@import "mixins"; + +#results-toolbar { + .help_tooltips { + .btn-default { + border: 0; + color: inherit; + margin-left: 10px; + } + } +} diff --git a/app/assets/stylesheets/reports.scss b/app/assets/stylesheets/reports.scss index e03d04417..e939cc080 100644 --- a/app/assets/stylesheets/reports.scss +++ b/app/assets/stylesheets/reports.scss @@ -66,10 +66,6 @@ label { height: auto !important; width: auto !important; } - - .ht_clone_top,.ht_clone_left,.ht_clone_corner { - display: none !important; - } } diff --git a/app/assets/stylesheets/shared/comments.scss b/app/assets/stylesheets/shared/comments.scss index b0746a06b..90e837e25 100644 --- a/app/assets/stylesheets/shared/comments.scss +++ b/app/assets/stylesheets/shared/comments.scss @@ -144,6 +144,21 @@ } } + // Looks like PDF has some specail CSS rules, here is some hack + &.report { + display: block; + float: left; + width: 100%; + + .avatar-placehodler { + float: left; + } + + .content-placeholder { + float: left; + } + } + &[data-edit-mode="0"]:hover, &[data-edit-mode="1"] { .comment-right { @@ -224,13 +239,16 @@ .new-comment-button { cursor: pointer; - font-size: 18px; + font-size: 14px; line-height: 18px; - margin: 8px; + margin: 4px; + padding: 4px; position: absolute; right: -36px; + text-align: center; top: 0; transition: $md-transaction; + width: 26px; &.show { right: 0; diff --git a/app/assets/stylesheets/steps.scss b/app/assets/stylesheets/steps.scss index 17806d96b..caa76e0e6 100644 --- a/app/assets/stylesheets/steps.scss +++ b/app/assets/stylesheets/steps.scss @@ -124,6 +124,35 @@ .pseudo-attachment-container { display: flex; justify-content: center; + overflow: hidden; + + .file-preview-link { + position: relative; + } + + &.new { + order: 0 !important; + + .file-preview-link { + transition: .5s; + } + + .attachment-placeholder { + border: 1px solid $brand-primary; + + &::before { + background: $brand-primary; + border-radius: 0 5px; + bottom: 16px; + color: $color-white; + content: "NEW"; + left: 8px; + line-height: 20px; + position: absolute; + width: 50px; + } + } + } } } @@ -131,10 +160,14 @@ @include md-card-style; color: $color-silver-chalice; height: 280px; - margin: 8px; + margin: 4px 8px 16px; text-align: center; width: 220px; + .attachment-thumbnail { + width: calc(100% - 20px); + } + a { color: inherit; } @@ -183,6 +216,7 @@ .remove-icon { bottom: 15px; + cursor: pointer; display: none; position: relative; right: 10px; diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 276de21df..87a233770 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -919,6 +919,7 @@ ul.content-activities { align-items: center; display: flex; flex-wrap: wrap; + margin-bottom: 5px; .protocol-button { margin-bottom: 5px; diff --git a/app/assets/stylesheets/tiny_mce.scss b/app/assets/stylesheets/tiny_mce.scss index f69adf97c..57eee9a8f 100644 --- a/app/assets/stylesheets/tiny_mce.scss +++ b/app/assets/stylesheets/tiny_mce.scss @@ -5,8 +5,11 @@ border: solid 1px; border-color: $color-white; border-radius: 3px; + float: left; + margin-bottom: 10px; min-height: 100px; padding: 3px; + width: 100%; &:hover { border-color: $color-gainsboro; @@ -63,4 +66,50 @@ .mce-toolbar { background: $color-white !important; } + +.mce-stack-layout { + .tinymce-active-object-handler { + border-top: 1px solid rgb(226, 228, 231); + height: 33px; + width: 100%; + + .tool-button { + border: 1px solid transparent; + cursor: pointer; + display: inline-block; + line-height: 27px; + margin: 2px; + text-align: center; + width: 30px; + + &:hover { + border: 1px solid rgb(226, 228, 231); + } + } + + .mce-i-donwload::before { + content: "\F019"; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + line-height: 16px; + position: absolute; + } + + .mce-i-pencil::before { + content: "\F303"; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + line-height: 16px; + position: absolute; + } + + .mce-i-image::before { + content: "\F03E"; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + line-height: 16px; + position: absolute; + } + } +} // scss-lint:enable ImportantRule diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index f3c4c5211..da3bde03f 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + class AssetsController < ApplicationController include WopiUtil + include AssetsActions # include ActionView::Helpers include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::TextHelper @@ -10,14 +13,14 @@ class AssetsController < ApplicationController include FileIconsHelper before_action :load_vars, except: :create_wopi_file + before_action :check_read_permission before_action :check_edit_permission, only: :edit def file_preview response_json = { 'id' => @asset.id, - 'type' => (@asset.image? ? 'image' : 'file'), - + 'type' => @asset.file.metadata[:asset_type] || (@asset.image? ? 'image' : 'file'), 'filename' => truncate(escape_input(@asset.file_name), length: Constants::FILENAME_TRUNCATION_LENGTH), 'download-url' => download_asset_path(@asset, timestamp: Time.now.to_i) @@ -30,17 +33,25 @@ class AssetsController < ApplicationController elsif @assoc.class == RepositoryCell can_manage_repository_rows?(@repository.team) end - - if @asset.image? - if ['image/jpeg', 'image/pjpeg'].include? @asset.content_type + if response_json['type'] == 'image' + if ['image/jpeg', 'image/pjpeg'].include? @asset.file.content_type response_json['quality'] = @asset.file_image_quality || 90 end response_json.merge!( 'editable' => @asset.editable_image? && can_edit, 'mime-type' => @asset.file.content_type, - 'large-preview-url' => @asset.large_preview + 'large-preview-url' => rails_representation_url(@asset.large_preview) + ) + elsif response_json['type'] == 'marvinjs' + response_json.merge!( + 'editable' => can_edit, + 'large-preview-url' => rails_representation_url(@asset.large_preview), + 'update-url' => marvin_js_asset_path(@asset.id), + 'description' => @asset.file.metadata[:description], + 'name' => @asset.file.metadata[:name] ) else + response_json['preview-icon'] = render_to_string(partial: 'shared/file_preview_icon.html.erb', locals: { asset: @asset }) end @@ -115,6 +126,10 @@ class AssetsController < ApplicationController render layout: false end + def create_start_edit_image_activity + create_edit_image_activity(@asset, current_user, :start_editing) + end + def update_image @asset = Asset.find(params[:id]) orig_file_size = @asset.file_size @@ -123,6 +138,7 @@ class AssetsController < ApplicationController @asset.file.attach(io: params.require(:image), filename: orig_file_name) @asset.save! + create_edit_image_activity(@asset, current_user, :finish_editing) # release previous image space @asset.team.release_space(orig_file_size) # Post process file here diff --git a/app/controllers/concerns/assets_actions.rb b/app/controllers/concerns/assets_actions.rb new file mode 100644 index 000000000..de32d2f6b --- /dev/null +++ b/app/controllers/concerns/assets_actions.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module AssetsActions + extend ActiveSupport::Concern + + def create_edit_image_activity(asset, current_user, started_editing) + action = if started_editing == :start_editing + t('activities.file_editing.started') + elsif started_editing == :finish_editing + t('activities.file_editing.finished') + end + if asset.step.class == Step + protocol = asset.step.protocol + default_step_items = + { step: asset.step.id, + step_position: { id: asset.step.id, value_for: 'position_plus_one' }, + asset_name: { id: asset.id, value_for: 'file_file_name' }, + action: action } + if protocol.in_module? + project = protocol.my_module.experiment.project + team = project.team + type_of = :edit_image_on_step + message_items = { my_module: protocol.my_module.id } + else + type_of = :edit_image_on_step_in_repository + project = nil + team = protocol.team + message_items = { protocol: protocol.id } + end + message_items = default_step_items.merge(message_items) + Activities::CreateActivityService + .call(activity_type: type_of, + owner: current_user, + subject: protocol, + team: team, + project: project, + message_items: message_items) + elsif asset.result.class == Result + my_module = asset.result.my_module + Activities::CreateActivityService + .call(activity_type: :edit_image_on_result, + owner: current_user, + subject: asset.result, + team: my_module.experiment.project.team, + project: my_module.experiment.project, + message_items: { + result: asset.result.id, + asset_name: { id: asset.id, value_for: 'file_file_name' }, + action: action + }) + end + end +end diff --git a/app/controllers/marvin_js_assets_controller.rb b/app/controllers/marvin_js_assets_controller.rb new file mode 100644 index 000000000..83a7aeb56 --- /dev/null +++ b/app/controllers/marvin_js_assets_controller.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +class MarvinJsAssetsController < ApplicationController + 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) + + def create + result = MarvinJsService.create_sketch(marvin_params, current_user, current_team) + if result[:asset] && marvin_params[:object_type] == 'Step' + render json: { + html: render_to_string( + partial: 'steps/attachments/item.html.erb', + locals: { asset: result[:asset], + i: 0, + assets_count: 0, + step: result[:object], + order_atoz: 0, + order_ztoa: 0 } + ) + } + elsif result[:asset] && marvin_params[:object_type] == 'Result' + @my_module = result[:object].my_module + render json: { + html: render_to_string( + partial: 'my_modules/result.html.erb', + locals: { result: result[:object] } + ) + }, status: :ok + elsif result[:asset] + render json: result[:asset] + else + render json: result[:asset].errors, status: :unprocessable_entity + end + end + + def update + asset = MarvinJsService.update_sketch(marvin_params, current_user, current_team) + if asset + render json: { url: rails_representation_url(asset.medium_preview), id: asset.id, file_name: asset.file_name } + else + render json: { error: t('marvinjs.no_sketches_found') }, status: :unprocessable_entity + end + end + + private + + def load_vars + @asset = current_team.assets.find_by_id(params[:id]) + return render_404 unless @asset + + @assoc ||= @asset.step + @assoc ||= @asset.result + + if @assoc.class == Step + @protocol = @assoc.protocol + elsif @assoc.class == Result + @my_module = @assoc.my_module + end + end + + def load_create_vars + @assoc = Step.find_by_id(marvin_params[:object_id]) if marvin_params[:object_type] == 'Step' + @assoc = MyModule.find_by_id(params[:object_id]) if marvin_params[:object_type] == 'Result' + + if @assoc.class == Step + @protocol = @assoc.protocol + elsif @assoc.class == MyModule + @my_module = @assoc + end + end + + def check_read_permission + if @assoc.class == Step + return render_403 unless can_read_protocol_in_module?(@protocol) || + can_read_protocol_in_repository?(@protocol) + elsif @assoc.class == Result || @assoc.class == MyModule + return render_403 unless can_read_experiment?(@my_module.experiment) + else + render_403 + end + end + + def check_edit_permission + if @assoc.class == Step + return render_403 unless can_manage_protocol_in_module?(@protocol) || + can_manage_protocol_in_repository?(@protocol) + elsif @assoc.class == Result || @assoc.class == MyModule + return render_403 unless can_manage_module?(@my_module) + else + render_403 + end + end + + def marvin_params + params.permit(:id, :description, :object_id, :object_type, :name, :image) + end +end diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index ad876c878..63287a0da 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -278,6 +278,11 @@ class MyModulesController < ApplicationController def protocols @protocol = @my_module.protocol + @recent_protcols_positive = Protocol.recent_protocols( + current_user, + current_team, + Constants::RECENT_PROTOCOL_LIMIT + ).any? current_team_switch(@protocol.team) end diff --git a/app/controllers/protocols_controller.rb b/app/controllers/protocols_controller.rb index 7997576d7..c35cf182b 100644 --- a/app/controllers/protocols_controller.rb +++ b/app/controllers/protocols_controller.rb @@ -106,6 +106,14 @@ class ProtocolsController < ApplicationController end end + def recent_protocols + render json: Protocol.recent_protocols( + current_user, + current_team, + Constants::RECENT_PROTOCOL_LIMIT + ).select(:id, :name) + end + def linked_children respond_to do |format| format.json do diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 138dbca40..d0fcd9040 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -3,12 +3,11 @@ class StepsController < ApplicationController include ApplicationHelper include StepsActions - before_action :load_vars, only: %i(edit update destroy show toggle_step_state - checklistitem_state) + before_action :load_vars, only: %i(edit update destroy show toggle_step_state checklistitem_state update_view_state) before_action :load_vars_nested, only: [:new, :create] before_action :convert_table_contents_to_utf8, only: [:create, :update] - before_action :check_view_permissions, only: [:show] + before_action :check_view_permissions, only: %i(show update_view_state) before_action :check_manage_permissions, only: %i(new create edit update destroy) before_action :check_complete_and_checkbox_permissions, only: @@ -214,6 +213,17 @@ class StepsController < ApplicationController end end + def update_view_state + view_state = @step.current_view_state(current_user) + view_state.state['assets']['sort'] = params.require(:assets).require(:order) + view_state.save! if view_state.changed? + respond_to do |format| + format.json do + render json: {}, status: :ok + end + end + end + def destroy if @step.can_destroy? # Update position on other steps of this module @@ -617,7 +627,11 @@ class StepsController < ApplicationController :name, :contents, :_destroy - ] + ], + marvin_js_assets_attributes: %i( + id + _destroy + ) ) end diff --git a/app/controllers/tiny_mce_assets_controller.rb b/app/controllers/tiny_mce_assets_controller.rb index 2e64a4248..2d3b0d870 100644 --- a/app/controllers/tiny_mce_assets_controller.rb +++ b/app/controllers/tiny_mce_assets_controller.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true class TinyMceAssetsController < ApplicationController + before_action :load_vars, only: %i(marvinjs_show marvinjs_update download) + + before_action :check_read_permission, only: %i(marvinjs_show marvinjs_update download) + before_action :check_edit_permission, only: %i(marvinjs_update) + def create image = params.fetch(:file) { render_404 } tiny_img = TinyMceAsset.new(team_id: current_team.id, saved: false) @@ -23,4 +28,91 @@ class TinyMceAssetsController < ApplicationController }, status: :unprocessable_entity end end + + def download + if @asset&.image&.attached? + redirect_to rails_blob_path(@asset.image, disposition: 'attachment') + else + render_404 + end + end + + def marvinjs_show + asset = current_team.tiny_mce_assets.find_by_id(Base62.decode(params[:id])) + return render_404 unless asset + + render json: { + name: asset.image.metadata[:name], + description: asset.image.metadata[:description] + } + end + + def marvinjs_create + result = MarvinJsService.create_sketch(marvin_params, current_user, current_team) + if result[:asset] + render json: { + image: { + url: rails_representation_url(result[:asset].preview), + token: Base62.encode(result[:asset].id), + source_type: result[:asset].image.metadata[:asset_type] + } + }, content_type: 'text/html' + else + render json: result[:asset].errors, status: :unprocessable_entity + end + end + + def marvinjs_update + asset = MarvinJsService.update_sketch(marvin_params, current_user, current_team) + if asset + render json: { url: rails_representation_url(asset.preview), id: asset.id } + else + render json: { error: t('marvinjs.no_sketches_found') }, status: :unprocessable_entity + end + end + + private + + def load_vars + @asset = current_team.tiny_mce_assets.find_by_id(Base62.decode(params[:id])) + return render_404 unless @asset + + @assoc = @asset.object + + if @assoc.class == Step + @protocol = @assoc.protocol + elsif @assoc.class == Protocol + @protocol = @assoc + elsif @assoc.class == MyModule + @my_module = @assoc + elsif @assoc.class == ResultText + @my_module = @assoc.result.my_module + end + end + + def check_read_permission + if @assoc.class == Step || @assoc.class == Protocol + return render_403 unless can_read_protocol_in_module?(@protocol) || + can_read_protocol_in_repository?(@protocol) + elsif @assoc.class == ResultText || @assoc.class == MyModule + return render_403 unless can_read_experiment?(@my_module.experiment) + else + render_403 + end + end + + def check_edit_permission + if @assoc.class == Step || @assoc.class == Protocol + return render_403 unless can_manage_protocol_in_module?(@protocol) || + can_manage_protocol_in_repository?(@protocol) + elsif @assoc.class == ResultText || @assoc.class == MyModule + return render_403 unless can_manage_module?(@my_module) + else + render_403 + end + end + + def marvin_params + params.permit(:id, :description, :object_id, :object_type, :name, :image) + end end diff --git a/app/helpers/my_modules_helper.rb b/app/helpers/my_modules_helper.rb index f91672d89..698015546 100644 --- a/app/helpers/my_modules_helper.rb +++ b/app/helpers/my_modules_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MyModulesHelper def ordered_step_of(my_module) my_module.protocol.steps.order(:position) @@ -8,7 +10,15 @@ module MyModulesHelper end def ordered_assets(step) - step.assets.order(:file_updated_at) + view_state = step.current_view_state(current_user) + sort = case view_state.state.dig('assets', 'sort') + when 'old' then { created_at: :asc } + when 'atoz' then { file_file_name: :asc } + when 'ztoa' then { file_file_name: :desc } + else { created_at: :desc } + end + + step.assets.order(sort) end def az_ordered_assets_index(step, asset_id) @@ -39,11 +49,10 @@ module MyModulesHelper end def is_steps_page? - action_name == "steps" + action_name == 'steps' end def is_results_page? - action_name == "results" + action_name == 'results' end - end diff --git a/app/helpers/protocol_status_helper.rb b/app/helpers/protocol_status_helper.rb index eb074241a..71cdb8fce 100644 --- a/app/helpers/protocol_status_helper.rb +++ b/app/helpers/protocol_status_helper.rb @@ -7,7 +7,7 @@ module ProtocolStatusHelper res << 'data-trigger="focus" data-placement="bottom" title="' res << protocol_status_popover_title(parent) + '" data-content="' + protocol_status_popover_content(parent) + - '">' + protocol_name(parent) + '' + '">' + protocol_name(parent).truncate(Constants::NAME_TRUNCATION_LENGTH) + '' res.html_safe end diff --git a/app/models/asset.rb b/app/models/asset.rb index 7c1faca30..7c664cfc0 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -241,7 +241,7 @@ class Asset < ApplicationRecord download_blob_to_tempfile do |tmp_file| to_asset.file.attach(io: tmp_file.open, filename: file_name) end - new_asset.post_process_file(new_asset.team) + to_asset.post_process_file(to_asset.team) end def extract_image_quality @@ -257,7 +257,7 @@ class Asset < ApplicationRecord end def image? - content_type == %r{^image/#{Regexp.union(Constants::WHITELISTED_IMAGE_TYPES)}} + content_type =~ %r{^image/#{Regexp.union(Constants::WHITELISTED_IMAGE_TYPES)}} end def text? diff --git a/app/models/asset_text_datum.rb b/app/models/asset_text_datum.rb index 30fa7bc6e..3b00cfe78 100644 --- a/app/models/asset_text_datum.rb +++ b/app/models/asset_text_datum.rb @@ -5,7 +5,7 @@ class AssetTextDatum < ApplicationRecord validates :data, presence: true validates :asset, presence: true, uniqueness: true - belongs_to :asset, inverse_of: :asset_text_datum, optional: true + belongs_to :asset, inverse_of: :asset_text_datum after_save :update_ts_index diff --git a/app/models/checklist.rb b/app/models/checklist.rb index 321ed514b..4150e1739 100644 --- a/app/models/checklist.rb +++ b/app/models/checklist.rb @@ -7,7 +7,7 @@ class Checklist < ApplicationRecord length: { maximum: Constants::TEXT_MAX_LENGTH } validates :step, presence: true - belongs_to :step, inverse_of: :checklists, touch: true, optional: true + belongs_to :step, inverse_of: :checklists, touch: true belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', diff --git a/app/models/checklist_item.rb b/app/models/checklist_item.rb index bc9f7b4c3..d66c1d14c 100644 --- a/app/models/checklist_item.rb +++ b/app/models/checklist_item.rb @@ -7,8 +7,7 @@ class ChecklistItem < ApplicationRecord validates :checked, inclusion: { in: [true, false] } belongs_to :checklist, - inverse_of: :checklist_items, - optional: true + inverse_of: :checklist_items belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', diff --git a/app/models/concerns/tiny_mce_images.rb b/app/models/concerns/tiny_mce_images.rb index b5cc990f2..e469d1912 100644 --- a/app/models/concerns/tiny_mce_images.rb +++ b/app/models/concerns/tiny_mce_images.rb @@ -18,7 +18,7 @@ module TinyMceImages description = TinyMceAsset.update_old_tinymce(description, self) tiny_mce_assets.each do |tm_asset| - tm_asset_key = tm_asset.image.preview.key + tm_asset_key = tm_asset.preview.key encoded_tm_asset = Base64.strict_encode64(tm_asset.image.service.download(tm_asset_key)) new_tm_asset_src = "data:image/jpg;base64,#{encoded_tm_asset}" html_description = Nokogiri::HTML(description) @@ -73,7 +73,7 @@ module TinyMceImages tiny_img_clone.transaction do tiny_img_clone.save! - tiny_img_clone.image.attach(io: tiny_img.image.open, filename: tiny_img.image.filename.to_s) + tiny_img.duplicate_file(tiny_img_clone) end target.tiny_mce_assets << tiny_img_clone @@ -96,8 +96,6 @@ module TinyMceImages next if asset && asset.object == self && asset.team_id != asset_team_id - new_image = asset.image - new_image_filename = new_image.file_name else # We need implement size and type checks here new_image = URI.parse(image['src']).open @@ -111,7 +109,11 @@ module TinyMceImages new_asset.transaction do new_asset.save! - new_asset.image.attach(io: new_image, filename: new_image_filename) + if image['data-mce-token'] + asset.duplicate_file(new_asset) + else + new_asset.image.attach(io: new_image, filename: new_image_filename) + end end image['src'] = '' diff --git a/app/models/concerns/viewable_model.rb b/app/models/concerns/viewable_model.rb index 5446fd198..731d7783c 100644 --- a/app/models/concerns/viewable_model.rb +++ b/app/models/concerns/viewable_model.rb @@ -7,6 +7,18 @@ module ViewableModel has_many :view_states, as: :viewable, dependent: :destroy end + # This module requres that the class which includes it implements these methods: + # => default_view_state, returning hash with default state representation + # => validate_view_state(view_state), custom validator for the state hash + + def default_view_state + raise NotImplementedError, 'default_view_state should be implemented!' + end + + def validate_view_state(_view_state) + raise NotImplementedError, 'validate_view_state(view_state) should be implemented!' + end + def current_view_state(user) state = view_states.where(user: user).take state || view_states.create!(user: user, state: default_view_state) diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 5d5bbb40b..5410a8838 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -3,15 +3,13 @@ class Experiment < ApplicationRecord include SearchableModel include SearchableByNameModel - belongs_to :project, inverse_of: :experiments, touch: true, optional: true + belongs_to :project, inverse_of: :experiments, touch: true belongs_to :created_by, foreign_key: :created_by_id, - class_name: 'User', - optional: true + class_name: 'User' belongs_to :last_modified_by, foreign_key: :last_modified_by_id, - class_name: 'User', - optional: true + class_name: 'User' belongs_to :archived_by, foreign_key: :archived_by_id, class_name: 'User', optional: true belongs_to :restored_by, @@ -225,6 +223,12 @@ class Experiment < ApplicationRecord workflowimg.service.exist?(workflowimg.blob.key) end + def workflowimg_file_name + return '' unless workflowimg.attached? + + workflowimg.blob&.filename&.sanitized + end + # Get projects where user is either owner or user in the same team # as this experiment def projects_with_role_above_user(current_user) diff --git a/app/models/my_module.rb b/app/models/my_module.rb index d764eb14f..ed3da9b15 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -34,7 +34,7 @@ class MyModule < ApplicationRecord foreign_key: 'restored_by_id', class_name: 'User', optional: true - belongs_to :experiment, inverse_of: :my_modules, touch: true, optional: true + belongs_to :experiment, inverse_of: :my_modules, touch: true belongs_to :my_module_group, inverse_of: :my_modules, optional: true has_many :results, inverse_of: :my_module, dependent: :destroy has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy diff --git a/app/models/my_module_group.rb b/app/models/my_module_group.rb index 718c53fc0..6682ba6a0 100644 --- a/app/models/my_module_group.rb +++ b/app/models/my_module_group.rb @@ -3,7 +3,7 @@ class MyModuleGroup < ApplicationRecord validates :experiment, presence: true - belongs_to :experiment, inverse_of: :my_module_groups, optional: true + belongs_to :experiment, inverse_of: :my_module_groups belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', diff --git a/app/models/my_module_repository_row.rb b/app/models/my_module_repository_row.rb index c28ce10a0..f0d9eb674 100644 --- a/app/models/my_module_repository_row.rb +++ b/app/models/my_module_repository_row.rb @@ -4,10 +4,8 @@ class MyModuleRepositoryRow < ApplicationRecord class_name: 'User', optional: true belongs_to :repository_row, - optional: true, inverse_of: :my_module_repository_rows belongs_to :my_module, - optional: true, touch: true, inverse_of: :my_module_repository_rows diff --git a/app/models/my_module_tag.rb b/app/models/my_module_tag.rb index 9dac83c2e..10963cec6 100644 --- a/app/models/my_module_tag.rb +++ b/app/models/my_module_tag.rb @@ -2,10 +2,10 @@ class MyModuleTag < ApplicationRecord validates :my_module, :tag, presence: true validates :tag_id, uniqueness: { scope: :my_module_id } - belongs_to :my_module, inverse_of: :my_module_tags, optional: true + belongs_to :my_module, inverse_of: :my_module_tags belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', optional: true - belongs_to :tag, inverse_of: :my_module_tags, optional: true + belongs_to :tag, inverse_of: :my_module_tags end diff --git a/app/models/project.rb b/app/models/project.rb index 1ffd8983e..7b42f9d71 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -29,7 +29,7 @@ class Project < ApplicationRecord foreign_key: 'restored_by_id', class_name: 'User', optional: true - belongs_to :team, inverse_of: :projects, touch: true, optional: true + belongs_to :team, inverse_of: :projects, touch: true has_many :user_projects, inverse_of: :project has_many :users, through: :user_projects has_many :experiments, inverse_of: :project diff --git a/app/models/protocol.rb b/app/models/protocol.rb index 942386d98..23542d4a4 100644 --- a/app/models/protocol.rb +++ b/app/models/protocol.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Protocol < ApplicationRecord include SearchableModel include RenamingUtil @@ -15,6 +17,12 @@ class Protocol < ApplicationRecord in_repository_archived: 4 } + scope :recent_protocols, lambda { |user, team, amount| + where(team: team, protocol_type: :in_repository_public) + .or(where(team: team, protocol_type: :in_repository_private, added_by: user)) + .order(updated_at: :desc).limit(amount) + } + auto_strip_attributes :name, :description, nullify: false # Name is required when its actually specified (i.e. :in_repository? is true) validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } @@ -46,7 +54,7 @@ class Protocol < ApplicationRecord protocol .validates_uniqueness_of :name, case_sensitive: false, scope: :team, - conditions: -> { + conditions: lambda { where( protocol_type: Protocol @@ -59,8 +67,8 @@ class Protocol < ApplicationRecord # Private protocol must have unique name inside its team & user scope protocol .validates_uniqueness_of :name, case_sensitive: false, - scope: [:team, :added_by], - conditions: -> { + scope: %i(team added_by), + conditions: lambda { where( protocol_type: Protocol @@ -72,8 +80,8 @@ class Protocol < ApplicationRecord # Archived protocol must have unique name inside its team & user scope protocol .validates_uniqueness_of :name, case_sensitive: false, - scope: [:team, :added_by], - conditions: -> { + scope: %i(team added_by), + conditions: lambda { where( protocol_type: Protocol @@ -92,7 +100,7 @@ class Protocol < ApplicationRecord belongs_to :my_module, inverse_of: :protocols, optional: true - belongs_to :team, inverse_of: :protocols, optional: true + belongs_to :team, inverse_of: :protocols belongs_to :parent, foreign_key: 'parent_id', class_name: 'Protocol', @@ -244,7 +252,7 @@ class Protocol < ApplicationRecord src.clone_tinymce_assets(dest, dest.team) # Update keywords - if clone_keywords then + if clone_keywords src.protocol_keywords.each do |keyword| ProtocolProtocolKeyword.create( protocol: dest, @@ -294,6 +302,7 @@ class Protocol < ApplicationRecord step.assets.each do |asset| asset2 = asset.dup asset2.save! + asset.duplicate_file(asset2) step2.assets << asset2 assets_to_clone << [asset.id, asset2.id] end @@ -362,7 +371,7 @@ class Protocol < ApplicationRecord def space_taken st = 0 - self.steps.find_each do |step| + steps.find_each do |step| st += step.space_taken end st @@ -437,7 +446,7 @@ class Protocol < ApplicationRecord # Update all module protocols that had # parent set to this protocol - if result then + if result Protocol.where(parent: self).find_each do |p| p.update( parent: nil, @@ -466,7 +475,7 @@ class Protocol < ApplicationRecord self.archived_on = nil self.restored_by = user self.restored_on = Time.now - if self.published_on.present? + if published_on.present? self.published_on = Time.now self.protocol_type = Protocol.protocol_types[:in_repository_public] else @@ -491,15 +500,15 @@ class Protocol < ApplicationRecord self.record_timestamps = false # First, destroy all keywords - self.protocol_protocol_keywords.destroy_all + protocol_protocol_keywords.destroy_all if keywords.present? keywords.each do |kw_name| kw = ProtocolKeyword.find_or_create_by(name: kw_name, team: team) - self.protocol_keywords << kw + protocol_keywords << kw end end end - rescue + rescue StandardError result = false end result @@ -509,7 +518,7 @@ class Protocol < ApplicationRecord self.parent = nil self.parent_updated_at = nil self.protocol_type = Protocol.protocol_types[:unlinked] - self.save! + save! end def update_parent(current_user) @@ -518,16 +527,16 @@ class Protocol < ApplicationRecord parent.reload # Now, clone step contents - Protocol.clone_contents(self, self.parent, current_user, false) + Protocol.clone_contents(self, parent, current_user, false) # Lastly, update the metadata parent.reload parent.record_timestamps = false - parent.updated_at = self.updated_at + parent.updated_at = updated_at parent.save! self.record_timestamps = false - self.parent_updated_at = self.updated_at - self.save! + self.parent_updated_at = updated_at + save! end def update_from_parent(current_user) @@ -535,15 +544,15 @@ class Protocol < ApplicationRecord destroy_contents(current_user) # Now, clone parent's step contents - Protocol.clone_contents(self.parent, self, current_user, false) + Protocol.clone_contents(parent, self, current_user, false) # Lastly, update the metadata - self.reload + reload self.record_timestamps = false - self.updated_at = self.parent.updated_at - self.parent_updated_at = self.parent.updated_at + self.updated_at = parent.updated_at + self.parent_updated_at = parent.updated_at self.added_by = current_user - self.save! + save! end def load_from_repository(source, current_user) @@ -554,14 +563,14 @@ class Protocol < ApplicationRecord Protocol.clone_contents(source, self, current_user, false) # Lastly, update the metadata - self.reload + reload self.record_timestamps = false self.updated_at = source.updated_at self.parent = source self.parent_updated_at = source.updated_at self.added_by = current_user self.protocol_type = Protocol.protocol_types[:linked] - self.save! + save! end def copy_to_repository(new_name, new_protocol_type, link_protocols, current_user) @@ -570,29 +579,23 @@ class Protocol < ApplicationRecord description: description, protocol_type: new_protocol_type, added_by: current_user, - team: self.team + team: team ) - if clone.in_repository_public? - clone.published_on = Time.now - end + clone.published_on = Time.now if clone.in_repository_public? # Don't proceed further if clone is invalid - if clone.invalid? - return clone - end + return clone if clone.invalid? # Okay, clone seems to be valid: let's clone it clone = deep_clone(clone, current_user) # If the above operation went well, update published_on # timestamp - if clone.in_repository_public? - clone.update(published_on: Time.now) - end + clone.update(published_on: Time.now) if clone.in_repository_public? # Link protocols if neccesary - if link_protocols then - self.reload + if link_protocols + reload self.record_timestamps = false self.added_by = current_user self.parent = clone @@ -600,23 +603,23 @@ class Protocol < ApplicationRecord self.parent_updated_at = ts self.updated_at = ts self.protocol_type = Protocol.protocol_types[:linked] - self.save! + save! end - return clone + clone end def deep_clone_my_module(my_module, current_user) clone = Protocol.new_blank_for_module(my_module) - clone.name = self.name - clone.authors = self.authors - clone.description = self.description - clone.protocol_type = self.protocol_type + clone.name = name + clone.authors = authors + clone.description = description + clone.protocol_type = protocol_type - if self.linked? + if linked? clone.added_by = current_user - clone.parent = self.parent - clone.parent_updated_at = self.parent_updated_at + clone.parent = parent + clone.parent_updated_at = parent_updated_at end deep_clone(clone, current_user) @@ -624,13 +627,13 @@ class Protocol < ApplicationRecord def deep_clone_repository(current_user) clone = Protocol.new( - name: self.name, - authors: self.authors, - description: self.description, + name: name, + authors: authors, + description: description, added_by: current_user, - team: self.team, - protocol_type: self.protocol_type, - published_on: self.in_repository_public? ? Time.now : nil, + team: team, + protocol_type: protocol_type, + published_on: in_repository_public? ? Time.now : nil ) cloned = deep_clone(clone, current_user) @@ -653,19 +656,17 @@ class Protocol < ApplicationRecord def destroy_contents(current_user) # Calculate total space taken by the protocol - st = self.space_taken + st = space_taken steps.pluck(:id).each do |id| - unless Step.find(id).destroy(current_user) - raise ActiveRecord::RecordNotDestroyed - end + raise ActiveRecord::RecordNotDestroyed unless Step.find(id).destroy(current_user) end # Release space taken by the step - self.team.release_space(st) - self.team.save + team.release_space(st) + team.save # Reload protocol - self.reload + reload end def can_destroy? @@ -700,15 +701,14 @@ class Protocol < ApplicationRecord p.record_timestamps = false p.decrement!(:nr_of_linked_children) end - if self.parent_id != nil - self.parent.record_timestamps = false - self.parent.increment!(:nr_of_linked_children) + unless parent_id.nil? + parent.record_timestamps = false + parent.increment!(:nr_of_linked_children) end end end def decrement_linked_children - self.parent.decrement!(:nr_of_linked_children) if self.parent.present? + parent.decrement!(:nr_of_linked_children) if parent.present? end - end diff --git a/app/models/report.rb b/app/models/report.rb index ff8f9ecdb..f9f63db38 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -11,8 +11,8 @@ class Report < ApplicationRecord validates :project, presence: true validates :user, presence: true - belongs_to :project, inverse_of: :reports, optional: true - belongs_to :user, inverse_of: :reports, optional: true + belongs_to :project, inverse_of: :reports + belongs_to :user, inverse_of: :reports belongs_to :team, inverse_of: :reports belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', diff --git a/app/models/repository.rb b/app/models/repository.rb index 618cbdc83..231ee7493 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -6,7 +6,7 @@ class Repository < ApplicationRecord attribute :discarded_by_id, :integer - belongs_to :team, optional: true + belongs_to :team belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User' has_many :repository_columns, dependent: :destroy has_many :repository_rows, dependent: :destroy diff --git a/app/models/repository_asset_value.rb b/app/models/repository_asset_value.rb index e1eb6e4c3..eb63be53e 100644 --- a/app/models/repository_asset_value.rb +++ b/app/models/repository_asset_value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryAssetValue < ApplicationRecord belongs_to :created_by, foreign_key: :created_by_id, @@ -15,6 +17,9 @@ class RepositoryAssetValue < ApplicationRecord validates :asset, :repository_cell, presence: true + SORTABLE_COLUMN_NAME = 'assets.file_file_name' + SORTABLE_VALUE_INCLUDE = { repository_asset_value: :asset }.freeze + def formatted asset.file_name end @@ -28,8 +33,8 @@ class RepositoryAssetValue < ApplicationRecord end def update_data!(new_data, user) - file.original_filename = new_data[:file_name] - asset.file.attach(io: new_data[:file_data], filename: new_data[:file_name]) + asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data].split(',')[1])), + filename: new_data[:file_name]) asset.last_modified_by = user self.last_modified_by = user asset.save! && save! @@ -38,15 +43,15 @@ class RepositoryAssetValue < ApplicationRecord def self.new_with_payload(payload, attributes) value = new(attributes) team = value.repository_cell.repository_column.repository.team - file = Paperclip.io_adapters.for(payload[:file_data]) - file.original_filename = payload[:file_name] value.asset = Asset.create!( - file: file, created_by: value.created_by, last_modified_by: value.created_by, team: team ) - value.asset.post_process_file(team) + value.asset.file.attach( + io: StringIO.new(Base64.decode64(payload[:file_data].split(',')[1])), + filename: payload[:file_name] + ) value end end diff --git a/app/models/repository_cell.rb b/app/models/repository_cell.rb index 2ee710f0a..826f61e49 100644 --- a/app/models/repository_cell.rb +++ b/app/models/repository_cell.rb @@ -35,7 +35,7 @@ class RepositoryCell < ActiveRecord::Base validates_inclusion_of :repository_column, in: (lambda do |cell| - cell.repository_row.repository.repository_columns + cell.repository_row&.repository&.repository_columns || [] end) validates :repository_column, presence: true validate :repository_column_data_type diff --git a/app/models/repository_date_value.rb b/app/models/repository_date_value.rb index fae44f6aa..6e6e9e692 100644 --- a/app/models/repository_date_value.rb +++ b/app/models/repository_date_value.rb @@ -9,6 +9,9 @@ class RepositoryDateValue < ApplicationRecord validates :repository_cell, presence: true validates :data, presence: true + SORTABLE_COLUMN_NAME = 'repository_date_values.data' + SORTABLE_VALUE_INCLUDE = :repository_date_value + def formatted data end diff --git a/app/models/repository_list_value.rb b/app/models/repository_list_value.rb index 77574d3da..1ef154bef 100644 --- a/app/models/repository_list_value.rb +++ b/app/models/repository_list_value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryListValue < ApplicationRecord belongs_to :repository_list_item belongs_to :created_by, @@ -12,11 +14,12 @@ class RepositoryListValue < ApplicationRecord validates :repository_cell, presence: true validates_inclusion_of :repository_list_item, in: (lambda do |list_value| - list_value.repository_cell - .repository_column - .repository_list_items + list_value.repository_cell&.repository_column&.repository_list_items || [] end) + SORTABLE_COLUMN_NAME = 'repository_list_items.data' + SORTABLE_VALUE_INCLUDE = { repository_list_value: :repository_list_item }.freeze + def formatted data.to_s end diff --git a/app/models/repository_text_value.rb b/app/models/repository_text_value.rb index 65b5a5dc3..61eb31857 100644 --- a/app/models/repository_text_value.rb +++ b/app/models/repository_text_value.rb @@ -11,6 +11,9 @@ class RepositoryTextValue < ApplicationRecord presence: true, length: { maximum: Constants::TEXT_MAX_LENGTH } + SORTABLE_COLUMN_NAME = 'repository_text_values.data' + SORTABLE_VALUE_INCLUDE = :repository_text_value + def formatted data end diff --git a/app/models/step.rb b/app/models/step.rb index ee5111216..be4a269fa 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -2,6 +2,7 @@ class Step < ApplicationRecord include SearchableModel include SearchableByNameModel include TinyMceImages + include ViewableModel auto_strip_attributes :name, :description, nullify: false validates :name, @@ -65,6 +66,16 @@ class Step < ApplicationRecord end end + def default_view_state + { 'assets' => { 'sort' => 'new' } } + end + + def validate_view_state(view_state) + unless %w(new old atoz ztoa).include?(view_state.state.dig('assets', 'sort')) + view_state.errors.add(:state, :wrong_state) + end + end + def destroy(current_user) @current_user = current_user diff --git a/app/models/team.rb b/app/models/team.rb index ccd350add..9cfa780a7 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -41,6 +41,7 @@ class Team < ApplicationRecord has_many :repositories, dependent: :destroy has_many :reports, inverse_of: :team, dependent: :destroy has_many :activities, inverse_of: :team, dependent: :destroy + has_many :assets, inverse_of: :team, dependent: :destroy attr_accessor :without_templates attr_accessor :without_intro_demo @@ -57,6 +58,13 @@ class Team < ApplicationRecord 'filter' => 'active' } } end + def validate_view_state(view_state) + unless %w(new old atoz ztoa).include?(view_state.state.dig('projects', 'cards', 'sort')) && + %w(active archived).include?(view_state.state.dig('projects', 'filter')) + view_state.errors.add(:state, :wrong_state) + end + end + def search_users(query = nil) a_query = "%#{query}%" users.where.not(confirmed_at: nil) diff --git a/app/models/tiny_mce_asset.rb b/app/models/tiny_mce_asset.rb index 4178192b7..b7478ab95 100644 --- a/app/models/tiny_mce_asset.rb +++ b/app/models/tiny_mce_asset.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class TinyMceAsset < ApplicationRecord + include ActiveStorage::Downloading extend ProtocolsExporter attr_accessor :reference before_create :set_reference, optional: true @@ -120,7 +121,8 @@ class TinyMceAsset < ApplicationRecord new_format = "" asset = find_by_id(token) - unless asset + # impor flag only for import from file cases, because we don't have image in DB + unless asset || import # remove tag if asset deleted description.sub!(old_format, '') next @@ -138,10 +140,9 @@ class TinyMceAsset < ApplicationRecord if exists? order(:id).each do |tiny_mce_asset| asset_guid = get_guid(tiny_mce_asset.id) - asset_file_name = "rte-#{asset_guid.to_s + tiny_mce_asset.image.blob.filename.extension}" + asset_file_name = "rte-#{asset_guid}.#{tiny_mce_asset.image.blob.filename.extension}" ostream.put_next_entry("#{dir}/#{asset_file_name}") ostream.print(tiny_mce_asset.image.download) - input_file.close end end ostream @@ -170,7 +171,7 @@ class TinyMceAsset < ApplicationRecord tiny_img_clone.transaction do tiny_img_clone.save! - tiny_img_clone.image.attach(io: image.download, filename: image.filename.sanitized) + duplicate_file(tiny_img_clone) end return false unless tiny_img_clone.persisted? @@ -188,6 +189,17 @@ class TinyMceAsset < ApplicationRecord obj.reassign_tiny_mce_image_references(cloned_img_ids) end + def blob + image&.blob + end + + def duplicate_file(to_asset) + download_blob_to_tempfile do |tmp_file| + to_asset.image.attach(io: tmp_file.open, filename: file_name) + end + TinyMceAsset.update_estimated_size(to_asset.id) + end + private def self_destruct diff --git a/app/models/user.rb b/app/models/user.rb index 9698b474a..2d177f6dd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,7 @@ class User < ApplicationRecord include User::ProjectRoles include TeamBySubjectModel include InputSanitizeHelper + include ActiveStorage::Downloading acts_as_token_authenticatable devise :invitable, :confirmable, :database_authenticatable, :registerable, @@ -248,6 +249,8 @@ class User < ApplicationRecord end def avatar_variant(style) + return Constants::DEFAULT_AVATAR_URL.gsub(':style', style) unless avatar.attached? + format = case style.to_sym when :medium Constants::MEDIUM_PIC_FORMAT @@ -563,6 +566,28 @@ class User < ApplicationRecord .map { |i| { name: escape_input(i[:full_name]), id: i[:id] } } end + def file_name + return '' unless avatar.attached? + + avatar.blob&.filename&.sanitized + end + + def avatar_base64(style) + unless avatar.present? + missing_link = File.open("#{Rails.root}/app/assets/images/#{style}/missing.png").to_a.join + return "data:image/png;base64,#{Base64.strict_encode64(missing_link)}" + end + + avatar_uri = if avatar.options[:storage].to_sym == :s3 + URI.parse(avatar.url(style)).open.to_a.join + else + File.open(avatar.path(style)).to_a.join + end + + encoded_data = Base64.strict_encode64(avatar_uri) + "data:#{avatar_content_type};base64,#{encoded_data}" + end + protected def confirmation_required? diff --git a/app/models/view_state.rb b/app/models/view_state.rb index 9d3905111..833ab538b 100644 --- a/app/models/view_state.rb +++ b/app/models/view_state.rb @@ -8,4 +8,13 @@ class ViewState < ApplicationRecord scope: %i(viewable_type user_id), message: :not_unique } + + validate :validate_state_content + + private + + def validate_state_content + return unless state.present? + viewable.validate_view_state(self) + end end diff --git a/app/models/zip_export.rb b/app/models/zip_export.rb index 6706662a8..83e0f955f 100644 --- a/app/models/zip_export.rb +++ b/app/models/zip_export.rb @@ -49,7 +49,7 @@ class ZipExport < ApplicationRecord end def zip_file_name - return '' unless file.attached? + return '' unless zip_file.attached? zip_file.blob&.filename&.to_s end diff --git a/app/serializers/api/v1/repository_asset_value_serializer.rb b/app/serializers/api/v1/repository_asset_value_serializer.rb index a64c49532..b55e08664 100644 --- a/app/serializers/api/v1/repository_asset_value_serializer.rb +++ b/app/serializers/api/v1/repository_asset_value_serializer.rb @@ -18,10 +18,10 @@ module Api end def url - if !object.asset&.file&.exists? + if !object.asset&.file&.attached? nil else - rails_blob_path(object.asset.file, disposition: 'attachment') + Rails.application.routes.url_helpers.rails_blob_path(object.asset.file, disposition: 'attachment') end end end diff --git a/app/serializers/api/v1/result_asset_serializer.rb b/app/serializers/api/v1/result_asset_serializer.rb index d86e999f2..d3da45635 100644 --- a/app/serializers/api/v1/result_asset_serializer.rb +++ b/app/serializers/api/v1/result_asset_serializer.rb @@ -19,10 +19,10 @@ module Api end def url - if !object.asset&.file&.exists? + if !object.asset&.file&.attached? nil else - rails_blob_path(object.asset.file, disposition: 'attachment') + Rails.application.routes.url_helpers.rails_blob_path(object.asset.file, disposition: 'attachment') end end end diff --git a/app/serializers/api/v1/user_serializer.rb b/app/serializers/api/v1/user_serializer.rb index 4b29121b2..22c3f262d 100644 --- a/app/serializers/api/v1/user_serializer.rb +++ b/app/serializers/api/v1/user_serializer.rb @@ -5,20 +5,20 @@ module Api class UserSerializer < ActiveModel::Serializer type :users attributes :full_name, :initials, :email - attribute :avatar_file_name, if: -> { object.avatar.present? } - attribute :avatar_file_size, if: -> { object.avatar.present? } - attribute :avatar_url, if: -> { object.avatar.present? } + attribute :avatar_file_name, if: -> { object.avatar.attached? } + attribute :avatar_file_size, if: -> { object.avatar.attached? } + attribute :avatar_url, if: -> { object.avatar.attached? } def avatar_file_name - object.avatar_file_name + object.avatar.blob.filename end def avatar_file_size - object.avatar.size + object.avatar.blob.byte_size end def avatar_url - object.avatar.url(:icon) + object.avatar_url(:icon) end end end diff --git a/app/services/marvin_js_service.rb b/app/services/marvin_js_service.rb new file mode 100644 index 000000000..4b337ad86 --- /dev/null +++ b/app/services/marvin_js_service.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +class MarvinJsService + class << self + def url + ENV['MARVINJS_URL'] + end + + def enabled? + !ENV['MARVINJS_URL'].nil? || !ENV['MARVINJS_API_KEY'].nil? + end + + def create_sketch(params, current_user, current_team) + file = generate_image(params) + if params[:object_type] == 'TinyMceAsset' + asset = TinyMceAsset.new(team_id: current_team.id) + attach_file(asset.image, file, params) + asset.save! + return { asset: asset } + end + + 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! + connect_asset(asset, params, current_user) + end + + def update_sketch(params, _current_user, current_team) + if params[:object_type] == 'TinyMceAsset' + asset = current_team.tiny_mce_assets.find(Base62.decode(params[:id])) + attachment = asset&.image + else + asset = current_team.assets.find(params[:id]) + attachment = asset&.file + end + return unless attachment + + file = generate_image(params) + attach_file(attachment, file, params) + asset + end + + private + + def connect_asset(asset, params, current_user) + if params[:object_type] == 'Step' + object = params[:object_type].constantize.find(params[:object_id]) + object.assets << asset + elsif params[:object_type] == 'Result' + my_module = MyModule.find_by_id(params[:object_id]) + return unless my_module + + object = Result.create(user: current_user, + my_module: my_module, + name: prepare_name(params[:name]), + asset: asset, + last_modified_by: current_user) + end + { asset: asset, object: object } + end + + def generate_image(params) + StringIO.new(Base64.decode64(params[:image].split(',')[1])) + end + + def attach_file(asset, file, params) + asset.attach( + io: file, + filename: "#{prepare_name(params[:name])}.jpg", + content_type: 'image/jpeg', + metadata: { + name: prepare_name(params[:name]), + description: params[:description], + asset_type: 'marvinjs' + } + ) + end + + def prepare_name(sketch_name) + if !sketch_name.empty? + sketch_name + else + I18n.t('marvinjs.new_sketch') + end + end + end +end diff --git a/app/services/model_exporters/experiment_exporter.rb b/app/services/model_exporters/experiment_exporter.rb index 607a122a6..e5b63b298 100644 --- a/app/services/model_exporters/experiment_exporter.rb +++ b/app/services/model_exporters/experiment_exporter.rb @@ -67,10 +67,19 @@ module ModelExporters { result: result, result_comments: result.result_comments, - asset: result.asset, + asset: result_assets_data(result.asset), table: table(result.table), result_text: result.result_text } end + + def result_assets_data(asset) + return unless asset&.file&.attached? + + { + asset: asset, + asset_blob: asset.file.blob + } + end end end diff --git a/app/services/model_exporters/model_exporter.rb b/app/services/model_exporters/model_exporter.rb index 54c1fc09f..141572ce5 100644 --- a/app/services/model_exporters/model_exporter.rb +++ b/app/services/model_exporters/model_exporter.rb @@ -14,27 +14,21 @@ module ModelExporters def copy_files(assets, attachment_name, dir_name) assets.flatten.each do |a| - next unless a.public_send(attachment_name).present? + next unless a.public_send(attachment_name).attached? - unless a.public_send(attachment_name).exists? - raise StandardError, - "File id:#{a.id} of type #{attachment_name} is missing" - end yield if block_given? dir = FileUtils.mkdir_p(File.join(dir_name, a.id.to_s)).first - if defined?(S3_BUCKET) - s3_asset = - S3_BUCKET.object(a.public_send(attachment_name).path.remove(%r{^/})) - file_name = a.public_send(attachment_name).original_filename - File.open(File.join(dir, file_name), 'wb') do |f| - s3_asset.get(response_target: f) - end - else - FileUtils.cp( - a.public_send(attachment_name).path, - File.join(dir, a.public_send(attachment_name).original_filename) - ) - end + + tempfile = Tempfile.new + tempfile.binmode + a.public_send(attachment_name).blob.download { |chunk| tempfile.write(chunk) } + tempfile.flush + tempfile.rewind + FileUtils.cp( + tempfile.path, + File.join(dir, a.file_name) + ) + tempfile.close! end end @@ -57,12 +51,21 @@ module ModelExporters checklists: step.checklists.map { |c| checklist(c) }, step_comments: step.step_comments, step_assets: step.step_assets, - assets: step.assets, + assets: step.assets.map { |a| assets_data(a) }, step_tables: step.step_tables, tables: step.tables.map { |t| table(t) } } end + def assets_data(asset) + return unless asset.file.attached? + + { + asset: asset, + asset_blob: asset.file.blob + } + end + def checklist(checklist) { checklist: checklist, diff --git a/app/services/model_exporters/team_exporter.rb b/app/services/model_exporters/team_exporter.rb index 8bab1de3b..dc09b1a72 100644 --- a/app/services/model_exporters/team_exporter.rb +++ b/app/services/model_exporters/team_exporter.rb @@ -39,9 +39,7 @@ module ModelExporters private def team(team) - if team.tiny_mce_assets.present? - @tiny_mce_assets_to_copy.push(team.tiny_mce_assets) - end + @tiny_mce_assets_to_copy.push(team.tiny_mce_assets) if team.tiny_mce_assets.present? { team: team, default_admin_id: team.user_teams.where(role: 2).first.user.id, @@ -53,7 +51,7 @@ module ModelExporters .map { |n| notification(n) }, custom_fields: team.custom_fields, repositories: team.repositories.map { |r| repository(r) }, - tiny_mce_assets: team.tiny_mce_assets, + tiny_mce_assets: team.tiny_mce_assets.map { |tma| tiny_mce_asset_data(tma) }, protocols: team.protocols.where(my_module: nil).map do |pr| protocol(pr) end, @@ -80,6 +78,7 @@ module ModelExporters user_json['sign_in_count'] = user.sign_in_count user_json['last_sign_in_at'] = user.last_sign_in_at user_json['last_sign_in_ip'] = user.last_sign_in_ip + user_json['avatar'] = user.avatar.blob if user.avatar.attached? copy_files([user], :avatar, File.join(@dir_to_export, 'avatars')) { user: user_json, @@ -152,11 +151,21 @@ module ModelExporters } end + def tiny_mce_asset_data(asset) + { + tiny_mce_asset: asset, + tiny_mce_asset_blob: asset.image.blob + } + end + def get_cell_value_asset(cell) return unless cell.value_type == 'RepositoryAssetValue' @assets_to_copy.push(cell.value.asset) - cell.value.asset + { + asset: cell.value.asset, + asset_blob: cell.value.asset.blob + } end end end diff --git a/app/services/projects_overview_service.rb b/app/services/projects_overview_service.rb index 082c0b589..78edc42cd 100644 --- a/app/services/projects_overview_service.rb +++ b/app/services/projects_overview_service.rb @@ -84,7 +84,9 @@ class ProjectsOverviewService ).joins( "LEFT OUTER JOIN (#{due_modules.to_sql}) due_modules "\ "ON due_modules.experiment_id = experiments.id" - ).left_outer_joins(:user_projects, :project_comments) + ).joins( + 'LEFT OUTER JOIN user_projects ON user_projects.project_id = projects.id' + ).left_outer_joins(:project_comments) # Only admins see all projects of the team unless @user.is_admin_of_team?(@team) diff --git a/app/services/repository_actions/duplicate_cell.rb b/app/services/repository_actions/duplicate_cell.rb index e3f717fa1..6a2d51bf1 100644 --- a/app/services/repository_actions/duplicate_cell.rb +++ b/app/services/repository_actions/duplicate_cell.rb @@ -10,7 +10,7 @@ module RepositoryActions end def call - self.send("duplicate_#{@cell.value_type.underscore}") + __send__("duplicate_#{@cell.value_type.split('::').last.underscore}") end private diff --git a/app/services/repository_datatable_service.rb b/app/services/repository_datatable_service.rb index 9a5e55160..1871c3ed4 100644 --- a/app/services/repository_datatable_service.rb +++ b/app/services/repository_datatable_service.rb @@ -119,9 +119,18 @@ class RepositoryDatatableService end elsif sortable_columns[column_id - 1] == 'repository_cell.value' id = @mappings.key(column_id.to_s) - type = RepositoryColumn.find_by_id(id) - return records unless type - return select_type(type.data_type, records, id, dir) + sorting_column = RepositoryColumn.find_by_id(id) + return records unless sorting_column + + sorting_data_type = sorting_column.data_type.constantize + + cells = RepositoryCell.joins(sorting_data_type::SORTABLE_VALUE_INCLUDE) + .where('repository_cells.repository_column_id': sorting_column.id) + .select("repository_cells.repository_row_id, + #{sorting_data_type::SORTABLE_COLUMN_NAME} AS value") + + records.joins("LEFT OUTER JOIN (#{cells.to_sql}) AS values ON values.repository_row_id = repository_rows.id") + .order("values.value #{dir}") elsif sortable_columns[column_id - 1] == 'users.full_name' # We don't need join user table, because it already joined in fetch_row method return records.order("users.full_name #{dir}") @@ -151,19 +160,6 @@ class RepositoryDatatableService records.order(order_by_index) end - def select_type(type, records, id, dir) - case type - when 'RepositoryTextValue' - filter_by_text_value(records, id, dir) - when 'RepositoryListValue' - filter_by_list_value(records, id, dir) - when 'RepositoryAssetValue' - filter_by_asset_value(records, id, dir) - else - records - end - end - def sort_null_direction(val) val == 'ASC' ? 'LAST' : 'FIRST' end diff --git a/app/services/repository_table_state_column_update_service.rb b/app/services/repository_table_state_column_update_service.rb index 2598a7327..78d433f9f 100644 --- a/app/services/repository_table_state_column_update_service.rb +++ b/app/services/repository_table_state_column_update_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryTableStateColumnUpdateService # We're using Constants::REPOSITORY_TABLE_DEFAULT_STATE as a reference for # default table state; this Ruby Hash makes heavy use of Ruby symbols @@ -56,9 +58,7 @@ class RepositoryTableStateColumnUpdateService state['order'].reject! { |_, v| v[0] == old_column_index } state['order'].each do |k, v| - if v[0].to_i > old_column_index.to_i - state['order'][k] = [(v[0].to_i - 1).to_s, v[1]] - end + state['order'][k] = [(v[0].to_i - 1).to_s, v[1]] if v[0].to_i > old_column_index.to_i end if state['order'].empty? # Fallback to default order if user had table ordered by @@ -73,4 +73,4 @@ class RepositoryTableStateColumnUpdateService table_state.save end end -end \ No newline at end of file +end diff --git a/app/services/team_importer.rb b/app/services/team_importer.rb index ecb3c7dc1..906243b9f 100644 --- a/app/services/team_importer.rb +++ b/app/services/team_importer.rb @@ -90,6 +90,7 @@ class TeamImporter user_notification.notification_id = @notification_mappings[user_notification.notification_id] next if user_notification.notification_id.blank? + user_notification.save! end @@ -241,6 +242,7 @@ class TeamImporter comment.save! if update_annotation(comment.message) end next unless res.result_text + res.save! if update_annotation(res.result_text.text) end end @@ -250,6 +252,7 @@ class TeamImporter # Returns true if text was updated def update_annotation(text) return false if text.nil? + updated = false %w(prj exp tsk rep_item).each do |name| text.scan(/~#{name}~\w+\]/).each do |text_match| @@ -267,6 +270,7 @@ class TeamImporter @repository_row_mappings[orig_id] end next unless new_id + new_id_encoded = new_id.base62_encode text.sub!("~#{name}~#{orig_id_encoded}]", "~#{name}~#{new_id_encoded}]") updated = true @@ -276,6 +280,7 @@ class TeamImporter orig_id_encoded = user_match.match(/\[@[\w+-@?! ]+~(\w+)\]/)[1] orig_id = orig_id_encoded.base62_decode next unless @user_mappings[orig_id] + new_id_encoded = @user_mappings[orig_id].base62_encode text.sub!("~#{orig_id_encoded}]", "~#{new_id_encoded}]") updated = true @@ -327,11 +332,12 @@ class TeamImporter def create_tiny_mce_assets(tmce_assets_json, team) tmce_assets_json.each do |tiny_mce_asset_json| - tiny_mce_asset = TinyMceAsset.new(tiny_mce_asset_json) + tiny_mce_asset = TinyMceAsset.new(tiny_mce_asset_json['tiny_mce_asset']) + tiny_mce_asset_blob = tiny_mce_asset_json['tiny_mce_asset_blob'] # Try to find and load file File.open( File.join(@import_dir, 'tiny_mce_assets', tiny_mce_asset.id.to_s, - tiny_mce_asset.image_file_name) + tiny_mce_asset_blob['filename']) ) do |tiny_mce_file| orig_tmce_id = tiny_mce_asset.id tiny_mce_asset.id = nil @@ -341,7 +347,7 @@ class TeamImporter end tiny_mce_asset.team = team tiny_mce_asset.save! - tiny_mce_asset.image.attach(io: tiny_mce_file, filename: tiny_mce_file.basename) + tiny_mce_asset.image.attach(io: tiny_mce_file, filename: File.basename(tiny_mce_file)) @mce_asset_counter += 1 if tiny_mce_asset.object_id.present? object = tiny_mce_asset.object @@ -368,12 +374,10 @@ class TeamImporter user.password = user_json['user']['encrypted_password'] user.current_team_id = team.id user.invited_by_id = @user_mappings[user.invited_by_id] - if user.avatar.present? + if user_json['user']['avatar'] avatar_path = File.join(@import_dir, 'avatars', orig_user_id.to_s, - user.avatar_file_name) - if File.exist?(avatar_path) - File.open(avatar_path) { |f| user.avatar = f } - end + user_json['user']['avatar']['filename']) + File.open(avatar_path) { |f| user.avatar = f } if File.exist?(avatar_path) end user.save! @user_counter += 1 @@ -394,6 +398,7 @@ class TeamImporter notifications_json.each do |notification_json| notification = Notification.new(notification_json) next if notification.type_of.blank? + orig_notification_id = notification.id notification.id = nil notification.generator_user_id = find_user(notification.generator_user_id) @@ -415,7 +420,6 @@ class TeamImporter @repository_mappings[orig_repository_id] = repository.id @repository_counter += 1 repository_json['repository_columns'].each do |repository_column_json| - repository_column = RepositoryColumn.new( repository_column_json['repository_column'] ) @@ -427,6 +431,7 @@ class TeamImporter repository_column.save! @repository_column_mappings[orig_rep_col_id] = repository_column.id next unless repository_column.data_type == 'RepositoryListValue' + repository_column_json['repository_list_items'].each do |list_item| created_by_id = find_user(repository_column.created_by_id) repository_list_item = RepositoryListItem.new(data: list_item['data']) @@ -662,9 +667,7 @@ class TeamImporter protocol.archived_by_id = find_user(protocol.archived_by_id) protocol.restored_by_id = find_user(protocol.restored_by_id) protocol.my_module = my_module unless protocol.my_module_id.nil? - unless protocol.parent_id.nil? - protocol.parent_id = @protocol_mappings[protocol.parent_id] - end + protocol.parent_id = @protocol_mappings[protocol.parent_id] unless protocol.parent_id.nil? protocol.save! @protocol_counter += 1 @protocol_mappings[orig_protocol_id] = protocol.id @@ -792,9 +795,10 @@ class TeamImporter # returns asset object def create_asset(asset_json, team, user_id = nil) - asset = Asset.new(asset_json) + asset = Asset.new(asset_json['asset']) + asset_blob = asset_json['asset_blob'] File.open( - "#{@import_dir}/assets/#{asset.id}/#{asset.file_name}" + "#{@import_dir}/assets/#{asset.id}/#{asset_blob['filename']}" ) do |file| orig_asset_id = asset.id asset.id = nil @@ -804,7 +808,7 @@ class TeamImporter asset.team = team asset.in_template = true if @is_template asset.save! - asset.file.attach(io: file, filename: file.basename) + asset.file.attach(io: file, filename: File.basename(file)) asset.post_process_file(team) @asset_mappings[orig_asset_id] = asset.id @asset_counter += 1 @@ -864,22 +868,14 @@ class TeamImporter report_element.my_module_id = @my_module_mappings[report_element.my_module_id] end - if report_element.step_id - report_element.step_id = @step_mappings[report_element.step_id] - end - if report_element.result_id - report_element.result_id = @result_mappings[report_element.result_id] - end + report_element.step_id = @step_mappings[report_element.step_id] if report_element.step_id + report_element.result_id = @result_mappings[report_element.result_id] if report_element.result_id if report_element.checklist_id report_element.checklist_id = @checklist_mappings[report_element.checklist_id] end - if report_element.asset_id - report_element.asset_id = @asset_mappings[report_element.asset_id] - end - if report_element.table_id - report_element.table_id = @table_mappings[report_element.table_id] - end + report_element.asset_id = @asset_mappings[report_element.asset_id] if report_element.asset_id + report_element.table_id = @table_mappings[report_element.table_id] if report_element.table_id if report_element.experiment_id report_element.experiment_id = @experiment_mappings[report_element.experiment_id] @@ -902,7 +898,8 @@ class TeamImporter def find_user(user_id) return nil if user_id.nil? - @user_mappings[user_id] ? @user_mappings[user_id] : @admin_id + + @user_mappings[user_id] || @admin_id end def find_list_item_id(list_item_id) diff --git a/app/utilities/delayed_uploader_demo.rb b/app/utilities/delayed_uploader_demo.rb index e8a5a9f80..998db58c0 100644 --- a/app/utilities/delayed_uploader_demo.rb +++ b/app/utilities/delayed_uploader_demo.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + module DelayedUploaderDemo # Get asset from demo_files folder def self.get_asset(user, team, file_name) - Asset.new( - file: File.open( - "#{Rails.root}/app/assets/demo_files/#{file_name}", 'r' - ), + asset = Asset.create( created_by: user, team: team, last_modified_by: user ) + asset.file.attach(io: File.open("#{Rails.root}/app/assets/demo_files/#{file_name}", 'r'), filename: file_name) + asset end # Generates results asset for given module, file_name assumes file is located @@ -32,7 +33,6 @@ module DelayedUploaderDemo ) temp_result.save - temp_asset.save # Generate comment if it exists generate_result_comment(temp_result, current_user, comment) if comment diff --git a/app/utilities/protocols_exporter.rb b/app/utilities/protocols_exporter.rb index f63d31246..1c05dfa02 100644 --- a/app/utilities/protocols_exporter.rb +++ b/app/utilities/protocols_exporter.rb @@ -56,6 +56,7 @@ module ProtocolsExporter "fileRef=\"#{asset_file_name}\">\n" asset_xml << "#{img.file_name}\n" asset_xml << "#{img.content_type}\n" + asset_xml << "\n" asset_xml << "\n" tiny_assets_xml << asset_xml end @@ -104,6 +105,7 @@ module ProtocolsExporter "fileRef=\"#{asset_file_name}\">\n" asset_xml << "#{asset.file_name}\n" asset_xml << "#{asset.content_type}\n" + asset_xml << "\n" asset_xml << "\n" step_xml << asset_xml end diff --git a/app/utilities/protocols_importer.rb b/app/utilities/protocols_importer.rb index 00726582d..c4debc036 100644 --- a/app/utilities/protocols_importer.rb +++ b/app/utilities/protocols_importer.rb @@ -111,7 +111,10 @@ module ProtocolsImporter ) # Decode the file bytes - asset.file.attach(io: StringIO.new(Base64.decode64(asset_json['bytes'])), filename: asset_json['fileName']) + asset.file.attach(io: StringIO.new(Base64.decode64(asset_json['bytes'])), + filename: asset_json['fileName'], + content_type: asset_json['fileType'], + metadata: JSON.parse(asset_json['fileMetadata'] || '{}')) asset.save! asset_ids << asset.id @@ -143,7 +146,7 @@ module ProtocolsImporter def populate_rte(object_json, object, team) return populate_rte_legacy(object_json) unless object_json['descriptionAssets'] - description = TinyMceAsset.update_old_tinymce(object_json['description']) + description = TinyMceAsset.update_old_tinymce(object_json['description'], nil, true) object_json['descriptionAssets'].values.each do |tiny_mce_img_json| tiny_mce_img = TinyMceAsset.new( object: object, @@ -153,8 +156,11 @@ module ProtocolsImporter tiny_mce_img.save! # Decode the file bytes - tiny_mce_img.image.attach(io: StringIO.new(Base64.decode64(tiny_mce_img_json['bytes'])), - filename: tiny_mce_img_json['fileName']) + file = StringIO.new(Base64.decode64(tiny_mce_img_json['bytes'])) + tiny_mce_img.image.attach(io: file, + filename: tiny_mce_img_json['fileName'], + content_type: tiny_mce_img_json['fileType'], + metadata: JSON.parse(tiny_mce_img_json['fileMetadata'] || '{}')) if description.gsub!("data-mce-token=\"#{tiny_mce_img_json['tokenId']}\"", "data-mce-token=\"#{Base62.encode(tiny_mce_img.id)}\"") else diff --git a/app/utilities/repository_import_parser/repository_cell_value_resolver.rb b/app/utilities/repository_import_parser/repository_cell_value_resolver.rb index 08912f120..b312c019f 100644 --- a/app/utilities/repository_import_parser/repository_cell_value_resolver.rb +++ b/app/utilities/repository_import_parser/repository_cell_value_resolver.rb @@ -14,7 +14,7 @@ module RepositoryImportParser def get_value(value, record_row) return unless @column - send("new_#{@column.data_type.underscore}", value, record_row) + __send__("new_#{@column.data_type.split('::').last.underscore}", value, record_row) end private diff --git a/app/views/assets/marvinjs/_create_marvin_sketch_button.html.erb b/app/views/assets/marvinjs/_create_marvin_sketch_button.html.erb new file mode 100644 index 000000000..d4d3c472f --- /dev/null +++ b/app/views/assets/marvinjs/_create_marvin_sketch_button.html.erb @@ -0,0 +1,12 @@ + + + <%= image_tag 'icon_small/marvinjs.svg' %> + + <%= t('marvinjs.new_button') %> + \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 534d7785a..066a5190a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -7,6 +7,11 @@ <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_include_tag 'application' %> + <% if ENV['MARVINJS_API_KEY'] %> + + + <% end %> + <%= favicon_link_tag "favicon.ico" %> <%= favicon_link_tag "favicon-16.png", type: "image/png", size: "16x16" %> <%= favicon_link_tag "favicon-32.png", type: "image/png", size: "32x32" %> @@ -43,6 +48,7 @@ <%= render "shared/about_modal" %> <%= render "shared/file_preview_modal.html.erb" %> <%= render "shared/file_edit_modal.html.erb" %> + <%= render "shared/marvinjs_modal.html.erb" %> <%= render "shared/navigation" %> <% if user_signed_in? && flash[:system_notification_modal] && current_user.show_login_system_notification? %> diff --git a/app/views/my_modules/_recent_protocol_dropdown.html.erb b/app/views/my_modules/_recent_protocol_dropdown.html.erb new file mode 100644 index 000000000..1a24d943c --- /dev/null +++ b/app/views/my_modules/_recent_protocol_dropdown.html.erb @@ -0,0 +1,21 @@ +<% display_status = protocol.description.blank? && protocol.steps.count.zero? %> +<% if @recent_protcols_positive %> +
+
+
<%= t('my_modules.module_header.recent_protocols_from_repository') %>
+ + +
+
+<% end %> \ No newline at end of file diff --git a/app/views/my_modules/protocols.html.erb b/app/views/my_modules/protocols.html.erb index c8f037715..1acf4ad13 100644 --- a/app/views/my_modules/protocols.html.erb +++ b/app/views/my_modules/protocols.html.erb @@ -18,6 +18,9 @@ <%= render partial: "my_modules/protocols/protocol_status_bar.html.erb" %> <%= render partial: "my_modules/protocols/protocol_buttons.html.erb" %> + <% if can_manage_protocol_in_module?(@protocol) %> + <%= render partial: "my_modules/recent_protocol_dropdown.html.erb", locals: {protocol: @my_module.protocol}%> + <% end %>
diff --git a/app/views/my_modules/results.html.erb b/app/views/my_modules/results.html.erb index 48f2d1b7a..3f0e9bb80 100644 --- a/app/views/my_modules/results.html.erb +++ b/app/views/my_modules/results.html.erb @@ -41,6 +41,8 @@ + <%= render partial: '/assets/marvinjs/create_marvin_sketch_button.html.erb', + 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_button", locals: { element_id: @my_module.id, element_type: 'Result' } %>
diff --git a/app/views/projects/experiment_archive/_experiment.html.erb b/app/views/projects/experiment_archive/_experiment.html.erb index fe27a09d7..da055d451 100644 --- a/app/views/projects/experiment_archive/_experiment.html.erb +++ b/app/views/projects/experiment_archive/_experiment.html.erb @@ -23,7 +23,7 @@
- <% if experiment.workflowimg? %> + <% if experiment.workflowimg.attached? %>
<%= image_tag( experiment.workflowimg.expiring_url( diff --git a/app/views/reports/elements/_result_comments_element.html.erb b/app/views/reports/elements/_result_comments_element.html.erb index 68614856f..b6635caef 100644 --- a/app/views/reports/elements/_result_comments_element.html.erb +++ b/app/views/reports/elements/_result_comments_element.html.erb @@ -18,22 +18,13 @@
-
+
<% if comments.count == 0 %> <%=t "projects.reports.elements.result_comments.no_comments" %> <% else %> -
    +
      <% comments.each do |comment| %> - <% comment_ts = comment.created_at %> -
    • - - <%=t "projects.reports.elements.result_comments.comment_prefix", user: comment.user.full_name, date: l(comment_ts, format: :full_date), time: l(comment_ts, format: :time) %> - - -   - <%= custom_auto_link(comment.message, team: current_team) %> - -
    • + <%= render partial: 'shared/comments/item.html.erb', locals: { comment: comment, readonly: true, report: true } %> <% end %>
    <% end %> diff --git a/app/views/reports/elements/_step_comments_element.html.erb b/app/views/reports/elements/_step_comments_element.html.erb index 00f279c7a..5bf307378 100644 --- a/app/views/reports/elements/_step_comments_element.html.erb +++ b/app/views/reports/elements/_step_comments_element.html.erb @@ -18,22 +18,13 @@
-
+
<% if comments.count == 0 %> <%=t "projects.reports.elements.step_comments.no_comments" %> <% else %> -
    +
      <% comments.each do |comment| %> - <% comment_ts = comment.created_at %> -
    • - - <%=t "projects.reports.elements.step_comments.comment_prefix", user: comment.user.full_name, date: l(comment_ts, format: :full_date), time: l(comment_ts, format: :time) %> - - -   - <%= custom_auto_link(comment.message, team: current_team) %> - -
    • + <%= render partial: 'shared/comments/item.html.erb', locals: { comment: comment, readonly: true, report: true } %>` <% end %>
    <% end %> diff --git a/app/views/repository_columns/_manage_column_modal.html.erb b/app/views/repository_columns/_manage_column_modal.html.erb index fa5c2ab52..b4e6382e1 100644 --- a/app/views/repository_columns/_manage_column_modal.html.erb +++ b/app/views/repository_columns/_manage_column_modal.html.erb @@ -25,24 +25,26 @@ <%= f.text_field :name %> <%= f.form_group :data_type, label: { text: t('libraries.manange_modal_column.colum_type') } do %>
    - <%= f.radio_button :data_type, - 'RepositoryTextValue'.freeze, - label: t('libraries.manange_modal_column.labels.text'), - inline: true, - checked: checked?(@repository_column, 'RepositoryTextValue'.freeze), - disabled: disabled?(@repository_column, 'RepositoryTextValue'.freeze) %> - <%= f.radio_button :data_type, - 'RepositoryAssetValue'.freeze, - label: t('libraries.manange_modal_column.labels.file'), - inline: true, - checked: checked?(@repository_column, 'RepositoryAssetValue'.freeze), - disabled: disabled?(@repository_column, 'RepositoryAssetValue'.freeze) %> - <%= f.radio_button :data_type, - 'RepositoryListValue'.freeze, - label: t('libraries.manange_modal_column.labels.dropdown'), - inline: true, - checked: checked?(@repository_column, 'RepositoryListValue'.freeze), - disabled: disabled?(@repository_column, 'RepositoryListValue'.freeze) %> + + <%= f.radio_button :data_type, + 'RepositoryTextValue'.freeze, + label: t('libraries.manange_modal_column.labels.text'), + inline: true, + checked: checked?(@repository_column, 'RepositoryTextValue'.freeze), + disabled: disabled?(@repository_column, 'RepositoryTextValue'.freeze) %> + <%= f.radio_button :data_type, + 'RepositoryAssetValue'.freeze, + label: t('libraries.manange_modal_column.labels.file'), + inline: true, + checked: checked?(@repository_column, 'RepositoryAssetValue'.freeze), + disabled: disabled?(@repository_column, 'RepositoryAssetValue'.freeze) %> + <%= f.radio_button :data_type, + 'RepositoryListValue'.freeze, + label: t('libraries.manange_modal_column.labels.dropdown'), + inline: true, + checked: checked?(@repository_column, 'RepositoryListValue'.freeze), + disabled: disabled?(@repository_column, 'RepositoryListValue'.freeze) %> + <% end %> + <%= image_tag 'medium/processing.gif' %> + + <%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %> + +<% else %> + <% if asset.is_image? && display_image_tag %> + <%= image_tag asset.url(:medium) %> + <% end %> + <% if display_image_tag %> +

    + <%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %> +

    + <% else %> + + <%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %> + + <% end %> +<% end %> diff --git a/app/views/shared/_marvinjs_modal.html.erb b/app/views/shared/_marvinjs_modal.html.erb new file mode 100644 index 000000000..1c9ecaecb --- /dev/null +++ b/app/views/shared/_marvinjs_modal.html.erb @@ -0,0 +1,47 @@ +<% if MarvinJsService.enabled? %> + +<% else %> + +<% end %> diff --git a/app/views/shared/comments/_comments.html.erb b/app/views/shared/comments/_comments.html.erb index 7168ff3ac..542a41ed5 100644 --- a/app/views/shared/comments/_comments.html.erb +++ b/app/views/shared/comments/_comments.html.erb @@ -1,6 +1,6 @@
    > <% per_page = Constants::COMMENTS_SEARCH_LIMIT %> -
    +
    <% if comments.size == per_page %>
    +<% report = false unless defined?(report) %> +<% readonly = false unless defined?(readonly) %> +<% edit_mode = (comment.user == current_user && !readonly) %>
    - - <%= image_tag avatar_path(comment.user, :icon_small), class: 'avatar' %> + + <% if report %> + <%= image_tag comment.user.avatar_base64(:icon_small), class: 'avatar' %> + <% else %> + <%= image_tag avatar_path(comment.user, :icon_small), class: 'avatar' %> + <% end %>
    <%= comment.user.full_name %>
    <%= l(comment.created_at, format: :full) %>
    - <% if user_comment %> + <% if edit_mode %>
    <%= t('general.save') %> @@ -39,7 +45,7 @@
    <%= custom_auto_link(comment.message, team: current_team, simple_format: true).html_safe %>
    - <% if user_comment %> + <% if edit_mode %> <%= text_area_tag 'message', comment.message, disabled: true, class: 'smart-text-area hidden' %> <% end %>
    diff --git a/app/views/steps/_empty_step.html.erb b/app/views/steps/_empty_step.html.erb index 3964609a7..1017916fb 100644 --- a/app/views/steps/_empty_step.html.erb +++ b/app/views/steps/_empty_step.html.erb @@ -87,4 +87,4 @@ <%= t("protocols.steps.new.add_table") %> <% end %>
    -
    +
    diff --git a/app/views/steps/attachments/_item.html.erb b/app/views/steps/attachments/_item.html.erb index 1b5faeb86..2723e1ffa 100644 --- a/app/views/steps/attachments/_item.html.erb +++ b/app/views/steps/attachments/_item.html.erb @@ -1,3 +1,4 @@ +
    <%= link_to download_asset_path(asset), class: 'file-preview-link', @@ -5,8 +6,8 @@ data: { no_turbolink: true, id: true, 'preview-url': asset_file_preview_path(asset), - 'order-atoz': az_ordered_assets_index(step, asset.id), - 'order-ztoa': assets_count - az_ordered_assets_index(step, asset.id), + 'order-atoz': order_atoz, + 'order-ztoa': order_ztoa, 'order-old': i, 'order-new': assets_count - i, } do %> diff --git a/app/views/steps/attachments/_list.html.erb b/app/views/steps/attachments/_list.html.erb index c13c04d97..0d619d9ce 100644 --- a/app/views/steps/attachments/_list.html.erb +++ b/app/views/steps/attachments/_list.html.erb @@ -1,4 +1,4 @@ -<% assets = ordered_assets step %> +<% assets = ordered_assets(step) %>

    @@ -6,26 +6,32 @@

    - <%= t('protocols.steps.files', count: assets.count) %> + <%= t('protocols.steps.files', count: assets.length) %>

    +
    <% if !(preview) && (can_manage_protocol_in_module?(@protocol) || can_manage_protocol_in_repository?(@protocol)) %> + <%= render partial: '/assets/marvinjs/create_marvin_sketch_button.html.erb', + locals: { element_id: step.id, element_type: 'Step', sketch_container: ".attachments#att-#{step.id}" } %> <%= render partial: '/assets/wopi/create_wopi_file_button.html.erb', locals: { element_id: step.id, element_type: 'Step' } %> <% end %>