Implement DirectUpload for Steps, Results and Inventories [SCI-3679]

This commit is contained in:
Oleksii Kriuchykhin 2019-07-25 18:00:24 +02:00
parent 64b7a5646a
commit 5e65b07bdc
22 changed files with 1183 additions and 1049 deletions

View file

@ -1,30 +1,21 @@
FROM ruby:2.6.3
FROM ruby:2.6.3-buster
MAINTAINER BioSistemika <info@biosistemika.com>
# Get version of Debian (lsb_release substitute) and save it to /tmp/lsb_release for further commands
RUN cat /etc/os-release | grep -Po "VERSION=.*\(\K\w+" | tee /tmp/lsb_release
# Add Debian stretch backports repository
RUN echo "deb http://http.debian.net/debian $(cat /tmp/lsb_release)-backports main" \
| tee /etc/apt/sources.list.d/$(cat /tmp/lsb_release)-backports.list
# additional dependecies
# libSSL-1.0 is required by wkhtmltopdf binary
# libreoffice for file preview generation
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
apt-get update -qq && \
RUN apt-get update -qq && \
apt-get install -y \
libjemalloc1 \
libssl1.0-dev \
libjemalloc2 \
libssl-dev \
nodejs \
yarnpkg \
postgresql-client \
default-jre-headless \
unison \
sudo graphviz --no-install-recommends \
poppler-utils \
sudo graphviz --no-install-recommends \
libreoffice \
libfile-mimeinfo-perl && \
apt-get install -y --no-install-recommends -t $(cat /tmp/lsb_release)-backports libreoffice && \
npm install -g yarn && \
rm -rf /var/lib/apt/lists/*
# heroku tools
@ -34,6 +25,7 @@ ENV BUNDLE_PATH /usr/local/bundle/
# create app directory
ENV APP_HOME /usr/src/app
ENV PATH $APP_HOME/bin:$PATH
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

View file

@ -1,7 +1,7 @@
FROM ruby:2.6.3
FROM ruby:2.6.3-buster
MAINTAINER BioSistemika <info@biosistemika.com>
RUN echo deb "http://http.debian.net/debian stretch-backports main" >> /etc/apt/sources.list
RUN echo deb "http://http.debian.net/debian buster-backports main" >> /etc/apt/sources.list
# additional dependecies
# libSSL-1.0 is required by wkhtmltopdf binary
@ -9,19 +9,19 @@ RUN echo deb "http://http.debian.net/debian stretch-backports main" >> /etc/apt/
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
apt-get update -qq && \
apt-get install -y \
libjemalloc1 \
libssl1.0-dev \
libjemalloc2 \
libssl-dev \
nodejs \
yarnpkg \
groff-base \
awscli \
postgresql-client \
netcat \
default-jre-headless \
sudo graphviz --no-install-recommends \
poppler-utils \
sudo graphviz --no-install-recommends \
libfile-mimeinfo-perl && \
apt-get install -y --no-install-recommends -t stretch-backports libreoffice && \
npm install -g yarn && \
apt-get install -y --no-install-recommends -t buster-backports libreoffice && \
rm -rf /var/lib/apt/lists/*
ENV RAILS_ENV production

View file

@ -40,6 +40,7 @@
//= require perfect-scrollbar.min
//= require select2_customization
//= require shared/inline_editing
//= require activestorage
//= require turbolinks

View file

@ -1,3 +1,8 @@
/*
global Results ActiveStorage animateSpinner Comments ResultAssets FilePreviewModal
TinyMCE getParam applyCreateWopiFileCallback initFormSubmitLinks textValidator
*/
(function(global) {
'use strict';
@ -5,38 +10,9 @@
var ResultTypeEnum = Object.freeze({
FILE: 0,
TABLE: 1,
TEXT: 2,
COMMENT: 3
TEXT: 2
});
function init() {
initHandsOnTables($(document));
_expandAllResults();
applyCollapseLinkCallBack();
applyCreateWopiFileCallback();
$(function () {
$('#results-collapse-btn').click(function () {
$('.result .panel-collapse').collapse('hide');
$(document).find('span.collapse-result-icon').each(function() {
$(this).addClass('fa-caret-square-down');
$(this).removeClass('fa-caret-square-up');
});
});
$('#results-expand-btn').click(_expandAllResults);
});
// This checks if the ctarget param exist in the rendered url and opens the
// comment tab
if( getParam('ctarget') ){
var target = '#'+ getParam('ctarget');
$(target).find('a.comment-tab-link').click();
}
}
function initHandsOnTables(root) {
root.find('div.hot-table').each(function() {
var $container = $(this).find('.step-result-hot-table');
@ -50,19 +26,19 @@
colHeaders: true,
fillHandle: false,
formulas: true,
cells: function (row, col, prop) {
cells: function(row, col) {
var cellProperties = {};
if (col >= 0)
if (col >= 0) {
cellProperties.readOnly = true;
else
} else {
cellProperties.readOnly = false;
}
return cellProperties;
}
});
var hot = $container.handsontable('getInstance');
var data = JSON.parse(contents.attr('value'));
let hot = $container.handsontable('getInstance');
let data = JSON.parse(contents.attr('value'));
hot.loadData(data.data);
});
}
@ -80,7 +56,7 @@
// Toggle editing buttons
function toggleResultEditButtons(show) {
if(show) {
if (show) {
$('#results-toolbar').show();
$('.edit-result-button').show();
} else {
@ -89,15 +65,23 @@
}
}
function renderTable(table) {
$(table).handsontable('render');
// Yet another dirty hack to solve HandsOnTable problems
if (parseInt($(table).css('height'), 10) < parseInt($(table).css('max-height'), 10) - 30) {
$(table).find('.ht_master .wtHolder').css({ height: '100%', width: '100%' });
}
}
// Expand all results
function _expandAllResults() {
function expandAllResults() {
$('.result .panel-collapse').collapse('show');
$(document).find('span.collapse-result-icon').each(function() {
$(this).addClass('fa-caret-square-up');
$(this).removeClass('fa-caret-square-down');
});
$(document).find('div.step-result-hot-table').each(function() {
_renderTable(this);
renderTable(this);
});
}
@ -107,98 +91,86 @@
$(this).addClass('fa-caret-square-up');
$(this).removeClass('fa-caret-square-down');
});
_renderTable($(result).find('div.step-result-hot-table'));
renderTable($(result).find('div.step-result-hot-table'));
animateSpinner(null, false);
}
function _renderTable(table) {
$(table).handsontable('render');
// Yet another dirty hack to solve HandsOnTable problems
if (parseInt($(table).css('height'), 10) <
parseInt($(table).css('max-height'), 10) - 30) {
$(table).find('.ht_master .wtHolder').css({ 'height': '100%',
'width': '100%' });
}
}
function processResult(ev, resultTypeEnum, editMode) {
var $form = $(ev.target.form);
$form.clearFormErrors();
switch(resultTypeEnum) {
case ResultTypeEnum.FILE:
_handleResultFileSubmit($form, ev);
break;
case ResultTypeEnum.TABLE:
var $nameInput = $form.find('#result_name');
var nameValid = textValidator(ev, $nameInput, 0,
<%= Constants::NAME_MAX_LENGTH %>);
break;
case ResultTypeEnum.TEXT:
var $nameInput = $form.find('#result_name');
var nameValid = textValidator(ev, $nameInput, 0,
<%= Constants::NAME_MAX_LENGTH %>);
var $descrTextarea = $form.find("#result_text_attributes_textarea");
var $tinyMCEInput = TinyMCE.getContent();
textValidator(ev, $descrTextarea, 1, <%= Constants::RICH_TEXT_MAX_LENGTH %>, false, $tinyMCEInput);
break;
case ResultTypeEnum.COMMENT:
var $commentInput = $form.find('#comment_message');
var commentValid = textValidator(ev, $commentInput, 1,
<%= Constants::TEXT_MAX_LENGTH %>);
break;
}
}
// create custom ajax request in order to fix issuses with remote: true from
function _handleResultFileSubmit(form, ev) {
function handleResultFileSubmit(form, ev) {
ev.preventDefault();
ev.stopPropagation();
const url = form.find('#result_asset_attributes_file').data('directUploadUrl');
const file = form.find('#result_asset_attributes_file')[0].files[0];
const upload = new ActiveStorage.DirectUpload(file, url);
animateSpinner();
var data = new FormData();
var file = document.getElementById('result_asset_attributes_file')
.files[0];
data.append('result[name]', form.find('#result_name').val());
data.append('result[asset_attributes][id]',
form.find('#result_asset_attributes_id').val());
if( file ) {
data.append('result[asset_attributes][file]', file);
}
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
let formData = new FormData();
formData.append('result[name]', form.find('#result_name').val());
formData.append('result[asset_attributes][id]', form.find('#result_asset_attributes_id').val());
formData.append('result[asset_attributes][signed_blob_id]', blob.signed_id);
$.ajax({
type: 'PUT',
url: form.attr('action'),
data: data,
data: formData,
success: function(data) {
animateSpinner(null, false);
$('.edit_result').parent().remove();
$(data.html).prependTo('#results').promise().done(function() {
$.each($('#results').find('.result'),
function() {
$(data.html).prependTo('#results').promise().done(() => {
$.each($('#results').find('.result'), function() {
initFormSubmitLinks($(this));
ResutlAssets.applyEditResultAssetCallback();
ResultAssets.applyEditResultAssetCallback();
applyCollapseLinkCallBack();
applyCreateWopiFileCallback();
toggleResultEditButtons(true);
FilePreviewModal.init();
Comments.init();
ResutlAssets.initNewResultAsset();
ResultAssets.initNewResultAsset();
expandResult($(this));
});
});
$('#results-toolbar').show();
},
error: function(XHR) {
animateSpinner(null, false)
$('.edit_result').renderFormErrors('result',
XHR.responseJSON['errors']);
animateSpinner(null, false);
$('.edit_result').renderFormErrors('result', XHR.responseJSON.errors);
},
processData: false,
contentType: false,
contentType: false
});
}
});
}
function processResult(ev, resultTypeEnum) {
var $form = $(ev.target.form);
$form.clearFormErrors();
switch (resultTypeEnum) {
case ResultTypeEnum.FILE:
handleResultFileSubmit($form, ev);
break;
case ResultTypeEnum.TABLE:
textValidator(ev, $form.find('#result_name'), 0, $form.data('name-max-length'));
break;
case ResultTypeEnum.TEXT:
textValidator(ev, $form.find('#result_name'), 0, $form.data('name-max-length'));
textValidator(
ev, $form.find('#result_text_attributes_textarea'), 1,
$form.data('rich-text-max-length'), false, TinyMCE.getContent()
);
break;
default:
// do nothing
}
}
// init cancel button
function initCancelFormButton(form, callback) {
@ -217,14 +189,40 @@
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
var el = $(element);
if(confirm(el.data('confirm-text'))) {
let el = $(element);
if (confirm(el.data('confirm-text'))) {
animateSpinner();
$('#' + el.data('form-id')).submit();
}
}
var publicAPI = Object.freeze({
function init() {
initHandsOnTables($(document));
expandAllResults();
applyCollapseLinkCallBack();
applyCreateWopiFileCallback();
$(function() {
$('#results-collapse-btn').click(function() {
$('.result .panel-collapse').collapse('hide');
$(document).find('span.collapse-result-icon').each(function() {
$(this).addClass('fa-caret-square-down');
$(this).removeClass('fa-caret-square-up');
});
});
$('#results-expand-btn').click(expandAllResults);
});
// This checks if the ctarget param exist in the rendered url and opens the
// comment tab
if (getParam('ctarget')) {
let target = '#' + getParam('ctarget');
$(target).find('a.comment-tab-link').click();
}
}
let publicAPI = Object.freeze({
init: init,
initHandsOnTables: initHandsOnTables,
applyCollapseLinkCallBack: applyCollapseLinkCallBack,
@ -237,7 +235,7 @@
});
return publicAPI;
})();
}());
Results.init();
})(window);
}(window));

