Merge branch 'develop' into jg_sci_2228

This commit is contained in:
Urban Rotnik 2020-10-22 14:21:39 +02:00
commit aa1520bfb8
208 changed files with 5134 additions and 2855 deletions

View file

@ -1,6 +1,6 @@
ruby:
config_file: .rubocop.yml
version: 0.75.0
version: 0.83.0
eslint:
enabled: true

View file

@ -6,6 +6,7 @@ AllCops:
Exclude:
- "vendor/**/*"
- "db/schema.rb"
NewCops: enable
UseCache: false
TargetRubyVersion: 2.6

View file

@ -37,6 +37,7 @@ gem 'jsonapi-renderer', '~> 0.2.2'
gem 'jwt', '~> 1.5'
gem 'kaminari'
gem 'rack-attack'
gem 'rack-cors'
# JS datetime library, requirement of datetime picker
gem 'momentjs-rails', '~> 2.17.1'
@ -123,7 +124,7 @@ group :development, :test do
gem 'pry-rails'
gem 'rails-controller-testing'
gem 'rspec-rails', '>= 4.0.0.beta2'
gem 'rubocop', '>= 0.75.0', require: false
gem 'rubocop', '= 0.83.0', require: false
gem 'rubocop-performance'
gem 'rubocop-rails'
gem 'timecop'

View file

@ -17,7 +17,7 @@ GIT
GIT
remote: https://github.com/biosistemika/yomu
revision: 8845246f3e6a6cbc49b902cd4b908ba70553cbdd
revision: 063b855d672e9dd9de1e6e585b349a9b63e120c3
branch: master
specs:
yomu (0.2.4)
@ -110,7 +110,7 @@ GEM
ajax-datatables-rails (0.3.1)
railties (>= 3.1)
aspector (0.14.0)
ast (2.4.0)
ast (2.4.1)
auto_strip_attributes (2.5.0)
activerecord (>= 4.0)
autoprefixer-rails (9.7.0)
@ -287,7 +287,6 @@ GEM
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.13, < 3)
iniparse (1.4.4)
jaro_winkler (1.5.4)
jbuilder (2.9.1)
activesupport (>= 4.2.0)
jmespath (1.4.0)
@ -338,9 +337,9 @@ GEM
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (0.9.2)
mime-types (3.3)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0904)
mime-types-data (3.2020.0512)
mimemagic (0.3.5)
mini_magick (4.9.5)
mini_mime (1.0.2)
@ -387,9 +386,9 @@ GEM
overcommit (0.49.1)
childprocess (>= 0.6.3, < 2.0)
iniparse (~> 1.4)
parallel (1.19.1)
parser (2.6.5.0)
ast (~> 2.4.0)
parallel (1.19.2)
parser (2.7.1.4)
ast (~> 2.4.1)
pg (1.1.4)
pg_search (2.3.0)
activerecord (>= 4.2)
@ -410,6 +409,8 @@ GEM
rack (2.2.3)
rack-attack (6.1.0)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
@ -463,6 +464,7 @@ GEM
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.4)
rgl (0.5.6)
lazy_priority_queue (~> 0.1.0)
stream (~> 0.5.2)
@ -492,13 +494,13 @@ GEM
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.8.2)
rubocop (0.78.0)
jaro_winkler (~> 1.5.1)
rubocop (0.83.0)
parallel (~> 1.10)
parser (>= 2.6)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-performance (1.5.1)
rubocop (>= 0.71.0)
rubocop-rails (2.4.0)
@ -569,7 +571,7 @@ GEM
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
underscore-rails (1.8.3)
unicode-display_width (1.6.0)
unicode-display_width (1.7.0)
uniform_notifier (1.12.1)
warden (1.2.8)
rack (>= 2.0.6)
@ -666,6 +668,7 @@ DEPENDENCIES
pry-rails
puma
rack-attack
rack-cors
rails (~> 6.0.0)
rails-controller-testing
rails_12factor
@ -676,7 +679,7 @@ DEPENDENCIES
rotp
rqrcode
rspec-rails (>= 4.0.0.beta2)
rubocop (>= 0.75.0)
rubocop (= 0.83.0)
rubocop-performance
rubocop-rails
ruby-graphviz (~> 1.2)

View file

@ -1 +1 @@
1.19.6
1.20.1

View file

