diff --git a/app/assets/images/icon_small/marvinjs.svg b/app/assets/images/icon_small/marvinjs.svg index 7f4c24fbe..a39a2016d 100644 --- a/app/assets/images/icon_small/marvinjs.svg +++ b/app/assets/images/icon_small/marvinjs.svg @@ -1,3 +1,10 @@ - - + + + + + + + + + diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 6e00179f6..c568a3051 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -291,3 +291,10 @@ var HelperModule = (function(){ function notTurbolinksPreview() { return !document.documentElement.hasAttribute("data-turbolinks-preview"); } + +const windowScrollEvents = {}; +$(window).scroll(function() { + $.each(windowScrollEvents, function(key, scroll_function){ + scroll_function(); + }) +}) diff --git a/app/assets/javascripts/my_modules/repositories.js b/app/assets/javascripts/my_modules/repositories.js index 897553a4d..b3640dc38 100644 --- a/app/assets/javascripts/my_modules/repositories.js +++ b/app/assets/javascripts/my_modules/repositories.js @@ -186,7 +186,6 @@ var MyModuleRepositories = (function() { drawCallback: function() { FULL_VIEW_TABLE.columns.adjust(); - FilePreviewModal.init(); renderFullViewRepositoryName( tableContainer.attr('data-repository-name'), tableContainer.attr('data-repository-snapshot-created'), diff --git a/app/assets/javascripts/my_modules/results.js b/app/assets/javascripts/my_modules/results.js index 05973eb8d..10dc834c4 100644 --- a/app/assets/javascripts/my_modules/results.js +++ b/app/assets/javascripts/my_modules/results.js @@ -118,7 +118,6 @@ ResultAssets.applyEditResultAssetCallback(); applyCreateWopiFileCallback(); toggleResultEditButtons(true); - FilePreviewModal.init(); Comments.init(); ResultAssets.initNewResultAsset(); expandResult($(this)); diff --git a/app/assets/javascripts/protocols/index.js b/app/assets/javascripts/protocols/index.js index 77cf7f3c9..341b0afa6 100644 --- a/app/assets/javascripts/protocols/index.js +++ b/app/assets/javascripts/protocols/index.js @@ -223,7 +223,6 @@ function initProtocolPreviewModal() { modal.modal("show"); ProtocolRepositoryHeader.init(); initHandsOnTable(modalBody); - FilePreviewModal.init({ readOnly: true }); }, error: function (error) { // TODO diff --git a/app/assets/javascripts/protocols/steps.js.erb b/app/assets/javascripts/protocols/steps.js.erb index 9f883a5d5..d6b98b723 100644 --- a/app/assets/javascripts/protocols/steps.js.erb +++ b/app/assets/javascripts/protocols/steps.js.erb @@ -69,7 +69,6 @@ setTimeout(function() { initStepsComments(); - FilePreviewModal.init(); SmartAnnotation.preventPropagation('.atwho-user-popover'); TinyMCE.destroyAll(); DragNDropSteps.clearFiles(); @@ -97,7 +96,6 @@ toggleButtons(false); initializeCheckboxSorting(); animateSpinner(null, false); - FilePreviewModal.init(); DragNDropSteps.clearFiles(); if (tinyMCE.editors.step_description_textarea) tinyMCE.editors.step_description_textarea.remove(); TinyMCE.init('#step_description_textarea'); @@ -176,8 +174,6 @@ initCallBacks(); initHandsOnTable($new_step); toggleButtons(true); - FilePreviewModal.init(); - TinyMCE.destroyAll(); SmartAnnotation.preventPropagation('.atwho-user-popover'); // Show the edited step @@ -553,7 +549,7 @@ expandStep($new_step); toggleButtons(true); SmartAnnotation.preventPropagation('.atwho-user-popover'); - + $new_step.find('.attachments').trigger('reorder'); tinyMCE.editors.step_description_textarea.remove(); MarvinJsEditor.initNewButton('.new-marvinjs-upload-button'); @@ -563,7 +559,6 @@ }); animateSpinner(null, false); DragNDropSteps.clearFiles(); - FilePreviewModal.init(); if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); }, error: function(xhr) { @@ -617,28 +612,10 @@ Comments.init(); } - // Reorder attachments - 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 initCallBacks(); initHandsOnTable($(document)); expandAllSteps(); - FilePreviewModal.init(); TinyMCE.highlight(); SmartAnnotation.preventPropagation('.atwho-user-popover'); newStepHandler(); @@ -652,3 +629,66 @@ global.initHandsOnTable = initHandsOnTable; })(window); + +(function() { + // Reorder attachments + function reorderAttachmentsInit() { + $('#steps').on('click', '.attachments-actions .change-order', function(e){ + var orderDropdown = $(this).closest('.dropdown-menu'); + var assetsContainer = $(`.attachments[data-step-id=${orderDropdown.data('step-id')}]`) + var order = $(this).data('order'); + e.preventDefault(); + assetsContainer.data('order', order); + orderDropdown.find('.change-order').removeClass('selected'); + $(this).addClass('selected'); + assetsContainer.trigger('reorder'); + $.post(orderDropdown.data('state-save-url'), { + assets: { order: order } + }); + }) + + $('#steps').on('reorder', '.attachments', function() { + var assets = $(`.attachments[data-step-id=${$(this).data('step-id')}] .asset`); + var order = $(this).data('order'); + var sortedAssets = assets.sort(function(a, b) { + if (a.dataset.assetOrder == b.dataset.assetOrder) { + if (order == 'new') { + return b.dataset.assetUpdatedAt - a.dataset.assetUpdatedAt; + } if (order == 'old') { + return a.dataset.assetUpdatedAt - b.dataset.assetUpdatedAt; + } if (order == 'atoz') { + return a.dataset.assetFileName.toLowerCase() > b.dataset.assetFileName.toLowerCase() ? 1 : -1; + } if (order == 'ztoa') { + return b.dataset.assetFileName.toLowerCase() > a.dataset.assetFileName.toLowerCase() ? 1 : -1; + } + } + + return a.dataset.assetOrder > b.dataset.assetOrder ? 1 : -1 + }) + + $.each(sortedAssets, function(i, element){ + element.style.order = i + }) + }) + .on('DOMSubtreeModified', '.attachments', function() { + $(this).trigger('reorder'); + }) + $('.attachments').trigger('reorder'); + } + + function initAssetViewModeToggle(){ + $('#steps').on('click', '.attachments-actions .attachments-view-mode', function () { + var viewModeBtn = $(this); + $.post(viewModeBtn.closest('.dropdown-menu').data('view-mode-url'), { + assets_view_mode: viewModeBtn.data('assets-view-mode') + }, function(result) { + viewModeBtn.closest('.dropdown-menu').find('.attachments-view-mode').removeClass('selected'); + viewModeBtn.addClass('selected'); + viewModeBtn.closest('.step').find('.attachments').html(result.html) + }) + }) + } + + reorderAttachmentsInit(); + initAssetViewModeToggle(); +})(); diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index c05fc64c9..d0698f9fe 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -118,7 +118,6 @@ var RepositoryDatatable = (function(global) { currentMode = 'viewMode'; // Table specific stuff TABLE.button(0).enable(true); - FilePreviewModal.init(); $(TABLE_WRAPPER_ID).find('tr').removeClass('blocked'); updateButtons(); disableCheckboxToggleOnCheckboxPreview(); @@ -362,12 +361,6 @@ var RepositoryDatatable = (function(global) { }); } - function disableCheckboxToggleOnAssetDownload() { - $('.file-preview-link').on('click', function(ev) { - ev.stopPropagation(); - }); - } - // Adjust columns width in table header function adjustTableHeader() { TABLE.columns.adjust(); @@ -502,7 +495,6 @@ var RepositoryDatatable = (function(global) { animateSpinner(this, false); changeToViewMode(); updateDataTableSelectAllCtrl(); - FilePreviewModal.init(); // Prevent row toggling when selecting user smart annotation link SmartAnnotation.preventPropagation('.atwho-user-popover'); @@ -548,8 +540,6 @@ var RepositoryDatatable = (function(global) { }); }, fnInitComplete: function() { - disableCheckboxToggleOnAssetDownload(); - FilePreviewModal.init(); initHeaderTooltip(); disableCheckboxToggleOnCheckboxPreview(); diff --git a/app/assets/javascripts/results/result_assets.js b/app/assets/javascripts/results/result_assets.js index d8b92a117..35477a682 100644 --- a/app/assets/javascripts/results/result_assets.js +++ b/app/assets/javascripts/results/result_assets.js @@ -50,7 +50,6 @@ $form.remove(); applyEditResultAssetCallback(); Results.toggleResultEditButtons(true); - FilePreviewModal.init(); }); Results.toggleResultEditButtons(false); @@ -71,7 +70,6 @@ Results.toggleResultEditButtons(true); Results.expandResult($newResult); - FilePreviewModal.init(); Comments.init(); initNewResultAsset(); }).on('ajax:error', function(e, xhr) { @@ -97,5 +95,4 @@ ResultAssets.initNewResultAsset(); ResultAssets.applyEditResultAssetCallback(); - FilePreviewModal.init(); }(window)); diff --git a/app/assets/javascripts/sitewide/assets.js b/app/assets/javascripts/sitewide/assets.js new file mode 100644 index 000000000..e3aa289dd --- /dev/null +++ b/app/assets/javascripts/sitewide/assets.js @@ -0,0 +1,97 @@ +/* global windowScrollEvents HelperModule I18n */ +$(document).on('click', '.asset-context-menu .change-preview-type', function(e) { + var viewModeBtn = $(this); + var viewMode = viewModeBtn.data('preview-type'); + var toggleUrl = viewModeBtn.closest('.dropdown-menu').data('toggle-view-url'); + var assetId = viewModeBtn.closest('.dropdown-menu').data('asset-id'); + e.preventDefault(); + e.stopPropagation(); + $.ajax({ + url: toggleUrl, + type: 'PATCH', + dataType: 'json', + data: { asset: { view_mode: viewMode } }, + success: function(data) { + viewModeBtn.closest('.dropdown-menu').find('.change-preview-type').removeClass('selected'); + viewModeBtn.addClass('selected'); + $(`.asset[data-asset-id=${assetId}]`).replaceWith(data.html); + } + }); +}); + +$(document).on('click', '.asset .delete-asset', function(e) { + var asset = $(this).closest('.asset'); + e.preventDefault(); + e.stopPropagation(); + $.ajax({ + url: $(this).attr('href'), + type: 'DELETE', + dataType: 'json', + success: function(result) { + asset.remove(); + HelperModule.flashAlertMsg(result.flash, 'success'); + }, + error: function() { + HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger'); + } + }); +}); + +var InlineAttachments = (function() { + function elementVisible(element) { + var elementRect = element.getBoundingClientRect(); + var elementHeight = $(element).height(); + return elementRect.top + (elementHeight / 2) >= 0 + && elementRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + (elementHeight / 2); + } + + function showElement(element) { + setTimeout(() => { + var iframeUrl = $(element).find('.iframe-placeholder').data('iframe-url'); + if (elementVisible(element) && iframeUrl) { + $(element).find('.iframe-placeholder') + .replaceWith(``); + $(element).addClass('active').attr('data-created-at', new Date().getTime()); + } + }, 500); + } + + function hideElement(element) { + var iframeUrl = $(element).find('.active-iframe-preview').attr('src'); + if (!elementVisible(element) && iframeUrl) { + $(element).find('iframe') + .replaceWith(`
`); + $(element).removeClass('active').attr('data-created-at', null); + return true; + } + return false; + } + + function checkForAttachmentsState() { + $.each($('.inline-attachment-container'), function(i, element) { + showElement(element); + }); + if ($('.active-iframe-preview').length > 5) { + let sortedIframes = $('.inline-attachment-container.active').sort(function(a, b) { + return +a.dataset.createdAt - +b.dataset.createdAt; + }); + $.each(sortedIframes, function(i, element) { + if (hideElement(element)) return false; + }); + } + } + + return { + init: () => { + windowScrollEvents.InlineAttachments = InlineAttachments.scrollEvent; + }, + scrollEvent: () => { + checkForAttachmentsState(); + } + }; +})(); + +$(document).on('turbolinks:load', function() { + InlineAttachments.init(); + InlineAttachments.scrollEvent(); +}); diff --git a/app/assets/javascripts/sitewide/drag_n_drop.js b/app/assets/javascripts/sitewide/drag_n_drop.js index 07a69637e..a83c357c7 100644 --- a/app/assets/javascripts/sitewide/drag_n_drop.js +++ b/app/assets/javascripts/sitewide/drag_n_drop.js @@ -288,14 +288,14 @@ } function uploadedAssetPreview(asset, i) { - var html = `
-
+ var html = `
+
${truncateLongString(asset.name, GLOBAL_CONSTANTS.FILENAME_TRUNCATION_LENGTH)}
-
+
@@ -324,7 +324,7 @@ function listItems() { totalSize = 0; enableSubmitButton(); - $('.attachment-placeholder.new').remove(); + $('.attachment-container.new').remove(); dragNdropAssetsOff(); for (let i = 0; i < droppedFiles.length; i += 1) { @@ -435,7 +435,6 @@ initFormSubmitLinks($(this)); ResultAssets.applyEditResultAssetCallback(); Results.toggleResultEditButtons(true); - FilePreviewModal.init(); Comments.init(); ResultAssets.initNewResultAsset(); Results.expandResult($(this)); diff --git a/app/assets/javascripts/sitewide/file_preview.js b/app/assets/javascripts/sitewide/file_preview.js index 5edc691b4..715bcb6d0 100644 --- a/app/assets/javascripts/sitewide/file_preview.js +++ b/app/assets/javascripts/sitewide/file_preview.js @@ -1,545 +1,36 @@ /* 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 Assets ActiveStoragePreviews - PerfectScrollbar MarvinJsEditor refreshProtocolStatusBar */ - - var FilePreviewModal = (function() { 'use strict'; - var readOnly = false; - - function initPreviewModal(options = {}) { - var name; - var url; - var downloadUrl; - readOnly = options.readOnly; - - $('.file-preview-link').off('click'); - $('.file-preview-link').click(function(e) { - e.preventDefault(); - name = $(this).find('.attachment-label').text(); - url = $(this).data('preview-url'); - downloadUrl = $(this).attr('href'); - openPreviewModal(name, url, downloadUrl); - return true; - }); - - $('#filePreviewModal').find('.preview-close').click(function() { - $('#filePreviewModal').find('.file-preview-container').html(''); - $('#filePreviewModal').modal('hide'); - if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); - }); - } - - // Adding rotation icon - function updateFabricControls() { - fabric.Object.prototype.drawBorders = function(ctx, styleOverride = {}) { - var wh = this._calculateCurrentDimensions(); - var strokeWidth = 1 / this.borderScaleFactor; - var width = wh.x + strokeWidth; - var height = wh.y + strokeWidth; - var drawRotatingPoint = typeof styleOverride.hasRotatingPoint !== 'undefined' - ? styleOverride.hasRotatingPoint : this.hasRotatingPoint; - var hasControls = typeof styleOverride.hasControls !== 'undefined' - ? styleOverride.hasControls : this.hasControls; - var rotatingPointOffset = typeof styleOverride.rotatingPointOffset !== 'undefined' - ? styleOverride.rotatingPointOffset : this.rotatingPointOffset; - var rotateHeight = -height / 2; - ctx.save(); - ctx.strokeStyle = styleOverride.borderColor || this.borderColor; - this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray, null); - ctx.strokeRect( - -width / 2, - -height / 2, - width, - height - ); - if (drawRotatingPoint && this.isControlVisible('mtr') && hasControls) { - ctx.beginPath(); - ctx.moveTo(0, rotateHeight); - ctx.lineTo(0, rotateHeight - rotatingPointOffset + 10); - ctx.stroke(); - } - ctx.restore(); - return this; - }; - - fabric.Object.prototype.drawControls = function(ctx) { - var rotationImage = 'data:image/png;base64,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'; - var rotate = new Image(); - var rotateLeft; - var rotateTop; - var wh = this._calculateCurrentDimensions(); - var width = wh.x; - var height = wh.y; - var scaleOffset = this.cornerSize; - var left = -(width + scaleOffset) / 2; - var top = -(height + scaleOffset) / 2; - var methodName = this.transparentCorners ? 'stroke' : 'fill'; - - if (!this.hasControls) { - return this; - } - ctx.save(); - ctx.strokeStyle = this.cornerColor; - ctx.fillStyle = this.cornerColor; - if (!this.transparentCorners) { - ctx.strokeStyle = this.cornerStrokeColor; - } - this._setLineDash(ctx, this.cornerDashArray, null); - - // top-left - this._drawControl( - 'tl', - ctx, - methodName, - left, - top - ); - - // top-right - this._drawControl( - 'tr', - ctx, - methodName, - left + width, - top - ); - - // bottom-left - this._drawControl( - 'bl', - ctx, - methodName, - left, - top + height - ); - - // bottom-right - this._drawControl( - 'br', - ctx, - methodName, - left + width, - top + height - ); - - if (!this.get('lockUniScaling')) { - // middle-top - this._drawControl( - 'mt', - ctx, - methodName, - left + width / 2, - top - ); - - // middle-bottom - this._drawControl( - 'mb', - ctx, - methodName, - left + width / 2, - top + height - ); - - // middle-right - this._drawControl( - 'mr', - ctx, - methodName, - left + width, - top + height / 2 - ); - - // middle-left - this._drawControl( - 'ml', - ctx, - methodName, - left, - top + height / 2 - ); - } - // middle-top-rotate - if (this.hasRotatingPoint) { - rotate.src = rotationImage; - rotateLeft = left + width / 2 - 6; - rotateTop = top - this.rotatingPointOffset - 6; - ctx.drawImage(rotate, rotateLeft, rotateTop, 32, 32); - } - ctx.restore(); - - return this; - }; - } - - function preInitImageEditor(data) { - $.ajax({ - url: data['download-url'], - type: 'get', - success: function(responseData) { - var fileUrl = responseData; - initImageEditor(data, fileUrl); - } - }); - } - - function initImageEditor(data, fileUrl) { - var imageEditor; - var ps; - var blackTheme = { - 'common.bi.image': '', - 'common.bisize.width': '0', - 'common.bisize.height': '0', - 'common.backgroundImage': 'none', - 'common.backgroundColor': '#1e1e1e', - 'common.border': '0px', - - // header - 'header.backgroundImage': 'none', - 'header.backgroundColor': 'transparent', - 'header.border': '0px', - - // load button - 'loadButton.backgroundColor': '#fff', - 'loadButton.border': '1px solid #ddd', - 'loadButton.color': '#222', - 'loadButton.fontFamily': '\'Noto Sans\', sans-serif', - 'loadButton.fontSize': '12px', - - // download button - 'downloadButton.backgroundColor': '#fdba3b', - 'downloadButton.border': '1px solid #fdba3b', - 'downloadButton.color': '#fff', - 'downloadButton.fontFamily': '\'Noto Sans\', sans-serif', - 'downloadButton.fontSize': '12px', - - // main icons - 'menu.normalIcon.path': '/images/icon-d.svg', - 'menu.normalIcon.name': 'icon-d', - 'menu.activeIcon.path': '/images/icon-b.svg', - 'menu.activeIcon.name': 'icon-b', - 'menu.disabledIcon.path': '/images/icon-a.svg', - 'menu.disabledIcon.name': 'icon-a', - 'menu.hoverIcon.path': '/images/icon-c.svg', - 'menu.hoverIcon.name': 'icon-c', - 'menu.iconSize.width': '24px', - 'menu.iconSize.height': '24px', - - // submenu primary color - 'submenu.backgroundColor': '#1e1e1e', - 'submenu.partition.color': '#3c3c3c', - - // submenu icons - 'submenu.normalIcon.path': '/images/icon-d.svg', - 'submenu.normalIcon.name': 'icon-d', - 'submenu.activeIcon.path': '/images/icon-c.svg', - 'submenu.activeIcon.name': 'icon-c', - 'submenu.iconSize.width': '32px', - 'submenu.iconSize.height': '32px', - - // submenu labels - 'submenu.normalLabel.color': '#8a8a8a', - 'submenu.normalLabel.fontWeight': 'lighter', - 'submenu.activeLabel.color': '#fff', - 'submenu.activeLabel.fontWeight': 'lighter', - - // checkbox style - 'checkbox.border': '0px', - 'checkbox.backgroundColor': '#fff', - - // range style - 'range.pointer.color': '#fff', - 'range.bar.color': '#666', - 'range.subbar.color': '#d1d1d1', - - 'range.disabledPointer.color': '#414141', - 'range.disabledBar.color': '#282828', - 'range.disabledSubbar.color': '#414141', - - 'range.value.color': '#fff', - 'range.value.fontWeight': 'lighter', - 'range.value.fontSize': '11px', - 'range.value.border': '1px solid #353535', - 'range.value.backgroundColor': '#151515', - 'range.title.color': '#fff', - 'range.title.fontWeight': 'lighter', - - // colorpicker style - 'colorpicker.button.border': '1px solid #1e1e1e', - 'colorpicker.title.color': '#fff' - }; - - animateSpinner(null, true); - imageEditor = new tui.ImageEditor('#tui-image-editor', { - includeUI: { - loadImage: { - path: fileUrl, - name: data.filename - }, - theme: blackTheme, - initMenu: 'draw', - menuBarPosition: 'bottom' - }, - cssMaxWidth: 700, - cssMaxHeight: 500, - selectionStyle: { - cornerSize: 20, - rotatingPointOffset: 70, - borderColor: '#333', - cornerColor: '#333', - cornerStyle: 'circle', - borderScaleFactor: 3 - - }, - usageStatistics: false - }); - - imageEditor.on('image_loaded', () => { - $('.file-save-link').css('display', ''); - animateSpinner(null, false); - }); - - ps = new PerfectScrollbar($('.tui-image-editor-wrap')[0], { wheelSpeed: 0.5 }); - $('#tui-image-editor .tui-image-editor').on('mousewheel', (e) => { - var imageOriginalSize = { - width: imageEditor._graphics.canvasImage.width, - height: imageEditor._graphics.canvasImage.height - }; - var wDelta = e.originalEvent.wheelDelta || e.originalEvent.deltaY; - var imageEditorWindow = e.currentTarget; - var scrollContainer = $('.tui-image-editor-wrap'); - var initWidth = imageEditorWindow.style.width; - var initHeight = imageEditorWindow.style.height; - - var scrollContainerInitial = { - top: scrollContainer.scrollTop(), - left: scrollContainer.scrollLeft(), - height: scrollContainer[0].scrollHeight, - width: scrollContainer[0].scrollWidth - }; - - var mousePosition = { - top: e.clientY - (imageEditorWindow.offsetTop - scrollContainerInitial.top), - left: e.clientX - $(imageEditorWindow).offset().left - }; - - - var newWidth; - var newHeight; - var offsetY; - var offsetX; - if (wDelta > 0) { - newWidth = parseInt(initWidth, 10) * 1.1; - newHeight = parseInt(initHeight, 10) * 1.1; - if (newWidth > imageOriginalSize.width || newHeight > imageOriginalSize.height) { - newWidth = imageOriginalSize.width; - newHeight = imageOriginalSize.height; - } - } else { - newWidth = parseInt(initWidth, 10) * 0.9; - newHeight = parseInt(initHeight, 10) * 0.9; - if (parseInt(imageEditorWindow.dataset.minWidth, 10) * 0.5 > parseInt(newWidth, 10)) { - newWidth = parseInt(imageEditorWindow.dataset.minWidth, 10) * 0.5; - newHeight = parseInt(imageEditorWindow.dataset.minHeight, 10) * 0.5; - } - } - imageEditorWindow.style.width = newWidth + 'px'; - imageEditorWindow.style.height = newHeight + 'px'; - $(imageEditorWindow).find('canvas, .tui-image-editor-canvas-container') - .css('max-width', imageEditorWindow.style.width) - .css('max-height', imageEditorWindow.style.height); - if (imageEditorWindow.dataset.minHeight === undefined) { - imageEditorWindow.dataset.minHeight = initHeight; - imageEditorWindow.dataset.minWidth = initWidth; - } - - offsetY = (scrollContainer[0].scrollHeight - scrollContainerInitial.height) - * (mousePosition.top / scrollContainerInitial.height); - offsetX = (scrollContainer[0].scrollWidth - scrollContainerInitial.width) - * (mousePosition.left / scrollContainerInitial.width); - - scrollContainer.scrollTop(scrollContainerInitial.top + offsetY); - scrollContainer.scrollLeft(scrollContainerInitial.left + offsetX); - - ps.update(); - + function initPreviewModal() { + $(document).on('click', '.file-preview-link', function(e) { + var params = {}; + var galleryViewId = $(this).data('gallery-view-id'); e.preventDefault(); e.stopPropagation(); - }); - $('.tui-image-editor-wrap')[0].onwheel = function() { return false; }; - $('.tui-image-editor-wrap').css('height', 'calc(100% - 150px)'); - - $('#fileEditModal').find('.file-name').text('Editing: ' + data.filename); - $('#fileEditModal').modal('show'); - - $('.tui-image-editor-header').hide(); - - $('.file-save-link').css('display', 'none'); - $('.file-save-link').off().click(function(ev) { - var imageBlob; - var imageDataURL; - var imageParams; - var dataUpload = new FormData(); - var blobArray; - var bytePosition; - - ev.preventDefault(); - ev.stopPropagation(); - - if (data['mime-type'] === 'image/png') { - imageParams = { format: 'png' }; - } else { - imageParams = { format: 'jpeg', quality: (data.quality / 100) }; - } - - imageDataURL = imageEditor.toDataURL(imageParams); - imageDataURL = atob(imageDataURL.split(',')[1]); - - blobArray = new Uint8Array(imageDataURL.length); - - for (bytePosition = 0; bytePosition < imageDataURL.length; bytePosition += 1) { - blobArray[bytePosition] = imageDataURL.charCodeAt(bytePosition); - } - - imageBlob = new Blob([blobArray]); - - function closeEditor() { - animateSpinner(null, false); - imageEditor.destroy(); - imageEditor = {}; - $('#tui-image-editor').html(''); - $('#fileEditModal').modal('hide'); - } - - dataUpload.append('image', imageBlob); - animateSpinner(null, true); - $.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); - initPreviewModal(); - } - }).done(function() { - closeEditor(); - + params.gallery = $(`.file-preview-link[data-gallery-view-id=${galleryViewId}]`) + .toArray().sort((a, b) => $(a).closest('.asset').css('order') - $(b).closest('.asset').css('order')) + .map(i => i.dataset.id); + $.get($(this).data('preview-url'), params, function(result) { + $('#filePreviewModal .modal-content').html(result.html); + $('#filePreviewModal').modal('show'); }); - if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); }); - window.onresize = function() { - imageEditor.ui.resizeEditor(); - }; - } - - function openPreviewModal(name, url, downloadUrl) { - var modal = $('#filePreviewModal'); - updateFabricControls(); - $.ajax({ - url: url, - type: 'GET', - dataType: 'json', - success: function(data) { - var link = modal.find('.file-download-link'); - clearPrevieModal(); - if (Object.prototype.hasOwnProperty.call(data, 'wopi-controls')) { - modal.find('.file-wopi-controls').html(data['wopi-controls']); - } - link.attr('href', downloadUrl); - link.attr('data-no-turbolink', true); - link.attr('data-status', 'asset-present'); - if (data.type === 'previewable') { - animateSpinner('.file-preview-container', false); - modal.find('.file-preview-container') - .append($('') - .css('opacity', 0) - .attr('src', data['large-preview-url']) - .attr('alt', name) - .on('error', ActiveStoragePreviews.reCheckPreview) - .on('load', ActiveStoragePreviews.showPreview) - .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) { - $.post('/files/' + data.id + '/start_edit_image'); - ev.preventDefault(); - ev.stopPropagation(); - modal.modal('hide'); - preInitImageEditor(data); - }); - } else { - 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']); - } - if (readOnly) { - modal.find('#wopi_file_edit_button').remove(); - } - modal.find('.file-name').text(name); - modal.modal(); - modal.find('a[disabled=disabled]').click(function(ev) { - ev.preventDefault(); - }); - $('.modal-backdrop').last().css('z-index', modal.css('z-index') - 1); - }, - error: function() { - // TODO - } - }); - } - - 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($('') - .css('opacity', 0) - .attr('src', data['large-preview-url']) - .attr('alt', data.name) - .on('error', ActiveStoragePreviews.reCheckPreview) - .on('load', ActiveStoragePreviews.showPreview) - .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'); - $.post(data['update-url'] + '/start_editing'); - MarvinJsEditor.open({ - mode: 'edit', - data: data.description, - name: data.name, - marvinUrl: data['update-url'] - }); + $(document).on('click', '#filePreviewModal .gallery-switcher', function(e) { + e.preventDefault(); + e.stopPropagation(); + $.get($(this).attr('href'), { gallery: $(this).data('gallery-elements') }, function(result) { + $('#filePreviewModal .modal-content').html(result.html); }); - } else { - modal.find('.file-edit-link').css('display', 'none'); - } + }); } return Object.freeze({ - init: initPreviewModal, - imageEditor: initImageEditor + init: initPreviewModal }); -}(window)); +}()); + +FilePreviewModal.init(); diff --git a/app/assets/javascripts/sitewide/image_editor.js b/app/assets/javascripts/sitewide/image_editor.js new file mode 100644 index 000000000..2e0b8d477 --- /dev/null +++ b/app/assets/javascripts/sitewide/image_editor.js @@ -0,0 +1,420 @@ +/* global animateSpinner fabric PerfectScrollbar refreshProtocolStatusBar tui Uint8Array*/ +/* eslint-disable no-underscore-dangle */ + +var ImageEditorModal = (function() { + function updateFabricControls() { + fabric.Object.prototype.drawBorders = function(ctx, styleOverride = {}) { + var wh = this._calculateCurrentDimensions(); + var strokeWidth = 1 / this.borderScaleFactor; + var width = wh.x + strokeWidth; + var height = wh.y + strokeWidth; + var drawRotatingPoint = typeof styleOverride.hasRotatingPoint !== 'undefined' + ? styleOverride.hasRotatingPoint : this.hasRotatingPoint; + var hasControls = typeof styleOverride.hasControls !== 'undefined' + ? styleOverride.hasControls : this.hasControls; + var rotatingPointOffset = typeof styleOverride.rotatingPointOffset !== 'undefined' + ? styleOverride.rotatingPointOffset : this.rotatingPointOffset; + var rotateHeight = -height / 2; + ctx.save(); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray, null); + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + if (drawRotatingPoint && this.isControlVisible('mtr') && hasControls) { + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight - rotatingPointOffset + 10); + ctx.stroke(); + } + ctx.restore(); + return this; + }; + + fabric.Object.prototype.drawControls = function(ctx) { + var rotationImage = 'data:image/png;base64,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'; + var rotate = new Image(); + var rotateLeft; + var rotateTop; + var wh = this._calculateCurrentDimensions(); + var width = wh.x; + var height = wh.y; + var scaleOffset = this.cornerSize; + var left = -(width + scaleOffset) / 2; + var top = -(height + scaleOffset) / 2; + var methodName = this.transparentCorners ? 'stroke' : 'fill'; + + if (!this.hasControls) { + return this; + } + ctx.save(); + ctx.strokeStyle = this.cornerColor; + ctx.fillStyle = this.cornerColor; + if (!this.transparentCorners) { + ctx.strokeStyle = this.cornerStrokeColor; + } + this._setLineDash(ctx, this.cornerDashArray, null); + + // top-left + this._drawControl( + 'tl', + ctx, + methodName, + left, + top + ); + + // top-right + this._drawControl( + 'tr', + ctx, + methodName, + left + width, + top + ); + + // bottom-left + this._drawControl( + 'bl', + ctx, + methodName, + left, + top + height + ); + + // bottom-right + this._drawControl( + 'br', + ctx, + methodName, + left + width, + top + height + ); + + if (!this.get('lockUniScaling')) { + // middle-top + this._drawControl( + 'mt', + ctx, + methodName, + left + width / 2, + top + ); + + // middle-bottom + this._drawControl( + 'mb', + ctx, + methodName, + left + width / 2, + top + height + ); + + // middle-right + this._drawControl( + 'mr', + ctx, + methodName, + left + width, + top + height / 2 + ); + + // middle-left + this._drawControl( + 'ml', + ctx, + methodName, + left, + top + height / 2 + ); + } + // middle-top-rotate + if (this.hasRotatingPoint) { + rotate.src = rotationImage; + rotateLeft = left + width / 2 - 6; + rotateTop = top - this.rotatingPointOffset - 6; + ctx.drawImage(rotate, rotateLeft, rotateTop, 32, 32); + } + ctx.restore(); + + return this; + }; + } + + function initImageEditor(data, fileUrl) { + var imageEditor; + var ps; + var blackTheme = { + 'common.bi.image': '', + 'common.bisize.width': '0', + 'common.bisize.height': '0', + 'common.backgroundImage': 'none', + 'common.backgroundColor': '#1e1e1e', + 'common.border': '0px', + + // header + 'header.backgroundImage': 'none', + 'header.backgroundColor': 'transparent', + 'header.border': '0px', + + // load button + 'loadButton.backgroundColor': '#fff', + 'loadButton.border': '1px solid #ddd', + 'loadButton.color': '#222', + 'loadButton.fontFamily': '\'Noto Sans\', sans-serif', + 'loadButton.fontSize': '12px', + + // download button + 'downloadButton.backgroundColor': '#fdba3b', + 'downloadButton.border': '1px solid #fdba3b', + 'downloadButton.color': '#fff', + 'downloadButton.fontFamily': '\'Noto Sans\', sans-serif', + 'downloadButton.fontSize': '12px', + + // main icons + 'menu.normalIcon.path': '/images/icon-d.svg', + 'menu.normalIcon.name': 'icon-d', + 'menu.activeIcon.path': '/images/icon-b.svg', + 'menu.activeIcon.name': 'icon-b', + 'menu.disabledIcon.path': '/images/icon-a.svg', + 'menu.disabledIcon.name': 'icon-a', + 'menu.hoverIcon.path': '/images/icon-c.svg', + 'menu.hoverIcon.name': 'icon-c', + 'menu.iconSize.width': '24px', + 'menu.iconSize.height': '24px', + + // submenu primary color + 'submenu.backgroundColor': '#1e1e1e', + 'submenu.partition.color': '#3c3c3c', + + // submenu icons + 'submenu.normalIcon.path': '/images/icon-d.svg', + 'submenu.normalIcon.name': 'icon-d', + 'submenu.activeIcon.path': '/images/icon-c.svg', + 'submenu.activeIcon.name': 'icon-c', + 'submenu.iconSize.width': '32px', + 'submenu.iconSize.height': '32px', + + // submenu labels + 'submenu.normalLabel.color': '#8a8a8a', + 'submenu.normalLabel.fontWeight': 'lighter', + 'submenu.activeLabel.color': '#fff', + 'submenu.activeLabel.fontWeight': 'lighter', + + // checkbox style + 'checkbox.border': '0px', + 'checkbox.backgroundColor': '#fff', + + // range style + 'range.pointer.color': '#fff', + 'range.bar.color': '#666', + 'range.subbar.color': '#d1d1d1', + + 'range.disabledPointer.color': '#414141', + 'range.disabledBar.color': '#282828', + 'range.disabledSubbar.color': '#414141', + + 'range.value.color': '#fff', + 'range.value.fontWeight': 'lighter', + 'range.value.fontSize': '11px', + 'range.value.border': '1px solid #353535', + 'range.value.backgroundColor': '#151515', + 'range.title.color': '#fff', + 'range.title.fontWeight': 'lighter', + + // colorpicker style + 'colorpicker.button.border': '1px solid #1e1e1e', + 'colorpicker.title.color': '#fff' + }; + + animateSpinner(null, true); + imageEditor = new tui.ImageEditor('#tui-image-editor', { + includeUI: { + loadImage: { + path: fileUrl, + name: data.filename + }, + theme: blackTheme, + initMenu: 'draw', + menuBarPosition: 'bottom' + }, + cssMaxWidth: 700, + cssMaxHeight: 500, + selectionStyle: { + cornerSize: 20, + rotatingPointOffset: 70, + borderColor: '#333', + cornerColor: '#333', + cornerStyle: 'circle', + borderScaleFactor: 3 + + }, + usageStatistics: false + }); + + imageEditor.on('image_loaded', () => { + $('.file-save-link').css('display', ''); + animateSpinner(null, false); + }); + + ps = new PerfectScrollbar($('.tui-image-editor-wrap')[0], { wheelSpeed: 0.5 }); + $('#tui-image-editor .tui-image-editor').on('mousewheel', (e) => { + var imageOriginalSize = { + width: imageEditor._graphics.canvasImage.width, + height: imageEditor._graphics.canvasImage.height + }; + var wDelta = e.originalEvent.wheelDelta || e.originalEvent.deltaY; + var imageEditorWindow = e.currentTarget; + var scrollContainer = $('.tui-image-editor-wrap'); + var initWidth = imageEditorWindow.style.width; + var initHeight = imageEditorWindow.style.height; + + var scrollContainerInitial = { + top: scrollContainer.scrollTop(), + left: scrollContainer.scrollLeft(), + height: scrollContainer[0].scrollHeight, + width: scrollContainer[0].scrollWidth + }; + + var mousePosition = { + top: e.clientY - (imageEditorWindow.offsetTop - scrollContainerInitial.top), + left: e.clientX - $(imageEditorWindow).offset().left + }; + + + var newWidth; + var newHeight; + var offsetY; + var offsetX; + if (wDelta > 0) { + newWidth = parseInt(initWidth, 10) * 1.1; + newHeight = parseInt(initHeight, 10) * 1.1; + if (newWidth > imageOriginalSize.width || newHeight > imageOriginalSize.height) { + newWidth = imageOriginalSize.width; + newHeight = imageOriginalSize.height; + } + } else { + newWidth = parseInt(initWidth, 10) * 0.9; + newHeight = parseInt(initHeight, 10) * 0.9; + if (parseInt(imageEditorWindow.dataset.minWidth, 10) * 0.5 > parseInt(newWidth, 10)) { + newWidth = parseInt(imageEditorWindow.dataset.minWidth, 10) * 0.5; + newHeight = parseInt(imageEditorWindow.dataset.minHeight, 10) * 0.5; + } + } + imageEditorWindow.style.width = newWidth + 'px'; + imageEditorWindow.style.height = newHeight + 'px'; + $(imageEditorWindow).find('canvas, .tui-image-editor-canvas-container') + .css('max-width', imageEditorWindow.style.width) + .css('max-height', imageEditorWindow.style.height); + if (imageEditorWindow.dataset.minHeight === undefined) { + imageEditorWindow.dataset.minHeight = initHeight; + imageEditorWindow.dataset.minWidth = initWidth; + } + + offsetY = (scrollContainer[0].scrollHeight - scrollContainerInitial.height) + * (mousePosition.top / scrollContainerInitial.height); + offsetX = (scrollContainer[0].scrollWidth - scrollContainerInitial.width) + * (mousePosition.left / scrollContainerInitial.width); + + scrollContainer.scrollTop(scrollContainerInitial.top + offsetY); + scrollContainer.scrollLeft(scrollContainerInitial.left + offsetX); + + ps.update(); + + e.preventDefault(); + e.stopPropagation(); + }); + $('.tui-image-editor-wrap')[0].onwheel = function() { return false; }; + $('.tui-image-editor-wrap').css('height', 'calc(100% - 150px)'); + + $('#fileEditModal').find('.file-name').text('Editing: ' + data.filename); + $('#fileEditModal').modal('show'); + + $('.tui-image-editor-header').hide(); + + $('.file-save-link').css('display', 'none'); + $('.file-save-link').off().click(function(ev) { + var imageBlob; + var imageDataURL; + var imageParams; + var dataUpload = new FormData(); + var blobArray; + var bytePosition; + + ev.preventDefault(); + ev.stopPropagation(); + + if (data['mime-type'] === 'image/png') { + imageParams = { format: 'png' }; + } else { + imageParams = { format: 'jpeg', quality: (data.quality / 100) }; + } + + imageDataURL = imageEditor.toDataURL(imageParams); + imageDataURL = atob(imageDataURL.split(',')[1]); + + blobArray = new Uint8Array(imageDataURL.length); + + for (bytePosition = 0; bytePosition < imageDataURL.length; bytePosition += 1) { + blobArray[bytePosition] = imageDataURL.charCodeAt(bytePosition); + } + + imageBlob = new Blob([blobArray]); + + function closeEditor() { + animateSpinner(null, false); + imageEditor.destroy(); + imageEditor = {}; + $('#tui-image-editor').html(''); + $('#fileEditModal').modal('hide'); + } + + dataUpload.append('image', imageBlob); + animateSpinner(null, true); + $.ajax({ + type: 'POST', + url: '/files/' + data.id + '/update_image', + data: dataUpload, + contentType: false, + processData: false, + success: function(res) { + $(`.asset[data-asset-id=${data.id}]`).replaceWith(res.html); + $(`.asset[data-asset-id=${data.id}]`).closest('.attachments').trigger('reorder'); + closeEditor(); + } + }); + if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar(); + }); + + window.onresize = function() { + imageEditor.ui.resizeEditor(); + }; + } + + function preInitImageEditor() { + $(document).on('click', '.image-edit-button', function() { + var editButton = $(this); + updateFabricControls(); + $.get(editButton.data('image-url'), function(responseData) { + var fileUrl = responseData; + var data = { + id: editButton.data('image-id'), + quality: editButton.data('image-quality'), + filename: editButton.data('image-name'), + 'mime-type': editButton.data('image-mime-type') + }; + $('#filePreviewModal').modal('hide'); + $.post(editButton.data('image-start-edit-url')); + initImageEditor(data, fileUrl); + }); + }); + } + + return Object.freeze({ + init: preInitImageEditor + }); +}()); +ImageEditorModal.init(); diff --git a/app/assets/javascripts/sitewide/marvinjs_editor.js b/app/assets/javascripts/sitewide/marvinjs_editor.js index 5467d72d6..1bf2f63d3 100644 --- a/app/assets/javascripts/sitewide/marvinjs_editor.js +++ b/app/assets/javascripts/sitewide/marvinjs_editor.js @@ -1,5 +1,4 @@ -/* global TinyMCE, ChemicalizeMarvinJs, MarvinJSUtil, I18n, FilePreviewModal, tinymce */ -/* global Results, Comments */ +/* global TinyMCE, ChemicalizeMarvinJs, MarvinJSUtil, I18n, tinymce, HelperModule */ /* eslint-disable no-param-reassign */ /* eslint-disable wrap-iife */ /* eslint-disable no-use-before-define */ @@ -166,7 +165,6 @@ var MarvinJsEditorApi = (function() { TinyMCE.updateImages(config.editor); } $(marvinJsModal).modal('hide'); - FilePreviewModal.init(); config.button.dataset.inProgress = false; }).error((response) => { if (response.status === 403) { @@ -319,7 +317,17 @@ var MarvinJsEditorApi = (function() { })(); // Initialization - +$(document).on('click', '.marvinjs-edit-button', function() { + var editButton = $(this); + $.post(editButton.data('sketch-start-edit-url')); + $('#filePreviewModal').modal('hide'); + MarvinJsEditor.open({ + mode: 'edit', + data: editButton.data('sketch-description'), + name: editButton.data('sketch-name'), + marvinUrl: editButton.data('update-url') + }); +}); $(document).on('turbolinks:load', function() { MarvinJsEditor = MarvinJsEditorApi(); diff --git a/app/assets/javascripts/sitewide/repository_row_info_modal.js b/app/assets/javascripts/sitewide/repository_row_info_modal.js index af74b1da7..a4d5364fc 100644 --- a/app/assets/javascripts/sitewide/repository_row_info_modal.js +++ b/app/assets/javascripts/sitewide/repository_row_info_modal.js @@ -16,7 +16,6 @@ $(this).find('.modal-body #repository_row-info-table').DataTable().destroy(); $(this).remove(); }); - FilePreviewModal.init(); $('#repository_row-info-table').DataTable({ dom: 'RBltpi', stateSave: false, diff --git a/app/assets/stylesheets/marvinjs.scss b/app/assets/stylesheets/marvinjs.scss index 21c809f1c..e175bae8d 100644 --- a/app/assets/stylesheets/marvinjs.scss +++ b/app/assets/stylesheets/marvinjs.scss @@ -143,28 +143,11 @@ } } -button.new-marvinjs-upload-button { - padding: 1px 16px !important; - +.new-marvinjs-upload-button { .new-marvinjs-upload-icon { display: inline-block; - height: 32px; - width: 32px; - - img { - height: 100%; - width: 100%; - } - } -} - -li.new-marvinjs-upload-button { - - .new-marvinjs-upload-icon { - display: inline-block; - height: 24px; - margin-left: -9px; - width: 24px; + height: 1.5em; + width: 1.5em; img { height: 100%; diff --git a/app/assets/stylesheets/my_modules.scss b/app/assets/stylesheets/my_modules.scss index b82e1fc79..0ca5ed9b0 100644 --- a/app/assets/stylesheets/my_modules.scss +++ b/app/assets/stylesheets/my_modules.scss @@ -57,9 +57,11 @@ // Create wopi file .create-wopi-file-btn { + cursor: pointer; + img { - margin-right: 5px; - height: 20px; + height: 1.5em; + width: 1.5em; } } diff --git a/app/assets/stylesheets/my_modules/results/index.scss b/app/assets/stylesheets/my_modules/results/index.scss index 9594ff981..b62910335 100644 --- a/app/assets/stylesheets/my_modules/results/index.scss +++ b/app/assets/stylesheets/my_modules/results/index.scss @@ -17,14 +17,12 @@ .fas { padding-right: .5em; + width: 1.75em; } - .create-wopi-file-btn { + .create-wopi-file-btn, + .new-marvinjs-upload-button { padding: 0; - - img { - margin-left: -5px; - } } } } diff --git a/app/assets/stylesheets/reports.scss b/app/assets/stylesheets/reports.scss index c99cd8a56..fad2bbe86 100644 --- a/app/assets/stylesheets/reports.scss +++ b/app/assets/stylesheets/reports.scss @@ -369,6 +369,10 @@ label { margin-left: 15px; } } + + img { + max-width: 100%; + } } // Result table element style @@ -422,6 +426,11 @@ label { } } + + img { + max-width: 100%; + } + .report-element-children { height: 0; } @@ -453,6 +462,7 @@ label { .report-element-header { .file-name { margin-left: 5px; + white-space: nowrap; } } diff --git a/app/assets/stylesheets/reports_print.scss b/app/assets/stylesheets/reports_print.scss index 06122fc40..32f41d3ae 100644 --- a/app/assets/stylesheets/reports_print.scss +++ b/app/assets/stylesheets/reports_print.scss @@ -93,6 +93,10 @@ div.print-report { &:hover > .report-element-header .attachment-icon { color: $color-black; } + + img { + max-width: 100%; + } } .report-step-table-element { @@ -195,6 +199,10 @@ div.print-report { } } + img { + max-width: 100%; + } + &:hover > .report-element-header { .result-icon, .result-name, diff --git a/app/assets/stylesheets/shared/assets.scss b/app/assets/stylesheets/shared/assets.scss new file mode 100644 index 000000000..b64a668b7 --- /dev/null +++ b/app/assets/stylesheets/shared/assets.scss @@ -0,0 +1,302 @@ +// scss-lint:disable ImportantRule PropertyUnits NestingDepth SelectorDepth + +.attachment-container { + @include md-card-style; + grid-row: span 6; + height: 23em; + overflow: hidden; + padding: 1em; + position: relative; + width: var(--attachment-column-width); + + + .file-preview-link { + display: block; + height: 100%; + width: 100%; + } + + .attachment-preview { + border-radius: $border-radius-default; + color: $color-volcano; + height: 14em; + position: relative; + text-align: center; + width: 100%; + + &.processing { + background-image: url("/images/medium/processing.gif"); + background-position: center; + background-repeat: no-repeat; + } + + img { + max-height: 100%; + max-width: 100%; + } + + &.marvinjs { + + &::before, + &::after { + border-radius: 1em 0 0 1em; + bottom: 1em; + content: ""; + display: block; + height: 2em; + line-height: 2em; + position: absolute; + right: -1em; + width: 2.25em; + } + + &::before { + background: $marvinjs-color; + } + + &::after { + background-image: url("/images/icon_small/marvinjs-white.svg"); + height: 2.25em; + right: -.75em; + width: 2em; + } + } + + .fas { + font-size: 100px; + line-height: 160px; + } + } + + .no-shadow { + box-shadow: none; + } + + .attachment-label, + .attachment-metadata { + background: $color-white; + overflow: hidden; + padding-top: 1em; + position: relative; + text-align: center; + text-overflow: ellipsis; + transition: $md-transaction; + white-space: nowrap; + } + + .attachment-label { + @include font-main; + margin-top: 1.5em; + z-index: 2; + } + + .attachment-metadata { + @include font-small; + color: $color-silver-chalice; + margin-top: -4em; + } + + .remove-icon { + bottom: .5em; + cursor: pointer; + display: none; + position: absolute; + right: .5em; + } + + &:hover { + box-shadow: $md-shadow; + + .file-preview-link { + text-decoration: none; + } + + + .remove-icon { + display: inline-block; + } + + .attachment-label, + .attachment-metadata { + margin-top: 0; + + } + } + + &.new { + border: 1px solid $brand-primary; + + .dnd-error { + bottom: 15px; + float: left; + padding-left: 5px; + position: relative; + } + + &::before { + background: $brand-primary; + border-radius: 0 $border-radius-default; + bottom: 0; + color: $color-white; + content: "NEW"; + left: 0; + line-height: 20px; + position: absolute; + text-align: center; + width: 50px; + z-index: 2; + } + } + + .asset-context-menu { + position: absolute; + right: 0; + top: 0; + } +} + +.inline-attachment-container { + @include md-card-style; + grid-column: 1/-1; + grid-row: span 12; + height: 47em; + + .active-iframe-preview { + border: 0; + height: calc(100% - 4em); + width: 100%; + } + + .image-container, + .general-file-container { + align-items: center; + background: $color-concrete; + display: flex; + height: calc(100% - 4em); + justify-content: center; + padding: .5em; + width: 100%; + + img { + max-height: 100%; + max-width: 100%; + } + + .fas { + font-size: 10em; + } + } + + .header { + align-items: center; + display: flex; + height: 4em; + padding: 0 1em; + + .show-as-thumbnail { + cursor: pointer; + margin-left: auto; + } + + .file-name { + @include font-main; + color: $brand-primary; + } + + .file-metadata { + @include font-small; + color: $color-silver-chalice; + display: grid; + grid-column-gap: 1em; + grid-template-columns: max-content max-content; + } + + .asset-context-menu { + margin-left: auto; + } + } +} + +.list-attachment-container { + @include md-card-style; + align-items: center; + display: flex; + grid-column: 1/-1; + height: 3em; + padding: .5em; + + .file-icon { + @include font-main; + } + + .file-name { + @include font-main; + color: $brand-primary; + margin: 0 .5em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .file-metadata { + @include font-small; + color: $color-silver-chalice; + display: grid; + grid-column-gap: 1em; + grid-template-columns: max-content max-content; + } + + .asset-context-menu { + margin-left: auto; + } +} + +.asset-context-menu { + display: inline-block; + + #dropdownAssetContextMenu { + background: $color-white; + + &:focus, + &:active { + box-shadow: none; + } + + &:hover { + background: $color-concrete; + } + } + + .dropdown-menu { + @include font-button; + min-width: 200px; + padding: .5em 0; + + .divider-label { + @include font-small; + color: $color-alto; + padding-left: 1em; + } + + a { + cursor: pointer; + padding: .5em 1em; + text-align: left; + } + + .change-preview-type, + .delete-asset { + .fas { + width: 1.5em; + } + + &.selected::after { + @include font-awesome; + content: $font-fas-check; + margin-left: auto; + position: absolute; + right: 1em; + } + } + } +} diff --git a/app/assets/stylesheets/shared/file_preview.scss b/app/assets/stylesheets/shared/file_preview.scss new file mode 100644 index 000000000..ca255ef3d --- /dev/null +++ b/app/assets/stylesheets/shared/file_preview.scss @@ -0,0 +1,145 @@ +// scss-lint:disable ImportantRule PropertyUnits NestingDepth SelectorDepth + +// Image preview modal +.modal-file-preview { + background: transparent; + font-size: $font-size-base; + padding: 0 !important; + + .modal-dialog { + background: inherit; + height: 100%; + margin: 0; + padding: 0; + width: 100%; + } + + .modal-content { + background: inherit; + border: 0; + border-radius: 0; + height: auto; + min-height: 100%; + } + + .file-preview-container { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + text-align: center; + width: 100%; + + &.processing { + background-image: url("/images/medium/processing.gif"); + background-position: center; + background-repeat: no-repeat; + } + + .wopi-file-preview { + height: 100%; + width: 100%; + } + + } + + img { + background: url(asset-path("custom/checkerboard-pattern.png")); + height: auto; + max-width: 100%; + + @media (max-height: 520px) { + height: 80%; + max-width: 100%; + } + } + + .modal-header { + align-items: center; + background: $color-white; + display: flex; + height: 4em; + padding: 0 1em; + + .file-name { + font-weight: bold; + margin-right: auto; + } + + .asset-context-menu { + float: left; + } + } + + .modal-body { + align-items: center; + background: transparent; + display: flex; + height: calc(100vh - 8em); + justify-content: center; + overflow: hidden; + padding: 0; + width: 100%; + } + + .modal-footer { + background: $color-white; + height: 4em; + padding: 0 1em; + + .gallery { + align-items: center; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + height: 100%; + padding: 0 1em; + width: 100%; + + img { + max-height: 2em; + max-width: 2em; + } + + + .file-counter { + font-weight: bold; + grid-column-start: 2; + margin: 0 auto; + } + + .gallery-switcher { + align-items: center; + color: $color-black; + display: flex; + + &:hover { + text-decoration: none; + } + + .file-name { + display: inline-block; + max-width: 16em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + + .fa-angle-right, + .fa-angle-left { + margin: 0 .5em; + } + + &.previous-asset { + grid-column-start: 1; + margin-right: auto; + } + + &.next-asset { + grid-column-start: 3; + margin-left: auto; + } + } + } + } +} diff --git a/app/assets/stylesheets/shared/image_edit_modal.scss b/app/assets/stylesheets/shared/image_edit_modal.scss new file mode 100644 index 000000000..b5f592e90 --- /dev/null +++ b/app/assets/stylesheets/shared/image_edit_modal.scss @@ -0,0 +1,37 @@ +// Image edit modal +.modal-file-edit { + background: transparent; + font-size: $font-size-base; + padding: 0 !important; + + .modal-dialog { + height: 100%; + margin: 0; + width: auto; + } + + .modal-content { + border: 0; + height: 100%; + } + + .modal-header { + align-items: center; + background: $color-white; + display: flex; + height: 4em; + padding: 0 1em; + + .file-name { + font-weight: bold; + margin-right: auto; + } + } + + .modal-body { + height: calc(100% - 4em); + padding: 0; + } +} + + diff --git a/app/assets/stylesheets/shared_styles/elements/buttons.scss b/app/assets/stylesheets/shared_styles/elements/buttons.scss index 51364b734..70a3f22ba 100644 --- a/app/assets/stylesheets/shared_styles/elements/buttons.scss +++ b/app/assets/stylesheets/shared_styles/elements/buttons.scss @@ -16,9 +16,10 @@ transition: .3s; user-select: none; - .fas { + .fas, + img { color: inherit; - margin-right: 5px; + margin-right: .25em; } &:hover { diff --git a/app/assets/stylesheets/steps.scss b/app/assets/stylesheets/steps.scss index 1cd848d4c..72dd7ffb1 100644 --- a/app/assets/stylesheets/steps.scss +++ b/app/assets/stylesheets/steps.scss @@ -5,10 +5,17 @@ // scss-lint:disable NestingDepth // scss-lint:disable SelectorFormat // scss-lint:disable ImportantRule +// scss-lint:disable Unknown @import "constants"; @import "mixins"; +:root { + --attachment-column-width: 16em; + --attachment-row-height: 3em; +} + + #new_step, .panel-step-attachment { ul { @@ -145,194 +152,18 @@ .attachments { display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-auto-rows: var(--attachment-row-height); + grid-column-gap: 1em; + grid-row-gap: 1em; + grid-template-columns: repeat(auto-fill, minmax(var(--attachment-column-width), 1fr)); + margin: 1em 0; - .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; - } - } - } + .nested_fields { + display: contents; } } -.attachment-placeholder { - @include md-card-style; - color: $color-silver-chalice; - height: 280px; - margin: 4px 8px 16px; - text-align: center; - width: 220px; - a { - color: inherit; - } - - &.new { - background-color: rgba(95, 95, 95, .1); - } - - .attachment-thumbnail { - display: inline-block; - height: 160px; - margin: 16px 0 5px; - overflow: hidden; - padding: 0 10px; - position: relative; - text-align: center; - width: 100%; - - img { - border-radius: 5px; - max-height: 100%; - max-width: 100%; - } - - &.marvinjs { - - &::before, - &::after { - content: ""; - border-radius: 16px 0 0 16px; - display: block; - height: 32px; - right: 0; - line-height: 32px; - position: absolute; - bottom: 10px; - width: 36px; - } - - &::before { - background: $marvinjs-color; - } - - &::after { - background-image: url("/images/icon_small/marvinjs-white.svg"); - right: 4px; - width: 32px; - } - } - - i.fas { - font-size: 100px; - line-height: 160px; - } - } - - .no-shadow { - box-shadow: none; - } - - .attachment-label { - background: $color-white; - color: $brand-primary; - font-family: Lato; - font-size: 16px; - height: 40px; - line-height: 18px; - margin: 0 auto; - overflow: hidden; - position: relative; - text-align: center; - top: 20px; - transition: $md-transaction; - width: 190px; - z-index: 2; - } - - .spencer-bonnet-modif { - align-items: center; - color: $color-silver-chalice; - display: flex; - font-family: Lato; - font-size: 12px; - height: 40px; - justify-content: center; - line-height: 15px; - margin: 0 auto 5px; - position: relative; - text-align: center; - top: -20px; - transition: $md-transaction; - width: 149px; - } - - .remove-icon { - bottom: 15px; - cursor: pointer; - display: none; - position: relative; - right: 10px; - } - - &:hover { - box-shadow: $md-shadow; - - .remove-icon { - display: inline-block; - } - - .attachment-label { - top: 0; - } - - .spencer-bonnet-modif { - top: 0; - } - } - - &.new { - background-color: rgba(95, 95, 95, .1); - - .attachment-label { - background-color: transparent; - } - - .dnd-error { - bottom: 15px; - float: left; - padding-left: 5px; - position: relative; - } - - &:hover { - .attachment-label { - top: 20px; - } - } - } -} - -.attachments-order { - display: inline-block; -} .attachments-actions { align-items: center; @@ -344,18 +175,45 @@ flex-shrink: 0; } - .attachments-order { - color: $color-silver-chalice; - min-width: 140px; - } + .dropdown-attachment-options { + @include font-button; + min-width: 200px; - .btn-default { - border: 0; - color: inherit; - margin-left: 10px; + .divider-label { + @include font-small; + color: $color-alto; + padding-left: 1em; + } - &:hover { - background: inherit; + a { + cursor: pointer; + padding: .5em 1em; + } + + .change-order { + padding-left: 2.75em; + + &.selected::after { + @include font-awesome; + content: $font-fas-check; + margin-left: auto; + position: absolute; + right: 1em; + } + } + + .attachments-view-mode { + .fas { + width: 1.5em; + } + + &.selected::after { + @include font-awesome; + content: $font-fas-check; + margin-left: auto; + position: absolute; + right: 1em; + } } } } diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 30c00a7b6..3b27f3926 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -1442,209 +1442,6 @@ ul.content-activities { } } -.attachment-thumbnail { - display: inline-block; - height: 160px; - margin: 16px 10px 5px; - overflow: hidden; - text-align: center; - width: 100%; - - img { - border-radius: 5px; - max-height: 100%; - max-width: 100%; - } - - .fas { - font-size: 100px; - line-height: 160px; - } - - &.processing { - background-image: url("/images/medium/processing.gif"); - background-position: center; - background-repeat: no-repeat; - } -} - -// Image preview modal -.modal-file-preview { - background: transparent; - font-size: $font-size-large; - padding-right: 0 !important; - z-index: 1060; - - .preview-close { - background: transparent; - border: 0; - color: $color-white; - display: inline-block; - float: right; - } - - .file-wopi-controls { - display: inline-block; - - .btn { - margin: 0 15px; - } - } - - .modal-dialog { - height: 100%; - margin: 0; - padding: 0; - width: auto; - } - - .modal-content { - background: transparent; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - color: $color-white; - height: 100%; - width: auto; - } - - .file-preview-container { - align-items: center; - color: $gray-dark; - display: -moz-flex; - display: -webkit-flex; - display: flex; - height: 100%; - justify-content: center; - text-align: center; - width: 60%; - - &.processing { - background-image: url("/images/medium/processing.gif"); - background-position: center; - background-repeat: no-repeat; - } - - .file-name { - color: $color-black; - margin: 30px 0; - } - } - - img { - background: url(asset-path("custom/checkerboard-pattern.png")); - height: auto; - max-width: 100%; - - @media (max-height: 520px) { - height: 80%; - max-width: 100%; - } - } - - .modal-header { - background: $color-black; - border: 0; - height: 60px; - line-height: 40px; - text-align: center; - - .file-name { - float: left; - } - } - - .modal-body { - align-items: center; - display: -moz-flex; - display: -webkit-flex; - display: flex; - height: calc(100% - 120px); - justify-content: center; - overflow: hidden; - padding: 0; - } - - .modal-footer { - background: $color-black; - border: 0; - bottom: 0; - height: 60px; - position: absolute; - text-align: center; - width: 100%; - } - - .file-download-link { - color: $color-white; - display: inline-block; - float: right; - margin-right: 20px; - } - - .file-edit-link { - color: $color-white; - display: inline-block; - float: right; - margin-right: 20px; - } -} - -// Image edit modal -.modal-file-edit { - 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; - -webkit-box-shadow: none; - box-shadow: none; - color: $color-white; - height: 100%; - width: auto; - } - - .modal-header { - background: $color-black; - border: 0; - height: 60px; - text-align: center; - - .file-name { - float: left; - } - } - - .modal-body { - height: calc(100% - 60px); - padding: 0; - } - - .file-save-link { - color: $color-white; - display: inline-block; - float: right; - margin-right: 20px; - } -} - // Disable textarea resizing throughout application // (will be done via autosize JS plugin) textarea { diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index c1a31a88e..05fe68420 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -14,86 +14,38 @@ class AssetsController < ApplicationController include FileIconsHelper include MyModulesHelper + helper_method :wopi_file_edit_button_status + before_action :load_vars, except: :create_wopi_file - before_action :check_read_permission, except: :edit - before_action :check_edit_permission, only: :edit + before_action :check_read_permission, except: %i(edit destroy create_wopi_file) + before_action :check_edit_permission, only: %i(edit destroy) def file_preview - file_type = @asset.file.metadata[:asset_type] || (@asset.previewable? ? 'previewable' : false) - response_json = { - 'id' => @asset.id, - 'type' => file_type, - 'filename' => truncate(escape_input(@asset.file_name), - length: Constants::FILENAME_TRUNCATION_LENGTH), - 'download-url' => asset_file_url_path(@asset) - } - - can_edit = if @assoc.class == Step - can_manage_protocol_in_module?(@protocol) || can_manage_protocol_in_repository?(@protocol) - elsif @assoc.class == Result - can_manage_module?(@my_module) - elsif @assoc.class == RepositoryCell && !@repository.is_a?(RepositorySnapshot) - can_manage_repository_rows?(@repository) - end - if response_json['type'] == 'previewable' - if ['image/jpeg', 'image/pjpeg'].include? @asset.file.content_type - response_json['quality'] = @asset.file_image_quality || 80 - end - response_json.merge!( - 'editable' => @asset.editable_image? && can_edit, - 'mime-type' => @asset.file.content_type, - '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 - - if wopi_enabled? && wopi_file?(@asset) - edit_supported, title = wopi_file_edit_button_status - response_json['wopi-controls'] = render_to_string( - partial: 'assets/wopi/file_wopi_controls.html.erb', - locals: { - asset: @asset, - can_edit: can_edit, - edit_supported: edit_supported, - title: title - } - ) - end - respond_to do |format| - format.json do - render json: response_json - end - end + render json: { html: render_to_string( + partial: 'shared/file_preview/content.html.erb', + locals: { + asset: @asset, + can_edit: can_manage_asset?(@asset), + gallery: params[:gallery] + } + ) } end - # Check whether the wopi file can be edited and return appropriate response - def wopi_file_edit_button_status - file_ext = @asset.file_name.split('.').last - if Constants::WOPI_EDITABLE_FORMATS.include?(file_ext) - edit_supported = true - title = '' - else - edit_supported = false - title = if Constants::FILE_TEXT_FORMATS.include?(file_ext) - I18n.t('assets.wopi_supported_text_formats_title') - elsif Constants::FILE_TABLE_FORMATS.include?(file_ext) - I18n.t('assets.wopi_supported_table_formats_title') - else - I18n.t('assets.wopi_supported_presentation_formats_title') - end + def toggle_view_mode + @asset.view_mode = toggle_view_mode_params[:view_mode] + if @asset.save(touch: false) + gallery_view_id = @assoc.id if @assoc.class == Step + + html = render_to_string(partial: 'assets/asset.html.erb', locals: { + asset: @asset, + gallery_view_id: gallery_view_id + }) + respond_to do |format| + format.json do + render json: { html: html }, status: :ok + end + end end - return edit_supported, title end def file_url @@ -113,7 +65,7 @@ class AssetsController < ApplicationController tkn = current_user.get_wopi_token @token = tkn.token @ttl = (tkn.ttl * 1000).to_s - @asset.step&.protocol&.update(updated_at: Time.now) + @asset.step&.protocol&.update(updated_at: Time.zone.now) create_wopi_file_activity(current_user, true) @@ -147,41 +99,17 @@ class AssetsController < ApplicationController @asset.team.release_space(orig_file_size) # Post process file here @asset.post_process_file(@asset.team) - @asset.step&.protocol&.update(updated_at: Time.now) + @asset.step&.protocol&.update(updated_at: Time.zone.now) - render_html = if @asset.step - assets = @asset.step.assets - order_atoz = az_ordered_assets_index(@asset.step, @asset.id) - order_ztoa = assets.length - az_ordered_assets_index(@asset.step, @asset.id) - asset_position = @asset.step.asset_position(@asset) + render_html = if @asset.step || @asset.result render_to_string( - partial: 'steps/attachments/item.html.erb', - locals: { - asset: @asset, - i: asset_position[:pos], - assets_count: asset_position[:count], - step: @asset.step, - order_atoz: order_atoz, - order_ztoa: order_ztoa - }, - formats: :html - ) - elsif @asset.result - render_to_string( - partial: 'steps/attachments/item.html.erb', - locals: { - asset: @asset, - i: 0, - assets_count: 0, - step: nil, - order_atoz: 0, - order_ztoa: 0 - }, + partial: 'assets/asset.html.erb', + locals: { asset: @asset }, formats: :html ) else render_to_string( - partial: 'shared/asset_link', + partial: 'assets/asset_link.html.erb', locals: { asset: @asset, display_image_tag: true }, formats: :html ) @@ -211,7 +139,7 @@ class AssetsController < ApplicationController unless asset.valid?(:wopi_file_creation) render json: { message: asset.errors - }, status: 400 and return + }, status: :bad_request and return end # Create file depending on the type @@ -220,7 +148,7 @@ class AssetsController < ApplicationController render_403 && return unless can_manage_protocol_in_module?(step.protocol) || can_manage_protocol_in_repository?(step.protocol) step_asset = StepAsset.create!(step: step, asset: asset) - step.protocol&.update(updated_at: Time.now) + step.protocol&.update(updated_at: Time.zone.now) edit_url = edit_asset_url(step_asset.asset_id) elsif params[:element_type] == 'Result' @@ -248,10 +176,18 @@ class AssetsController < ApplicationController }, status: :ok end + def destroy + if @asset.destroy + render json: { flash: I18n.t('assets.file_deleted', file_name: @asset.file_name ) } + else + render json: {}, status: :unprocessable_entity + end + end + private def load_vars - @asset = Asset.find_by_id(params[:id]) + @asset = Asset.find_by(id: params[:id]) return render_404 unless @asset @assoc ||= @asset.step @@ -268,25 +204,11 @@ class AssetsController < ApplicationController end def check_read_permission - if @assoc.class == Step - render_403 && return unless can_read_protocol_in_module?(@protocol) || - can_read_protocol_in_repository?(@protocol) - elsif @assoc.class == Result - render_403 and return unless can_read_experiment?(@my_module.experiment) - elsif @assoc.class == RepositoryCell - render_403 and return unless can_read_repository?(@repository) - end + render_403 and return unless can_read_asset?(@asset) end def check_edit_permission - if @assoc.class == Step - render_403 && return unless can_manage_protocol_in_module?(@protocol) || - can_manage_protocol_in_repository?(@protocol) - elsif @assoc.class == Result - render_403 and return unless can_manage_module?(@my_module) - elsif @assoc.class == RepositoryCell - render_403 and return unless can_manage_repository_rows?(@repository) - end + render_403 and return unless can_manage_asset?(@asset) end def append_wd_params(url) @@ -299,6 +221,10 @@ class AssetsController < ApplicationController params.permit(:file) end + def toggle_view_mode_params + params.require(:asset).permit(:view_mode) + end + def asset_data_type(asset) return 'wopi' if wopi_file?(asset) return 'image' if asset.image? diff --git a/app/controllers/marvin_js_assets_controller.rb b/app/controllers/marvin_js_assets_controller.rb index aa4c1e8a9..ed5c7745d 100644 --- a/app/controllers/marvin_js_assets_controller.rb +++ b/app/controllers/marvin_js_assets_controller.rb @@ -17,15 +17,10 @@ class MarvinJsAssetsController < ApplicationController 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 } - ) + html: render_to_string(partial: 'assets/asset.html.erb', locals: { + asset: result[:asset], + gallery_view_id: marvin_params[:object_id] + }) } elsif result[:asset] && marvin_params[:object_type] == 'Result' @my_module = result[:object].my_module @@ -63,7 +58,7 @@ class MarvinJsAssetsController < ApplicationController private def load_vars - @asset = current_team.assets.find_by_id(params[:id]) + @asset = current_team.assets.find_by(id: params[:id]) return render_404 unless @asset @assoc ||= @asset.step @@ -77,8 +72,8 @@ class MarvinJsAssetsController < ApplicationController 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' + @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 diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index fd10d4667..13feb9de2 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -5,11 +5,11 @@ class StepsController < ApplicationController include MarvinJsActions before_action :load_vars, only: %i(edit update destroy show toggle_step_state checklistitem_state update_view_state - move_up move_down) + move_up move_down update_asset_view_mode) before_action :load_vars_nested, only: %i(new create) before_action :convert_table_contents_to_utf8, only: %i(create update) - before_action :check_view_permissions, only: %i(show update_view_state) + before_action :check_view_permissions, only: %i(show update_view_state update_asset_view_mode) before_action :check_manage_permissions, only: %i(new create edit update destroy move_up move_down toggle_step_state) before_action :check_complete_and_checkbox_permissions, only: %i(toggle_step_state checklistitem_state) @@ -145,7 +145,12 @@ class StepsController < ApplicationController step_params_all[:assets_attributes]&.each do |key, value| next unless value[:signed_blob_id] - new_asset = @step.assets.create!(created_by: current_user, last_modified_by: current_user, team: current_team) + new_asset = @step.assets.create!( + created_by: current_user, + last_modified_by: current_user, + team: current_team, + view_mode: @step.assets_view_mode + ) new_asset.file .attach(value[:signed_blob_id]) new_assets.push(new_asset.id) @@ -218,6 +223,25 @@ class StepsController < ApplicationController end end + def update_asset_view_mode + html = '' + ActiveRecord::Base.transaction do + @step.assets_view_mode = params[:assets_view_mode] + @step.save!(touch: false) + @step.assets.update_all(view_mode: @step.assets_view_mode) + end + @step.assets.each do |asset| + html += render_to_string(partial: 'assets/asset.html.erb', locals: { + asset: asset, + gallery_view_id: @step.id + }) + end + render json: { html: html }, status: :ok + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error(e.message) + render json: { errors: e.message }, status: :unprocessable_entity + end + def destroy if @step.can_destroy? diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb index de1f9a26b..b0e4d811e 100644 --- a/app/controllers/wopi_controller.rb +++ b/app/controllers/wopi_controller.rb @@ -334,7 +334,7 @@ class WopiController < ActionController::Base url = request.original_url.upcase.encode('utf-8') if convert_to_unix_timestamp(timestamp) + 20.minutes >= Time.now - if current_wopi_discovery.verify_proof(token, timestamp, signed_proof, signed_proof_old, url) + if wopi_verify_proof(token, timestamp, signed_proof, signed_proof_old, url) logger.warn 'WOPI: proof verification: successful' else logger.warn 'WOPI: proof verification: not verified' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 579b70df2..5c48e7ef8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -186,4 +186,23 @@ module ApplicationHelper def wopi_enabled? ENV['WOPI_ENABLED'] == 'true' end + + # Check whether the wopi file can be edited and return appropriate response + def wopi_file_edit_button_status(asset) + file_ext = asset.file_name.split('.').last + if Constants::WOPI_EDITABLE_FORMATS.include?(file_ext) + edit_supported = true + title = '' + else + edit_supported = false + title = if Constants::FILE_TEXT_FORMATS.include?(file_ext) + I18n.t('assets.wopi_supported_text_formats_title') + elsif Constants::FILE_TABLE_FORMATS.include?(file_ext) + I18n.t('assets.wopi_supported_table_formats_title') + else + I18n.t('assets.wopi_supported_presentation_formats_title') + end + end + return edit_supported, title + end end diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb index 23f96ffc3..269eed8bd 100644 --- a/app/helpers/reports_helper.rb +++ b/app/helpers/reports_helper.rb @@ -99,9 +99,9 @@ module ReportsHelper # "Hack" to omit file preview URL because of WKHTML issues def report_image_asset_url(asset) - image_tag(asset.medium_preview - .processed - .service_url(expires_in: Constants::URL_LONG_EXPIRE_TIME)) + preview = asset.inline? ? asset.large_preview : asset.medium_preview + image_tag(preview.processed + .service_url(expires_in: Constants::URL_LONG_EXPIRE_TIME)) end # "Hack" to load Glyphicons css directly from the CDN diff --git a/app/models/asset.rb b/app/models/asset.rb index ea036f300..909b9b7c6 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -12,6 +12,8 @@ class Asset < ApplicationRecord # Lock duration set to 30 minutes LOCK_DURATION = 60 * 30 + enum view_mode: { thumbnail: 0, inline: 1, list: 2 } + # ActiveStorage configuration has_one_attached :file @@ -315,7 +317,7 @@ class Asset < ApplicationRecord file_ext = file_name.split('.').last action = get_action(file_ext, action) if !action.nil? - action_url = action.urlsrc + action_url = action[:urlsrc] if ENV['WOPI_BUSINESS_USERS'] && ENV['WOPI_BUSINESS_USERS'] == 'true' action_url = action_url.gsub(//, 'IsLicensedUser=1&') @@ -349,7 +351,7 @@ class Asset < ApplicationRecord def favicon_url(action) file_ext = file_name.split('.').last action = get_action(file_ext, action) - action.wopi_app.icon if action.try(:wopi_app) + action[:icon] if action[:icon] end # locked?, lock_asset and refresh_lock rely on the asset diff --git a/app/models/step.rb b/app/models/step.rb index 90eb0cd38..cf36a81d0 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -4,6 +4,8 @@ class Step < ApplicationRecord include TinyMceImages include ViewableModel + enum assets_view_mode: { thumbnail: 0, inline: 1, list: 2 } + auto_strip_attributes :name, :description, nullify: false validates :name, presence: true, diff --git a/app/models/wopi_action.rb b/app/models/wopi_action.rb index 9a51b2b2e..2eff2690f 100644 --- a/app/models/wopi_action.rb +++ b/app/models/wopi_action.rb @@ -4,9 +4,4 @@ class WopiAction < ApplicationRecord belongs_to :wopi_app, foreign_key: 'wopi_app_id', class_name: 'WopiApp' validates :action, :extension, :urlsrc, :wopi_app, presence: true - - def self.find_action(extension, activity) - WopiAction.distinct - .where('extension = ? and action = ?', extension, activity).first - end end diff --git a/app/models/wopi_discovery.rb b/app/models/wopi_discovery.rb index 45bffd41a..6ce192d15 100644 --- a/app/models/wopi_discovery.rb +++ b/app/models/wopi_discovery.rb @@ -13,43 +13,4 @@ class WopiDiscovery < ApplicationRecord :proof_key_old_mod, :proof_key_old_exp, presence: true - - # Verifies if proof from headers, X-WOPI-Proof/X-WOPI-OldProof was encrypted - # with this discovery public key (two key possible old/new) - def verify_proof(token, timestamp, signed_proof, signed_proof_old, url) - token_length = [token.length].pack('>N').bytes - timestamp_bytes = [timestamp.to_i].pack('>Q').bytes.reverse - timestamp_length = [timestamp_bytes.length].pack('>N').bytes - url_length = [url.length].pack('>N').bytes - - expected_proof = token_length + token.bytes + - url_length + url.upcase.bytes + - timestamp_length + timestamp_bytes - - key = generate_key(proof_key_mod, proof_key_exp) - old_key = generate_key(proof_key_old_mod, proof_key_old_exp) - - # Try all possible combiniations - try_verification(expected_proof, signed_proof, key) || - try_verification(expected_proof, signed_proof_old, key) || - try_verification(expected_proof, signed_proof, old_key) - end - - # Generates a public key from given modulus and exponent - def generate_key(modulus, exponent) - mod = Base64.decode64(modulus).unpack('H*').first.to_i(16) - exp = Base64.decode64(exponent).unpack('H*').first.to_i(16) - - seq = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::Integer.new(mod), - OpenSSL::ASN1::Integer.new(exp)]) - OpenSSL::PKey::RSA.new(seq.to_der) - end - - # Verify if decrypting signed_proof with public_key equals to expected_proof - def try_verification(expected_proof, signed_proof_b64, public_key) - signed_proof = Base64.decode64(signed_proof_b64) - public_key.verify(OpenSSL::Digest::SHA256.new, signed_proof, - expected_proof.pack('c*')) - end - end diff --git a/app/permissions/asset.rb b/app/permissions/asset.rb new file mode 100644 index 000000000..482c56a8a --- /dev/null +++ b/app/permissions/asset.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +Canaid::Permissions.register_for(Asset) do + can :read_asset do |user, asset| + object = asset.step || asset.result || asset.repository_cell + + case object + when Step + protocol = object.protocol + can_read_protocol_in_module?(user, protocol) || can_read_protocol_in_repository?(user, protocol) + when Result + can_read_experiment?(user, object.my_module.experiment) + when RepositoryCell + can_read_repository?(user, object.repository_column.repository) + end + end + + can :manage_asset do |user, asset| + object = asset.step || asset.result || asset.repository_cell + + case object + when Step + protocol = object.protocol + can_manage_protocol_in_module?(user, protocol) || can_manage_protocol_in_repository?(user, protocol) + when Result + can_manage_experiment?(user, object.my_module.experiment) + when RepositoryCell + return false if object.repository_column.repository.is_a?(RepositorySnapshot) + + can_manage_repository?(user, object.repository_column.repository) + end + end +end diff --git a/app/services/reports/docx/draw_result_asset.rb b/app/services/reports/docx/draw_result_asset.rb index fb5ffa88d..5edb70978 100644 --- a/app/services/reports/docx/draw_result_asset.rb +++ b/app/services/reports/docx/draw_result_asset.rb @@ -7,17 +7,22 @@ module Reports::Docx::DrawResultAsset asset = result.asset timestamp = asset.created_at + asset_url = Rails.application.routes.url_helpers.asset_download_url(asset) color = @color @docx.p @docx.p do text result.name, italic: true + text ' ' + link I18n.t('projects.reports.elements.download'), asset_url do + italic true + end text ' ' + I18n.t('search.index.archived'), color: color[:gray] if result.archived? text ' ' + I18n.t('projects.reports.elements.result_asset.file_name', file: asset.file_name) text ' ' + I18n.t('projects.reports.elements.result_asset.user_time', user: result.user.full_name, timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] end - Reports::DocxRenderer.render_asset_image(@docx, asset) if asset.image? + Reports::DocxRenderer.render_asset_image(@docx, asset) if asset.previewable? && !asset.list? subject['children'].each do |child| public_send("draw_#{child['type_of']}", child, result) diff --git a/app/services/reports/docx/draw_step_asset.rb b/app/services/reports/docx/draw_step_asset.rb index 80a87c8da..657148833 100644 --- a/app/services/reports/docx/draw_step_asset.rb +++ b/app/services/reports/docx/draw_step_asset.rb @@ -6,15 +6,20 @@ module Reports::Docx::DrawStepAsset return unless asset timestamp = asset.created_at + asset_url = Rails.application.routes.url_helpers.asset_download_url(asset) color = @color @docx.p @docx.p do text (I18n.t 'projects.reports.elements.step_asset.file_name', file: asset.file_name), italic: true text ' ' + link I18n.t('projects.reports.elements.download'), asset_url do + italic true + end + text ' ' text I18n.t('projects.reports.elements.step_asset.user_time', timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] end - Reports::DocxRenderer.render_asset_image(@docx, asset) if asset.image? + Reports::DocxRenderer.render_asset_image(@docx, asset) if asset.previewable? && !asset.list? end end diff --git a/app/services/reports/docx_renderer.rb b/app/services/reports/docx_renderer.rb index 7defe7b73..3dba735e9 100644 --- a/app/services/reports/docx_renderer.rb +++ b/app/services/reports/docx_renderer.rb @@ -141,9 +141,9 @@ module Reports def self.render_asset_image(docx, asset) return unless asset - image_path = Reports::Utils.image_path(asset.file) + asset_preview = Reports::Utils.image_prepare(asset) - dimension = FastImage.size(image_path) + dimension = FastImage.size(asset_preview.service_url) return unless dimension x = dimension[0] @@ -152,8 +152,14 @@ module Reports y = y * 300 / x x = 300 end - docx.img image_path.split('&')[0] do - data asset.blob.download + blob_data = if asset_preview.class == ActiveStorage::Preview + asset_preview.image.download + else + asset_preview.blob.download + end + + docx.img asset_preview.service_url.split('&')[0] do + data blob_data width x height y end diff --git a/app/services/reports/html_to_word_converter.rb b/app/services/reports/html_to_word_converter.rb index d04e7e1a1..b4ccc55cb 100644 --- a/app/services/reports/html_to_word_converter.rb +++ b/app/services/reports/html_to_word_converter.rb @@ -121,7 +121,7 @@ module Reports image = TinyMceAsset.find_by(id: Base62.decode(elem.attributes['data-mce-token'].value)) return unless image - image_path = Reports::Utils.image_path(image.image) + image_path = Reports::Utils.image_prepare(image).service_url dimension = FastImage.size(image_path) return unless dimension diff --git a/app/services/reports/utils.rb b/app/services/reports/utils.rb index b61f1cf1c..51c3ad339 100644 --- a/app/services/reports/utils.rb +++ b/app/services/reports/utils.rb @@ -6,8 +6,16 @@ module Reports link[0] == '/' ? scinote_url + link : link end - def self.image_path(attachment) - attachment.service_url + def self.image_prepare(asset) + if asset.class == Asset + if asset.inline? + asset.large_preview + else + asset.medium_preview + end + elsif asset.class == TinyMceAsset + asset.image + end end def self.calculate_color_hsp(color) diff --git a/app/utilities/wopi_util.rb b/app/utilities/wopi_util.rb index 2ed5ebb9e..520cee3ee 100644 --- a/app/utilities/wopi_util.rb +++ b/app/utilities/wopi_util.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module WopiUtil require 'open-uri' @@ -5,72 +7,101 @@ module WopiUtil UNIX_EPOCH_IN_CLR_TICKS = 621355968000000000 CLR_TICKS_PER_SECOND = 10000000 - DISCOVERY_TTL = 1.days + DISCOVERY_TTL = 1.day DISCOVERY_TTL.freeze # For more explanation see this: # http://stackoverflow.com/questions/11888053/ # convert-net-datetime-ticks-property-to-date-in-objective-c def convert_to_unix_timestamp(timestamp) - Time.at((timestamp - UNIX_EPOCH_IN_CLR_TICKS) / CLR_TICKS_PER_SECOND) + Time.zone.at((timestamp - UNIX_EPOCH_IN_CLR_TICKS) / CLR_TICKS_PER_SECOND) end - def get_action(extension, activity) - current_wopi_discovery - WopiAction.find_action(extension, activity) + def get_action(extension, action) + discovery = current_wopi_discovery + discovery[:actions].find { |i| i[:extension] == extension && i[:action] == action } end def current_wopi_discovery - discovery = WopiDiscovery.first - return discovery if discovery && discovery.expires >= Time.now.to_i - initialize_discovery(discovery) + initialize_discovery + end + + # Verifies if proof from headers, X-WOPI-Proof/X-WOPI-OldProof was encrypted + # with this discovery public key (two key possible old/new) + def wopi_verify_proof(token, timestamp, signed_proof, signed_proof_old, url) + discovery = current_wopi_discovery + token_length = [token.length].pack('>N').bytes + timestamp_bytes = [timestamp.to_i].pack('>Q').bytes.reverse + timestamp_length = [timestamp_bytes.length].pack('>N').bytes + url_length = [url.length].pack('>N').bytes + + expected_proof = token_length + token.bytes + + url_length + url.upcase.bytes + + timestamp_length + timestamp_bytes + + key = generate_key(discovery[:proof_key_mod], discovery[:proof_key_exp]) + old_key = generate_key(discovery[:proof_key_old_mod], discovery[:proof_key_old_exp]) + + # Try all possible combiniations + try_verification(expected_proof, signed_proof, key) || + try_verification(expected_proof, signed_proof_old, key) || + try_verification(expected_proof, signed_proof, old_key) end private # Currently only saves Excel, Word and PowerPoint view and edit actions - def initialize_discovery(discovery) - Rails.logger.warn 'Initializing discovery' - discovery.destroy if discovery + def initialize_discovery + Rails.cache.fetch(:wopi_discovery, expires_in: DISCOVERY_TTL) do + @doc = Nokogiri::XML(Kernel.open(ENV['WOPI_DISCOVERY_URL'])) + discovery_json = {} + key = @doc.xpath('//proof-key') + discovery_json[:proof_key_mod] = key.xpath('@modulus').first.value + discovery_json[:proof_key_exp] = key.xpath('@exponent').first.value + discovery_json[:proof_key_old_mod] = key.xpath('@oldmodulus').first.value + discovery_json[:proof_key_old_exp] = key.xpath('@oldexponent').first.value + discovery_json[:actions] = [] - @doc = Nokogiri::XML(Kernel.open(ENV['WOPI_DISCOVERY_URL'])) + @doc.xpath('//app').each do |app| + app_name = app.xpath('@name').first.value + next unless %w(Excel Word PowerPoint WopiTest).include?(app_name) - discovery = WopiDiscovery.new - discovery.expires = Time.now.to_i + DISCOVERY_TTL - key = @doc.xpath('//proof-key') - discovery.proof_key_mod = key.xpath('@modulus').first.value - discovery.proof_key_exp = key.xpath('@exponent').first.value - discovery.proof_key_old_mod = key.xpath('@oldmodulus').first.value - discovery.proof_key_old_exp = key.xpath('@oldexponent').first.value - discovery.save! + icon = app.xpath('@favIconUrl').first.value - @doc.xpath('//app').each do |app| - app_name = app.xpath('@name').first.value - next unless %w(Excel Word PowerPoint WopiTest).include?(app_name) + app.xpath('action').each do |action| + action_name = action.xpath('@name').first.value + next unless %w(view edit editnew embedview wopitest).include?(action_name) - wopi_app = WopiApp.new - wopi_app.name = app.xpath('@name').first.value - wopi_app.icon = app.xpath('@favIconUrl').first.value - wopi_app.wopi_discovery_id = discovery.id - wopi_app.save! - app.xpath('action').each do |action| - name = action.xpath('@name').first.value - next unless %w(view edit editnew wopitest).include?(name) - - wopi_action = WopiAction.new - wopi_action.action = name - wopi_action.extension = action.xpath('@ext').first.value - wopi_action.urlsrc = action.xpath('@urlsrc').first.value - wopi_action.wopi_app_id = wopi_app.id - wopi_action.save! + action_json = {} + action_json[:icon] = icon + action_json[:action] = action_name + action_json[:extension] = action.xpath('@ext').first.value + action_json[:urlsrc] = action.xpath('@urlsrc').first.value + discovery_json[:actions].push(action_json) + end end + discovery_json end - discovery - rescue => e + rescue StandardError => e Rails.logger.warn 'WOPI: initialization failed: ' + e.message e.backtrace.each { |line| Rails.logger.error line } - discovery = WopiDiscovery.first - discovery.destroy if discovery + end + + # Generates a public key from given modulus and exponent + def generate_key(modulus, exponent) + mod = Base64.decode64(modulus).unpack1('H*').to_i(16) + exp = Base64.decode64(exponent).unpack1('H*').to_i(16) + + seq = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::Integer.new(mod), + OpenSSL::ASN1::Integer.new(exp)]) + OpenSSL::PKey::RSA.new(seq.to_der) + end + + # Verify if decrypting signed_proof with public_key equals to expected_proof + def try_verification(expected_proof, signed_proof_b64, public_key) + signed_proof = Base64.decode64(signed_proof_b64) + public_key.verify(OpenSSL::Digest::SHA256.new, signed_proof, + expected_proof.pack('c*')) end def create_wopi_file_activity(current_user, started_editing) diff --git a/app/views/assets/_asset.html.erb b/app/views/assets/_asset.html.erb new file mode 100644 index 000000000..ce712b9c4 --- /dev/null +++ b/app/views/assets/_asset.html.erb @@ -0,0 +1,19 @@ +<% +gallery_view_id = nil unless defined? gallery_view_id +deletable = asset.step.present? +editable = true + +partial_locals = { + asset: asset, + gallery_view_id: gallery_view_id, + deletable: deletable, + editable: editable +} +%> +<% if asset.inline? %> + <%= render partial: 'assets/asset_inline.html.erb', locals: partial_locals %> +<% elsif asset.list? %> + <%= render partial: 'assets/asset_list.html.erb', locals: partial_locals %> +<% else %> + <%= render partial: 'assets/asset_thumbnail.html.erb', locals: partial_locals %> +<% end %> diff --git a/app/views/assets/_asset_context_menu.html.erb b/app/views/assets/_asset_context_menu.html.erb new file mode 100644 index 000000000..ff3d87ee4 --- /dev/null +++ b/app/views/assets/_asset_context_menu.html.erb @@ -0,0 +1,70 @@ + diff --git a/app/views/assets/_asset_inline.html.erb b/app/views/assets/_asset_inline.html.erb new file mode 100644 index 000000000..67fe6a5bf --- /dev/null +++ b/app/views/assets/_asset_inline.html.erb @@ -0,0 +1,45 @@ +
+
+
+ <%= link_to rails_blob_path(asset.file, disposition: 'attachment'), + class: "file-preview-link file-name", + id: "modal_link#{asset.id}", + data: { + no_turbolink: true, + id: asset.id, + gallery_view_id: gallery_view_id, + preview_url: asset_file_preview_path(asset) + } do %> + <% if asset.file.attached? && asset&.file&.metadata['asset_type'] %> + <%= asset.file.metadata['name'] %> + <% else %> + <%= asset.file_name %> + <% end %> + <% end %> + +
+ <%= render partial: 'assets/asset_context_menu.html.erb', locals: { asset: asset, deletable: deletable, editable: editable } %> +
+ <% if wopi_enabled? && wopi_file?(asset) %> +
+ <% elsif asset.previewable? %> +
+ <%= image_tag asset.large_preview, + onerror: 'ActiveStoragePreviews.reCheckPreview(event)', + onload: 'ActiveStoragePreviews.showPreview(event)', + style: 'opacity: 0' %> +
+ <% else %> +
+ +
+ <% end %> +
diff --git a/app/views/shared/_asset_link.html.erb b/app/views/assets/_asset_link.html.erb similarity index 100% rename from app/views/shared/_asset_link.html.erb rename to app/views/assets/_asset_link.html.erb diff --git a/app/views/assets/_asset_list.html.erb b/app/views/assets/_asset_list.html.erb new file mode 100644 index 000000000..88d5f675b --- /dev/null +++ b/app/views/assets/_asset_list.html.erb @@ -0,0 +1,30 @@ +
+
+ +
+ <%= link_to rails_blob_path(asset.file, disposition: 'attachment'), + class: "file-preview-link file-name", + id: "modal_link#{asset.id}", + data: { + no_turbolink: true, + id: asset.id, + gallery_view_id: gallery_view_id, + preview_url: asset_file_preview_path(asset) + } do %> + <% if asset.file.attached? && asset&.file&.metadata['asset_type'] %> + <%= asset.file.metadata['name'] %> + <% else %> + <%= asset.file_name %> + <% end %> + <% end %> + + <%= render partial: 'assets/asset_context_menu.html.erb', locals: { asset: asset, deletable: deletable, editable: editable } %> +
diff --git a/app/views/assets/_asset_thumbnail.html.erb b/app/views/assets/_asset_thumbnail.html.erb new file mode 100644 index 000000000..59106809b --- /dev/null +++ b/app/views/assets/_asset_thumbnail.html.erb @@ -0,0 +1,39 @@ +
+ <%= link_to rails_blob_path(asset.file, disposition: 'attachment'), + class: "file-preview-link", + id: "modal_link#{asset.id}", + data: { + no_turbolink: true, + id: asset.id, + gallery_view_id: gallery_view_id, + preview_url: asset_file_preview_path(asset) + } do %> +
+ <% if asset.previewable? %> + <%= image_tag asset.medium_preview, + onerror: 'ActiveStoragePreviews.reCheckPreview(event)', + onload: 'ActiveStoragePreviews.showPreview(event)', + style: 'opacity: 0' %> + <% else %> + + <% end %> +
+
+ <% if asset.file.attached? && asset&.file&.metadata['asset_type'] %> + <%= asset.file.metadata['name'] %> + <% else %> + <%= asset.file_name %> + <% end %> +
+ + <% end %> + <%= render partial: 'assets/asset_context_menu.html.erb', locals: { asset: asset, deletable: deletable, editable: editable } %> +
diff --git a/app/views/steps/attachments/_new_attachment.html.erb b/app/views/assets/_new_asset.html.erb similarity index 57% rename from app/views/steps/attachments/_new_attachment.html.erb rename to app/views/assets/_new_asset.html.erb index a7bef3902..9425e6928 100644 --- a/app/views/steps/attachments/_new_attachment.html.erb +++ b/app/views/assets/_new_asset.html.erb @@ -1,9 +1,9 @@ -
-
+
+
<%= truncate(file_name || file_url, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
-
+
diff --git a/app/views/assets/marvinjs/_create_marvin_sketch_button.html.erb b/app/views/assets/marvinjs/_create_marvin_sketch_button.html.erb deleted file mode 100644 index dcb4baa54..000000000 --- a/app/views/assets/marvinjs/_create_marvin_sketch_button.html.erb +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/app/views/assets/marvinjs/_create_marvin_sketch_li.html.erb b/app/views/assets/marvinjs/_create_marvin_sketch_li.html.erb index 4941f2e09..bb6877709 100644 --- a/app/views/assets/marvinjs/_create_marvin_sketch_li.html.erb +++ b/app/views/assets/marvinjs/_create_marvin_sketch_li.html.erb @@ -1,12 +1,14 @@ -
  • - - <%= image_tag 'icon_small/marvinjs.svg' %> - - <%= t('marvinjs.new_li_button') %> +
  • + + + <%= image_tag 'icon_small/marvinjs.svg' %> + + <%= t('marvinjs.new_li_button') %> +
  • diff --git a/app/views/assets/wopi/_create_wopi_file_button.html.erb b/app/views/assets/wopi/_create_wopi_file_button.html.erb deleted file mode 100644 index bf8fb53a5..000000000 --- a/app/views/assets/wopi/_create_wopi_file_button.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% if wopi_enabled? %> - <%= link_to create_wopi_file_path, - class: 'btn btn-light create-wopi-file-btn', - target: '_blank', - title: 'Create_new_file', - data: { 'id': element_id, 'type': element_type, } do %> - - <%= image_tag 'office/office.svg' %> - <%=t 'assets.create_wopi_file.button_text' %> - - <% end %> -<% end %> diff --git a/app/views/assets/wopi/_file_wopi_controls.html.erb b/app/views/assets/wopi/_file_wopi_controls.html.erb index 836c91b72..f86e59554 100644 --- a/app/views/assets/wopi/_file_wopi_controls.html.erb +++ b/app/views/assets/wopi/_file_wopi_controls.html.erb @@ -1,28 +1,18 @@ -<%= link_to view_asset_url(id: asset), - id: 'wopi_file_view_button', - class: 'btn btn-default btn-sm', - target: '_blank', - style: 'display: inline-block' do %> - <%= file_application_icon(asset) %> - <%= wopi_button_text(asset, 'view') %> -<% end %> -<% if can_edit && edit_supported %> +<% if edit_supported %> <%= link_to edit_asset_url(id: asset), id: 'wopi_file_edit_button', - class: 'btn btn-default btn-sm', - target: '_blank', - style: 'display: inline-block' do %> + class: 'btn btn-light', + target: '_blank' do %> <%= file_application_icon(asset) %> <%= wopi_button_text(asset, 'edit') %> <% end %> -<% elsif can_edit %> +<% else %> <%= link_to edit_asset_url(id: asset), id: 'wopi_file_edit_button', - class: 'btn btn-default btn-sm', + class: 'btn btn-light', target: '_blank', title: title, - disabled: true, - style: 'display: inline-block' do %> + disabled: true do %> <%= file_application_icon(asset) %> <%= wopi_button_text(asset, 'edit') %> <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f83f49d62..e01e1f280 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -46,7 +46,7 @@ <%= render "shared/about_modal" %> - <%= render "shared/file_preview_modal.html.erb" %> + <%= render "shared/file_preview/modal.html.erb" %> <%= render "shared/file_edit_modal.html.erb" %> <%= render "shared/marvinjs_modal.html.erb" %> <%= render "shared/navigation" %> diff --git a/app/views/my_modules/_result_user_generated.html.erb b/app/views/my_modules/_result_user_generated.html.erb index b00aa5eaf..dfd3a9d68 100644 --- a/app/views/my_modules/_result_user_generated.html.erb +++ b/app/views/my_modules/_result_user_generated.html.erb @@ -3,6 +3,5 @@ <% elsif result.is_table %> <%= render partial: "results/result_table.html.erb", locals: {result: result} %> <% elsif result.is_asset %> - <%= render partial: 'steps/attachments/item.html.erb', - locals: { asset: result.asset, i: 0, assets_count: 0, step: nil, order_atoz: 0, order_ztoa: 0 } %> + <%= render partial: 'assets/asset.html.erb', locals: { asset: result.asset, gallery_view_id: result.my_module.id } %> <% end %> diff --git a/app/views/my_modules/archive.html.erb b/app/views/my_modules/archive.html.erb index 5b5bed9bd..1ccad0796 100644 --- a/app/views/my_modules/archive.html.erb +++ b/app/views/my_modules/archive.html.erb @@ -17,5 +17,3 @@ <% end %>
    - - diff --git a/app/views/reports/elements/_my_module_result_asset_element.html.erb b/app/views/reports/elements/_my_module_result_asset_element.html.erb index 3ff08a35a..a90520949 100644 --- a/app/views/reports/elements/_my_module_result_asset_element.html.erb +++ b/app/views/reports/elements/_my_module_result_asset_element.html.erb @@ -1,9 +1,8 @@ <% result ||= @result %> <% asset = result.asset %> -<% is_image = result.asset.image? %> <% comments = result.result_comments %> <% timestamp = asset.created_at %> -<% icon_class = 'fas ' + (is_image ? 'fa-image' : 'fa-file') %> +<% icon_class = 'fas ' + file_fa_icon_class(asset) if asset.file_name %>
    <%= t("projects.reports.elements.result_asset.file_name", file: filename) %> <% else %> - <%= t("projects.reports.elements.result_asset.file_name", - file: truncate(asset.file_name, length: Constants::FILENAME_TRUNCATION_LENGTH)) %> + + <%= t("projects.reports.elements.result_asset.file_name", + file: truncate(asset.file_name, length: Constants::FILENAME_TRUNCATION_LENGTH)) %> + + <%= t('projects.reports.elements.download') %> + + <% end %>
    @@ -42,7 +46,7 @@
    - <% if is_image %> + <% if asset.previewable? && !asset.list? %>
    diff --git a/app/views/reports/elements/_step_asset_element.html.erb b/app/views/reports/elements/_step_asset_element.html.erb index ef768927b..321af96b7 100644 --- a/app/views/reports/elements/_step_asset_element.html.erb +++ b/app/views/reports/elements/_step_asset_element.html.erb @@ -1,23 +1,26 @@ <% asset ||= @asset %> -<% is_image = asset.image? %> <% timestamp = asset.created_at %> -<% icon_class = 'fas ' + (is_image ? 'fa-image' : 'fa-file') %> +<% icon_class = file_fa_icon_class(asset) if asset.file_name %>
    " data-icon-class="<%= icon_class %>">
    - +
    <% if defined? export_all and export_all %> - <%=t 'projects.reports.elements.step_asset.file_name', - file: filename %> + <%= t('projects.reports.elements.step_asset.file_name', file: filename) %> <% else %> - <%=t 'projects.reports.elements.step_asset.file_name', - file: truncate(asset.file_name, - length: Constants::FILENAME_TRUNCATION_LENGTH) %> + + <%= t('projects.reports.elements.step_asset.file_name', + file: truncate(asset.file_name, length: Constants::FILENAME_TRUNCATION_LENGTH)) %> + + <%= t('projects.reports.elements.download') %> + + + <% end %>
    @@ -29,7 +32,7 @@
    - <% if is_image %> + <% if asset.previewable? && !asset.list? %>
    <% if defined?(export_all) && export_all %> diff --git a/app/views/repositories/_repository_row_info_modal.html.erb b/app/views/repositories/_repository_row_info_modal.html.erb index 0529b29f5..860cf6d59 100644 --- a/app/views/repositories/_repository_row_info_modal.html.erb +++ b/app/views/repositories/_repository_row_info_modal.html.erb @@ -93,7 +93,6 @@ <% end %> <% end %>
    - diff --git a/app/views/shared/_file_edit_modal.html.erb b/app/views/shared/_file_edit_modal.html.erb index 74eced9b4..a57db6bfd 100644 --- a/app/views/shared/_file_edit_modal.html.erb +++ b/app/views/shared/_file_edit_modal.html.erb @@ -8,11 +8,15 @@