View file

@ -564,10 +564,10 @@
tableNamesValid ) {
$form.find("[data-role='editable-table']").each(function() {
var hot = $(this).find(".hot").handsontable('getInstance');
var contents = $(this).find('.hot-contents');
var data = JSON.stringify({data: hot.getData()});
contents.attr("value", data);
let hot = $(this).find(".hot").handsontable('getInstance');
let contents = $(this).find('.hot-contents');
let tableData = JSON.stringify({tableData: hot.getData()});
contents.attr("value", tableData);
});
setTimeout(function() {
@ -576,12 +576,12 @@
}, 1000);
animateSpinner(null, true);
var data = DragNDropSteps.appendFilesToForm(ev);
data.append('step[description]', TinyMCE.getContent());
DragNDropSteps.appendFilesToForm(ev).then(formData => {
// formData.append('step[description]', TinyMCE.getContent());
$.ajax({
url: $form.attr('action'),
method: 'POST',
data: data,
data: formData,
contentType: false,
processData: false,
beforeSend: function() {
@ -625,6 +625,7 @@
}
});
newStepHandler();
});
}
}

View file

@ -1,3 +1,5 @@
/* global Promise _ ActiveStorage RepositoryItemEditForm */
//= require sugar.min
//= require jquerymy-1.2.14.min
@ -116,55 +118,78 @@
var formData = this.formData;
var formDataObj = new FormData();
var removeFileColumns = [];
var filesToUploadCntr = 0;
var filesUploadedCntr = 0;
const directUploadUrl = $(tableID).data('directUploadUrl');
formDataObj.append('request_url', $(tableID).data('current-uri'));
formDataObj.append('repository_row_id', $(selectedRecord).attr('id'));
return new Promise((resolve, reject) => {
$(_.keys(this.formData)).each(function(_, element) {
var value = formData[element];
if (element === "rowName") {
if (element === 'rowName') {
formDataObj.append('repository_row_name', value);
} else {
var colId = element.replace('colId-', '');
var $el = $('#' + element);
let colId = element.replace('colId-', '');
let $el = $('#' + element);
// don't save anything if element is not visible
if($el.length == 0) {
return true;
if ($el.length === 0) {
return;
}
if($el.attr('type') === 'file') {
if ($el.attr('type') === 'file') {
// handle deleting of element
if($el.attr('remove') === "true") {
if ($el.attr('remove') === 'true') {
removeFileColumns.push(colId);
formDataObj.append('repository_cells[' + colId + ']', null);
} else {
formDataObj.append('repository_cells[' + colId + ']',
getFileValue($el));
} else if ($el[0].files.length > 0) {
filesToUploadCntr += 1;
}
} else if(value.length >= 0) {
} else if (value.length >= 0) {
formDataObj.append('repository_cells[' + colId + ']', value);
}
}
});
formDataObj.append('remove_file_columns', JSON.stringify(removeFileColumns));
return formDataObj;
// No files for upload, so return earlier
if (filesToUploadCntr === 0) {
resolve(formDataObj);
return;
}
// Second run, just for files
$(_.keys(this.formData)).each(function(_, element) {
let $el = $('#' + element);
let colId = element.replace('colId-', '');
if ($el.attr('type') === 'file' && $el.attr('remove') !== 'true') {
let upload = new ActiveStorage.DirectUpload($el[0].files[0], directUploadUrl);
upload.create(function(error, blob) {
if (error) {
reject(error);
} else {
formDataObj.append('repository_cells[' + colId + ']', blob.signed_id);
filesUploadedCntr += 1;
if (filesUploadedCntr === filesToUploadCntr) {
resolve(formDataObj);
}
}
});
}
});
});
};
/**
* |-----------------|
* | Private methods |
* |-----------------|
*/
/**
* Resolves the file cell on FormData creation
*
* @param {Object} elementId
*
* @returns (String | Object)
*/
function getFileValue(element) {
var file = element[0].files[0];
return (file) ? file : '';
}
/**
* Takes object and surrounds it with input
*
@ -176,8 +201,8 @@
* @returns (String)
*/
function changeToInputField(object, name, value, id) {
return "<div class='form-group'><input class='form-control' data-object='" +
object + "' name='" + name + "' value='" + value + "' id='" + id + "'></input></div>";
return "<div class='form-group'><input class='form-control' data-object='"
+ object + "' name='" + name + "' value='" + value + "' id='" + id + "'></input></div>";
}
/**
@ -368,4 +393,4 @@
formData[generateInputFieldReference(columnId)] = undefined;
}
}
})(window);
}(window));

View file

@ -695,65 +695,13 @@ var RepositoryDatatable = (function(global) {
});
}
// Save record
global.onClickSave = function() {
var node;
var rowData;
var formData;
if (saveAction === 'update') {
var row = TABLE.row(selectedRecord);
node = row.node();
rowData = row.data();
formData = SCINOTE_REPOSITORY_EDITED_ROWS[0].parseToFormObject(
TABLE_ID, selectedRecord
);
} else if (saveAction === 'create') {
node = selectedRecord;
// First fetch all the data in input fields
formData = new FormData();
formData.append('request_url', $(TABLE_ID).data('current-uri'));
formData.append('repository_row_id', $(selectedRecord).attr('id'));
// Direct record attributes
// Record name
formData.append('repository_row_name', $('td input[data-object = repository_row]').val());
// Custom cells text type
$(node).find('td input[data-object = repository_cell]').each(function() {
// Send data only and only if cell is not empty
if ($(this).val().trim()) {
formData.append('repository_cells[' + $(this).attr('name') + ']', $(this).val());
}
});
// Custom cells file type
$(node).find('td input[data-object = repository_cell_file]').each(function() {
// Send data only and only if cell is not empty
if ($(this).context.files.length == 1 ) {
if ($(this).data('changed')) {
formData.append('repository_cells[' + $(this).attr('name') + ']',
$(this).context.files[0]);
} else {
formData.append('repository_cells[' + $(this).attr('name') + ']', '');
}
}
});
// Custom cells list type
$(node).find('td[column_id]').each(function(index, el) {
var value = $(el).attr('list_item_id');
formData.append('repository_cells[' + $(el).attr('column_id') + ']', value);
});
}
function submitForm(url, formData) {
var url;
var type;
if (saveAction === 'update') {
url = rowData.recordUpdateUrl;
type = 'PUT';
} else {
type = 'POST';
url = $('table' + TABLE_ID).data('create-record');
}
$.ajax({
url: url,
@ -767,8 +715,10 @@ var RepositoryDatatable = (function(global) {
SmartAnnotation.closePopup();
SCINOTE_REPOSITORY_EDITED_ROWS = [];
onClickCancel();
animateSpinner(null, false);
},
error: function(e) {
animateSpinner(null, false);
SmartAnnotation.closePopup();
var data = e.responseJSON;
clearAllErrors();
@ -825,6 +775,92 @@ var RepositoryDatatable = (function(global) {
}
}
});
}
function buildNewFormData() {
return new Promise((resolve, reject) => {
var node = selectedRecord;
var formData = new FormData();
var filesToUploadCntr = 0;
var filesUploadedCntr = 0;
const directUploadUrl = $(TABLE_ID).data('directUploadUrl');
// First fetch all the data in input fields
formData.append('request_url', $(TABLE_ID).data('current-uri'));
formData.append('repository_row_id', $(selectedRecord).attr('id'));
// Direct record attributes
// Record name
formData.append('repository_row_name', $('td input[data-object = repository_row]').val());
// Custom cells text type
$(node).find('td input[data-object = repository_cell]').each(function() {
// Send data only and only if cell is not empty
if ($(this).val().trim()) {
formData.append('repository_cells[' + $(this).attr('name') + ']', $(this).val());
}
});
// Custom cells list type
$(node).find('td[column_id]').each(function(index, el) {
var value = $(el).attr('list_item_id');
formData.append('repository_cells[' + $(el).attr('column_id') + ']', value);
});
// Custom cells file type, first run, count files ready for upload
$(node).find('td input[data-object = repository_cell_file]').each(function() {
// Send data only and only if cell is not empty
if ($(this)[0].files.length == 1 ) {
filesToUploadCntr += 1;
}
});
// No files for upload, so return earlier
if (filesToUploadCntr === 0) {
resolve(formData);
return;
}
// Custom cells file type, second run, upload files
$(node).find('td input[data-object = repository_cell_file]').each(function() {
// Send data only and only if cell is not empty
if ($(this)[0].files.length == 1 ) {
let upload = new ActiveStorage.DirectUpload($(this)[0].files[0], directUploadUrl);
let colId = $(this).attr('name');
upload.create(function(error, blob) {
if (error) {
reject(error);
} else {
formData.append('repository_cells[' + colId + ']', blob.signed_id);
filesUploadedCntr += 1;
if (filesUploadedCntr === filesToUploadCntr) {
resolve(formData);
}
}
});
}
});
});
}
// Save record
global.onClickSave = function() {
animateSpinner(null, true);
if (saveAction === 'update') {
let row = TABLE.row(selectedRecord);
let rowData = row.data();
SCINOTE_REPOSITORY_EDITED_ROWS[0].parseToFormObject(
TABLE_ID, selectedRecord
).then(formData => {
submitForm(rowData.recordUpdateUrl, formData);
});
} else if (saveAction === 'create') {
buildNewFormData().then(formData => {
submitForm($('table' + TABLE_ID).data('create-record'), formData);
});
}
};
// Delete record

View file

@ -1,7 +1,7 @@
(function(global) {
'use strict';
global.ResutlAssets = (function() {
global.ResultAssets = (function() {
// New result asset behaviour
function initNewResultAsset() {
$('#new-result-asset').on('click', function(event) {
@ -96,7 +96,7 @@
return publicAPI;
})();
ResutlAssets.initNewResultAsset();
ResutlAssets.applyEditResultAssetCallback();
ResultAssets.initNewResultAsset();
ResultAssets.applyEditResultAssetCallback();
FilePreviewModal.init();
}(window));

View file

@ -0,0 +1,630 @@
/* global Promise ActiveStorage animateSpinner copyFromClipboard I18n
Results ResultAssets FilePreviewModal Comments truncateLongString
DragNDropSteps DragNDropResults initFormSubmitLinks dragNdropAssetsInit */
(function(global) {
'use strict';
// Copy from clipboard
global.copyFromClipboard = (function() {
var UPLOADED_IMAGE = {};
var LOCATION = '';
function retrieveImageFromClipboardAsBlob(pasteEvent, callback) {
if (pasteEvent.clipboardData === false) {
if ((typeof callback) === 'function') {
callback(undefined);
}
}
let items = pasteEvent.clipboardData.items;
if (items === undefined) {
if ((typeof callback) === 'function') {
callback(undefined);
}
}
for (let i = 0; i < items.length; i += 1) {
if (items[i].type.indexOf('image') !== -1) {
let blob = items[i].getAsFile();
if ((typeof callback) === 'function') {
callback(blob);
}
}
}
}
// $(..).modal('hide') don't work properly so here we manually remove the
// displayed modal
function hideModalForGood() {
$('#clipboardPreviewModal').removeClass('in');
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
$('body').css('padding-right', '');
$('#clipboardPreviewModal').hide();
}
function closeModal() {
hideModalForGood();
$('#clipboardPreviewModal').remove();
}
function addImageCallback() {
$('[data-action="addImageFormClipboard"]').on('click', function() {
let inputArray = [];
let newName = $('#clipboardImageName').val();
// check if the name is set
if (newName && newName.length > 0) {
let extension = UPLOADED_IMAGE.name.slice(
(Math.max(0, UPLOADED_IMAGE.name.lastIndexOf('.')) || Infinity) + 1
);
// hack to inject custom name in File object
let name = newName + '.' + extension;
let blob = UPLOADED_IMAGE.slice(0, UPLOADED_IMAGE.size, UPLOADED_IMAGE.type);
// make new blob with the correct name;
let newFile = new File([blob], name, { type: UPLOADED_IMAGE.type });
inputArray.push(newFile);
} else { // return the default name
inputArray.push(UPLOADED_IMAGE);
}
// close modal
closeModal();
// reuse file upload from drag'n drop :)
if (LOCATION === 'steps') {
DragNDropSteps.init(inputArray);
} else {
DragNDropResults.init(inputArray);
}
// clear all uploaded images
UPLOADED_IMAGE = {};
});
}
// removes modal from dom
function destroyModalCallback() {
let modal = $('#clipboardPreviewModal');
modal.on('hidden.bs.modal', function() {
modal.modal('hide').promise().done(function() {
modal.remove();
});
UPLOADED_IMAGE = {};
});
}
// Generate modal html and hook callbacks
function clipboardPasteModal() {
var html = '<div id="clipboardPreviewModal" class="modal fade" ';
html += 'tabindex="-1" role="dialog" aria-hidden="true">';
html += '<div class="modal-dialog" role="document">';
html += '<div class="modal-content"><div class="modal-header">';
html += '<button type="button" class="close" data-dismiss="modal"';
html += ' aria-label="Close"><span aria-hidden="true">&times;</span>';
html += '</button><h4 class="modal-title">' + I18n.t('assets.from_clipboard.modal_title') + '</h4>';
html += '</div><div class="modal-body"><p><strong>' + I18n.t('assets.from_clipboard.image_preview') + '</strong></p>';
html += '<canvas style="border:1px solid grey;max-width:400px;max-height:300px" id="clipboardPreview" />';
html += '<p><strong>' + I18n.t('assets.from_clipboard.file_name') + '</strong></p>';
html += '<div class="input-group">';
html += '<input id="clipboardImageName" type="text" class="form-control" ';
html += 'placeholder="' + I18n.t('assets.from_clipboard.file_name_placeholder') + '" aria-describedby="image-name">';
html += '<span class="input-group-addon" id="image-name"></span></div>';
html += '</div><div class="modal-footer">';
html += '<button type="button" class="btn btn-default" data-dismiss="modal">' + I18n.t('general.cancel') + '</button>';
html += '<button type="button" class="btn btn-success" data-action="addImageFormClipboard">' + I18n.t('assets.from_clipboard.add_image') + '</button>';
html += '</div></div></div></div><!-- /.modal -->';
return $(html).appendTo($('body')).promise().done(function() {
// display modal
$('#clipboardPreviewModal').modal('show');
// add callback to remove modal from DOM
destroyModalCallback();
// add callback on image submit
addImageCallback();
});
}
function listener(pasteEvent) {
retrieveImageFromClipboardAsBlob(pasteEvent, function(imageBlob) {
if (imageBlob) {
clipboardPasteModal().promise().done(function() {
var canvas = document.getElementById('clipboardPreview');
var ctx = canvas.getContext('2d');
var img = new Image();
img.onload = function() {
canvas.width = this.width;
canvas.height = this.height;
ctx.drawImage(img, 0, 0);
};
let URLObj = window.URL || window.webkitURL;
img.src = URLObj.createObjectURL(imageBlob);
let extension = imageBlob.name.slice(
(Math.max(0, imageBlob.name.lastIndexOf('.')) || Infinity) + 1
);
$('#image-name').html('.' + extension); // add extension near file name
// temporary store image blob
UPLOADED_IMAGE = imageBlob;
});
}
});
}
function init(location) {
LOCATION = location;
global.addEventListener('paste', listener, false);
$.initTooltips();
}
function destroy() {
global.removeEventListener('paste', listener, false);
}
return Object.freeze({
init: init,
destroy: destroy
});
}());
// Module to handle file uploading in Steps
global.DragNDropSteps = (function() {
var droppedFiles = [];
var filesValid = true;
var totalSize = 0;
var fileMaxSizeMb;
var fileMaxSize;
var uploadedFilesCounter = 0;
// return the status of files if they are ready to submit
function filesStatus() {
return filesValid;
}
function clearFiles() {
droppedFiles = [];
}
function incrementUploadedFilesCounter() {
uploadedFilesCounter += 1;
}
function getUploadedFilesCounter() {
return uploadedFilesCounter;
}
function dragNdropAssetsOff() {
$('body').off('drag dragstart dragend dragover dragenter dragleave drop');
$('.is-dragover').hide();
// remove listeners for clipboard images
copyFromClipboard.destroy();
}
// append the files to the form before submit
function appendFilesToForm(ev) {
return new Promise((resolve, reject) => {
const form = $(ev.target).closest('form').get(0);
const url = $(form).find('#drag-n-drop-assets').data('directUploadUrl');
const regex = /step\[assets_attributes\]\[[0-9]*\]\[id\]/;
const numberOfFiles = droppedFiles.length;
let prevEls = $('input').filter(function() {
return this.name.match(regex);
});
let fd = new FormData(form);
uploadedFilesCounter = 0;
fd.delete('step[file][]');
if (droppedFiles.length === 0) {
resolve(fd);
return;
}
for (let i = 0; i < droppedFiles.length; i += 1) {
let upload = new ActiveStorage.DirectUpload(droppedFiles[i], url);
let index = i + prevEls.length;
upload.create(function(error, blob) {
if (error) {
reject(error);
} else {
fd.append('step[assets_attributes][' + index + '][signed_blob_id]', blob.signed_id);
incrementUploadedFilesCounter();
if (getUploadedFilesCounter() === numberOfFiles) {
resolve(fd);
}
}
});
}
filesValid = true;
totalSize = 0;
dragNdropAssetsOff();
});
}
function disableSubmitButton() {
$('.step-save').prop('disabled', true);
}
function enableSubmitButton() {
$('.step-save').prop('disabled', false);
}
function filerAndCheckFiles() {
for (let i = 0; i < droppedFiles.length; i += 1) {
if (droppedFiles[i].isValid === false) {
return false;
}
}
return (droppedFiles.length > 0);
}
function validateFilesSize(file) {
var fileSize = file.size;
totalSize += parseInt(fileSize, 10);
if (fileSize > fileMaxSize) {
file.isValid = false;
disableSubmitButton();
return "<p class='dnd-error'>" + I18n.t('general.file.size_exceeded', { file_size: fileMaxSizeMb }) + '</p>';
}
return '';
}
function validateTotalSize() {
if (totalSize > fileMaxSize) {
filesValid = false;
disableSubmitButton();
$.each($('.panel-step-attachment-new'), function() {
if (!$(this).find('p').hasClass('dnd-total-error')) {
$(this)
.find('.panel-body')
.append("<p class='dnd-total-error'>" + I18n.t('general.file.total_size', { size: fileMaxSizeMb }) + '</p>');
}
});
} else {
$('.dnd-total-error').remove();
if (filerAndCheckFiles()) {
filesValid = true;
enableSubmitButton();
}
}
}
function uploadedAssetPreview(asset, i) {
var html = '<div class="attachment-placeholder pull-left new">';
html += '<div class="attachment-thumbnail no-shadow new %>">';
html += '<i class="fas fa-image"></i>';
html += '</div>';
html += '<div class="attachment-label">';
html += truncateLongString(asset.name, $(document.body).data('filename-max-length'));
html += '</div>';
html += '<div class="spencer-bonnet-modif">';
html += '</div>';
html += '<div class="remove-icon pull-right">';
html += '<a data-item-id="' + i + '" href="#">';
html += '<span class="fas fa-trash"></span>';
html += '</a> </div>';
html += validateFilesSize(asset);
html += '</div>';
return html;
}
function removeItemHandler(id, callback) {
$('[data-item-id="' + id + '"]').off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
let $el = $(this);
let index = $el.data('item-id');
totalSize -= parseInt(droppedFiles[index].size, 10);
droppedFiles.splice(index, 1);
callback();
});
}
// loops through a list of files and display each file in a separate panel
function listItems() {
totalSize = 0;
enableSubmitButton();
$('.attachment-placeholder.new').remove();
dragNdropAssetsOff();
for (let i = 0; i < droppedFiles.length; i += 1) {
$('.attachments.edit')
.append(uploadedAssetPreview(droppedFiles[i], i))
.promise()
.done(function() {
removeItemHandler(i, listItems);
});
}
validateTotalSize();
dragNdropAssetsInit('steps');
}
function init(files) {
fileMaxSizeMb = $(document.body).data('file-max-size-mb');
fileMaxSize = fileMaxSizeMb * 1024 * 1024;
for (let i = 0; i < files.length; i += 1) {
droppedFiles.push(files[i]);
}
listItems();
}
return Object.freeze({
init: init,
appendFilesToForm: appendFilesToForm,
listItems: listItems,
filesStatus: filesStatus,
clearFiles: clearFiles
});
}());
// Module to handle file uploading in Results
global.DragNDropResults = (function() {
var droppedFiles = [];
var isValid = true;
var totalSize = 0;
var fileMaxSizeMb;
var fileMaxSize;
function disableSubmitButton() {
$('.save-result').prop('disabled', true);
}
function enableSubmitButton() {
$('.save-result').prop('disabled', false);
}
function filerAndCheckFiles() {
for (let i = 0; i < droppedFiles.length; i += 1) {
if (droppedFiles[i].isValid === false) {
return false;
}
}
return (droppedFiles.length > 0);
}
function dragNdropAssetsOff() {
$('body').off('drag dragstart dragend dragover dragenter dragleave drop');
$('.is-dragover').hide();
}
function destroyAll() {
dragNdropAssetsOff();
droppedFiles = [];
isValid = true;
totalSize = 0;
}
// return the status of files if they are ready to submit
function filesStatus() {
return isValid;
}
function validateTotalSize() {
if (totalSize > fileMaxSize) {
isValid = false;
disableSubmitButton();
$.each($('.panel-result-attachment-new'), function() {
if (!$(this).find('p').hasClass('dnd-total-error')) {
$(this)
.find('.panel-body')
.append("<p class='dnd-total-error'>" + I18n.t('general.file.total_size', { size: fileMaxSizeMb }) + '</p>');
}
});
} else {
$('.dnd-total-error').remove();
if (filerAndCheckFiles()) {
isValid = true;
enableSubmitButton();
}
}
}
function submitResultForm(url, formData) {
$.ajax({
url: url,
method: 'POST',
data: formData,
contentType: false,
processData: false,
success: function(data) {
animateSpinner(null, false);
$('#new-result-assets-select').parent().remove();
$(data.html).prependTo('#results').promise().done(function() {
$.each($('[data-container="new-reports"]').find('.result'), function() {
initFormSubmitLinks($(this));
ResultAssets.applyEditResultAssetCallback();
Results.applyCollapseLinkCallBack();
Results.toggleResultEditButtons(true);
FilePreviewModal.init();
Comments.init();
ResultAssets.initNewResultAsset();
Results.expandResult($(this));
});
});
$('#results-toolbar').show();
},
error: function() {
animateSpinner();
location.reload();
}
});
}
// appent the files to the form before submit
function appendFilesToForm(ev, fd) {
const form = $(ev.target.form);
const url = form.find('#drag-n-drop-assets').data('directUploadUrl');
const numberOfFiles = droppedFiles.length;
let resultNames = [];
$.each($('input[rel="results[name]"]'), function() {
resultNames.push($(this).val());
});
resultNames.reverse();
for (let i = 0; i < numberOfFiles; i += 1) {
let upload = new ActiveStorage.DirectUpload(droppedFiles[i], url);
upload.create(function(error, blob) {
if (error) {
// Handle the error
} else {
fd.append('results_names[' + i + ']', resultNames[i]);
fd.append('results_files[' + i + '][signed_blob_id]', blob.signed_id);
if ((i + 1) === numberOfFiles) {
submitResultForm($(ev.target).attr('data-href'), fd);
destroyAll();
}
}
});
}
}
/* eslint no-param-reassign: ["error", { "props": false }] */
function validateFilesSize(file) {
var fileSize = file.size;
totalSize += parseInt(fileSize, 10);
if (fileSize > fileMaxSize) {
file.isValid = false;
disableSubmitButton();
return "<p class='dnd-error'>" + I18n.t('general.file.size_exceeded', { file_size: fileMaxSizeMb }) + '</p>';
}
return '';
}
function validateTextSize(input) {
if (input.value.length > $(document.body).data('name-max-length')) {
$(input).parent().find('.dnd-error').remove();
$(input).after("<p class='dnd-error'>" + I18n.t('general.text.length_too_long', { max_length: $(document.body).data('name-max-length') }) + '</p>');
isValid = false;
} else {
$(input).parent().find('.dnd-error').remove();
isValid = true;
}
}
function uploadedAssetPreview(asset, i) {
var html = '<div class="panel panel-default panel-result-attachment-new">';
html += '<div class="panel-heading">';
html += '<span class="fas fa-paperclip"></span>';
html += I18n.t('assets.drag_n_drop.file_label');
html += '<div class="pull-right">';
html += '<a data-item-id="' + i + '" href="#">';
html += '<span class="fas fa-times"></span></a></div></div>';
html += '<div class="panel-body"><div class="form-group">';
html += '<label class="control-label">Name</label>';
html += '<input type="text" class="form-control" ';
html += 'onChange="DragNDropResults.validateTextSize(this)"';
html += ' rel="results[name]" name="results[name][' + i + ']">';
html += '</div><div class="form-group"><label class="control-label">';
html += I18n.t('assets.drag_n_drop.file_label') + ':</label> ';
html += truncateLongString(asset.name, $(document.body).data('filename-max-length'));
html += validateFilesSize(asset);
html += '</div></div>';
return html;
}
function processResult(ev) {
ev.preventDefault();
ev.stopPropagation();
if (isValid && filerAndCheckFiles()) {
animateSpinner();
let formData = new FormData();
appendFilesToForm(ev, formData);
}
}
function removeItemHandler(id, callback) {
$('[data-item-id="' + id + '"]').off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
let $el = $(this);
let index = $el.data('item-id');
totalSize -= parseInt(droppedFiles[index].size, 10);
droppedFiles.splice(index, 1);
callback();
});
}
// loops through a list of files and display each file in a separate panel
function listItems() {
totalSize = 0;
$('.panel-result-attachment-new').remove();
if (droppedFiles.length < 1) {
disableSubmitButton();
} else {
dragNdropAssetsOff();
for (let i = 0; i < droppedFiles.length; i += 1) {
$('#new-result-assets-select')
.after(uploadedAssetPreview(droppedFiles[i], i))
.promise()
.done(function() {
removeItemHandler(i, listItems);
});
}
validateTotalSize();
dragNdropAssetsInit('results');
}
}
function init(files) {
fileMaxSizeMb = $(document.body).data('file-max-size-mb');
fileMaxSize = fileMaxSizeMb * 1024 * 1024;
for (let i = 0; i < files.length; i += 1) {
droppedFiles.push(files[i]);
}
listItems();
}
return Object.freeze({
init: init,
listItems: listItems,
destroyAll: destroyAll,
filesStatus: filesStatus,
validateTextSize: validateTextSize,
processResult: processResult
});
}());
global.dragNdropAssetsInit = function(location) {
var inWindow = true;
$('body')
.on('drag dragstart dragend dragover dragenter dragleave drop', (e) => {
e.preventDefault();
e.stopPropagation();
})
.on('dragover', function() {
inWindow = true;
$('.is-dragover').show();
})
.on('dragleave', function() {
inWindow = false;
setTimeout(function() {
if (!inWindow) {
$('.is-dragover').hide();
}
}, 5000);
})
.on('drop', function(e) {
$('.is-dragover').hide();
let files = e.originalEvent.dataTransfer.files;
if (location === 'steps') {
DragNDropSteps.init(files);
} else {
DragNDropResults.init(files);
}
});
copyFromClipboard.init(location);
};
}(window));

