mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-05 19:34:34 +08:00
Merge branch 'activestorage_migration' into ok_SCI_3679
This commit is contained in:
commit
eaf9d59819
166 changed files with 3024 additions and 997 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -77,3 +77,8 @@ spec/addons
|
|||
|
||||
# RVM/rbenv ruby version for local development
|
||||
.ruby-version
|
||||
|
||||
#ignore marvinJs
|
||||
|
||||
public/marvinjs
|
||||
public/marvin4js-license.cxl
|
||||
|
|
10
Gemfile
10
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'
|
||||
|
|
|
@ -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
|
||||
|
|
1
app/assets/images/icon_small/marvinjs.svg
Normal file
1
app/assets/images/icon_small/marvinjs.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-2{fill:#505050}</style></defs><path fill="none" d="M0 0h24v24H0z" id="Trim_Area" data-name="Trim Area"/><g id="Icons"><path class="cls-2" d="M12 2.56l8 4.62v9.64l-8 4.62-8-4.62V7.18zm0-.95L3 6.8v10.4l9 5.19 9-5.19V6.8l-9-5.19z"/><path class="cls-2" d="M5.28 9.52l8.21-4.74-1-.58-7.21 4.16v1.16zM18.51 16V7.67l-1-.57v9.48l1-.58zm-13.23-.95v1.15l6.44 3.72 1-.58-7.44-4.29z"/></g></svg>
|
After Width: | Height: | Size: 461 B |
|
@ -42,6 +42,7 @@
|
|||
//= require shared/inline_editing
|
||||
//= require activestorage
|
||||
//= require turbolinks
|
||||
//= require marvinjslauncher
|
||||
|
||||
|
||||
// Initialize links for submitting forms. This is useful for submitting
|
||||
|
|
485
app/assets/javascripts/my_modules/protocols.js
Normal file
485
app/assets/javascripts/my_modules/protocols.js
Normal file
|
@ -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 "<input type='radio'>";
|
||||
}
|
||||
}, {
|
||||
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) => {
|
||||
$('<div class="protocol"><i class="fas fa-file-alt"></i>'
|
||||
+ truncateLongString(protocol.name, globalConstants.name_truncation_length)
|
||||
+ '</div>').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();
|
|
@ -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('<!--[CDATA[', '')
|
||||
.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('<!--[CDATA[', '')
|
||||
.replace(' ]]-->', '')
|
||||
.replace(']]>', '');
|
||||
}
|
||||
stepAssetJson.bytes = getAssetBytes(
|
||||
protocolFolders[index],
|
||||
stepGuid,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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($('<img>')
|
||||
.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));
|
||||
|
|
318
app/assets/javascripts/sitewide/marvinjs_editor.js
Normal file
318
app/assets/javascripts/sitewide/marvinjs_editor.js
Normal file
|
@ -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 = '<cml><MDocument></MDocument></cml>';
|
||||
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 = "<img src='" + json.image.url + "'";
|
||||
imgstr += " width='300' height='300'";
|
||||
imgstr += " data-mce-token='" + json.image.token + "'";
|
||||
imgstr += " data-source-type='" + json.image.source_type + "'";
|
||||
imgstr += " alt='description-" + json.image.token + "' />";
|
||||
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');
|
||||
});
|
|
@ -88,7 +88,7 @@
|
|||
form = createElement('form', {
|
||||
action: editor.getParam(
|
||||
'customimageuploader_form_url',
|
||||
'/tinymce_assets'
|
||||
'/tiny_mce_assets'
|
||||
),
|
||||
target: iframe._id,
|
||||
method: 'POST',
|
||||
|
|
|
@ -23,5 +23,6 @@
|
|||
@import "select2.min";
|
||||
@import "extend/perfect-scrollbar";
|
||||
@import "my_modules/protocols/*";
|
||||
@import "my_modules/results/*";
|
||||
@import "protocols/*";
|
||||
@import "hooks/*";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
166
app/assets/stylesheets/marvinjs.scss
Normal file
166
app/assets/stylesheets/marvinjs.scss
Normal file
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
17
app/assets/stylesheets/my_modules/results/index.scss
Normal file
17
app/assets/stylesheets/my_modules/results/index.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,10 +66,6 @@ label {
|
|||
height: auto !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.ht_clone_top,.ht_clone_left,.ht_clone_corner {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -919,6 +919,7 @@ ul.content-activities {
|
|||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.protocol-button {
|
||||
margin-bottom: 5px;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
53
app/controllers/concerns/assets_actions.rb
Normal file
53
app/controllers/concerns/assets_actions.rb
Normal file
|
@ -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
|
100
app/controllers/marvin_js_assets_controller.rb
Normal file
100
app/controllers/marvin_js_assets_controller.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) + '</a>'
|
||||
'">' + protocol_name(parent).truncate(Constants::NAME_TRUNCATION_LENGTH) + '</a>'
|
||||
res.html_safe
|
||||
end
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'] = ''
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = "<img src=\"\" class=\"img-responsive\" data-mce-token=\"#{Base62.encode(token.to_i)}\"/>"
|
||||
|
||||
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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
89
app/services/marvin_js_service.rb
Normal file
89
app/services/marvin_js_service.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
end
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -56,6 +56,7 @@ module ProtocolsExporter
|
|||
"fileRef=\"#{asset_file_name}\">\n"
|
||||
asset_xml << "<fileName>#{img.file_name}</fileName>\n"
|
||||
asset_xml << "<fileType>#{img.content_type}</fileType>\n"
|
||||
asset_xml << "<fileMetadata><!--[CDATA[ #{img.image.metadata.to_json} ]]--></fileMetadata>\n"
|
||||
asset_xml << "</tinyMceAsset>\n"
|
||||
tiny_assets_xml << asset_xml
|
||||
end
|
||||
|
@ -104,6 +105,7 @@ module ProtocolsExporter
|
|||
"fileRef=\"#{asset_file_name}\">\n"
|
||||
asset_xml << "<fileName>#{asset.file_name}</fileName>\n"
|
||||
asset_xml << "<fileType>#{asset.content_type}</fileType>\n"
|
||||
asset_xml << "<fileMetadata><!--[CDATA[ #{asset.file.metadata.to_json} ]]--></fileMetadata>\n"
|
||||
asset_xml << "</asset>\n"
|
||||
step_xml << asset_xml
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<span
|
||||
class="btn btn-default new-marvinjs-upload-button"
|
||||
data-object-id="<%= element_id %>"
|
||||
data-object-type="<%= element_type %>"
|
||||
data-marvin-url="<%= marvin_js_assets_path %>"
|
||||
data-sketch-container="<%= sketch_container %>"
|
||||
>
|
||||
<span class="new-marvinjs-upload-icon">
|
||||
<%= image_tag 'icon_small/marvinjs.svg' %>
|
||||
</span>
|
||||
<%= t('marvinjs.new_button') %>
|
||||
</span>
|
|
@ -7,6 +7,11 @@
|
|||
<%= stylesheet_link_tag 'application', media: 'all' %>
|
||||
<%= javascript_include_tag 'application' %>
|
||||
|
||||
<% if ENV['MARVINJS_API_KEY'] %>
|
||||
<script src="https://marvinjs.chemicalize.com/v1/<%= ENV['MARVINJS_API_KEY'] %>/client-settings.js"></script>
|
||||
<script src="https://marvinjs.chemicalize.com/v1/client.js"></script>
|
||||
<% 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? %>
|
||||
|
|
21
app/views/my_modules/_recent_protocol_dropdown.html.erb
Normal file
21
app/views/my_modules/_recent_protocol_dropdown.html.erb
Normal file
|
@ -0,0 +1,21 @@
|
|||
<% display_status = protocol.description.blank? && protocol.steps.count.zero? %>
|
||||
<% if @recent_protcols_positive %>
|
||||
<div class="my-module-recent-protocols"
|
||||
style="display: <%= display_status ? '' : 'none' %>"
|
||||
data-update-url="<%= load_from_repository_protocol_path(protocol) %>"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<div class="title"><%= t('my_modules.module_header.recent_protocols_from_repository') %></div>
|
||||
<div
|
||||
class="dropdown-button"
|
||||
title="Recent protocols"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
<ul class="dropdown-menu">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
|
@ -18,6 +18,9 @@
|
|||
<%= render partial: "my_modules/protocols/protocol_status_bar.html.erb" %>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
<span class="fas fa-paperclip"></span>
|
||||
<span class="hidden-xs"><%= t("my_modules.results.new_asset_result") %></span>
|
||||
</a>
|
||||
<%= 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' } %>
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<% if experiment.workflowimg? %>
|
||||
<% if experiment.workflowimg.attached? %>
|
||||
<div class="workflowimg-container">
|
||||
<%= image_tag(
|
||||
experiment.workflowimg.expiring_url(
|
||||
|
|
|
@ -18,22 +18,13 @@
|
|||
</div>
|
||||
<div class="report-element-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 comments-container">
|
||||
<div class="col-xs-12 comments-container simple">
|
||||
<% if comments.count == 0 %>
|
||||
<em><%=t "projects.reports.elements.result_comments.no_comments" %></em>
|
||||
<% else %>
|
||||
<ul class="no-style comments-list">
|
||||
<ul class="no-style content-comments">
|
||||
<% comments.each do |comment| %>
|
||||
<% comment_ts = comment.created_at %>
|
||||
<li class="comment" data-ts="<%= comment_ts.to_i %>">
|
||||
<span class="comment-prefix">
|
||||
<em><%=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) %></em>
|
||||
</span>
|
||||
<span class="comment-message">
|
||||
|
||||
<%= custom_auto_link(comment.message, team: current_team) %>
|
||||
</span>
|
||||
</li>
|
||||
<%= render partial: 'shared/comments/item.html.erb', locals: { comment: comment, readonly: true, report: true } %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
|
|
@ -18,22 +18,13 @@
|
|||
</div>
|
||||
<div class="report-element-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 comments-container">
|
||||
<div class="col-xs-12 comments-container simple">
|
||||
<% if comments.count == 0 %>
|
||||
<em><%=t "projects.reports.elements.step_comments.no_comments" %></em>
|
||||
<% else %>
|
||||
<ul class="no-style comments-list">
|
||||
<ul class="no-style content-comments">
|
||||
<% comments.each do |comment| %>
|
||||
<% comment_ts = comment.created_at %>
|
||||
<li class="comment" data-ts="<%= comment_ts.to_i %>">
|
||||
<span class="comment-prefix">
|
||||
<em><%=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) %></em>
|
||||
</span>
|
||||
<span class="comment-message">
|
||||
|
||||
<%= custom_auto_link(comment.message, team: current_team) %>
|
||||
</span>
|
||||
</li>
|
||||
<%= render partial: 'shared/comments/item.html.erb', locals: { comment: comment, readonly: true, report: true } %>`
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
|
|
@ -25,24 +25,26 @@
|
|||
<%= f.text_field :name %>
|
||||
<%= f.form_group :data_type, label: { text: t('libraries.manange_modal_column.colum_type') } do %>
|
||||
<br />
|
||||
<%= 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) %>
|
||||
<span id="repository-column-types-list">
|
||||
<%= 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) %>
|
||||
</span>
|
||||
<% end %>
|
||||
<input class="form-control"
|
||||
data-role="tagsinput"
|
||||
|
|
19
app/views/shared/_asset_link_placeholder.html.erb
Normal file
19
app/views/shared/_asset_link_placeholder.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
|||
<% if asset.file.processing? && display_image_tag && asset.is_image? %>
|
||||
<%= image_tag 'medium/processing.gif' %>
|
||||
<span>
|
||||
<%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
|
||||
</span>
|
||||
<% else %>
|
||||
<% if asset.is_image? && display_image_tag %>
|
||||
<%= image_tag asset.url(:medium) %>
|
||||
<% end %>
|
||||
<% if display_image_tag %>
|
||||
<p>
|
||||
<%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
|
||||
</p>
|
||||
<% else %>
|
||||
<span>
|
||||
<%= truncate(asset.file_file_name, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
47
app/views/shared/_marvinjs_modal.html.erb
Normal file
47
app/views/shared/_marvinjs_modal.html.erb
Normal file
|
@ -0,0 +1,47 @@
|
|||
<% if MarvinJsService.enabled? %>
|
||||
<div id="MarvinJsModal"
|
||||
class="modal modal-marvin-js"
|
||||
role="dialog"
|
||||
aria-labelledby="marvinJsModal"
|
||||
aria-hidden="true"
|
||||
data-backdrop="static"
|
||||
data-keyboard="false">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="preview-close" data-dismiss="modal"><span class="fas fa-times"></span></button>
|
||||
<span class="file-name">
|
||||
<%= t('marvinjs.modal_name_title') %>
|
||||
<%= text_field_tag :sketch_name, '', placeholder: t('marvinjs.structure_placeholder') %>
|
||||
</span>
|
||||
<p class="file-save-link"><span class="fas fa-save"></span> <%= t('SaveClose')%></p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="marvinjs-editor" data-marvinjs-mode="<%= ENV['MARVINJS_API_KEY'] ? 'remote' : 'local' %>">
|
||||
<% if ENV['MARVINJS_API_KEY'] %>
|
||||
<div id="marvinjs-sketch" style="width: 600px; height: 480px"></div>
|
||||
<% elsif ENV['MARVINJS_URL'] %>
|
||||
<iframe id="marvinjs-sketch" src="<%= MarvinJsService.url %>" frameBorder="0"></iframe>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="modal" id="MarvinJsPromoModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
MarvinJS Promo Title
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
MarvinJS Promo Description
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,6 +1,6 @@
|
|||
<div class="comments-container" data-object-id = <%= object.id %>>
|
||||
<% per_page = Constants::COMMENTS_SEARCH_LIMIT %>
|
||||
<div class="content-comments">
|
||||
<div class="content-comments inline_scroll_block">
|
||||
<% if comments.size == per_page %>
|
||||
<div class="comment-more text-center">
|
||||
<a class="btn btn-default btn-more-comments-new"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<% user_comment = comment.user == current_user %>
|
||||
<% report = false unless defined?(report) %>
|
||||
<% readonly = false unless defined?(readonly) %>
|
||||
<% edit_mode = (comment.user == current_user && !readonly) %>
|
||||
<div
|
||||
class="comment-container <%= user_comment ? 'comment-editable-field' : '' %>"
|
||||
class="comment-container <%= edit_mode ? 'comment-editable-field' : '' %> <%= report ? 'report' : '' %>"
|
||||
data-field-to-update="message"
|
||||
data-params-group="comment"
|
||||
data-path-to-update="<%= comment_action_url(comment) %>"
|
||||
|
@ -10,15 +12,19 @@
|
|||
error="false"
|
||||
>
|
||||
<div class="avatar-placehodler">
|
||||
<span class='global-avatar-container'>
|
||||
<%= image_tag avatar_path(comment.user, :icon_small), class: 'avatar' %>
|
||||
<span class='global-avatar-container'>
|
||||
<% if report %>
|
||||
<%= image_tag comment.user.avatar_base64(:icon_small), class: 'avatar' %>
|
||||
<% else %>
|
||||
<%= image_tag avatar_path(comment.user, :icon_small), class: 'avatar' %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="content-placeholder">
|
||||
<div class="comment-name"><%= comment.user.full_name %></div>
|
||||
<div class="comment-right">
|
||||
<div class="comment-datetime"><%= l(comment.created_at, format: :full) %></div>
|
||||
<% if user_comment %>
|
||||
<% if edit_mode %>
|
||||
<div class="comment-actions">
|
||||
<div class="edit-buttons">
|
||||
<span class="save-button"><i class="fas fa-save"></i><%= t('general.save') %></span>
|
||||
|
@ -39,7 +45,7 @@
|
|||
</div>
|
||||
<div class="comment-message">
|
||||
<div class="view-mode"><%= custom_auto_link(comment.message, team: current_team, simple_format: true).html_safe %></div>
|
||||
<% if user_comment %>
|
||||
<% if edit_mode %>
|
||||
<%= text_area_tag 'message', comment.message, disabled: true, class: 'smart-text-area hidden' %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -87,4 +87,4 @@
|
|||
<%= t("protocols.steps.new.add_table") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
<div class="pseudo-attachment-container" style="order: <%= assets_count - i %>">
|
||||
<%= 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 %>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<% assets = ordered_assets step %>
|
||||
<% assets = ordered_assets(step) %>
|
||||
<div class="col-xs-12">
|
||||
<hr>
|
||||
</div>
|
||||
|
@ -6,26 +6,32 @@
|
|||
|
||||
<div class="title">
|
||||
<h4>
|
||||
<%= t('protocols.steps.files', count: assets.count) %>
|
||||
<%= t('protocols.steps.files', count: assets.length) %>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="attachemnts-header pull-right">
|
||||
<% 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 %>
|
||||
<div class="dropdown attachments-order" id="dd-att-step-<%= step.id %>">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span id="dd-att-step-<%= step.id %>-label"><%= t('protocols.steps.attachments.sort_new').html_safe %></span>
|
||||
<span id="dd-att-step-<%= step.id %>-label">
|
||||
<%= t("protocols.steps.attachments.sort.#{step.current_view_state(current_user).state.dig('assets', 'sort')}_html") %>
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="sortMenu">
|
||||
<ul class="dropdown-menu" aria-labelledby="sortMenu" data-state-save-path="<%= update_view_state_step_path(step.id) %>">
|
||||
<% ['new', 'old', 'atoz', 'ztoa'].each do |sort| %>
|
||||
<li>
|
||||
<a data-order="<%= sort %>" onClick="reorderAttachments('<%= step.id %>', '<%= sort %>')">
|
||||
<%= t('protocols.steps.attachments.sort_' + sort ).html_safe %></a>
|
||||
<a data-order="<%= sort %>" onClick="reorderAttachments(this, '<%= step.id %>', '<%= sort %>')">
|
||||
<%= t("protocols.steps.attachments.sort.#{sort}_html") %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
@ -36,8 +42,10 @@
|
|||
|
||||
<div class="col-xs-12 attachments" id="att-<%= step.id %>">
|
||||
<% assets.each_with_index do |asset, i| %>
|
||||
<% order_atoz = az_ordered_assets_index(step, asset.id) %>
|
||||
<% order_ztoa = assets.length - az_ordered_assets_index(step, asset.id) %>
|
||||
<%= render partial: 'steps/attachments/item.html.erb',
|
||||
locals: { asset: asset, i: i, assets_count: assets.count, step: step } %>
|
||||
locals: { asset: asset, i: i, assets_count: assets.length, step: step, order_atoz: order_atoz, order_ztoa: order_ztoa } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<hr>
|
||||
|
|
|
@ -64,6 +64,9 @@ class Constants
|
|||
# Max characters for repository name in Atwho modal
|
||||
ATWHO_REP_NAME_LIMIT = 16
|
||||
|
||||
# Number of protocols in recent protocol dropdown
|
||||
RECENT_PROTOCOL_LIMIT = 14
|
||||
|
||||
#=============================================================================
|
||||
# File and data memory size
|
||||
#=============================================================================
|
||||
|
|
|
@ -218,19 +218,22 @@ class Extends
|
|||
export_protocol_from_task: 106,
|
||||
import_inventory_items: 107,
|
||||
create_tag: 108,
|
||||
delete_tag: 109
|
||||
delete_tag: 109,
|
||||
edit_image_on_result: 110,
|
||||
edit_image_on_step: 111,
|
||||
edit_image_on_step_in_repository: 112,
|
||||
}
|
||||
|
||||
ACTIVITY_GROUPS = {
|
||||
projects: [*0..7, 32, 33, 34, 95, 108, 65, 109],
|
||||
task_results: [23, 26, 25, 42, 24, 40, 41, 99],
|
||||
task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110],
|
||||
task: [8, 58, 9, 59, 10, 11, 12, 13, 14, 35, 36, 37, 53, 54, *60..64, *66..69, 106],
|
||||
task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 45, 46, 47],
|
||||
task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 111, 45, 46, 47],
|
||||
task_inventory: [55, 56],
|
||||
experiment: [*27..31, 57],
|
||||
reports: [48, 50, 49],
|
||||
inventories: [70, 71, 105, 72, 73, 74, 102, 75, 76, 77, 78, 96, 107],
|
||||
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82, 83, 101],
|
||||
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82, 83, 101, 112],
|
||||
team: [92, 94, 93, 97, 104]
|
||||
}.freeze
|
||||
end
|
||||
|
|
|
@ -75,6 +75,8 @@ en:
|
|||
attributes:
|
||||
viewable_id:
|
||||
not_unique: "State already exists for this user and parent object"
|
||||
state:
|
||||
wrong_state: "Wrong parameters"
|
||||
|
||||
helpers:
|
||||
label:
|
||||
|
@ -606,6 +608,7 @@ en:
|
|||
no_description: "No task description"
|
||||
description_label: "Description"
|
||||
empty_description_edit_label: "Click here to enter Task Description (optional)"
|
||||
recent_protocols_from_repository: "Recent protocols from the Repository"
|
||||
protocols:
|
||||
head_title: "%{project} | %{module} | Protocols"
|
||||
protocol_status_bar:
|
||||
|
@ -1331,6 +1334,9 @@ en:
|
|||
wupi_file_editing:
|
||||
started: "editing started"
|
||||
finished: "editing finished"
|
||||
file_editing:
|
||||
started: "editing started"
|
||||
finished: "editing finished"
|
||||
protocols:
|
||||
my_to_team_message: 'My protocols to Team protocols'
|
||||
team_to_my_message: 'Team protocols to My protocols'
|
||||
|
@ -1759,10 +1765,11 @@ en:
|
|||
complete_title: "Complete Step"
|
||||
uncomplete_title: "Uncomplete Step"
|
||||
attachments:
|
||||
sort_new: "Newest first ↓"
|
||||
sort_old: "Oldest first ↑"
|
||||
sort_atoz: "Name ↓"
|
||||
sort_ztoa: "Name ↑"
|
||||
sort:
|
||||
new_html: "Newest first ↓"
|
||||
old_html: "Oldest first ↑"
|
||||
atoz_html: "Name ↓"
|
||||
ztoa_html: "Name ↑"
|
||||
new:
|
||||
add_step_title: "Add new step"
|
||||
tab_checklists: "Checklists"
|
||||
|
@ -2147,3 +2154,9 @@ en:
|
|||
new: "https://support.scinote.net/hc/en-us/articles/360004627792"
|
||||
visibility: "https://support.scinote.net/hc/en-us/articles/360004627472"
|
||||
manage_columns: "https://support.scinote.net/hc/en-us/articles/360004695831"
|
||||
marvinjs:
|
||||
new_sketch: "New structure"
|
||||
new_button: "New structure"
|
||||
structure_placeholder: "Click here to enter structure name"
|
||||
modal_name_title: "Structure name:"
|
||||
checmical_drawing: "Chemical drawings"
|
|
@ -131,6 +131,9 @@ en:
|
|||
edit_tag_html: "%{user} edited tag <strong>%{tag}</strong> in project %{project}."
|
||||
delete_tag_html: "%{user} deleted tag <strong>%{tag}</strong> in project %{project}."
|
||||
import_inventory_items_html: "%{user} imported %{num_of_items} inventory item(s) to %{repository}."
|
||||
edit_image_on_result_html: "%{user} edited image %{asset_name} on result %{result}: %{action}."
|
||||
edit_image_on_step_html: "%{user} edited image %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}: %{action}."
|
||||
edit_image_on_step_in_repository_html: "%{user} edited image %{asset_name} on protocol %{protocol}'s step %{step_position} %{step} in Protocol repository: %{action}."
|
||||
|
||||
activity_name:
|
||||
create_project: "Project created"
|
||||
|
@ -233,6 +236,9 @@ en:
|
|||
edit_tag: "Tag edited"
|
||||
delete_tag: "Tag deleted"
|
||||
import_inventory_items: "Inventory items imported"
|
||||
edit_image_on_result: "Image on result edited"
|
||||
edit_image_on_step: "Image on task step edited"
|
||||
edit_image_on_step_in_repository: "Image on step edited"
|
||||
|
||||
activity_group:
|
||||
projects: "Projects"
|
||||
|
|
|
@ -433,6 +433,7 @@ Rails.application.routes.draw do
|
|||
post 'toggle_step_state'
|
||||
get 'move_down'
|
||||
get 'move_up'
|
||||
post 'update_view_state'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -448,7 +449,16 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
# tinyMCE image uploader endpoint
|
||||
post '/tinymce_assets', to: 'tiny_mce_assets#create', as: :tiny_mce_assets
|
||||
resources :tiny_mce_assets, only: [:create] do
|
||||
member do
|
||||
get :download
|
||||
get :marvinjs, to: 'tiny_mce_assets#marvinjs_show'
|
||||
put :marvinjs, to: 'tiny_mce_assets#marvinjs_update'
|
||||
end
|
||||
collection do
|
||||
post :marvinjs, to: 'tiny_mce_assets#marvinjs_create'
|
||||
end
|
||||
end
|
||||
|
||||
resources :results, only: [:update, :destroy] do
|
||||
resources :result_comments,
|
||||
|
@ -516,6 +526,7 @@ Rails.application.routes.draw do
|
|||
to: 'protocols#protocolsio_import_create'
|
||||
post 'protocolsio_import_save', to: 'protocols#protocolsio_import_save'
|
||||
get 'export', to: 'protocols#export'
|
||||
get 'recent_protocols'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -589,6 +600,7 @@ Rails.application.routes.draw do
|
|||
post 'files/create_wopi_file',
|
||||
to: 'assets#create_wopi_file',
|
||||
as: 'create_wopi_file'
|
||||
post 'files/:id/start_edit_image', to: 'assets#create_start_edit_image_activity', as: 'start_edit_image'
|
||||
|
||||
devise_scope :user do
|
||||
get 'avatar/:id/:style' => 'users/registrations#avatar', as: 'avatar'
|
||||
|
@ -672,6 +684,12 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resources :marvin_js_assets, only: %i(create update destroy show) do
|
||||
collection do
|
||||
get :team_sketches
|
||||
end
|
||||
end
|
||||
|
||||
post 'global_activities', to: 'global_activities#index'
|
||||
|
||||
constraints WopiSubdomain do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Paperclip
|
||||
class CustomFilePreview < Processor
|
||||
def make
|
||||
libreoffice_path = ENV['LIBREOFFICE_PATH'] || 'libreoffice'
|
||||
libreoffice_path = ENV['LIBREOFFICE_PATH'] || 'soffice'
|
||||
directory = File.dirname(@file.path)
|
||||
basename = File.basename(@file.path, '.*')
|
||||
original_preview_file = File.join(directory, "#{basename}.png")
|
||||
|
|
70
spec/controllers/assets_controller_spec.rb
Normal file
70
spec/controllers/assets_controller_spec.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe AssetsController, type: :controller do
|
||||
login_user
|
||||
|
||||
let(:user) { subject.current_user }
|
||||
let!(:team) { create :team, created_by: user }
|
||||
let(:user_team) { create :user_team, :admin, user: user, team: team }
|
||||
let!(:user_project) { create :user_project, :owner, user: user }
|
||||
let(:project) do
|
||||
create :project, team: team, user_projects: [user_project]
|
||||
end
|
||||
let(:experiment) { create :experiment, project: project }
|
||||
let(:my_module) { create :my_module, name: 'test task', experiment: experiment }
|
||||
let(:protocol) do
|
||||
create :protocol, my_module: my_module, team: team, added_by: user
|
||||
end
|
||||
let(:step) { create :step, protocol: protocol, user: user }
|
||||
let(:step_asset_task) { create :step_asset, step: step }
|
||||
|
||||
let(:result) do
|
||||
create :result, name: 'test result', my_module: my_module, user: user
|
||||
end
|
||||
let(:result_asset) { create :result_asset, result: result }
|
||||
|
||||
let(:protocol_in_repository) { create :protocol, :in_public_repository, team: team }
|
||||
let(:step_in_repository) { create :step, protocol: protocol_in_repository, user: user }
|
||||
|
||||
let!(:asset) { create :asset }
|
||||
let(:step_asset_in_repository) { create :step_asset, step: step_in_repository, asset: asset }
|
||||
|
||||
describe 'POST start_edit' do
|
||||
before do
|
||||
allow(controller).to receive(:check_edit_permission).and_return(true)
|
||||
end
|
||||
let(:action) { post :create_start_edit_image_activity, params: params, format: :json }
|
||||
let!(:params) do
|
||||
{ id: nil }
|
||||
end
|
||||
it 'calls create activity service (start edit image on step)' do
|
||||
params[:id] = step_asset_task.asset.id
|
||||
expect(Activities::CreateActivityService).to receive(:call)
|
||||
.with(hash_including(activity_type: :edit_image_on_step))
|
||||
action
|
||||
end
|
||||
|
||||
it 'calls create activity service (start edit image on result)' do
|
||||
params[:id] = result_asset.asset.id
|
||||
expect(Activities::CreateActivityService).to receive(:call)
|
||||
.with(hash_including(activity_type: :edit_image_on_result))
|
||||
action
|
||||
end
|
||||
|
||||
it 'calls create activity service (start edit image on step in repository)' do
|
||||
params[:id] = step_asset_in_repository.asset.id
|
||||
user_team
|
||||
expect(Activities::CreateActivityService).to receive(:call)
|
||||
.with(hash_including(activity_type: :edit_image_on_step_in_repository))
|
||||
action
|
||||
end
|
||||
|
||||
it 'adds activity in DB' do
|
||||
params[:id] = step_asset_task.asset.id
|
||||
expect { action }
|
||||
.to(change { Activity.count })
|
||||
end
|
||||
end
|
||||
end
|
108
spec/controllers/wopi_controller_spec.rb
Normal file
108
spec/controllers/wopi_controller_spec.rb
Normal file
|
@ -0,0 +1,108 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe WopiController, type: :controller do
|
||||
ENV['WOPI_USER_HOST'] = 'localhost'
|
||||
|
||||
login_user
|
||||
let(:user) { subject.current_user }
|
||||
let!(:team) { create :team, created_by: user }
|
||||
let(:user_team) { create :user_team, :admin, user: user, team: team }
|
||||
let!(:user_project) { create :user_project, :owner, user: user }
|
||||
let(:project) do
|
||||
create :project, team: team, user_projects: [user_project]
|
||||
end
|
||||
let(:experiment) { create :experiment, project: project }
|
||||
let(:my_module) { create :my_module, name: 'test task', experiment: experiment }
|
||||
let(:result) do
|
||||
create :result, name: 'test result', my_module: my_module, user: user
|
||||
end
|
||||
let(:protocol) do
|
||||
create :protocol, my_module: my_module, team: team, added_by: user
|
||||
end
|
||||
let(:step) { create :step, protocol: protocol, user: user }
|
||||
|
||||
let(:protocol_in_repository) { create :protocol, :in_public_repository, team: team }
|
||||
let(:step_in_repository) { create :step, protocol: protocol_in_repository, user: user }
|
||||
|
||||
let!(:asset) { create :asset }
|
||||
let(:step_asset) { create :step_asset, step: step, asset: asset }
|
||||
let(:step_asset_in_repository) { create :step_asset, step: step_in_repository, asset: asset }
|
||||
let(:result_asset) { create :result_asset, result: result, asset: asset }
|
||||
let(:token) { Token.create(token: 'token', ttl: 0, user_id: user.id) }
|
||||
|
||||
describe 'POST unlock' do
|
||||
before do
|
||||
token
|
||||
ENV['WOPI_SUBDOMAIN'] = nil
|
||||
allow(controller).to receive(:verify_proof!).and_return(true)
|
||||
@request.headers['X-WOPI-Override'] = 'UNLOCK'
|
||||
@request.headers['X-WOPI-Lock'] = 'lock'
|
||||
asset.lock_asset('lock')
|
||||
end
|
||||
|
||||
let(:action) { post :post_file_endpoint, params: { id: asset.id, access_token: 'token' } }
|
||||
|
||||
describe 'Result asset' do
|
||||
before do
|
||||
result_asset
|
||||
end
|
||||
|
||||
it 'calls create activity for finish wopi editing' do
|
||||
expect(Activities::CreateActivityService)
|
||||
.to(receive(:call)
|
||||
.with(hash_including(activity_type:
|
||||
:edit_wopi_file_on_result)))
|
||||
|
||||
action
|
||||
end
|
||||
|
||||
it 'adds activity in DB' do
|
||||
expect { action }
|
||||
.to(change { Activity.count })
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Step asset' do
|
||||
before do
|
||||
step_asset
|
||||
end
|
||||
|
||||
it 'calls create activity for finish wopi editing' do
|
||||
expect(Activities::CreateActivityService)
|
||||
.to(receive(:call)
|
||||
.with(hash_including(activity_type:
|
||||
:edit_wopi_file_on_step)))
|
||||
|
||||
action
|
||||
end
|
||||
|
||||
it 'adds activity in DB' do
|
||||
expect { action }
|
||||
.to(change { Activity.count })
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Step asset in repository' do
|
||||
before do
|
||||
step_asset_in_repository
|
||||
user_team
|
||||
end
|
||||
|
||||
it 'calls create activity for finish wopi editing' do
|
||||
expect(Activities::CreateActivityService)
|
||||
.to(receive(:call)
|
||||
.with(hash_including(activity_type:
|
||||
:edit_wopi_file_on_step_in_repository)))
|
||||
|
||||
action
|
||||
end
|
||||
|
||||
it 'adds activity in DB' do
|
||||
expect { action }
|
||||
.to(change { Activity.count })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
FactoryBot.define do
|
||||
factory :asset do
|
||||
file_file_name { 'sample_file.txt' }
|
||||
file_content_type { 'text/plain' }
|
||||
file_file_size { 69 }
|
||||
version { 1 }
|
||||
estimated_size { 232 }
|
||||
file_processing { false }
|
||||
file do
|
||||
fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'test.jpg'), 'image/jpg')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue