Merge branch 'develop' into features/step-editing-improvements

This commit is contained in:
Martin Artnik 2022-06-02 11:26:39 +02:00
commit 9abbe22c26
75 changed files with 957 additions and 402 deletions

View file

@ -1,4 +1,4 @@
FROM ruby:2.7.5-bullseye
FROM ruby:2.7.6-bullseye
MAINTAINER BioSistemika <info@biosistemika.com>
ARG WKHTMLTOPDF_PACKAGE_URL=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb

View file

@ -1,4 +1,4 @@
FROM ruby:2.7.5-bullseye
FROM ruby:2.7.6-bullseye
MAINTAINER BioSistemika <info@biosistemika.com>
ARG WKHTMLTOPDF_PACKAGE_URL=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb

View file

@ -2,7 +2,7 @@
source 'http://rubygems.org'
ruby '2.7.5'
ruby '2.7.6'
gem 'bootsnap', require: false
gem 'bootstrap-sass', '~> 3.4.1'
@ -12,7 +12,7 @@ gem 'devise_invitable'
gem 'figaro'
gem 'pg', '~> 1.1'
gem 'pg_search' # PostgreSQL full text search
gem 'rails', '~> 6.1.4'
gem 'rails', '~> 6.1.5'
gem 'psych', '< 4.0'
gem 'view_component', require: 'view_component/engine'
gem 'recaptcha', require: 'recaptcha/rails'
@ -73,7 +73,7 @@ gem 'i18n-js', '~> 3.6' # Localization in javascript files
gem 'jbuilder' # JSON structures via a Builder-style DSL
gem 'logging', '~> 2.0.0'
gem 'nested_form_fields'
gem 'nokogiri', '~> 1.13.4' # HTML/XML parser
gem 'nokogiri', '~> 1.13.6' # HTML/XML parser
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
gem 'rgl' # Graph framework for project diagram calculations
gem 'roo', '~> 2.8.2' # Spreadsheet parser

View file