View file

@ -1,568 +0,0 @@
(function(global) {
'use strict';
// Copy from clipboard
global.copyFromClipboard = (function() {
var UPLOADED_IMAGE = {};
var LOCATION = '';
function init(location) {
LOCATION = location;
global.addEventListener('paste', _listener, false);
$.initTooltips();
};
function destroy() {
global.removeEventListener('paste', _listener, false);
}
function _listener(pasteEvent) {
_retrieveImageFromClipboardAsBlob(pasteEvent, function(imageBlob) {
if(imageBlob){
_clipboardPasteModal().promise().done(function() {
var canvas = document.getElementById('clipboardPreview');
var ctx = canvas.getContext('2d');
var img = new Image();
img.onload = function() {
canvas.width = this.width;
canvas.height = this.height;
ctx.drawImage(img, 0, 0);
};
var URLObj = window.URL || window.webkitURL;
img.src = URLObj.createObjectURL(imageBlob);
var extension = imageBlob.name.slice(
(Math.max(0, imageBlob.name.lastIndexOf(".")) || Infinity) + 1
);
$('#image-name').html('.' + extension); // add extension near file name
// temporary store image blob
UPLOADED_IMAGE = imageBlob
});
}
});
}
function _retrieveImageFromClipboardAsBlob(pasteEvent, callback){
if(pasteEvent.clipboardData == false) {
if(typeof(callback) == "function"){
callback(undefined);
}
};
var items = pasteEvent.clipboardData.items;
if(items == undefined){
if(typeof(callback) == "function"){
callback(undefined);
}
};
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") == -1) continue;
var blob = items[i].getAsFile();
if(typeof(callback) == "function") {
callback(blob);
}
}
};
// removes modal from dom
function _destroyModalCallback() {
var modal = $('#clipboardPreviewModal');
modal.on('hidden.bs.modal', function() {
modal.modal('hide').promise().done(function() {
modal.remove();
});
UPLOADED_IMAGE = {};
});
}
function _closeModal() {
_hideModalForGood();
$("#clipboardPreviewModal").remove();
}
// $(..).modal('hide') don't work properly so here we manually remove the
// displayed modal
function _hideModalForGood(){
$("#clipboardPreviewModal").removeClass("in");
$(".modal-backdrop").remove();
$('body').removeClass('modal-open');
$('body').css('padding-right', '');
$("#clipboardPreviewModal").hide();
}
function _addImageCallback() {
$('[data-action="addImageFormClipboard"]').on('click', function() {
var inputArray = [];
var newName = $('#clipboardImageName').val();
// check if the name is set
if( newName && newName.length > 0 ) {
var extension = UPLOADED_IMAGE.name.slice(
(Math.max(0, UPLOADED_IMAGE.name.lastIndexOf(".")) || Infinity) + 1
);
// hack to inject custom name in File object
var name = newName + '.' + extension;
var blob = UPLOADED_IMAGE.slice(0, UPLOADED_IMAGE.size, UPLOADED_IMAGE.type);
// make new blob with the correct name;
var newFile = new File([blob], name, { type: UPLOADED_IMAGE.type });
inputArray.push(newFile);
} else { // return the default name
inputArray.push(UPLOADED_IMAGE);
}
// close modal
_closeModal();
// reuse file upload from drag'n drop :)
if(LOCATION === 'steps') {
DragNDropSteps.init(inputArray);
} else {
DragNDropResults.init(inputArray);
}
// clear all uploaded images
UPLOADED_IMAGE = {};
});
}
// Generate modal html and hook callbacks
function _clipboardPasteModal() {
var html = '<div id="clipboardPreviewModal" class="modal fade" ';
html += 'tabindex="-1" role="dialog" aria-hidden="true">';
html += '<div class="modal-dialog" role="document">';
html += '<div class="modal-content"><div class="modal-header">';
html += '<button type="button" class="close" data-dismiss="modal"';
html += ' aria-label="Close"><span aria-hidden="true">&times;</span>';
html += '</button><h4 class="modal-title"><%= I18n.t('assets.from_clipboard.modal_title') %></h4>';
html += '</div><div class="modal-body"><p><strong><%= I18n.t('assets.from_clipboard.image_preview') %></strong></p>';
html += '<canvas style="border:1px solid grey;max-width:400px;max-height:300px" id="clipboardPreview" />';
html += '<p><strong><%= I18n.t('assets.from_clipboard.file_name') %></strong></p>';
html += '<div class="input-group">';
html += '<input id="clipboardImageName" type="text" class="form-control" ';
html += 'placeholder="<%= I18n.t('assets.from_clipboard.file_name_placeholder') %>" aria-describedby="image-name">';
html += '<span class="input-group-addon" id="image-name"></span></div>';
html += '</div><div class="modal-footer">';
html += '<button type="button" class="btn btn-default" data-dismiss="modal"><%= I18n.t('general.cancel') %></button>';
html += '<button type="button" class="btn btn-success" data-action="addImageFormClipboard"><%= I18n.t('assets.from_clipboard.add_image') %></button>';
html += '</div></div></div></div><!-- /.modal -->';
return $(html).appendTo($('body')).promise().done(function() {
// display modal
$('#clipboardPreviewModal').modal('show');
// add callback to remove modal from DOM
_destroyModalCallback();
// add callback on image submit
_addImageCallback();
});
}
return Object.freeze({
init: init,
destroy: destroy
});
})();
// Module to handle file uploading in Steps
global.DragNDropSteps = (function() {
var droppedFiles = [];
var filesValid = true;
var totalSize = 0;
var fileMaxSizeMb;
var fileMaxSize;
function init(files) {
fileMaxSizeMb = $(document.body).data('file-max-size-mb');
fileMaxSize = fileMaxSizeMb * 1024 * 1024;
for(var i = 0; i < files.length; i++) {
droppedFiles.push(files[i]);
}
listItems();
}
// return the status of files if they are ready to submit
function filesStatus() {
return filesValid;
}
function clearFiles() {
droppedFiles = [];
}
// loops through a list of files and display each file in a separate panel
function listItems() {
totalSize = 0;
_enableSubmitButton();
$('.attachment-placeholder.new').remove();
_dragNdropAssetsOff();
for(var i = 0; i < droppedFiles.length; i++) {
$('.attachments.edit')
.append(_uploadedAssetPreview(droppedFiles[i], i))
.promise()
.done(function() {
_removeItemHandler(i);
});
}
_validateTotalSize();
dragNdropAssetsInit('steps');
}
// append the files to the form before submit
function appendFilesToForm(ev) {
var regex = /step\[assets_attributes\]\[[0-9]*\]\[id\]/;
var prevEls = $('input').filter(function() {
return this.name.match(regex);
});
var fd = new FormData($(ev.target).closest('form').get(0));
for(var i = 0; i < droppedFiles.length; i++) {
var index = i + prevEls.length;
var name = 'step[assets_attributes][' + index + '][file]';
fd.append(name, droppedFiles[i]);
}
filesValid = true;
totalSize = 0;
_dragNdropAssetsOff();
return fd;
}
function _disableSubmitButton() {
$('.step-save').prop('disabled', true);
}
function _enableSubmitButton() {
$('.step-save').prop('disabled', false);
}
function _filerAndCheckFiles() {
for(var i = 0; i < droppedFiles.length; i++) {
if(droppedFiles[i].isValid == false) {
return false;
}
}
return (droppedFiles.length > 0);
}
function _validateFilesSize(file) {
var fileSize = file.size;
totalSize += parseInt(fileSize);
if(fileSize > fileMaxSize) {
file.isValid = false;
_disableSubmitButton();
return "<p class='dnd-error'>" + I18n.t('general.file.size_exceeded', { file_size: fileMaxSizeMb }) + '</p>';
}
return '';
}
function _validateTotalSize() {
if(totalSize > fileMaxSize) {
filesValid = false;
_disableSubmitButton();
$.each($('.panel-step-attachment-new'), function() {
if(!$(this).find('p').hasClass('dnd-total-error')) {
$(this)
.find('.panel-body')
.append("<p class='dnd-total-error'>" + I18n.t('general.file.total_size', { size: fileMaxSizeMb }) + '</p>');
}
});
} else {
$('.dnd-total-error').remove();
if(_filerAndCheckFiles()) {
filesValid = true;
_enableSubmitButton();
}
}
}
function _uploadedAssetPreview(asset, i) {
var html = '<div class="attachment-placeholder pull-left new">';
html +='<div class="attachment-thumbnail no-shadow new %>">';
html +='<i class="fas fa-image"></i>';
html +='</div>';
html +='<div class="attachment-label">' + truncateLongString(asset.name, <%= Constants::FILENAME_TRUNCATION_LENGTH %>);
html += '</div>';
html +='<div class="spencer-bonnet-modif">';
html +='</div>';
html +='<div class="remove-icon pull-right">';
html +='<a data-item-id="' + i + '" href="#">';
html +='<span class="fas fa-trash"></span>';
html +='</a> </div>';
html += _validateFilesSize(asset);
html +='</div>';
return html;
}
function _removeItemHandler(id) {
$('[data-item-id="' + id +'"]').off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
var $el = $(this);
var index = $el.data('item-id');
totalSize -= parseInt(droppedFiles[index].size);
droppedFiles.splice(index, 1);
listItems();
});
}
function _dragNdropAssetsOff() {
$('body').off('drag dragstart dragend dragover dragenter dragleave drop');
$('.is-dragover').hide();
// remove listeners for clipboard images
copyFromClipboard.destroy();
}
return Object.freeze({
init: init,
appendFilesToForm: appendFilesToForm,
listItems: listItems,
filesStatus: filesStatus,
clearFiles: clearFiles
});
})();
// Module to handle file uploading in Results
global.DragNDropResults = (function() {
var droppedFiles = [];
var isValid = true;
var totalSize = 0;
var fileMaxSizeMb;
var fileMaxSize;
function init(files) {
fileMaxSizeMb = $(document.body).data('file-max-size-mb');
fileMaxSize = fileMaxSizeMb * 1024 * 1024;
for(var i = 0; i < files.length; i++) {
droppedFiles.push(files[i]);
}
listItems();
}
// return the status of files if they are ready to submit
function filesStatus() {
return isValid;
}
// loops through a list of files and display each file in a separate panel
function listItems() {
totalSize = 0;
$('.panel-result-attachment-new').remove();
if(droppedFiles.length < 1) {
_disableSubmitButton();
} else {
_dragNdropAssetsOff();
for(var i = 0; i < droppedFiles.length; i++) {
$('#new-result-assets-select')
.after(_uploadedAssetPreview(droppedFiles[i], i))
.promise()
.done(function() {
_removeItemHandler(i);
});
}
_validateTotalSize();
dragNdropAssetsInit('results');
}
}
// appent the files to the form before submit
function _appendFilesToForm() {
var regex = /result\[assets_attributes\]\[[0-9]*\]\[id\]/;
var prevEls = $('input').filter(function() {
return this.name.match(regex);
});
var fd = new FormData();
var result_names = [];
$.each($('input[rel="results[name]"]'), function() {
result_names.push($(this).val());
});
result_names.reverse();
for(var i = 0; i < droppedFiles.length; i++) {
var index = i + prevEls.length;
var file_name = 'results_files[' + index + ']';
fd.append(file_name, droppedFiles[i]);
fd.append('results_names[' + i + ']', result_names[i]);
}
destroyAll();
return fd;
}
function _disableSubmitButton() {
$('.save-result').prop('disabled', true);
}
function _enableSubmitButton() {
$('.save-result').prop('disabled', false);
}
function _filerAndCheckFiles() {
for(var i = 0; i < droppedFiles.length; i++) {
if(droppedFiles[i].isValid == false) {
return false;
}
}
return (droppedFiles.length > 0);
}
function _validateFilesSize(file) {
var fileSize = file.size;
totalSize += parseInt(fileSize);
if(fileSize > fileMaxSize) {
file.isValid = false;
_disableSubmitButton();
return "<p class='dnd-error'>" + I18n.t('general.file.size_exceeded', { file_size: fileMaxSizeMb }) + '</p>';
}
return '';
}
function _validateTotalSize() {
if(totalSize > fileMaxSize) {
isValid = false;
_disableSubmitButton();
$.each($('.panel-result-attachment-new'), function() {
if(!$(this).find('p').hasClass('dnd-total-error')) {
$(this)
.find('.panel-body')
.append("<p class='dnd-total-error'>" + I18n.t('general.file.total_size', { size: fileMaxSizeMb }) + '</p>');
}
});
} else {
$('.dnd-total-error').remove();
if(_filerAndCheckFiles()) {
isValid = true;
_enableSubmitButton();
}
}
}
function validateTextSize(input) {
if(input.value.length > <%= Constants::NAME_MAX_LENGTH %>) {
$(input).parent().find('.dnd-error').remove();
$(input).after("<p class='dnd-error'><%= I18n.t('general.text.length_too_long', max_length: Constants::NAME_MAX_LENGTH) %></p>");
isValid = false;
} else {
$(input).parent().find('.dnd-error').remove();
isValid = true;
}
}
function _uploadedAssetPreview(asset, i) {
var html = '<div class="panel panel-default panel-result-attachment-new">';
html += '<div class="panel-heading">';
html += '<span class="fas fa-paperclip"></span>';
html += '<%= I18n.t 'assets.drag_n_drop.file_label' %>';
html += '<div class="pull-right">';
html += '<a data-item-id="' + i + '" href="#">';
html += '<span class="fas fa-times"></span></a></div></div>';
html += '<div class="panel-body"><div class="form-group">';
html += '<label class="control-label">Name</label>';
html += '<input type="text" class="form-control" ';
html += 'onChange="DragNDropResults.validateTextSize(this)"';
html += ' rel="results[name]" name="results[name][' + i + ']">';
html += '</div><div class="form-group"><label class="control-label">';
html += '<%= I18n.t 'assets.drag_n_drop.file_label' %>:</label> ';
html += truncateLongString(asset.name,
<%= Constants::FILENAME_TRUNCATION_LENGTH %>);
html += _validateFilesSize(asset);
html += '</div></div>';
return html;
}
function _removeItemHandler(id) {
$('[data-item-id="' + id +'"]').off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
var $el = $(this);
var index = $el.data('item-id');
totalSize -= parseInt(droppedFiles[index].size);
droppedFiles.splice(index, 1);
listItems();
});
}
function processResult(button) {
if(isValid && _filerAndCheckFiles()) {
animateSpinner();
$.ajax({
url: $(button).attr('data-href'),
method: 'POST',
data: _appendFilesToForm(),
contentType: false,
processData: false,
success: function(data) {
animateSpinner(null, false);
$('#new-result-assets-select').parent().remove();
$(data.html).prependTo('#results').promise().done(function() {
$.each($('[data-container="new-reports"]').find('.result'),
function() {
initFormSubmitLinks($(this));
ResutlAssets.applyEditResultAssetCallback();
Results.applyCollapseLinkCallBack();
Results.toggleResultEditButtons(true);
FilePreviewModal.init();
Comments.init();
ResutlAssets.initNewResultAsset();
Results.expandResult($(this));
});
});
$('#results-toolbar').show();
},
error: function() {
animateSpinner();
location.reload();
}
})
}
}
function destroyAll() {
_dragNdropAssetsOff();
droppedFiles = [];
isValid = true;
totalSize = 0;
}
function _dragNdropAssetsOff() {
$('body').off('drag dragstart dragend dragover dragenter dragleave drop');
$('.is-dragover').hide();
}
return Object.freeze({
init: init,
listItems: listItems,
destroyAll: destroyAll,
filesStatus: filesStatus,
validateTextSize: validateTextSize,
processResult: processResult
});
})();
global.dragNdropAssetsInit = function(location) {
var in_window = true;
$('body').on('drag dragstart dragend dragover dragenter dragleave drop',
function(e) {
e.preventDefault();
e.stopPropagation();
}).on('dragover', function() {
in_window = true;
$('.is-dragover').show();
}).on('dragleave', function() {
in_window = false;
setTimeout(function() {
if(!in_window) {
$('.is-dragover').hide();
}
}, 5000);
}).on('drop', function(e) {
$('.is-dragover').hide();
var files = e.originalEvent.dataTransfer.files;
if(location === 'steps') {
DragNDropSteps.init(files);
} else {
DragNDropResults.init(files);
}
});
copyFromClipboard.init(location);
}
})(window);