@ -1,31 +1,43 @@
/* global dropdownSelector I18n animateSpinner PerfectSb InfiniteScroll */
/* global dropdownSelector animateSpinner PerfectSb InfiniteScroll */
/* eslint-disable no-param-reassign */
var DasboardCurrentTasksWidget = (function() {
var sortFilter = '.curent-tasks-filters .sort-filter';
var viewFilter = '.curent-tasks-filters .view-filter';
var projectFilter = '.curent-tasks-filters .project-filter';
var experimentFilter = '.curent-tasks-filters .experiment-filter';
var sortFilter = '.current-tasks-filters .sort-filter';
var statusFilter = '.current-tasks-filters .view-filter';
var projectFilter = '.current-tasks-filters .project-filter';
var experimentFilter = '.current-tasks-filters .experiment-filter';
function generateTasksListHtml(json, container) {
$.each(json.data, (i, task) => {
var currentTaskItem = ` <a class="current-task-item" href="${task.link}">
<div class="current-task-breadcrumbs">${task.project}<span class="slash">/</span>${task.experiment}</div>
<div class="item-row">
<div class="task-name">${task.name}</div>
<div class="task-due-date ${task.state.class} ${task.due_date ? '' : 'hidden'}">
<i class="fas fa-calendar-day"></i> ${I18n.t('dashboard.current_tasks.due_date', { date: task.due_date })}
</div>
<div class="task-progress-container ${task.state.class}">
<div class="task-progress" style="padding-left: ${task.steps_precentage}%"></div>
<div class="task-progress-label">${task.state.text}</div>
</div>
<div class="task-name row-border">${task.name}</div>
<div class="task-due-date row-border ${task.due_date.state}">
<span class="${task.due_date.text ? '' : 'hidden'}">
<i class="fas fa-calendar-day"></i> ${task.due_date.text}
</span>
</div>
<div class="task-status-container row-border">
<span class="task-status" style="background:${task.status_color}">${task.status_name}</span>
</div>
</a>`;
$(container).append(currentTaskItem);
});
}
function getDefaultStatusValues() {
// Select uncompleted status values
var values = [];
$(statusFilter).find('option').each(function(_, option) {
if ($(option).data('completionConsequence')) {
return false;
}
values.push(option.value);
return this;
});
return values;
}
function initInfiniteScroll() {
InfiniteScroll.init('.current-tasks-list', {
url: $('.current-tasks-list').data('tasksListUrl'),
@ -36,7 +48,7 @@ var DasboardCurrentTasksWidget = (function() {
params.project_id = dropdownSelector.getValues(projectFilter);
params.experiment_id = dropdownSelector.getValues(experimentFilter);
params.sort = dropdownSelector.getValues(sortFilter);
params.view = dropdownSelector.getValues(viewFilter);
params.statuses = dropdownSelector.getValues(statusFilter);
params.query = $('.current-tasks-widget .task-search-field').val();
params.mode = $('.current-tasks-navbar .active').data('mode');
return params;
@ -47,8 +59,53 @@ var DasboardCurrentTasksWidget = (function() {
function filtersEnabled() {
return dropdownSelector.getValues(experimentFilter)
|| dropdownSelector.getValues(projectFilter)
|| $('.current-tasks-widget .task-search-field').val().length > 0
|| dropdownSelector.getValues(viewFilter) !== 'uncompleted';
|| $('.current-tasks-widget .task-search-field').val().length > 0;
}
function filterStateSave() {
var teamId = $('.current-tasks-filters').data('team-id');
var filterState = {
sort: dropdownSelector.getValues(sortFilter),
statuses: dropdownSelector.getValues(statusFilter),
project_id: dropdownSelector.getData(projectFilter),
experiment_id: dropdownSelector.getData(experimentFilter),
mode: $('.current-tasks-navbar .active').data('mode')
};
if (filterState) {
localStorage.setItem('current_tasks_filters_per_team/' + teamId, JSON.stringify(filterState));
}
}
function filterStateLoad() {
var teamId = $('.current-tasks-filters').data('team-id');
var filterState = localStorage.getItem('current_tasks_filters_per_team/' + teamId);
var parsedFilterState;
var allStatusValues = $.map($(statusFilter).find('option'), function(option) {
return option.value;
});
if (filterState) {
try {
parsedFilterState = JSON.parse(filterState);
dropdownSelector.selectValues(sortFilter, parsedFilterState.sort);
// Check if saved statuses are valid
if (parsedFilterState.statuses.every(status => allStatusValues.includes(status))) {
dropdownSelector.selectValues(statusFilter, parsedFilterState.statuses);
} else {
dropdownSelector.selectValues(statusFilter, getDefaultStatusValues());
}
dropdownSelector.setData(projectFilter, parsedFilterState.project_id);
dropdownSelector.setData(experimentFilter, parsedFilterState.experiment_id);
// Select saved navbar state
$('.current-tasks-navbar .navbar-link').removeClass('active');
$('.current-tasks-navbar').find(`[data-mode='${parsedFilterState.mode}']`).addClass('active');
} catch (e) {
dropdownSelector.selectValues(statusFilter, getDefaultStatusValues());
}
} else {
dropdownSelector.selectValues(statusFilter, getDefaultStatusValues());
}
}
function loadCurrentTasksList(newList) {
@ -57,7 +114,7 @@ var DasboardCurrentTasksWidget = (function() {
project_id: dropdownSelector.getValues(projectFilter),
experiment_id: dropdownSelector.getValues(experimentFilter),
sort: dropdownSelector.getValues(sortFilter),
view: dropdownSelector.getValues(viewFilter),
statuses: dropdownSelector.getValues(statusFilter),
query: $('.current-tasks-widget .task-search-field').val(),
mode: $('.current-tasks-navbar .active').data('mode')
};
@ -81,11 +138,12 @@ var DasboardCurrentTasksWidget = (function() {
}
function initFilters() {
$('.curent-tasks-filters .clear-button').click((e) => {
$('.current-tasks-filters .clear-button').click((e) => {
e.stopPropagation();
e.preventDefault();
dropdownSelector.selectValue(sortFilter, 'due_date');
dropdownSelector.selectValue(viewFilter, 'uncompleted');
dropdownSelector.selectValues(sortFilter, 'due_date');
dropdownSelector.selectValues(statusFilter, getDefaultStatusValues());
dropdownSelector.clearData(projectFilter);
dropdownSelector.clearData(experimentFilter);
});
@ -98,12 +156,9 @@ var DasboardCurrentTasksWidget = (function() {
disableSearch: true
});
dropdownSelector.init(viewFilter, {
noEmptyOption: true,
singleSelect: true,
closeOnSelect: true,
dropdownSelector.init(statusFilter, {
selectAppearance: 'simple',
disableSearch: true
optionClass: 'checkbox-icon'
});
dropdownSelector.init(projectFilter, {
@ -138,25 +193,27 @@ var DasboardCurrentTasksWidget = (function() {
}
});
$('.curent-tasks-filters').click((e) => {
$('.current-tasks-filters').click((e) => {
// Prevent filter window close
e.stopPropagation();
e.preventDefault();
dropdownSelector.closeDropdown(sortFilter);
dropdownSelector.closeDropdown(viewFilter);
dropdownSelector.closeDropdown(statusFilter);
dropdownSelector.closeDropdown(projectFilter);
dropdownSelector.closeDropdown(experimentFilter);
});
$('.curent-tasks-filters .apply-filters').click((e) => {
$('.curent-tasks-filters').dropdown('toggle');
$('.current-tasks-filters .apply-filters').click((e) => {
$('.current-tasks-filters').dropdown('toggle');
e.stopPropagation();
e.preventDefault();
loadCurrentTasksList(true);
filterStateSave();
});
$('.filter-container').on('hide.bs.dropdown', () => {
loadCurrentTasksList(true);
filterStateSave();
$('.current-tasks-list').removeClass('disabled');
});
@ -170,6 +227,7 @@ var DasboardCurrentTasksWidget = (function() {
$(this).parent().find('.navbar-link').removeClass('active');
$(this).addClass('active');
loadCurrentTasksList(true);
filterStateSave();
});
}
@ -179,13 +237,13 @@ var DasboardCurrentTasksWidget = (function() {
});
}
return {
init: () => {
if ($('.current-tasks-widget').length) {
initNavbar();
initFilters();
initSearch();
filterStateLoad();
loadCurrentTasksList();
initInfiniteScroll();
}

View file

@ -14,16 +14,6 @@
});
}
function initExpandCollapseButton() {
$('.ga-activities-list').on('hidden.bs.collapse', function(ev) {
$(ev.target.dataset.buttonLink)
.find('.fas').removeClass('fa-chevron-down').addClass('fa-chevron-right');
});
$('.ga-activities-list').on('shown.bs.collapse', function(ev) {
$(ev.target.dataset.buttonLink)
.find('.fas').removeClass('fa-chevron-right').addClass('fa-chevron-down');
});
}
function initShowMoreButton() {
var moreButton = $('.btn-more-activities');
moreButton.on('click', function(ev) {
@ -70,6 +60,5 @@
}
initExpandCollapseAllButtons();
initExpandCollapseButton();
initShowMoreButton();
}());

View file

@ -1,388 +1,425 @@
/* global I18n dropdownSelector */
/* global I18n dropdownSelector HelperModule animateSpinner */
/* eslint-disable no-use-before-define */
(function() {
const STATUS_POLLING_INTERVAL = 5000;
function initTaskCollapseState() {
let taskView = '.my-modules-protocols-index';
let taskSection = '.task-section-caret';
let taskId = $(taskView).data('task-id');
function initTaskCollapseState() {
let taskView = '.my-modules-protocols-index';
let taskSection = '.task-section-caret';
let taskId = $(taskView).data('task-id');
function collapseStateSave() {
$(taskView).on('click', taskSection, function() {
let collapsed = $(this).attr('aria-expanded');
let taskSectionType = $(this).attr('aria-controls');
function collapseStateSave() {
$(taskView).on('click', taskSection, function() {
let collapsed = $(this).attr('aria-expanded');
let taskSectionType = $(this).attr('aria-controls');
if (collapsed === 'true') {
localStorage.setItem('task_section_collapsed/' + taskId + '/' + taskSectionType, collapsed);
} else {
localStorage.removeItem('task_section_collapsed/' + taskId + '/' + taskSectionType);
}
});
}
function collapseStateLoad() {
$(taskSection).each(function() {
let taskSectionType = $(this).attr('aria-controls');
var collapsed = localStorage.getItem('task_section_collapsed/' + taskId + '/' + taskSectionType);
if (JSON.parse(collapsed)) {
$('#' + taskSectionType).collapse('hide');
}
$(this).closest('.task-section').removeClass('hidden');
});
}
collapseStateSave();
collapseStateLoad();
}
function updateStartDate() {
let updateUrl = $('#startDateContainer').data('update-url');
let val = $('#calendarStartDate').val();
$.ajax({
url: updateUrl,
type: 'PATCH',
dataType: 'json',
data: { my_module: { started_on: val } },
success: function(result) {
$('#startDateLabelContainer').html(result.start_date_label);
}
});
}
// Bind ajax for editing due dates
function initStartDatePicker() {
$('#calendarStartDate').on('dp.change', function() {
updateStartDate();
});
}
function updateDueDate() {
let updateUrl = $('#dueDateContainer').data('update-url');
let val = $('#calendarDueDate').val();
$.ajax({
url: updateUrl,
type: 'PATCH',
dataType: 'json',
data: { my_module: { due_date: val } },
success: function(result) {
$('#dueDateLabelContainer').html(result.due_date_label);
}
});
}
// Bind ajax for editing due dates
function initDueDatePicker() {
$('#calendarDueDate').on('dp.change', function() {
updateDueDate();
});
}
// Bind ajax for editing tags
function bindEditTagsAjax() {
var manageTagsModal = null;
var manageTagsModalBody = null;
// Initialize reloading of manage tags modal content after posting new
// tag.
function initAddTagForm() {
manageTagsModalBody.find('.add-tag-form')
.submit(function() {
var selectOptions = manageTagsModalBody.find('#new_my_module_tag .dropdown-menu li').length;
if (selectOptions === 0 && this.id === 'new_my_module_tag') return false;
return true;
})
.on('ajax:success', function(e, data) {
var newTag;
initTagsModalBody(data);
newTag = $('#manage-module-tags-modal .list-group-item').last();
dropdownSelector.addValue('#module-tags-selector', {
value: newTag.data('tag-id'),
label: newTag.data('name'),
params: {
color: newTag.data('color')
}
}, true);
});
}
// Initialize edit tag & remove tag functionality from my_module links.
function initTagRowLinks() {
manageTagsModalBody.find('.edit-tag-link')
.on('click', function() {
var $this = $(this);
var li = $this.parents('li.list-group-item');
var editDiv = $(li.find('div.tag-edit'));
// Revert all rows to their original states
manageTagsModalBody.find('li.list-group-item').each(function() {
var li2 = $(this);
li2.css('background-color', li2.data('color'));
li2.find('.edit-tag-form').clearFormErrors();
li2.find('input[type=text]').val(li2.data('name'));
});
// Hide all other edit divs, show all show divs
manageTagsModalBody.find('div.tag-edit').hide();
manageTagsModalBody.find('div.tag-show').show();
editDiv.find('input[type=text]').val(li.data('name'));
editDiv.find('.edit-tag-color').colorselector('setColor', li.data('color'));
li.find('div.tag-show').hide();
editDiv.show();
});
manageTagsModalBody.find('div.tag-edit .dropdown-colorselector > .dropdown-menu li a')
.on('click', function() {
// Change background of the <li>
var $this = $(this);
var li = $this.parents('li.list-group-item');
li.css('background-color', $this.data('value'));
});
manageTagsModalBody.find('.remove-tag-link')
.on('ajax:success', function(e, data) {
dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true);
initTagsModalBody(data);
});
manageTagsModalBody.find('.delete-tag-form')
.on('ajax:success', function(e, data) {
dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true);
initTagsModalBody(data);
});
manageTagsModalBody.find('.edit-tag-form')
.on('ajax:success', function(e, data) {
var newTag;
initTagsModalBody(data);
dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true);
newTag = $('#manage-module-tags-modal .list-group-item[data-tag-id=' + this.dataset.tagId + ']');
dropdownSelector.addValue('#module-tags-selector', {
value: newTag.data('tag-id'),
label: newTag.data('name'),
params: {
color: newTag.data('color')
}
}, true);
})
.on('ajax:error', function(e, data) {
$(this).renderFormErrors('tag', data.responseJSON);
});
manageTagsModalBody.find('.cancel-tag-link')
.on('click', function() {
var $this = $(this);
var li = $this.parents('li.list-group-item');
li.css('background-color', li.data('color'));
li.find('.edit-tag-form').clearFormErrors();
li.find('div.tag-edit').hide();
li.find('div.tag-show').show();
});
}
// Initialize ajax listeners and elements style on modal body. This
// function must be called when modal body is changed.
function initTagsModalBody(data) {
manageTagsModalBody.html(data.html);
manageTagsModalBody.find('.selectpicker').selectpicker();
initAddTagForm();
initTagRowLinks();
}
manageTagsModal = $('#manage-module-tags-modal');
manageTagsModalBody = manageTagsModal.find('.modal-body');
// Reload tags HTML element when modal is closed
manageTagsModal.on('hide.bs.modal', function() {
var tagsEl = $('#module-tags');
// Load HTML
$.ajax({
url: tagsEl.attr('data-module-tags-url'),
type: 'GET',
dataType: 'json',
success: function(data) {
var newOptions = $(data.html_module_header).find('option');
$('#module-tags-selector').find('option').remove();
$(newOptions).appendTo('#module-tags-selector').change();
},
error: function() {
// TODO
}
});
});
// Remove modal content when modal window is closed.
manageTagsModal.on('hidden.bs.modal', function() {
manageTagsModalBody.html('');
});
// initialize my_module tab remote loading
$('.edit-tags-link')
.on('ajax:before', function() {
manageTagsModal.modal('show');
})
.on('ajax:success', function(e, data) {
$('#manage-module-tags-modal-module').text(data.my_module.name);
initTagsModalBody(data);
});
}
// Sets callback for completing/uncompleting task
function applyTaskCompletedCallBack() {
$("[data-action='complete-task'], [data-action='uncomplete-task']")
.on('click', function() {
var button = $(this);
$.ajax({
url: button.data('link-url'),
type: 'POST',
dataType: 'json',
success: function(data) {
if (data.completed === true) {
button.attr('data-action', 'uncomplete-task');
button.find('.btn')
.removeClass('btn-primary').addClass('btn-default');
} else {
button.attr('data-action', 'complete-task');
button.find('.btn')
.removeClass('btn-default').addClass('btn-primary');
}
$('#dueDateContainer').html(data.module_header_due_date);
initDueDatePicker();
$('.task-state-label').html(data.module_state_label);
button.find('button').replaceWith(data.new_btn);
},
error: function() {
if (collapsed === 'true') {
localStorage.setItem('task_section_collapsed/' + taskId + '/' + taskSectionType, collapsed);
} else {
localStorage.removeItem('task_section_collapsed/' + taskId + '/' + taskSectionType);
}
});
});
}
}
function initTagsSelector() {
var myModuleTagsSelector = '#module-tags-selector';
function collapseStateLoad() {
$(taskSection).each(function() {
let taskSectionType = $(this).attr('aria-controls');
var collapsed = localStorage.getItem('task_section_collapsed/' + taskId + '/' + taskSectionType);
dropdownSelector.init(myModuleTagsSelector, {
closeOnSelect: true,
tagClass: 'my-module-white-tags',
tagStyle: (data) => {
return `background: ${data.params.color}`;
},
customDropdownIcon: () => {
return '';
},
optionLabel: (data) => {
if (data.value > 0) {
return `<span class="my-module-tags-color" style="background:${data.params.color}"></span>
${data.label}`;
if (JSON.parse(collapsed)) {
$('#' + taskSectionType).collapse('hide');
}
$(this).closest('.task-section').removeClass('hidden');
});
}
collapseStateSave();
collapseStateLoad();
}
function updateStartDate() {
let updateUrl = $('#startDateContainer').data('update-url');
let val = $('#calendarStartDate').val();
$.ajax({
url: updateUrl,
type: 'PATCH',
dataType: 'json',
data: { my_module: { started_on: val } },
success: function(result) {
$('#startDateLabelContainer').html(result.start_date_label);
},
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
}
return `<span class="my-module-tags-color"></span>
${data.label + ' '}
<span class="my-module-tags-create-new"> (${I18n.t('my_modules.details.create_new_tag')})</span>`;
},
onOpen: function() {
$('.select-container .edit-button-container').removeClass('hidden');
},
onClose: function() {
$('.select-container .edit-button-container').addClass('hidden');
},
onSelect: function() {
var selectElement = $(myModuleTagsSelector);
var lastTag = selectElement.next().find('.ds-tags').last();
var lastTagId = lastTag.find('.tag-label').data('ds-tag-id');
var newTag;
});
}
if (lastTagId > 0) {
newTag = { my_module_tag: { tag_id: lastTagId } };
$.post(selectElement.data('update-module-tags-url'), newTag)
.fail(function() {
dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true);
});
} else {
newTag = {
tag: {
name: lastTag.find('.tag-label').html(),
project_id: selectElement.data('project-id'),
color: null
},
my_module_id: selectElement.data('module-id'),
simple_creation: true
};
$.post(selectElement.data('tags-create-url'), newTag, function(result) {
dropdownSelector.removeValue(myModuleTagsSelector, 0, '', true);
dropdownSelector.addValue(myModuleTagsSelector, {
value: result.tag.id,
label: result.tag.name,
// Bind ajax for editing due dates
function initStartDatePicker() {
$('#calendarStartDate').on('dp.change', function() {
updateStartDate();
});
}
function updateDueDate() {
let updateUrl = $('#dueDateContainer').data('update-url');
let val = $('#calendarDueDate').val();
$.ajax({
url: updateUrl,
type: 'PATCH',
dataType: 'json',
data: { my_module: { due_date: val } },
success: function(result) {
$('#dueDateLabelContainer').html(result.due_date_label);
},
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
}
});
}
// Bind ajax for editing due dates
function initDueDatePicker() {
$('#calendarDueDate').on('dp.change', function() {
updateDueDate();
});
}
// Bind ajax for editing tags
function bindEditTagsAjax() {
var manageTagsModal = null;
var manageTagsModalBody = null;
// Initialize reloading of manage tags modal content after posting new
// tag.
function initAddTagForm() {
manageTagsModalBody.find('.add-tag-form')
.submit(function() {
var selectOptions = manageTagsModalBody.find('#new_my_module_tag .dropdown-menu li').length;
if (selectOptions === 0 && this.id === 'new_my_module_tag') return false;
return true;
})
.on('ajax:success', function(e, data) {
var newTag;
initTagsModalBody(data);
newTag = $('#manage-module-tags-modal .list-group-item').last();
dropdownSelector.addValue('#module-tags-selector', {
value: newTag.data('tag-id'),
label: newTag.data('name'),
params: {
color: result.tag.color
color: newTag.data('color')
}
}, true);
});
}
},
onUnSelect: (id) => {
$.post(`${$(myModuleTagsSelector).data('update-module-tags-url')}/${id}/destroy_by_tag_id`);
dropdownSelector.closeDropdown(myModuleTagsSelector);
}
}).getContainer(myModuleTagsSelector).addClass('my-module-tags-container');
}
function initAssignedUsersSelector() {
var manageUsersModal = $('#manage-module-users-modal');
var manageUsersModalBody = manageUsersModal.find('.modal-body');
// Initialize edit tag & remove tag functionality from my_module links.
function initTagRowLinks() {
manageTagsModalBody.find('.edit-tag-link')
.on('click', function() {
var $this = $(this);
var li = $this.parents('li.list-group-item');
var editDiv = $(li.find('div.tag-edit'));
// Initialize users editing modal remote loading
function initUsersEditLink() {
$('.task-details').on('ajax:success', '.manage-users-link', function(e, data) {
manageUsersModal.modal('show');
manageUsersModal.find('#manage-module-users-modal-module').text(data.my_module.name);
// Revert all rows to their original states
manageTagsModalBody.find('li.list-group-item').each(function() {
var li2 = $(this);
li2.css('background-color', li2.data('color'));
li2.find('.edit-tag-form').clearFormErrors();
li2.find('input[type=text]').val(li2.data('name'));
});
// Hide all other edit divs, show all show divs
manageTagsModalBody.find('div.tag-edit').hide();
manageTagsModalBody.find('div.tag-show').show();
editDiv.find('input[type=text]').val(li.data('name'));
editDiv.find('.edit-tag-color').colorselector('setColor', li.data('color'));
li.find('div.tag-show').hide();
editDiv.show();
});
manageTagsModalBody.find('div.tag-edit .dropdown-colorselector > .dropdown-menu li a')
.on('click', function() {
// Change background of the <li>
var $this = $(this);
var li = $this.parents('li.list-group-item');
li.css('background-color', $this.data('value'));
});
manageTagsModalBody.find('.remove-tag-link')
.on('ajax:success', function(e, data) {
dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true);
initTagsModalBody(data);
});
manageTagsModalBody.find('.delete-tag-form')
.on('ajax:success', function(e, data) {
dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true);
initTagsModalBody(data);
});
manageTagsModalBody.find('.edit-tag-form')
.on('ajax:success', function(e, data) {
var newTag;
initTagsModalBody(data);
dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true);
newTag = $('#manage-module-tags-modal .list-group-item[data-tag-id=' + this.dataset.tagId + ']');
dropdownSelector.addValue('#module-tags-selector', {
value: newTag.data('tag-id'),
label: newTag.data('name'),
params: {
color: newTag.data('color')
}
}, true);
})
.on('ajax:error', function(e, data) {
$(this).renderFormErrors('tag', data.responseJSON);
});
manageTagsModalBody.find('.cancel-tag-link')
.on('click', function() {
var $this = $(this);
var li = $this.parents('li.list-group-item');
li.css('background-color', li.data('color'));
li.find('.edit-tag-form').clearFormErrors();
li.find('div.tag-edit').hide();
li.find('div.tag-show').show();
});
}
// Initialize ajax listeners and elements style on modal body. This
// function must be called when modal body is changed.
function initTagsModalBody(data) {
manageTagsModalBody.html(data.html);
manageTagsModalBody.find('.selectpicker').selectpicker();
initAddTagForm();
initTagRowLinks();
}
manageTagsModal = $('#manage-module-tags-modal');
manageTagsModalBody = manageTagsModal.find('.modal-body');
// Reload tags HTML element when modal is closed
manageTagsModal.on('hide.bs.modal', function() {
var tagsEl = $('#module-tags');
// Load HTML
$.ajax({
url: tagsEl.attr('data-module-tags-url'),
type: 'GET',
dataType: 'json',
success: function(data) {
var newOptions = $(data.html_module_header).find('option');
$('#module-tags-selector').find('option').remove();
$(newOptions).appendTo('#module-tags-selector').change();
},
error: function() {
// TODO
}
});
});
// Remove modal content when modal window is closed.
manageTagsModal.on('hidden.bs.modal', function() {
manageTagsModalBody.html('');
});
// initialize my_module tab remote loading
$('.edit-tags-link')
.on('ajax:before', function() {
manageTagsModal.modal('show');
})
.on('ajax:success', function(e, data) {
$('#manage-module-tags-modal-module').text(data.my_module.name);
initTagsModalBody(data);
});
}
function checkStatusState() {
$.getJSON($('.status-flow-dropdown').data('status-check-url'), (statusData) => {
if (statusData.status_changing) {
setTimeout(() => { checkStatusState(); }, STATUS_POLLING_INTERVAL);
} else {
location.reload();
}
});
}
function applyTaskStatusChangeCallBack() {
if ($('.status-flow-dropdown').data('status-changing')) {
setTimeout(() => { checkStatusState(); }, STATUS_POLLING_INTERVAL);
}
$('.task-flows').on('click', '#dropdownTaskFlowList > li[data-state-id]', function() {
var list = $('#dropdownTaskFlowList');
var item = $(this);
animateSpinner();
$.ajax({
url: list.data('link-url'),
beforeSend: function(e, ajaxSettings) {
if (item.data('beforeSend') instanceof Function) {
return item.data('beforeSend')(item, ajaxSettings)
}
return true
},
type: 'PATCH',
data: { my_module: { status_id: item.data('state-id') } },
error: function(e) {
animateSpinner(null, false);
if (e.status === 403) {
HelperModule.flashAlertMsg(I18n.t('my_module_statuses.update_status.error.no_permission'), 'danger');
} else if (e.status === 422) {
HelperModule.flashAlertMsg(e.responseJSON.errors, 'danger');
} else {
HelperModule.flashAlertMsg('error', 'danger');
}
}
});
});
}
function initTagsSelector() {
var myModuleTagsSelector = '#module-tags-selector';
dropdownSelector.init(myModuleTagsSelector, {
closeOnSelect: true,
tagClass: 'my-module-white-tags',
tagStyle: (data) => {
return `background: ${data.params.color}`;
},
customDropdownIcon: () => {
return '';
},
optionLabel: (data) => {
if (data.value > 0) {
return `<span class="my-module-tags-color" style="background:${data.params.color}"></span>
${data.label}`;
}
return `<span class="my-module-tags-color"></span>
${data.label + ' '}
<span class="my-module-tags-create-new"> (${I18n.t('my_modules.details.create_new_tag')})</span>`;
},
onOpen: function() {
$('.select-container .edit-button-container').removeClass('hidden');
},
onClose: function() {
$('.select-container .edit-button-container').addClass('hidden');
},
onSelect: function() {
var selectElement = $(myModuleTagsSelector);
var lastTag = selectElement.next().find('.ds-tags').last();
var lastTagId = lastTag.find('.tag-label').data('ds-tag-id');
var newTag;
if (lastTagId > 0) {
newTag = { my_module_tag: { tag_id: lastTagId } };
$.post(selectElement.data('update-module-tags-url'), newTag)
.fail(function(response) {
dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true);
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
});
} else {
newTag = {
tag: {
name: lastTag.find('.tag-label').html(),
project_id: selectElement.data('project-id'),
color: null
},
my_module_id: selectElement.data('module-id'),
simple_creation: true
};
$.post(selectElement.data('tags-create-url'), newTag, function(result) {
dropdownSelector.removeValue(myModuleTagsSelector, 0, '', true);
dropdownSelector.addValue(myModuleTagsSelector, {
value: result.tag.id,
label: result.tag.name,
params: {
color: result.tag.color
}
}, true);
}).fail(function() {
dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true);
});
}
},
onUnSelect: (id) => {
$.post(`${$(myModuleTagsSelector).data('update-module-tags-url')}/${id}/destroy_by_tag_id`)
.success(function() {
dropdownSelector.closeDropdown(myModuleTagsSelector);
})
.fail(function(r) {
if (r.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
});
}
}).getContainer(myModuleTagsSelector).addClass('my-module-tags-container');
}
function initAssignedUsersSelector() {
var manageUsersModal = $('#manage-module-users-modal');
var manageUsersModalBody = manageUsersModal.find('.modal-body');
// Initialize users editing modal remote loading
function initUsersEditLink() {
$('.task-details').on('ajax:success', '.manage-users-link', function(e, data) {
manageUsersModal.modal('show');
manageUsersModal.find('#manage-module-users-modal-module').text(data.my_module.name);
initUsersModalBody(data);
});
}
// Initialize ajax listeners and elements style on modal body.
// This function must be called when modal body is changed.
function initUsersModalBody(data) {
manageUsersModalBody.html(data.html);
manageUsersModalBody.find('.selectpicker').selectpicker();
}
// Initialize reloading manage user modal content after posting new user
manageUsersModalBody.on('ajax:success', '.add-user-form', function(e, data) {
initUsersModalBody(data);
});
}
// Initialize ajax listeners and elements style on modal body.
// This function must be called when modal body is changed.
function initUsersModalBody(data) {
manageUsersModalBody.html(data.html);
manageUsersModalBody.find('.selectpicker').selectpicker();
}
// Initialize reloading manage user modal content after posting new user
manageUsersModalBody.on('ajax:success', '.add-user-form', function(e, data) {
initUsersModalBody(data);
});
// Initialize remove user from my_module links
manageUsersModalBody.on('ajax:success', '.remove-user-link', function(e, data) {
initUsersModalBody(data);
});
// Reload users HTML element when modal is closed
manageUsersModal.on('hide.bs.modal', function() {
var usersEl = $('.task-assigned-users');
// Load HTML to refresh users
$.ajax({
url: usersEl.attr('data-module-users-url'),
type: 'GET',
dataType: 'json',
success: function(data) {
$('.task-assigned-users').replaceWith(data.html);
},
error: function() {
// TODO
}
// Initialize remove user from my_module links
manageUsersModalBody.on('ajax:success', '.remove-user-link', function(e, data) {
initUsersModalBody(data);
});
});
// Remove users modal content when modal window is closed.
manageUsersModal.on('hidden.bs.modal', function() {
manageUsersModalBody.html('');
});
// Reload users HTML element when modal is closed
manageUsersModal.on('hide.bs.modal', function() {
var usersEl = $('.task-assigned-users');
// Load HTML to refresh users
$.ajax({
url: usersEl.attr('data-module-users-url'),
type: 'GET',
dataType: 'json',
success: function(data) {
$('.task-assigned-users').replaceWith(data.html);
},
error: function() {
// TODO
}
});
});
initUsersEditLink();
}
// Remove users modal content when modal window is closed.
manageUsersModal.on('hidden.bs.modal', function() {
manageUsersModalBody.html('');
});
initTaskCollapseState();
applyTaskCompletedCallBack();
initTagsSelector();
bindEditTagsAjax();
initStartDatePicker();
initDueDatePicker();
initAssignedUsersSelector();
initUsersEditLink();
}
initTaskCollapseState();
applyTaskStatusChangeCallBack();
initTagsSelector();
bindEditTagsAjax();
initStartDatePicker();
initDueDatePicker();
initAssignedUsersSelector();
}());

View file

@ -11,16 +11,24 @@ var selectedRow = null;
function initEditMyModuleDescription() {
var viewObject = $('#my_module_description_view');
viewObject.on('click', function() {
viewObject.on('click', function(e) {
if ($(e.target).hasClass('record-info-link')) return;
TinyMCE.init('#my_module_description_textarea');
}).on('click', 'a', function(e) {
if ($(this).hasClass('record-info-link')) return;
e.stopPropagation();
});
TinyMCE.initIfHasDraft(viewObject);
}
function initEditProtocolDescription() {
var viewObject = $('#protocol_description_view');
viewObject.on('click', function() {
viewObject.on('click', function(e) {
if ($(e.target).hasClass('record-info-link')) return;
TinyMCE.init('#protocol_description_textarea', refreshProtocolStatusBar);
}).on('click', 'a', function(e) {
if ($(this).hasClass('record-info-link')) return;
e.stopPropagation();
});
TinyMCE.initIfHasDraft(viewObject);
}
@ -361,11 +369,13 @@ function loadFromRepository() {
// Simply reload page
location.reload();
},
error: function(ev) {
// Display error message in alert()
alert(ev.responseJSON.message);
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
} else {
alert(response.responseJSON.message);
}
// Hide modal
modal.modal('hide');
}
});

View file

@ -453,7 +453,8 @@ var MyModuleRepositories = (function() {
FULL_VIEW_MODAL.on('show.bs.modal', function() {
FULL_VIEW_MODAL.find('.table-container').empty();
FULL_VIEW_MODAL.find('.repository-name').empty();
FULL_VIEW_MODAL.find('.repository-title').empty();
FULL_VIEW_MODAL.find('.repository-version').empty();
updateFullViewRowsCount('');
});
}
@ -518,29 +519,31 @@ var MyModuleRepositories = (function() {
function updateFullViewRowsCount(value) {
FULL_VIEW_MODAL.data('rows-count', value);
FULL_VIEW_MODAL.find('.repository-name').attr('data-rows-count', value);
FULL_VIEW_MODAL.find('.repository-version').attr('data-rows-count', value);
}
function renderFullViewRepositoryName(name, snapshotDate, assignMode) {
var title;
var repositoryName = name || FULL_VIEW_MODAL.find('.repository-name').data('repository-name');
var version;
var repositoryName = name || FULL_VIEW_MODAL.find('.repository-title').data('repository-name');
if (assignMode) {
title = I18n.t('my_modules.repository.full_view.assign_modal_header', {
repository_name: repositoryName
});
version = '';
} else if (snapshotDate) {
title = I18n.t('my_modules.repository.full_view.modal_snapshot_header', {
repository_name: repositoryName,
title = repositoryName;
version = I18n.t('my_modules.repository.full_view.modal_snapshot_header', {
snaphot_date: snapshotDate
});
} else {
title = I18n.t('my_modules.repository.full_view.modal_live_header', {
repository_name: repositoryName
});
title = repositoryName;
version = I18n.t('my_modules.repository.full_view.modal_live_header');
}
FULL_VIEW_MODAL.find('.repository-name').data('repository-name', repositoryName);
FULL_VIEW_MODAL.find('.repository-name').html(title);
FULL_VIEW_MODAL.find('.repository-title').data('repository-name', repositoryName);
FULL_VIEW_MODAL.find('.repository-title').html(title);
FULL_VIEW_MODAL.find('.repository-version').html(version);
}
function initRepoistoryAssignView() {
@ -635,9 +638,13 @@ var MyModuleRepositories = (function() {
updateFullViewRowsCount(data.rows_count);
renderFullViewAssignButtons();
},
error: function(data) {
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
} else {
HelperModule.flashAlertMsg(response.responseJSON.flash, 'danger');
}
UPDATE_REPOSITORY_MODAL.modal('hide');
HelperModule.flashAlertMsg(data.responseJSON.flash, 'danger');
SELECTED_ROWS = {};
FULL_VIEW_TABLE.ajax.reload(null, false);
}

View file

@ -47,17 +47,6 @@
});
}
function applyCollapseLinkCallBack() {
$('.panel-collapse')
.on('shown.bs.collapse hidden.bs.collapse', function() {
var collapseIcon = $(this).closest('.panel').find('.collapse-result-icon');
var collapsed = $(this).closest('.panel').find('.result-panel-collapse-link').hasClass('collapsed');
// Toggle collapse button
collapseIcon.toggleClass('fa-caret-up', !collapsed);
collapseIcon.toggleClass('fa-caret-down', collapsed);
});
}
// Toggle editing buttons
function toggleResultEditButtons(show) {
if (show) {
@ -80,10 +69,6 @@
// Expand all results
function expandAllResults() {
$('.result .panel-collapse').collapse('show');
$(document).find('span.collapse-result-icon').each(function() {
$(this).addClass('fa-caret-up');
$(this).removeClass('fa-caret-down');
});
$(document).find('div.step-result-hot-table').each(function() {
renderTable(this);
});
@ -91,10 +76,6 @@
function expandResult(result) {
$('.panel-collapse', result).collapse('show');
$(result).find('span.collapse-result-icon').each(function() {
$(this).addClass('fa-caret-up');
$(this).removeClass('fa-caret-down');
});
renderTable($(result).find('div.step-result-hot-table'));
animateSpinner(null, false);
}
@ -135,7 +116,6 @@
$.each($('#results').find('.result'), function() {
initFormSubmitLinks($(this));
ResultAssets.applyEditResultAssetCallback();
applyCollapseLinkCallBack();
applyCreateWopiFileCallback();
toggleResultEditButtons(true);
FilePreviewModal.init();
@ -208,15 +188,11 @@
function init() {
initHandsOnTables($(document));
expandAllResults();
applyCollapseLinkCallBack();
applyCreateWopiFileCallback();
$(function() {
$('#results-collapse-btn').click(function() {
$('.result .panel-collapse').collapse('hide');
$(document).find('span.collapse-result-icon')
.addClass('fa-caret-down')
.removeClass('fa-caret-square-up');
});
$('#results-expand-btn').click(expandAllResults);
@ -233,7 +209,6 @@
let publicAPI = Object.freeze({
init: init,
initHandsOnTables: initHandsOnTables,
applyCollapseLinkCallBack: applyCollapseLinkCallBack,
toggleResultEditButtons: toggleResultEditButtons,
expandResult: expandResult,
processResult: processResult,

View file

@ -0,0 +1,16 @@
/* global animateSpinner */
(function() {
$('.task-flows').on('click', '#viewTaskFlow', function() {
$('#statusFlowModal').modal('show');
});
$('#statusFlowModal').on('show.bs.modal', function() {
var $modalBody = $(this).find('.modal-body');
animateSpinner($modalBody);
$.get($(this).data('status-flow-url'), function(result) {
animateSpinner($modalBody, false);
$modalBody.html(result.html);
});
});
}());

View file

@ -176,10 +176,6 @@ function expandAllSteps() {
$(document).find("[data-role='step-hot-table']").each(function() {
renderTable($(this));
});
$(document).find('span.collapse-step-icon').each(function() {
$(this).addClass('fa-caret-square-up');
$(this).removeClass('fa-caret-square-down');
});
}
function handleFormSubmit(modal) {

View file

@ -22,8 +22,12 @@ var ProtocolRepositoryHeader = (function() {
function initEditDescription() {
var viewObject = $('#protocol_description_view');
viewObject.on('click', function() {
viewObject.on('click', function(e) {
if ($(e.target).hasClass('record-info-link')) return;
TinyMCE.init('#protocol_description_textarea');
}).on('click', 'a', function(e) {
if ($(this).hasClass('record-info-link')) return;
e.stopPropagation();
});
TinyMCE.initIfHasDraft(viewObject);
}

View file

@ -439,79 +439,78 @@ function updateButtons() {
var archiveBtn = $("[data-action='archive']");
var restoreBtn = $("[data-action='restore']");
var exportBtn = $("[data-action='export']");
var row = $("tr[data-row-id='" + rowsSelected[0] + "']");
var rows = [];
if (rowsSelected.length == 1) {
if (rowsSelected.length === 1) {
// 1 ROW SELECTED
var row = $("tr[data-row-id='" + rowsSelected[0] + "']");
if (row.is("[data-can-edit]")) {
editBtn.removeAttr("disabled");
editBtn.off("click").on("click", function() { editSelectedProtocol(); });
if (row.is('[data-can-edit]')) {
editBtn.removeClass('disabled hidden');
editBtn.off('click').on('click', function() { editSelectedProtocol(); });
} else {
editBtn.attr("disabled", "disabled");
editBtn.off("click");
editBtn.removeClass('hidden').addClass('disabled');
editBtn.off('click');
}
if (row.is("[data-can-clone]")) {
cloneBtn.removeAttr("disabled");
cloneBtn.off("click").on("click", function() { cloneSelectedProtocol(); });
if (row.is('[data-can-clone]')) {
cloneBtn.removeClass('disabled hidden');
cloneBtn.off('click').on('click', function() { cloneSelectedProtocol(); });
} else {
cloneBtn.attr("disabled", "disabled");
cloneBtn.off("click");
cloneBtn.removeClass('hidden').addClass('disabled');
cloneBtn.off('click');
}
if (row.is("[data-can-make-private]")) {
makePrivateBtn.removeAttr("disabled");
makePrivateBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
if (row.is('[data-can-make-private]')) {
makePrivateBtn.removeClass('disabled hidden');
makePrivateBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
makePrivateBtn.attr("disabled", "disabled");
makePrivateBtn.off("click");
makePrivateBtn.removeClass('hidden').addClass('disabled');
makePrivateBtn.off('click');
}
if (row.is("[data-can-publish]")) {
publishBtn.removeAttr("disabled");
publishBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
if (row.is('[data-can-publish]')) {
publishBtn.removeClass('disabled hidden');
publishBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
publishBtn.attr("disabled", "disabled");
publishBtn.off("click");
publishBtn.removeClass('hidden').addClass('disabled');
publishBtn.off('click');
}
if (row.is("[data-can-archive]")) {
archiveBtn.removeAttr("disabled");
archiveBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
if (row.is('[data-can-archive]')) {
archiveBtn.removeClass('disabled hidden');
archiveBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
archiveBtn.attr("disabled", "disabled");
archiveBtn.off("click");
archiveBtn.removeClass('hidden').addClass('disabled');
archiveBtn.off('click');
}
if (row.is("[data-can-restore]")) {
restoreBtn.removeAttr("disabled");
restoreBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
if (row.is('[data-can-restore]')) {
restoreBtn.removeClass('disabled hidden');
restoreBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
restoreBtn.attr("disabled", "disabled");
restoreBtn.off("click");
restoreBtn.removeClass('hidden').addClass('disabled');
restoreBtn.off('click');
}
if (row.is("[data-can-export]")) {
exportBtn.removeAttr("disabled");
exportBtn.off("click").on("click", function() { exportProtocols(rowsSelected); });
if (row.is('[data-can-export]')) {
exportBtn.removeClass('disabled hidden');
exportBtn.off('click').on('click', function() { exportProtocols(rowsSelected); });
} else {
exportBtn.attr("disabled", "disabled");
exportBtn.off("click");
exportBtn.removeClass('hidden').addClass('disabled');
exportBtn.off('click');
}
} else if (rowsSelected.length === 0) {
// 0 ROWS SELECTED
editBtn.attr("disabled", "disabled");
editBtn.off("click");
cloneBtn.attr("disabled", "disabled");
cloneBtn.off("click");
makePrivateBtn.attr("disabled", "disabled");
makePrivateBtn.off("click");
publishBtn.attr("disabled", "disabled");
publishBtn.off("click");
archiveBtn.attr("disabled", "disabled");
archiveBtn.off("click");
restoreBtn.attr("disabled", "disabled");
restoreBtn.off("click");
exportBtn.attr("disabled", "disabled");
exportBtn.off("click");
editBtn.addClass('disabled hidden');
editBtn.off('click');
cloneBtn.addClass('disabled hidden');
cloneBtn.off('click');
makePrivateBtn.addClass('disabled hidden');
makePrivateBtn.off('click');
publishBtn.addClass('disabled hidden');
publishBtn.off('click');
archiveBtn.addClass('disabled hidden');
archiveBtn.off('click');
restoreBtn.addClass('disabled hidden');
restoreBtn.off('click');
exportBtn.addClass('disabled hidden');
exportBtn.off('click');
} else {
// > 1 ROWS SELECTED
var rows = [];
_.each(rowsSelected, function(rowId) {
rows.push($("tr[data-row-id='" + rowId + "']")[0]);
});
@ -519,44 +518,44 @@ function updateButtons() {
// Only enable button if all selected rows can
// be published/archived/restored/exported
editBtn.attr("disabled", "disabled");
editBtn.off("click");
cloneBtn.attr("disabled", "disabled");
cloneBtn.off("click");
if (!rows.is(":not([data-can-make-private])")) {
makePrivateBtn.removeAttr("disabled");
makePrivateBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
editBtn.removeClass('hidden').addClass('disabled');
editBtn.off('click');
cloneBtn.removeClass('hidden').addClass('disabled');
cloneBtn.off('click');
if (!rows.is(':not([data-can-make-private])')) {
makePrivateBtn.removeClass('disabled hidden');
makePrivateBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
makePrivateBtn.attr("disabled", "disabled");
makePrivateBtn.off("click");
makePrivateBtn.removeClass('hidden').addClass('disabled');
makePrivateBtn.off('click');
}
if (!rows.is(":not([data-can-publish])")) {
publishBtn.removeAttr("disabled");
publishBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
if (!rows.is(':not([data-can-publish])')) {
publishBtn.removeClass('disabled hidden');
publishBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
publishBtn.attr("disabled", "disabled");
publishBtn.off("click");
publishBtn.removeClass('hidden').addClass('disabled');
publishBtn.off('click');
}
if (!rows.is(":not([data-can-archive])")) {
archiveBtn.removeAttr("disabled");
archiveBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
if (!rows.is(':not([data-can-archive])')) {
archiveBtn.removeClass('disabled hidden');
archiveBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
archiveBtn.attr("disabled", "disabled");
archiveBtn.off("click");
archiveBtn.removeClass('hidden').addClass('disabled');
archiveBtn.off('click');
}
if (!rows.is(":not([data-can-restore])")) {
restoreBtn.removeAttr("disabled");
restoreBtn.off("click").on("click", function() { processMoveButtonClick($(this)); });
if (!rows.is(':not([data-can-restore])')) {
restoreBtn.removeClass('disabled hidden');
restoreBtn.off('click').on('click', function() { processMoveButtonClick($(this)); });
} else {
restoreBtn.attr("disabled", "disabled");
restoreBtn.off("click");
restoreBtn.removeClass('hidden').addClass('disabled');
restoreBtn.off('click');
}
if (!rows.is(":not([data-can-export])")) {
exportBtn.removeAttr("disabled");
exportBtn.off("click").on("click", function() { exportProtocols(rowsSelected); });
if (!rows.is(':not([data-can-export])')) {
exportBtn.removeClass('disabled hidden');
exportBtn.off('click').on('click', function() { exportProtocols(rowsSelected); });
} else {
exportBtn.attr("disabled", "disabled");
exportBtn.off("click");
exportBtn.removeClass('hidden').addClass('disabled');
exportBtn.off('click');
}
}
}

View file

@ -24,38 +24,6 @@
});
}
// Complete mymodule
function complete_my_module_actions() {
var modal = $('#completed-task-modal');
modal.find('[data-action="complete"]')
.off().on().click(function(event) {
event.stopPropagation();
event.preventDefault();
event.stopImmediatePropagation();
$.ajax({
url: modal.data('url'),
type: 'GET',
success: function(data) {
var task_button = $("[data-action='complete-task']");
task_button.attr('data-action', 'uncomplete-task');
task_button.find('.btn')
.removeClass('btn-toggle').addClass('btn-default');
$('.task-due-date').html(data.module_header_due_date);
$('.task-state-label').html(data.module_state_label);
task_button
.find('button')
.html('<span class="fas fa-times"></span>&nbsp;' +
data.task_button_title);
modal.modal('hide');
},
error: function() {
modal.modal('hide');
}
});
});
}
// Sets callback for completing/uncompleting step
function applyStepCompletedCallBack() {
// First, remove old event handlers, as we use turbolinks
@ -77,25 +45,20 @@
button = step.find("[data-action='complete-step']");
button.attr("data-action", "uncomplete-step");
button.find(".btn").removeClass("btn-toggle").addClass("btn-default");
button.find("button").html('<span class="fas fa-times"></span>&nbsp;' + data.new_title);
if (data.task_ready_to_complete) {
$('#completed-task-modal').modal('show');
complete_my_module_actions();
}
button.html('<span class="fas fa-times"></span>&nbsp;' + data.new_title);
}
else {
step.addClass("not-completed").removeClass("completed");
button = step.find("[data-action='uncomplete-step']");
button.attr("data-action", "complete-step");
button.find(".btn").removeClass("btn-default").addClass("btn-toggle");
button.find("button").html('<span class="fas fa-check"></span>&nbsp;' + data.new_title);
button.html('<span class="fas fa-check"></span>&nbsp;' + data.new_title);
}
},
error: function (data) {
console.log ("error");
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
}
});
});
@ -157,6 +120,11 @@
tinyMCE.editors.step_description_textarea.remove();
TinyMCE.init('#step_description_textarea');
});
})
.on("ajax:error", function(e, response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
});
}
@ -166,44 +134,24 @@
if ($.isEmptyObject(data)) return;
let $step = $(this).closest('.step');
let stepUpPosition = data.step_up_position;
let stepDownPosition = data.step_down_position;
let $stepDown, $stepUp;
let steps = $('#steps').find('.step');
$('#steps').append($.map(data.steps_order, function(step_data) {
let step = $('#steps').find(`.step[data-id=${step_data.id}]`);
step.find('.step-number').html(`${step_data.position + 1}.`);
return step;
}));
switch ($(this).data('direction')) {
case 'up':
$stepDown = $step.prev('.step');
$stepUp = $step;
break;
case 'down':
$stepDown = $step;
$stepUp = $step.next('.step');
}
// Switch position of top and bottom steps
if (!_.isUndefined($stepDown) && !_.isUndefined($stepUp)) {
$stepDown.insertAfter($stepUp);
$stepDown.find('.step-number').html(`${stepDownPosition + 1}.`);
$stepUp.find('.step-number').html(`${stepUpPosition + 1}.`);
$('html, body').animate({ scrollTop: $step.offset().top - window.innerHeight / 2 });
}
$('html, body').animate({ scrollTop: $step.offset().top - window.innerHeight / 2 });
if (typeof refreshProtocolStatusBar === 'function') refreshProtocolStatusBar();
})
.on("ajax:error", function(e, xhr) {
if (xhr.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
});
}
function applyCollapseLinkCallBack() {
$(".step-panel-collapse-link")
.on("ajax:success", function() {
var collapseIcon = $(this).find(".collapse-step-icon");
var collapsed = $(this).hasClass("collapsed");
// Toggle collapse button
collapseIcon.toggleClass("fa-chevron-up", !collapsed);
collapseIcon.toggleClass("fa-chevron-down", collapsed);
});
}
function formCallback($form) {
$form
.on("fields_added.nested_form_fields", function(e, param) {
@ -387,7 +335,6 @@
applyStepCompletedCallBack();
applyEditCallBack();
applyMoveStepCallBack();
applyCollapseLinkCallBack();
initDeleteStep();
TinyMCE.highlight();
}
@ -518,8 +465,11 @@
});
},
error: function() {
newStepHandler();
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
animateSpinner(null, false);
}
}
})
});
@ -648,17 +598,10 @@
$(document).find("[data-role='step-hot-table']").each(function() {
renderTable($(this));
});
$(document).find("span.collapse-step-icon").each(function() {
$(this).addClass("fa-chevron-up");
$(this).removeClass("fa-chevron-down");
});
}
function expandStep(step) {
$('.panel-collapse', step).collapse('show');
$(step).find("span.collapse-step-icon")
.addClass("fa-chevron-up")
.removeClass("fa-chevron-down");
$(step).find("div.step-result-hot-table").each(function() {
renderTable($(this));
});
@ -706,10 +649,6 @@
$(function () {
$("[data-action='collapse-steps']").click(function () {
$('.step .panel-collapse').collapse('hide');
$(document).find("span.collapse-step-icon").each(function() {
$(this).addClass("fa-chevron-down");
$(this).removeClass("fa-chevron-up");
});
});
$("[data-action='expand-steps']").click(expandAllSteps);
});

View file

@ -114,14 +114,14 @@
var editReportButton = $('#edit-report-btn');
var deleteReportsButton = $('#delete-reports-btn');
if (CHECKED_REPORTS.length === 0) {
editReportButton.addClass("disabled");
deleteReportsButton.addClass("disabled");
editReportButton.addClass('disabled hidden');
deleteReportsButton.addClass('disabled hidden');
} else if (CHECKED_REPORTS.length === 1) {
editReportButton.removeClass("disabled");
deleteReportsButton.removeClass("disabled");
editReportButton.removeClass('disabled hidden');
deleteReportsButton.removeClass('disabled hidden');
} else {
editReportButton.addClass("disabled");
deleteReportsButton.removeClass("disabled");
editReportButton.removeClass('hidden').addClass('disabled');
deleteReportsButton.removeClass('disabled hidden');
}
}

View file

@ -68,7 +68,6 @@
initFormSubmitLinks($newResult);
$(this).remove();
applyEditResultAssetCallback();
Results.applyCollapseLinkCallBack();
Results.toggleResultEditButtons(true);
Results.expandResult($newResult);

View file

@ -47,7 +47,6 @@
$(this).remove();
applyEditResultTableCallback();
Results.applyCollapseLinkCallBack();
Results.initHandsOnTables($result);
Results.toggleResultEditButtons(true);
Results.expandResult($result);

View file

@ -73,7 +73,6 @@
initFormSubmitLinks(newResult);
$(this).remove();
applyEditResultTextCallback();
Results.applyCollapseLinkCallBack();
Results.toggleResultEditButtons(true);
Results.expandResult(newResult);
TinyMCE.destroyAll();

View file

@ -1,5 +1,5 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "initInlineEditing" }]*/
/* global SmartAnnotation */
/* global SmartAnnotation HelperModule I18n */
var inlineEditing = (function() {
const SIDEBAR_ITEM_TYPES = ['project', 'experiment', 'my_module', 'repository'];
@ -103,6 +103,9 @@ var inlineEditing = (function() {
},
error: function(response) {
var error = response.responseJSON[fieldToUpdate];
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
if (!error) error = response.responseJSON.errors[fieldToUpdate];
container.addClass('error');
container.find('.error-block').html(error.join(', '));

View file

@ -0,0 +1,241 @@
/* global _ */
var SmartAnnotation = (function() {
'use strict';
// stop the user annotation popover on click propagation
function atwhoStopPropagation(element) {
$(element).on('click', function(e) {
e.stopPropagation();
e.preventDefault();
});
}
function SetAtWho(field) {
var FilterTypeEnum = Object.freeze({
USER: { tag: 'users', dataUrl: $(document.body).attr('data-atwho-users-url') },
TASK: { tag: 'sa-tasks', dataUrl: $(document.body).attr('data-atwho-task-url') },
PROJECT: { tag: 'sa-projects', dataUrl: $(document.body).attr('data-atwho-project-url') },
EXPERIMENT: { tag: 'sa-experiments', dataUrl: $(document.body).attr('data-atwho-experiment-url') },
REPOSITORY: { tag: 'sa-repositories', dataUrl: $(document.body).attr('data-atwho-rep-items-url') },
MENU: { tag: 'menu', dataUrl: $(document.body).attr('data-atwho-menu-items') }
});
var DEFAULT_SEARCH_FILTER = FilterTypeEnum.REPOSITORY;
function matchHighlighter(html, query) {
var $html = $(html);
var $liText = $html.find('.item-text');
if ($liText.length === 0 || !query) return html;
$.each($liText, function(i, item) {
$(item).html($(item).text().replace(new RegExp(query.split(' ').join('|'), 'gi'),
'<span class="atwho-highlight">$&</span>'));
});
return $html;
}
// Generates suggestion dropdown filter
function generateFilterMenu() {
var menu = '';
$.ajax({
async: false,
dataType: 'json',
url: $(document.body).attr('data-atwho-repositories-url'),
success: function(data) {
menu = data.html;
}
});
return menu;
}
function atWhoSettings(at) {
return {
at: at,
callbacks: {
remoteFilter: function(query, callback) {
var $currentAtWho = $(`.atwho-view[data-at-who-id=${$(field).attr('data-smart-annotation')}]`);
var filterType;
var params = { query: query };
filterType = FilterTypeEnum[$currentAtWho.find('.tab-pane.active').data('object-type')];
if (!filterType) {
callback([{ name: '' }]);
return false;
}
if (filterType.tag === 'sa-repositories') {
let repositoryTab = $currentAtWho.find('[data-object-type="REPOSITORY"]');
let activeRepository = repositoryTab.find('.btn-primary');
if (activeRepository.length) {
params.repository_id = activeRepository.data('object-id');
}
}
$.getJSON(filterType.dataUrl, params, function(data) {
localStorage.setItem('smart_annotation_states/teams/' + data.team, JSON.stringify({
tag: filterType.tag,
repository: data.repository
}));
callback(data.res);
if (data.repository) {
$currentAtWho.find(`.repository-object[data-object-id="${data.repository}"]`)
.addClass('btn-primary').removeClass('btn-light');
}
});
return true;
},
tplEval: function(_tpl, items) {
var $items = $(items.name);
$items.find('li').data('item-data', { 'atwho-at': at }); // Emulate at.js insertContentFor method
return $items;
},
highlighter: function(li, query) {
return matchHighlighter(li, query);
},
beforeInsert: function(value, li) {
return `[#${li.attr('data-name')}~${li.attr('data-type')}~${li.attr('data-id')}]`;
},
matcher: function(flag, subtext, shouldStartWithSpace) {
var a;
var y;
var match;
var regexp;
var cleanedFlag = flag.replace(/[-[]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
if (shouldStartWithSpace) {
cleanedFlag = '(?:^|\\s)' + cleanedFlag;
}
a = decodeURI('%C3%80');
y = decodeURI('%C3%BF');
regexp = new RegExp(`${cleanedFlag}$|${cleanedFlag}(\\S[A-Za-z${a}-${y}0-9_/:\\s+-]*)$|${cleanedFlag}(\\S[^\\x00-\\xff]*)$`, 'gi');
match = regexp.exec(subtext);
if (match) {
return match[1] || '';
}
return null;
}
},
headerTpl: generateFilterMenu(),
startWithSpace: true,
acceptSpaceBar: true,
displayTimeout: 120000
};
}
function init() {
$(field)
.on('shown.atwho', function() {
var $currentAtWho = $('.atwho-view[style]:not(.old)');
var atWhoId = $currentAtWho.find('.atwho-header-res').data('at-who-key');
$currentAtWho.addClass('old').attr('data-at-who-id', atWhoId);
$(field).attr('data-smart-annotation', atWhoId);
$currentAtWho.find('.tab-button').off().on('shown.bs.tab', function() {
$(field).click().focus();
$(this).closest('.nav-tabs').find('.tab-button').removeClass('active');
$(this).addClass('active');
});
$currentAtWho.find('.repository-object').off().on('click', function() {
$(this).parent().find('.repository-object').removeClass('btn-primary')
.addClass('btn-light');
$(this).addClass('btn-primary').removeClass('btn-light');
$(field).click().focus();
});
if ($currentAtWho.find('.tab-pane.active').length === 0) {
let filterType = DEFAULT_SEARCH_FILTER.tag;
let teamId = $currentAtWho.find('.atwho-header-res').data('team-id');
let remeberedState = localStorage.getItem('smart_annotation_states/teams/' + teamId);
if (remeberedState) {
try {
remeberedState = JSON.parse(remeberedState);
filterType = remeberedState.tag;
$currentAtWho.find(`.repository-object[data-object-id=${remeberedState.repository}]`)
.addClass('btn-primary');
} catch (error) {
console.error(error);
}
}
$currentAtWho.find(`.${filterType}`).click();
}
})
.on('reposition.atwho', function(event, flag, query) {
let inputFieldLeft = query.$inputor.offset().left;
if (inputFieldLeft > $(window).width()) {
let leftPosition;
if (inputFieldLeft < flag.left + $(window).scrollLeft()) {
leftPosition = inputFieldLeft;
} else {
leftPosition = flag.left + $(window).scrollLeft();
}
query.$el.find('.atwho-view').css('left', leftPosition + 'px');
}
if ($('.repository-show').length) {
query.$el.find('.atwho-view').css('top', flag.top + 'px');
}
})
.atwho({
at: '@',
callbacks: {
remoteFilter: function(query, callback) {
$.getJSON(FilterTypeEnum.USER.dataUrl, { query: query }, function(data) {
callback(data.users);
});
},
tplEval: function(_tpl, items) {
var $items = $(items.name);
$items.find('li').data('item-data', { 'atwho-at': '@' }); // Emulate at.js insertContentFor method
return $items;
},
highlighter: function(li, query) {
return matchHighlighter(li, query);
},
beforeInsert: function(value, li) {
return `[@${li.attr('data-full-name')}~${li.attr('data-id')}]`;
}
},
startsWithSpace: true,
acceptSpaceBar: true,
displayTimeout: 120000
})
.atwho(atWhoSettings('#'));
// .atwho(atWhoSettings('task#', FilterTypeEnum.TASK)) Waiting for better times
// .atwho(atWhoSettings('project#', FilterTypeEnum.PROJECT))
// .atwho(atWhoSettings('experiment#', FilterTypeEnum.EXPERIMENT))
// .atwho(atWhoSettings('sample#', FilterTypeEnum.REPOSITORY));
}
return {
init: init
};
}
// Closes the atwho popup * needed in repositories to close the popup
// if nothing is selected and the user leaves the form *
function closePopup() {
$('.atwho-header-res').find('.fa-times').click();
}
function initialize(field) {
var atWho = new SetAtWho(field);
atWho.init();
}
return Object.freeze({
init: initialize,
preventPropagation: atwhoStopPropagation,
closePopup: closePopup
});
}());
// initialize the smart annotations
(function() {
$(document).on('focus', '[data-atwho-edit]', function() {
if (_.isUndefined($(this).data('atwho'))) {
SmartAnnotation.init(this);
}
});
$(document).on('click', '.atwho-view .dismiss', function() {
$(this).closest('.atwho-view').hide();
});
}());

View file

@ -1,520 +0,0 @@
var SmartAnnotation = (function() {
'use strict';
// utilities
var Util = (function() {
// helper method that binds show/hidden action
function showHideBinding() {
$.each(['show', 'hide'], function (i, ev) {
var el = $.fn[ev];
$.fn[ev] = function () {
this.trigger(ev);
return el.apply(this, arguments);
};
});
}
var publicApi = {
showHideBinding: showHideBinding
};
return publicApi;
})();
// stop the user annotation popover on click propagation
function atwhoStopPropagation(element) {
$(element).on('click', function(e) {
e.stopPropagation();
e.preventDefault();
});
}
function setAtWho(field) {
var FilterTypeEnum = Object.freeze({
USER: {tag: "users",
dataUrl: $(document.body).attr('data-atwho-users-url')},
TASK: {tag: "tsk",
dataUrl: $(document.body).attr('data-atwho-task-url')},
PROJECT: {tag: "prj",
dataUrl: $(document.body).attr('data-atwho-project-url')},
EXPERIMENT: {tag: "exp",
dataUrl: $(document.body).attr('data-atwho-experiment-url')},
REPOSITORY: {tag: "rep",
dataUrl: $(document.body).attr('data-atwho-rep-items-url')},
MENU: {tag: "menu",
dataUrl: $(document.body).attr('data-atwho-menu-items')}
});
var prevAt,
// Default selected filter when using '#'
DEFAULT_SEARCH_FILTER = FilterTypeEnum.REPOSITORY,
atWhoUpdating = false;
var defaultRepId;
// helper methods for AtWho callback
function _templateEval(_tpl, map) {
var res;
try {
if (map.no_results) {
res = noResultsTemplate();
} else {
res = generateTemplate(map);
}
} catch (_error) {
res = '';
}
return res;
}
function _matchHighlighter(li, query, filterType) {
var $li, re;
function highlight(el, sel, re) {
var prevVal, newVal;
prevVal = el.find(sel).html();
newVal = prevVal.replace(re, '<strong>$&</strong>');
el.find(sel).html(newVal);
}
if (!query || $(li).data('no-results')) {
return li;
}
$li = $(li);
re = new RegExp(query, 'gi');
// search_filter is not passed for the user
if(filterType) {
highlight($li, '[data-val=name]', re);
} else {
highlight($li, '[data-val=full-name]', re);
highlight($li, '[data-val=email]', re);
}
return $li[0].outerHTML
}
function _generateInputTag(value, li) {
var res = '';
res += '[#' + li.attr('data-name');
res += '~' + li.attr('data-type');
res += '~' + li.attr('data-id') + ']';
return res;
}
// initialise dropdown dismiss button
function initDismissButton($currentAtWho) {
$currentAtWho.find('.dismiss').off('click')
.on('click', function() {
$(field).atwho('destroy');
init();
});
}
// Initialize or update dropdown header buttons
function updateHeaderButtons(query, filterType) {
var $currentAtWho = $('.atwho-view[style]');
initDismissButton($currentAtWho);
// Update the selected filter button when changing smart annotation type
$currentAtWho.find('[data-filter]')
.removeClass('btn-primary')
.addClass('btn-default');
if(filterType.tag === 'rep') {
$currentAtWho.find('[data-rep-id="' + filterType.repository_id + '"]')
.removeClass('btn-default')
.addClass('btn-primary');
} else {
$currentAtWho.find('[data-filter="' + filterType.tag + '"]')
.removeClass('btn-default')
.addClass('btn-primary');
}
// Update the selected filter button when clicking on one of them
$currentAtWho.find('[data-filter]').off()
.on('click', function(e) {
if($(this).hasClass('btn-primary')) {
return;
}
var $selectedBtn = $(this);
var $prevBtn = $selectedBtn.closest('.atwho-header-res')
.children('.btn-primary');
$selectedBtn.removeClass('btn-default').addClass('btn-primary');
$prevBtn.removeClass('btn-primary').addClass('btn-default');
// Updates query and dropdown elements; focuses input
$(field).click().focus();
});
}
// Generates suggestion dropdown filter
function generateFilterMenu(active, res_data) {
var rep_buttons = '';
$.ajax({
async: false,
dataType: 'json',
url: $(document.body).attr('data-atwho-repositories-url'),
success: function(data) {
$.each(data['repositories'], function(id, name) {
if(defaultRepId === undefined){
defaultRepId = id;
}
rep_buttons += '<button data-filter="rep" data-rep-id="' + id +
'" class="btn btn-xs btn-primary">' + name + '</button>';
});
}
});
var header = '<div class="atwho-header-res">' +
'<button data-filter="prj" class="btn btn-xs btn-primary' +
'">Projects</button>' +
'<button data-filter="exp" class="btn btn-xs btn-primary' +
'">Experiments</button>' +
'<button data-filter="tsk" class="btn btn-xs btn-primary' +
'">Tasks</button>' +
rep_buttons +
'<div class="dismiss">' +
'<span class="fas fa-times"></span>' +
'</div>' +
'<div class="help">' +
'<div>' +
'<strong><%= I18n.t("atwho.users.navigate_1") %></strong> ' +
'<%= I18n.t("atwho.users.navigate_2") %>' +
'</div>' +
'<div><strong><%= I18n.t("atwho.users.confirm_1") %></strong> ' +
'<%= I18n.t("atwho.users.confirm_2") %>' +
'</div>' +
'<div>' +
'<strong><%= I18n.t("atwho.users.dismiss_1") %></strong> ' +
'<%= I18n.t("atwho.users.dismiss_2") %>' +
'</div>' +
'</div>' +
'</div>';
return header;
}
function noResultsTemplate() {
var res = '<div class="atwho-no-results" data-no-results="1">';
res += '<span><%= I18n.t("atwho.no_results") %></span>';
res += '</div>';
return res;
}
// Generates resources list items
function generateTemplate(map) {
var res = '';
res += '<li class="atwho-li atwho-li-res" data-name="' +
truncateLongString(map.name,
<%= Constants::NAME_TRUNCATION_LENGTH %>) +
'" data-id="' + map.id + '" data-type="' +
map.type + '">';
switch(map.type) {
case 'tsk':
res += '<span data-type class="res-type">' + map.type + '</span>';
break;
case 'prj':
res += '<span data-type class="res-type">' + map.type + '</span>';
break;
case 'exp':
res += '<span data-type class="res-type">' + map.type + '</span>';
break;
case 'rep_item':
res += '<span data-type class="res-type">' +
map.repository_tag + '</span>';
break;
}
res += '&nbsp;';
res += '<span data-val="name" class="res-name">';
res += truncateLongString(map.name,
<%= Constants::NAME_TRUNCATION_LENGTH %>);
res += '</span>';
if(map.archived) {
res += '<%= I18n.t("atwho.res.archived") %></span>';
} else {
res += '</span>';
}
res += '&nbsp;';
switch (map.type) {
case 'tsk':
res += '<span class="res-description">&lt; ' + map.experimentName +
' &lt; ' + map.projectName + '</span>';
break;
case 'exp':
res += '<span class="res-description">&lt; ' + map.projectName + '</span>';
break;
}
res += '</li>';
return res;
}
/**
* Hackish wrapper function to make AtWho work when switching between
* multiple AtWho instances (e.g. from # to task#).
*
* Prevents second execution of AtWho update callback, triggered when user
* switches to different AtWho instance (e.g. from # to task#), which causes
* both of them to be called. In such case, AtWhO modal needs to be
* rerendered.
*/
function atWhoSwitchHack(filterTypeTag, remoteFilterCb) {
if(atWhoUpdating || (!$(field).length && _.isUndefined(filterTypeTag))) {
setTimeout(function() {
$(field).click();
}, 100);
return;
}
atWhoUpdating = true;
setTimeout(function() {
remoteFilterCb();
atWhoUpdating = false;
}, 100);
}
function atWhoSettings(at, defaultFilterType) {
return {
at: at,
callbacks: {
remoteFilter: function(query, callback) {
var $currentAtWho = $('.atwho-view[style]');
var filterTypeTag = $currentAtWho
.find('.btn-primary')
.data('filter');
atWhoSwitchHack(filterTypeTag, function() {
var filterType;
if (_.isUndefined(filterTypeTag)) {
// Switched smart annotation type (i.e. changed input)
filterType = defaultFilterType;
} else {
// Switched filtering type (i.e. different filter button
// pressed; works also for specific annotation types, e.g.
// task#, and coverts to the correct annotation type on confirm)
$.each(FilterTypeEnum, function(k, v) {
if (v.tag == filterTypeTag) {
filterType = FilterTypeEnum[k];
return false;
}
});
}
if (prevAt != at) {
// Switching smart annotation type (i.e. chaned input)
prevAt = at;
filterType = defaultFilterType;
// Hide current AtWho
$currentAtWho.removeAttr("style");
}
var params = { query: query };
if(filterType.tag === 'rep') {
params.repository_id = $currentAtWho
.find('.btn-primary')
.data('rep-id');
if(params.repository_id === undefined) {
params.repository_id = defaultRepId;
}
filterType.repository_id = params.repository_id;
}
$.getJSON(
filterType.dataUrl,
params,
function(data) {
// Updates dropdown
if (data.res.length < 1) {
callback([{no_results: 1}]);
} else {
callback(data.res);
}
updateHeaderButtons(query, filterType);
}
);
});
},
sorter: function(query, items, _searchKey) {
// Sorting is already done on server-side
return items;
},
tplEval: function(_tpl, map) {
return _templateEval(_tpl, map);
},
highlighter: function(li, query) {
return _matchHighlighter(li, query, true);
},
beforeInsert: function(value, li) {
return _generateInputTag(value, li);
},
matcher:function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
var _a, _y, match, regexp, space;
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
if (should_startWithSpace) {
flag = '(?:^|\\s)' + flag;
}
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_/:\\s\+\-\]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
match = regexp.exec(subtext);
if (match) {
return match[2] || match[1];
} else {
return null;
}
},
},
headerTpl: generateFilterMenu(defaultFilterType),
limit: <%= Constants::ATWHO_SEARCH_LIMIT %>,
startWithSpace: true,
acceptSpaceBar: true,
displayTimeout: 120000
}
}
function init() {
$(field)
.on("reposition.atwho", function(event, flag, query) {
let inputFieldLeft = query.$inputor.offset().left;
if (inputFieldLeft > $(window).width()) {
let leftPosition;
if (inputFieldLeft < flag.left + $(window).scrollLeft()) {
leftPosition = inputFieldLeft;
} else {
leftPosition = flag.left + $(window).scrollLeft();
}
query.$el.find('.atwho-view').css('left', leftPosition + 'px');
}
if ($('.repository-show').length) {
query.$el.find('.atwho-view').css('top', flag.top + 'px');
}
})
.atwho({
at: '@',
callbacks: {
remoteFilter: function(query, callback) {
$.getJSON(
FilterTypeEnum.USER.dataUrl,
{query: query},
function(data) {
if (data.users.length < 1) {
callback([{no_results: 1}]);
} else {
callback(data.users);
}
initDismissButton($('.atwho-view[style]'));
}
);
},
sorter: function(query, items, _searchKey) {
// Sorting is already done on server-side
return items;
},
tplEval: function(_tpl, map) {
var res;
try {
if (map.no_results) {
res = noResultsTemplate();
} else {
res = '';
res += '<li class="atwho-li atwho-li-user" ';
res += 'data-id="' + map.id + '" ';
res += 'data-full-name="' + map.full_name + '">';
res += '<span class="global-avatar-container"><img src="' + map.img_url + '" class="avatar" /></span>';
res += '<span data-val="full-name">';
res += map.full_name;
res += '</span>';
res += '<small>';
res += '&nbsp;';
res += '&#183;';
res += '&nbsp;';
res += '<span data-val="email">';
res += map.email;
res += '</span>';
res += '</small>';
res += '</li>';
}
} catch (_error) {
res = '';
}
return res;
},
highlighter: function(li, query) {
return _matchHighlighter(li, query);
},
beforeInsert: function(value, li) {
var res = '';
res += '[@' + li.attr('data-full-name');
res += '~' + li.attr('data-id') + ']';
return res;
}
},
headerTpl:
'<div class="atwho-header-res">' +
'<div class="title-user"><%= I18n.t("atwho.users.title") %></div>' +
'<div class="help">' +
'<div>' +
'<strong><%= I18n.t("atwho.users.navigate_1") %></strong> ' +
'<%= I18n.t("atwho.users.navigate_2") %>' +
'</div>' +
'<div>' +
'<strong><%= I18n.t("atwho.users.confirm_1") %></strong> ' +
'<%= I18n.t("atwho.users.confirm_2") %>' +
'</div>' +
'<div>' +
'<strong><%= I18n.t("atwho.users.dismiss_1") %></strong> ' +
'<%= I18n.t("atwho.users.dismiss_2") %>' +
'</div>' +
'</div>' +
'<div class="dismiss">' +
'<span class="fas fa-times"></span>' +
'</div>' +
'</div>',
limit: <%= Constants::ATWHO_SEARCH_LIMIT %>,
startsWithSpace: true,
acceptSpaceBar: true,
displayTimeout: 120000
})
.atwho(atWhoSettings('#', DEFAULT_SEARCH_FILTER))
// .atwho(atWhoSettings('task#', FilterTypeEnum.TASK)) Waiting for better times
// .atwho(atWhoSettings('project#', FilterTypeEnum.PROJECT))
// .atwho(atWhoSettings('experiment#', FilterTypeEnum.EXPERIMENT));
}
return {
init: init
};
}
// Closes the atwho popup * needed in repositories to close the popup
// if nothing is selected and the user leaves the form *
function closePopup() {
$('.atwho-header-res').find('.fa-times').click();
}
function initialize(field) {
var atWho = new setAtWho(field);
atWho.init();
}
var publicApi = Object.freeze({
init: initialize,
preventPropagation: atwhoStopPropagation,
closePopup: closePopup
});
return publicApi;
})();
// initialize the smart annotations
(function initSmartAnnotation() {
$(document).on('focus', '[data-atwho-edit]', function() {
if(_.isUndefined($(this).data('atwho'))) {
SmartAnnotation.init(this);
}
});
})();

View file

@ -1,4 +1,4 @@
/* global inlineEditing PerfectScrollbar */
/* global inlineEditing PerfectScrollbar HelperModule I18n */
/* eslint-disable no-restricted-globals, no-alert */
var Comments = (function() {
function changeCounter(comment, value) {
@ -49,7 +49,11 @@ var Comments = (function() {
$this.closest('.comment-container').remove();
},
error: (error) => {
alert(error.responseJSON.errors.message);
if (error.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
} else {
alert(error.responseJSON.errors.message);
}
}
});
}
@ -85,6 +89,9 @@ var Comments = (function() {
$el.find('textarea').focus().blur();
})
.error((error) => {
if (error.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
errorField.html(error.responseJSON.errors.message);
newButton.disable = false;
});

View file

@ -434,7 +434,6 @@
$.each($('[data-container="new-reports"]').find('.result'), function() {
initFormSubmitLinks($(this));
ResultAssets.applyEditResultAssetCallback();
Results.applyCollapseLinkCallBack();
Results.toggleResultEditButtons(true);
FilePreviewModal.init();
Comments.init();

View file

@ -91,6 +91,9 @@ var dropdownSelector = (function() {
// Get data in JSON from field
function getCurrentData(container) {
if (!container.find('.data-field').val()) {
return '';
}
return JSON.parse(container.find('.data-field').val());
}
@ -179,8 +182,19 @@ var dropdownSelector = (function() {
}
// Add selected option to value
function addSelectedOption(selector, container) {
setData(selector, [convertOptionToJson($(selector).find('option:selected')[0])], true);
function addSelectedOptions(selector, container) {
var selectedOptions = [];
var optionSelector = selector.data('config').noEmptyOption ? 'option:selected' : 'option[data-selected=true]';
$.each($(selector).find(optionSelector), function(i, option) {
selectedOptions.push(convertOptionToJson(option));
if (selector.data('config').singleSelect) return false;
return true;
});
if (!selectedOptions.length) return false;
setData(selector, selectedOptions, true);
return true;
}
// Prepare custom dropdown icon
@ -422,8 +436,8 @@ var dropdownSelector = (function() {
}
// Select default value
if (config.noEmptyOption && config.singleSelect) {
addSelectedOption(selectElement, dropdownContainer);
if (!selectElement.data('ajax-url')) {
addSelectedOptions(selectElement, dropdownContainer);
}
// Enable simple mode for dropdown selector
@ -849,17 +863,20 @@ var dropdownSelector = (function() {
return this;
},
// Select value
selectValue: function(selector, value) {
var $selector;
// Select values
selectValues: function(selector, values) {
var $selector = $(selector);
var option;
var valuesArray = [].concat(values);
var options = [];
if ($(selector).length === 0) return false;
$selector = $(selector);
option = $selector.find(`option[value="${value}"]`)[0];
setData($selector, [convertOptionToJson(option)]);
if ($selector.length === 0) return false;
valuesArray.forEach(function(value) {
option = $selector.find(`option[value="${value}"]`)[0];
options.push(convertOptionToJson(option));
});
setData($selector, options);
return this;
},

View file

@ -168,6 +168,10 @@ var MarvinJsEditorApi = (function() {
$(marvinJsModal).modal('hide');
FilePreviewModal.init();
config.button.dataset.inProgress = false;
}).error((response) => {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
});
}
@ -196,6 +200,11 @@ var MarvinJsEditorApi = (function() {
}
$(marvinJsModal).modal('hide');
config.button.dataset.inProgress = false;
},
error: function(response) {
if (response.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
}
});
}

View file

@ -1,4 +1,4 @@
/* global _ hljs tinyMCE SmartAnnotation I18n GLOBAL_CONSTANTS */
/* global _ hljs tinyMCE SmartAnnotation I18n GLOBAL_CONSTANTS HelperModule */
/* eslint-disable no-unused-vars */
var TinyMCE = (function() {
@ -278,6 +278,9 @@ var TinyMCE = (function() {
var model = editor.getElement().dataset.objectType;
$(this).renderFormErrors(model, data.responseJSON);
editor.setProgressState(0);
if (data.status === 403) {
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
}
});
// Init Cancel button

View file

@ -1,5 +1,3 @@
@import url(https://fonts.googleapis.com/css?family=Lato:400,400i,700&subset=latin-ext);
//==============================================================================
// Colors
//==============================================================================

View file

@ -66,7 +66,7 @@
margin-right: 4px;
width: 36px;
.curent-tasks-filters {
.current-tasks-filters {
padding: 0;
width: 230px;
@ -133,28 +133,33 @@
}
}
.current-tasks-list {
display: flex;
flex-direction: column;
.current-tasks-list-wrapper {
height: 100%;
overflow-y: auto;
padding: 0 10px;
position: relative;
}
.current-tasks-list {
align-items: center;
display: grid;
grid-template-columns: 1fr max-content max-content;
padding: 0 1em;
&.disabled {
pointer-events: none;
}
.current-task-item {
border-bottom: $border-tertiary;
color: $color-volcano;
padding: 6px;
display: contents;
text-decoration: none;
.current-task-breadcrumbs {
@include font-small;
color: $color-silver-chalice;
line-height: 14px;
grid-column: span 3;
line-height: 1em;
padding: .5em .5em .25em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -166,134 +171,61 @@
}
}
.item-row {
display: flex;
.row-border {
border-bottom: $border-tertiary;
height: 32px;
line-height: 24px;
padding-bottom: 8px;
}
.task-name {
flex-grow: 1;
font-size: $font-size-base;
.task-name {
font-size: $font-size-base;
font-weight: bold;
overflow: hidden;
padding: 0 .5em;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-due-date {
padding: 0 2em 0 1em;
.fas {
padding: .25em;
}
&.overdue {
color: $brand-danger;
}
&.day-prior {
color: $brand-warning;
}
&.completed {
color: $brand-success;
}
}
.task-status-container {
grid-column: 3;
padding: 0 .5em;
text-align: right;
.task-status {
@include font-small;
border-radius: $border-radius-tag;
color: $color-white;
font-weight: bold;
overflow: hidden;
padding-right: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-due-date {
flex-basis: 280px;
flex-shrink: 0;
font-size: 14px;
.fas {
padding: 4px;
}
&.overdue {
color: $brand-danger;
}
&.day-prior {
color: $brand-warning;
}
&.completed {
color: $brand-success;
}
padding: .25em .5em;
}
}
&:hover {
&:hover > * {
background: $color-concrete;
}
}
}
.task-progress-container {
height: 20px;
max-width: 250px;
min-width: 150px;
position: relative;
width: 100%;
&::after {
@include font-small;
@include font-awesome;
content: "";
line-height: 18px;
position: absolute;
right: 8px;
top: 1px;
}
.task-progress {
background: $brand-focus-light;
border: $border-tertiary;
border-radius: $border-radius-tag;
display: flex;
height: 20px;
position: relative;
&::after {
background: $color-white;
content: "";
height: 18px;
width: 100%;
}
}
.task-progress-label {
@include font-small;
font-weight: bold;
height: 20px;
left: 0;
line-height: 20px;
padding-left: 8px;
position: absolute;
top: 0;
width: calc(100% - 30px);
}
&.overdue {
.task-progress {
background: $brand-danger-light;
}
.task-progress-label {
color: $brand-danger;
}
&::after {
color: $brand-danger;
content: $font-fas-exclamation-triangle;
}
}
&.day-prior {
.task-progress-label {
color: $brand-warning;
}
}
&.completed {
.task-progress {
outline: $border-success;
}
.task-progress,
.task-progress::after {
background: $brand-success-light;
}
.task-progress-label {
color: $brand-success;
}
&::after {
color: $brand-success;
content: $font-fas-check;
}
}
}
}
@media (max-width: 1500px) {
@ -370,22 +302,37 @@
}
.current-tasks-list {
grid-template-columns: auto;
.current-task-item {
.item-row {
flex-wrap: wrap;
.task-due-date {
@include font-small;
.current-task-breadcrumbs {
grid-column: 1;
padding-left: 0;
}
.fas {
display: none;
}
.task-name {
border: 0;
padding: 0;
height: 1.5em;
}
.task-due-date {
@include font-small;
border: 0;
height: 24px;
padding-left: 0;
.fas {
display: none;
}
}
.task-progress-container {
flex-basis: 100%;
max-width: none;
}
.task-status-container {
grid-column: 1;
text-align: left;
padding-left: 0;
}
}
}

View file

@ -8,10 +8,9 @@
word-break: initial;
thead {
background-color: $color-concrete;
tr {
th {
background-color: $color-concrete;
border-bottom: 2px solid $color-white;
border-left: 2px solid $color-white;
font-weight: bold;
@ -31,6 +30,11 @@
&:first-child {
border-left: 0;
border-top-left-radius: $border-radius-default;
}
&:last-child {
border-top-right-radius: $border-radius-default;
}
}

View file

@ -169,20 +169,6 @@
.date-container {
flex-shrink: 0;
padding-right: 20px;
.activities-group-expand-button {
user-select: none;
&:hover,
&:visited,
&:focus {
text-decoration: none;
}
.fas {
margin-right: 3px;
}
}
}
.date-activities {
@ -197,11 +183,24 @@
.activities-group-expand-button {
color: $color-emperor;
user-select: none;
&:hover,
&:visited,
&:focus {
text-decoration: none;
}
.fas {
display: inline-block;
margin-right: 3px;
text-align: center;
width: 10px;
}
&:not(.collapsed) .fas {
@include rotate(90deg);
}
}
.activity-card {

View file

@ -166,10 +166,6 @@
display: flex;
flex-wrap: wrap;
max-width: 100%;
.complete-step-btn {
width: 100%;
}
}
}

View file

@ -503,6 +503,26 @@
}
}
.task-information {
column-gap: 1em;
display: grid;
grid-template-columns: auto minmax(max-content, 18em);
.task-section-header {
grid-column: 1 / span 1;
}
.task-details {
grid-column: 1 / span 1;
grid-row: 2 / span 1;
}
.task-flows {
grid-column: 2 / span 1;
grid-row: 1 / span 2;
}
}
@media (max-width: 700px) {
.my-module-protocol-status {
.status-info-dropdown {
@ -518,4 +538,18 @@
}
}
}
.task-information {
grid-template-columns: auto;
row-gap: .5em;
.task-details {
grid-row: 3 / span 1;
}
.task-flows {
grid-column: unset;
grid-row: 2 / span 1;
}
}
}

View file

@ -5,21 +5,9 @@
@include font-h3;
line-height: 22px;
overflow: hidden;
padding-right: 55px;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
&::after {
color: $color-alto;
content: '[' attr(data-rows-count) ']';
display: inline-block;
line-height: 22px;
padding-left: 5px;
position: absolute;
right: 0;
width: 55px;
}
}
.my-module-inventories {
@ -131,6 +119,16 @@
.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-right: .7em;
position: absolute;
right: 0;
}
}
.action-buttons {
@ -218,11 +216,26 @@
flex-grow: 1;
max-width: calc(100% - 20px);
.repository-name {
.repository-name-container {
display: flex;
}
.repository-title {
@include my-module-repository-title;
@include font-h2;
display: inline-block;
width: 100%;
}
.repository-version {
@include font-h2;
flex-shrink: 0;
padding-right: .7em;
&::after {
color: $color-alto;
content: '[' attr(data-rows-count) ']';
display: inline-block;
padding-left: .3em;
}
}
.breadcrumbs {

View file

@ -0,0 +1,167 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
// scss-lint:disable SelectorFormat
// scss-lint:disable ImportantRule
@import "constants";
@import "mixins";
.content-pane.my-modules-protocols-index {
.status-flow-dropdown {
.dropdown-toggle {
color: $color-white;
text-align: left;
width: 100%;
.caret {
margin: 8px 0;
}
}
&.open .dropdown-menu{
align-items: center;
display: grid;
grid-template-columns: minmax(0, auto) 12px minmax(0, auto);
padding: .5em 0 0;
li {
display: contents;
> * {
cursor: pointer;
line-height: 2em;
padding: .5em 1em;
}
&:hover > *{
background: $color-concrete;
}
&.disabled {
pointer-events: none;
.status-name {
background: $color-alto !important;
}
}
}
.fa-long-arrow-alt-right {
color: $color-silver-chalice;
padding: .5em 0;
}
.status-container {
display: flex;
}
.status-name {
border-radius: $border-radius-tag;
color: $color-white;
display: inline-block;
font-weight: bold;
line-height: 1em;
max-width: 100%;
overflow: hidden;
padding: .5em;
text-overflow: ellipsis;
white-space: nowrap;
}
.error-message {
@include font-small;
color: $color-silver-chalice;
grid-column: span 3;
line-height: 1em;
padding: 0em 1em .5em;
&:empty {
display: none;
}
&.permission-error {
padding: .5em 1em;
}
}
#viewTaskFlow {
border-top: $border-default;
cursor: pointer;
display: list-item;
grid-column: span 3;
line-height: 2em;
margin-top: .5em;
padding: .5em 1em;
}
}
}
}
#statusFlowModal {
.status-flow {
padding: 2em;
.status-container {
align-items: center;
display: grid;
grid-template-columns: 1fr min-content 1fr;
grid-template-rows: 28px;
justify-content: space-around;
position: relative;
.current-status {
@include font-small;
justify-self: end;
.fas {
margin: 0 .5em;
}
}
.status-block {
@include font-button;
border-radius: $border-radius-tag;
color: $color-white;
font-weight: bold;
line-height: 1em;
padding: .5em;
white-space: nowrap;
}
.status-comment {
@include font-small;
color: $color-silver-chalice;
padding-left: .5em;
}
}
.connector {
background: $color-black;
height: 2em;
margin: 0 auto;
position: relative;
width: 2px;
&:before,
&:after {
border-left: .2em solid transparent;
border-right: .2em solid transparent;
content: '';
display: block;
margin-left: -.1em;
position: absolute;
}
&:before {
border-top: .2em solid $color-black;
top: 0;
}
&:after {
border-bottom: .2em solid $color-black;
bottom: 0;
}
}
}
}

View file

@ -237,6 +237,7 @@ path, ._jsPlumb_endpoint {
.panel-body .due-date-link {
color: $color-emperor;
display: block;
}
.panel-body .due-date-label {
@ -316,10 +317,10 @@ path, ._jsPlumb_endpoint {
.module-large .tags-container,
.module-medium .tags-container {
padding-top: 2px;
padding-top: 4px;
div {
font-size: 22pt;
font-size: 20px;
width: 4px;
height: 0px;
display: inline-block;
@ -335,9 +336,9 @@ path, ._jsPlumb_endpoint {
}
& span.badge {
margin-left: -8px;
margin-top: -10px;
margin-left: -12px;
margin-right: 4px;
margin-top: -7px;
}
}

View file

@ -280,10 +280,17 @@ label {
.module-start-date,
.module-due-date {
margin-left: 5px;
white-space: nowrap;
}
.module-status {
.status-block {
border-radius: $border-radius-tag;
color: $color-white;
padding: 2px 4px;
}
}
.module-tags {
margin-left: 0;
margin-top: 10px;
@ -389,6 +396,16 @@ label {
&:hover > .report-element-body .step-name {
color: $brand-primary;
}
.step-label-default {
@include font-h3;
color: $color-alto;
}
.step-label-success {
@include font-h3;
color: $brand-success;
}
}
/* Step attachment style (table, asset or checklist) */

View file

@ -87,4 +87,8 @@
margin-right: .5em;
}
}
table > tbody > tr:first-child > td {
border-top: 0;
}
}

View file

@ -49,6 +49,8 @@
}
.task-link {
color: $brand-primary;
cursor: pointer;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;

View file

@ -0,0 +1,180 @@
.atwho-view {
background: $color_white;
border-radius: $border-radius-default;
box-shadow: $modal-shadow;
display: none;
left: 0;
margin-top: 18px;
max-width: 700px;
min-width: 600px;
overflow: auto;
position: absolute;
top: 0;
z-index: 11110 !important;
.atwho-header-res {
.nav-tabs {
align-items: center;
margin-bottom: 0;
}
.rep-tab.active:not(:empty) {
border-bottom: $border-default;
display: flex;
padding: .25em;
}
.dismiss {
@include font-button;
color: $color-silver-chalice;
cursor: pointer;
margin-left: auto;
padding: .5em .75em;
}
.repository-object {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.atwho-view-ul {
margin: 0;
padding: 0;
}
.atwho-no-results {
color: $color-silver-chalice;
padding: 1.5em 4em;
text-align: center;
.description {
@include font-main;
padding: 0 4em 2.5em;
}
}
.atwho-header {
@include font-small;
border-bottom: $border-default;
color: $color-silver-chalice;
padding: .5em;
.dismiss {
@include font-button;
cursor: pointer;
float: right;
padding: 0 .25em;
position: relative;
}
}
.atwho-footer {
@include font-small;
border-top: $border-default;
color: $color-silver-chalice;
padding: .5em;
white-space: pre;
}
.atwho-scroll-container {
max-height: 200px;
overflow-y: auto;
padding: .5em;
position: relative;
.atwho-breadcrumbs {
@include font-small;
color: $color-silver-chalice;
display: flex;
.atwho-breadcrumb {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.slash {
margin: 0 .5em;
}
}
.sa-type {
font-size: 8px;
}
.item {
cursor: pointer;
margin-left: -.5em;
overflow: hidden;
padding: .25em .5em;
text-overflow: ellipsis;
width: calc(100% + 1em);
white-space: nowrap;
&.cur {
background: $color-concrete;
}
.atwho-highlight {
background: $brand-warning-light;
}
}
}
.atwho-user {
align-items: center;
cursor: pointer;
display: flex;
padding: .5em 0;
&.cur {
background: $color-concrete;
}
.atwho-highlight {
background: $brand-warning-light;
}
&:not:first-child {
border-top: $border-default;
}
.avatar {
display: inline-block;
height: 30px;
width: 30px;
}
.user-info {
display: inline-block;
margin-left: .5em;
}
.user-email {
@include font-small;
color: $color-silver-chalice;
line-height: 1em;
}
}
.more-results {
color: $color-silver-chalice;
padding: .5em 0;
}
}
.sa-type {
font-size: 10px;
font-weight: 600;
padding-left: 2px;
text-decoration: none;
text-transform: uppercase;
vertical-align: super;
&:hover {
text-decoration: none;
}
}

View file

@ -0,0 +1,28 @@
.sci-nav-tabs {
border-bottom: $border-default;
display: flex;
a {
color: $color-volcano;
padding: .5em;
position: relative;
&:hover {
text-decoration: none;
}
&.active {
color: initial;
&::after {
content: '';
background: $brand-primary;
bottom: 0;
height: .25em;
left: 0;
position:absolute;
width: 100%;
}
}
}
}

View file

@ -16,7 +16,7 @@
border-color: $brand-focus;
.caret {
transform: rotateX(180deg)
transform: rotateX(180deg);
}
}

View file

@ -20,6 +20,16 @@
}
}
}
.checklist-name-container,
.table-name-container {
align-items: center;
display: flex;
.remove-container {
padding-top: 10px;
}
}
}
#steps {
@ -40,8 +50,12 @@
}
}
.complete-step-btn {
display: inline-block;
.step-panel-collapse-link {
padding-left: 5px;
&:not(.collapsed) .fas {
@include rotate(90deg);
}
}
.step-heading {

View file

@ -133,6 +133,14 @@ body {
font-size: $font-size-small;
}
.modal-body {
font-size: $font-size-base;
label {
@include font-small;
}
}
.jumbotron {
background-color: inherit;
}
@ -742,6 +750,47 @@ ul.double-line > li {
}
}
#canvas-container {
.panel-heading {
padding: 10px 15px 4px;
}
.panel-body {
padding: 6px 15px;
.status-label {
background-color: var(--state-color);
color: $color-white;
display: inline-block;
margin: 3px 0;
max-width: 100%;
overflow: hidden;
padding: 2px 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.panel-footer {
.nav > li > a {
padding: 6px 15px;
}
.btn {
height: 30px;
}
.badge-indicator {
background: transparent;
color: $color-silver-chalice;
font-size: 12px;
margin-left: 0;
padding: 0;
top: 0;
}
}
}
.panel-options {
position: relative;
bottom: 8px;
@ -1112,6 +1161,15 @@ ul.content-activities {
.result-panel-collapse-link {
text-decoration: none;
&:not(.collapsed) .fas {
@include rotate(90deg);
}
.fas {
margin-right: 7px;
text-align: center;
}
}
.row {
@ -1647,199 +1705,6 @@ th.custom-field .modal-tooltiptext {
pointer-events: none;
}
// AtWho (smart annotations)
// <Custom atwho style>
.atwho-view {
position: absolute;
top: 0;
left: 0;
display: none;
margin-top: 18px;
background: $color-white;
color: $color-black;
border: 1px solid $color-emperor;
border-radius: 3px;
box-shadow: 0 0 5px $color-gainsboro;
max-width: 800px;
min-width: 700px;
overflow: auto;
z-index: 11110 !important;
small {
font-size: smaller;
color: $color-emperor;
font-weight: normal;
}
strong {
color: $brand-primary;
}
.cur {
background: $brand-primary;
color: $color-white;
small {
color: $color-white;
}
strong {
color: $color-white;
font: bold;
}
.res-description {
color: $color-white;
}
.res-type {
border-color: $color-white;
}
}
ul {
list-style: none;
padding: 0;
margin: auto;
li {
align-items: center;
border-bottom: 1px solid $color-emperor;
cursor: pointer;
display: flex;
padding: 5px 10px;
.global-avatar-container {
margin-right: 5px;
}
}
}
}
// <End of overrides>
.atwho-header-res {
background-color: $color-concrete;
border-bottom: 1px solid $color-emperor;
display: flex;
flex-wrap: wrap;
padding: 3px 5px;
.btn {
border-radius: 4px;
margin: 5px;
padding: 3px;
}
.btn-default {
background-color: transparent;
}
.title-user {
padding-top: 4px;
}
.help {
margin-left: auto;
margin-right: 15px;
order: 99;
padding: 4px;
white-space: nowrap;
div {
display: inline;
font-size: smaller;
margin-left: 15px;
}
strong {
color: $color-black;
}
}
.dismiss {
color: $color-emperor;
position: absolute;
right: 5px;
top: 5px;
}
.dismiss:hover {
color: $color-black;
cursor: pointer;
}
}
.atwho-li-res {
.fa-tint {
margin-left: 5px;
margin-right: 5px;
}
.res-type {
border: 1px solid $color-black;
border-radius: 4px;
font-weight: 600;
margin-left: 5px;
margin-right: 5px;
padding: 0 2px;
text-transform: capitalize;
}
.res-name {
font-weight: 600;
margin-right: 5px;
}
.res-description {
color: $color-emperor;
font-size: 10px;
}
}
.sa-type {
border: 1px solid $color-emperor;
border-radius: 4px;
font-weight: 600;
padding: 0 2px;
text-decoration: none;
text-transform: capitalize;
&:hover {
text-decoration: none;
}
}
.atwho-user-container {
align-items: center;
display: inline-flex;
.global-avatar-container {
line-height: 30px;
margin: 0 2px 0 4px;
img {
position: relative;
top: -2px;
}
}
}
.atwho-user-popover {
cursor: pointer;
display: inline-block;
}
.atwho-user-img-popover {
cursor: default;
}
.atwho-no-results {
padding: 5px 10px;
}
.popover {
border-radius: 3px;
min-width: 450px;

View file

@ -31,7 +31,9 @@ module ActiveStorage
unless processing
ActiveStorage::PreviewJob.perform_later(@blob.id)
@blob.attachments.take.record.update(file_processing: true)
ActiveRecord::Base.no_touching do
@blob.attachments.take.record.update(file_processing: true)
end
end
false

View file

@ -23,14 +23,14 @@ module Api
raise PermissionError.new(Asset, :create) unless can_manage_protocol_in_module?(@protocol)
if @form_multipart_upload
asset = @step.assets.new(asset_params)
asset = @step.assets.new(asset_params.merge({ team_id: @team.id }))
else
blob = ActiveStorage::Blob.create_after_upload!(
io: StringIO.new(Base64.decode64(asset_params[:file_data])),
filename: asset_params[:file_name],
content_type: asset_params[:file_type]
)
asset = @step.assets.new(file: blob)
asset = @step.assets.new(file: blob, team: @team)
end
asset.save!(context: :on_api_upload)

View file

@ -201,6 +201,10 @@ module Api
@checklist_item = @checklist.checklist_items.find(params.require(key))
raise PermissionError.new(Protocol, :read) unless can_read_protocol_in_module?(@step.protocol)
end
def load_workflow(key = :workflow_id)
@workflow = MyModuleStatusFlow.find(params.require(key))
end
end
end
end

View file

@ -8,6 +8,7 @@ module Api
before_action only: :show do
load_experiment(:id)
end
before_action :load_experiment_for_managing, only: %i(update)
def index
experiments = @project.experiments
@ -19,6 +20,47 @@ module Api
def show
render jsonapi: @experiment, serializer: ExperimentSerializer
end
def create
raise PermissionError.new(Experiment, :create) unless can_create_experiments?(@project)
experiment = @project.experiments.create!(experiment_params.merge!(created_by: current_user,
last_modified_by: current_user))
render jsonapi: experiment, serializer: ExperimentSerializer, status: :created
end
def update
@experiment.assign_attributes(experiment_params)
return render body: nil, status: :no_content unless @experiment.changed?
if @experiment.archived_changed?
if @experiment.archived?
@experiment.archived_by = current_user
@experiment.archived_on = DateTime.now
else
@experiment.restored_by = current_user
@experiment.restored_on = DateTime.now
end
end
@experiment.last_modified_by = current_user
@experiment.save!
render jsonapi: @experiment, serializer: ExperimentSerializer, status: :ok
end
private
def experiment_params
raise TypeError unless params.require(:data).require(:type) == 'experiments'
params.require(:data).require(:attributes).permit(:name, :description, :archived)
end
def load_experiment_for_managing
@experiment = @project.experiments.find(params.require(:id))
raise PermissionError.new(Experiment, :manage) unless can_manage_experiment?(@experiment)
end
end
end
end

View file

@ -8,6 +8,7 @@ module Api
load_project(:id)
end
before_action :load_project, only: :activities
before_action :load_project_for_managing, only: %i(update)
def index
projects = @team.projects
@ -22,6 +23,31 @@ module Api
render jsonapi: @project, serializer: ProjectSerializer
end
def create
raise PermissionError.new(Project, :create) unless can_create_projects?(@team)
project = @team.projects.create!(project_params.merge!(created_by: current_user))
render jsonapi: project, serializer: ProjectSerializer, status: :created
end
def update
@project.assign_attributes(project_params)
return render body: nil, status: :no_content unless @project.changed?
if @project.archived_changed?
if @project.archived?
@project.archived_by = current_user
else
@project.restored_by = current_user
end
end
@project.last_modified_by = current_user
@project.save!
render jsonapi: @project, serializer: ProjectSerializer, status: :ok
end
def activities
activities = @project.activities
.page(params.dig(:page, :number))
@ -29,6 +55,19 @@ module Api
render jsonapi: activities,
each_serializer: ActivitySerializer
end
private
def project_params
raise TypeError unless params.require(:data).require(:type) == 'projects'
params.require(:data).require(:attributes).permit(:name, :visibility, :archived)
end
def load_project_for_managing
@project = @team.projects.find(params.require(:id))
raise PermissionError.new(Project, :manage) unless can_manage_project?(@project)
end
end
end
end

View file

@ -104,10 +104,10 @@ module Api
Result.transaction do
@result = @task.results.create!(result_params.merge(user_id: current_user.id))
if @form_multipart_upload
asset = Asset.create!(result_file_params)
asset = Asset.create!(result_file_params.merge({ team_id: @team.id }))
else
blob = create_blob_from_params
asset = Asset.create!(file: blob)
asset = Asset.create!(file: blob, team: @team)
end
ResultAsset.create!(asset: asset, result: @result)
end

View file

@ -16,6 +16,7 @@ module Api
def index
tasks = @experiment.my_modules
.includes(:my_module_status, :my_modules, :my_module_antecessors)
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
@ -29,7 +30,7 @@ module Api
def create
raise PermissionError.new(MyModule, :create) unless can_manage_experiment?(@experiment)
my_module = @experiment.my_modules.create!(task_params)
my_module = @experiment.my_modules.create!(task_params_create)
render jsonapi: my_module, serializer: TaskSerializer,
rte_rendering: render_rte?,
@ -37,7 +38,7 @@ module Api
end
def update
@task.assign_attributes(task_params)
@task.assign_attributes(task_params_update)
if @task.changed? && @task.save!
render jsonapi: @task, serializer: TaskSerializer, status: :ok
@ -56,10 +57,16 @@ module Api
private
def task_params
def task_params_create
raise TypeError unless params.require(:data).require(:type) == 'tasks'
params.require(:data).require(:attributes).permit(%i(name x y description state))
params.require(:data).require(:attributes).permit(%i(name x y description))
end
def task_params_update
raise TypeError unless params.require(:data).require(:type) == 'tasks'
params.require(:data).require(:attributes).permit(%i(name x y description my_module_status_id))
end
def load_task_for_managing

View file

@ -6,6 +6,7 @@ module Api
before_action :load_team
before_action :load_project
before_action :load_user_project, only: :show
before_action :load_user_project_for_managing, only: %i(update destroy)
def index
user_projects = @project.user_projects
@ -23,11 +24,44 @@ module Api
include: :user
end
def create
raise PermissionError.new(Project, :manage) unless can_manage_project?(@project)
user_project = @project.user_projects.create!(user_project_params.merge!(assigned_by: current_user))
render jsonapi: user_project, serializer: UserProjectSerializer, status: :created
end
def update
@user_project.role = user_project_params[:role]
return render body: nil, status: :no_content unless @user_project.changed?
@user_project.assigned_by = current_user
@user_project.save!
render jsonapi: @user_project, serializer: UserProjectSerializer, status: :ok
end
def destroy
@user_project.destroy!
render body: nil
end
private
def load_user_project
@user_project = @project.user_projects.find(params.require(:id))
end
def load_user_project_for_managing
@user_project = @project.user_projects.find(params.require(:id))
raise PermissionError.new(Project, :manage) unless can_manage_project?(@project)
end
def user_project_params
raise TypeError unless params.require(:data).require(:type) == 'user_projects'
params.require(:data).require(:attributes).permit(:user_id, :role)
end
end
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Api
module V1
class WorkflowStatusesController < BaseController
before_action only: :index do
load_workflow(:workflow_id)
end
def index
statuses = @workflow.my_module_statuses
render jsonapi: statuses, each_serializer: WorkflowStatusSerializer
end
end
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Api
module V1
class WorkflowsController < BaseController
before_action only: :show do
load_workflow(:id)
end
def index
workflows = MyModuleStatusFlow.all
render jsonapi: workflows, each_serializer: WorkflowSerializer
end
def show
render jsonapi: @workflow, serializer: WorkflowSerializer
end
end
end
end

View file

@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base
# Sets current team for all controllers
def current_team
Team.find_by_id(current_user.current_team_id)
@current_team ||= current_user.teams.find_by(id: current_user.current_team_id)
end
def to_user_date_format
@ -83,13 +83,12 @@ class ApplicationController < ActionController::Base
private
def update_current_team
current_team = Team.find_by_id(current_user.current_team_id)
if (current_team.nil? || !current_user.is_member_of_team?(current_team)) &&
current_user.teams.count.positive?
return if current_team.present? && current_team.id == current_user.current_team_id
current_user.update(
current_team_id: current_user.teams.first.id
)
if current_user.current_team_id
@current_team = current_user.teams.find_by(id: current_user.current_team_id)
elsif current_user.teams.any?
current_user.update(current_team_id: current_user.teams.first.id)
end
end

View file

@ -5,10 +5,11 @@ class AtWhoController < ApplicationController
before_action :check_users_permissions
def users
users = @team.search_users(@query).limit(Constants::ATWHO_SEARCH_LIMIT + 1)
respond_to do |format|
format.json do
render json: {
users: generate_users_data,
users: [render_to_string(partial: 'shared/smart_annotation/users.html.erb', locals: {users: users})],
status: :ok
}
end
@ -31,7 +32,7 @@ class AtWhoController < ApplicationController
end
def rep_items
repository = Repository.find_by_id(params[:repository_id])
repository = Repository.find_by_id(params[:repository_id]) || Repository.active.accessible_by_teams(@team).first
items =
if repository && can_read_repository?(repository)
SmartAnnotation.new(current_user, current_team, @query)
@ -42,25 +43,21 @@ class AtWhoController < ApplicationController
respond_to do |format|
format.json do
render json: {
res: items,
res: [render_to_string(partial: 'shared/smart_annotation/repository_items.html.erb', locals: {
repository_rows: items
})],
repository: repository.id,
team: current_team.id,
status: :ok
}
end
end
end
def repositories
def menu
repositories = Repository.active.accessible_by_teams(@team)
respond_to do |format|
format.json do
render json: {
repositories: repositories.map do |r|
[r.id, escape_input(r.name.truncate(Constants::ATWHO_REP_NAME_LIMIT))]
end.to_h,
status: :ok
}
end
end
render json: { html: render_to_string({ partial: "shared/smart_annotation/menu.html.erb",
locals: { repositories: repositories } }) }
end
def projects
@ -68,7 +65,10 @@ class AtWhoController < ApplicationController
respond_to do |format|
format.json do
render json: {
res: res.projects,
res: [render_to_string(partial: 'shared/smart_annotation/project_items.html.erb', locals: {
projects: res.projects
})],
team: current_team.id,
status: :ok
}
end
@ -80,7 +80,10 @@ class AtWhoController < ApplicationController
respond_to do |format|
format.json do
render json: {
res: res.experiments,
res: [render_to_string(partial: 'shared/smart_annotation/experiment_items.html.erb', locals: {
experiments: res.experiments
})],
team: current_team.id,
status: :ok
}
end
@ -92,7 +95,10 @@ class AtWhoController < ApplicationController
respond_to do |format|
format.json do
render json: {
res: res.my_modules,
res: [render_to_string(partial: 'shared/smart_annotation/my_module_items.html.erb', locals: {
my_modules: res.my_modules
})],
team: current_team.id,
status: :ok
}
end
@ -110,23 +116,4 @@ class AtWhoController < ApplicationController
def check_users_permissions
render_403 unless can_read_team?(@team)
end
def generate_users_data
# Search users
res = @team.search_users(@query)
.limit(Constants::ATWHO_SEARCH_LIMIT)
.pluck(:id, :full_name, :email)
# Add avatars, Base62, convert to JSON
data = []
res.each do |obj|
tmp = {}
tmp['id'] = obj[0].base62_encode
tmp['full_name'] = escape_input(obj[1].truncate(Constants::NAME_TRUNCATION_LENGTH_DROPDOWN))
tmp['email'] = escape_input(obj[2])
tmp['img_url'] = avatar_path(obj[0], :icon_small)
data << tmp
end
data
end
end

View file

@ -25,7 +25,7 @@ module Dashboard
tasks = tasks.left_outer_joins(:user_my_modules).where(user_my_modules: { user_id: current_user.id })
end
tasks = filter_by_state(tasks)
tasks = tasks.where(my_module_status_id: task_filters[:statuses])
case task_filters[:sort]
when 'start_date'
@ -41,7 +41,9 @@ module Dashboard
end
page = (params[:page] || 1).to_i
tasks = tasks.with_step_statistics.search_by_name(current_user, current_team, task_filters[:query])
tasks = tasks.search_by_name(current_user, current_team, task_filters[:query])
.joins(:my_module_status)
.select('my_modules.*', 'my_module_statuses.name as status_name', 'my_module_statuses.color as status_color')
.preload(experiment: :project).page(page).per(Constants::INFINITE_SCROLL_LIMIT)
tasks_list = tasks.map do |task|
@ -50,9 +52,9 @@ module Dashboard
experiment: escape_input(task.experiment.name),
project: escape_input(task.experiment.project.name),
name: escape_input(task.name),
due_date: task.due_date.present? ? I18n.l(task.due_date, format: :full_date) : nil,
state: task_state(task),
steps_precentage: task.steps_completed_percentage }
due_date: prepare_due_date(task),
status_color: task.status_color,
status_name: task.status_name }
end
render json: { data: tasks_list, next_page: tasks.next_page }
@ -90,31 +92,28 @@ module Dashboard
private
def task_state(task)
if task.state == 'completed'
task_state_class = task.state
task_state_text = t('dashboard.current_tasks.progress_bar.completed')
else
task_state_text = t('dashboard.current_tasks.progress_bar.in_progress')
task_state_class = 'day-prior' if task.is_one_day_prior?
if task.is_overdue?
task_state_text = t('dashboard.current_tasks.progress_bar.overdue')
task_state_class = 'overdue'
end
if task.steps_total.positive?
task_state_text += t('dashboard.current_tasks.progress_bar.completed_steps',
steps: task.steps_completed, total_steps: task.steps_total)
end
def prepare_due_date(task)
if task.completed?
return { state: '', text: I18n.t('dashboard.current_tasks.completed_on_html',
date: I18n.l(task.completed_on, format: :full_date)) }
end
{ text: task_state_text, class: task_state_class }
end
if task.due_date.present?
due_date_formatted = I18n.l(task.due_date, format: :full_date)
if task.is_overdue?
return { state: 'overdue', text: I18n.t('dashboard.current_tasks.due_date_overdue_html',
date: due_date_formatted) }
elsif task.is_one_day_prior?
return { state: 'day-prior', text: I18n.t('dashboard.current_tasks.due_date_html',
date: due_date_formatted) }
end
def filter_by_state(tasks)
tasks.where(my_modules: { state: task_filters[:view] })
return { state: '', text: I18n.t('dashboard.current_tasks.due_date_html', date: due_date_formatted) }
end
{ state: nil, text: nil }
end
def task_filters
params.permit(:project_id, :experiment_id, :mode, :view, :sort, :query, :page)
params.permit(:project_id, :experiment_id, :mode, :sort, :query, :page, statuses: [])
end
def load_project

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
class DashboardsController < ApplicationController
def show; end
def show
@my_module_status_flows = MyModuleStatusFlow.all.preload(my_module_statuses: :my_module_status_consequences)
end
end

View file

@ -123,7 +123,7 @@ class MyModuleRepositoriesController < ApplicationController
Activities::CreateActivityService.call(
activity_type: :export_inventory_items_assigned_to_task,
owner: current_user,
subject: @repository,
subject: @my_module,
team: current_team,
message_items: {
my_module: @my_module.id,

View file

@ -31,7 +31,8 @@ class MyModuleRepositorySnapshotsController < ApplicationController
end
def create
repository_snapshot = @repository.provision_snapshot(@my_module, current_user)
repository_snapshot = RepositorySnapshot.create_preliminary(@repository, @my_module, current_user)
RepositorySnapshotProvisioningJob.perform_later(repository_snapshot)
render json: {
html: render_to_string(partial: 'my_modules/repositories/full_view_version',
@ -108,7 +109,7 @@ class MyModuleRepositorySnapshotsController < ApplicationController
Activities::CreateActivityService.call(
activity_type: :export_inventory_snapshot_items_assigned_to_task,
owner: current_user,
subject: @repository_snapshot,
subject: @my_module,
team: current_team,
message_items: {
my_module: @my_module.id,

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
class MyModuleStatusFlowController < ApplicationController
before_action :load_my_module
before_action :check_view_permissions
def show
my_module_statuses = @my_module.my_module_status_flow
.my_module_statuses
.preload(:my_module_status_implications, next_status: :my_module_status_conditions)
.sort_by_position
render json: { html: render_to_string(partial: 'my_modules/modals/status_flow_modal_body.html.erb',
locals: { my_module_statuses: my_module_statuses }) }
end
private
def load_my_module
@my_module = MyModule.find_by(id: params[:my_module_id])
render_404 unless @my_module
end
def check_view_permissions
render_403 unless can_read_experiment?(@my_module.experiment)
end
end

View file

@ -3,7 +3,7 @@ class MyModuleTagsController < ApplicationController
before_action :load_vars, except: :canvas_index
before_action :check_view_permissions, only: :index
before_action :check_manage_permissions, only: %i(create index_edit destroy)
before_action :check_manage_permissions, only: %i(create index_edit destroy destroy_by_tag_id)
def index_edit
@my_module_tags = @my_module.my_module_tags.order(:id)
@ -155,11 +155,7 @@ class MyModuleTagsController < ApplicationController
end
def check_manage_permissions
render_403 unless can_manage_tags?(@my_module.experiment.project)
end
def init_gui
@tags = @my_module.unassigned_tags
render_403 unless can_manage_module?(@my_module)
end
def mt_params

View file

@ -10,9 +10,8 @@ class MyModulesController < ApplicationController
before_action :load_projects_tree, only: %i(protocols results activities archive)
before_action :check_archive_and_restore_permissions, only: %i(update)
before_action :check_manage_permissions, only: %i(description due_date update_description update_protocol_description)
before_action :check_view_permissions, except: %i(update update_description update_protocol_description
toggle_task_state)
before_action :check_complete_module_permission, only: %i(complete_my_module toggle_task_state)
before_action :check_view_permissions, except: %i(update update_description update_protocol_description)
before_action :check_update_state_permissions, only: :update_state
before_action :set_inline_name_editing, only: %i(protocols results activities archive)
layout 'fluid'.freeze
@ -45,6 +44,14 @@ class MyModulesController < ApplicationController
end
end
def status_state
respond_to do |format|
format.json do
render json: { status_changing: @my_module.status_changing? }
end
end
end
def activities
params[:subjects] = {
MyModule: [@my_module.id]
@ -126,6 +133,8 @@ class MyModulesController < ApplicationController
log_activity(:restore_module)
end
else
render_403 && return unless can_manage_module?(@my_module)
saved = @my_module.save
if saved
if description_changed
@ -258,100 +267,23 @@ class MyModulesController < ApplicationController
def archive
@archived_results = @my_module.archived_results
current_team_switch(@my_module
.experiment
.project
.team)
current_team_switch(@my_module.experiment.project.team)
end
def update_state
old_status_id = @my_module.my_module_status_id
if @my_module.update(my_module_status_id: update_status_params[:status_id])
log_activity(:change_status_on_task_flow, @my_module, my_module_status_old: old_status_id,
my_module_status_new: @my_module.my_module_status.id)
# Complete/uncomplete task
def toggle_task_state
respond_to do |format|
@my_module.completed? ? @my_module.uncompleted! : @my_module.completed!
task_completion_activity
# Render new button HTML
new_btn_partial = if @my_module.completed?
'my_modules/state_button_uncomplete.html.erb'
else
'my_modules/state_button_complete.html.erb'
end
format.json do
render json: {
new_btn: render_to_string(partial: new_btn_partial),
completed: @my_module.completed?,
module_header_due_date: render_to_string(
partial: 'my_modules/module_header_due_date.html.erb',
locals: { my_module: @my_module }
),
module_state_label: render_to_string(
partial: 'my_modules/module_state_label.html.erb',
locals: { my_module: @my_module }
)
}
end
end
end
def complete_my_module
respond_to do |format|
if @my_module.uncompleted? && @my_module.check_completness_status
@my_module.completed!
task_completion_activity
format.json do
render json: {
task_button_title: t('my_modules.buttons.uncomplete'),
module_header_due_date: render_to_string(
partial: 'my_modules/module_header_due_date.html.erb',
locals: { my_module: @my_module }
),
module_state_label: render_to_string(
partial: 'my_modules/module_state_label.html.erb',
locals: { my_module: @my_module }
)
}, status: :ok
end
else
format.json { render json: {}, status: :unprocessable_entity }
end
return redirect_to protocols_my_module_path(@my_module)
else
render json: { errors: @my_module.errors.messages.values.flatten.join('\n') }, status: :unprocessable_entity
end
end
private
def task_completion_activity
completed = @my_module.completed?
log_activity(completed ? :complete_task : :uncomplete_task)
start_work_on_next_task_notification
end
def start_work_on_next_task_notification
if @my_module.completed?
title = t('notifications.start_work_on_next_task',
user: current_user.full_name,
module: @my_module.name)
message = t('notifications.start_work_on_next_task_message',
project: link_to(@project.name, project_url(@project)),
experiment: link_to(@experiment.name,
canvas_experiment_url(@experiment)),
my_module: link_to(@my_module.name,
protocols_my_module_url(@my_module)))
notification = Notification.create(
type_of: :recent_changes,
title: sanitize_input(title, %w(strong a)),
message: sanitize_input(message, %w(strong a)),
generator_user_id: current_user.id
)
# create notification for all users on the next modules in the workflow
@my_module.my_modules.map(&:users).flatten.uniq.each do |target_user|
next if target_user == current_user || !target_user.recent_notification
UserNotification.create(notification: notification, user: target_user)
end
end
end
def load_vars
@my_module = MyModule.find_by_id(params[:id])
if @my_module
@ -384,8 +316,9 @@ class MyModulesController < ApplicationController
render_403 unless can_read_experiment?(@my_module.experiment)
end
def check_complete_module_permission
render_403 unless can_complete_module?(@my_module)
def check_update_state_permissions
return render_403 unless can_change_my_module_flow_status?(@my_module)
render_404 unless @my_module.my_module_status
end
def set_inline_name_editing
@ -414,6 +347,10 @@ class MyModulesController < ApplicationController
update_params
end
def update_status_params
params.require(:my_module).permit(:status_id)
end
def log_start_date_change_activity(start_date_changes)
type_of = if start_date_changes[0].nil? # set started_on
message_items = { my_module_started_on: @my_module.started_on }

View file

@ -3,6 +3,7 @@ class ProjectsController < ApplicationController
include TeamsHelper
include InputSanitizeHelper
before_action :switch_team_with_param, only: :index
before_action :load_vars, only: %i(show edit update
notifications reports
experiment_archive)
@ -34,7 +35,6 @@ class ProjectsController < ApplicationController
}
end
format.html do
current_team_switch(Team.find_by_id(params[:team])) if params[:team]
@teams = current_user.teams
# New project for create new project modal
@project = Project.new

View file

@ -325,7 +325,7 @@ class ProtocolsController < ApplicationController
@protocol.unlink
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
raise ActiveRecord::Rollback
end
end
@ -353,13 +353,11 @@ class ProtocolsController < ApplicationController
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
begin
# Revert is basically update from parent
@protocol.update_from_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
# Revert is basically update from parent
@protocol.update_from_parent(current_user)
rescue StandardError
transaction_error = true
raise ActiveRecord::Rollback
end
if transaction_error
@ -397,12 +395,10 @@ class ProtocolsController < ApplicationController
if @protocol.parent.can_destroy?
transaction_error = false
Protocol.transaction do
begin
@protocol.update_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
@protocol.update_parent(current_user)
rescue StandardError
transaction_error = true
raise ActiveRecord::Rollback
end
if transaction_error
@ -440,12 +436,10 @@ class ProtocolsController < ApplicationController
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
begin
@protocol.update_from_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
@protocol.update_from_parent(current_user)
rescue StandardError
transaction_error = true
raise ActiveRecord::Rollback
end
if transaction_error
@ -483,12 +477,10 @@ class ProtocolsController < ApplicationController
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
begin
@protocol.load_from_repository(@source, current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
@protocol.load_from_repository(@source, current_user)
rescue StandardError
transaction_error = true
raise ActiveRecord::Rollback
end
if transaction_error
@ -1140,7 +1132,7 @@ class ProtocolsController < ApplicationController
@source = Protocol.find_by_id(params[:source_id])
render_403 unless @protocol.present? && @source.present? &&
(can_manage_protocol_in_module?(@protocol) ||
(can_manage_protocol_in_module?(@protocol) &&
can_read_protocol_in_repository?(@source))
end

View file

@ -10,7 +10,7 @@ class StepsController < ApplicationController
before_action :convert_table_contents_to_utf8, only: %i(create update)
before_action :check_view_permissions, only: %i(show update_view_state)
before_action :check_manage_permissions, only: %i(new create edit update destroy move_up move_down)
before_action :check_manage_permissions, only: %i(new create edit update destroy move_up move_down toggle_step_state)
before_action :check_complete_and_checkbox_permissions, only: %i(toggle_step_state checklistitem_state)
def new
@ -307,10 +307,6 @@ class StepsController < ApplicationController
@step.completed = completed
if @step.save
if @protocol.in_module?
ready_to_complete = @protocol.my_module.check_completness_status
end
# Create activity
if changed
completed_steps = @protocol.steps.where(completed: true).count
@ -336,14 +332,7 @@ class StepsController < ApplicationController
t('protocols.steps.options.uncomplete_title')
end
format.json do
if ready_to_complete && @protocol.my_module.uncompleted?
render json: {
task_ready_to_complete: true,
new_title: localized_title
}, status: :ok
else
render json: { new_title: localized_title }, status: :ok
end
render json: { new_title: localized_title }, status: :ok
end
else
format.json { render json: {}, status: :unprocessable_entity }
@ -354,16 +343,11 @@ class StepsController < ApplicationController
def move_up
respond_to do |format|
format.json do
if @step.protocol.steps.minimum(:position) != @step.position
@step.update!(position: @step.position - 1)
@step.move_up
render json: {
step_up_position: @step.position,
step_down_position: @step.position + 1
}
else
render json: {}
end
render json: {
steps_order: @protocol.steps.order(:position).select(:id, :position)
}
end
end
end
@ -371,16 +355,11 @@ class StepsController < ApplicationController
def move_down
respond_to do |format|
format.json do
if @step.protocol.steps.maximum(:position) != @step.position
@step.update!(position: @step.position + 1)
@step.move_down
render json: {
step_up_position: @step.position - 1,
step_down_position: @step.position
}
else
render json: {}
end
render json: {
steps_order: @protocol.steps.order(:position).select(:id, :position)
}
end
end
end
@ -490,6 +469,7 @@ class StepsController < ApplicationController
item_record = ck.checklist_items.find_by(id: item[1][:id])
next unless item_record
item_record.update_attribute('position', item[1][:position])
end
end

View file

@ -2,6 +2,7 @@ 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)
@ -153,6 +154,12 @@ class TagsController < ApplicationController
end
end
def check_manage_my_module_permissions
my_module = MyModule.find_by id: params[:my_module_id]
render_403 if my_module && !can_manage_module?(my_module)
end
def check_manage_permissions
render_403 unless can_manage_tags?(@project)
end

View file

@ -121,10 +121,6 @@ class UserMyModulesController < ApplicationController
render_403 unless can_manage_users_in_module?(@my_module)
end
def init_gui
@users = @my_module.unassigned_users
end
def um_params
params.require(:user_my_module).permit(:user_id, :my_module_id)
end

View file

@ -3,7 +3,7 @@ class UserProjectsController < ApplicationController
include InputSanitizeHelper
before_action :load_vars
before_action :load_up_var, only: %i(update destroy)
before_action :load_user_project, only: %i(update destroy)
before_action :check_view_permissions, only: :index
before_action :check_manage_users_permissions, only: :index_edit
before_action :check_create_permissions, only: :create
@ -26,9 +26,9 @@ class UserProjectsController < ApplicationController
end
def index_edit
@users = @project.user_projects
@user_projects = @project.user_projects
@unassigned_users = @project.unassigned_users
@up = UserProject.new(project: @project)
@new_user_project = UserProject.new(project: @project)
respond_to do |format|
format.json do
@ -48,10 +48,10 @@ class UserProjectsController < ApplicationController
end
def create
@up = UserProject.new(up_params.merge(project: @project))
@up.assigned_by = current_user
@user_project = @project.user_projects.new(user_project_params)
@user_project.assigned_by = current_user
if @up.save
if @user_project.save
log_activity(:assign_user_to_project)
respond_to do |format|
@ -61,23 +61,23 @@ class UserProjectsController < ApplicationController
end
else
error = t('user_projects.create.can_add_user_to_project')
error = t('user_projects.create.select_user_role') unless @up.role
error = t('user_projects.create.select_user_role') unless @user_project.role
respond_to do |format|
format.json {
render :json => {
format.json do
render json: {
status: 'error',
error: error
}
}
end
end
end
end
def update
@up.role = up_params[:role]
@user_project.role = user_project_params[:role]
if @up.save
if @user_project.save
log_activity(:change_user_role_on_project)
respond_to do |format|
@ -90,7 +90,7 @@ class UserProjectsController < ApplicationController
format.json do
render json: {
status: 'error',
errors: @up.errors
errors: @user_project.errors
}
end
end
@ -98,20 +98,20 @@ class UserProjectsController < ApplicationController
end
def destroy
if @up.destroy
if @user_project.destroy
log_activity(:unassign_user_from_project)
respond_to do |format|
format.json do
redirect_to project_users_edit_path(format: :json),
turbolinks: false,
status: 303
status: :see_other
end
end
else
respond_to do |format|
format.json do
render json: {
errors: @up.errors
errors: @user_project.errors
}
end
end
@ -121,13 +121,13 @@ class UserProjectsController < ApplicationController
private
def load_vars
@project = Project.find_by_id(params[:project_id])
@project = Project.find_by(id: params[:project_id])
render_404 unless @project
end
def load_up_var
@up = UserProject.find(params[:id])
render_404 unless @up
def load_user_project
@user_project = @project.user_projects.find(params[:id])
render_404 unless @user_project
end
def check_view_permissions
@ -139,19 +139,14 @@ class UserProjectsController < ApplicationController
end
def check_create_permissions
render_403 unless can_create_projects?(current_team)
render_403 unless can_manage_project?(@project)
end
def check_manage_permissions
render_403 unless can_manage_project?(@project) &&
@up.user_id != current_user.id
render_403 unless can_manage_project?(@project) && @user_project.user_id != current_user.id
end
def init_gui
@users = @project.unassigned_users
end
def up_params
def user_project_params
params.require(:user_project).permit(:user_id, :project_id, :role)
end
@ -163,7 +158,7 @@ class UserProjectsController < ApplicationController
team: @project.team,
project: @project,
message_items: { project: @project.id,
user_target: @up.user.id,
role: @up.role_str })
user_target: @user_project.user.id,
role: @user_project.role_str })
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
module ExperimentsHelper
def grouped_by_prj(experiments)
ungrouped_experiments = experiments.joins(:project)
.select('projects.name as project_name,
projects.archived as project_archived,
experiments.*')
ungrouped_experiments.group_by { |i| [i[:project_name]] }.map do |group, exps|
{
project_name: group[0],
project_archived: exps[0]&.project_archived,
experiments: exps
}
end
end
end

View file

@ -31,11 +31,9 @@ module MyModulesHelper
def get_task_alert_color(my_module)
alert = ''
if !my_module.completed?
unless my_module.completed?
alert = ' alert-yellow' if my_module.is_one_day_prior?
alert = ' alert-red' if my_module.is_overdue?
elsif my_module.completed?
alert = ' alert-green'
end
alert
end

View file

@ -154,7 +154,7 @@ module ReportsHelper
style = 'default'
text = t('protocols.steps.uncompleted')
end
"<span class=\"label label-#{style}\">#{text}</span>".html_safe
"<span class=\"label step-label-#{style}\">[#{text}]</span>".html_safe
end
# Fixes issues with avatar images in reports

View file

@ -1,9 +1,10 @@
module TeamsHelper
# resets the current team if needed
def current_team_switch(team)
if team != current_team
if team != current_team && current_user.is_member_of_team?(team)
current_user.current_team_id = team.id
current_user.save
update_current_team
end
end
@ -17,11 +18,7 @@ module TeamsHelper
end
end
def team_created_by(team)
User.find_by_id(team.created_by_id)
end
def switch_team_with_param
current_team_switch(Team.find_by(id: params[:team])) if params[:team]
current_team_switch(current_user.teams.find_by(id: params[:team])) if params[:team]
end
end

View file

@ -0,0 +1 @@
require('typeface-lato');

View file

@ -6,7 +6,9 @@ class ActiveStorage::PreviewJob < ActiveStorage::BaseJob
discard_on StandardError do |job, error|
blob = ActiveStorage::Blob.find_by(id: job.arguments.first)
blob&.attachments&.take&.record&.update(file_processing: false)
ActiveRecord::Base.no_touching do
blob&.attachments&.take&.record&.update(file_processing: false)
end
Rails.logger.error "Couldn't generate preview for Blob with id: #{job.arguments.first}. Error:\n #{error}"
end
@ -24,6 +26,8 @@ class ActiveStorage::PreviewJob < ActiveStorage::BaseJob
Rails.logger.info "Preview for the Blod with id: #{blob.id} - successfully generated.\n" \
"Transformations applied: #{preview.variation.transformations}"
blob.attachments.take.record.update(file_processing: false)
ActiveRecord::Base.no_touching do
blob.attachments.take.record.update(file_processing: false)
end
end
end

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
class MyModuleStatusConsequencesJob < ApplicationJob
queue_as :high_priority
def perform(my_module, my_module_status_consequences)
error_raised = false
my_module.transaction do
my_module_status_consequences.each do |consequence|
consequence.call(my_module)
end
my_module.update!(status_changing: false)
rescue StandardError => e
Rails.logger.error(e.message)
Rails.logger.error(e.backtrace.join("\n"))
error_raised = true
end
if error_raised
my_module.my_module_status = my_module.changing_from_my_module_status
my_module.status_changing = false
my_module.save!
end
end
end

View file

@ -32,17 +32,15 @@ class Asset < ApplicationRecord
optional: true
belongs_to :team, optional: true
has_one :step_asset, inverse_of: :asset, dependent: :destroy
has_one :step, through: :step_asset, dependent: :nullify
has_one :step, through: :step_asset, touch: true, dependent: :nullify
has_one :result_asset, inverse_of: :asset, dependent: :destroy
has_one :result, through: :result_asset, dependent: :nullify
has_one :result, through: :result_asset, touch: true, dependent: :nullify
has_one :repository_asset_value, inverse_of: :asset, dependent: :destroy
has_one :repository_cell, through: :repository_asset_value,
dependent: :nullify
has_many :report_elements, inverse_of: :asset, dependent: :destroy
has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy
after_save { result&.touch; step&.touch }
attr_accessor :file_content, :file_info, :in_template
def self.search(
@ -222,8 +220,7 @@ class Asset < ApplicationRecord
Rails.logger.info "Asset #{id}: Creating extract text job"
# The extract_asset_text also includes
# estimated size calculation
Asset.delay(queue: :assets, run_at: 20.minutes.from_now)
.extract_asset_text_delayed(id, in_template)
Asset.delay(queue: :assets).extract_asset_text_delayed(id, in_template)
elsif marvinjs?
extract_asset_text
else

View file

@ -15,7 +15,7 @@ module ArchivableModel
# Helper for archiving project. Timestamp of archiving is handler by
# before_save callback.
# Sets the archived_by value to the current user.
def archive (current_user)
def archive(current_user)
self.archived = true
self.archived_by = current_user
save
@ -29,7 +29,7 @@ module ArchivableModel
# Helper for restoring project from archive.
# Sets the restored_by value to the current user.
def restore (current_user)
def restore(current_user)
self.archived = false
self.restored_by = current_user
save

View file

@ -2,20 +2,30 @@
module SearchableByNameModel
extend ActiveSupport::Concern
# rubocop:disable Metrics/BlockLength
included do
def self.search_by_name(user, teams = [], query = nil, options = {})
return if user.blank? || teams.blank?
viewable_by_user(user, teams)
.where_attributes_like("#{table_name}.name", query, options)
.limit(Constants::SEARCH_LIMIT)
sql_q = viewable_by_user(user, teams)
if options[:intersect]
query_array = query.gsub(/[[:space:]]+/, ' ').split(' ')
query_array.each do |string|
sql_q = sql_q.where("trim_html_tags(#{table_name}.name) ILIKE ?", "%#{string}%")
end
else
sql_q = sql_q.where_attributes_like("#{table_name}.name", query, options)
end
sql_q.limit(Constants::SEARCH_LIMIT)
end
def self.filter_by_teams(teams = [])
return self if teams.empty?
if column_names.include? 'team_id'
return where(team_id: teams)
where(team_id: teams)
else
valid_subjects = Extends::ACTIVITY_SUBJECT_CHILDREN
parent_array = [to_s.underscore]
@ -38,8 +48,9 @@ module SearchableByNameModel
query = child.to_s.camelize.constantize.where("#{last_parent}_id" => query)
last_parent = child
end
return where("#{last_parent}_id" => query)
where("#{last_parent}_id" => query)
end
end
end
# rubocop:enable Metrics/BlockLength
end

View file

@ -248,12 +248,6 @@ class Experiment < ApplicationRecord
private
# Archive all modules. Receives an array of module integer IDs.
def archive_modules(module_ids)
my_modules.where(id: module_ids).each(&:archive!)
my_modules.reload
end
# Archive all modules. Receives an array of module integer IDs
# and current user.
def archive_modules(module_ids, current_user)

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class MyModule < ApplicationRecord
include ArchivableModel
include SearchableModel
@ -7,9 +9,11 @@ class MyModule < ApplicationRecord
enum state: Extends::TASKS_STATES
before_create :create_blank_protocol
before_validation :set_completed_on, if: :state_changed?
before_create :assign_default_status_flow
auto_strip_attributes :name, :description, nullify: false
around_save :exec_status_consequences, if: :my_module_status_id_changed?
auto_strip_attributes :name, :description, nullify: false, if: proc { |mm| mm.name_changed? || mm.description_changed? }
validates :name,
length: { minimum: Constants::NAME_MIN_LENGTH,
maximum: Constants::NAME_MAX_LENGTH }
@ -20,12 +24,19 @@ class MyModule < ApplicationRecord
validate :coordinates_uniqueness_check, if: :active?
validates :completed_on, presence: true, if: proc { |mm| mm.completed? }
validate :check_status, if: :my_module_status_id_changed?
validate :check_status_conditions, if: :my_module_status_id_changed?
validate :check_status_implications
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', optional: true
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true
belongs_to :archived_by, foreign_key: 'archived_by_id', class_name: 'User', optional: true
belongs_to :restored_by, foreign_key: 'restored_by_id', class_name: 'User', optional: true
belongs_to :experiment, inverse_of: :my_modules, touch: true
belongs_to :my_module_group, inverse_of: :my_modules, optional: true
belongs_to :my_module_status, optional: true
belongs_to :changing_from_my_module_status, optional: true, class_name: 'MyModuleStatus'
delegate :my_module_status_flow, to: :my_module_status, allow_nil: true
has_many :results, inverse_of: :my_module, dependent: :destroy
has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy
has_many :tags, through: :my_module_tags
@ -55,16 +66,6 @@ class MyModule < ApplicationRecord
end)
scope :workflow_ordered, -> { order(workflow_order: :asc) }
scope :uncomplete, -> { where(state: 'uncompleted') }
scope :with_step_statistics, (lambda do
left_outer_joins(protocols: :steps)
.group(:id)
.select('my_modules.*')
.select('COUNT(steps.id) AS steps_total')
.select('COUNT(steps.id) FILTER (where steps.completed = true) AS steps_completed')
.select('CASE COUNT(steps.id) WHEN 0 THEN 0 ELSE'\
'((COUNT(steps.id) FILTER (where steps.completed = true)) * 100 / COUNT(steps.id)) '\
'END AS steps_completed_percentage')
end)
# A module takes this much space in canvas (x, y) in database
WIDTH = 30
@ -139,14 +140,16 @@ class MyModule < ApplicationRecord
# Remove association with module group.
self.my_module_group = nil
was_archived = false
MyModule.transaction do
archived = super
was_archived = super
# Remove all connection between modules.
archived = Connection.where(input_id: id).delete_all if archived
archived = Connection.where(output_id: id).delete_all if archived
raise ActiveRecord::Rollback unless archived
was_archived = Connection.where(input_id: id).destroy_all if was_archived
was_archived = Connection.where(output_id: id).destroy_all if was_archived
raise ActiveRecord::Rollback unless was_archived
end
archived
was_archived
end
# Similar as super restore, but also calculate new module position
@ -393,6 +396,8 @@ class MyModule < ApplicationRecord
clone.save!
clone.assign_user(current_user)
# Remove the automatically generated protocol,
# & clone the protocol instead
clone.protocol.destroy
@ -436,18 +441,6 @@ class MyModule < ApplicationRecord
{ x: 0, y: positions.last[1] + HEIGHT }
end
# Check if my_module is ready to become completed
def check_completness_status
if protocol && protocol.steps.count > 0
completed = true
protocol.steps.find_each do |step|
completed = false unless step.completed
end
return true if completed
end
false
end
def assign_user(user, assigned_by = nil)
user_my_modules.create(
assigned_by: assigned_by || user,
@ -465,12 +458,6 @@ class MyModule < ApplicationRecord
private
def set_completed_on
return if completed? && completed_on.present?
self.completed_on = completed? ? DateTime.now : nil
end
def create_blank_protocol
protocols << Protocol.new_blank_for_module(self)
end
@ -480,4 +467,54 @@ class MyModule < ApplicationRecord
errors.add(:position, I18n.t('activerecord.errors.models.my_module.attributes.position.not_unique'))
end
end
def assign_default_status_flow
return if my_module_status.present? || MyModuleStatusFlow.global.blank?
self.my_module_status = MyModuleStatusFlow.global.first.initial_status
end
def check_status_conditions
return if my_module_status.blank?
my_module_status.my_module_status_conditions.each do |condition|
condition.call(self)
end
end
def check_status_implications
return if my_module_status.blank?
my_module_status.my_module_status_implications.each do |implication|
implication.call(self)
end
end
def check_status
return unless my_module_status_id_was
original_status = MyModuleStatus.find_by(id: my_module_status_id_was)
unless my_module_status && [original_status.next_status, original_status.previous_status].include?(my_module_status)
errors.add(:my_module_status_id,
I18n.t('activerecord.errors.models.my_module.attributes.my_module_status_id.not_correct_order'))
end
end
def exec_status_consequences
return if my_module_status.blank? || status_changing
self.changing_from_my_module_status_id = my_module_status_id_was if my_module_status_id_was.present?
self.status_changing = true
yield
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)
else
my_module_status.my_module_status_consequences.each do |consequence|
consequence.call(self)
end
update!(status_changing: false)
end
end
end

View file

@ -0,0 +1,70 @@
# frozen_string_literal: true
class MyModuleStatus < ApplicationRecord
has_many :my_modules, dependent: :nullify
has_many :my_module_status_conditions, dependent: :destroy
has_many :my_module_status_consequences, dependent: :destroy
has_many :my_module_status_implications, dependent: :destroy
belongs_to :my_module_status_flow
belongs_to :created_by, class_name: 'User', optional: true
belongs_to :last_modified_by, class_name: 'User', optional: true
has_one :next_status, class_name: 'MyModuleStatus',
foreign_key: 'previous_status_id',
inverse_of: :previous_status,
dependent: :nullify
belongs_to :previous_status, class_name: 'MyModuleStatus', inverse_of: :next_status, optional: true
validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH, maximum: Constants::NAME_MAX_LENGTH }
validates :color, presence: true
validates :description, length: { maximum: Constants::TEXT_MAX_LENGTH }
validates :next_status, uniqueness: true, if: -> { next_status.present? }
validates :previous_status, uniqueness: true, if: -> { previous_status.present? }
validate :next_in_same_flow, if: -> { next_status.present? }
validate :previous_in_same_flow, if: -> { previous_status.present? }
def initial_status?
my_module_status_flow.initial_status == self
end
def final_status?
my_module_status_flow.final_status == self
end
def self.sort_by_position(order = :asc)
ordered_statuses, statuses = all.to_a.partition { |i| i.previous_status_id.nil? }
return [] if ordered_statuses.empty?
until statuses.empty?
next_element, statuses = statuses.partition { |i| ordered_statuses.last.id == i.previous_status_id }
if next_element.empty?
break
else
ordered_statuses.concat(next_element)
end
end
ordered_statuses = ordered_statuses.reverse if order == :desc
ordered_statuses
end
def conditions_errors(my_module)
mm_copy = my_module.clone
mm_copy.errors.clear
my_module_status_conditions.each do |condition|
condition.call(mm_copy)
end
mm_copy.errors.messages&.values&.flatten
end
private
def next_in_same_flow
errors.add(:next_status, :different_flow) unless next_status.my_module_status_flow == my_module_status_flow
end
def previous_in_same_flow
errors.add(:previous_status, :different_flow) unless previous_status.my_module_status_flow == my_module_status_flow
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class MyModuleStatusCondition < ApplicationRecord
belongs_to :my_module_status
def description
''
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
# Just an example, to be replaced with an actual implementation
module MyModuleStatusConditions
class Active < MyModuleStatusCondition
def call(my_module)
my_module.errors.add(:status_conditions, I18n.t('my_module_statuses.conditions.error.my_module_not_active')) unless my_module.active?
end
def description
I18n.t('my_module_statuses.conditions.error.my_module_not_active')
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class MyModuleStatusConsequence < ApplicationRecord
belongs_to :my_module_status
def runs_in_background?
false
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
# Just an example, to be replaced with an actual implementation
module MyModuleStatusConsequences
class ChangeActivity < MyModuleStatusConsequence
def call(my_module)
# Create new activity here
puts "State changed to #{my_module_status.name}} for #{my_module.name}"
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Just an example, to be replaced with an actual implementation
module MyModuleStatusConsequences
class Completion < MyModuleStatusConsequence
def call(my_module)
my_module.state = 'completed'
my_module.completed_on = DateTime.now
my_module.save!
end
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
module MyModuleStatusConsequences
class RepositorySnapshot < MyModuleStatusConsequence
def runs_in_background?
true
end
def call(my_module)
my_module.assigned_repositories.each do |repository|
repository_snapshot = ::RepositorySnapshot.create_preliminary(repository, my_module)
service = Repositories::SnapshotProvisioningService.call(repository_snapshot: repository_snapshot)
unless service.succeed?
repository_snapshot.failed!
raise StandardError, service.errors
end
end
end
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
# Just an example, to be replaced with an actual implementation
module MyModuleStatusConsequences
class Uncompletion < MyModuleStatusConsequence
def call(my_module)
return unless my_module.state == 'completed'
my_module.state = 'uncompleted'
my_module.completed_on = nil
my_module.save!
end
end
end

View file

@ -0,0 +1,42 @@
# frozen_string_literal: true
class MyModuleStatusFlow < ApplicationRecord
enum visibility: { global: 0, in_team: 1 }
has_many :my_module_statuses, dependent: :destroy
belongs_to :team, optional: true
belongs_to :created_by, class_name: 'User', optional: true
belongs_to :last_modified_by, class_name: 'User', optional: true
validates :visibility, presence: true
validates :team, presence: true, if: :in_team?
validates :name, uniqueness: { scope: :team_id, case_sensitive: false }, if: :in_team?
validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH, maximum: Constants::NAME_MAX_LENGTH }
validates :description, length: { maximum: Constants::TEXT_MAX_LENGTH }
def initial_status
my_module_statuses.find_by(previous_status: nil)
end
def final_status
my_module_statuses.left_outer_joins(:next_status).find_by('next_statuses_my_module_statuses.id': nil)
end
def self.ensure_default
return if MyModuleStatusFlow.global.any?
status_flow = MyModuleStatusFlow.create!(name: Extends::DEFAULT_FLOW_NAME, visibility: :global)
prev_id = nil
Extends::DEFAULT_FLOW_STATUSES.each do |status|
new_status = MyModuleStatus.create!(my_module_status_flow: status_flow,
name: status[:name],
color: status[:color],
previous_status_id: prev_id)
prev_id = new_status.id
status[:conditions]&.each { |condition| condition.constantize.create!(my_module_status: new_status) }
status[:implications]&.each { |implication| implication.constantize.create!(my_module_status: new_status) }
status[:consequences]&.each { |consequence| consequence.constantize.create!(my_module_status: new_status) }
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class MyModuleStatusImplication < ApplicationRecord
belongs_to :my_module_status
def description
''
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
# Just an example, to be replaced with an actual implementation
module MyModuleStatusImplications
class ReadOnly < MyModuleStatusImplication
def call(my_module)
my_module.errors.add(:status_implication, 'Is read only')
false
end
end
end

View file

@ -229,14 +229,16 @@ class Protocol < ApplicationRecord
# Deep-clone given array of assets
def self.deep_clone_assets(assets_to_clone)
assets_to_clone.each do |src_id, dest_id|
src = Asset.find_by(id: src_id)
dest = Asset.find_by(id: dest_id)
dest.destroy! if src.blank? && dest.present?
next unless src.present? && dest.present?
ActiveRecord::Base.no_touching do
assets_to_clone.each do |src_id, dest_id|
src = Asset.find_by(id: src_id)
dest = Asset.find_by(id: dest_id)
dest.destroy! if src.blank? && dest.present?
next unless src.present? && dest.present?
# Clone file
src.duplicate_file(dest)
# Clone file
src.duplicate_file(dest)
end
end
end
@ -524,12 +526,14 @@ class Protocol < ApplicationRecord
end
def update_parent(current_user)
# First, destroy parent's step contents
parent.destroy_contents
parent.reload
ActiveRecord::Base.no_touching do
# First, destroy parent's step contents
parent.destroy_contents
parent.reload
# Now, clone step contents
Protocol.clone_contents(self, parent, current_user, false)
# Now, clone step contents
Protocol.clone_contents(self, parent, current_user, false)
end
# Lastly, update the metadata
parent.reload
@ -542,11 +546,13 @@ class Protocol < ApplicationRecord
end
def update_from_parent(current_user)
# First, destroy step contents
destroy_contents
ActiveRecord::Base.no_touching do
# First, destroy step contents
destroy_contents
# Now, clone parent's step contents
Protocol.clone_contents(parent, self, current_user, false)
# Now, clone parent's step contents
Protocol.clone_contents(parent, self, current_user, false)
end
# Lastly, update the metadata
reload
@ -558,11 +564,13 @@ class Protocol < ApplicationRecord
end
def load_from_repository(source, current_user)
# First, destroy step contents
destroy_contents
ActiveRecord::Base.no_touching do
# First, destroy step contents
destroy_contents
# Now, clone source's step contents
Protocol.clone_contents(source, self, current_user, false)
# Now, clone source's step contents
Protocol.clone_contents(source, self, current_user, false)
end
# Lastly, update the metadata
reload
@ -588,12 +596,14 @@ class Protocol < ApplicationRecord
# Don't proceed further if clone is invalid
return clone if clone.invalid?
# Okay, clone seems to be valid: let's clone it
clone = deep_clone(clone, current_user)
ActiveRecord::Base.no_touching do
# Okay, clone seems to be valid: let's clone it
clone = deep_clone(clone, current_user)
# If the above operation went well, update published_on
# timestamp
clone.update(published_on: Time.now) if clone.in_repository_public?
# If the above operation went well, update published_on
# timestamp
clone.update(published_on: Time.zone.now) if clone.in_repository_public?
end
# Link protocols if neccesary
if link_protocols
@ -659,7 +669,7 @@ class Protocol < ApplicationRecord
def destroy_contents
# Calculate total space taken by the protocol
st = space_taken
steps.destroy_all
steps.order(position: :desc).destroy_all
# Release space taken by the step
team.release_space(st)

View file

@ -142,7 +142,7 @@ class Repository < RepositoryBase
end
def self.viewable_by_user(_user, teams)
where(team: teams)
accessible_by_teams(teams)
end
def self.name_like(query)
@ -209,21 +209,6 @@ class Repository < RepositoryBase
importer.run
end
def provision_snapshot(my_module, created_by = nil)
created_by ||= self.created_by
repository_snapshot = dup.becomes(RepositorySnapshot)
repository_snapshot.assign_attributes(type: RepositorySnapshot.name,
original_repository: self,
my_module: my_module,
created_by: created_by,
team: my_module.experiment.project.team,
permission_level: Extends::SHARED_INVENTORIES_PERMISSION_LEVELS[:not_shared])
repository_snapshot.provisioning!
repository_snapshot.reload
RepositorySnapshotProvisioningJob.perform_later(repository_snapshot)
repository_snapshot
end
def assigned_rows(my_module)
repository_rows.joins(:my_module_repository_rows).where(my_module_repository_rows: { my_module_id: my_module.id })
end

View file

@ -75,6 +75,8 @@ class RepositoryChecklistValue < ApplicationRecord
end
def self.import_from_text(text, attributes, _options = {})
return nil if text.blank?
value = new(attributes)
column = attributes.dig(:repository_cell_attributes, :repository_column)
RepositoryImportParser::Util.split_by_delimiter(text: text, delimiter: column.delimiter_char).each do |item_text|

View file

@ -66,6 +66,8 @@ class RepositoryListValue < ApplicationRecord
end
def self.import_from_text(text, attributes, _options = {})
return nil if text.blank?
value = new(attributes)
column = attributes.dig(:repository_cell_attributes, :repository_column)
list_item = column.repository_list_items.find { |item| item.data == text }

View file

@ -26,6 +26,19 @@ class RepositorySnapshot < RepositoryBase
.order(:parent_id, updated_at: :desc)
}
def self.create_preliminary(repository, my_module, created_by = nil)
created_by ||= repository.created_by
repository_snapshot = repository.dup.becomes(RepositorySnapshot)
repository_snapshot.assign_attributes(type: RepositorySnapshot.name,
original_repository: repository,
my_module: my_module,
created_by: created_by,
team: my_module.experiment.project.team,
permission_level: Extends::SHARED_INVENTORIES_PERMISSION_LEVELS[:not_shared])
repository_snapshot.provisioning!
repository_snapshot.reload
end
def default_columns_count
Constants::REPOSITORY_SNAPSHOT_TABLE_DEFAULT_STATE['length']
end

View file

@ -13,10 +13,10 @@ class Step < ApplicationRecord
validates :completed, inclusion: { in: [true, false] }
validates :user, :protocol, presence: true
validates :completed_on, presence: true, if: proc { |s| s.completed? }
validates :position, uniqueness: { scope: :protocol }, if: :position_changed?
before_validation :set_completed_on, if: :completed_changed?
before_save :set_last_modified_by
around_save :adjust_positions_on_save, if: :position_changed?
before_destroy :cascade_before_destroy
after_destroy :adjust_positions_after_destroy
@ -124,25 +124,66 @@ class Step < ApplicationRecord
end
end
def move_up
return if position.zero?
move_in_protocol(:up)
end
def move_down
return if position == protocol.steps.count - 1
move_in_protocol(:down)
end
private
def adjust_positions_on_save
step_to_swap = protocol.steps.find_by(position: position)
def move_in_protocol(direction)
transaction do
re_index_following_steps
return yield unless step_to_swap
case direction
when :up
new_position = position - 1
when :down
new_position = position + 1
else
return
end
position_to_swap = position_was
step_to_swap.position = -1
yield
step_to_swap.update!(position: position_to_swap)
step_to_swap = protocol.steps.find_by(position: new_position)
position_to_swap = position
if step_to_swap
step_to_swap.update!(position: -1)
update!(position: new_position)
step_to_swap.update!(position: position_to_swap)
else
update!(position: new_position)
end
end
end
def adjust_positions_after_destroy
protocol.steps.where('position > ?', position).find_each do |step|
re_index_following_steps
protocol.steps.where('position > ?', position).order(:position).each do |step|
step.update!(position: step.position - 1)
end
end
def re_index_following_steps
steps = protocol.steps.where(position: position..).order(:position).where.not(id: id)
i = position
steps.each do |step|
i += 1
step.position = i
end
steps.reverse_each do |step|
step.save! if step.position_changed?
end
end
def cascade_before_destroy
assets.each(&:destroy)
tables.each(&:destroy)

View file

@ -25,7 +25,17 @@ Canaid::Permissions.register_for(Experiment) do
# module: create, copy, reposition, create/update/delete connection,
# assign/reassign/unassign tags
can :manage_experiment do |user, experiment|
user.is_user_or_higher_of_project?(experiment.project)
user.is_user_or_higher_of_project?(experiment.project) &&
MyModule.joins(:experiment)
.where(experiment: experiment)
.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
true
end
end
end
# experiment: archive
@ -53,82 +63,6 @@ Canaid::Permissions.register_for(Experiment) do
end
end
Canaid::Permissions.register_for(MyModule) do
# Module, its experiment and its project must be active for all the specified
# permissions
%i(manage_module
manage_users_in_module
assign_repository_rows_to_module
complete_module
create_comments_in_module
create_my_module_repository_snapshot
manage_my_module_repository_snapshots)
.each do |perm|
can perm do |_, my_module|
my_module.active? &&
my_module.experiment.active? &&
my_module.experiment.project.active?
end
end
# module: update
# result: create, update
can :manage_module do |user, my_module|
can_manage_experiment?(user, my_module.experiment)
end
# module: archive
can :archive_module do |user, my_module|
can_manage_experiment?(user, my_module.experiment)
end
# NOTE: Must not be dependent on canaid parmision for which we check if it's
# active
# module: restore
can :restore_module do |user, my_module|
user.is_user_or_higher_of_project?(my_module.experiment.project) &&
my_module.archived?
end
# module: move
can :move_module do |user, my_module|
can_manage_experiment?(user, my_module.experiment)
end
# module: assign/reassign/unassign users
can :manage_users_in_module do |user, my_module|
user.is_owner_of_project?(my_module.experiment.project)
end
# module: assign/unassign repository record
# NOTE: Use 'module_page? &&' before calling this permission!
can :assign_repository_rows_to_module do |user, my_module|
user.is_technician_or_higher_of_project?(my_module.experiment.project)
end
# module: complete/uncomplete
can :complete_module do |user, my_module|
user.is_technician_or_higher_of_project?(my_module.experiment.project)
end
# module: create comment
# result: create comment
# step: create comment
can :create_comments_in_module do |user, my_module|
can_create_comments_in_project?(user, my_module.experiment.project)
end
# module: create a snapshot of repository item
can :create_my_module_repository_snapshot do |user, my_module|
user.is_technician_or_higher_of_project?(my_module.experiment.project)
end
# module: make a repository snapshot selected
can :manage_my_module_repository_snapshots do |user, my_module|
user.is_technician_or_higher_of_project?(my_module.experiment.project)
end
end
Canaid::Permissions.register_for(Protocol) do
# Protocol needs to be in a module for all Protocol permissions below
# experiment level
@ -170,7 +104,7 @@ Canaid::Permissions.register_for(Protocol) do
# step: complete/uncomplete
can :complete_or_checkbox_step do |user, protocol|
can_complete_module?(user, protocol.my_module)
can_change_my_module_flow_status?(user, protocol.my_module)
end
end

Some files were not shown because too many files have changed in this diff Show more