@ -42,40 +42,40 @@ GIT
GEM
remote: http://rubygems.org/
specs:
actioncable (6.1.4.7)
actionpack (= 6.1.4.7)
activesupport (= 6.1.4.7)
actioncable (6.1.5.1)
actionpack (= 6.1.5.1)
activesupport (= 6.1.5.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.4.7)
actionpack (= 6.1.4.7)
activejob (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
actionmailbox (6.1.5.1)
actionpack (= 6.1.5.1)
activejob (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
mail (>= 2.7.1)
actionmailer (6.1.4.7)
actionpack (= 6.1.4.7)
actionview (= 6.1.4.7)
activejob (= 6.1.4.7)
activesupport (= 6.1.4.7)
actionmailer (6.1.5.1)
actionpack (= 6.1.5.1)
actionview (= 6.1.5.1)
activejob (= 6.1.5.1)
activesupport (= 6.1.5.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.4.7)
actionview (= 6.1.4.7)
activesupport (= 6.1.4.7)
actionpack (6.1.5.1)
actionview (= 6.1.5.1)
activesupport (= 6.1.5.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.4.7)
actionpack (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
actiontext (6.1.5.1)
actionpack (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
nokogiri (>= 1.8.5)
actionview (6.1.4.7)
activesupport (= 6.1.4.7)
actionview (6.1.5.1)
activesupport (= 6.1.5.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -85,24 +85,24 @@ GEM
activemodel (>= 4.1, < 6.2)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (6.1.4.7)
activesupport (= 6.1.4.7)
activejob (6.1.5.1)
activesupport (= 6.1.5.1)
globalid (>= 0.3.6)
activemodel (6.1.4.7)
activesupport (= 6.1.4.7)
activerecord (6.1.4.7)
activemodel (= 6.1.4.7)
activesupport (= 6.1.4.7)
activemodel (6.1.5.1)
activesupport (= 6.1.5.1)
activerecord (6.1.5.1)
activemodel (= 6.1.5.1)
activesupport (= 6.1.5.1)
activerecord-import (1.0.7)
activerecord (>= 3.2)
activestorage (6.1.4.7)
actionpack (= 6.1.4.7)
activejob (= 6.1.4.7)
activerecord (= 6.1.4.7)
activesupport (= 6.1.4.7)
marcel (~> 1.0.0)
activestorage (6.1.5.1)
actionpack (= 6.1.5.1)
activejob (= 6.1.5.1)
activerecord (= 6.1.5.1)
activesupport (= 6.1.5.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.4.7)
activesupport (6.1.5.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -210,7 +210,7 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.9)
concurrent-ruby (1.1.10)
crack (0.4.5)
rexml
crass (1.0.6)
@ -354,7 +354,7 @@ GEM
logging (2.0.0)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.14.0)
loofah (2.16.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -381,7 +381,7 @@ GEM
rails (>= 3.2.0)
newrelic_rpm (6.15.0)
nio4r (2.5.8)
nokogiri (1.13.4)
nokogiri (1.13.6)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogumbo (2.0.4)
@ -441,7 +441,7 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.6.0)
rack (2.2.3)
rack (2.2.3.1)
rack-attack (6.4.0)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
@ -450,20 +450,20 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.4.7)
actioncable (= 6.1.4.7)
actionmailbox (= 6.1.4.7)
actionmailer (= 6.1.4.7)
actionpack (= 6.1.4.7)
actiontext (= 6.1.4.7)
actionview (= 6.1.4.7)
activejob (= 6.1.4.7)
activemodel (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
rails (6.1.5.1)
actioncable (= 6.1.5.1)
actionmailbox (= 6.1.5.1)
actionmailer (= 6.1.5.1)
actionpack (= 6.1.5.1)
actiontext (= 6.1.5.1)
actionview (= 6.1.5.1)
activejob (= 6.1.5.1)
activemodel (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
bundler (>= 1.15.0)
railties (= 6.1.4.7)
railties (= 6.1.5.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -481,11 +481,11 @@ GEM
rails (> 3.1)
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (6.1.4.7)
actionpack (= 6.1.4.7)
activesupport (= 6.1.4.7)
railties (6.1.5.1)
actionpack (= 6.1.5.1)
activesupport (= 6.1.5.1)
method_source
rake (>= 0.13)
rake (>= 12.2)
thor (~> 1.0)
rainbow (3.0.0)
rake (13.0.6)
@ -688,7 +688,7 @@ DEPENDENCIES
momentjs-rails (~> 2.17.1)
nested_form_fields
newrelic_rpm
nokogiri (~> 1.13.4)
nokogiri (~> 1.13.6)
omniauth
omniauth-azure-activedirectory
omniauth-linkedin-oauth2
@ -704,7 +704,7 @@ DEPENDENCIES
puma
rack-attack
rack-cors
rails (~> 6.1.4)
rails (~> 6.1.5)
rails-controller-testing
rails_12factor
rails_autolink (~> 1.1, >= 1.1.6)
@ -744,7 +744,7 @@ DEPENDENCIES
yomu!
RUBY VERSION
ruby 2.7.5p203
ruby 2.7.6p219
BUNDLED WITH
2.1.4

View file

@ -1 +1 @@
1.24.2
1.25.0

View file

@ -283,7 +283,7 @@
function initTagsSelector() {
var myModuleTagsSelector = '#module-tags-selector';
dropdownSelector.init(myModuleTagsSelector, {
dropdownSelector.init($(myModuleTagsSelector), {
closeOnSelect: true,
tagClass: 'my-module-white-tags',
tagStyle: (data) => {

View file

@ -54,14 +54,16 @@ var MyModuleRepositories = (function() {
return columns;
}
function reloadRepositoriesList(repositoryId) {
function reloadRepositoriesList(repositoryId, expand = false) {
var repositoriesContainer = $('#assigned-items-container');
$.get(repositoriesContainer.data('repositories-list-url'), function(result) {
repositoriesContainer.html(result.html);
$('.assigned-items-title').attr('data-assigned-items-count', result.assigned_rows_count);
// expand recently updated repository
if (expand) {
$('#assigned-items-container').collapse('show');
$('#assigned-repository-items-container-' + repositoryId).collapse('show');
}
});
}
@ -566,6 +568,7 @@ var MyModuleRepositories = (function() {
versionsList.find('.list-group-item').attr('data-selected', false);
versionsList.find('.list-group-item.active').attr('data-selected', true);
$('#setDefaultVersionButton').parent().addClass('hidden');
reloadRepositoriesList(FULL_VIEW_MODAL.find('.table').data('id'));
animateSpinner(null, false);
}
});
@ -595,6 +598,7 @@ var MyModuleRepositories = (function() {
renderFullViewRepositoryName(repositoryNameObject.text());
FULL_VIEW_MODAL.modal('show');
initCloseFullViewModal();
$.getJSON($(this).data('table-url'), (data) => {
FULL_VIEW_MODAL.find('.table-container').html(data.html);
renderFullViewTable(FULL_VIEW_MODAL.find('.table'), { assigned: true, skipCheckbox: true });
@ -682,6 +686,7 @@ var MyModuleRepositories = (function() {
var assignUrlModal = $(this).data('assign-url-modal');
var updateUrlModal = $(this).data('update-url-modal');
FULL_VIEW_MODAL.modal('show');
initCloseFullViewModal();
$.get($(this).data('table-url'), (data) => {
FULL_VIEW_MODAL.find('.table-container').html(data.html);
FULL_VIEW_MODAL.data('assign-url-modal', assignUrlModal);
@ -776,7 +781,7 @@ var MyModuleRepositories = (function() {
$(FULL_VIEW_TABLE.table().container()).find('.dataTable')
.attr('data-assigned-items-count', data.rows_count);
FULL_VIEW_TABLE.ajax.reload(null, false);
reloadRepositoriesList(data.repository_id);
reloadRepositoriesList(data.repository_id, true);
updateFullViewRowsCount(data.rows_count);
renderFullViewAssignButtons();
},
@ -809,6 +814,14 @@ var MyModuleRepositories = (function() {
});
}
function initCloseFullViewModal() {
$(FULL_VIEW_MODAL).on('keyup', function(e) {
if (e.key === 'Escape') {
FULL_VIEW_MODAL.modal('hide');
}
});
}
return {
init: () => {
initSimpleTable();

View file

@ -1,4 +1,4 @@
/* global SmartAnnotation I18n MyModuleRepositories GLOBAL_CONSTANTS formatDecimalValue */
/* global SmartAnnotation I18n MyModuleRepositories GLOBAL_CONSTANTS formatDecimalValue Decimal */
var MyModuleStockConsumption = (function() {
const CONSUMPTION_MODAL = '#consumeRepositoryStockValueModal';
const WARNING_MODAL = '#consumeRepositoryStockValueModalWarning';
@ -28,16 +28,29 @@ var MyModuleStockConsumption = (function() {
SmartAnnotation.init($(CONSUMPTION_MODAL + ' #comment')[0]);
$('#stock_consumption').on('input', function() {
let initialValue = parseFloat($(this).data('initial-value'));
let initialStock = parseFloat($(this).data('initial-stock'));
let initialValue = new Decimal($(this).data('initial-value') || 0);
let initialStock = new Decimal($(this).data('initial-stock'));
let decimals = $(this).data('decimals');
this.value = formatDecimalValue(String(this.value), decimals);
let finalValue = initialValue - ($(this).val() || 0) + initialStock;
let finalValue = initialValue.minus(new Decimal($(this).val() || 0)).plus(initialStock);
$('.stock-final-container .value')
.text(formatDecimalValue(String(finalValue), $('#stock_consumption').data('decimals')));
$('.stock-final-container').toggleClass('error', finalValue <= 0);
$(this).closest('.sci-input-container').toggleClass('error', this.value === '');
$('.update-consumption-button').attr('disabled', $(this).val() === '');
$('.stock-final-container').toggleClass('negative', finalValue <= 0);
$(this).closest('.sci-input-container').toggleClass('error', !($(this).val().length && this.value >= 0));
if ($(this).val().length === 0) {
$(this).closest('.sci-input-container')
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.amount_error')
);
} else if (this.value <= 0) {
$(this).closest('.sci-input-container')
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.negative_error')
);
}
$('.update-consumption-button').attr('disabled', !($(this).val().length && this.value >= 0));
});
$(CONSUMPTION_MODAL + ' form').on('ajax:success', function() {
@ -85,7 +98,7 @@ var MyModuleStockConsumption = (function() {
} else if (e.key === 'Enter') {
$('.update-consumption-button').trigger('click', [true]);
}
})
});
$(WARNING_MODAL).on('click', '.cancel-consumption', function(e) {
$(WARNING_MODAL).modal('hide');
$(CONSUMPTION_MODAL).modal('show');

View file

@ -209,27 +209,19 @@ function initializeEdit() {
if (canEditModules) {
initEditModules();
$(".edit-module").on("click touchstart", editModuleHandler);
$("#canvas-container").on("click touchstart", ".edit-module", editModuleHandler);
}
if (canCloneModules) {
bindCloneModuleAction(
$(".module-options a.clone-module"),
".diagram .module",
GRID_DIST_EDIT_X,
GRID_DIST_EDIT_Y);
bindCloneModuleGroupAction(
$(".module-options a.clone-module-group"),
".diagram .module",
GRID_DIST_EDIT_X,
GRID_DIST_EDIT_Y);
bindCloneModuleAction(".diagram .module", GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
bindCloneModuleGroupAction(".diagram .module", GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
}
if (canMoveModules) {
initMoveModules();
$(".move-module").on("click touchstart", moveModuleHandler);
$("#canvas-container").on("click touchstart", ".move-module", moveModuleHandler);
initMoveModuleGroups();
$(".move-module-group").on("click touchstart", moveModuleGroupHandler);
$("#canvas-container").on("click touchstart", ".move-module-group", moveModuleGroupHandler);
}
if (canDeleteModules) {
bindDeleteModuleAction();
@ -250,6 +242,17 @@ function initializeEdit() {
{ color: 'white', shadow: true }
);
});
$('.module-options-dropdown')
.on('show.bs.dropdown', function() {
let dropdownContainer = $(this);
$.getJSON(dropdownContainer.data('dropdown-menu-path'), function(result) {
dropdownContainer.find('.dropdown-menu').html(result.html);
});
})
.on('show.bs.dropdown', function() {
$(this).find('.dropdown-menu').html('');
});
}
function destroyEdit() {
@ -1174,9 +1177,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
.addClass("edit-module")
.html($("#edit-link-placeholder").text().trim())
.appendTo(editModuleItem);
// Add click handler for the edit module
$(editModuleLink).on("click touchstart", editModuleHandler);
}
// Add clone links if neccesary
@ -1192,9 +1192,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
.html($("#clone-link-placeholder").text().trim())
.appendTo(cloneModuleItem);
// Add clone click handler for the new module
bindCloneModuleAction($(cloneModuleLink), ".diagram .module", gridDistX, gridDistY);
var cloneModuleGroupItem = document.createElement("li");
$(cloneModuleGroupItem).appendTo(dropdownMenu);
$(cloneModuleGroupItem).hide();
@ -1207,9 +1204,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
.html($("#clone-group-link-placeholder").text().trim())
.appendTo(cloneModuleGroupItem);
// Add clone click handler for the new module
bindCloneModuleGroupAction($(cloneModuleGroupLink), ".diagram .module", gridDistX, gridDistY);
bindEditModeDropdownHandlers(module);
}
@ -1226,9 +1220,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
.html($("#move-link-placeholder").text().trim())
.appendTo(moveModuleItem);
// Add clone click handler for the new module
$(moveModuleLink).on("click touchstart", moveModuleHandler);
// Add buttons for module groups
var moveModuleGroupItem = document.createElement("li");
$(moveModuleGroupItem).appendTo(dropdownMenu);
@ -1241,8 +1232,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
.addClass("move-module-group")
.html($("#move-group-link-placeholder").text().trim())
.appendTo(moveModuleGroupItem);
$(moveModuleGroupLink).on("click touchstart", moveModuleGroupHandler);
}
// Add delete links if neccesary
@ -1258,9 +1247,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
.html($("#delete-link-placeholder").text().trim())
.appendTo(deleteModuleItem);
// Add delete click handler for the new module
$(deleteModuleLink).on("click touchstart", deleteModuleHandler);
var deleteModuleGroupItem = document.createElement("li");
$(deleteModuleGroupItem).appendTo(dropdownMenu);
$(deleteModuleGroupItem).hide();
@ -1272,9 +1258,6 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
.addClass("delete-module-group")
.html($("#delete-group-link-placeholder").text().trim())
.appendTo(deleteModuleGroupItem);
// Add delete click handler for the new module
$(deleteModuleGroupLink).on("click touchstart", deleteModuleGroupHandler);
}
// Set it up for jsPlumb, depending on permissions
@ -1482,8 +1465,9 @@ function initEditModules() {
var moduleId = modal.attr("data-module-id");
var moduleEl = $("#" + moduleId);
// Update the module's name in GUI
moduleEl.attr("data-module-name", newName);
moduleEl.find(".panel-heading .panel-title").html(newName);
moduleEl.attr('data-module-name', newName);
moduleEl.find('.panel-heading .panel-title').attr('title', newName);
moduleEl.find('.panel-heading .panel-title').html(newName);
// Add this information to form
var formAddInput = $('#update-canvas form input#add');
@ -1769,7 +1753,7 @@ moveModuleGroupHandler = function(ev) {
*/
function bindDeleteModuleAction() {
// First, bind the delete module handler onto all "delete module" links
$(".module-options a.delete-module").on("click touchstart", deleteModuleHandler);
$("#canvas-container").on("click touchstart", ".delete-module", deleteModuleHandler);
// Then, bind on modal events
var modal = $("#modal-delete-module");
@ -1961,7 +1945,7 @@ deleteModuleHandler =function() {
function bindDeleteModuleGroupAction() {
// First, bind the delete module group handler onto all
// "delete module group" links
$(".module-options a.delete-module-group").on("click touchstart", deleteModuleGroupHandler);
$("#canvas-container").on("click touchstart", ".delete-module-group", deleteModuleGroupHandler);
// Then, bind on modal events
var modal = $("#modal-delete-module-group");
@ -2031,8 +2015,8 @@ deleteModuleGroupHandler = function() {
* @param gridDistX - The grid distance in X direction.
* @param gridDistY - The grid distance in Y direction.
*/
function bindCloneModuleAction(element, modulesSel, gridDistX, gridDistY) {
element.on("click touchstart", function(event) {
function bindCloneModuleAction(modulesSel, gridDistX, gridDistY) {
$("#canvas-container").on("click touchstart", ".clone-module", function(event) {
cloneModuleHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY);
event.preventDefault();
event.stopPropagation();
@ -2133,8 +2117,8 @@ function cloneModule(originalModule, gridDistX, gridDistY, left, top) {
* @param gridDistX - The grid distance in X direction.
* @param gridDistY - The grid distance in Y direction.
*/
function bindCloneModuleGroupAction(element, modulesSel, gridDistX, gridDistY) {
element.on("click touchstart", function(event) {
function bindCloneModuleGroupAction(modulesSel, gridDistX, gridDistY) {
$("#canvas-container").on("click touchstart", ".clone-module-group", function(event) {
cloneModuleGroupHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY);
event.preventDefault();
event.stopPropagation();

View file

@ -209,7 +209,7 @@ $.fn.dataTable.render.RepositoryStockValue = function(data) {
</a>`;
}
return `<span class="stock-value-view-render
${data.stock_managable !== undefined ? `stock-${data.stock_status}` : ''}">
${data.displayWarnings ? `stock-${data.stock_status}` : ''}">
${data.value.stock_formatted}
</span>`;
}
@ -231,18 +231,18 @@ $.fn.dataTable.render.defaultRepositoryStockValue = function() {
$.fn.dataTable.render.RepositoryStockConsumptionValue = function(data = {}) {
// covers case of snapshots
if (!data.stock_present && data.value && data.value.consumed_stock_formatted) {
if (!data.stock_present && data.value && data.value.consumed_stock !== null) {
return `<span class="empty-consumed-stock-render">${data.value.consumed_stock_formatted}</span>`;
}
if (!data.stock_present) {
return '<span class="empty-consumed-stock-render"> - </span>';
}
if (!data.consumptionManagable) {
if (!data.consumptionManagable && data.value && !data.value.consumed_stock) {
return `<span class="consumption-locked">
${I18n.t('libraries.manange_modal_column.stock_type.stock_consumption_locked')}
</span>`;
}
if (!data.consumptionPermitted) {
if (!data.consumptionPermitted || !data.consumptionManagable) {
return `<span class="empty-consumed-stock-render">${data.value.consumed_stock_formatted}</span>`;
}
if (!data.value.consumed_stock) {

View file

@ -1,7 +1,8 @@
/*
globals HelperModule animateSpinner SmartAnnotation AssetColumnHelper GLOBAL_CONSTANTS
globals HelperModule animateSpinner SmartAnnotation AssetColumnHelper GLOBAL_CONSTANTS I18n
*/
/* eslint-disable no-unused-vars */
/* eslint-disable no-alert no-unused-vars */
/* eslint-disable no-alert */
var RepositoryDatatableRowEditor = (function() {
const NAME_COLUMN_ID = 'row-name';
@ -49,7 +50,10 @@ var RepositoryDatatableRowEditor = (function() {
$form.submit();
return false;
}).catch((reason) => {
alert(reason);
if (reason.includes('Status: 403')) {
HelperModule.flashAlertMsg(I18n.t('activerecord.errors.storage.limit_reached'), 'danger');
} else alert(reason);
animateSpinner($table, false);
return false;
});

View file

@ -1,28 +1,28 @@
/* global dropdownSelector GLOBAL_CONSTANTS I18n SmartAnnotation formatDecimalValue */
/* global dropdownSelector GLOBAL_CONSTANTS I18n SmartAnnotation formatDecimalValue Decimal */
var RepositoryStockValues = (function() {
const UNIT_SELECTOR = '#repository-stock-value-units';
function updateChangeAmount($element) {
var currentAmount = parseFloat($element.data('currentAmount'));
var inputAmount = parseFloat($element.val());
var newAmount;
if (!$element.val()) {
$('.stock-final-container .value').text('-');
return;
}
if (!(inputAmount >= 0)) return;
if (!($element.val() >= 0)) return;
let currentAmount = new Decimal($element.data('currentAmount') || 0);
let inputAmount = new Decimal($element.val());
let newAmount;
switch ($element.data('operator')) {
case 'set':
newAmount = inputAmount;
break;
case 'add':
newAmount = currentAmount + inputAmount;
newAmount = currentAmount.plus(inputAmount);
break;
case 'remove':
newAmount = currentAmount - inputAmount;
newAmount = currentAmount.minus(inputAmount);
break;
default:
newAmount = currentAmount;
@ -41,6 +41,9 @@ var RepositoryStockValues = (function() {
let amountChanged = false;
$('.repository-show').on('click', '.manage-repository-stock-value-link', function() {
let colIndex = this.parentNode.cellIndex;
let rowIndex = this.parentNode.parentNode.rowIndex;
$.ajax({
url: $(this).closest('tr').data('manage-stock-url'),
type: 'GET',
@ -73,10 +76,15 @@ var RepositoryStockValues = (function() {
.dropdown-selector-container .search-field
`).attr('tabindex', 2);
$manageModal.find('form').on('ajax:success', function() {
var dataTable = $('.dataTable').DataTable();
$manageModal.find('form').on('ajax:success', function(_, data) {
$manageModal.modal('hide');
dataTable.ajax.reload(null, false);
let $cell = $('.dataTable').find(
`tr:nth-child(${rowIndex}) td:nth-child(${colIndex + 1})`
);
$cell.parent().data('manage-stock-url', data.manageStockUrl);
$cell.html(
$.fn.dataTable.render.RepositoryStockValue(data)
);
});
$('.stock-operator-option').click(function() {
@ -114,7 +122,7 @@ var RepositoryStockValues = (function() {
SmartAnnotation.init($('#repository-stock-value-comment')[0]);
$('#repository-stock-value-comment').on('keyup change', function() {
$('#repository-stock-value-comment').on('input', function() {
$(this).closest('.sci-input-container').toggleClass(
'error',
this.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
@ -128,7 +136,6 @@ var RepositoryStockValues = (function() {
$('#reminder-selector-checkbox').on('change', function() {
let valueContainer = $('.repository-stock-reminder-value');
valueContainer.toggleClass('hidden', !this.checked);
valueContainer.find('input').attr('required', this.checked);
if (!this.checked) {
$(this).data('reminder-value', valueContainer.find('input').val());
valueContainer.find('input').val(null);
@ -157,14 +164,50 @@ var RepositoryStockValues = (function() {
} else {
dropdownSelector.hideError(UNIT_SELECTOR);
}
if ($('#stock-input-amount').val().length) {
$('#stock-input-amount').parent().removeClass('error');
let stockInput = $('#stock-input-amount');
if (stockInput.val().length && stockInput.val() >= 0) {
stockInput.parent().removeClass('error');
} else {
$('#stock-input-amount').parent().addClass('error');
stockInput.parent().addClass('error');
if (stockInput.val().length === 0) {
stockInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.amount_error')
);
} else {
stockInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.negative_error')
);
}
status = false;
}
let reminderInput = $('.repository-stock-reminder-value input');
if ($('#reminder-selector-checkbox')[0].checked) {
if (reminderInput.val().length && reminderInput.val() >= 0) {
reminderInput.parent().removeClass('error');
} else {
reminderInput.parent().addClass('error');
if (reminderInput.val().length === 0) {
reminderInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.amount_error')
);
} else {
reminderInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.negative_error')
);
}
status = false;
}
}
return status;
});

View file

@ -1,4 +1,4 @@
/* global dropdownSelector */
/* global GLOBAL_CONSTANTS dropdownSelector */
/* eslint-disable no-unused-vars */
var RepositoryDateColumnType = (function() {
const columnContainer = '.date-column-type';
@ -12,9 +12,7 @@ var RepositoryDateColumnType = (function() {
});
}
function initReminders() {
let $modal = $('#manage-repository-column');
$modal.on('input', `${columnContainer} .reminder-value, ${columnContainer} .reminder-unit`, function() {
function setReminderDelta() {
let reminderValueInput = $(columnContainer).find('.reminder-value');
reminderValueInput.val(reminderValueInput.val().replace(/[^0-9]/, ''));
let value = reminderValueInput.val();
@ -24,6 +22,12 @@ var RepositoryDateColumnType = (function() {
value * $(columnContainer).find('.reminder-unit').val()
);
}
}
function initReminders() {
let $modal = $('#manage-repository-column');
$modal.on('input', `${columnContainer} .reminder-value, ${columnContainer} .reminder-unit`, function() {
setReminderDelta();
});
$modal.on('change', `${columnContainer} #date-reminder, ${columnContainer} #date-range`, function() {
@ -32,10 +36,21 @@ var RepositoryDateColumnType = (function() {
rangeCheckbox.attr('disabled', reminderCheckbox.is(':checked'));
reminderCheckbox.attr('disabled', rangeCheckbox.is(':checked'));
$(columnContainer).find('.reminder-group').toggleClass('hidden', !reminderCheckbox.is(':checked'));
if (reminderCheckbox.is(':checked')) setReminderDelta();
});
$modal.on('columnModal::partialLoadedForRepositoryDateValue', function() {
initReminderUnitDropdown();
$('#date-reminder-message').on('input', function() {
$(this).closest('.sci-input-container').toggleClass(
'error',
this.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
);
$('#update-repo-column-submit').toggleClass(
'disabled',
this.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
);
});
});
}

View file

@ -1,4 +1,4 @@
/* global dropdownSelector */
/* global GLOBAL_CONSTANTS dropdownSelector */
/* eslint-disable no-unused-vars */
var RepositoryDateTimeColumnType = (function() {
const columnContainer = '.datetime-column-type';
@ -12,9 +12,7 @@ var RepositoryDateTimeColumnType = (function() {
});
}
function initReminders() {
let $modal = $('#manage-repository-column');
$modal.on('input', `${columnContainer} .reminder-value, ${columnContainer} .reminder-unit`, function() {
function setReminderDelta() {
let reminderValueInput = $(columnContainer).find('.reminder-value');
reminderValueInput.val(reminderValueInput.val().replace(/[^0-9]/, ''));
let value = reminderValueInput.val();
@ -24,6 +22,12 @@ var RepositoryDateTimeColumnType = (function() {
value * $(columnContainer).find('.reminder-unit').val()
);
}
}
function initReminders() {
let $modal = $('#manage-repository-column');
$modal.on('input', `${columnContainer} .reminder-value, ${columnContainer} .reminder-unit`, function() {
setReminderDelta();
});
$modal.on('change', `${columnContainer} #datetime-reminder, ${columnContainer} #datetime-range`, function() {
@ -32,10 +36,21 @@ var RepositoryDateTimeColumnType = (function() {
rangeCheckbox.attr('disabled', reminderCheckbox.is(':checked'));
reminderCheckbox.attr('disabled', rangeCheckbox.is(':checked'));
$(columnContainer).find('.reminder-group').toggleClass('hidden', !reminderCheckbox.is(':checked'));
if (reminderCheckbox.is(':checked')) setReminderDelta();
});
$modal.on('columnModal::partialLoadedForRepositoryDateTimeValue', function() {
initReminderUnitDropdown();
$('#datetime-reminder-message').on('input', function() {
$(this).closest('.sci-input-container').toggleClass(
'error',
this.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
);
$('#update-repo-column-submit').toggleClass(
'disabled',
this.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
);
});
});
}

View file

@ -26,7 +26,7 @@ var InfiniteScroll = (function() {
$container.data('next-page', result.next_page);
} else {
$container.addClass('last-page');
if ($container.data('config').endOfListTemplate) {
if ($container.data('config').endOfListTemplate && page > 2) {
$($($container.data('config').endOfListTemplate).html()).appendTo($container);
}
}

View file

@ -65,10 +65,20 @@ function prepareRepositoryHeaderForExport(th) {
function initReminderDropdown(table) {
$(table).on('keyup', '.row-reminders-dropdown', function(e) {
if (e.key === 'Escape') {
if (e.key === 'Escape' && $('.row-reminders-dropdown').hasClass('open')) {
$(this).children('.dropdown-menu').dropdown('toggle');
// Preventing closing modal on full view mode for assigning repository items
e.preventDefault();
e.stopPropagation();
}
});
$(table).on('keyup', '.row-reminders-footer', function(e) {
if (e.key === ' ') {
$(this).click();
}
});
$(table).on('show.bs.dropdown', '.row-reminders-dropdown', function() {
let row = $(this).closest('tr');
let screenHeight = screen.height;

View file

@ -0,0 +1,17 @@
$(document).on('turbolinks:load', function() {
$.each($('input[type="password"]'), function(i, e) {
$('<i class="fas fa-eye show-password" style="cursor: pointer; z-index: 10"></i>').insertAfter(e);
$(e).parent().addClass('right-icon');
});
});
$(document).on('click', '.show-password', function() {
let $icon = $(this);
if ($icon.hasClass('fa-eye')) {
$icon.removeClass('fa-eye').addClass('fa-eye-slash');
$icon.parent().find('input[type=password]').attr('type', 'text');
} else {
$icon.removeClass('fa-eye-slash').addClass('fa-eye');
$icon.parent().find('input[type=text]').attr('type', 'password');
}
});

View file

@ -44,6 +44,7 @@
labelHTML: true,
tagClass: 'users-dropdown-list',
inputTagMode: true,
selectKeys: [13, 32, 44, 188],
customDropdownIcon: () => { return '<i class="fas fa-search right-icon"></i>'; },
onChange: () => {
let values = dropdownSelector.getValues(emailsInput);
@ -57,6 +58,14 @@
}
});
modal.find('.search-field').focusout(function() {
if ($(this).val()) {
$(this).trigger(
$.Event('keypress', { keyCode: 13 })
);
}
});
dropdownSelector.init('#role', {
noEmptyOption: true,
singleSelect: true,

View file

@ -45,14 +45,13 @@
.assigned-repository-title {
@include my-module-repository-title;
padding-right: 2.2em;
&::after {
color: $color-alto;
content: '[' attr(data-rows-count) ']';
display: inline-block;
padding-left: 1px;
padding-right: .7em;
position: absolute;
right: 0;
}
}
@ -214,6 +213,11 @@
color: $color-volcano;
}
.empty-stock-render,
.consumption-locked {
color: $color-silver-chalice;
}
.row-stock,
.row-consumption {
min-width: 140px;

View file

@ -27,6 +27,14 @@ $color-module-hover: $brand-primary;
.actions-button {
margin-right: 15px;
.fas {
margin-right: .4em;
}
a {
padding: .5em 1em;
}
}
.toolbarButtons {

View file

@ -120,7 +120,7 @@
padding: 1em;
.apply-button {
margin-left: auto !important;
margin-left: auto;
}
.filters-columns-list {
@ -232,6 +232,49 @@
}
}
@media (max-width: 992px) {
.filters-container {
width: 200px;
.filter-container {
.filter-attributes {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.operator-selector {
.dropdown-selector-container {
width: 125px;
}
}
.date-time-picker {
display: flex;
flex-wrap: wrap;
}
.between-delimiter {
height: 10px;
margin: 5px auto;
transform: rotate(90deg);
}
}
}
.footer {
display: flex;
flex-wrap: wrap;
justify-content: center;
text-align: center;
.apply-button {
margin-left: 0;
margin-top: .5em;
}
}
}
}
.task-option {
background-image: image-url("icon_small/task-black.svg");
background-position: left center;

View file

@ -75,13 +75,14 @@
@media (max-width: 1299px) {
.repository-toolbar {
.btn {
.btn:not(.prevent-shrink) {
padding: 7px;
width: 36px;
}
.button-text {
display: none;
}
}
.auto-shrink-button {
.fas {

View file

@ -53,6 +53,13 @@ module Api
)
end
rescue_from URI::InvalidURIError do
render_error(
I18n.t('api.service.errors.url.not_valid'),
I18n.t('api.service.errors.url.not_valid'), :bad_request
)
end
rescue_from JWT::DecodeError,
JWT::InvalidPayload,
JWT::VerificationError,

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Api
module Service
class ProjectsJsonExportController < BaseController
require 'uri'
def projects_json_export
projects_json_export_params = projects_json_export_data_params
valid_url?(projects_json_export_params[:callback_url])
ProjectsJsonExportJob.perform_later(projects_json_export_params[:task_ids],
projects_json_export_params[:callback_url],
current_user)
render json: { status: :ok }, status: :accepted
end
private
def projects_json_export_data_params
raise ActionController::ParameterMissing, I18n.t('api.service.errors.missing_task_ids') unless
params.require(:data).require(:task_ids)
raise ActionController::ParameterMissing, I18n.t('api.service.errors.callback_missing') unless
params.require(:data).require(:callback_url)
params.require(:data).permit(:callback_url, task_ids: [])
end
def valid_url?(url)
uri = URI.parse(url)
raise URI::InvalidURIError if uri.host.blank?
end
end
end
end

View file

@ -1,13 +1,15 @@
class CanvasController < ApplicationController
before_action :load_vars
before_action :check_view_canvas, only: [:edit, :full_zoom, :medium_zoom, :small_zoom]
before_action :check_edit_canvas, only: [:edit, :update]
before_action :check_view_canvas, except: %i(edit update)
before_action :check_edit_canvas, only: %i(edit update)
def edit
render partial: 'canvas/edit',
locals: { experiment: @experiment, my_modules: @my_modules },
:content_type => 'text/html'
locals: {
experiment: @experiment,
my_modules: @my_modules
}, content_type: 'text/html'
end
def full_zoom
@ -201,11 +203,12 @@ class CanvasController < ApplicationController
end
end
@my_modules = @experiment.my_modules.active
@my_modules = @experiment.my_modules.active.preload(outputs: :to, user_assignments: %i(user user_role))
end
def check_edit_canvas
render_403 and return unless can_manage_experiment?(@experiment)
@experiment_managable = can_manage_experiment?(@experiment)
return render_403 unless @experiment_managable
end
def check_view_canvas

View file

@ -67,8 +67,13 @@ class ExperimentsController < ApplicationController
def canvas
redirect_to module_archive_experiment_path(@experiment) if @experiment.archived_branch?
@project = @experiment.project
@active_modules = @experiment.my_modules.active.order(:name).includes(:tags, :inputs, :outputs)
current_team_switch(@project.team)
@active_modules = @experiment.my_modules.active.order(:name)
.left_outer_joins(:designated_users, :task_comments)
.preload(:tags, outputs: :to)
.preload(:my_module_status, :my_module_group, user_assignments: %i(user user_role))
.select('COUNT(DISTINCT users.id) as designated_users_count')
.select('COUNT(DISTINCT comments.id) as task_comments_count')
.select('my_modules.*').group(:id)
end
def edit
@ -254,6 +259,12 @@ class ExperimentsController < ApplicationController
def module_archive
@my_modules = @experiment.archived_branch? ? @experiment.my_modules : @experiment.my_modules.archived
@my_modules = @my_modules.with_granted_permissions(current_user, MyModulePermissions::READ_ARCHIVED)
.left_outer_joins(:designated_users, :task_comments)
.preload(:tags, outputs: :to)
.preload(:my_module_status, :my_module_group, user_assignments: %i(user user_role))
.select('COUNT(DISTINCT users.id) as designated_users_count')
.select('COUNT(DISTINCT comments.id) as task_comments_count')
.select('my_modules.*').group(:id)
end
def fetch_workflow_img
@ -290,7 +301,7 @@ class ExperimentsController < ApplicationController
private
def load_experiment
@experiment = Experiment.find_by(id: params[:id])
@experiment = Experiment.preload(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @experiment
end

View file

@ -167,6 +167,7 @@ class MyModuleRepositoriesController < ApplicationController
module_repository_row.consume_stock(current_user, params[:stock_consumption], params[:comment])
log_activity(module_repository_row, current_stock, params[:comment])
protocol_consumption_notification(params[:comment], module_repository_row)
end
render json: {}, status: :ok
@ -229,6 +230,21 @@ class MyModuleRepositoriesController < ApplicationController
end
end
def protocol_consumption_notification(comment, module_repository_row)
smart_annotation_notification(
old_text: nil,
new_text: comment,
title: t('notifications.my_module_consumption_comment_annotation_title',
repository_item: module_repository_row.repository_row.name,
repository: @repository.name,
user: current_user.full_name),
message: t('notifications.my_module_consumption_comment_annotation_message_html',
project: link_to(@my_module.experiment.project.name, project_url(@my_module.experiment.project)),
experiment: link_to(@my_module.experiment.name, canvas_experiment_url(@my_module.experiment)),
my_module: link_to(@my_module.name, protocols_my_module_url(@my_module)))
)
end
def log_activity(module_repository_row, stock_consumption_was, comment)
Activities::CreateActivityService
.call(activity_type: :task_inventory_item_stock_consumed,

View file

@ -58,6 +58,15 @@ class MyModulesController < ApplicationController
end
end
def canvas_dropdown_menu
@experiment_managable = can_manage_experiment?(@experiment)
@group_my_modules = @my_module.my_module_group&.my_modules&.preload(user_assignments: %i(user user_role))
render json: {
html: render_to_string({ partial: 'canvas/edit/my_module_dropdown_menu',
locals: { my_module: @my_module } })
}
end
def activities
params[:subjects] = {
MyModule: [@my_module.id]
@ -340,7 +349,7 @@ class MyModulesController < ApplicationController
private
def load_vars
@my_module = MyModule.find_by_id(params[:id])
@my_module = MyModule.preload(user_assignments: %i(user user_role)).find_by(id: params[:id])
if @my_module
@experiment = @my_module.experiment
@project = @my_module.experiment.project if @experiment

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class RepositoryStockValuesController < ApplicationController
include RepositoryDatatableHelper # for use of display_cell_value method on stock update
before_action :load_vars
before_action :check_manage_permissions
@ -45,7 +47,11 @@ class RepositoryStockValuesController < ApplicationController
)
end
render json: @repository_stock_value
render json: {
stock_managable: true,
stock_status: @repository_stock_value.status,
manageStockUrl: edit_repository_stock_repository_repository_row_url(@repository, @repository_row)
}.merge(display_cell_value(@repository_stock_value.repository_cell, current_team, @repository))
end
private

View file

@ -58,6 +58,7 @@ class ResultAssetsController < ApplicationController
if update_params.dig(:asset_attributes, :signed_blob_id)
@result.asset.last_modified_by = current_user
@result.asset.update(file: update_params[:asset_attributes][:signed_blob_id])
@result.asset.file_pdf_preview.purge if @result.asset.file_pdf_preview.attached?
update_params.delete(:asset_attributes)
end

View file

@ -2,7 +2,6 @@ class TagsController < ApplicationController
before_action :load_vars, only: [:create, :update, :destroy]
before_action :load_vars_nested, only: [:update, :destroy]
before_action :check_manage_permissions, only: %i(create update destroy)
before_action :check_manage_my_module_permissions, only: %i(create)
def create
@tag = Tag.new(tag_params)
@ -154,16 +153,12 @@ class TagsController < ApplicationController
end
end
def check_manage_my_module_permissions
def check_manage_permissions
my_module = MyModule.find_by id: params[:my_module_id]
render_403 if my_module && !can_manage_my_module_tags?(my_module)
end
def check_manage_permissions
render_403 unless can_manage_project_tags?(@project)
end
def tag_params
params.require(:tag).permit(:name, :color, :project_id)
end

View file

@ -59,15 +59,19 @@ module RepositoryDatatableHelper
# always add stock cell, even if empty
row['stock'] = stock_present ? display_cell_value(record.repository_stock_cell, team, repository) : {}
row['stock'][:stock_managable] = stock_managable
if !options[:include_stock_consumption] || stock_consumption_permitted?(repository, options[:my_module])
row['stock']['displayWarnings'] = display_stock_warnings?(repository)
row['stock'][:stock_status] = record.repository_stock_cell&.value&.status
end
row['stock']['value_type'] = 'RepositoryStockValue'
if options[:include_stock_consumption] && record.repository.has_stock_management? && options[:my_module]
consumption_managable = stock_consumption_managable?(record, repository, options[:my_module])
consumed_stock_formatted =
number_with_precision(
record.consumed_stock,
precision: (record.repository.repository_stock_column.metadata['decimals'].to_i || 0),
strip_insignificant_zeros: true
)
row['consumedStock'] = {
stock_present: stock_present,
consumptionPermitted: stock_consumption_permitted?(repository, options[:my_module]),
@ -80,7 +84,7 @@ module RepositoryDatatableHelper
value: {
consumed_stock: record.consumed_stock,
consumed_stock_formatted:
"#{record.consumed_stock || 0} #{record.repository_stock_value&.repository_stock_unit_item&.data}"
"#{consumed_stock_formatted || 0} #{record.repository_stock_value&.repository_stock_unit_item&.data}"
}
}
end
@ -115,10 +119,8 @@ module RepositoryDatatableHelper
consumption_managable = stock_consumption_managable?(record, repository, my_module)
row['stock'] = stock_present ? display_cell_value(record.repository_stock_cell, record.repository.team, repository) : {}
if !options[:include_stock_consumption] || stock_consumption_permitted?(repository, my_module)
row['stock']['displayWarnings'] = display_stock_warnings?(repository)
row['stock'][:stock_status] = record.repository_stock_cell&.value&.status
end
row['stock'][:stock_managable] = stock_managable
if record.repository.is_a?(RepositorySnapshot)
@ -136,10 +138,16 @@ module RepositoryDatatableHelper
my_module, record.repository, row_id: record.id
)
end
consumed_stock_formatted =
number_with_precision(
record.consumed_stock,
precision: (record.repository.repository_stock_column.metadata['decimals'].to_i || 0),
strip_insignificant_zeros: true
)
row['consumedStock'][:value] = {
consumed_stock: record.consumed_stock,
consumed_stock_formatted:
"#{record.consumed_stock || 0} #{record.repository_stock_value&.repository_stock_unit_item&.data}"
"#{consumed_stock_formatted || 0} #{record.repository_stock_value&.repository_stock_unit_item&.data}"
}
end
@ -170,10 +178,14 @@ module RepositoryDatatableHelper
end
if options[:include_stock_consumption] && repository_snapshot.has_stock_management?
stock_present = record.repository_stock_cell.present?
row['stock'] = stock_present ? display_cell_value(record.repository_stock_cell, team, repository_snapshot) : {}
row['stock'] = if record.repository_stock_cell.present?
display_cell_value(record.repository_stock_cell, team, repository_snapshot)
else
{ value_type: 'RepositoryStockValue' }
end
row['consumedStock'] =
if stock_present
if record.repository_stock_consumption_cell.present?
display_cell_value(record.repository_stock_consumption_cell, team, repository_snapshot)
else
{}
@ -262,6 +274,9 @@ module RepositoryDatatableHelper
# don't load reminders for archived repositories
return [] if repository_rows.blank? || repository.archived?
# don't load reminders for snapshots
return [] if repository.is_a?(RepositorySnapshot)
repository_rows.active.with_active_reminders(current_user).to_a.pluck(:id).uniq
end
@ -277,4 +292,8 @@ module RepositoryDatatableHelper
true
end
def display_stock_warnings?(repository)
!repository.is_a?(RepositorySnapshot)
end
end

View file

@ -1 +1,2 @@
window.bwipjs = require('bwip-js');
window.Decimal = require('decimal.js');

View file

@ -19,7 +19,7 @@
/>
</div>
</div>
<button class="btn btn-light clear-filters-btn" @click="clearFilters()">
<button class="btn btn-light clear-filters-btn prevent-shrink" @click="clearFilters()">
<i class="fas fa-times-circle"></i>
{{ i18n.t('repositories.show.filters.clear') }}
</button>
@ -36,7 +36,7 @@
</div>
<div class="footer">
<div id="filtersColumnsDropdown" class="dropup filters-columns-dropdown" @click="toggleColumnsFilters">
<button class="btn btn-secondary add-filter" >
<button class="btn btn-secondary add-filter prevent-shrink" >
<i class="fas fa-plus"></i>
{{ i18n.t('repositories.show.filters.add_filter') }}
</button>
@ -50,7 +50,7 @@
/>
</div>
</div>
<button @click="$emit('filters:apply')" class="btn btn-primary apply-button">
<button @click="$emit('filters:apply')" class="btn btn-primary apply-button prevent-shrink">
{{ i18n.t('repositories.show.filters.apply') }}
</button>
</div>

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class ProjectsJsonExportJob < ApplicationJob
def perform(task_ids, callback, user)
projects_json_export_service = ProjectsJsonExportService.new(task_ids,
callback,
user)
projects_json_export_service.generate_data
projects_json_export_service.post_request
end
end

View file

@ -15,7 +15,8 @@ module ReminderRepositoryCellJoinable
).joins( # stock reminders
'LEFT OUTER JOIN "repository_stock_values" ON "repository_stock_values"."id" = "repository_cells"."value_id" AND '\
'"repository_cells"."value_type" = \'RepositoryStockValue\' AND '\
'repository_stock_values.amount <= repository_stock_values.low_stock_threshold'
'(repository_stock_values.amount <= repository_stock_values.low_stock_threshold OR '\
' repository_stock_values.amount <= 0)'
).joins(
'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON '\
'"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND '\

View file

@ -3,6 +3,7 @@
class MyModule < ApplicationRecord
SEARCHABLE_ATTRIBUTES = ['my_modules.name', 'my_modules.description']
include ActionView::Helpers::NumberHelper
include ArchivableModel
include SearchableModel
include SearchableByNameModel
@ -310,8 +311,7 @@ class MyModule < ApplicationRecord
rows = rows.left_joins(my_module_repository_rows: :repository_stock_unit_item)
.select(
'repository_rows.*',
'my_module_repository_rows.stock_consumption',
'repository_stock_unit_items.data AS stock_unit'
'my_module_repository_rows.stock_consumption'
)
end
rows.find_each do |row|
@ -325,7 +325,18 @@ class MyModule < ApplicationRecord
consumed_stock = row.repository_stock_consumption_cell&.value&.formatted
row_json << (consumed_stock || 0)
else
row_json << "#{row['stock_consumption'] || 0} #{row['stock_unit']}"
consumed_stock_formatted =
if row.repository_stock_cell.present?
consumed_stock = number_with_precision(
row.stock_consumption || 0,
precision: (row.repository.repository_stock_column.metadata['decimals'].to_i || 0),
strip_insignificant_zeros: true
)
"#{consumed_stock} #{row.repository_stock_value&.repository_stock_unit_item&.data}"
else
'-'
end
row_json << consumed_stock_formatted
end
end
data << row_json

View file

@ -1,8 +1,8 @@
class MyModuleRepositoryRow < ApplicationRecord
include ActionView::Helpers::NumberHelper
attr_accessor :last_modified_by
attr_accessor :comment
attribute :last_modified_by, :integer
attribute :comment, :text
belongs_to :assigned_by,
foreign_key: 'assigned_by_id',

View file

@ -65,6 +65,8 @@ class RepositoryAssetValue < ApplicationRecord
asset.file.attach(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?
asset.last_modified_by = user
self.last_modified_by = user
asset.save! && save!

View file

@ -87,8 +87,12 @@ class RepositoryColumn < ApplicationRecord
end
def importable?
if data_type == 'RepositoryStockValue'
RepositoryBase.stock_management_enabled?
else
Extends::REPOSITORY_IMPORTABLE_TYPES.include?(data_type.to_sym)
end
end
def deep_dup
new_column = super

View file

@ -14,8 +14,12 @@ class RepositoryStockValue < ApplicationRecord
accepts_nested_attributes_for :repository_cell
validates :repository_cell, presence: true
validates :low_stock_threshold, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
before_save :update_consumption_stock_units, if: :repository_stock_unit_item_id_changed?
after_create do
next if is_a?(RepositoryStockConsumptionValue)
repository_ledger_records.create!(user: created_by,
amount: amount,
balance: amount,
@ -182,4 +186,12 @@ class RepositoryStockValue < ApplicationRecord
end
alias export_formatted formatted
private
def update_consumption_stock_units
repository_cell.repository_row
.my_module_repository_rows
.update_all(repository_stock_unit_item_id: repository_stock_unit_item_id)
end
end

View file

@ -2,7 +2,6 @@
class Settings < ApplicationRecord
def self.instance
@instance ||= first
@instance ||= new
first || new
end
end

View file

@ -27,7 +27,7 @@ Canaid::Permissions.register_for(Asset) do
if object.repository_column.repository.is_a?(RepositorySnapshot)
false
else
can_manage_repository?(user, object.repository_column.repository)
can_manage_repository_assets?(user, object.repository_column.repository)
end
end
end

View file

@ -19,7 +19,7 @@ Canaid::Permissions.register_for(Experiment) do
# assign/reassign/unassign tags
can :manage_experiment do |user, experiment|
experiment.permission_granted?(user, ExperimentPermissions::MANAGE) &&
experiment.my_modules.all? do |my_module|
experiment.my_modules.preload(my_module_status: :my_module_status_implications).all? do |my_module|
if my_module.my_module_status
my_module.my_module_status.my_module_status_implications.all? { |implication| implication.call(my_module) }
else
@ -72,7 +72,7 @@ Canaid::Permissions.register_for(Experiment) do
end
can :clone_experiment do |user, experiment|
experiment.permission_granted?(user, ExperimentPermissions::MANAGE)
experiment.permission_granted?(user, ExperimentPermissions::READ)
end
can :move_experiment do |user, experiment|

View file

@ -69,6 +69,10 @@ Canaid::Permissions.register_for(Repository) do
end
end
can :manage_repository_assets do |user, repository|
can_create_repository_rows?(user, repository)
end
# repository: update/delete records
can :manage_repository_rows do |user, repository|
can_create_repository_rows?(user, repository)

View file

@ -7,10 +7,6 @@ Canaid::Permissions.register_for(RepositoryColumn) do
managable = repository_column.repository.repository_snapshots.provisioning.none? &&
can_create_repository_columns?(user, repository_column.repository)
if repository_column.data_type == 'RepositoryStockValue'
managable = can_manage_repository_stock?(user, repository_column.repository) && managable
end
managable
end
end

View file

@ -0,0 +1,183 @@
# frozen_string_literal: true
class ProjectsJsonExportService
include Canaid::Helpers::PermissionsHelper
require 'net/http'
def initialize(task_ids, callback, user)
@user = user
@task_ids = task_ids
@experiment_ids = MyModule.where(id: @task_ids).pluck(:experiment_id).uniq
@project_ids = Experiment.where(id: @experiment_ids).pluck(:project_id).uniq
@callback = callback
@storage_location = ENV['ACTIVESTORAGE_SERVICE'] || 'local'
@request_json = {}
end
def generate_data
project_json = []
projects = Project.where(id: @project_ids)
projects.find_each do |prj|
next unless can_read_project?(@user, prj)
project = prj.as_json(only: %i(id name))
experiments = []
prj.experiments.find_each do |exp|
if @experiment_ids.map(&:to_i).include?(exp.id.to_i)
next unless can_read_experiment?(@user, exp)
experiment = exp.as_json(only: %i(id name description))
tasks = []
exp.my_modules.where(archived: false).find_each do |tsk|
if @task_ids.map(&:to_i).include?(tsk.id.to_i)
next unless can_read_my_module?(@user, tsk)
tasks << task_json(tsk)
end
end
experiment['tasks'] = tasks if tasks.any?
experiments << experiment
end
project['experiments'] = experiments if experiments.any?
end
project_json << project
end
@request_json['projects'] = project_json
end
def post_request
url = URI.parse(@callback)
req = Net::HTTP::Post.new(url.request_uri, 'Content-Type': 'application/json;charset=utf-8')
Rails.logger.info(@request_json.to_json)
req.body = @request_json.to_json
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
http.request(req)
end
private
def protocol_json(protocol)
protocol_json = protocol.as_json(only: %i(id description))
t_m_files = protocol.tiny_mce_assets.map { |asset| tiny_mce_file_json(asset) }
protocol_json['tiny_mce_files'] = t_m_files if t_m_files.any?
protocol_json
end
def step_json(step)
step_json = step.as_json(only: %i(id position name description completed))
checklists = []
step.checklists.find_each do |cl|
checklist = cl.as_json(only: %i(id name))
items = []
cl.checklist_items.find_each do |cli|
item = cli.as_json(only: %i(id position text checked))
items << item
end
checklist['items'] = items if items.any?
checklists << checklist
end
step_json['checklists'] = checklists if checklists.any?
files = []
step.assets.find_each do |asset|
files << asset_file_json(asset)
end
step_json['files'] = files if files.any?
t_m_files = []
step.tiny_mce_assets.find_each do |asset|
t_m_files << tiny_mce_file_json(asset)
end
step_json['tiny_mce_files'] = t_m_files if t_m_files.any?
tables = []
step.tables.find_each do |tbl|
table = tbl.as_json(only: %i(id name))
table['contents'] = JSON.parse(tbl.contents)['data']
tables << table
end
step_json['tables'] = tables if tables.any?
step_json
end
def task_json(task)
task_json = task.as_json(only: %i(id name description))
t_m_files = task.tiny_mce_assets.map { |asset| tiny_mce_file_json(asset) }
task_json['tiny_mce_files'] = t_m_files if t_m_files.any?
steps = []
task_json['protocols'] = []
task.protocols.find_each do |protocol|
task_json['protocols'] << protocol_json(protocol)
protocol.steps.order(:position).find_each { |stp| steps << step_json(stp) }
end
task_json['steps'] = steps if steps.any?
results = []
Rails.logger.info(task.results.to_yaml.to_s)
task.results
.where(archived: false)
.order(created_at: :desc)
.find_each { |res| results << result_json(res) }
task_json['results'] = results if results.any?
task_json
end
def result_json(result)
result_json = result.as_json(only: %i(id name))
if result.result_text
result_json['type'] = 'text'
result_json['text'] = result.result_text.text
t_m_files = []
result.result_text.tiny_mce_assets.find_each do |asset|
t_m_files << tiny_mce_file_json(asset)
end
result_json['tiny_mce_files'] = t_m_files if t_m_files.any?
elsif result.asset
result_json['type'] = 'file'
result_json['bucket'] = ENV['S3_BUCKET']
result_json['key'] = ActiveStorage::Blob.service.path_for(result.asset.file.key)
result_json['url'] = asset_url(result.asset)
elsif result.table
result_json['type'] = 'table'
result_json['contents'] = JSON.parse(result.table.contents)['data']
end
result_json
end
def asset_file_json(asset)
if ENV['ACTIVESTORAGE_SERVICE'] && ENV['S3_BUCKET']
{
'storage' => ENV['ACTIVESTORAGE_SERVICE'],
'id' => asset.id,
'bucket' => ENV['S3_BUCKET'],
'key' => ActiveStorage::Blob.service.path_for(asset.file.key),
'url' => asset_url(asset)
}
else
{
'storage' => 'local',
'id' => asset.id,
'url' => asset_url(asset)
}
end
end
def tiny_mce_file_json(asset)
if ENV['ACTIVESTORAGE_SERVICE'] && ENV['S3_BUCKET']
{
'storage' => @storage_location,
'id' => asset.id,
'bucket' => ENV['S3_BUCKET'],
'key' => ActiveStorage::Blob.service.path_for(asset.image.key)
}
else
{
'storage' => @storage_location,
'id' => asset.id,
'url' => Rails.application.routes.url_helpers.url_for(asset.image)
}
end
end
def asset_url(asset)
ENV['MAIL_SERVER_URL'] + Rails.application.routes.url_helpers.asset_file_url_path(asset)
end
end

View file

@ -83,10 +83,7 @@ module ReportActions
my_module_element = save_element!({ 'my_module_id' => my_module.id }, :my_module, experiment_element)
@repositories.each do |repository|
repository = assigned_repository_or_snapshot(my_module, repository)
next unless repository
my_module.live_and_snapshot_repositories_list.each do |repository|
save_element!(
{ 'my_module_id' => my_module.id, 'repository_id' => repository.id },
:my_module_repository,

View file

@ -2,6 +2,7 @@
module Reports::Docx::RepositoryHelper
include InputSanitizeHelper
include ActionView::Helpers::NumberHelper
def prepare_row_columns(repository_data, my_module, repository)
result = [repository_data[:headers]]
@ -20,7 +21,11 @@ module Reports::Docx::RepositoryHelper
consumed_stock = record.repository_stock_consumption_cell&.value&.formatted || 0
cell_values[cell.repository_column_id] = consumed_stock
else
consumption = record.my_module_repository_rows.find_by(my_module: my_module)&.stock_consumption || 0
consumption = number_with_precision(
record.my_module_repository_rows.find_by(my_module: my_module)&.stock_consumption || 0,
precision: (record.repository.repository_stock_column.metadata['decimals'].to_i || 0),
strip_insignificant_zeros: true
)
unit = cell.value.repository_stock_unit_item&.data
cell_values[cell.repository_column_id] = "#{consumption} #{unit}"
end
@ -28,6 +33,9 @@ module Reports::Docx::RepositoryHelper
cell_values[cell.repository_column_id] = cell.value.formatted
end
end
if repository.repository_stock_column.present? && record.repository_stock_cell.blank?
cell_values[repository.repository_stock_column.id] = '-'
end
repository_data[:custom_columns].each do |column_id|
value = cell_values[column_id]

View file

@ -71,10 +71,16 @@ module Repositories
my_module_repository_row =
repository_row.my_module_repository_rows.find { |mrr| mrr.my_module_id == @repository_snapshot.my_module_id }
stock_unit_item =
@repository_snapshot.repository_stock_consumption_column
stock_unit_item_data =
if my_module_repository_row.repository_stock_unit_item.present?
my_module_repository_row.repository_stock_unit_item.data
else
repository_row.repository_stock_cell&.repository_stock_value&.repository_stock_unit_item&.data
end
stock_unit_item = @repository_snapshot.repository_stock_consumption_column
.repository_stock_unit_items
.find { |item| item.data == my_module_repository_row.repository_stock_unit_item&.data }
.find { |item| item.data == stock_unit_item_data }
RepositoryStockConsumptionValue.create!(
repository_cell_attributes: {
repository_column: @repository_snapshot.repository_stock_consumption_column,

View file

@ -52,7 +52,7 @@ module SmartAnnotations
def generate_rep_snippet(name, object)
if object&.repository
repository_name = fetch_repository_name(object)
"<a href='#{ROUTES.repository_path(object)}' " \
"<a href='#{ROUTES.repository_path(object.repository)}' " \
"><span class='sa-type'>#{trim_repository_name(repository_name)}</span>" \
"#{object.name} #{object.archived? ? I18n.t('atwho.res.archived') : ''}</a>"
else

View file

@ -1,15 +1,15 @@
<div id="update-canvas"
data-can-create-modules="<%= can_manage_experiment?(@experiment) ? "yes" : "no" %>"
data-can-edit-modules="<%= can_manage_experiment?(@experiment) ? "yes" : "no" %>"
data-can-clone-modules="<%= can_manage_experiment?(@experiment) ? "yes" : "no" %>"
data-can-move-modules="<%= can_manage_experiment?(@experiment) ? "yes" : "no" %>"
data-can-delete-modules="<%= can_manage_experiment?(@experiment) ? "yes" : "no" %>"
data-can-reposition-modules="<%= can_manage_experiment?(@experiment) ? "yes" : "no" %>"
data-can-edit-connections="<%= can_manage_experiment?(@experiment) ? "yes" : "no" %>"
data-can-create-modules="<%= @experiment_managable ? "yes" : "no" %>"
data-can-edit-modules="<%= @experiment_managable ? "yes" : "no" %>"
data-can-clone-modules="<%= @experiment_managable ? "yes" : "no" %>"
data-can-move-modules="<%= @experiment_managable ? "yes" : "no" %>"
data-can-delete-modules="<%= @experiment_managable ? "yes" : "no" %>"
data-can-reposition-modules="<%= @experiment_managable ? "yes" : "no" %>"
data-can-edit-connections="<%= @experiment_managable ? "yes" : "no" %>"
data-unsaved-work-text="<%=t "experiments.canvas.edit.unsaved_work" %>"
>
<%= bootstrap_form_tag url: canvas_experiment_url, method: "post", html: {class: "canvas-header"} do |f| %>
<% if can_manage_experiment?(@experiment) %>
<% if @experiment_managable %>
<%=link_to "", type: "button", class: "btn btn-primary", id: "canvas-new-module" do %>
<span class="hbtn-default">
<span class="fas fa-credit-card"></span>
@ -71,7 +71,7 @@
</span>
</div>
<div id="diagram-container">
<% if can_manage_experiment?(@experiment) %>
<% if @experiment_managable %>
<div id="diagram" class="diagram">
<% my_modules.each do |my_module| %>
<% if can_read_my_module?(my_module) %>
@ -85,7 +85,7 @@
</div>
<%-# Since we need to preload modals, we just check permission for experiment, instead of permissions for every module and module group -%>
<% if can_manage_experiment?(@experiment) %>
<% if @experiment_managable %>
<%= render partial: "canvas/edit/modal/new_module", locals: {experiment: @experiment} %>
<%= render partial: "canvas/edit/modal/edit_module", locals: {experiment: @experiment } %>
<%= render partial: "canvas/edit/modal/move_module", locals: {experiment: @experiment } %>

View file

@ -7,12 +7,12 @@
<span class="fas fa-tag"></span>
</div>
<% end %>
<% if tags2.count == 0 %>
<% if tags2.length.zero? %>
<div class="add-tag last"><span class="fas fa-tag"></span></div>
<% end %>
<% if my_module.tags.count > 0 %>
<% if my_module.tags.size.positive? %>
<span class="badge badge-indicator">
<%= my_module.tags.count %>
<%= my_module.tags.size %>
</span>
<% else %>
<span class="badge badge-indicator <%= "invisible" unless can_manage_my_module?(my_module) %>">

View file

@ -6,68 +6,23 @@
data-module-y="<%= my_module.y %>"
data-module-conns="<%= construct_module_connections(my_module) %>">
<% module_group = my_module.my_module_group %>
<div class="panel-heading">
<h3 class="panel-title"><%= my_module.name %></h3>
<h3 class="panel-title", title="<%= my_module.name %>"><%= my_module.name %></h3>
<div class="dropdown pull-right module-options">
<div class="dropdown module-options-dropdown pull-right module-options" data-dropdown-menu-path="<%= canvas_dropdown_menu_my_module_path(my_module) %>">
<a class="dropdown-toggle" id="<%= my_module.id %>_options" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="fas fa-caret-down" aria-hidden="true"></span>
</a>
<ul class="dropdown-menu custom-dropdown-menu no-scale" aria-labelledby="<%= my_module.id %>_options">
<% if can_manage_my_module?(my_module) %>
<li>
<a class="edit-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.edit_module') %></a>
</li>
<% end %>
<% if can_manage_experiment?(my_module.experiment) %>
<li>
<a class ="clone-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.clone_module') %></a>
</li>
<% end %>
<% if can_move_my_module?(my_module) %>
<li>
<a class="move-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.move_module') %></a>
</li>
<% end %>
<li>
<%= link_to t('experiments.canvas.edit.task_access'),
can_manage_my_module_users?(my_module) ? edit_access_permissions_project_experiment_my_module_path(my_module.experiment.project, my_module.experiment, my_module) : access_permissions_project_experiment_my_module_path(my_module.experiment.project, my_module.experiment, my_module),
data: { action: 'remote-modal'} %>
</li>
<% if can_archive_my_module?(my_module) %>
<li>
<a class="delete-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.delete_module') %></a>
</li>
<% end %>
<% if can_manage_experiment?(my_module.experiment) %>
<li <%= 'style=display:none;' if my_module.my_module_group.blank? %>>
<a class ="clone-module-group" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.clone_module_group') %></a>
</li>
<% end %>
<% if module_group&.my_modules&.all? { |my_module| can_move_my_module?(my_module) } %>
<li>
<a class="move-module-group" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.move_module_group') %></a>
</li>
<% end %>
<% if module_group&.my_modules&.all? { |my_module| can_archive_my_module?(my_module) } %>
<li data-hook="archive-module-group">
<a class ="delete-module-group" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.delete_module_group') %></a>
</li>
<% end %>
</ul>
</div>
</div>
<% if can_manage_experiment?(my_module.experiment) %>
<div class="panel-body ep">
<%= t('experiments.canvas.edit.drag_connections') %>
</div>
<% end %>
<div class="overlay"></div>

View file

@ -0,0 +1,41 @@
<% if can_manage_my_module?(my_module) %>
<li>
<a class="edit-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.edit_module') %></a>
</li>
<% end %>
<% if @experiment_managable %>
<li>
<a class="clone-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.clone_module') %></a>
</li>
<% end %>
<% if can_move_my_module?(my_module) %>
<li>
<a class="move-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.move_module') %></a>
</li>
<% end %>
<li>
<%= link_to t('experiments.canvas.edit.task_access'),
can_manage_my_module_users?(my_module) ? edit_access_permissions_project_experiment_my_module_path(my_module.experiment.project, my_module.experiment, my_module) : access_permissions_project_experiment_my_module_path(my_module.experiment.project, my_module.experiment, my_module),
data: { action: 'remote-modal'} %>
</li>
<% if can_archive_my_module?(my_module) %>
<li>
<a class="delete-module" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.delete_module') %></a>
</li>
<% end %>
<% if @experiment_managable %>
<li <%= 'style=display:none;' if my_module.my_module_group.blank? %>>
<a class="clone-module-group" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.clone_module_group') %></a>
</li>
<% end %>
<% if @group_my_modules&.all? { |my_module| can_move_my_module?(my_module) } %>
<li>
<a class="move-module-group" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.move_module_group') %></a>
</li>
<% end %>
<% if @group_my_modules&.all? { |my_module| can_archive_my_module?(my_module) } %>
<li data-hook="archive-module-group">
<a class="delete-module-group" href="" data-module-id="<%= my_module.id %>"><%= t('experiments.canvas.edit.delete_module_group') %></a>
</li>
<% end %>

View file

@ -41,7 +41,7 @@
<div class="panel-heading">
<h3 class="panel-title">
<%= link_to_if can_read_experiment?(my_module.experiment), my_module.name, protocols_my_module_path(my_module), class: 'my-module-title' %>
<%= link_to my_module.name, protocols_my_module_path(my_module), title: my_module.name, class: 'my-module-title' %>
</h3>
</div>
@ -82,13 +82,12 @@
<div class="panel-footer panel-footer-scinote buttons-container">
<ul class="nav nav-tabs nav-tabs-less" role="tablist">
<% if can_read_experiment?(my_module.experiment) %>
<li role="presentation">
<a class="btn btn-link task-card-view-users" href="<%= index_old_my_module_user_my_modules_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_users" role="tab" data-remote="true">
<span class="fas fa-users" aria-hidden="true"></span>
<span class="badge badge-indicator users-badge-indicator <%= 'hidden' unless my_module.designated_users.count.positive? %>"
<span class="badge badge-indicator users-badge-indicator <%= 'hidden' unless my_module.designated_users_count.positive? %>"
data-linked-id="<%= my_module.id %>">
<%= my_module.designated_users.count %>
<%= my_module.designated_users_count %>
</span>
</a>
</li>
@ -100,23 +99,20 @@
<li role="presentation">
<a class="btn btn-link task-card-view-comments" href="<%= my_module_my_module_comments_url(my_module_id: my_module.id, format: :json) %>" aria-controls="<%= my_module.id %>_comments" role="tab" data-remote="true">
<span class="fas fa-comment" aria-hidden="true"></span>
<span id="comment-counter-<%= my_module.id %>" class="badge badge-indicator comments-badge-indicator <%= 'hidden' unless my_module.task_comments.count.positive? %>"
<span id="comment-counter-<%= my_module.id %>" class="badge badge-indicator comments-badge-indicator <%= 'hidden' unless my_module.task_comments_count.positive? %>"
data-linked-id="<%= my_module.id %>">
<%= my_module.task_comments.count %>
<%= my_module.task_comments_count %>
</span>
</a>
</li>
<% end %>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<% if can_read_experiment?(my_module.experiment) %>
<div role="tabpanel" class="tab-pane" id="<%= my_module.id %>_info" data-contents="info"></div>
<div role="tabpanel" class="tab-pane" id="<%= my_module.id %>_users" data-contents="users"></div>
<div role="tabpanel" class="tab-pane" id="<%= my_module.id %>_activities" data-contents="activities"></div>
<div role="tabpanel" class="tab-pane" id="<%= my_module.id %>_comments" data-contents="comments"></div>
<% end %>
</div>
</div>
</div>

View file

@ -22,7 +22,7 @@
<div class="panel-heading">
<h3 class="panel-title">
<%= link_to_if can_read_experiment?(my_module.experiment), my_module.name, protocols_my_module_path(my_module) %>
<%= link_to_if can_read_experiment?(my_module.experiment), my_module.name, protocols_my_module_path(my_module), title: my_module.name %>
</h3>
</div>
</div>

View file

@ -10,6 +10,6 @@
data-module-y="<%= my_module.y %>"
data-module-conns="<%= construct_module_connections(my_module) %>">
<span>
<%= link_to_if can_read_experiment?(my_module.experiment), my_module.name[0], protocols_my_module_path(my_module) %>
<%= link_to_if can_read_experiment?(my_module.experiment), my_module.name[0], protocols_my_module_path(my_module), title: my_module.name %>
</span>
</div>

View file

@ -3,48 +3,64 @@
data-id="<%= experiment.id %>">
<% if can_manage_experiment?(experiment) %>
<li><%= link_to t('experiments.edit.label_title'),
edit_experiment_url(experiment),
<li>
<%= link_to edit_experiment_url(experiment),
remote: true,
type: 'button',
data: { id: experiment.id },
class: 'edit-experiment' %></li>
class: 'edit-experiment' do %>
<i class="fas fa-pen"></i>
<span><%= t('experiments.edit.label_title') %></span>
<% end %>
</li>
<% end %>
<li data-hook="experiment-actions-second-child"></li>
<% if can_clone_experiment?(experiment) %>
<li><%= link_to t('experiments.clone.label_title'),
clone_modal_experiment_url(experiment),
<li>
<%= link_to clone_modal_experiment_url(experiment),
remote: true, type: 'button',
class: 'clone-experiment' %>
class: 'clone-experiment' do %>
<i class="fas fa-copy"></i>
<span><%= t('experiments.clone.label_title') %></span>
<% end %>
</li>
<% end %>
<% if can_move_experiment?(experiment) %>
<li><%= link_to t('experiments.move.label_title'),
move_modal_experiment_url(experiment),
<li>
<%= link_to move_modal_experiment_url(experiment),
remote: true, type: 'button',
class: 'move-experiment'%>
class: 'move-experiment' do %>
<i class="fas fa-arrow-right"></i>
<span><%= t('experiments.move.label_title') %></span>
<% end %>
</li>
<% end %>
<!-- Set or view user experiment assignments -->
<% if can_manage_experiment_users?(experiment) %>
<li>
<%= link_to edit_access_permissions_project_experiment_path(project, experiment), data: { action: 'remote-modal'} do %>
<i class="fas fa-door-open"></i>
<span><%= t('experiments.index.experiment_access') %></span>
<% end %>
</li>
<% else %>
<li>
<%= link_to access_permissions_project_experiment_path(project, experiment), data: { action: 'remote-modal'} do %>
<i class="fas fa-door-open"></i>
<span><%= t('experiments.index.experiment_access') %></span>
<% end %>
</li>
<% end %>
<% if can_archive_experiment?(experiment) %>
<li><%= link_to t('experiments.archive.label_title'),
archive_experiment_url(experiment),
<li>
<%= link_to archive_experiment_url(experiment),
type: 'button',
method: :post,
data: { confirm: t('experiments.canvas.archive_confirm') } %></li>
data: { confirm: t('experiments.canvas.archive_confirm') } do %>
<i class="fas fa-archive"></i>
<span><%= t('experiments.archive.label_title') %></span>
<% end %>
</li>
<% end %>
<% if can_restore_experiment?(experiment) %>
<% experiment_form = nil %>
@ -52,6 +68,12 @@
<% experiment_form = f %>
<%= f.hidden_field :archived, value: false %>
<% end %>
<li><a href="#" class="form-submit-link" data-turbolinks="false" data-submit-form="<%= experiment_form.options[:html][:id] %>"><%= t "projects.experiment_archive.restore_option" %></a></li>
<li><a href="#" class="form-submit-link" data-turbolinks="false" data-submit-form="<%= experiment_form.options[:html][:id] %>">
<i class="fas fa-undo"></i><%= t "projects.experiment_archive.restore_option" %></a></li>
<% end %>
<li class="form-dropdown-item">
<div class="form-dropdown-item-info">
<small><%= t('experiments.experiment_id') %>: <strong><%= experiment.code %></strong></small>
</div>
</li>
</ul>

View file

@ -21,6 +21,8 @@
<span class="fas fa-pencil-alt"></span>
<span class="hidden-xs"><%=t 'experiments.canvas.canvas_edit' %></span>
<% end %>
<% end %>
<% if can_manage_experiment?(@experiment) || can_clone_experiment?(@experiment) %>
<!-- experiment actions -->
<span class="dropdown actions-button">
<button class="btn btn-secondary dropdown-toggle" type="button" id="exActionsMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">

View file

@ -67,7 +67,6 @@
</div>
</div>
<% end %>
<% if can_manage_project_tags?(@my_module.experiment.project) && can_manage_my_module_tags?(@my_module) %>
<div class="pull-right create-new-tag-btn">
<%= bootstrap_form_for [@my_module.experiment.project, @new_tag], remote: true, format: :json, html: { class: 'add-tag-form' } do |f| %>
<%= hidden_field_tag :my_module_id, @my_module.id %>
@ -80,5 +79,4 @@
<% end %>
<% end %>
</div>
<% end %>
</div>

View file

@ -34,7 +34,7 @@
<div class="row mt-3">
<div class="col-sm-12">
<div class="stock-update-view">
<div class="stock-initial-container">
<div class="stock-initial-container <%= 'negative' if @stock_value.amount < 0 %>">
<span class="subtitle"><%= t('repository_stock_values.manage_modal.current_stock') %></span>
<span class="value"><%= @stock_value.formatted_value %></span>
<span class="units"><%= @stock_value.repository_stock_unit_item&.data %></span>
@ -42,7 +42,7 @@
<div class="stock-arrow">
<i class="fas fa-arrow-right"></i>
</div>
<div class="stock-final-container">
<div class="stock-final-container ">
<span class="subtitle"><%= t('repository_stock_values.manage_modal.new_stock') %></span>
<span class="value">-</span>
<span class="units"><%= @stock_value.repository_stock_unit_item&.data %></span>

View file

@ -1,4 +1,4 @@
<div class="modal" id="myModuleRepositoryFullViewModal" tabindex="-1" role="dialog">
<div class="modal" id="myModuleRepositoryFullViewModal" data-keyboard="false" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">

View file

@ -53,11 +53,11 @@
</span>
<span id="saveCancel" data-toggle="buttons" style="display:none">
<button type="button" class="btn btn-success" id="saveRecord" data-view-mode="active">
<button type="button" class="btn btn-success prevent-shrink" id="saveRecord" data-view-mode="active">
<span class="fas fa-save"></span>
<%= t("repositories.save_record") %>
</button>
<button type="button" class="btn btn-light" id="cancelSave" data-view-mode="active">
<button type="button" class="btn btn-light prevent-shrink" id="cancelSave" data-view-mode="active">
<span class="fas fa-times-circle"></span>
<%= t("repositories.cancel_save") %>
</button>

View file

@ -1,12 +1,12 @@
<div class="repository-cog dropdown hidden" id="saveRepositoryFilters">
<button type="button"
class="btn btn-default"
class="btn btn-light auto-shrink-button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true">
<span class="fas fa-save"></span>
<span class="hidden-xs"><%= t("repositories.show.filters.save_filters") %></span>
<span class="caret"></span>
<span class="hidden-xs button-text"><%= t("repositories.show.filters.save_filters") %></span>
<span class="caret button-text"></span>
</button>
<ul class="dropdown-menu">
<li id="newFilterLink">

View file

@ -82,7 +82,7 @@
<option data-create-url="<%= repository_repository_columns_stock_columns_path(@repository) %>"
data-edit-url="<%= repository_repository_columns_stock_column_path(@repository, @repository_column) unless @repository_column.new_record? %>"
value="RepositoryStockValue"
<% if @repository.repository_columns.where(data_type: 'RepositoryStockValue').any? %>
<% if @repository.repository_stock_column.present? %>
data-params="<%= {
optionClass: 'disabled-option',
text_description: 'Only one per inventory allowed'
@ -91,6 +91,12 @@
<%= 'selected' if @repository_column.repository_stock_value? %> >
<%= t('libraries.manange_modal_column.select.repository_stock_value') %>
</option>
<% elsif @repository_column.repository_stock_value? && @repository_column.persisted? %>
<option value="RepositoryTextValue" class="disabled-option"
data-params="<%= { optionClass: 'disabled-option' }.to_json %>"
selected>
<%= t('libraries.manange_modal_column.select.repository_stock_value') %>
</option>
<% end %>
<option data-delimiter=true></option>

View file

@ -54,10 +54,9 @@
<%= t('libraries.manange_modal_column.datetime_type.reminder_message') %>
</label>
<div class="col-sm-9">
<div class="sci-input-container">
<input type="text" class="sci-input-field reminder-message"
<div class="sci-input-container" data-error-text="<%= t('libraries.manange_modal_column.datetime_type.reminder_message_limit') %>">
<input id="date-reminder-message" type="text" class="sci-input-field reminder-message"
placeholder="<%= t('libraries.manange_modal_column.datetime_type.enter_reminder_message')%>"
maxlength="<%= Constants::NAME_MAX_LENGTH %>"
value="<%= column&.metadata['reminder_message'] %>">
</div>
</div>

View file

@ -54,10 +54,9 @@
<%= t('libraries.manange_modal_column.datetime_type.reminder_message') %>
</label>
<div class="col-sm-9">
<div class="sci-input-container">
<input type="text" class="sci-input-field reminder-message"
<div class="sci-input-container" data-error-text="<%= t('libraries.manange_modal_column.datetime_type.reminder_message_limit') %>">
<input id="datetime-reminder-message" type="text" class="sci-input-field reminder-message"
placeholder="<%= t('libraries.manange_modal_column.datetime_type.enter_reminder_message')%>"
maxlength="<%= Constants::NAME_MAX_LENGTH %>"
value="<%= column&.metadata['reminder_message'] %>">
</div>
</div>

View file

@ -64,13 +64,12 @@
<% if can_read_experiment?(@my_module.experiment) %>
<li id="steps-nav-tab" class="<%= "active" if is_module_protocols? %>">
<a href="<%= protocols_my_module_url(@my_module) %>" title="<%=t "nav2.modules.steps" %>">
<span class="hidden-xs"><%=t "nav2.modules.steps" %></span>
<span class="hidden-xs hidden-lg fas fa-arrow-circle-right"></span>
<%=t "nav2.modules.steps" %>
</a>
</li>
<li id="results-nav-tab" class="<%= "active" if is_module_results? %>">
<a href="<%= results_my_module_url(@my_module) %>" title="<%=t "nav2.modules.results" %>">
<span class="hidden-xs"><%=t "nav2.modules.results" %></span>
<%=t "nav2.modules.results" %>
<% if @my_module.results.size.positive? %>
<sup class="navigation-results-counter"><%= @my_module.results.size %></sup>
<% end %>
@ -79,8 +78,7 @@
</li>
<li id="activities-nav-tab" class="<%= "active" if is_module_activities? %>">
<a href="<%= activities_my_module_url(@my_module) %>" title="<%=t "nav2.modules.activities" %>">
<span class="hidden-xs"><%=t "nav2.modules.activities" %></span>
<span class="hidden-xs hidden-lg fas fa-list"></span>
<%=t "nav2.modules.activities" %>
</a>
</li>
<% end %>
@ -89,8 +87,7 @@
<% if can_read_experiment?(@my_module.experiment) && !@my_module.archived_branch? %>
<li id="archive-nav-tab" class="<%= "active" if is_module_archive? %>">
<a href="<%= archive_my_module_url(@my_module) %>" title="<%= t'nav2.modules.archive' %>">
<span class="hidden-xs"><%=t "nav2.modules.archive" %></span>
<span class="hidden-xs hidden-lg fas fa-briefcase"></span>
<%=t "nav2.modules.archive" %>
</a>
</li>
<% end %>

View file

@ -180,6 +180,8 @@ en:
creates_cycle: "mustn't create cycle"
errors:
general: "Something went wrong."
storage:
limit_reached: "Storage limit has been reached."
helpers:
label:
team:
@ -1069,7 +1071,7 @@ en:
open_mobile_app: "Open mobile app"
status_error:
general: "Status transition failed"
repository_snapshot: "Snapshot failed in %{repository}"
repository_snapshot: "Snapshot failed in %{repository} inventory"
experiments:
id: "ID"
experiment_id: "Experiment ID"
@ -1096,7 +1098,7 @@ en:
more: "more"
index:
edit_option: "Edit details"
clone_option: "Duplicate (as template)"
clone_option: "Duplicate as template"
move_option: "Move"
archive_option: "Archive"
archive_confirm: "Are you sure you want to archive this project?"
@ -1138,11 +1140,11 @@ en:
success_flash: "<strong>%{number}</strong> experiment(s) successfully restored."
error_flash: "Failed to restore experiment(s)."
clone:
modal_title: 'Copy experiment %{experiment} as template'
label_title: 'Copy as template'
modal_submit: 'Copy'
success_flash: 'Successfully copied experiment %{experiment} as template.'
error_flash: 'Could not copy the experiment as template.'
modal_title: 'Duplicate experiment %{experiment} as template'
label_title: 'Duplicate as template'
modal_submit: 'Duplicate'
success_flash: 'Successfully duplicated experiment %{experiment} as template.'
error_flash: 'Could not duplicate the experiment as template.'
current_project: '(current project)'
move:
modal_title: 'Move experiment %{experiment}'
@ -1731,6 +1733,7 @@ en:
amount_placeholder: "Enter amount"
amount_placeholder_new: "100, 2000, ..."
amount_error: "Enter an amount"
negative_error: "Negative values are not allowed."
unit: "Unit"
unit_prompt: "Select unit"
unit_error: "Select a unit"
@ -1813,6 +1816,7 @@ en:
reminder_message: 'Message (optional)'
enter_reminder_message: 'Enter a reminder message'
range_label: 'Range'
reminder_message_limit: "Maximum number of characters reached (255)"
stock_type:
multiple_options: 'selected'
all_options: 'All options'
@ -1904,7 +1908,7 @@ en:
stock_low_html: "Only <strong>%{stock_formated}</strong> left."
stock_empty: "There is no stock left."
date_reminder: "Date reminder."
date_expiration_html: "This item is expiring in <strong>%{date_expiration}</strong>"
date_expiration_html: "This item is expiring in <strong>%{date_expiration}</strong>."
item_expired: "This item has expired."
day: "day"
label_printers:
@ -2093,8 +2097,8 @@ en:
teams: "Teams"
webhooks: "Webhooks"
account_nav:
profile: "Profile"
preferences: "Preferences"
profile: "My profile"
preferences: "My preferences"
addons: "Add-ons"
connected_accounts: "Connected Accounts"
account:
@ -2706,6 +2710,8 @@ en:
my_module_comment_annotation_message_html: "Project: %{project} | Experiment: %{experiment} | Task: %{my_module}"
my_module_protocol_annotation_title: "%{user} mentioned you in a protocol description on task %{my_module}."
my_module_protocol_annotation_message_html: "Project: %{project} | Experiment: %{experiment} | Task: %{my_module}"
my_module_consumption_comment_annotation_title: "%{user} mentioned you in a comment on item consumption of item %{repository_item} from inventory %{repository}"
my_module_consumption_comment_annotation_message_html: "Project: %{project} | Experiment: %{experiment} | Task: %{my_module}"
step_comment_annotation_title: "%{user} mentioned you in a comment on step %{step}."
step_description_title: "%{user} mentioned you in a description on step %{step}."
checklist_title: "%{user} mentioned you in a checklist on step %{step}."
@ -3063,6 +3069,12 @@ en:
detail: "Text contains reference to nonexisting TinyMCE image"
result_wrong_tinymce:
detail: "Image reference not found in the text"
service:
errors:
missing_task_ids: "Missing task_ids"
callback_missing: "Missing callback"
url:
not_valid: "Not valid URL"
Add: "Add"

View file

@ -205,10 +205,10 @@ en:
register_molecule_on_step_html: "%{user} registered molecule %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}."
register_molecule_on_result_html: "%{user} registered molecule %{asset_name} on result %{result}."
register_molecule_on_step_in_repository_html: "%{user} registered molecule %{asset_name} on protocol %{protocol}'s step %{step_position} %{step}."
inventory_item_stock_set_html: "%{user} set total stock for %{repository_row} to %{new_amount} %{unit} in %{repository} \n%{comment}"
inventory_item_stock_add_html: "%{user} added %{change_amount} %{unit} of stock to a total of %{new_amount} %{unit} for %{repository_row} in %{repository} \n%{comment}"
inventory_item_stock_remove_html: "%{user} removed %{change_amount} %{unit} of stock to a total of %{new_amount} %{unit} for %{repository_row} in %{repository} \n%{comment}"
task_inventory_item_stock_consumed_html: "%{user} changed consumption of %{repository_row} from %{stock_consumption_was} %{unit} to %{stock_consumption} %{unit} in %{my_module} \n%{comment}"
inventory_item_stock_set_html: "%{user} set total stock for inventory item %{repository_row} to %{new_amount} %{unit} in inventory %{repository} \n%{comment}"
inventory_item_stock_add_html: "%{user} added %{change_amount} %{unit} of stock to a total %{new_amount} %{unit} for inventory item %{repository_row} in inventory %{repository} \n%{comment}"
inventory_item_stock_remove_html: "%{user} removed %{change_amount} %{unit} of stock to a total %{new_amount} %{unit} for inventory item %{repository_row} in inventory %{repository} \n%{comment}"
task_inventory_item_stock_consumed_html: "%{user} changed consumption of inventory item %{repository_row} from %{stock_consumption_was} %{unit} to %{stock_consumption} %{unit} on task %{my_module} \n%{comment}"
task_steps_rearranged_html: "%{user} rearranged protocol's steps on task %{my_module}"
task_step_content_rearranged_html: "%{user} rearranged content of protocol's step %{step_position} <strong>%{step}</strong> on task %{my_module}"
task_step_file_added_html: "%{user} added file <strong>%{file}</strong> to protocol's step %{step_position} <strong>%{step}</strong> on task %{my_module}"

View file

@ -419,6 +419,7 @@ Rails.application.routes.draw do
# AJAX popup accessed from full-zoom canvas for single module,
# as well as full activities view (HTML) for single module
get 'description'
get 'canvas_dropdown_menu'
get 'activities'
post 'activities'
get 'activities_tab' # Activities in tab view for single module
@ -713,6 +714,7 @@ Rails.application.routes.draw do
get 'health', to: 'api#health'
get 'status', to: 'api#status'
namespace :service do
post 'projects_json_export', to: 'projects_json_export#projects_json_export'
resources :teams, except: %i(index new create show edit update destroy) do
post 'clone_experiment' => 'experiments#clone'
end

View file

@ -24,10 +24,15 @@ class CreateLabelTemplates < ActiveRecord::Migration[6.1]
content:
<<~HEREDOC
^XA
^MTT
^MUD,300,300
^PR2
^MD30
^LH20,20
^PW310
^CF0,23
^FO0,0^FD{{item_id}}^FS
^FO0,7^BQN,2,4^FDMA\{{item_id}}^FS
^FO0,20^BQN,2,4^FDMA,{{item_id}}^FS
^FO95,30^FB180,4,0,L^FD{{item_name}}^FS^FS
^XZ
HEREDOC

View file

@ -64,6 +64,7 @@
"compression-webpack-plugin": "^1.1.11",
"croppie": "^2.6.4",
"css-loader": "2.1.1",
"decimal.js": "^10.3.1",
"extract-text-webpack-plugin": "^3.0.2",
"fabric": "1.6.7",
"file-loader": "^4.0.0",

View file

@ -3262,6 +3262,11 @@ decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decimal.js@^10.3.1:
version "10.3.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -4055,9 +4060,9 @@ events@^3.2.0:
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
eventsource@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
version "1.1.1"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.1.tgz#4544a35a57d7120fba4fa4c86cb4023b2c09df2f"
integrity sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==
dependencies:
original "^1.0.0"