View file

@ -1078,6 +1078,10 @@ ul.content-activities {
}
}
.drag-n-drop-file-input {
display: none !important;
}
.dnd-error,
.dnd-total-error {
color: $brand-danger;

View file

@ -61,55 +61,39 @@ class ResultAssetsController < ApplicationController
end
def update
success_flash = nil
saved = false
@result.transaction do
update_params = result_params
previous_size = @result.space_taken
previous_asset = @result.asset
if update_params.key? :asset_attributes
asset = Asset.find_by_id(update_params[:asset_attributes][:id])
asset.created_by = current_user
asset.last_modified_by = current_user
asset.team = current_team
@result.asset = asset
if update_params.dig(:asset_attributes, :signed_blob_id)
@result.asset.last_modified_by = current_user
@result.asset.update(file: update_params[:asset_attributes][:signed_blob_id])
update_params.delete(:asset_attributes)
end
@result.last_modified_by = current_user
@result.assign_attributes(update_params)
success_flash = t('result_assets.update.success_flash',
module: @my_module.name)
success_flash = t('result_assets.update.success_flash', module: @my_module.name)
if @result.archived_changed?(from: false, to: true)
if previous_asset.locked?
respond_to do |format|
format.html do
flash[:error] = t('result_assets.archive.error_flash')
redirect_to results_my_module_path(@my_module)
return
end
end
if @result.asset.locked?
@result.errors.add(:asset_attributes, t('result_assets.archive.error_flash'))
raise ActiveRecord:: Rollback
end
saved = @result.archive(current_user)
success_flash = t('result_assets.archive.success_flash',
module: @my_module.name)
if saved
log_activity(:archive_result)
end
success_flash = t('result_assets.archive.success_flash', module: @my_module.name)
log_activity(:archive_result) if saved
elsif @result.archived_changed?(from: true, to: false)
render_403
@result.errors.add(:asset_attributes, t('result_assets.archive.error_flash'))
raise ActiveRecord:: Rollback
else
if previous_asset.locked?
@result.errors.add(:asset_attributes,
t('result_assets.edit.locked_file_error'))
respond_to do |format|
format.json do
render json: {
status: 'error',
errors: @result.errors
}, status: :bad_request
return
end
end
if @result.asset.locked?
@result.errors.add(:asset_attributes, t('result_assets.edit.locked_file_error'))
raise ActiveRecord:: Rollback
end
# Asset (file) and/or name has been changed
saved = @result.save
@ -127,6 +111,7 @@ class ResultAssetsController < ApplicationController
log_activity(:edit_result)
end
end
end
respond_to do |format|
if saved
@ -181,37 +166,29 @@ class ResultAssetsController < ApplicationController
end
def result_params
params.require(:result).permit(
:name, :archived,
asset_attributes: [
:id,
:file
]
)
params.require(:result).permit(:name, :archived, asset_attributes: :signed_blob_id)
end
def create_multiple_results
success = true
success = false
results = []
ActiveRecord::Base.transaction do
params[:results_files].values.each_with_index do |file, index|
asset = Asset.new(created_by: current_user,
last_modified_by: current_user,
team: current_team)
asset.file.attach(file)
result = Result.new(user: current_user,
asset = Asset.create!(created_by: current_user, last_modified_by: current_user, team: current_team)
asset.file.attach(file[:signed_blob_id])
result = Result.create!(user: current_user,
my_module: @my_module,
name: params[:results_names][index.to_s],
asset: asset,
last_modified_by: current_user)
if result.save && asset.save
results << result
# Post process file here
asset.post_process_file(@my_module.experiment.project.team)
log_activity(:add_result, result)
else
success = false
end
success = true
end
{ status: success, results: results }
end

View file

@ -29,17 +29,27 @@ class StepsController < ApplicationController
end
def create
@step = Step.new(step_params)
@step = Step.new
@step.transaction do
new_step_params = step_params
# Attach newly uploaded files, and than remove their blob ids from the parameters
new_step_params[:assets_attributes]&.each do |key, value|
next unless value[:signed_blob_id]
asset = Asset.create!(created_by: current_user, last_modified_by: current_user, team: current_team)
asset.file.attach(value[:signed_blob_id])
@step.assets << asset
new_step_params[:assets_attributes].delete(key)
end
@step.assign_attributes(new_step_params)
# gerate a tag that replaces img tag in database
@step.completed = false
@step.position = @protocol.number_of_steps
@step.protocol = @protocol
@step.user = current_user
@step.last_modified_by = current_user
@step.assets.each do |asset|
asset.created_by = current_user
asset.team = current_team
end
@step.tables.each do |table|
table.created_by = current_user
table.team = current_team
@ -51,8 +61,8 @@ class StepsController < ApplicationController
end
end
respond_to do |format|
if @step.save
@step.save!
# Post process all assets
@step.assets.each do |asset|
asset.post_process_file(@protocol.team)
@ -72,7 +82,10 @@ class StepsController < ApplicationController
# Update protocol timestamp
update_protocol_ts(@step)
end
respond_to do |format|
if @step.errors.empty?
format.json do
render json: {
html: render_to_string(
@ -129,6 +142,17 @@ class StepsController < ApplicationController
# NOTE - step_params_all variable is updated
destroy_attributes(step_params_all)
# Attach newly uploaded files, and than remove their blob ids from the parameters
step_params_all[:assets_attributes]&.each do |key, value|
next unless value[:signed_blob_id]
@step.assets
.create!(created_by: current_user, last_modified_by: current_user, team: current_team)
.file
.attach(value[:signed_blob_id])
step_params_all[:assets_attributes].delete(key)
end
@step.assign_attributes(step_params_all)
@step.last_modified_by = current_user
@ -161,7 +185,7 @@ class StepsController < ApplicationController
# Post process step assets
@step.assets.each do |asset|
asset.post_process_file(team)
asset.post_process_file(team) if asset.changed?
end
# Generate activity
@ -469,14 +493,12 @@ class StepsController < ApplicationController
# Checks if hash contains destroy parameter '_destroy' and returns
# boolean value.
def has_destroy_params(params)
for key, values in params do
if values.respond_to?(:each)
for pos, attrs in params[key] do
if attrs[:_destroy] == '1'
return true
end
end
def has_destroy_params?(params)
params.each do |key, values|
next unless values.respond_to?(:each)
params[key].each do |_, attrs|
return true if attrs[:_destroy] == '1'
end
end
@ -487,12 +509,13 @@ class StepsController < ApplicationController
# values that contains destroy parameters from original variable and
# puts them into update_params variable.
def extract_destroy_params(params, update_params)
for key, values in params do
if values.respond_to?(:each)
params.each do |key, values|
next unless values.respond_to?(:each)
update_params[key] = {} unless update_params[key]
attr_params = update_params[key]
for pos, attrs in params[key] do
params[key].each do |pos, attrs|
if attrs[:_destroy] == '1'
if attrs[:id].present?
asset = Asset.find_by_id(attrs[:id])
@ -503,14 +526,13 @@ class StepsController < ApplicationController
end
end
params[key].delete(pos)
elsif has_destroy_params(params[key][pos])
elsif has_destroy_params?(params[key][pos])
attr_params[pos] = { id: attrs[:id] }
extract_destroy_params(params[key][pos], attr_params[pos])
end
end
end
end
end
def load_vars
@step = Step.find_by_id(params[:id])
@ -587,8 +609,8 @@ class StepsController < ApplicationController
],
assets_attributes: [
:id,
:file,
:_destroy
:_destroy,
:signed_blob_id
],
tables_attributes: [
:id,

View file

@ -33,6 +33,8 @@
data-atwho-rep-items-url="<%= atwho_rep_items_team_path(current_team) %>"
data-atwho-menu-items="<%= atwho_menu_items_team_path(current_team) %>"
data-file-max-size-mb="<%= Rails.configuration.x.file_max_size_mb %>"
data-name-max-length="<%= Constants::NAME_MAX_LENGTH %>"
data-filename-max-length="<%= Constants::FILENAME_TRUNCATION_LENGTH %>"
data-tooltips-enabled="<%= current_user.settings[:tooltips_enabled] %>"
<% end %>
>

View file

@ -7,6 +7,7 @@
data-create-record="<%= repository_repository_rows_path(repository) %>"
data-delete-record="<%= repository_delete_records_path(repository) %>"
data-copy-records="<%= repository_copy_records_path(repository) %>"
data-direct-upload-url="<%= rails_direct_uploads_url %>"
data-max-dropdown-length="<%= Constants::NAME_TRUNCATION_LENGTH_DROPDOWN %>"
data-repository-columns-ids="<%= repository.available_columns_ids %>"
data-save-text="<%= I18n.t('general.save') %>"

View file

@ -7,7 +7,7 @@
<%= file_extension_icon(ff.object) %>
<%= ff.object.file_name %>
</p>
<%= ff.file_field :file %>
<%= ff.file_field :file, direct_upload: true %>
<% end %>
<hr>
<div class="align-right">
@ -16,7 +16,7 @@
</button>
<%= f.submit t("general.save"),
class: 'btn btn-success save-result',
onclick: "Results.processResult(event, Results.ResultTypeEnum.FILE, true);" %>
onclick: "Results.processResult(event, Results.ResultTypeEnum.FILE);" %>
</div>
<% end %>
</div>

View file

@ -1,32 +1,34 @@
<div class="well">
<%= form_for(@result, url: my_module_result_assets_path(format: :json), data: { type: :json }) do |f| %>
<div id="new-result-assets-select" class="text-center new-asset-box">
<span class="help_tooltips"
data-tooltiplink="<%= I18n.t('tooltips.link.protocol.step_add_files') %>"
data-tooltipcontent="<%= I18n.t('tooltips.text.protocol.step_add_files') %>">
<%=t 'assets.drag_n_drop.label_html' %>
<%= t('assets.drag_n_drop.label_html') %>
<label>
<span class="btn btn-default new-asset-upload-button">
<span class="fas fa-file-upload new-asset-upload-icon"></span>
<%=t 'assets.drag_n_drop.browse_label' %>
<%= t('assets.drag_n_drop.browse_label') %>
</span>
<input type="file"
onchange="DragNDropResults.init(this.files, 'select')"
id="drag-n-drop-assets"
style="display: none" multiple>
<%= f.file_field :file,
id: 'drag-n-drop-assets',
class: 'drag-n-drop-file-input',
direct_upload: true,
multiple: true,
onchange: "DragNDropResults.init(this.files);" %>
</label>
</span>
</div>
<br />
<div class="align-right">
<button type="button"
class="btn btn-default cancel-new"
onClick="DragNDropResults.destroyAll()">
<%= t("general.cancel")%>
</button>
<button type="button"
class="btn btn-success save-result"
disabled="true"
data-href="<%= my_module_result_assets_path(format: :json) %>"
onClick="DragNDropResults.processResult(this)"><%=t 'result_assets.new.create' %></button>
<%= f.submit t('general.cancel'),
class: 'btn btn-default cancel-new',
onclick: 'DragNDropResults.destroyAll();' %>
<%= f.submit t('result_assets.new.create'),
class: 'btn btn-success save-result',
onclick: 'DragNDropResults.processResult(event);',
disabled: true,
data: { href: my_module_result_assets_path(format: :json) } %>
</div>
<% end %>
</div>

View file

@ -1,5 +1,7 @@
<div id="table-form" class="well">
<%= bootstrap_form_for(@result, url: result_table_path(format: :json), remote: true) do |f| %>
<%= bootstrap_form_for(@result, url: result_table_path(format: :json),
data: { 'name-max-length': Constants::NAME_MAX_LENGTH },
remote: true) do |f| %>
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
<div class="editable-table">
<%= f.fields_for :table do |ff| %>
@ -15,7 +17,7 @@
</button>
<%= f.submit t("general.save"),
class: 'btn btn-success save-result',
onclick: "Results.processResult(event, Results.ResultTypeEnum.TABLE, true);" %>
onclick: "Results.processResult(event, Results.ResultTypeEnum.TABLE);" %>
</div>
<% end %>
</div>

View file

@ -1,5 +1,7 @@
<div id="table-form" class="well">
<%= bootstrap_form_for(@result, url: my_module_result_tables_path(format: :json), remote: true) do |f| %>
<%= bootstrap_form_for(@result, url: my_module_result_tables_path(format: :json),
data: { 'name-max-length': Constants::NAME_MAX_LENGTH },
remote: true) do |f| %>
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
<div class="editable-table" style="margin-bottom: 25px;">
<%= f.fields_for :table do |ff| %>
@ -14,7 +16,7 @@
</button>
<%= f.submit t("result_tables.new.create"),
class: 'btn btn-success save-result',
onclick: "Results.processResult(event, Results.ResultTypeEnum.TABLE, false);" %>
onclick: "Results.processResult(event, Results.ResultTypeEnum.TABLE);" %>
</div>
<% end %>
</div>

View file

@ -1,5 +1,7 @@
<div class="well">
<%= bootstrap_form_for(@result, url: result_text_path(format: :json), remote: :true) do |f| %>
<%= bootstrap_form_for(@result, url: result_text_path(format: :json),
data: { 'name-max-length': Constants::NAME_MAX_LENGTH, 'rich-text-max-length': Constants::RICH_TEXT_MAX_LENGTH },
remote: :true) do |f| %>
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
<%= f.fields_for :result_text do |ff| %>
<%= ff.tiny_mce_editor(:text,
@ -15,7 +17,7 @@
</button>
<%= f.submit t("general.save"),
class: 'btn btn-success save-result',
onclick: "Results.processResult(event, Results.ResultTypeEnum.TEXT, true);" %>
onclick: "Results.processResult(event, Results.ResultTypeEnum.TEXT);" %>
</div>
<% end %>
</div>

View file

@ -1,5 +1,7 @@
<div class="well">
<%= bootstrap_form_for(@result, url: my_module_result_texts_path(format: :json), remote: true) do |f| %>
<%= bootstrap_form_for(@result, url: my_module_result_texts_path(format: :json),
data: { 'name-max-length': Constants::NAME_MAX_LENGTH, 'rich-text-max-length': Constants::RICH_TEXT_MAX_LENGTH },
remote: true) do |f| %>
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
<%= f.fields_for :result_text do |ff| %>
<%= ff.tiny_mce_editor(:text,
@ -14,7 +16,7 @@
</button>
<%= f.submit t("result_texts.new.create"),
class: 'btn btn-success save-result',
onclick: "Results.processResult(event, Results.ResultTypeEnum.TEXT, false);" %>
onclick: "Results.processResult(event, Results.ResultTypeEnum.TEXT);" %>
</div>
<% end %>
</div>

View file

@ -59,10 +59,13 @@
<span class="fas fa-file-upload new-asset-upload-icon"></span>
<%=t 'assets.drag_n_drop.browse_label' %>
</span>
<input type="file"
onchange="DragNDropSteps.init(this.files)"
id="drag-n-drop-assets"
style="display: none" multiple>
<span style="display:none;">
<%= f.file_field :file,
id: "drag-n-drop-assets",
multiple: true,
direct_upload: true,
onchange: "DragNDropSteps.init(this.files)" %>
</span>
</label>
</span>
</div>