mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-25 17:24:51 +08:00
Merge pull request #8053 from scinote-eln/develop
November 2024 Release
This commit is contained in:
commit
4c57d97f77
165 changed files with 2205 additions and 795 deletions
7
Gemfile
7
Gemfile
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'http://rubygems.org'
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '~> 3.2.2'
|
||||
|
||||
|
@ -13,7 +13,7 @@ gem 'pg', '~> 1.5'
|
|||
gem 'pg_search' # PostgreSQL full text search
|
||||
gem 'psych', '< 4.0'
|
||||
gem 'rails', '~> 7.0.8'
|
||||
gem 'recaptcha', require: 'recaptcha/rails'
|
||||
gem 'recaptcha'
|
||||
gem 'sanitize'
|
||||
gem 'sprockets-rails'
|
||||
gem 'view_component'
|
||||
|
@ -47,8 +47,7 @@ gem 'aspector' # Aspect-oriented programming for Rails
|
|||
gem 'auto_strip_attributes', '~> 2.1' # Removes unnecessary whitespaces AR
|
||||
gem 'bcrypt', '~> 3.1.10'
|
||||
# gem 'caracal'
|
||||
gem 'caracal',
|
||||
git: 'https://github.com/scinote-eln/caracal.git', branch: 'rubyzip2' # Build docx report
|
||||
gem 'caracal', git: 'https://github.com/scinote-eln/caracal.git', branch: 'custom-docx-reports' # Build docx report
|
||||
gem 'caxlsx' # Build XLSX files
|
||||
gem 'deface', '~> 1.9'
|
||||
gem 'down', '~> 5.0'
|
||||
|
|
137
Gemfile.lock
137
Gemfile.lock
|
@ -16,10 +16,10 @@ GIT
|
|||
|
||||
GIT
|
||||
remote: https://github.com/scinote-eln/caracal.git
|
||||
revision: f8e4c279adfee7801eb1024e1d6a18bb06c9c76a
|
||||
branch: rubyzip2
|
||||
revision: 54c21353798569476a1eaa73b5fd3e275ac85419
|
||||
branch: custom-docx-reports
|
||||
specs:
|
||||
caracal (1.4.1)
|
||||
caracal (1.4.2)
|
||||
nokogiri (~> 1.6)
|
||||
rubyzip (>= 2.3)
|
||||
tilt (>= 1.4)
|
||||
|
@ -50,49 +50,49 @@ GIT
|
|||
mime-types (>= 1.23)
|
||||
|
||||
GEM
|
||||
remote: http://rubygems.org/
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
actioncable (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
actionmailbox (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
actionmailer (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
actionpack (7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
actiontext (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
actionview (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -102,14 +102,14 @@ GEM
|
|||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activejob (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activerecord (7.0.8.4)
|
||||
activemodel (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activemodel (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activerecord (7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activerecord-import (1.4.1)
|
||||
activerecord (>= 4.2)
|
||||
activerecord-session_store (2.1.0)
|
||||
|
@ -119,14 +119,14 @@ GEM
|
|||
multi_json (~> 1.11, >= 1.11.2)
|
||||
rack (>= 2.0.8, < 4)
|
||||
railties (>= 6.1)
|
||||
activestorage (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activestorage (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8.4)
|
||||
activesupport (7.0.8.5)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -428,7 +428,7 @@ GEM
|
|||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.2)
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
|
@ -447,20 +447,17 @@ GEM
|
|||
rails (>= 3.2.0)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.10)
|
||||
net-imap (0.4.17)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.4.0.1)
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
newrelic_rpm (9.2.2)
|
||||
newrelic_rpm (9.14.0)
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
|
@ -547,7 +544,7 @@ GEM
|
|||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.9)
|
||||
rack (2.2.10)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (2.0.2)
|
||||
|
@ -564,20 +561,20 @@ GEM
|
|||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.8.4)
|
||||
actioncable (= 7.0.8.4)
|
||||
actionmailbox (= 7.0.8.4)
|
||||
actionmailer (= 7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
actiontext (= 7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activemodel (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
rails (7.0.8.5)
|
||||
actioncable (= 7.0.8.5)
|
||||
actionmailbox (= 7.0.8.5)
|
||||
actionmailer (= 7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actiontext (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8.4)
|
||||
railties (= 7.0.8.5)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
|
@ -598,9 +595,9 @@ GEM
|
|||
railties (> 3.1)
|
||||
rails_serve_static_assets (0.0.5)
|
||||
rails_stdout_logging (0.0.5)
|
||||
railties (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
railties (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
|
@ -611,12 +608,12 @@ GEM
|
|||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rdoc (6.3.4.1)
|
||||
recaptcha (5.14.0)
|
||||
recaptcha (5.17.0)
|
||||
regexp_parser (2.8.1)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.3.7)
|
||||
rexml (3.3.9)
|
||||
rgl (0.6.3)
|
||||
pairing_heap (>= 0.3.0)
|
||||
rexml (~> 3.2, >= 3.2.4)
|
||||
|
@ -715,14 +712,12 @@ GEM
|
|||
faraday-follow_redirects
|
||||
sys-uname (1.2.3)
|
||||
ffi (~> 1.1)
|
||||
tailwindcss-rails (2.4.0)
|
||||
railties (>= 6.0.0)
|
||||
tailwindcss-rails (2.4.0-arm64-darwin)
|
||||
railties (>= 6.0.0)
|
||||
tailwindcss-rails (2.4.0-x86_64-linux)
|
||||
railties (>= 6.0.0)
|
||||
thor (1.3.1)
|
||||
tilt (2.2.0)
|
||||
tilt (2.4.0)
|
||||
timecop (0.9.6)
|
||||
timeout (0.4.1)
|
||||
turbolinks (5.2.1)
|
||||
|
@ -776,7 +771,6 @@ GEM
|
|||
|
||||
PLATFORMS
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
|
@ -810,7 +804,6 @@ DEPENDENCIES
|
|||
deface (~> 1.9)
|
||||
delayed_job_active_record
|
||||
devise (~> 4.8.1)
|
||||
devise-async!
|
||||
devise_invitable
|
||||
discard
|
||||
doorkeeper (>= 4.6)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.37.0
|
||||
1.38.0
|
||||
|
|
|
@ -29,7 +29,8 @@ function initEditMyModuleDescription() {
|
|||
});
|
||||
|
||||
setTimeout(function() {
|
||||
TinyMCE.wrapTables(viewObject);
|
||||
const notesContainerEl = document.getElementById('notes-container');
|
||||
window.wrapTables(notesContainerEl);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
@ -327,16 +328,6 @@ function initAccessModal() {
|
|||
});
|
||||
}
|
||||
|
||||
function initWrapTables() {
|
||||
const viewMode = new URLSearchParams(window.location.search).get('view_mode');
|
||||
if (['archived', 'locked'].includes(viewMode)) {
|
||||
setTimeout(() => {
|
||||
const notesContainerEl = document.getElementById('notes-container');
|
||||
window.wrapTables(notesContainerEl);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes page
|
||||
*/
|
||||
|
@ -348,7 +339,6 @@ function init() {
|
|||
initProtocolSectionOpenEvent();
|
||||
initDetailsDropdown();
|
||||
initAccessModal();
|
||||
initWrapTables();
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
|
@ -968,6 +968,16 @@ function reportHandsonTableConverter() {
|
|||
}
|
||||
|
||||
(function() {
|
||||
function getSelectedRepositoryColumnValues(element, selectedAll = false) {
|
||||
const values = [];
|
||||
$(element).find('option').each((_, option) => {
|
||||
if ($(option).attr('selected-value') || selectedAll) {
|
||||
values.push(option.value);
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function getReportData() {
|
||||
var reportData = {};
|
||||
|
||||
|
@ -982,7 +992,7 @@ function reportHandsonTableConverter() {
|
|||
|
||||
// Template values
|
||||
reportData.template_values = {};
|
||||
$.each($('.report-template-values-container').find('.sci-input-field'), function(i, field) {
|
||||
$.each($('.report-template-values-container').find('.sci-input-field').not('.report-template-value-dropdown'), (_, field) => {
|
||||
if (field.value.length === 0) return;
|
||||
|
||||
reportData.template_values[field.name] = {
|
||||
|
@ -1036,6 +1046,7 @@ function reportHandsonTableConverter() {
|
|||
|
||||
// Settings
|
||||
reportData.report.settings.template = dropdownSelector.getValues('#templateSelector');
|
||||
reportData.report.settings.docx_template = dropdownSelector.getValues('#docxTemplateSelector');
|
||||
reportData.report.settings.all_tasks = $('.project-contents-container .select-all-my-modules-checkbox')
|
||||
.prop('checked');
|
||||
$.each($('.task-contents-container .content-element .protocol-setting'), function(i, e) {
|
||||
|
@ -1045,12 +1056,24 @@ function reportHandsonTableConverter() {
|
|||
reportData.report.settings.task[e.value] = e.checked;
|
||||
});
|
||||
reportData.report.settings.task.repositories = [];
|
||||
$.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), function(i, e) {
|
||||
reportData.report.settings.task.repositories.push(parseInt(e.value, 10));
|
||||
reportData.report.settings.task.excluded_repository_columns = {};
|
||||
|
||||
$.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), (_, e) => {
|
||||
const value = parseInt(e.value, 10);
|
||||
const $repositoryColumn = $(e).parent().siblings('.repository-columns')[0];
|
||||
const selectedValues = dropdownSelector.getValues($repositoryColumn);
|
||||
const excludedValues = getSelectedRepositoryColumnValues($repositoryColumn, true)
|
||||
.filter((item) => !selectedValues.includes(item))
|
||||
.map((el) => parseInt(el, 10));
|
||||
reportData.report.settings.task.repositories.push(value);
|
||||
reportData.report.settings.task.excluded_repository_columns[value] = excludedValues;
|
||||
});
|
||||
|
||||
reportData.report.settings.task.result_order = dropdownSelector.getValues('#taskResultsOrder');
|
||||
|
||||
reportData.report.settings.exclude_task_metadata = $('.exclude-task-metadata-setting')[0].checked;
|
||||
reportData.report.settings.exclude_timestamps = $('.exclude-timestamps-setting')[0].checked;
|
||||
|
||||
return reportData;
|
||||
}
|
||||
|
||||
|
@ -1254,7 +1277,9 @@ function reportHandsonTableConverter() {
|
|||
|
||||
function reCheckContinueButton() {
|
||||
if (dropdownSelector.getValues('#projectSelector').length > 0
|
||||
&& dropdownSelector.getValues('#templateSelector').length > 0) {
|
||||
&& dropdownSelector.getValues('#templateSelector').length > 0
|
||||
&& (dropdownSelector.getValues('#docxTemplateSelector').length > 0
|
||||
|| $('#docxTemplateSelector').closest('.hidden').length > 0)) {
|
||||
$('.continue-button').attr('disabled', false);
|
||||
} else {
|
||||
$('.continue-button').attr('disabled', true);
|
||||
|
@ -1276,9 +1301,18 @@ function reportHandsonTableConverter() {
|
|||
}
|
||||
if (dropdownSelector.getValues('#projectSelector').length > 0) {
|
||||
dropdownSelector.enableSelector('#templateSelector');
|
||||
dropdownSelector.enableSelector('#docxTemplateSelector');
|
||||
if ($('#templateSelector').data('defaultTemplate')) {
|
||||
dropdownSelector.selectValues('#templateSelector', $('#templateSelector').data('defaultTemplate'));
|
||||
}
|
||||
if ($('#docxTemplateSelector').data('defaultTemplate')) {
|
||||
dropdownSelector.selectValues('#docxTemplateSelector', $('#docxTemplateSelector').data('defaultTemplate'));
|
||||
}
|
||||
} else {
|
||||
dropdownSelector.selectValues('#templateSelector', '');
|
||||
dropdownSelector.disableSelector('#templateSelector');
|
||||
dropdownSelector.selectValues('#docxTemplateSelector', '');
|
||||
dropdownSelector.disableSelector('#docxTemplateSelector');
|
||||
}
|
||||
reCheckContinueButton();
|
||||
}
|
||||
|
@ -1292,12 +1326,12 @@ function reportHandsonTableConverter() {
|
|||
disableSearch: true,
|
||||
onSelect: function() {
|
||||
if (dropdownSelector.getValues('#templateSelector').length === 0) {
|
||||
$('.report-template-values-container').html('').addClass('hidden');
|
||||
$('.report-template-values-container.pdf').html('').addClass('hidden');
|
||||
reCheckContinueButton();
|
||||
return;
|
||||
}
|
||||
|
||||
let filledFieldsCount = $('.report-template-values-container')
|
||||
let filledFieldsCount = $('.report-template-values-container.pdf')
|
||||
.find('input.sci-input-field, textarea.sci-input-field').filter(function() {
|
||||
return !!this.value;
|
||||
}).length;
|
||||
|
@ -1311,13 +1345,57 @@ function reportHandsonTableConverter() {
|
|||
}
|
||||
});
|
||||
|
||||
dropdownSelector.init('#docxTemplateSelector', {
|
||||
singleSelect: true,
|
||||
closeOnSelect: true,
|
||||
noEmptyOption: true,
|
||||
selectAppearance: 'simple',
|
||||
disableSearch: true,
|
||||
onSelect: function() {
|
||||
if (dropdownSelector.getValues('#docxTemplateSelector').length === 0) {
|
||||
$('.report-template-values-container.docx').html('').addClass('hidden');
|
||||
reCheckContinueButton();
|
||||
return;
|
||||
}
|
||||
|
||||
let filledFieldsCount = $('.report-template-values-container.docx')
|
||||
.find('input.sci-input-field, textarea.sci-input-field').filter(function() {
|
||||
return !!this.value;
|
||||
}).length;
|
||||
|
||||
if (filledFieldsCount === 0) {
|
||||
loadDocxTemplate();
|
||||
} else {
|
||||
$('#templateReportWarningModal').modal('show');
|
||||
}
|
||||
reCheckContinueButton();
|
||||
}
|
||||
});
|
||||
|
||||
if (dropdownSelector.getValues('#templateSelector').length > 0) {
|
||||
loadTemplate();
|
||||
}
|
||||
if (dropdownSelector.getValues('#docxTemplateSelector').length > 0) {
|
||||
loadDocxTemplate();
|
||||
}
|
||||
|
||||
$('.repository-columns').each((_, element) => {
|
||||
const elementId = `#${$(element).attr('id')}`;
|
||||
const elements = getSelectedRepositoryColumnValues(elementId);
|
||||
|
||||
dropdownSelector.init(elementId, {
|
||||
selectAppearance: 'simple',
|
||||
optionClass: 'checkbox-icon'
|
||||
});
|
||||
|
||||
if (elements.length) {
|
||||
dropdownSelector.selectValues(elementId, elements);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadTemplate() {
|
||||
let template = $('#templateSelector').val();
|
||||
const template = dropdownSelector.getValues('#templateSelector');
|
||||
let params = {
|
||||
project_id: dropdownSelector.getValues('#projectSelector'),
|
||||
template: template
|
||||
|
@ -1325,8 +1403,38 @@ function reportHandsonTableConverter() {
|
|||
|
||||
$('#templateSelector').data('selected-template', template);
|
||||
$.get($('#templateSelector').data('valuesEditorPath'), params, function(result) {
|
||||
$('.report-template-values-container').removeClass('hidden');
|
||||
$('.report-template-values-container').html(result.html);
|
||||
$('.report-template-values-container.pdf').removeClass('hidden');
|
||||
$('.report-template-values-container.pdf').html(result.html);
|
||||
|
||||
$('.section').each(function() {
|
||||
var section = $(this);
|
||||
var collapseButton = section.find('.sn-icon-down');
|
||||
var valuesContainer = section.find('.values-container');
|
||||
|
||||
if (valuesContainer.children().length === 0) {
|
||||
collapseButton.hide();
|
||||
}
|
||||
});
|
||||
|
||||
$('.report-template-value-dropdown').each(function() {
|
||||
dropdownSelector.init($(this), {
|
||||
noEmptyOption: true
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadDocxTemplate() {
|
||||
const template = dropdownSelector.getValues('#docxTemplateSelector');
|
||||
let params = {
|
||||
project_id: dropdownSelector.getValues('#projectSelector'),
|
||||
template: template
|
||||
};
|
||||
|
||||
$('#docxTemplateSelector').data('selected-template', template);
|
||||
$.get($('#docxTemplateSelector').data('valuesEditorPath'), params, function(result) {
|
||||
$('.report-template-values-container.docx').removeClass('hidden');
|
||||
$('.report-template-values-container.docx').html(result.html);
|
||||
|
||||
$('.section').each(function() {
|
||||
var section = $(this);
|
||||
|
@ -1448,7 +1556,9 @@ function reportHandsonTableConverter() {
|
|||
.on('hide.bs.modal', function() {
|
||||
if (!$('#templateReportWarningModal').hasClass('skip-hide-event')) {
|
||||
let previousTemplate = $('#templateSelector').data('selected-template');
|
||||
let previousDocxTemplate = $('#docxTemplateSelector').data('selected-template');
|
||||
dropdownSelector.selectValues('#templateSelector', previousTemplate);
|
||||
dropdownSelector.selectValues('#docxTemplateSelector', previousDocxTemplate);
|
||||
}
|
||||
$('#templateReportWarningModal').removeClass('skip-hide-event');
|
||||
});
|
||||
|
@ -1461,7 +1571,7 @@ function reportHandsonTableConverter() {
|
|||
|
||||
$('#reportWizardEditWarning').modal('show');
|
||||
$('.experiment-contents').sortable();
|
||||
|
||||
|
||||
initNameContainerFocus();
|
||||
initGenerateButton();
|
||||
initReportWizard();
|
||||
|
|
|
@ -328,6 +328,40 @@ var RepositoryDatatable = (function(global) {
|
|||
});
|
||||
}
|
||||
|
||||
function initDeleteAssetValueConfirmModal() {
|
||||
$('#deleteRepositoryAssetValueModal').on('shown.bs.modal', function() {
|
||||
let $fileBtn = $(this).data('cellFileBtn');
|
||||
let $input = $(this).data('cellInput');
|
||||
let $label = $(this).data('cellLabel');
|
||||
|
||||
$('#confirmAssetValueDelete').one('click', function() {
|
||||
$fileBtn.addClass('new-file');
|
||||
$label.text('');
|
||||
$input.val('');
|
||||
$fileBtn.removeClass('error');
|
||||
|
||||
if (!$input.data('is-empty')) { // set hidden field for deletion only if original value has been set on rendering
|
||||
$input
|
||||
.prev('.file-hidden-field-container')
|
||||
.html(`<input type="hidden"
|
||||
form="${$input.attr('form')}"
|
||||
name="repository_cells[${$input.data('col-id')}]"
|
||||
value=""/>`);
|
||||
}
|
||||
|
||||
$('#deleteRepositoryAssetValueModal').modal('hide');
|
||||
});
|
||||
});
|
||||
|
||||
$('#deleteRepositoryAssetValueModal').on('hidden.bs.modal', function() {
|
||||
const $deleteRepositoryAssetValueModal = $('#deleteRepositoryAssetValueModal');
|
||||
|
||||
$deleteRepositoryAssetValueModal.data('cellFileBtn', null);
|
||||
$deleteRepositoryAssetValueModal.data('cellInput', null);
|
||||
$deleteRepositoryAssetValueModal.data('cellLabel', null);
|
||||
});
|
||||
}
|
||||
|
||||
function initActiveRemindersFilter() {
|
||||
$(TABLE_WRAPPER_ID).find('#only_reminders').on('change', function() {
|
||||
var $activeRemindersFilter = $(this).closest('.active-reminders-filter');
|
||||
|
@ -804,6 +838,7 @@ var RepositoryDatatable = (function(global) {
|
|||
initSaveButton();
|
||||
initCancelButton();
|
||||
initBSTooltips();
|
||||
initDeleteAssetValueConfirmModal();
|
||||
window.initRepositoryStateMenu();
|
||||
DataTableHelpers.initLengthAppearance($(TABLE_ID).closest('.dataTables_wrapper'));
|
||||
|
||||
|
|
|
@ -80,25 +80,17 @@ var RepositoryDatatableRowEditor = (function() {
|
|||
$fileBtn.removeClass('error');
|
||||
});
|
||||
|
||||
|
||||
deleteButtons.on('click', function() {
|
||||
const $deleteRepositoryAssetValueModal = $('#deleteRepositoryAssetValueModal');
|
||||
let $fileBtn = $(this).parent();
|
||||
let $input = $fileBtn.prev('input[type=file]');
|
||||
let $label = $fileBtn.find('label');
|
||||
|
||||
$fileBtn.addClass('new-file');
|
||||
$label.text('');
|
||||
$input.val('');
|
||||
$fileBtn.removeClass('error');
|
||||
$deleteRepositoryAssetValueModal.data('cellFileBtn', $fileBtn);
|
||||
$deleteRepositoryAssetValueModal.data('cellInput', $input);
|
||||
$deleteRepositoryAssetValueModal.data('cellLabel', $label);
|
||||
|
||||
if (!$input.data('is-empty')) { // set hidden field for deletion only if original value has been set on rendering
|
||||
$input
|
||||
.prev('.file-hidden-field-container')
|
||||
.html(`<input type="hidden"
|
||||
form="${$input.attr('form')}"
|
||||
name="repository_cells[${$input.data('col-id')}]"
|
||||
value=""/>`);
|
||||
}
|
||||
$('#deleteRepositoryAssetValueModal').modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
/* global */
|
||||
|
||||
(function () {
|
||||
const rtf = $('.rtf-view').toArray();
|
||||
for (let i = 0; i < rtf.length; i += 1) {
|
||||
const container = $(rtf[i]).find('table').toArray();
|
||||
|
||||
for (let j = 0; j < container.length; j += 1) {
|
||||
const table = $(container[j]);
|
||||
if ($(table).parent().hasClass('table-wrapper')) return;
|
||||
|
||||
$(table).wrap(`
|
||||
<div class="table-wrapper w-full" style="overflow: auto;"></div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
$('.rtf-view').toArray().forEach((rtf) => {
|
||||
$(rtf).find('table').toArray().forEach((table) => {
|
||||
if ($(table).parents('table').length === 0) {
|
||||
$(table).css('float', 'none')
|
||||
.wrapAll('<div class="table-wrapper w-full" style="overflow: auto"></div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
}());
|
||||
|
|
|
@ -22,8 +22,10 @@ var ActiveStoragePreviews = (function() {
|
|||
if (!$(img).parent().hasClass('processing')) $(img).parent().addClass('processing');
|
||||
|
||||
setTimeout(() => {
|
||||
img.src = src;
|
||||
img.retryCount += 1;
|
||||
if (document.body.contains(img)) {
|
||||
img.src = src;
|
||||
img.retryCount += 1;
|
||||
}
|
||||
}, RETRY_DELAY);
|
||||
},
|
||||
showPreview: function(ev) {
|
||||
|
|
|
@ -353,7 +353,7 @@ var dropdownSelector = (function() {
|
|||
|
||||
// If we setup Select All we draw it and add correspond logic
|
||||
if (selectElement.data('select-all-button')) {
|
||||
$(`<div class="dropdown-select-all btn">${selectElement.data('select-all-button')}</div>`)
|
||||
$(`<div class="dropdown-select-all">${selectElement.data('select-all-button')}</div>`)
|
||||
.appendTo(dropdownContainer.find('.dropdown-container'))
|
||||
.click(() => {
|
||||
// For AJAX dropdown we will use only "Deselect All"
|
||||
|
|
|
@ -142,23 +142,14 @@ $.fn.initSubmitModal = function(modalID, modelName) {
|
|||
* @returns {string} - HTML with tables wrapped.
|
||||
*/
|
||||
function wrapTables(htmlStringOrDomEl) {
|
||||
if (typeof htmlStringOrDomEl === 'string') {
|
||||
const container = $(`<span class="text-base">${htmlStringOrDomEl}</span>`);
|
||||
container.find('table').toArray().forEach((table) => {
|
||||
if ($(table).parent().hasClass('table-wrapper')) return;
|
||||
$(table).css('float', 'none').wrapAll(`
|
||||
<div class="table-wrapper" style="overflow: auto; width: 100%"></div>
|
||||
`);
|
||||
});
|
||||
return container.prop('outerHTML');
|
||||
}
|
||||
// Check if the value is a DOM element
|
||||
if (htmlStringOrDomEl instanceof Element) {
|
||||
const tableElement = $(htmlStringOrDomEl).find('table');
|
||||
if (tableElement.length > 0) {
|
||||
tableElement.wrap('<div class="table-wrapper" style="overflow: auto; width: 100%"></div>');
|
||||
const updatedHtml = $(htmlStringOrDomEl).html();
|
||||
$(htmlStringOrDomEl).replaceWith(updatedHtml);
|
||||
const htmlContent = `<span class="text-base">${htmlStringOrDomEl}</span>`;
|
||||
const container = typeof htmlStringOrDomEl === 'string' ? $(htmlContent) : $(htmlStringOrDomEl);
|
||||
|
||||
container.find('table').toArray().forEach((table) => {
|
||||
if ($(table).parents('table').length === 0) {
|
||||
$(table).css('float', 'none')
|
||||
.wrapAll('<div class="table-wrapper w-full" style="overflow: auto"></div>');
|
||||
}
|
||||
}
|
||||
});
|
||||
return container.prop('outerHTML');
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
flex-direction: column;
|
||||
height: calc(100vh - 8rem);
|
||||
padding: 1.5rem;
|
||||
width: 400px;
|
||||
width: 600px;
|
||||
|
||||
.sci--navigation--notificaitons-flyout-title {
|
||||
@include font-h2;
|
||||
|
|
|
@ -250,6 +250,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
// scss-lint:disable ImportantRule
|
||||
.dropdown-selector-container {
|
||||
.dropdown-container {
|
||||
left: auto !important;
|
||||
margin: auto !important;
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
// scss-lint:enable ImportantRule
|
||||
|
||||
.repositories-contents {
|
||||
.dropdown-selector-container {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-selector-container {
|
||||
background: $color-white;
|
||||
box-shadow: $modal-shadow;
|
||||
|
|
|
@ -175,6 +175,10 @@
|
|||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
|
||||
&:hover {
|
||||
background: $color-concrete;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-blank {
|
||||
|
|
|
@ -1362,6 +1362,10 @@ th.custom-field .modal-tooltiptext {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tooltip-open {
|
||||
background-color: $color-concrete;
|
||||
color: $color-black;
|
||||
|
|
11
app/components/reports/repositories_input_component.html.erb
Normal file
11
app/components/reports/repositories_input_component.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<% if @editing %>
|
||||
<div class="sci-select-container">
|
||||
<%= label_tag @name, @label %>
|
||||
<%= select_tag @name, options_from_collection_for_select(@repositories, :id, :name, @value), placeholder: @placeholder, class: 'sci-input-field report-template-value-dropdown', data: { type: 'RepositoriesInputComponent' }, multiple: true %>
|
||||
</div>
|
||||
<% else %>
|
||||
<% @project_members.where(id: @value).each do |member| %>
|
||||
<%= member.public_send(@displayed_field) %>
|
||||
<br>
|
||||
<% end %>
|
||||
<% end %>
|
17
app/components/reports/repositories_input_component.rb
Normal file
17
app/components/reports/repositories_input_component.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Reports
|
||||
class RepositoriesInputComponent < TemplateValueComponent
|
||||
def initialize(report:, name:, label:, placeholder: nil, editing: true, displayed_field: :name, user: nil)
|
||||
super(report: report, name: name, label: label, placeholder: placeholder, editing: editing)
|
||||
live_repositories = Repository.viewable_by_user(user, report.team).sort_by { |r| r.name.downcase }
|
||||
snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository)
|
||||
.where(team: report.team)
|
||||
.where.not(original_repository: live_repositories)
|
||||
.select('DISTINCT ON ("repositories"."parent_id") "repositories".*')
|
||||
.sort_by { |r| r.name.downcase }
|
||||
@repositories = live_repositories + snapshots_of_deleted
|
||||
@displayed_field = displayed_field
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,6 +8,8 @@ module Api
|
|||
|
||||
before_action :authenticate_request!, except: %i(status health)
|
||||
|
||||
newrelic_ignore only: %i(health status)
|
||||
|
||||
rescue_from StandardError do |e|
|
||||
logger.error e.message
|
||||
logger.error e.backtrace.join("\n")
|
||||
|
|
|
@ -180,6 +180,9 @@ module Api
|
|||
|
||||
def load_inventory(key = :inventory_id)
|
||||
@inventory = @team.repositories.find(params.require(key))
|
||||
|
||||
@inventory.unlock! if @inventory.is_a?(SoftLockedRepository)
|
||||
|
||||
raise PermissionError.new(Repository, :read) unless can_read_repository?(@inventory)
|
||||
end
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@ module Api
|
|||
before_action only: %i(show update destroy) do
|
||||
load_inventory_column(:id)
|
||||
end
|
||||
before_action :check_manage_permissions, only: %i(update destroy)
|
||||
before_action :check_create_permissions, only: %i(create)
|
||||
before_action :check_manage_permissions, only: %i(update)
|
||||
before_action :check_delete_permissions, only: %i(destroy)
|
||||
|
||||
def index
|
||||
columns = timestamps_filter(@inventory.repository_columns).includes(:repository_list_items)
|
||||
|
@ -61,6 +62,10 @@ module Api
|
|||
raise PermissionError.new(RepositoryColumn, :manage) unless can_manage_repository_column?(@inventory_column)
|
||||
end
|
||||
|
||||
def check_delete_permissions
|
||||
raise PermissionError.new(RepositoryColumn, :delete) unless can_delete_repository_column?(@inventory_column)
|
||||
end
|
||||
|
||||
def check_create_permissions
|
||||
raise PermissionError.new(RepositoryColumn, :create) unless can_create_repository_columns?(@inventory)
|
||||
end
|
||||
|
|
|
@ -124,7 +124,7 @@ module Api
|
|||
Result.transaction do
|
||||
old_checksum = asset.file.blob.checksum
|
||||
if @form_multipart_upload
|
||||
asset.file.attach(result_file_params[:file])
|
||||
asset.attach_file_version(result_file_params[:file])
|
||||
else
|
||||
blob = create_blob_from_params
|
||||
asset.update!(file: blob)
|
||||
|
|
|
@ -16,7 +16,8 @@ class AssetSyncController < ApplicationController
|
|||
asset_sync_token = current_user.asset_sync_tokens.find_or_create_by(asset_id: params[:asset_id])
|
||||
|
||||
unless asset_sync_token.token_valid?
|
||||
asset_sync_token = current_user.asset_sync_tokens.create(asset_id: params[:asset_id])
|
||||
asset_sync_token =
|
||||
current_user.asset_sync_tokens.create(asset_id: params[:asset_id])
|
||||
end
|
||||
|
||||
render json: AssetSyncTokenSerializer.new(asset_sync_token).as_json
|
||||
|
@ -27,34 +28,32 @@ class AssetSyncController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if @asset_sync_token.conflicts?(request.headers['VersionToken'])
|
||||
ActiveRecord::Base.transaction do
|
||||
conflict_response = AssetSyncTokenSerializer.new(conflicting_asset_copy_token).as_json
|
||||
error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) }
|
||||
log_activity(:create)
|
||||
render json: conflict_response.merge(error_message), status: :conflict
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
orig_file_size = @asset.file_size
|
||||
asset_conflicts = @asset_sync_token.conflicts?(request.headers['VersionToken'])
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@asset.update(last_modified_by: current_user)
|
||||
if wopi_file?(@asset)
|
||||
@asset.update_contents(request.body)
|
||||
else
|
||||
@asset.file.attach(io: request.body, filename: @asset.file.filename)
|
||||
@asset.attach_file_version(io: request.body, filename: @asset.file.filename)
|
||||
@asset.touch
|
||||
end
|
||||
|
||||
@asset.team.release_space(orig_file_size)
|
||||
@asset.post_process_file
|
||||
|
||||
log_activity(:edit)
|
||||
end
|
||||
|
||||
if asset_conflicts
|
||||
ActiveRecord::Base.transaction do
|
||||
conflict_response = AssetSyncTokenSerializer.new(@asset_sync_token).as_json
|
||||
error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) }
|
||||
render json: conflict_response.merge(error_message), status: :conflict
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json
|
||||
end
|
||||
|
||||
|
@ -94,7 +93,7 @@ class AssetSyncController < ApplicationController
|
|||
metadata: @asset.blob.metadata
|
||||
)
|
||||
|
||||
new_asset.file.attach(blob)
|
||||
new_asset.attach_file_version(blob)
|
||||
|
||||
case @asset.parent
|
||||
when Step
|
||||
|
|
|
@ -19,15 +19,14 @@ class AssetsController < ApplicationController
|
|||
before_action :load_vars, except: :create_wopi_file
|
||||
before_action :check_read_permission, except: %i(edit destroy duplicate create_wopi_file toggle_view_mode)
|
||||
before_action :check_manage_permission, only: %i(edit destroy duplicate rename toggle_view_mode)
|
||||
before_action :check_restore_permission, only: :restore_version
|
||||
|
||||
def file_preview
|
||||
editable = can_manage_asset?(@asset) && (@asset.repository_asset_value.blank? ||
|
||||
!@asset.repository_cell.repository_row.repository.is_a?(SoftLockedRepository))
|
||||
render json: { html: render_to_string(
|
||||
partial: 'shared/file_preview/content',
|
||||
locals: {
|
||||
asset: @asset,
|
||||
can_edit: editable,
|
||||
can_edit: can_manage_asset?(@asset),
|
||||
gallery: params[:gallery],
|
||||
preview: params[:preview]
|
||||
},
|
||||
|
@ -197,7 +196,7 @@ class AssetsController < ApplicationController
|
|||
return render_403 unless can_read_team?(@asset.team)
|
||||
|
||||
@asset.last_modified_by = current_user
|
||||
@asset.file.attach(io: params.require(:image), filename: orig_file_name)
|
||||
@asset.attach_file_version(io: params.require(:image), filename: orig_file_name)
|
||||
@asset.save!
|
||||
create_edit_image_activity(@asset, current_user, :finish_editing)
|
||||
# release previous image space
|
||||
|
@ -242,9 +241,9 @@ class AssetsController < ApplicationController
|
|||
|
||||
# Asset validation
|
||||
asset = Asset.new(created_by: current_user, team: current_team)
|
||||
asset.file.attach(io: StringIO.new,
|
||||
filename: "#{params[:file_name]}.#{params[:file_type]}",
|
||||
content_type: wopi_content_type(params[:file_type]))
|
||||
asset.attach_file_version(io: StringIO.new,
|
||||
filename: "#{params[:file_name]}.#{params[:file_type]}",
|
||||
content_type: wopi_content_type(params[:file_type]))
|
||||
|
||||
unless asset.valid?(:wopi_file_creation)
|
||||
render json: {
|
||||
|
@ -362,7 +361,8 @@ class AssetsController < ApplicationController
|
|||
when Step, Result
|
||||
new_asset = @asset.duplicate(
|
||||
new_name:
|
||||
"#{@asset.file.filename.base} (1).#{@asset.file.filename.extension}"
|
||||
"#{@asset.file.filename.base} (1).#{@asset.file.filename.extension}",
|
||||
created_by: current_user
|
||||
)
|
||||
|
||||
@asset.parent.assets << new_asset
|
||||
|
@ -397,13 +397,70 @@ class AssetsController < ApplicationController
|
|||
render json: { checksum: @asset.file.blob.checksum }
|
||||
end
|
||||
|
||||
def versions
|
||||
blobs =
|
||||
[@asset.file.blob] +
|
||||
@asset.previous_files.map(&:blob).sort_by { |b| -1 * b.metadata['version'].to_i }[0..(VersionedAttachments.enabled? ? -1 : 1)]
|
||||
render(
|
||||
json: ActiveModel::SerializableResource.new(
|
||||
blobs,
|
||||
each_serializer: ActiveStorage::BlobSerializer,
|
||||
user: current_user
|
||||
).as_json.merge(
|
||||
enabled: VersionedAttachments.enabled?,
|
||||
enable_url: ENV.fetch('SCINOTE_FILE_VERSIONING_ENABLE_URL', nil),
|
||||
disabled_disclaimer: VersionedAttachments.disabled_disclaimer
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def restore_version
|
||||
render_403 unless VersionedAttachments.enabled?
|
||||
|
||||
@asset.last_modified_by = current_user
|
||||
|
||||
@asset.restore_file_version(params[:version].to_i)
|
||||
@asset.restore_preview_image_version(params[:version].to_i) if @asset.preview_image.attached?
|
||||
|
||||
message_items = {
|
||||
version: params[:version].to_i,
|
||||
file: @asset.file_name
|
||||
}
|
||||
|
||||
case @asset.parent
|
||||
when Step
|
||||
if @asset.parent.protocol.in_module?
|
||||
message_items.merge!({ my_module: @assoc.protocol.my_module.id, step: @asset.parent.id })
|
||||
log_restore_activity(:task_step_restore_asset_version, @assoc.protocol,
|
||||
@assoc.protocol.team, @assoc.my_module&.project, message_items)
|
||||
else
|
||||
message_items.merge!({ protocol: @assoc.protocol.id, step: @asset.parent.id })
|
||||
log_restore_activity(:protocol_step_restore_asset_version, @assoc.protocol,
|
||||
@assoc.protocol.team, nil, message_items)
|
||||
end
|
||||
when Result
|
||||
message_items.merge!({ result: @assoc.id, my_module: @assoc.my_module.id })
|
||||
log_restore_activity(:task_result_restore_asset_version, @assoc,
|
||||
@assoc.my_module.team, @assoc.my_module.project, message_items)
|
||||
when RepositoryCell
|
||||
message_items.merge!({ repository_column: @assoc.repository_column.id, repository: @repository.id })
|
||||
log_restore_activity(:repository_column_restore_asset_version, @repository,
|
||||
@repository.team, nil, message_items)
|
||||
end
|
||||
|
||||
@asset.save!
|
||||
|
||||
render json: @asset.file.blob
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_vars
|
||||
@asset = Asset.find_by(id: params[:id])
|
||||
return render_404 unless @asset
|
||||
|
||||
current_user.permission_team = @asset.team
|
||||
# don't overwrite permission team if asset is in a repositoy, since then sharing rules may apply and depend on user's current team
|
||||
current_user.permission_team = @asset.team unless @asset.repository_cell
|
||||
|
||||
@assoc ||= @asset.step
|
||||
@assoc ||= @asset.result
|
||||
|
@ -426,6 +483,10 @@ class AssetsController < ApplicationController
|
|||
render_403 and return unless can_manage_asset?(@asset)
|
||||
end
|
||||
|
||||
def check_restore_permission
|
||||
render_403 and return unless can_restore_asset?(@asset)
|
||||
end
|
||||
|
||||
def append_wd_params(url)
|
||||
exclude_params = %w(wdPreviousSession wdPreviousCorrelation)
|
||||
wd_params = params.as_json.select { |key, _value| key[/^wd.*/] && !(exclude_params.include? key) }.to_query
|
||||
|
@ -472,4 +533,14 @@ class AssetsController < ApplicationController
|
|||
result: result.id
|
||||
}.merge(message_items))
|
||||
end
|
||||
|
||||
def log_restore_activity(type_of, subject, team, project = nil, message_items = {})
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: subject,
|
||||
team: team,
|
||||
project: project,
|
||||
message_items: message_items)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,15 +72,12 @@ class GeneSequenceAssetsController < ApplicationController
|
|||
|
||||
ensure_asset!
|
||||
|
||||
@asset.file.purge
|
||||
@asset.preview_image.purge
|
||||
|
||||
@asset.file.attach(
|
||||
@asset.attach_file_version(
|
||||
io: StringIO.new(params[:sequence_data].to_json),
|
||||
filename: "#{params[:sequence_name]}.json"
|
||||
)
|
||||
|
||||
@asset.preview_image.attach(
|
||||
@asset.attach_preview_image_version(
|
||||
io: StringIO.new(Base64.decode64(params[:base64_image].split(',').last)),
|
||||
filename: "#{params[:sequence_name]}.png"
|
||||
)
|
||||
|
|
|
@ -151,7 +151,25 @@ class GlobalActivitiesController < ApplicationController
|
|||
end
|
||||
|
||||
def activity_filter_params
|
||||
params.permit(:name, filter: {})
|
||||
params.permit(
|
||||
:name,
|
||||
filter: [
|
||||
:to_date,
|
||||
:from_date,
|
||||
{ types: [] },
|
||||
{ subjects: {
|
||||
'Report' => [],
|
||||
'Project' => [],
|
||||
'MyModule' => [],
|
||||
'Protocol' => [],
|
||||
'Experiment' => [],
|
||||
'RepositoryRow' => [],
|
||||
'RepositoryBase' => []
|
||||
} },
|
||||
{ users: [] },
|
||||
{ teams: [] }
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def activity_filters
|
||||
|
|
|
@ -18,11 +18,13 @@ class ProtocolsController < ApplicationController
|
|||
print
|
||||
versions_modal
|
||||
protocol_status_bar
|
||||
linked_children
|
||||
linked_children_datatable
|
||||
versions_list
|
||||
permissions
|
||||
)
|
||||
before_action :check_linked_protocol_view_permissions, only: %i(
|
||||
linked_children
|
||||
linked_children_datatable
|
||||
)
|
||||
before_action :switch_team_with_param, only: %i(index protocolsio_index)
|
||||
before_action :check_view_all_permissions, only: %i(
|
||||
index
|
||||
|
@ -909,7 +911,6 @@ class ProtocolsController < ApplicationController
|
|||
end
|
||||
|
||||
def set_inline_name_editing
|
||||
return unless @protocol.initial_draft?
|
||||
return unless can_manage_protocol_draft_in_repository?(@protocol)
|
||||
|
||||
@inline_editable_title_config = {
|
||||
|
@ -934,6 +935,16 @@ class ProtocolsController < ApplicationController
|
|||
end
|
||||
|
||||
def check_view_permissions
|
||||
@protocol = Protocol.find_by(id: params[:id])
|
||||
current_team_switch(@protocol.team) if current_team != @protocol.team
|
||||
unless @protocol.present? &&
|
||||
(can_read_protocol_in_module?(@protocol) ||
|
||||
can_read_protocol_in_repository?(@protocol))
|
||||
render_403
|
||||
end
|
||||
end
|
||||
|
||||
def check_linked_protocol_view_permissions
|
||||
@protocol = Protocol.find_by(id: params[:id])
|
||||
current_team_switch(@protocol.team) if current_team != @protocol.team
|
||||
unless @protocol.present? &&
|
||||
|
|
|
@ -7,18 +7,20 @@ class ReportsController < ApplicationController
|
|||
before_action :load_vars, only: %i(edit update document_preview generate_pdf generate_docx status
|
||||
save_pdf_to_inventory_modal save_pdf_to_inventory_item)
|
||||
before_action :load_vars_nested, only: %i(create edit update generate_pdf
|
||||
generate_docx new_template_values project_contents)
|
||||
generate_docx new_template_values new_docx_template_values project_contents)
|
||||
before_action :load_wizard_vars, only: %i(new edit)
|
||||
before_action :load_repositories_vars, only: %i(new edit create update)
|
||||
before_action :load_available_repositories, only: %i(index save_pdf_to_inventory_modal available_repositories)
|
||||
before_action :check_project_read_permissions, only: %i(create edit update generate_pdf
|
||||
generate_docx new_template_values project_contents)
|
||||
generate_docx new_template_values new_docx_template_values project_contents)
|
||||
before_action :check_read_permissions, except: %i(index new create edit update destroy actions_toolbar generate_pdf
|
||||
generate_docx new_template_values project_contents
|
||||
generate_docx new_template_values project_contents new_docx_template_values
|
||||
available_repositories)
|
||||
before_action :check_create_permissions, only: %i(new create)
|
||||
before_action :check_manage_permissions, only: %i(edit update generate_pdf generate_docx)
|
||||
before_action :switch_team_with_param, only: :index
|
||||
after_action :generate_pdf_report, only: %i(create update generate_pdf)
|
||||
after_action :generate_pdf_report, only: %i(generate_pdf)
|
||||
after_action :generate_report, only: %i(create update)
|
||||
|
||||
# Index showing all reports of a single project
|
||||
def index
|
||||
|
@ -37,12 +39,15 @@ class ReportsController < ApplicationController
|
|||
# Report grouped by modules
|
||||
def new
|
||||
@templates = Extends::REPORT_TEMPLATES
|
||||
@docx_templates = Extends::DOCX_REPORT_TEMPLATES
|
||||
@report = current_team.reports.new
|
||||
end
|
||||
|
||||
def new_template_values
|
||||
if Extends::REPORT_TEMPLATES.key?(params[:template]&.to_sym)
|
||||
template = params[:template]
|
||||
@type = :pdf
|
||||
@template_name = Extends::REPORT_TEMPLATES[params[:template].to_sym]
|
||||
else
|
||||
return render_404
|
||||
end
|
||||
|
@ -68,6 +73,43 @@ class ReportsController < ApplicationController
|
|||
else
|
||||
render json: {
|
||||
html: render_to_string(partial: 'reports/wizard/no_template_values',
|
||||
locals: { type: @type, template: @template_name },
|
||||
formats: :html)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def new_docx_template_values
|
||||
if Extends::DOCX_REPORT_TEMPLATES.key?(params[:template]&.to_sym)
|
||||
template = params[:template]
|
||||
@type = :docx
|
||||
@template_name = Extends::DOCX_REPORT_TEMPLATES[params[:template].to_sym]
|
||||
else
|
||||
return render_404
|
||||
end
|
||||
|
||||
report = current_team.reports.where(project: @project).find_by(id: params[:report_id])
|
||||
if report.present?
|
||||
return render_403 unless can_manage_report?(report)
|
||||
else
|
||||
return render_403 unless can_create_reports?(current_team)
|
||||
|
||||
report = current_team.reports.new(project: @project)
|
||||
end
|
||||
|
||||
if lookup_context.any_templates?("reports/docx_templates/#{template}/edit")
|
||||
render json: {
|
||||
html: render_to_string(
|
||||
template: "reports/docx_templates/#{template}/edit",
|
||||
layout: 'reports/template_values_editor',
|
||||
locals: { report: report },
|
||||
formats: :html
|
||||
)
|
||||
}
|
||||
else
|
||||
render json: {
|
||||
html: render_to_string(partial: 'reports/wizard/no_template_values',
|
||||
locals: { type: @type, template: @template_name },
|
||||
formats: :html)
|
||||
}
|
||||
end
|
||||
|
@ -102,6 +144,7 @@ class ReportsController < ApplicationController
|
|||
def edit
|
||||
@edit = true
|
||||
@active_template = @report.settings[:template]
|
||||
@active_docx_template = @report.settings[:docx_template].presence || 'scinote_template'
|
||||
@report.settings = Report::DEFAULT_SETTINGS if @report.settings.blank?
|
||||
|
||||
@project_contents = {
|
||||
|
@ -312,13 +355,7 @@ class ReportsController < ApplicationController
|
|||
|
||||
def load_wizard_vars
|
||||
@templates = Extends::REPORT_TEMPLATES
|
||||
live_repositories = Repository.viewable_by_user(current_user).sort_by { |r| r.name.downcase }
|
||||
snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository)
|
||||
.where(team: current_team)
|
||||
.where.not(original_repository: live_repositories)
|
||||
.select('DISTINCT ON ("repositories"."parent_id") "repositories".*')
|
||||
.sort_by { |r| r.name.downcase }
|
||||
@repositories = live_repositories + snapshots_of_deleted
|
||||
@docx_templates = Extends::DOCX_REPORT_TEMPLATES
|
||||
@visible_projects = current_team.projects
|
||||
.active
|
||||
.joins(experiments: :my_modules)
|
||||
|
@ -327,6 +364,19 @@ class ReportsController < ApplicationController
|
|||
.merge(MyModule.active)
|
||||
.group(:id)
|
||||
.select(:id, :name)
|
||||
@default_template = Extends::REPORT_TEMPLATES.keys.first.to_s if Extends::REPORT_TEMPLATES.one?
|
||||
|
||||
@default_docx_template = Extends::DOCX_REPORT_TEMPLATES.keys.first.to_s if Extends::DOCX_REPORT_TEMPLATES.one? && custom_templates(Extends::DOCX_REPORT_TEMPLATES)
|
||||
end
|
||||
|
||||
def load_repositories_vars
|
||||
live_repositories = Repository.viewable_by_user(current_user).sort_by { |r| r.name.downcase }
|
||||
snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository)
|
||||
.where(team: current_team)
|
||||
.where.not(original_repository: live_repositories)
|
||||
.select('DISTINCT ON ("repositories"."parent_id") "repositories".*')
|
||||
.sort_by { |r| r.name.downcase }
|
||||
@repositories = live_repositories + snapshots_of_deleted
|
||||
end
|
||||
|
||||
def check_project_read_permissions
|
||||
|
@ -361,7 +411,7 @@ class ReportsController < ApplicationController
|
|||
|
||||
def report_params
|
||||
params.require(:report)
|
||||
.permit(:name, :description, :grouped_by, :report_contents, settings: {})
|
||||
.permit(:name, :description, :grouped_by, :report_contents, settings: permit_report_settings_structure(Report::DEFAULT_SETTINGS, @repositories))
|
||||
end
|
||||
|
||||
def search_params
|
||||
|
@ -394,6 +444,26 @@ class ReportsController < ApplicationController
|
|||
Rails.logger.error e.message
|
||||
end
|
||||
|
||||
def generate_docx_report
|
||||
return unless @report.persisted?
|
||||
|
||||
@report.docx_processing!
|
||||
log_activity(:generate_docx_report)
|
||||
|
||||
ensure_report_template!
|
||||
Reports::DocxJob.perform_later(@report.id, user_id: current_user.id, root_url: root_url)
|
||||
rescue ActiveRecord::ActiveRecordError => e
|
||||
Rails.logger.error e.message
|
||||
end
|
||||
|
||||
def generate_report
|
||||
return unless @report.persisted?
|
||||
|
||||
generate_pdf_report
|
||||
|
||||
generate_docx_report if @report.settings['docx_template'].present? && custom_templates(Extends::DOCX_REPORT_TEMPLATES)
|
||||
end
|
||||
|
||||
def ensure_report_template!
|
||||
return if @report.settings['template'].present?
|
||||
|
||||
|
|
|
@ -495,7 +495,7 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def set_inline_name_editing
|
||||
return unless can_manage_repository?(@repository) && !@repository.is_a?(SoftLockedRepository)
|
||||
return unless can_manage_repository?(@repository)
|
||||
|
||||
@inline_editable_title_config = {
|
||||
name: 'title',
|
||||
|
|
|
@ -5,7 +5,8 @@ class RepositoryColumnsController < ApplicationController
|
|||
before_action :load_repository
|
||||
before_action :load_column, only: %i(edit update destroy_html destroy items)
|
||||
before_action :check_create_permissions, only: %i(new create)
|
||||
before_action :check_manage_permissions, only: %i(edit update destroy_html destroy)
|
||||
before_action :check_manage_permissions, only: %i(edit update)
|
||||
before_action :check_delete_permissions, only: %i(destroy_html destroy)
|
||||
before_action :load_asset_type_columns, only: :available_asset_type_columns
|
||||
|
||||
def index
|
||||
|
@ -130,6 +131,10 @@ class RepositoryColumnsController < ApplicationController
|
|||
render_403 unless can_manage_repository_column?(@repository_column)
|
||||
end
|
||||
|
||||
def check_delete_permissions
|
||||
render_403 unless can_delete_repository_column?(@repository_column)
|
||||
end
|
||||
|
||||
def search_params
|
||||
params.permit(:q, :repository_id)
|
||||
end
|
||||
|
|
|
@ -125,7 +125,7 @@ class ResultAssetsController < ApplicationController
|
|||
ActiveRecord::Base.transaction do
|
||||
params[:results_files].each do |index, file|
|
||||
asset = Asset.create!(created_by: current_user, last_modified_by: current_user, team: current_team)
|
||||
asset.file.attach(file[:signed_blob_id])
|
||||
asset.attach_file_version(file[:signed_blob_id])
|
||||
result = Result.create!(user: current_user,
|
||||
my_module: @my_module,
|
||||
name: params[:results_names][index],
|
||||
|
|
|
@ -90,7 +90,7 @@ class ResultsController < ApplicationController
|
|||
team: @my_module.team,
|
||||
view_mode: @result.assets_view_mode
|
||||
)
|
||||
@asset.file.attach(params[:signed_blob_id])
|
||||
@asset.attach_file_version(params[:signed_blob_id])
|
||||
@asset.post_process_file
|
||||
end
|
||||
|
||||
|
|
|
@ -145,7 +145,10 @@ class SearchController < ApplicationController
|
|||
|
||||
def quick
|
||||
results = if params[:filter].present?
|
||||
object_quick_search(params[:filter].singularize)
|
||||
class_name = params[:filter].singularize
|
||||
return render_422(t('general.invalid_params')) unless Constants::QUICK_SEARCH_SEARCHABLE_OBJECTS.include?(class_name)
|
||||
|
||||
object_quick_search(class_name)
|
||||
else
|
||||
Constants::QUICK_SEARCH_SEARCHABLE_OBJECTS.filter_map do |object|
|
||||
next if object == 'label_template' && !LabelTemplate.enabled?
|
||||
|
|
|
@ -41,7 +41,7 @@ class StepsController < ApplicationController
|
|||
team: @protocol.team,
|
||||
view_mode: @step.assets_view_mode
|
||||
)
|
||||
@asset.file.attach(params[:signed_blob_id])
|
||||
@asset.attach_file_version(params[:signed_blob_id])
|
||||
@asset.post_process_file
|
||||
|
||||
default_message_items = {
|
||||
|
|
|
@ -5,11 +5,18 @@ class UserNotificationsController < ApplicationController
|
|||
|
||||
def index
|
||||
page = (params.dig(:page, :number) || 1).to_i
|
||||
notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
|
||||
notifications = load_notifications
|
||||
|
||||
case params[:tab]
|
||||
when 'read'
|
||||
notifications = notifications.where.not(read_at: nil)
|
||||
when 'unread'
|
||||
notifications = notifications.where(read_at: nil)
|
||||
end
|
||||
|
||||
notifications = notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
|
||||
|
||||
render json: notifications, each_serializer: NotificationSerializer
|
||||
|
||||
notifications.mark_as_read!
|
||||
end
|
||||
|
||||
def unseen_counter
|
||||
|
@ -18,6 +25,17 @@ class UserNotificationsController < ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def mark_all_read
|
||||
load_notifications.mark_as_read!
|
||||
render json: { success: true }
|
||||
end
|
||||
|
||||
def toggle_read
|
||||
notification = current_user.notifications.find(params[:id])
|
||||
notification.update(read_at: (params[:mark_as_read] ? DateTime.now : nil))
|
||||
render json: notification, serializer: NotificationSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_notifications
|
||||
|
@ -25,5 +43,4 @@ class UserNotificationsController < ApplicationController
|
|||
.in_app
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -114,10 +114,7 @@ module Users
|
|||
email: auth_hash['info']['email'],
|
||||
password: generate_user_password
|
||||
)
|
||||
if auth_hash['info']['picture_url']
|
||||
avatar = URI.open(auth_hash['info']['picture_url'])
|
||||
@user.avatar.attach(io: avatar, filename: 'linkedin_avatar.jpg')
|
||||
end
|
||||
@user.avatar.attach(io: URI(auth_hash['info']['picture_url']).open, filename: 'linkedin_avatar.jpg') if auth_hash['info']['picture_url']
|
||||
user_identity = UserIdentity.new(user: @user,
|
||||
provider: auth_hash['provider'],
|
||||
uid: auth_hash['uid'])
|
||||
|
|
|
@ -200,7 +200,6 @@ class WopiController < ActionController::Base
|
|||
if @asset.lock == lock
|
||||
logger.warn 'WOPI: replacing file'
|
||||
|
||||
@team.release_space(@asset.estimated_size)
|
||||
@asset.last_modified_by = @user
|
||||
@asset.update_contents(request.body)
|
||||
@asset.save
|
||||
|
@ -220,7 +219,6 @@ class WopiController < ActionController::Base
|
|||
elsif !@asset.file_size.nil? && @asset.file_size.zero?
|
||||
logger.warn 'WOPI: initializing empty file'
|
||||
|
||||
@team.release_space(@asset.estimated_size)
|
||||
@asset.update_contents(request.body)
|
||||
@asset.last_modified_by = @user
|
||||
@asset.save
|
||||
|
|
|
@ -9,7 +9,7 @@ module FormTagHelper
|
|||
res << label_tag(:recaptcha_label, I18n.t('users.registrations.new.captcha_description'))
|
||||
end
|
||||
|
||||
res << recaptcha_tags
|
||||
res << recaptcha_tags(nonce: content_security_policy_nonce)
|
||||
if flash[:recaptcha_error]
|
||||
res << "<span class='help-block'>"
|
||||
res << flash[:recaptcha_error]
|
||||
|
|
|
@ -91,7 +91,7 @@ module GlobalActivitiesHelper
|
|||
end
|
||||
when Protocol
|
||||
if obj.my_module.nil?
|
||||
path = protocols_path(team: obj.team.id)
|
||||
path = protocol_path(obj)
|
||||
elsif obj.my_module.navigable?
|
||||
path = protocols_my_module_path(obj.my_module)
|
||||
else
|
||||
|
|
|
@ -40,11 +40,12 @@ module InputSanitizeHelper
|
|||
preview_repository = options.fetch(:preview_repository, false)
|
||||
format_opt = wrapper_tag.merge(sanitize: false)
|
||||
base64_encoded_imgs = options.fetch(:base64_encoded_imgs, false)
|
||||
text = simple_format(text, {}, format_opt) if simple_f
|
||||
|
||||
# allow base64 images when sanitizing if base64_encoded_imgs is true
|
||||
sanitizer_config = Constants::INPUT_SANITIZE_CONFIG.deep_dup
|
||||
|
||||
text = sanitize_input(text, tags, sanitizer_config: sanitizer_config)
|
||||
text = simple_format(text, {}, format_opt) if simple_f
|
||||
|
||||
text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository) if text.match?(SmartAnnotations::TagToHtml::ALL_REGEX)
|
||||
|
||||
|
|
|
@ -106,4 +106,25 @@ module ReportsHelper
|
|||
experiment_element.experiment.description
|
||||
end
|
||||
end
|
||||
|
||||
def permit_report_settings_structure(settings_definition, repositories)
|
||||
settings_definition.each_with_object([]) do |(key, value), permitted|
|
||||
permitted << if key == :excluded_repository_columns && repositories.present?
|
||||
{ key => repositories.each_with_object({}) { |repository, hash| hash[repository.id.to_s] = [] } }
|
||||
else
|
||||
case value
|
||||
when Hash
|
||||
{ key => permit_report_settings_structure(value, repositories) }
|
||||
when Array
|
||||
{ key => [] }
|
||||
else
|
||||
key
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def custom_templates(templates)
|
||||
templates.any? { |template, _| template != :scinote_template }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,8 +12,7 @@ module RepositoryDatatableHelper
|
|||
repository_row_connections_enabled = Repository.repository_row_connections_enabled?
|
||||
reminders_enabled = Repository.reminders_enabled?
|
||||
stock_managable = has_stock_management && !options[:disable_stock_management] &&
|
||||
can_manage_repository_stock?(repository) &&
|
||||
!repository.is_a?(SoftLockedRepository)
|
||||
can_manage_repository_stock?(repository)
|
||||
stock_consumption_permitted = has_stock_management && options[:include_stock_consumption] && options[:my_module] &&
|
||||
stock_consumption_permitted?(repository, options[:my_module])
|
||||
default_columns_method_name = "#{repository.class.name.underscore}_default_columns"
|
||||
|
@ -31,7 +30,7 @@ module RepositoryDatatableHelper
|
|||
row['relationships_enabled'] = repository_row_connections_enabled
|
||||
row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled
|
||||
|
||||
unless options[:view_mode] || repository.is_a?(SoftLockedRepository)
|
||||
unless options[:view_mode]
|
||||
row['recordUpdateUrl'] =
|
||||
Rails.application.routes.url_helpers.repository_repository_row_path(repository, record)
|
||||
|
||||
|
|
9
app/javascript/packs/tiny_mce.js
vendored
9
app/javascript/packs/tiny_mce.js
vendored
|
@ -489,11 +489,10 @@ window.TinyMCE = (() => {
|
|||
},
|
||||
wrapTables: (container) => {
|
||||
container.find('table').toArray().forEach((table) => {
|
||||
if ($(table).parent().hasClass('table-wrapper')) return;
|
||||
|
||||
$(table).css('float', 'none').wrapAll(`
|
||||
<div class="table-wrapper" style="overflow: auto; width: ${container.width()}px"></div>
|
||||
`);
|
||||
if ($(table).parents('table').length === 0) {
|
||||
$(table).css('float', 'none')
|
||||
.wrapAll('<div class="table-wrapper w-full" style="overflow: auto"></div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
22
app/javascript/packs/vue/design_system/breadcrumbs.js
Normal file
22
app/javascript/packs/vue/design_system/breadcrumbs.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import Breadcrumbs from '../../../vue/shared/breadcrumbs.vue';
|
||||
import { mountWithTurbolinks } from '../helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Very very very long name ', url: '' },
|
||||
{ name: 'Data', url: '' },
|
||||
{ name: 'Very very very very very very very very very very long name ', url: '' },
|
||||
{ name: 'Very very very very very very very long name ', url: '' },
|
||||
{ name: 'Very very very very very long name ', url: '' },
|
||||
{ name: 'Very very very very long name ', url: '' }
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
app.component('Breadcrumbs', Breadcrumbs);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#breadcrumbs');
|
|
@ -1,41 +1,63 @@
|
|||
<template>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification">
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-date">
|
||||
{{ notification.attributes.created_at }}
|
||||
</div>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-title"
|
||||
v-html="notification.attributes.title"
|
||||
:data-seen="notification.attributes.checked"></div>
|
||||
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
|
||||
<div v-if="notification.attributes.breadcrumbs" class="flex items-center flex-wrap gap-0.5">
|
||||
<template v-for="(breadcrumb, index) in notification.attributes.breadcrumbs" :key="index">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<i v-if="index > 0" class="sn-icon sn-icon-right"></i>
|
||||
<a :href="breadcrumb.url" :title="breadcrumb.name" class="truncate max-w-[20ch] inline-block">{{ breadcrumb.name }}</a>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification hover:bg-sn-super-light-grey !px-2 !-mx-2">
|
||||
<div class="flex item-center">
|
||||
<a :href="lastBreadcrumbUrl" @click="toggleRead(true); closeFlyout()" class="hover:no-underline text-black hover:text-black grow">
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-date">
|
||||
{{ notification.attributes.created_at }}
|
||||
</div>
|
||||
</template>
|
||||
</a>
|
||||
<div class="ml-auto cursor-pointer" @click="toggleRead()">
|
||||
<div v-if="!notification.attributes.checked" class="w-2.5 h-2.5 bg-sn-coral rounded-full cursor-pointer"></div>
|
||||
<div v-else class="w-2.5 h-2.5 border-2 border-sn-grey rounded-full border-solid cursor-pointer hover:border-sn-coral"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a :href="lastBreadcrumbUrl" @click="toggleRead(true); closeFlyout()" class="hover:no-underline text-black hover:text-black">
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-title"
|
||||
v-html="notification.attributes.title"
|
||||
:data-seen="notification.attributes.checked"></div>
|
||||
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
|
||||
</a>
|
||||
<div v-if="notification.attributes.breadcrumbs" class="flex items-center flex-wrap gap-0.5">
|
||||
<Breadcrumbs :breadcrumbs="notification.attributes.breadcrumbs" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import Breadcrumbs from '../../shared/breadcrumbs.vue';
|
||||
|
||||
export default {
|
||||
name: 'NotificationItem',
|
||||
props: {
|
||||
notification: Object
|
||||
},
|
||||
components: {
|
||||
Breadcrumbs
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
switch (this.notification.attributes.type_of) {
|
||||
case 'deliver':
|
||||
return 'fas fa-truck';
|
||||
case 'assignment':
|
||||
return 'fas fa-list-alt';
|
||||
case 'recent_changes':
|
||||
return 'fas fa-list-alt';
|
||||
case 'deliver_error':
|
||||
return 'sn-icon sn-icon-alert-warning';
|
||||
lastBreadcrumbUrl() {
|
||||
if (!this.notification.attributes.breadcrumbs) {
|
||||
return null;
|
||||
}
|
||||
return this.notification.attributes.breadcrumbs[this.notification.attributes.breadcrumbs.length - 1]?.url;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeFlyout() {
|
||||
this.$emit('close');
|
||||
},
|
||||
toggleRead(check = false) {
|
||||
const params = {};
|
||||
if (!this.notification.attributes.checked || check) {
|
||||
params.mark_as_read = true;
|
||||
}
|
||||
|
||||
axios.post(this.notification.attributes.toggle_read_url, params)
|
||||
.then((response) => {
|
||||
const notification = response.data.data;
|
||||
this.$emit('updateNotification', notification);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,17 +6,47 @@
|
|||
{{ i18n.t('nav.settings') }}
|
||||
</a>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
@click="changeTab('all')"
|
||||
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
|
||||
:class="{'!text-sn-black border-sn-blue': activeTab === 'all'}"
|
||||
>
|
||||
{{ i18n.t('nav.notifications.all') }}
|
||||
</div>
|
||||
<div
|
||||
@click="changeTab('unread')"
|
||||
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
|
||||
:class="{'!text-sn-black border-sn-blue': activeTab === 'unread'}"
|
||||
>
|
||||
{{ i18n.t('nav.notifications.unread') }}
|
||||
</div>
|
||||
<div
|
||||
@click="changeTab('read')"
|
||||
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
|
||||
:class="{'!text-sn-black border-sn-blue': activeTab === 'read'}"
|
||||
>
|
||||
{{ i18n.t('nav.notifications.read') }}
|
||||
</div>
|
||||
<div v-if="activeTab !== 'read'" class="py-2 ml-auto cursor-pointer" @click="markAllNotificationsAsRead">
|
||||
{{ i18n.t('nav.notifications.read_all') }}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="!mt-0.5">
|
||||
<perfect-scrollbar @wheel="preventPropagation" ref="scrollContainer" class="sci--navigation--notificaitons-flyout-notifications">
|
||||
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="todayNotifications.length">
|
||||
{{ i18n.t('nav.notifications.today') }}
|
||||
</div>
|
||||
<NotificationItem v-for="notification in todayNotifications" :key="notification.type_of + '-' + notification.id"
|
||||
@updateNotification="updateNotification"
|
||||
@close="$emit('close')"
|
||||
:notification="notification" />
|
||||
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="olderNotifications.length">
|
||||
{{ i18n.t('nav.notifications.older') }}
|
||||
</div>
|
||||
<NotificationItem v-for="notification in olderNotifications" :key="notification.type_of + '-' + notification.id"
|
||||
@updateNotification="updateNotification"
|
||||
@close="$emit('close')"
|
||||
:notification="notification" />
|
||||
<div class="next-page-loader">
|
||||
<img src="/images/medium/loading.svg" v-if="loadingPage" />
|
||||
|
@ -37,6 +67,7 @@ export default {
|
|||
},
|
||||
props: {
|
||||
notificationsUrl: String,
|
||||
markAllNotificationsUrl: String,
|
||||
unseenNotificationsCount: Number,
|
||||
preferencesUrl: String
|
||||
},
|
||||
|
@ -45,11 +76,14 @@ export default {
|
|||
notifications: [],
|
||||
nextPageUrl: null,
|
||||
scrollBar: null,
|
||||
loadingPage: false
|
||||
activeTab: 'all',
|
||||
loadingPage: false,
|
||||
firstPageUrl: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.nextPageUrl = this.notificationsUrl;
|
||||
this.firstPageUrl = this.notificationsUrl;
|
||||
this.loadNotifications();
|
||||
},
|
||||
mounted() {
|
||||
|
@ -76,6 +110,27 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
changeTab(tab) {
|
||||
this.activeTab = tab;
|
||||
this.notifications = [];
|
||||
this.nextPageUrl = this.firstPageUrl;
|
||||
this.loadNotifications();
|
||||
},
|
||||
markAllNotificationsAsRead() {
|
||||
axios.post(this.markAllNotificationsUrl)
|
||||
.then(() => {
|
||||
this.notifications = this.notifications.map((n) => {
|
||||
n.attributes.checked = true;
|
||||
return n;
|
||||
});
|
||||
this.$emit('update:unseenNotificationsCount');
|
||||
});
|
||||
},
|
||||
updateNotification(notification) {
|
||||
const index = this.notifications.findIndex((n) => n.id === notification.id);
|
||||
this.notifications.splice(index, 1, notification);
|
||||
this.$emit('update:unseenNotificationsCount');
|
||||
},
|
||||
preventPropagation(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -85,7 +140,7 @@ export default {
|
|||
|
||||
this.loadingPage = true;
|
||||
|
||||
axios.get(this.nextPageUrl)
|
||||
axios.get(this.nextPageUrl, { params: { tab: this.activeTab } })
|
||||
.then((response) => {
|
||||
this.notifications = this.notifications.concat(response.data.data);
|
||||
this.nextPageUrl = response.data.links.next;
|
||||
|
|
|
@ -45,8 +45,9 @@
|
|||
<NotificationsFlyout
|
||||
:preferencesUrl="user.preferences_url"
|
||||
:notificationsUrl="notificationsUrl"
|
||||
:markAllNotificationsUrl="markAllNotificationsUrl"
|
||||
:unseenNotificationsCount="unseenNotificationsCount"
|
||||
@update:unseenNotificationsCount="checkUnseenNotifications()"
|
||||
@update:unseenNotificationsCount="checkUnseenNotifications(false)"
|
||||
@close="$refs.notificationDropdown.$refs.field.click();"/>
|
||||
</template>
|
||||
</GeneralDropdown>
|
||||
|
@ -92,6 +93,7 @@ export default {
|
|||
props: {
|
||||
url: String,
|
||||
notificationsUrl: String,
|
||||
markAllNotificationsUrl: String,
|
||||
unseenNotificationsUrl: String,
|
||||
quickSearchUrl: String,
|
||||
teamsUrl: String,
|
||||
|
@ -119,7 +121,7 @@ export default {
|
|||
|
||||
$(document).on('turbolinks:load', () => {
|
||||
this.notificationsOpened = false;
|
||||
this.checkUnseenNotifications();
|
||||
this.checkUnseenNotifications(false);
|
||||
this.refreshCurrentTeam();
|
||||
this.hideSearch = !!document.getElementById('GlobalSearch');
|
||||
this.globalSearchKey += 1;
|
||||
|
@ -177,11 +179,13 @@ export default {
|
|||
searchValue(e) {
|
||||
window.open(`${this.searchUrl}?q=${e.target.value}`, '_self');
|
||||
},
|
||||
checkUnseenNotifications() {
|
||||
checkUnseenNotifications(repeat = true) {
|
||||
clearTimeout(this.unseenNotificationsTimeout);
|
||||
$.get(this.unseenNotificationsUrl, (result) => {
|
||||
this.unseenNotificationsCount = result.unseen;
|
||||
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
|
||||
if (repeat) {
|
||||
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshCurrentTeam() {
|
||||
|
|
|
@ -26,9 +26,9 @@
|
|||
data-id="true" data-status="asset-present" :data-preview-url=this?.preview_url :href=this?.url>
|
||||
{{ file_name }}
|
||||
</a>
|
||||
<tooltip-preview v-if="tooltipShowing && medium_preview_url" :id="id" :url="url" :file_name="file_name"
|
||||
<TooltipPreview v-if="tooltipShowing && medium_preview_url" :id="id" :url="url" :file_name="file_name"
|
||||
:preview_url="preview_url" :icon_html="icon_html" :medium_preview_url="medium_preview_url">
|
||||
</tooltip-preview>
|
||||
</TooltipPreview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,17 +44,26 @@
|
|||
{{ error }}
|
||||
</div>
|
||||
<input type="file" ref="fileInput" @change="handleFileChange" style="display: none" />
|
||||
|
||||
<Teleport to="body">
|
||||
<ConfirmationModal
|
||||
:title="i18n.t('repositories.modal_delete_asset_value.title')"
|
||||
:description="i18n.t('repositories.modal_delete_asset_value.notice_html')"
|
||||
confirmClass="btn btn-danger"
|
||||
:confirmText="i18n.t('repositories.modal_delete_asset_value.delete')"
|
||||
ref="deleteRepositoryAssetValueModal"
|
||||
></ConfirmationModal>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltipPreview from './TooltipPreview.vue';
|
||||
import ConfirmationModal from '../../shared/confirmation_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryAssetvalue',
|
||||
components: {
|
||||
'tooltip-preview': TooltipPreview
|
||||
},
|
||||
components: { TooltipPreview, ConfirmationModal },
|
||||
data() {
|
||||
return {
|
||||
tooltipShowing: false,
|
||||
|
@ -103,10 +112,12 @@ export default {
|
|||
this.uploadFiles(event.target.files[0]);
|
||||
}
|
||||
},
|
||||
clearFile() {
|
||||
this.$refs.fileInput.value = '';
|
||||
this.error = '';
|
||||
this.updateCell(null);
|
||||
async clearFile() {
|
||||
if (await this.$refs.deleteRepositoryAssetValueModal.show()) {
|
||||
this.$refs.fileInput.value = '';
|
||||
this.error = '';
|
||||
this.updateCell(null);
|
||||
}
|
||||
},
|
||||
uploadFiles(file) {
|
||||
this.uploading = true;
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
<ContentToolbar
|
||||
v-if="orderedElements.length > 2 && insertMenu.length > 0"
|
||||
:insertMenu="insertMenu"
|
||||
@create:table="(...args) => this.createElement('table', ...args)"
|
||||
@create:table="(args) => args ? this.createElement('table', ...args) : this.createElement('table')"
|
||||
@create:text="createElement('text')"
|
||||
@create:file="openLoadFromComputer"
|
||||
@create:wopi_file="openWopiFileModal"
|
||||
|
|
93
app/javascript/vue/shared/breadcrumbs.vue
Normal file
93
app/javascript/vue/shared/breadcrumbs.vue
Normal file
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div ref="container" class="w-full flex items-center flex-wrap gap-0.5">
|
||||
<template v-if="!allVisible">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<a :href="breadcrumbs[0].url" :title="breadcrumbs[0].name" class="max-w-[200px]">
|
||||
<StringWithEllipsis class="w-full" :text="breadcrumbs[0].name"></StringWithEllipsis>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<i class="sn-icon sn-icon-right text-sn-grey"></i>
|
||||
<GeneralDropdown>
|
||||
<template v-slot:field>
|
||||
<a>...</a>
|
||||
</template>
|
||||
<template v-slot:flyout>
|
||||
<div class="max-w-[600px]">
|
||||
<div v-for="(breadcrumb, index) in hiddenBreadcrumbs" :key="index" class="p-2 hover:bg-sn-super-light-grey cursor-pointer">
|
||||
<a :href="breadcrumb.url" :title="breadcrumb.name" class="max-w-[200px] hover:no-underline">
|
||||
<StringWithEllipsis class="w-full" :text="breadcrumb.name"></StringWithEllipsis>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</GeneralDropdown>
|
||||
<i class="sn-icon sn-icon-right text-sn-grey"></i>
|
||||
</div>
|
||||
</template>
|
||||
<div v-for="(breadcrumb, index) in visibleBreadcrumbs" :key="index" class="flex items-center gap-0.5">
|
||||
<i v-if="index > 0" class="sn-icon sn-icon-right text-sn-grey"></i>
|
||||
<a :href="breadcrumb.url" :title="breadcrumb.name" class="max-w-[200px]">
|
||||
<StringWithEllipsis class="w-full" :text="breadcrumb.name"></StringWithEllipsis>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StringWithEllipsis from './string_with_ellipsis.vue';
|
||||
import GeneralDropdown from './general_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'Breadcrumbs',
|
||||
props: {
|
||||
breadcrumbs: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StringWithEllipsis,
|
||||
GeneralDropdown
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerSize: 0,
|
||||
breadcrumbSize: 200
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
breadcrumbsVisibleCount() {
|
||||
const size = Math.floor(this.containerSize / this.breadcrumbSize);
|
||||
if (size < 2) {
|
||||
return 2;
|
||||
}
|
||||
return size;
|
||||
},
|
||||
allVisible() {
|
||||
return this.breadcrumbs.length <= this.breadcrumbsVisibleCount;
|
||||
},
|
||||
hiddenBreadcrumbs() {
|
||||
return this.breadcrumbs.slice(1, this.breadcrumbs.length - this.breadcrumbsVisibleCount + 1);
|
||||
},
|
||||
visibleBreadcrumbs() {
|
||||
if (this.allVisible) {
|
||||
return this.breadcrumbs;
|
||||
}
|
||||
return this.breadcrumbs.slice(this.breadcrumbs.length - this.breadcrumbsVisibleCount + 1);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setContainerSize();
|
||||
window.addEventListener('resize', this.setContainerSize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.setContainerSize);
|
||||
},
|
||||
methods: {
|
||||
setContainerSize() {
|
||||
this.containerSize = this.$refs.container.offsetWidth;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>;
|
|
@ -27,6 +27,7 @@
|
|||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@menu-toggle="$emit('attachment:toggle_menu', $event)"
|
||||
@attachment:versionRestored="$emit('attachment:versionRestored', $event)"
|
||||
:withBorder="withBorder"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
@duplicate="duplicate"
|
||||
@viewMode="changeViewMode"
|
||||
@move="showMoveModal"
|
||||
@fileVersionsModal="fileVersionsModal = true"
|
||||
@menu-toggle="$emit('menu-toggle', $event)"
|
||||
></MenuDropdown>
|
||||
<Teleport to="body">
|
||||
|
@ -55,6 +56,13 @@
|
|||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
|
||||
/>
|
||||
<FileVersionsModal
|
||||
v-if="fileVersionsModal"
|
||||
:versionsUrl="attachment.attributes.urls.versions"
|
||||
:restoreVersionUrl="attachment.attributes.urls.restore_version"
|
||||
@close="fileVersionsModal = false"
|
||||
@fileVersionRestored="$emit('attachment:versionRestored', $event)"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -65,6 +73,7 @@ import deleteAttachmentModal from './delete_modal.vue';
|
|||
import MoveAssetModal from '../modal/move.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import MenuDropdown from '../../menu_dropdown.vue';
|
||||
import FileVersionsModal from '../../file_versions_modal.vue';
|
||||
import axios from '../../../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
|
@ -73,6 +82,7 @@ export default {
|
|||
RenameAttachmentModal,
|
||||
deleteAttachmentModal,
|
||||
MoveAssetModal,
|
||||
FileVersionsModal,
|
||||
MenuDropdown
|
||||
},
|
||||
mixins: [MoveMixin],
|
||||
|
@ -91,7 +101,8 @@ export default {
|
|||
return {
|
||||
viewModeOptions: ['inline', 'thumbnail', 'list'],
|
||||
deleteModal: false,
|
||||
renameModal: false
|
||||
renameModal: false,
|
||||
fileVersionsModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -124,6 +135,12 @@ export default {
|
|||
data_e2e: 'e2e-BT-attachmentOptions-delete'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.urls.versions) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.context_menu.versions'),
|
||||
emit: 'fileVersionsModal'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.urls.toggle_view_mode) {
|
||||
this.viewModeOptions.forEach((viewMode, i) => {
|
||||
menu.push({
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:versionRestored="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenuDropdown"
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:versionRestored="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenuDropdown"
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
<template>
|
||||
<div class="sn-open-locally-menu" @mouseenter="fetchLocalAppInfo">
|
||||
<div v-if="(!canOpenLocally || disableLocalOpen) && (attachment.attributes.wopi && attachment.attributes.urls.edit_asset)">
|
||||
<a :href="editWopiSupported ? attachment.attributes.urls.edit_asset : null" target="_blank"
|
||||
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey"
|
||||
:class="{ 'disabled': !editWopiSupported }"
|
||||
:title="editWopiSupported ? null : attachment.attributes.wopi_context.title"
|
||||
style="pointer-events: all"
|
||||
>
|
||||
{{ attachment.attributes.wopi_context.button_text }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="!usesWebIntegration">
|
||||
<MenuDropdown
|
||||
v-if="this.menu.length > 1"
|
||||
class="ml-auto"
|
||||
:listItems="this.menu"
|
||||
:btnClasses="`btn btn-light icon-btn`"
|
||||
:position="'right'"
|
||||
:btnText="i18n.t('attachments.open_in')"
|
||||
:caret="true"
|
||||
@open-locally="openLocally"
|
||||
@open-image-editor="openImageEditor"
|
||||
></MenuDropdown>
|
||||
<a v-else-if="menu.length === 1" class="btn btn-light" :href="menu[0].url" :target="menu[0].url_target" @click="this[this.menu[0].emit]()">
|
||||
{{ menu[0].text }}
|
||||
<div class="sn-open-locally-menu flex" @mouseenter="fetchLocalAppInfo">
|
||||
<template v-if="canEdit">
|
||||
<div v-if="(!canOpenLocally) && (attachment.attributes.wopi && attachment.attributes.urls.edit_asset)">
|
||||
<a :href="editWopiSupported ? attachment.attributes.urls.edit_asset : null" target="_blank"
|
||||
class="btn btn-light"
|
||||
:class="{ 'disabled': !editWopiSupported }"
|
||||
:title="editWopiSupported ? null : attachment.attributes.wopi_context.title"
|
||||
style="pointer-events: all"
|
||||
>
|
||||
{{ attachment.attributes.wopi_context.button_text }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!usesWebIntegration">
|
||||
<MenuDropdown
|
||||
v-if="this.menu.length > 1"
|
||||
class="ml-auto"
|
||||
:listItems="this.menu"
|
||||
:btnClasses="`btn btn-light icon-btn`"
|
||||
:position="'right'"
|
||||
:btnText="i18n.t('attachments.open_in')"
|
||||
:caret="true"
|
||||
@open-locally="openLocally"
|
||||
@open-image-editor="openImageEditor"
|
||||
></MenuDropdown>
|
||||
<a v-else-if="menu.length === 1" class="btn btn-light" :href="menu[0].url" :target="menu[0].url_target" @click="this[this.menu[0].emit]()">
|
||||
{{ menu[0].text }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<a @click="fileVersionsModal = true" class="btn btn-light"><i class="sn-icon sn-icon-history-search"></i>{{ i18n.t('assets.context_menu.versions') }}</a>
|
||||
|
||||
<Teleport to="body">
|
||||
<NoPredefinedAppModal
|
||||
|
@ -47,6 +50,13 @@
|
|||
v-if="showUpdateVersionModal"
|
||||
@close="showUpdateVersionModal = false"
|
||||
/>
|
||||
<FileVersionsModal
|
||||
v-if="fileVersionsModal"
|
||||
:versionsUrl="attachment.attributes.urls.versions"
|
||||
:restoreVersionUrl="attachment.attributes.urls.restore_version"
|
||||
@close="fileVersionsModal = false"
|
||||
@fileVersionRestored="refreshPreview"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -55,14 +65,20 @@
|
|||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import MenuDropdown from '../../menu_dropdown.vue';
|
||||
import UpdateVersionModal from '../modal/update_version_modal.vue';
|
||||
import FileVersionsModal from '../../file_versions_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'OpenLocallyMenu',
|
||||
mixins: [OpenLocallyMixin],
|
||||
components: { MenuDropdown, UpdateVersionModal },
|
||||
components: { MenuDropdown, UpdateVersionModal, FileVersionsModal },
|
||||
props: {
|
||||
attachment: { type: Object, required: true },
|
||||
disableLocalOpen: { type: Boolean, default: false }
|
||||
canEdit: { type: Boolean, default: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fileVersionsModal: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchLocalAppInfo();
|
||||
|
@ -92,7 +108,7 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.canOpenLocally && !this.disableLocalOpen) {
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
@ -116,6 +132,13 @@ export default {
|
|||
methods: {
|
||||
openImageEditor() {
|
||||
document.getElementById('editImageButton').click();
|
||||
},
|
||||
refreshPreview() {
|
||||
const filePreview = document.querySelector('.file-preview-container');
|
||||
|
||||
if (!filePreview) return;
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:versionRestored="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@attachment:toggle_menu="toggleMenu"
|
||||
|
|
143
app/javascript/vue/shared/file_versions_modal.vue
Normal file
143
app/javascript/vue/shared/file_versions_modal.vue
Normal file
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="close" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button @click="close" type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title">
|
||||
{{ i18n.t("assets.file_versions_modal.title") }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="relative" v-if="fileVersions">
|
||||
<div v-for="(fileVersion, index) in fileVersions" :key="fileVersion.id">
|
||||
<div class="flex w-full border border-sn-light-grey rounded mb-1.5 p-1.5 items-center">
|
||||
<div class="basis-full">
|
||||
<div class="mb-1.5">
|
||||
<span v-if="fileVersion.attributes.version === 1" class="bg-sn-science-blue text-sn-white me-2 px-2 py-0.5 rounded">
|
||||
{{ i18n.t("assets.file_versions_modal.original_file") }}
|
||||
</span>
|
||||
<span v-else class="bg-sn-grey-300 me-2 px-2 py-0.5 rounded">v{{ fileVersion.attributes.version }}</span>
|
||||
<a :href="fileVersion.attributes.url" target="_blank" class="align-text-bottom">
|
||||
<span :class="{
|
||||
'max-w-80': !fileVersion.attributes.restored_from_version && fileVersion.attributes.version !== 1,
|
||||
'max-w-64': !!fileVersion.attributes.restored_from_version || fileVersion.attributes.version === 1
|
||||
}"
|
||||
class="text-ellipsis overflow-hidden text-nowrap inline-block align-middle"
|
||||
>{{ fileVersion.attributes.basename }}.</span><span class="inline-block align-middle">{{ fileVersion.attributes.extension }}</span>
|
||||
</a>
|
||||
<small class="inline-block ml-1" v-if="fileVersion.attributes.restored_from_version">
|
||||
({{ i18n.t("assets.file_versions_modal.restored_from_version", { version: fileVersion.attributes.restored_from_version }) }})
|
||||
</small>
|
||||
</div>
|
||||
<div class="flex text-xs text-sn-grey justify-start">
|
||||
<div class="mr-3">{{ fileVersion.attributes.created_at }}</div>
|
||||
<div v-if="fileVersion.attributes.created_by" class="mr-3 text-nowrap text-ellipsis overflow-hidden max-w-52">
|
||||
{{ fileVersion.attributes.created_by.full_name }}
|
||||
</div>
|
||||
<div>{{ i18n.t("assets.file_versions_modal.size") }}: {{ Math.round(fileVersion.attributes.byte_size/1024) }}KB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="basis-1/4 flex justify-end">
|
||||
<a class="btn btn-icon p-0 px-2 hover:bg-sn-light-grey"
|
||||
v-if="enabled || index === 0"
|
||||
:href="fileVersion.attributes.download_url"
|
||||
target="_blank"
|
||||
data-render-tooltip="true"
|
||||
:title="i18n.t('assets.file_versions_modal.download')"
|
||||
>
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
<a v-if="restoreVersionUrl && index !== 0"
|
||||
@click="restoreVersion(fileVersion.attributes.version)"
|
||||
data-render-tooltip="true"
|
||||
:title="i18n.t('assets.file_versions_modal.restore')"
|
||||
class="btn btn-icon p-0 px-2 hover:bg-sn-light-grey"
|
||||
>
|
||||
<i class="sn-icon sn-icon-restore"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!enabled" class="absolute bottom-0 w-full h-[150px] bg-gradient-to-t from-white to-transparent"></div>
|
||||
</div>
|
||||
<div v-else class="sci-loader"></div>
|
||||
<div v-if="fileVersions && !enabled" class="bg-sn-super-light-blue p-4 rounded flex items-start">
|
||||
<div class="mr-2">
|
||||
<i class="sn-icon sn-icon-upgrade"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mt-1 mb-2">{{ i18n.t('assets.file_versions_modal.title') }}</h3>
|
||||
{{ disabledDisclaimer.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type='button' class='btn btn-secondary' @click="close">
|
||||
{{ i18n.t('general.cancel') }}
|
||||
</button>
|
||||
<a v-if="fileVersions && !enabled" :href="enableUrl" class='btn btn-primary' target="_blank">
|
||||
{{ disabledDisclaimer.button }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalMixin from './modal_mixin';
|
||||
import axios from '../../packs/custom_axios';
|
||||
|
||||
export default {
|
||||
name: 'FileVersionsModal',
|
||||
props: {
|
||||
versionsUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
restoreVersionUrl: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
fileVersions: null,
|
||||
enabled: null,
|
||||
enableUrl: null,
|
||||
disabledDisclaimer: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.loadVersions();
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.querySelectorAll('[data-render-tooltip]').forEach((e) => {
|
||||
window.destroyTooltip(e);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
loadVersions() {
|
||||
axios.get(this.versionsUrl).then((response) => {
|
||||
this.fileVersions = response.data.data;
|
||||
this.enabled = response.data.enabled;
|
||||
this.enableUrl = response.data.enable_url;
|
||||
this.disabledDisclaimer = response.data.disabled_disclaimer;
|
||||
this.$nextTick(() => {
|
||||
document.querySelectorAll('[data-render-tooltip]').forEach((e) => {
|
||||
window.initTooltip(e);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
restoreVersion(version) {
|
||||
axios.post(this.restoreVersionUrl, { version: version }).then(() => {
|
||||
this.loadVersions();
|
||||
this.$emit('fileVersionRestored');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -44,11 +44,11 @@
|
|||
<span class="sci-radio-label"></span>
|
||||
</div>
|
||||
<span>{{ i18n.t('storage_locations.index.edit_modal.grid') }}</span>
|
||||
<div class="sci-input-container-v2 !w-28">
|
||||
<div class="sci-input-container-v2 !w-28" v-if="object.metadata.dimensions">
|
||||
<input type="number" :disabled="!canChangeGrid" v-model="object.metadata.dimensions[0]" min="1" max="24">
|
||||
</div>
|
||||
<i class="sn-icon sn-icon-close-small"></i>
|
||||
<div class="sci-input-container-v2 !w-28">
|
||||
<div class="sci-input-container-v2 !w-28" v-if="object.metadata.dimensions">
|
||||
<input type="number" :disabled="!canChangeGrid" v-model="object.metadata.dimensions[1]" min="1" max="24">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module MyModules
|
||||
class DueDateReminderJob < ApplicationJob
|
||||
def perform
|
||||
NewRelic::Agent.ignore_transaction
|
||||
my_modules = MyModule.uncomplete.approaching_due_dates
|
||||
|
||||
my_modules.each do |task|
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class NotificationCleanupJob < ApplicationJob
|
||||
def perform
|
||||
NewRelic::Agent.ignore_transaction
|
||||
Notification.where('created_at < ?', 3.months.ago).delete_all
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,39 +16,9 @@ class PdfPreviewJob < ApplicationJob
|
|||
|
||||
def perform(asset_id)
|
||||
asset = Asset.find(asset_id)
|
||||
blob = asset.file.blob
|
||||
blob.open(tmpdir: tempdir) do |input|
|
||||
work_dir = File.dirname(input.path)
|
||||
preview_filename = "#{File.basename(input.path, '.*')}.pdf"
|
||||
preview_file = File.join(work_dir, preview_filename)
|
||||
Rails.logger.info "Starting preparing document preview for file #{blob.filename.sanitized}..."
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
success = system(
|
||||
libreoffice_path, '--headless', '--invisible', '--convert-to', 'pdf', '--outdir', work_dir, input.path
|
||||
)
|
||||
unless success && File.file?(preview_file)
|
||||
raise StandardError, "There was an error generating PDF preview, blob id: #{blob.id}"
|
||||
end
|
||||
|
||||
ActiveRecord::Base.no_touching do
|
||||
asset.file_pdf_preview.attach(io: File.open(preview_file), filename: "#{blob.filename.base}.pdf")
|
||||
asset.update(pdf_preview_processing: false)
|
||||
end
|
||||
Rails.logger.info("Finished preparing PDF preview for file #{blob.filename.sanitized}.")
|
||||
end
|
||||
ensure
|
||||
File.delete(preview_file) if File.file?(preview_file)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tempdir
|
||||
Rails.root.join('tmp')
|
||||
end
|
||||
|
||||
def libreoffice_path
|
||||
ENV['LIBREOFFICE_PATH'] || 'soffice'
|
||||
PdfPreviewService.new(asset.file.blob, asset.file_pdf_preview).generate!
|
||||
ensure
|
||||
asset.update(pdf_preview_processing: false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -117,7 +117,7 @@ module Protocols
|
|||
def create_step_asset_element!(step, step_element_json)
|
||||
asset = @team.assets.new(created_by: @user, last_modified_by: @user)
|
||||
# Decode the file bytes
|
||||
asset.file.attach(io: StringIO.new(Base64.decode64(step_element_json['contents'])), filename: 'file.blob')
|
||||
asset.attach_file_version(io: StringIO.new(Base64.decode64(step_element_json['contents'])), filename: 'file.blob')
|
||||
asset.save!
|
||||
step.step_assets.create!(asset: asset)
|
||||
asset.post_process_file
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'caracal'
|
||||
|
||||
module Reports
|
||||
class DocxJob < ApplicationJob
|
||||
extend InputSanitizeHelper
|
||||
|
|
|
@ -15,39 +15,7 @@ module Reports
|
|||
|
||||
def perform(report_id)
|
||||
report = Report.find(report_id)
|
||||
blob = report.docx_file.blob
|
||||
blob.open(tmpdir: tempdir) do |input|
|
||||
work_dir = File.dirname(input.path)
|
||||
preview_filename = "#{File.basename(input.path, '.*')}.pdf"
|
||||
preview_file = File.join(work_dir, preview_filename)
|
||||
Rails.logger.info "Starting preparing document preview for file #{blob.filename.sanitized}..."
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
success = system(
|
||||
libreoffice_path, '--headless', '--invisible', '--convert-to', 'pdf', '--outdir', work_dir, input.path
|
||||
)
|
||||
unless success && File.file?(preview_file)
|
||||
raise StandardError, "There was an error generating PDF preview, blob id: #{blob.id}"
|
||||
end
|
||||
|
||||
ActiveRecord::Base.no_touching do
|
||||
report.docx_preview_file.attach(io: File.open(preview_file), filename: "#{blob.filename.base}.pdf")
|
||||
end
|
||||
Rails.logger.info("Finished preparing PDF preview for file #{blob.filename.sanitized}.")
|
||||
end
|
||||
ensure
|
||||
File.delete(preview_file) if File.file?(preview_file)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tempdir
|
||||
Rails.root.join('tmp')
|
||||
end
|
||||
|
||||
def libreoffice_path
|
||||
ENV['LIBREOFFICE_PATH'] || 'soffice'
|
||||
PdfPreviewService.new(report.docx_file.blob, report.docx_preview_file).generate!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ class RepositoryItemDateReminderJob < ApplicationJob
|
|||
queue_as :default
|
||||
|
||||
def perform
|
||||
NewRelic::Agent.ignore_transaction
|
||||
process_repository_values(RepositoryDateTimeValue, DateTime.current)
|
||||
process_repository_values(RepositoryDateValue, Date.current)
|
||||
end
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
class Asset < ApplicationRecord
|
||||
include SearchableModel
|
||||
include DatabaseHelper
|
||||
include Encryptor
|
||||
include WopiUtil
|
||||
include ActiveStorageFileUtil
|
||||
include ActiveStorageConcerns
|
||||
include ActiveStorageHelper
|
||||
include VersionedAttachments
|
||||
|
||||
require 'tempfile'
|
||||
# Lock duration set to 30 minutes
|
||||
|
@ -17,9 +17,9 @@ class Asset < ApplicationRecord
|
|||
enum view_mode: { thumbnail: 0, list: 1, inline: 2 }
|
||||
|
||||
# ActiveStorage configuration
|
||||
has_one_attached :file
|
||||
has_one_versioned_attached :file
|
||||
has_one_versioned_attached :preview_image
|
||||
has_one_attached :file_pdf_preview
|
||||
has_one_attached :preview_image
|
||||
|
||||
# Asset validation
|
||||
# This could cause some problems if you create empty asset and want to
|
||||
|
@ -145,41 +145,67 @@ class Asset < ApplicationRecord
|
|||
file&.blob&.content_type
|
||||
end
|
||||
|
||||
def duplicate(new_name: nil)
|
||||
def duplicate(new_name: nil, include_file_versions: false, created_by: nil)
|
||||
new_asset = dup
|
||||
file.filename = new_name if new_name
|
||||
|
||||
if created_by
|
||||
new_asset.created_by = created_by
|
||||
new_asset.last_modified_by = created_by
|
||||
end
|
||||
|
||||
return unless new_asset.save
|
||||
|
||||
duplicate_file(new_asset)
|
||||
duplicate_file(new_asset, new_name: new_name, include_file_versions: include_file_versions)
|
||||
|
||||
new_asset
|
||||
end
|
||||
|
||||
def duplicate_file(to_asset)
|
||||
def duplicate_blob(blob, attach_method, metadata: nil)
|
||||
new_blob = nil
|
||||
|
||||
blob.open do |tmp_file|
|
||||
new_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: tmp_file,
|
||||
filename: blob.filename,
|
||||
metadata: (metadata || blob.metadata)
|
||||
)
|
||||
|
||||
attach_method.call(new_blob)
|
||||
end
|
||||
|
||||
new_blob
|
||||
end
|
||||
|
||||
def duplicate_file_versions(to_asset)
|
||||
previous_files.map(&:blob).sort_by(&:created_at).each do |blob|
|
||||
duplicate_blob(blob, to_asset.previous_files.method(:attach)) # .update_column(:created_at, blob.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
def duplicate_file(to_asset, new_name: nil, include_file_versions: false)
|
||||
return unless file.attached?
|
||||
|
||||
raise ArgumentError, 'Destination asset should be persisted first!' unless to_asset.persisted?
|
||||
|
||||
file.blob.open do |tmp_file|
|
||||
to_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: tmp_file,
|
||||
filename: blob.filename,
|
||||
metadata: blob.metadata
|
||||
)
|
||||
to_asset.file.attach(to_blob)
|
||||
metadata = file.blob.metadata.dup
|
||||
|
||||
# set new name in metadata for OVE and MarvinJS files
|
||||
if new_name && file.blob.metadata['asset_type'].in?(%w(gene_sequence marvinjs))
|
||||
new_metadata_name = File.basename(new_name, File.extname(new_name))
|
||||
metadata['name'] = new_metadata_name
|
||||
end
|
||||
|
||||
if preview_image.attached?
|
||||
preview_image.blob.open do |tmp_preview_image|
|
||||
to_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: tmp_preview_image,
|
||||
filename: blob.filename,
|
||||
metadata: blob.metadata
|
||||
)
|
||||
to_asset.preview_image.attach(to_blob)
|
||||
end
|
||||
unless include_file_versions
|
||||
metadata.delete('version')
|
||||
metadata.delete('restored_from_version')
|
||||
metadata['created_by_id'] = to_asset.created_by_id
|
||||
end
|
||||
|
||||
duplicate_file_versions(to_asset) if include_file_versions
|
||||
duplicate_blob(blob, to_asset.method(:attach_file_version), metadata: metadata)
|
||||
duplicate_blob(preview_image.blob, to_asset.method(:attach_preview_image_version)) if preview_image.attached?
|
||||
|
||||
to_asset.post_process_file
|
||||
end
|
||||
|
||||
|
@ -341,7 +367,7 @@ class Asset < ApplicationRecord
|
|||
end
|
||||
|
||||
def update_contents(new_file)
|
||||
file.attach(io: new_file, filename: file_name)
|
||||
attach_file_version(io: new_file, filename: file_name)
|
||||
self.version = version.nil? ? 1 : version + 1
|
||||
save
|
||||
end
|
||||
|
|
61
app/models/concerns/versioned_attachments.rb
Normal file
61
app/models/concerns/versioned_attachments.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module VersionedAttachments
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def has_one_versioned_attached(name)
|
||||
has_one_attached name, dependent: :detach
|
||||
has_many_attached "previous_#{name.to_s.pluralize}", dependent: :detach
|
||||
|
||||
define_method :"attach_#{name}_version" do |*args, **options|
|
||||
ActiveRecord::Base.transaction(requires_new: true) do
|
||||
__send__(:"previous_#{name.to_s.pluralize}").attach(__send__(name).blob) if __send__(name).attached?
|
||||
__send__(name).attach(*args, **options)
|
||||
|
||||
new_blob = __send__(name).blob
|
||||
new_blob.metadata['created_by_id'] ||= last_modified_by_id
|
||||
|
||||
# set version of current latest file if previous versions exist
|
||||
new_blob.save! and next unless __send__(:"previous_#{name.to_s.pluralize}").any?
|
||||
|
||||
new_version =
|
||||
(__send__(:"previous_#{name.to_s.pluralize}").last.blob.metadata['version'] || 1) + 1
|
||||
new_blob.metadata['version'] = new_version
|
||||
new_blob.save!
|
||||
end
|
||||
end
|
||||
|
||||
define_method :"restore_#{name}_version" do |version|
|
||||
ActiveRecord::Base.transaction(requires_new: true) do
|
||||
blob = __send__(:"previous_#{name.to_s.pluralize}").map(&:blob).find do |b|
|
||||
(b.metadata['version'] || 1) == version
|
||||
end
|
||||
|
||||
blob.open do |tmp_file|
|
||||
new_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: tmp_file,
|
||||
filename: blob.filename,
|
||||
metadata: blob.metadata.merge({ 'restored_from_version' => version, 'created_by_id' => last_modified_by_id })
|
||||
)
|
||||
|
||||
__send__(:"attach_#{name}_version", new_blob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module_function
|
||||
|
||||
def enabled?
|
||||
ApplicationSettings.instance.values['versioned_attachments_enabled']
|
||||
end
|
||||
|
||||
def disabled_disclaimer
|
||||
{
|
||||
text: I18n.t('assets.file_versions_modal.disabled_disclaimer'),
|
||||
button: I18n.t('assets.file_versions_modal.enable_button')
|
||||
}
|
||||
end
|
||||
end
|
|
@ -24,9 +24,9 @@ class LinkedRepository < Repository
|
|||
'repository_rows.created_at',
|
||||
'users.full_name',
|
||||
'repository_rows.updated_at',
|
||||
'last_modified_bies_repository_rows.full_name',
|
||||
'last_modified_by.full_name',
|
||||
'repository_rows.archived_on',
|
||||
'archived_bies_repository_rows.full_name',
|
||||
'archived_by.full_name',
|
||||
'repository_rows.external_id'
|
||||
]
|
||||
end
|
||||
|
|
|
@ -391,17 +391,17 @@ class MyModule < ApplicationRecord
|
|||
{ data: data, headers: headers }
|
||||
end
|
||||
|
||||
def repository_docx_json(repository)
|
||||
headers = [
|
||||
I18n.t('repositories.table.id'),
|
||||
I18n.t('repositories.table.row_name'),
|
||||
I18n.t('repositories.table.added_on'),
|
||||
I18n.t('repositories.table.added_by')
|
||||
]
|
||||
def repository_docx_json(repository, excluded_columns)
|
||||
headers = Report.default_repository_columns.filter_map do |key, value|
|
||||
value unless excluded_columns.include?(key.to_s.to_i)
|
||||
end
|
||||
|
||||
custom_columns = []
|
||||
return false unless repository
|
||||
|
||||
repository.repository_columns.order(:id).each do |column|
|
||||
next if excluded_columns.include?(column.id)
|
||||
|
||||
if column.data_type == 'RepositoryStockValue'
|
||||
if repository.has_stock_consumption?
|
||||
headers.push(I18n.t('repositories.table.row_consumption'))
|
||||
|
@ -416,7 +416,7 @@ class MyModule < ApplicationRecord
|
|||
|
||||
records = repository.assigned_rows(self)
|
||||
.select(:id, :name, :created_at, :created_by_id, :repository_id, :parent_id, :archived)
|
||||
{ headers: headers, rows: records, custom_columns: custom_columns }
|
||||
{ headers: headers, rows: records, custom_columns: custom_columns, excluded_columns: excluded_columns }
|
||||
end
|
||||
|
||||
def deep_clone(current_user)
|
||||
|
@ -565,6 +565,12 @@ class MyModule < ApplicationRecord
|
|||
|
||||
yield
|
||||
|
||||
if status_changing_direction == :forward
|
||||
my_module_status.my_module_status_consequences.each do |consequence|
|
||||
consequence.before_forward_call(self)
|
||||
end
|
||||
end
|
||||
|
||||
if my_module_status.my_module_status_consequences.any?(&:runs_in_background?)
|
||||
MyModuleStatusConsequencesJob
|
||||
.perform_later(self, my_module_status.my_module_status_consequences.to_a, status_changing_direction)
|
||||
|
|
|
@ -7,6 +7,8 @@ class MyModuleStatusConsequence < ApplicationRecord
|
|||
|
||||
def backward(my_module); end
|
||||
|
||||
def before_forward_call(my_module); end
|
||||
|
||||
def runs_in_background?
|
||||
false
|
||||
end
|
||||
|
|
|
@ -6,9 +6,14 @@ module MyModuleStatusConsequences
|
|||
true
|
||||
end
|
||||
|
||||
def forward(my_module)
|
||||
def before_forward_call(my_module)
|
||||
my_module.assigned_repositories.each do |repository|
|
||||
repository_snapshot = ::RepositorySnapshot.create_preliminary!(repository, my_module)
|
||||
::RepositorySnapshot.create_preliminary!(repository, my_module)
|
||||
end
|
||||
end
|
||||
|
||||
def forward(my_module)
|
||||
my_module.repository_snapshots.where(status: :provisioning).find_each do |repository_snapshot|
|
||||
service = Repositories::SnapshotProvisioningService.call(repository_snapshot: repository_snapshot)
|
||||
|
||||
unless service.succeed?
|
||||
|
|
|
@ -70,27 +70,10 @@ class Protocol < ApplicationRecord
|
|||
with_options if: :in_repository_published_version? do
|
||||
validates :parent, presence: true
|
||||
validate :parent_type_constraint
|
||||
validate :versions_same_name_constraint
|
||||
end
|
||||
with_options if: :in_repository_draft? do
|
||||
# Only one draft can exist for each protocol
|
||||
validate :ensure_single_draft
|
||||
validate :versions_same_name_constraint
|
||||
end
|
||||
with_options if: -> { in_repository? && !parent && !archived_changed?(from: false) } do |protocol|
|
||||
# Active protocol must have unique name inside its team
|
||||
protocol
|
||||
.validates_uniqueness_of :name, case_sensitive: false,
|
||||
scope: :team,
|
||||
conditions: lambda {
|
||||
where(
|
||||
protocol_type: [
|
||||
Protocol.protocol_types[:in_repository_published_original],
|
||||
Protocol.protocol_types[:in_repository_draft]
|
||||
],
|
||||
parent_id: nil
|
||||
)
|
||||
}
|
||||
end
|
||||
with_options if: -> { in_repository? && archived? && !previous_version } do |protocol|
|
||||
protocol.validates :archived_by, presence: true
|
||||
|
@ -351,7 +334,9 @@ class Protocol < ApplicationRecord
|
|||
end
|
||||
|
||||
# Deep-clone given array of assets
|
||||
def self.deep_clone_assets(assets_to_clone)
|
||||
# There is an issue with Delayed Job delayed methods, ruby 3, and keyword arguments (https://github.com/collectiveidea/delayed_job/issues/1134)
|
||||
# so we're forced to use a normal argument with default value.
|
||||
def self.deep_clone_assets(assets_to_clone, include_file_versions = false)
|
||||
ActiveRecord::Base.no_touching do
|
||||
assets_to_clone.each do |src_id, dest_id|
|
||||
src = Asset.find_by(id: src_id)
|
||||
|
@ -360,12 +345,12 @@ class Protocol < ApplicationRecord
|
|||
next unless src.present? && dest.present?
|
||||
|
||||
# Clone file
|
||||
src.duplicate_file(dest)
|
||||
src.duplicate_file(dest, include_file_versions: include_file_versions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.clone_contents(src, dest, current_user, clone_keywords, only_contents = false)
|
||||
def self.clone_contents(src, dest, current_user, clone_keywords, only_contents: false, include_file_versions: false)
|
||||
dest.update(description: src.description, name: src.name) unless only_contents
|
||||
|
||||
src.clone_tinymce_assets(dest, dest.team)
|
||||
|
@ -382,7 +367,7 @@ class Protocol < ApplicationRecord
|
|||
|
||||
# Copy steps
|
||||
src.steps.each do |step|
|
||||
step.duplicate(dest, current_user, step_position: step.position)
|
||||
step.duplicate(dest, current_user, step_position: step.position, include_file_versions: include_file_versions)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -615,7 +600,7 @@ class Protocol < ApplicationRecord
|
|||
return draft if draft.invalid?
|
||||
|
||||
ActiveRecord::Base.no_touching do
|
||||
draft = deep_clone(draft, current_user)
|
||||
draft = deep_clone(draft, current_user, include_file_versions: true)
|
||||
end
|
||||
|
||||
parent_protocol.user_assignments.each do |parent_user_assignment|
|
||||
|
@ -760,7 +745,7 @@ class Protocol < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def deep_clone(clone, current_user)
|
||||
def deep_clone(clone, current_user, include_file_versions: false)
|
||||
# Save cloned protocol first
|
||||
success = clone.save
|
||||
|
||||
|
@ -772,7 +757,7 @@ class Protocol < ApplicationRecord
|
|||
|
||||
raise ActiveRecord::RecordNotSaved unless success
|
||||
|
||||
Protocol.clone_contents(self, clone, current_user, true, true)
|
||||
Protocol.clone_contents(self, clone, current_user, true, only_contents: true, include_file_versions: include_file_versions)
|
||||
|
||||
clone.reload
|
||||
clone
|
||||
|
@ -794,12 +779,6 @@ class Protocol < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def versions_same_name_constraint
|
||||
if parent.present? && !parent.name.eql?(name)
|
||||
errors.add(:base, I18n.t('activerecord.errors.models.protocol.wrong_version_name'))
|
||||
end
|
||||
end
|
||||
|
||||
def version_number_constraint
|
||||
if Protocol.where(protocol_type: Protocol::REPOSITORY_TYPES)
|
||||
.where.not(id: id)
|
||||
|
|
|
@ -43,6 +43,8 @@ class Report < ApplicationRecord
|
|||
|
||||
DEFAULT_SETTINGS = {
|
||||
all_tasks: true,
|
||||
exclude_task_metadata: false,
|
||||
exclude_timestamps: false,
|
||||
task: {
|
||||
protocol: {
|
||||
description: true,
|
||||
|
@ -62,8 +64,11 @@ class Report < ApplicationRecord
|
|||
result_comments: true,
|
||||
result_order: 'new',
|
||||
activities: true,
|
||||
repositories: []
|
||||
}
|
||||
repositories: [],
|
||||
excluded_repository_columns: {}
|
||||
},
|
||||
template: 'scinote_template',
|
||||
docx_template: 'scinote_template'
|
||||
}.freeze
|
||||
|
||||
def self.search(
|
||||
|
@ -124,4 +129,13 @@ class Report < ApplicationRecord
|
|||
ReportActions::ReportContent.new(report, content, {}, current_user).save_with_content
|
||||
report
|
||||
end
|
||||
|
||||
def self.default_repository_columns
|
||||
{
|
||||
'-1': I18n.t('repositories.table.id'),
|
||||
'-2': I18n.t('repositories.table.row_name'),
|
||||
'-3': I18n.t('repositories.table.added_on'),
|
||||
'-4': I18n.t('repositories.table.added_by')
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,9 +83,9 @@ class Repository < RepositoryBase
|
|||
'repository_rows.created_at',
|
||||
'users.full_name',
|
||||
'repository_rows.updated_at',
|
||||
'last_modified_bies_repository_rows.full_name',
|
||||
'last_modified_by.full_name',
|
||||
'repository_rows.archived_on',
|
||||
'archived_bies_repository_rows.full_name'
|
||||
'archived_by.full_name'
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -60,9 +60,9 @@ class RepositoryAssetValue < ApplicationRecord
|
|||
|
||||
def update_data!(new_data, user)
|
||||
if new_data.is_a?(String) # assume it's a signed_id_token
|
||||
asset.file.attach(new_data)
|
||||
asset.attach_file_version(new_data)
|
||||
elsif new_data[:file_data]
|
||||
asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data])), filename: new_data[:file_name])
|
||||
asset.attach_file_version(io: StringIO.new(Base64.decode64(new_data[:file_data])), filename: new_data[:file_name])
|
||||
end
|
||||
|
||||
asset.file_pdf_preview.purge if asset.file_pdf_preview.attached?
|
||||
|
@ -99,9 +99,9 @@ class RepositoryAssetValue < ApplicationRecord
|
|||
value.asset = Asset.create!(created_by: value.created_by, last_modified_by: value.created_by, team: team)
|
||||
|
||||
if payload.is_a?(String) # assume it's a signed_id_token
|
||||
value.asset.file.attach(payload)
|
||||
value.asset.attach_file_version(payload)
|
||||
elsif payload[:file_data]
|
||||
value.asset.file.attach(io: StringIO.new(Base64.decode64(payload[:file_data])), filename: payload[:file_name])
|
||||
value.asset.attach_file_version(io: StringIO.new(Base64.decode64(payload[:file_data])), filename: payload[:file_name])
|
||||
end
|
||||
|
||||
value.asset.post_process_file
|
||||
|
|
|
@ -77,9 +77,9 @@ class RepositoryCell < ApplicationRecord
|
|||
'"repository_date_time_values"."id" = "repository_cells"."value_id" AND ' \
|
||||
'"repository_cells"."value_type" = \'RepositoryDateTimeValueBase\' ' \
|
||||
'AND repository_reminder_columns.metadata ->> \'reminder_value\' <> \'\' AND ' \
|
||||
'(repository_date_time_values.data - NOW()) <= ' \
|
||||
'(repository_reminder_columns.metadata ->> \'reminder_value\')::int * ' \
|
||||
'(repository_reminder_columns.metadata ->> \'reminder_unit\')::int * interval \'1 sec\''
|
||||
'repository_date_time_values.data <= ' \
|
||||
'(NOW() AT TIME ZONE \'UTC\') + (repository_reminder_columns.metadata ->> \'reminder_value\')::int * ' \
|
||||
'(repository_reminder_columns.metadata ->> \'reminder_unit\')::int * \'1 SECOND\'::interval'
|
||||
).joins(
|
||||
'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON ' \
|
||||
'"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND ' \
|
||||
|
|
|
@ -8,4 +8,12 @@ class SoftLockedRepository < Repository
|
|||
def shareable_write?
|
||||
false
|
||||
end
|
||||
|
||||
def unlocked?
|
||||
@unlocked == true
|
||||
end
|
||||
|
||||
def unlock!
|
||||
@unlocked = true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -117,7 +117,7 @@ class Step < ApplicationRecord
|
|||
step_texts.order(created_at: :asc).first
|
||||
end
|
||||
|
||||
def duplicate(protocol, user, step_position: nil, step_name: nil)
|
||||
def duplicate(protocol, user, step_position: nil, step_name: nil, include_file_versions: false)
|
||||
ActiveRecord::Base.transaction do
|
||||
assets_to_clone = []
|
||||
|
||||
|
@ -142,7 +142,7 @@ class Step < ApplicationRecord
|
|||
# "Shallow" Copy assets
|
||||
assets.each do |asset|
|
||||
new_asset = asset.dup
|
||||
new_asset.save!
|
||||
new_asset.update!(created_by: user, last_modified_by: user)
|
||||
new_step.assets << new_asset
|
||||
assets_to_clone << [asset.id, new_asset.id]
|
||||
end
|
||||
|
@ -153,7 +153,7 @@ class Step < ApplicationRecord
|
|||
end
|
||||
|
||||
# Call clone helper
|
||||
Protocol.delay(queue: :assets).deep_clone_assets(assets_to_clone)
|
||||
Protocol.delay(queue: :assets).deep_clone_assets(assets_to_clone, include_file_versions)
|
||||
|
||||
new_step
|
||||
end
|
||||
|
|
|
@ -221,6 +221,7 @@ class Team < ApplicationRecord
|
|||
|
||||
def create_default_label_templates
|
||||
ZebraLabelTemplate.default.update(team: self, default: true)
|
||||
ZebraLabelTemplate.default_203dpi.update(team: self, default: false)
|
||||
FluicsLabelTemplate.default.update(team: self, default: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,4 +11,15 @@ class ZebraLabelTemplate < LabelTemplate
|
|||
density: 12
|
||||
)
|
||||
end
|
||||
|
||||
def self.default_203dpi
|
||||
ZebraLabelTemplate.new(
|
||||
name: I18n.t('label_templates.default_zebra_name_203dpi'),
|
||||
width_mm: 25.4,
|
||||
height_mm: 12.7,
|
||||
content: Extends::DEFAULT_LABEL_TEMPLATE_203DPI[:zpl],
|
||||
unit: 0,
|
||||
density: 12
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,11 +27,16 @@ Canaid::Permissions.register_for(Asset) do
|
|||
if object.repository_column.repository.is_a?(RepositorySnapshot)
|
||||
false
|
||||
else
|
||||
object.repository_row.active? &&
|
||||
can_manage_repository_assets?(user, object.repository_column.repository)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
can :restore_asset do |user, asset|
|
||||
VersionedAttachments.enabled? && can_manage_asset?(user, asset)
|
||||
end
|
||||
|
||||
can :open_asset_locally do |_user, asset|
|
||||
ENV['ASSET_SYNC_URL'].present?
|
||||
end
|
||||
|
|
|
@ -25,10 +25,12 @@ Canaid::Permissions.register_for(Repository) do
|
|||
create_repository_rows
|
||||
manage_repository_rows
|
||||
delete_repository_rows
|
||||
create_repository_columns)
|
||||
create_repository_columns
|
||||
manage_repository_columns)
|
||||
.each do |perm|
|
||||
can perm do |_, repository|
|
||||
repository.active? && repository.repository_snapshots.provisioning.none?
|
||||
repository.active? && repository.repository_snapshots.provisioning.none? &&
|
||||
(!repository.is_a?(SoftLockedRepository) || repository.unlocked?)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -105,7 +107,7 @@ Canaid::Permissions.register_for(Repository) do
|
|||
end
|
||||
|
||||
can :manage_repository_columns do |user, repository|
|
||||
repository.repository_snapshots.provisioning.none? && can_create_repository_columns?(user, repository)
|
||||
repository.permission_granted?(user, RepositoryPermissions::COLUMNS_UPDATE)
|
||||
end
|
||||
|
||||
# repository: create/update/delete filters
|
||||
|
@ -122,6 +124,10 @@ Canaid::Permissions.register_for(RepositoryColumn) do
|
|||
# repository: update/delete field
|
||||
# Tested in scope of RepositoryPermissions spec
|
||||
can :manage_repository_column do |user, repository_column|
|
||||
repository_column.repository.repository_snapshots.provisioning.none? && can_create_repository_columns?(user, repository_column.repository)
|
||||
repository_column.repository.repository_snapshots.provisioning.none? && repository_column.repository.permission_granted?(user, RepositoryPermissions::COLUMNS_UPDATE)
|
||||
end
|
||||
|
||||
can :delete_repository_column do |user, repository_column|
|
||||
repository_column.repository.repository_snapshots.provisioning.none? && repository_column.repository.permission_granted?(user, RepositoryPermissions::COLUMNS_DELETE)
|
||||
end
|
||||
end
|
||||
|
|
41
app/serializers/active_storage/blob_serializer.rb
Normal file
41
app/serializers/active_storage/blob_serializer.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveStorage
|
||||
class BlobSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :filename, :extension, :basename, :url, :created_at, :byte_size, :version, :restored_from_version, :created_by, :download_url
|
||||
|
||||
def basename
|
||||
object.filename.base
|
||||
end
|
||||
|
||||
def extension
|
||||
object.filename.extension_without_delimiter
|
||||
end
|
||||
|
||||
def download_url
|
||||
rails_blob_url(object, disposition: 'attachment')
|
||||
end
|
||||
|
||||
def version
|
||||
object.metadata['version'] || 1
|
||||
end
|
||||
|
||||
def restored_from_version
|
||||
object.metadata['restored_from_version']
|
||||
end
|
||||
|
||||
def created_at
|
||||
return object.created_at unless @instance_options[:user]
|
||||
|
||||
object.created_at.strftime("#{@instance_options[:user].date_format}, %H:%M")
|
||||
end
|
||||
|
||||
def created_by
|
||||
User.select(:id, :full_name, :email).find_by(
|
||||
id: object.metadata['created_by_id'] || object.attachments.first&.record&.created_by_id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -138,7 +138,8 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
load_asset: load_asset_path(object),
|
||||
asset_file: asset_file_url_path(object),
|
||||
marvin_js: marvin_js_asset_path(object),
|
||||
marvin_js_icon: image_path('icon_small/marvinjs.svg')
|
||||
marvin_js_icon: image_path('icon_small/marvinjs.svg'),
|
||||
versions: (asset_versions_path(object) if attached)
|
||||
}
|
||||
user = scope[:user] || @instance_options[:user]
|
||||
if can_manage_asset?(user, object)
|
||||
|
@ -155,6 +156,7 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
)
|
||||
end
|
||||
|
||||
urls[:restore_version] = asset_restore_version_path(object) if can_restore_asset?(user, object)
|
||||
urls[:open_vector_editor_edit] = edit_gene_sequence_asset_path(object.id) if can_manage_asset?(user, object)
|
||||
|
||||
if can_manage_asset?(user, object) && can_open_asset_locally?(user, object)
|
||||
|
|
|
@ -4,7 +4,7 @@ class NotificationSerializer < ActiveModel::Serializer
|
|||
include Rails.application.routes.url_helpers
|
||||
include BreadcrumbsHelper
|
||||
|
||||
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today
|
||||
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today, :toggle_read_url
|
||||
|
||||
def title
|
||||
object.to_notification.title
|
||||
|
@ -31,4 +31,7 @@ class NotificationSerializer < ActiveModel::Serializer
|
|||
object.read_at.present?
|
||||
end
|
||||
|
||||
def toggle_read_url
|
||||
toggle_read_user_notification_path(object)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ module LabelPrinters
|
|||
end
|
||||
|
||||
def sync_templates!
|
||||
NewRelic::Agent.ignore_transaction
|
||||
LabelPrinter.fluics.each do |printer|
|
||||
api_client = ApiClient.new(printer.fluics_api_key)
|
||||
templates = api_client.list_templates
|
||||
|
|
|
@ -22,7 +22,7 @@ class MarvinJsService
|
|||
asset = Asset.new(created_by: current_user,
|
||||
last_modified_by: current_user,
|
||||
team_id: current_team.id)
|
||||
attach_file(asset.file, file, params)
|
||||
attach_file_version(asset, file, params)
|
||||
asset.save!
|
||||
asset.post_process_file
|
||||
connect_asset(asset, params, current_user)
|
||||
|
@ -40,21 +40,14 @@ class MarvinJsService
|
|||
|
||||
file = generate_image(params)
|
||||
asset.update(last_modified_by: current_user) if asset.is_a?(Asset)
|
||||
attach_file(attachment, file, params)
|
||||
asset.post_process_file if asset.instance_of?(Asset)
|
||||
asset
|
||||
end
|
||||
|
||||
def update_file_name(new_name, asset_id, current_user, current_team)
|
||||
asset = current_team.assets.find(asset_id)
|
||||
prepared_name = prepare_name(new_name)
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
asset.last_modified_by = current_user
|
||||
asset.rename_file(prepared_name)
|
||||
asset.save!
|
||||
if params[:object_type] == 'TinyMceAsset'
|
||||
attach_file(attachment, file, params)
|
||||
else
|
||||
attach_file_version(asset, file, params)
|
||||
end
|
||||
|
||||
asset.post_process_file if asset.instance_of?(Asset)
|
||||
asset
|
||||
end
|
||||
|
||||
|
@ -90,6 +83,19 @@ class MarvinJsService
|
|||
)
|
||||
end
|
||||
|
||||
def attach_file_version(asset, file, params)
|
||||
asset.attach_file_version(
|
||||
io: file,
|
||||
filename: "#{prepare_name(params[:name])}.jpg",
|
||||
content_type: 'image/jpeg',
|
||||
metadata: {
|
||||
name: prepare_name(params[:name]),
|
||||
description: params[:description],
|
||||
asset_type: 'marvinjs'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def prepare_name(sketch_name)
|
||||
if !sketch_name.blank?
|
||||
sketch_name
|
||||
|
|
52
app/services/pdf_preview_service.rb
Normal file
52
app/services/pdf_preview_service.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PdfPreviewService
|
||||
def initialize(blob, attached)
|
||||
@blob = blob
|
||||
@attached = attached
|
||||
end
|
||||
|
||||
def generate!
|
||||
preview_file = nil
|
||||
|
||||
@blob.open(tmpdir: tempdir) do |input|
|
||||
work_dir = File.dirname(input.path)
|
||||
preview_filename = "#{File.basename(input.path, '.*')}.pdf"
|
||||
preview_file = File.join(work_dir, preview_filename)
|
||||
Rails.logger.info "Starting preparing document preview for file #{@blob.filename.sanitized}..."
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
success = system(
|
||||
'timeout',
|
||||
Constants::PREVIEW_TIMEOUT_SECONDS.to_s,
|
||||
libreoffice_path,
|
||||
'--headless',
|
||||
'--invisible',
|
||||
'--convert-to',
|
||||
'pdf', '--outdir',
|
||||
work_dir, input.path
|
||||
)
|
||||
unless success && File.file?(preview_file)
|
||||
raise StandardError, "There was an error generating PDF preview, blob id: #{@blob.id}"
|
||||
end
|
||||
|
||||
ActiveRecord::Base.no_touching do
|
||||
@attached.attach(io: File.open(preview_file), filename: "#{@blob.filename.base}.pdf")
|
||||
end
|
||||
Rails.logger.info("Finished preparing PDF preview for file #{@blob.filename.sanitized}.")
|
||||
end
|
||||
end
|
||||
ensure
|
||||
File.delete(preview_file) if File.file?(preview_file)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tempdir
|
||||
Rails.root.join('tmp')
|
||||
end
|
||||
|
||||
def libreoffice_path
|
||||
ENV['LIBREOFFICE_PATH'] || 'soffice'
|
||||
end
|
||||
end
|
|
@ -44,7 +44,7 @@ module ReportActions
|
|||
|
||||
def create_new_asset
|
||||
asset = Asset.create(created_by: @user, last_modified_by: @user, team: @team)
|
||||
asset.file.attach(@report.pdf_file.blob)
|
||||
asset.attach_file_version(@report.pdf_file.blob)
|
||||
asset
|
||||
end
|
||||
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
# rubocop:disable Style/ClassAndModuleChildren
|
||||
|
||||
Dir[Rails.root.join('app/views/reports/docx_templates/**/docx.rb')].each do |file|
|
||||
require file
|
||||
end
|
||||
|
||||
class Reports::Docx
|
||||
include ActionView::Helpers::TextHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
@ -26,15 +30,23 @@ class Reports::Docx
|
|||
@link_style = {}
|
||||
@color = {}
|
||||
@scinote_url = options[:scinote_url][0..-2]
|
||||
@template = @settings[:docx_template].presence || 'scinote_template'
|
||||
|
||||
extend "#{@template.camelize}Docx".constantize
|
||||
end
|
||||
|
||||
def draw
|
||||
initial_document_load
|
||||
|
||||
@report.root_elements.each do |subject|
|
||||
public_send("draw_#{subject.type_of}", subject)
|
||||
end
|
||||
prepare_docx
|
||||
|
||||
@docx
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_template_value(name)
|
||||
@report.report_template_values.find_by(name: name)&.value
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/ClassAndModuleChildren
|
||||
|
|
|
@ -4,6 +4,7 @@ module Reports::Docx::DrawExperiment
|
|||
def draw_experiment(subject)
|
||||
color = @color
|
||||
link_style = @link_style
|
||||
settings = @settings
|
||||
scinote_url = @scinote_url
|
||||
experiment = subject.experiment
|
||||
return unless can_read_experiment?(@user, experiment)
|
||||
|
@ -14,12 +15,15 @@ module Reports::Docx::DrawExperiment
|
|||
link_style
|
||||
end
|
||||
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.experiment.user_time',
|
||||
code: experiment.code, timestamp: I18n.l(experiment.created_at, format: :full)), color: color[:gray]
|
||||
if experiment.archived?
|
||||
text ' | '
|
||||
text I18n.t('search.index.archived'), color: color[:gray]
|
||||
if !settings['exclude_timestamps'] || experiment.archived?
|
||||
@docx.p do
|
||||
unless settings['exclude_timestamps']
|
||||
text I18n.t('projects.reports.elements.experiment.user_time',
|
||||
code: experiment.code,
|
||||
timestamp: I18n.l(experiment.created_at, format: :full)), color: color[:gray]
|
||||
text ' | ' if experiment.archived?
|
||||
end
|
||||
text I18n.t('search.index.archived'), color: color[:gray] if experiment.archived?
|
||||
end
|
||||
end
|
||||
html = custom_auto_link(experiment.description, team: @report_team)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Reports::Docx::DrawMyModule
|
||||
def draw_my_module(subject)
|
||||
def draw_my_module(subject, without_results: false, without_repositories: false)
|
||||
color = @color
|
||||
link_style = @link_style
|
||||
settings = @settings
|
||||
scinote_url = @scinote_url
|
||||
my_module = subject.my_module
|
||||
tags = my_module.tags.order(:id)
|
||||
|
@ -15,45 +16,50 @@ module Reports::Docx::DrawMyModule
|
|||
link_style
|
||||
end
|
||||
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.user_time', code: my_module.code,
|
||||
timestamp: I18n.l(my_module.created_at, format: :full)), color: color[:gray]
|
||||
if my_module.archived?
|
||||
text ' | '
|
||||
text I18n.t('search.index.archived'), color: color[:gray]
|
||||
end
|
||||
end
|
||||
|
||||
if my_module.started_on.present?
|
||||
if my_module.archived? || !settings['exclude_timestamps']
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.started_on',
|
||||
started_on: I18n.l(my_module.started_on, format: :full))
|
||||
unless settings['exclude_timestamps']
|
||||
text I18n.t('projects.reports.elements.module.user_time', code: my_module.code,
|
||||
timestamp: I18n.l(my_module.created_at, format: :full)), color: color[:gray]
|
||||
text ' | ' if my_module.archived?
|
||||
end
|
||||
|
||||
text I18n.t('search.index.archived'), color: color[:gray] if my_module.archived?
|
||||
end
|
||||
end
|
||||
|
||||
if my_module.due_date.present?
|
||||
unless settings['exclude_task_metadata']
|
||||
if my_module.started_on.present?
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.started_on',
|
||||
started_on: I18n.l(my_module.started_on, format: :full))
|
||||
end
|
||||
end
|
||||
|
||||
if my_module.due_date.present?
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.due_date',
|
||||
due_date: I18n.l(my_module.due_date, format: :full))
|
||||
end
|
||||
end
|
||||
|
||||
status = my_module.my_module_status
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.due_date',
|
||||
due_date: I18n.l(my_module.due_date, format: :full))
|
||||
text I18n.t('projects.reports.elements.module.status')
|
||||
text ' '
|
||||
text "[#{status.name}]", color: (status.light_color? ? '000000' : status.color.delete('#'))
|
||||
if my_module.completed?
|
||||
text " #{I18n.t('my_modules.states.completed')} #{I18n.l(my_module.completed_on, format: :full)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
status = my_module.my_module_status
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.status')
|
||||
text ' '
|
||||
text "[#{status.name}]", color: (status.light_color? ? '000000' : status.color.delete('#'))
|
||||
if my_module.completed?
|
||||
text " #{I18n.t('my_modules.states.completed')} #{I18n.l(my_module.completed_on, format: :full)}"
|
||||
end
|
||||
end
|
||||
|
||||
if tags.present?
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.tags_header')
|
||||
tags.each do |tag|
|
||||
text ' '
|
||||
text "[#{tag.name}]", color: tag.color.delete('#')
|
||||
if tags.present?
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.module.tags_header')
|
||||
tags.each do |tag|
|
||||
text ' '
|
||||
text "[#{tag.name}]", color: tag.color.delete('#')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -69,31 +75,16 @@ module Reports::Docx::DrawMyModule
|
|||
filter_steps_for_report(my_module.protocol.steps, @settings).order(:position).each do |step|
|
||||
draw_step(step)
|
||||
end
|
||||
@docx.p
|
||||
|
||||
if my_module.results.any? && (%w(file_results table_results text_results).any? { |k| @settings.dig('task', k) })
|
||||
@docx.h4 I18n.t('Results')
|
||||
order_results_for_report(my_module.results, @settings.dig('task', 'result_order')).each do |result|
|
||||
@docx.p do
|
||||
text result.name.presence || I18n.t('projects.reports.unnamed'), italic: true
|
||||
text " #{I18n.t('search.index.archived')} ", bold: true if result.archived?
|
||||
text I18n.t('projects.reports.elements.result.user_time',
|
||||
timestamp: I18n.l(result.created_at, format: :full),
|
||||
user: result.user.full_name), color: color[:gray]
|
||||
end
|
||||
draw_result_asset(result, @settings) if @settings.dig('task', 'file_results')
|
||||
result.result_orderable_elements.each do |element|
|
||||
if @settings.dig('task', 'table_results') && element.orderable_type == 'ResultTable'
|
||||
draw_result_table(element)
|
||||
elsif @settings.dig('task', 'text_results') && element.orderable_type == 'ResultText'
|
||||
draw_result_text(element)
|
||||
end
|
||||
end
|
||||
draw_result_comments(result) if @settings.dig('task', 'result_comments')
|
||||
end
|
||||
unless without_results
|
||||
draw_results(my_module)
|
||||
@docx.p
|
||||
end
|
||||
|
||||
@docx.p
|
||||
subject.children.active.each do |child|
|
||||
next if without_repositories && child.type_of == 'my_module_repository'
|
||||
|
||||
public_send("draw_#{child.type_of}", child)
|
||||
end
|
||||
|
||||
|
|
|
@ -12,13 +12,14 @@ module Reports::Docx::DrawMyModuleProtocol
|
|||
end
|
||||
|
||||
if @settings.dig('task', 'protocol', 'description') && protocol.description.present?
|
||||
@docx.p I18n.t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code,
|
||||
timestamp: I18n.l(protocol.created_at, format: :full)), color: @color[:gray]
|
||||
unless @settings['exclude_timestamps']
|
||||
@docx.p I18n.t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code,
|
||||
timestamp: I18n.l(protocol.created_at, format: :full)), color: @color[:gray]
|
||||
end
|
||||
html = custom_auto_link(protocol.description, team: @report_team)
|
||||
Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url,
|
||||
link_style: @link_style }).html_to_word_converter(html)
|
||||
@docx.p
|
||||
@docx.p
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,11 +5,12 @@ module Reports::Docx::DrawMyModuleRepository
|
|||
my_module = subject.my_module
|
||||
repository = subject.repository
|
||||
repository = assigned_repository_or_snapshot(my_module, repository)
|
||||
excluded_repository_columns = @settings.dig(:task, :excluded_repository_columns, repository.id.to_s) || {}
|
||||
|
||||
return unless repository && can_read_experiment?(@user, my_module.experiment) &&
|
||||
(repository.is_a?(RepositorySnapshot) || can_read_repository?(@user, repository))
|
||||
|
||||
repository_data = my_module.repository_docx_json(repository)
|
||||
repository_data = my_module.repository_docx_json(repository, excluded_repository_columns)
|
||||
|
||||
return false unless repository_data[:rows].any? && can_read_repository?(@user, repository)
|
||||
|
||||
|
@ -19,7 +20,12 @@ module Reports::Docx::DrawMyModuleRepository
|
|||
@docx.p I18n.t('projects.reports.elements.module_repository.name',
|
||||
repository: repository.name,
|
||||
my_module: my_module.name), bold: true, size: Constants::REPORT_DOCX_STEP_ELEMENTS_TITLE_SIZE
|
||||
@docx.table table, border_size: Constants::REPORT_DOCX_TABLE_BORDER_SIZE
|
||||
|
||||
if table.present?
|
||||
@docx.table table, border_size: Constants::REPORT_DOCX_TABLE_BORDER_SIZE
|
||||
else
|
||||
@docx.p I18n.t('projects.reports.elements.module_repository.no_columns'), italic: true
|
||||
end
|
||||
|
||||
@docx.p
|
||||
@docx.p
|
||||
|
|
|
@ -15,10 +15,12 @@ module Reports::Docx::DrawProjectHeader
|
|||
link_style
|
||||
end
|
||||
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.project_header.user_time', code: project.code,
|
||||
timestamp: I18n.l(project.created_at, format: :full)), color: color[:gray]
|
||||
br
|
||||
unless @settings['exclude_timestamps']
|
||||
@docx.p do
|
||||
text I18n.t('projects.reports.elements.project_header.user_time', code: project.code,
|
||||
timestamp: I18n.l(project.created_at, format: :full)), color: color[:gray]
|
||||
br
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,11 +25,9 @@ module Reports::Docx::DrawResultAsset
|
|||
end
|
||||
text " #{I18n.t('search.index.archived')} ", bold: true 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]
|
||||
|
||||
if settings.dig(:task, :file_results_previews) && ActiveStorageFileUtil.previewable_document?(asset&.file&.blob)
|
||||
text " #{I18n.t('projects.reports.elements.result_asset.full_preview_attached')}", color: color[:gray]
|
||||
unless settings['exclude_timestamps']
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@ module Reports::Docx::DrawResultComments
|
|||
@docx.p
|
||||
@docx.p I18n.t('projects.reports.elements.result_comments.name', result: result.name),
|
||||
bold: true, size: Constants::REPORT_DOCX_STEP_ELEMENTS_TITLE_SIZE
|
||||
comments.each do |comment|
|
||||
comments.find_each.with_index do |comment, index|
|
||||
comment_ts = comment.created_at
|
||||
|
||||
@docx.p unless index.zero?
|
||||
@docx.p I18n.t('projects.reports.elements.result_comments.comment_prefix',
|
||||
user: comment.user.full_name,
|
||||
date: I18n.l(comment_ts, format: :full_date),
|
||||
|
@ -17,7 +19,6 @@ module Reports::Docx::DrawResultComments
|
|||
html = custom_auto_link(comment.message, team: @report_team)
|
||||
Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url,
|
||||
link_style: @link_style }).html_to_word_converter(html)
|
||||
@docx.p
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ module Reports::Docx::DrawResultTable
|
|||
result = element.result
|
||||
table = element.orderable.table
|
||||
timestamp = table.created_at
|
||||
settings = @settings
|
||||
color = @color
|
||||
obj = self
|
||||
table_data = JSON.parse(table.contents_utf_8)['data']
|
||||
|
@ -39,9 +40,11 @@ module Reports::Docx::DrawResultTable
|
|||
end
|
||||
@docx.p do
|
||||
text I18n.t 'projects.reports.elements.result_table.table_name', name: table.name
|
||||
text ' '
|
||||
text I18n.t('projects.reports.elements.result_table.user_time',
|
||||
timestamp: I18n.l(timestamp, format: :full), user: result.user.full_name), color: color[:gray]
|
||||
unless settings['exclude_timestamps']
|
||||
text ' '
|
||||
text I18n.t('projects.reports.elements.result_table.user_time',
|
||||
timestamp: I18n.l(timestamp, format: :full), user: result.user.full_name), color: color[:gray]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,12 +4,18 @@ module Reports::Docx::DrawResultText
|
|||
def draw_result_text(element)
|
||||
result_text = element.orderable
|
||||
timestamp = element.created_at
|
||||
settings = @settings
|
||||
color = @color
|
||||
@docx.p do
|
||||
text result_text.name.presence || '', italic: true
|
||||
text ' '
|
||||
text I18n.t('projects.reports.elements.result_text.user_time',
|
||||
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
|
||||
if result_text.name.present? || !settings['exclude_timestamps']
|
||||
@docx.p do
|
||||
text result_text.name.to_s, italic: true
|
||||
text ' ' if result_text.name.present?
|
||||
|
||||
unless settings['exclude_timestamps']
|
||||
text I18n.t('projects.reports.elements.result_text.user_time',
|
||||
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
|
||||
end
|
||||
end
|
||||
end
|
||||
html = custom_auto_link(result_text.text, team: @report_team)
|
||||
Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url,
|
||||
|
|
42
app/services/reports/docx/draw_results.rb
Normal file
42
app/services/reports/docx/draw_results.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Reports::Docx::DrawResults
|
||||
def draw_results(my_module, with_my_module_name: false)
|
||||
color = @color
|
||||
settings = @settings
|
||||
scinote_url = @scinote_url
|
||||
link_style = @link_style
|
||||
return unless can_read_my_module?(@user, my_module)
|
||||
|
||||
if my_module.results.any? && (%w(file_results table_results text_results).any? { |k| @settings.dig('task', k) })
|
||||
if with_my_module_name
|
||||
@docx.h3 do
|
||||
link my_module.name,
|
||||
scinote_url + Rails.application.routes.url_helpers.protocols_my_module_path(my_module),
|
||||
link_style
|
||||
end
|
||||
end
|
||||
@docx.h4 I18n.t('Results')
|
||||
order_results_for_report(my_module.results, @settings.dig('task', 'result_order')).each do |result|
|
||||
@docx.p do
|
||||
text result.name.presence || I18n.t('projects.reports.unnamed'), italic: true
|
||||
text " #{I18n.t('search.index.archived')} ", bold: true if result.archived?
|
||||
unless settings['exclude_timestamps']
|
||||
text I18n.t('projects.reports.elements.result.user_time',
|
||||
timestamp: I18n.l(result.created_at, format: :full),
|
||||
user: result.user.full_name), color: color[:gray]
|
||||
end
|
||||
end
|
||||
draw_result_asset(result, @settings) if @settings.dig('task', 'file_results')
|
||||
result.result_orderable_elements.each do |element|
|
||||
if @settings.dig('task', 'table_results') && element.orderable_type == 'ResultTable'
|
||||
draw_result_table(element)
|
||||
elsif @settings.dig('task', 'text_results') && element.orderable_type == 'ResultText'
|
||||
draw_result_text(element)
|
||||
end
|
||||
end
|
||||
draw_result_comments(result) if @settings.dig('task', 'result_comments')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,22 +6,30 @@ module Reports::Docx::DrawStep
|
|||
step_type_str = step.completed ? 'completed' : 'uncompleted'
|
||||
user = (step.completed? && step.last_modified_by) || step.user
|
||||
timestamp = step.completed ? step.completed_on : step.created_at
|
||||
settings = @settings
|
||||
@docx.p
|
||||
@docx.h5(
|
||||
@docx.h4(
|
||||
"#{I18n.t('projects.reports.elements.step.step_pos', pos: step.position_plus_one)} #{step.name}"
|
||||
)
|
||||
@docx.p do
|
||||
if step.completed
|
||||
text I18n.t('protocols.steps.completed'), color: color[:green], bold: true
|
||||
else
|
||||
text I18n.t('protocols.steps.uncompleted'), color: color[:gray], bold: true
|
||||
|
||||
unless settings['exclude_task_metadata'] || settings['exclude_timestamps']
|
||||
@docx.p do
|
||||
unless settings['exclude_task_metadata']
|
||||
if step.completed
|
||||
text I18n.t('protocols.steps.completed'), color: color[:green], bold: true
|
||||
else
|
||||
text I18n.t('protocols.steps.uncompleted'), color: color[:gray], bold: true
|
||||
end
|
||||
end
|
||||
unless settings['exclude_timestamps']
|
||||
text ' | ' unless settings['exclude_task_metadata']
|
||||
text I18n.t(
|
||||
"projects.reports.elements.step.#{step_type_str}.user_time",
|
||||
user: user.full_name,
|
||||
timestamp: I18n.l(timestamp, format: :full)
|
||||
), color: color[:gray]
|
||||
end
|
||||
end
|
||||
text ' | '
|
||||
text I18n.t(
|
||||
"projects.reports.elements.step.#{step_type_str}.user_time",
|
||||
user: user.full_name,
|
||||
timestamp: I18n.l(timestamp, format: :full)
|
||||
), color: color[:gray]
|
||||
end
|
||||
|
||||
step.step_orderable_elements.order(:position).each do |element|
|
||||
|
@ -41,9 +49,6 @@ module Reports::Docx::DrawStep
|
|||
end
|
||||
|
||||
draw_step_comments(step) if @settings.dig('task', 'protocol', 'step_comments')
|
||||
|
||||
@docx.p
|
||||
@docx.p
|
||||
end
|
||||
|
||||
def handle_step_table(table)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue