Merge branch 'develop' into features/tiny_mce_6

This commit is contained in:
Martin Artnik 2023-01-04 14:21:02 +01:00
commit 4e779e70a4
179 changed files with 4443 additions and 882 deletions

View file

@ -273,7 +273,7 @@ Style/SymbolArray:
EnforcedStyle: percent
Style/SymbolProc:
IgnoredMethods:
AllowedMethods:
- respond_to
- define_method
@ -352,7 +352,7 @@ Metrics/AbcSize:
Enabled: false
Metrics/BlockLength:
IgnoredMethods: ['describe', 'context']
AllowedMethods: ['describe', 'context']
Metrics/ClassLength:
Enabled: false

View file

@ -68,12 +68,12 @@ gem 'deface', '~> 1.0'
gem 'down', '~> 5.0'
gem 'faker' # Generate fake data
gem 'fastimage' # Light gem to get image resolution
gem 'httparty', '~> 0.17.3'
gem 'httparty', '~> 0.21.0'
gem 'i18n-js', '~> 3.6' # Localization in javascript files
gem 'jbuilder' # JSON structures via a Builder-style DSL
gem 'logging', '~> 2.0.0'
gem 'nested_form_fields'
gem 'nokogiri', '~> 1.13.9' # HTML/XML parser
gem 'nokogiri', '~> 1.13.10' # HTML/XML parser
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
gem 'rgl' # Graph framework for project diagram calculations
gem 'roo', '~> 2.8.2' # Spreadsheet parser

View file

@ -305,8 +305,8 @@ GEM
hammerjs-rails (2.0.8)
hashdiff (1.0.1)
hashie (5.0.0)
httparty (0.17.3)
mime-types (~> 3.0)
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.11.0)
concurrent-ruby (~> 1.0)
@ -357,16 +357,16 @@ GEM
logging (2.0.0)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.18.0)
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.1104)
mime-types-data (3.2022.0105)
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.8.0)
@ -384,7 +384,7 @@ GEM
rails (>= 3.2.0)
newrelic_rpm (6.15.0)
nio4r (2.5.8)
nokogiri (1.13.9)
nokogiri (1.13.10)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
oauth2 (1.4.4)
@ -441,7 +441,7 @@ GEM
puma (5.6.4)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.6.0)
racc (1.6.1)
rack (2.2.4)
rack-attack (6.4.0)
rack (>= 1.0, < 3)
@ -473,8 +473,8 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.3)
loofah (~> 2.3)
rails-html-sanitizer (1.4.4)
loofah (~> 2.19, >= 2.19.1)
rails_12factor (0.0.3)
rails_serve_static_assets
rails_stdout_logging
@ -669,7 +669,7 @@ DEPENDENCIES
figaro
graphviz
hammerjs-rails
httparty (~> 0.17.3)
httparty (~> 0.21.0)
i18n-js (~> 3.6)
image_processing (~> 1.12)
jbuilder
@ -687,7 +687,7 @@ DEPENDENCIES
momentjs-rails (~> 2.17.1)
nested_form_fields
newrelic_rpm
nokogiri (~> 1.13.9)
nokogiri (~> 1.13.10)
omniauth
omniauth-azure-activedirectory
omniauth-linkedin-oauth2

View file

@ -1 +1 @@
1.26.3.1
1.26.4

View file

@ -0,0 +1,3 @@
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.447715 0.447715 0 1 0H13C13.5523 0 14 0.447715 14 1V7C14 7.55228 13.5523 8 13 8H1C0.447716 8 0 7.55228 0 7V1ZM2 6C2 5.44772 2.44772 5 3 5C3.55228 5 4 5.44772 4 6C4 6.55228 3.55228 7 3 7C2.44772 7 2 6.55228 2 6ZM7 5C6.44772 5 6 5.44772 6 6C6 6.55228 6.44772 7 7 7C7.55228 7 8 6.55228 8 6C8 5.44772 7.55228 5 7 5ZM10 6C10 5.44772 10.4477 5 11 5C11.5523 5 12 5.44772 12 6C12 6.55228 11.5523 7 11 7C10.4477 7 10 6.55228 10 6ZM3 1C2.44772 1 2 1.44772 2 2C2 2.55228 2.44772 3 3 3H11C11.5523 3 12 2.55228 12 2C12 1.44772 11.5523 1 11 1H3Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 707 B

View file

@ -0,0 +1,5 @@
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1C0 0.447715 0.447715 0 1 0H5C5.55228 0 6 0.447715 6 1V2C6 2.55228 5.55228 3 5 3H1C0.447715 3 0 2.55228 0 2V1Z" fill="#404048"/>
<path d="M0 6C0 5.44772 0.447715 5 1 5H5C5.55228 5 6 5.44772 6 6V7C6 7.55228 5.55228 8 5 8H1C0.447715 8 0 7.55228 0 7V6Z" fill="#404048"/>
<path d="M8 1C8 0.447715 8.44772 0 9 0H13C13.5523 0 14 0.447715 14 1V2C14 2.55228 13.5523 3 13 3H9C8.44772 3 8 2.55228 8 2V1Z" fill="#404048"/>
</svg>

After

Width:  |  Height:  |  Size: 525 B

View file

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 1.5C11 2.32843 10.3284 3 9.5 3C8.84689 3 8.29127 2.5826 8.08535 2H2.91465C2.764 2.42621 2.42621 2.764 2 2.91465V8.08535C2.5826 8.29127 3 8.84689 3 9.5C3 10.3284 2.32843 11 1.5 11C0.671573 11 0 10.3284 0 9.5C0 8.84689 0.417404 8.29127 1 8.08535V2.91465C0.417404 2.70873 0 2.15311 0 1.5C0 0.671573 0.671573 0 1.5 0C2.15311 0 2.70873 0.417404 2.91465 1H8.08535C8.29127 0.417404 8.84689 0 9.5 0C10.3284 0 11 0.671573 11 1.5Z" fill="#404048"/>
</svg>

After

Width:  |  Height:  |  Size: 555 B

View file

@ -5,11 +5,18 @@ var DasboardRecentWorkWidget = (function() {
function renderRecentWorkItem(data, container) {
$.each(data, (i, item) => {
var recentWorkItem = $($('#recent-work-item-template').html());
var recentWorkItemType = recentWorkItem.find('.object-type span');
recentWorkItem.attr('href', item.url);
recentWorkItem.find('.object-name').html(item.name);
recentWorkItem.find('.object-type').html(I18n.t('dashboard.recent_work.subject_type.' + item.subject_type));
recentWorkItemType.html(item.code || item.type);
recentWorkItem.find('.object-changed').html(item.last_change);
container.append(recentWorkItem);
if (item.code) {
recentWorkItemType.attr('data-toggle', 'tooltip');
recentWorkItemType.attr('title', `${item.type} ID: ${item.code}`);
recentWorkItemType.tooltip();
}
});
}

View file

@ -0,0 +1,121 @@
/* global dropdownSelector initBSTooltips I18n */
(function() {
function initNewMyModuleModal() {
let experimentWrapper = '.experiment-new-my_module';
let newMyModuleModal = '#new-my-module-modal';
let myModuleUserSelector = '#my_module_user_ids';
var myModuleTagsSelector = '#module-tags-selector';
// Modal's submit handler function
$(experimentWrapper)
.on('ajax:success', newMyModuleModal, function() {
$(newMyModuleModal).modal('hide');
})
.on('ajax:error', newMyModuleModal, function(ev, data) {
$(this).renderFormErrors('my_module', data.responseJSON);
})
.on('submit', newMyModuleModal, function() {
// To submit correct assigned user ids to new modal
// Clear default selected user in dropdown
$(`${myModuleUserSelector} option[value=${$('#new-my-module-modal').data('user-id')}]`)
.prop('selected', false);
$.map(dropdownSelector.getValues(myModuleUserSelector), function(val) {
$(`${myModuleUserSelector} option[value=${val}]`).prop('selected', true);
});
})
.on('ajax:success', '.new-my-module-button', function(ev, result) {
// Add and show modal
$(experimentWrapper).append($.parseHTML(result.html));
$(newMyModuleModal).modal('show');
$(newMyModuleModal).find("input[type='text']").focus();
// Remove modal when it gets closed
$(newMyModuleModal).on('hidden.bs.modal', function() {
$(newMyModuleModal).remove();
});
// initiaize user assing dropdown menu
dropdownSelector.init(myModuleUserSelector, {
closeOnSelect: true,
labelHTML: true,
tagClass: 'my-module-user-tags',
tagLabel: (data) => {
return `<img class="img-responsive block-inline" src="${data.params.avatar_url}" alt="${data.label}"/>
<span style="user-full-name block-inline">${data.label}</span>`;
},
optionLabel: (data) => {
if (data.params.avatar_url) {
return `<span class="global-avatar-container" style="margin-top: 10px">
<img src="${data.params.avatar_url}" alt="${data.label}"/></span>
<span style="margin-left: 10px">${data.label}</span>`;
}
return data.label;
}
});
dropdownSelector.selectValues(myModuleUserSelector, $('#new-my-module-modal').data('user-id'));
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>`;
},
ajaxParams: function(params) {
let newParams = params;
newParams.selected_tags = JSON.stringify(dropdownSelector.getValues(myModuleTagsSelector));
return newParams;
},
onSelect: function() {
var selectElement = $(myModuleTagsSelector);
var lastTag = selectElement.next().find('.ds-tags').last();
var lastTagId = lastTag.find('.tag-label').data('ds-tag-id');
if (lastTagId > 0) {
$('#my_module_tag_ids').val(JSON.stringify(dropdownSelector.getValues(myModuleTagsSelector)));
} else {
let newTag = {
tag: {
name: lastTag.find('.tag-label').html(),
project_id: selectElement.data('project-id'),
color: null
},
simple_creation: true
};
$.post(selectElement.data('tags-create-url'), newTag, function(res) {
dropdownSelector.removeValue(myModuleTagsSelector, 0, '', true);
dropdownSelector.addValue(myModuleTagsSelector, {
value: res.tag.id,
label: res.tag.name,
params: {
color: res.tag.color
}
}, true);
$('#my_module_tag_ids').val(JSON.stringify(dropdownSelector.getValues(myModuleTagsSelector)));
}).fail(function() {
dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true);
});
}
}
});
});
initBSTooltips();
}
initNewMyModuleModal();
}());

View file

@ -0,0 +1,751 @@
/* global I18n GLOBAL_CONSTANTS InfiniteScroll initBSTooltips filterDropdown dropdownSelector HelperModule */
var ExperimnetTable = {
permissions: ['editable', 'archivable', 'restorable', 'moveable'],
selectedId: [],
table: '.experiment-table',
render: {},
selectedMyModules: [],
activeFilters: {},
filters: [], // Filter {name: '', init(), closeFilter(), apply(), active(), clearFilter()}
myModulesCurrentSort: '',
pageSize: GLOBAL_CONSTANTS.DEFAULT_ELEMENTS_PER_PAGE,
getUrls: function(id) {
return $(`.table-row[data-id="${id}"]`).data('urls');
},
loadPlaceholder: function() {
let placeholder = '';
$.each(Array(this.pageSize), function() {
placeholder += $('#experimentTablePlaceholder').html();
});
$(placeholder).insertAfter($(this.table).find('.table-body'));
},
appendRows: function(result) {
$.each(result, (_j, data) => {
let row;
// Checkbox selector
row = `
<div class="table-body-cell">
<div class="sci-checkbox-container">
<div class="loading-overlay"></div>
<input type="checkbox" class="sci-checkbox my-module-selector" data-my-module="${data.id}">
<span class="sci-checkbox-label"></span>
</div>
</div>`;
// Task columns
$.each(data.columns, (_i, cell) => {
let hidden = '';
if ($(`.table-display-modal .fa-eye-slash[data-column="${cell.column_type}"]`).length === 1) {
hidden = 'hidden';
}
row += `
<div class="table-body-cell ${cell.column_type}-column ${hidden}">
${ExperimnetTable.render[cell.column_type](cell.data)}
</div>
`;
});
// Menu
row += `
<div class="table-body-cell">
<div ref="dropdown" class="dropdown my-module-menu" data-url="${data.urls.actions_dropdown}">
<div class="btn btn-ligh icon-btn" data-toggle="dropdown" >
<i class="fas fa-ellipsis-h"></i>
</div>
<div class="dropdown-menu dropdown-menu-right">
<a class="open-access-modal hidden" data-action="remote-modal" href="${data.urls.access}"></a>
</div>
</div>
</div>`;
let tableRowClass = `table-row ${data.provisioning_status === 'in_progress' ? 'table-row-provisioning' : ''}`;
$(`<div class="${tableRowClass}" data-urls='${JSON.stringify(data.urls)}' data-id="${data.id}">${row}</div>`)
.appendTo(`${this.table} .table-body`);
});
},
initDueDatePicker: function(data) {
// eslint-disable-next-line no-unused-vars
$.each(data, (id, _) => {
let element = `#calendarDueDate${id}`;
let dueDateContainer = $(element).closest('#dueDateContainer');
let dateText = $(element).closest('.date-text');
let clearDate = $(element).closest('.datetime-container').find('.clear-date');
$(element).on('dp.change', function() {
$.ajax({
url: dueDateContainer.data('update-url'),
type: 'PATCH',
dataType: 'json',
data: { my_module: { due_date: $(element).val() } },
success: function(result) {
dueDateContainer.find('#dueDateLabelContainer').html(result.table_due_date_label.html);
dateText.data('due-status', result.table_due_date_label.due_status);
if ($(result.table_due_date_label.html).data('due-date')) {
clearDate.addClass('open');
}
}
});
});
$(element).on('dp.hide', function() {
dateText.attr('data-original-title', dateText.data('due-status'));
clearDate.removeClass('open');
});
$(element).on('dp.show', function() {
dateText.attr('data-original-title', '').tooltip('hide');
if (dueDateContainer.find('.due-date-label').data('due-date')) {
clearDate.addClass('open');
}
});
});
},
initMyModuleActions: function() {
$(this.table).on('show.bs.dropdown', '.my-module-menu', (e) => {
let menu = $(e.target).find('.dropdown-menu');
$.get(e.target.dataset.url, (result) => {
$(menu).find('li').remove();
$(result.html).appendTo(menu);
});
});
$(this.table).on('click', '.archive-my-module', (e) => {
e.preventDefault();
this.archiveMyModules(e.target.href, e.target.dataset.id);
});
$(this.table).on('click', '.restore-my-module', (e) => {
e.preventDefault();
this.restoreMyModules(e.target.href, e.target.dataset.id);
});
$(this.table).on('click', '.duplicate-my-module', (e) => {
e.preventDefault();
this.duplicateMyModules($('#duplicateTasks').data('url'), e.target.dataset.id);
});
$(this.table).on('click', '.move-my-module', (e) => {
e.preventDefault();
this.openMoveModulesModal([e.target.dataset.id]);
});
$(this.table).on('click', '.edit-my-module', (e) => {
e.preventDefault();
$('#modal-edit-module').modal('show');
$('#modal-edit-module').data('id', e.target.dataset.id);
$('#edit-module-name-input').val($(`#taskName${$('#modal-edit-module').data('id')}`).data('full-name'));
});
},
initDuplicateMyModules: function() {
$('#duplicateTasks').on('click', (e) => {
this.duplicateMyModules(e.target.dataset.url, this.selectedMyModules);
});
},
duplicateMyModules: function(url, ids) {
$.post(url, { my_module_ids: ids }, () => {
this.loadTable();
}).error((data) => {
HelperModule.flashAlertMsg(data.responseJSON.message, 'danger');
});
},
initArchiveMyModules: function() {
$('#archiveTask').on('click', (e) => {
this.archiveMyModules(e.target.dataset.url, this.selectedMyModules);
});
},
archiveMyModules: function(url, ids) {
$.post(url, { my_modules: ids }, (data) => {
HelperModule.flashAlertMsg(data.message, 'success');
this.loadTable();
}).error((data) => {
HelperModule.flashAlertMsg(data.responseJSON.message, 'danger');
});
},
initRestoreMyModules: function() {
$('#restoreTask').on('click', (e) => {
this.restoreMyModules(e.target.dataset.url, this.selectedMyModules);
});
},
restoreMyModules: function(url, ids) {
$.post(url, { my_modules_ids: ids, view: 'table' });
},
initAccessModal: function() {
$('#manageTaskAccess').on('click', () => {
$(`.table-row[data-id="${this.selectedMyModules[0]}"] .open-access-modal`).click();
});
},
initRenameModal: function() {
$('#editTask').on('click', () => {
$('#modal-edit-module').modal('show');
$('#modal-edit-module').data('id', this.selectedMyModules[0]);
$('#edit-module-name-input').val($(`#taskName${$('#modal-edit-module').data('id')}`).data('full-name'));
});
$('#modal-edit-module').on('click', 'button[data-action="confirm"]', () => {
let id = $('#modal-edit-module').data('id');
let newValue = $('#edit-module-name-input').val();
if (newValue === $(`#taskName${id}`).data('full-name')) {
$('#modal-edit-module').modal('hide');
return false;
}
$.ajax({
url: this.getUrls(id).name_update,
type: 'PATCH',
dataType: 'json',
data: { my_module: { name: $('#edit-module-name-input').val() } },
success: () => {
$(`#taskName${id}`).text(newValue);
$(`#taskName${id}`).data('full-name', newValue);
$('#edit-module-name-input').closest('.sci-input-container').removeClass('error');
$('#modal-edit-module').modal('hide');
},
error: function(response) {
let error = response.responseJSON.name.join(', ');
$('#edit-module-name-input')
.closest('.sci-input-container')
.addClass('error')
.attr('data-error-text', error);
}
});
return true;
});
},
initManageUsersDropdown: function() {
$(this.table).on('show.bs.dropdown', '.assign-users-dropdown', (e) => {
let usersList = $(e.target).find('.users-list');
usersList.find('.user-container').remove();
$.get(usersList.data('list-url'), (result) => {
$.each(result, (_i, user) => {
$(`
<div class="user-container">
<div class="sci-checkbox-container">
<input type="checkbox"
class="sci-checkbox user-selector"
${user.params.designated ? 'checked' : ''}
value="${user.value}"
data-assign-url="${user.params.assign_url}"
data-unassign-url="${user.params.unassign_url}"
>
<span class="sci-checkbox-label"></span>
</div>
<div class="user-avatar">
<img src="${user.params.avatar_url}"></img>
</div>
<div class="user-name">
${user.label}
</div>
</div>
`).appendTo(usersList);
});
});
});
$(this.table).on('click', '.dropdown-menu', (e) => {
if (e.target.tagName === 'INPUT') return;
e.preventDefault();
e.stopPropagation();
});
$(this.table).on('change keyup', '.assign-users-dropdown .user-search', (e) => {
let query = e.target.value;
let usersList = $(e.target).closest('.dropdown-menu').find('.user-container');
$.each(usersList, (_i, user) => {
$(user).removeClass('hidden');
if (query.length && !$(user).find('.user-name').text().toLowerCase()
.includes(query.toLowerCase())) {
$(user).addClass('hidden');
}
});
});
$(this.table).on('change', '.assign-users-dropdown .user-selector', (e) => {
let checkbox = e.target;
if (checkbox.checked) {
$.post(checkbox.dataset.assignUrl, {
table: true,
user_my_module: {
my_module_id: $(checkbox).closest('.table-row').data('id'),
user_id: checkbox.value
}
}, (result) => {
checkbox.dataset.unassignUrl = result.unassign_url;
$(checkbox).closest('.table-row').find('.assigned-users-container')
.replaceWith($(result.html).find('.assigned-users-container'));
}).error((data) => {
HelperModule.flashAlertMsg(data.responseJSON.errors, 'danger');
});
} else {
$.ajax({
url: checkbox.dataset.unassignUrl,
method: 'DELETE',
data: { table: true },
success: (result) => {
$(checkbox).closest('.table-row').find('.assigned-users-container')
.replaceWith($(result.html).find('.assigned-users-container'));
},
error: (data) => {
HelperModule.flashAlertMsg(data.responseJSON.errors, 'danger');
}
});
}
});
},
initMoveModulesModal: function() {
$('#moveTask').on('click', () => {
this.openMoveModulesModal(this.selectedMyModules);
});
},
openMoveModulesModal: function(ids) {
let table = $(this.table);
$.get(table.data('move-modules-modal-url'), (modalData) => {
if ($('#modal-move-modules').length > 0) {
$('#modal-move-modules').replaceWith(modalData.html);
} else {
$('#experimentTable').append(modalData.html);
}
$('#modal-move-modules').on('shown.bs.modal', function() {
$(this).find('.selectpicker').selectpicker().focus();
});
$('#modal-move-modules').on('click', 'button[data-action="confirm"]', () => {
let moveParams = {
to_experiment_id: $('#modal-move-modules').find('.selectpicker').val(),
my_module_ids: ids
};
$.post(table.data('move-modules-url'), moveParams, (data) => {
HelperModule.flashAlertMsg(data.message, 'success');
this.loadTable();
}).error((data) => {
HelperModule.flashAlertMsg(data.responseJSON.message, 'danger');
});
$('#modal-move-modules').modal('hide');
});
$('#modal-move-modules').modal('show');
});
},
checkActionPermission: function(permission) {
let allMyModules;
allMyModules = this.selectedMyModules.every((id) => {
return $(`.table-row[data-id="${id}"]`).data(permission);
});
return allMyModules;
},
initSelectAllCheckbox: function() {
$(this.table).on('click', '.select-all-checkboxes .sci-checkbox', (e1) => {
$.each($('.my-module-selector'), (_i, e2) => {
if (e1.target.checked !== e2.checked) e2.click();
});
});
},
loadPermission: function(id) {
let row = $(`.table-row[data-id="${id}"]`);
$.get(this.getUrls(id).permissions, (result) => {
this.permissions.forEach((permission) => {
row.data(permission, result[permission]);
});
this.updateExperimentToolbar();
});
},
initSelector: function() {
$(this.table).on('click', '.my-module-selector', (e) => {
let checkbox = e.target;
let myModuleId = checkbox.dataset.myModule;
let index = $.inArray(myModuleId, this.selectedMyModules);
// If checkbox is checked and row ID is not in list of selected project IDs
if (checkbox.checked && index === -1) {
$(checkbox).closest('.table-row').addClass('selected');
this.selectedMyModules.push(myModuleId);
// Otherwise, if checkbox is not checked and ID is in list of selected IDs
} else if (!this.checked && index !== -1) {
$(checkbox).closest('.table-row').removeClass('selected');
this.selectedMyModules.splice(index, 1);
}
if (checkbox.checked) {
this.loadPermission(myModuleId);
} else {
this.updateExperimentToolbar();
}
});
},
updateExperimentToolbar: function() {
let experimentToolbar = $('.toolbar-row');
if (this.selectedMyModules.length === 0) {
experimentToolbar.find('.single-object-action, .multiple-object-action').addClass('hidden');
} else if (this.selectedMyModules.length === 1) {
experimentToolbar.find('.single-object-action, .multiple-object-action').removeClass('hidden');
} else {
experimentToolbar.find('.single-object-action').addClass('hidden');
experimentToolbar.find('.multiple-object-action').removeClass('hidden');
}
this.permissions.forEach((permission) => {
if (!this.checkActionPermission(permission)) {
experimentToolbar.find(`.btn[data-for="${permission}"]`).addClass('hidden');
}
});
if ($('#experimentTable').hasClass('archived')) {
experimentToolbar.find('.only-active').addClass('hidden');
}
},
selectDate: function($field) {
var datePicker = $field.data('DateTimePicker');
if (datePicker && datePicker.date()) {
return datePicker.date()._d.toUTCString();
}
return null;
},
initManageColumnsModal: function() {
$.each($('.table-display-modal .fa-eye-slash'), (_i, column) => {
$(column).parent().removeClass('visible');
});
$('.experiment-table')[0].style
.setProperty('--columns-count', $('.table-display-modal .fa-eye:not(.disabled)').length + 1);
$('.table-display-modal').on('click', '.column-container .fas', (e) => {
let icon = $(e.target);
if (icon.hasClass('fa-eye')) {
$(`.experiment-table .${icon.data('column')}-column`).addClass('hidden');
icon.removeClass('fa-eye').addClass('fa-eye-slash');
icon.parent().removeClass('visible');
} else {
$(`.experiment-table .${icon.data('column')}-column`).removeClass('hidden');
icon.addClass('fa-eye').removeClass('fa-eye-slash');
icon.parent().addClass('visible');
}
let visibleColumns = $('.table-display-modal .fa-eye').map((_i, col) => col.dataset.column).toArray();
// Update columns on backend - $.post('', { columns: visibleColumns }, () => {});
$.post($('.table-display-modal').data('column-state-url'), { columns: visibleColumns }, () => {});
$('.experiment-table')[0].style
.setProperty('--columns-count', $('.table-display-modal .fa-eye:not(.disabled)').length + 1);
});
},
clearRowTaskSelection: function() {
this.selectedMyModules = [];
this.updateExperimentToolbar();
},
initNewTaskModal: function(table) {
$('.experiment-new-my_module').on('ajax:success', '#new-my-module-modal', function() {
table.loadTable();
});
},
initSorting: function(table) {
$('#sortMenuDropdown a').click(function() {
if (table.myModulesCurrentSort !== $(this).data('sort')) {
$('#sortMenuDropdown a').removeClass('selected');
// eslint-disable-next-line no-param-reassign
table.myModulesCurrentSort = $(this).data('sort');
table.loadTable();
$(this).addClass('selected');
$('#sortMenu').dropdown('toggle');
}
});
},
initFilters: function() {
this.filterDropdown = filterDropdown.init();
let $experimentFilter = $('#experimentTable .my-modules-filters');
$.each(this.filters, (_i, filter) => {
filter.init($experimentFilter);
});
this.filterDropdown.on('filter:apply', () => {
$.each(this.filters, (_i, filter) => {
this.activeFilters[filter.name] = filter.apply($experimentFilter);
});
filterDropdown.toggleFilterMark(
this.filterDropdown,
this.filters.some((filter) => {
return filter.active(this.activeFilters[filter.name]);
})
);
this.loadTable();
});
this.filterDropdown.on('filter:clickBody', () => {
$.each(this.filters, (_i, filter) => {
filter.closeFilter($experimentFilter);
});
});
this.filterDropdown.on('filter:clear', () => {
$.each(this.filters, (_i, filter) => {
filter.clearFilter($experimentFilter);
});
});
},
loadTable: function() {
var tableParams = {
filters: this.activeFilters,
sort: this.myModulesCurrentSort
};
var dataUrl = $(this.table).data('my-modules-url');
this.loadPlaceholder();
$.get(dataUrl, tableParams, (result) => {
$(this.table).find('.table-row').remove();
this.appendRows(result.data);
this.initDueDatePicker(result.data);
InfiniteScroll.init(this.table, {
url: dataUrl,
eventTarget: window,
placeholderTemplate: '#experimentTablePlaceholder',
endOfListTemplate: '#experimentTableEndOfList',
pageSize: this.pageSize,
lastPage: !result.next_page,
customResponse: (response) => {
this.appendRows(response.data);
this.initDueDatePicker(response.data);
},
customParams: (params) => {
return { ...params, ...tableParams };
}
});
initBSTooltips();
this.clearRowTaskSelection();
this.initProvisioningStatusPolling();
});
},
initProvisioningStatusPolling: function() {
let provisioningStatusUrls = $('.table-row-provisioning').toArray()
.map((u) => $(u).data('urls').provisioning_status);
this.provisioningMyModulesCount = provisioningStatusUrls.length;
if (this.provisioningMyModulesCount > 0) this.pollProvisioningStatuses(provisioningStatusUrls);
},
pollProvisioningStatuses: function(provisioningStatusUrls) {
let remainingUrls = [];
provisioningStatusUrls.forEach((url) => {
jQuery.ajax({
url: url,
success: (data) => {
if (data.provisioning_status === 'in_progress') remainingUrls.push(url);
},
async: false
});
});
if (remainingUrls.length > 0) {
setTimeout(() => {
this.pollProvisioningStatuses(remainingUrls);
}, 10000);
} else {
HelperModule.flashAlertMsg(
I18n.t('experiments.duplicate_tasks.success', { count: this.provisioningMyModulesCount }),
'success'
);
this.loadTable();
}
},
init: function() {
this.initSelector();
this.initSelectAllCheckbox();
this.initFilters();
this.initSorting(this);
this.loadTable();
this.initRenameModal();
this.initAccessModal();
this.initDuplicateMyModules();
this.initMoveModulesModal();
this.initArchiveMyModules();
this.initManageColumnsModal();
this.initNewTaskModal(this);
this.initMyModuleActions();
this.initRestoreMyModules();
this.initManageUsersDropdown();
}
};
ExperimnetTable.render.task_name = function(data) {
if (data.provisioning_status === 'in_progress') {
return `<span data-full-name="${data.name}">${data.name}</span>`;
}
return `<a href="${data.url}" id="taskName${data.id}" data-full-name="${data.name}">${data.name}</a>`;
};
ExperimnetTable.render.id = function(data) {
return `
<div>${data.id}</div>
`;
};
ExperimnetTable.render.due_date = function(data) {
return data.data;
};
ExperimnetTable.render.archived = function(data) {
return data;
};
ExperimnetTable.render.age = function(data) {
return data;
};
ExperimnetTable.render.results = function(data) {
return `<a href="${data.url}">${data.count}</a>`;
};
ExperimnetTable.render.status = function(data) {
return `<div class="my-module-status" style="background-color: ${data.color}">${data.name}</div>`;
};
ExperimnetTable.render.assigned = function(data) {
return data.html;
};
ExperimnetTable.render.tags = function(data) {
const value = parseInt(data.tags, 10) === 0 ? I18n.t('experiments.table.add_tag') : data.tags;
return `<a href="${data.edit_url}"
id="myModuleTags${data.my_module_id}"
data-remote="true"
class="edit-tags-link">${value}</a>`;
};
ExperimnetTable.render.comments = function(data) {
return `<a href="#"
class="open-comments-sidebar" id="comment-count-${data.id}"
data-object-type="MyModule" data-object-id="${data.id}">
${data.count > 0 ? data.count : '+'}
${data.count_unseen > 0 ? `<span class="unseen-comments"> ${data.count_unseen} </span>` : ''}
</a>`;
};
// Filters
ExperimnetTable.filters.push({
name: 'name',
init: () => {},
closeFilter: ($container) => {
$('#textSearchFilterHistory').hide();
$('#textSearchFilterInput', $container).closest('.dropdown').removeClass('open');
},
apply: ($container) => {
return $('#textSearchFilterInput', $container).val();
},
active: (value) => { return value; },
clearFilter: ($container) => {
$('#textSearchFilterInput', $container).val('');
}
});
ExperimnetTable.filters.push({
name: 'due_date_from',
init: () => {},
closeFilter: () => {},
apply: ($container) => {
return ExperimnetTable.selectDate($('.due-date-filter .from-date', $container));
},
active: (value) => { return value; },
clearFilter: ($container) => {
if ($('.due-date-filter .from-date', $container).data('DateTimePicker')) {
$('.due-date-filter .from-date', $container).data('DateTimePicker').clear();
}
}
});
ExperimnetTable.filters.push({
name: 'due_date_to',
init: () => {},
closeFilter: () => {},
apply: ($container) => {
return ExperimnetTable.selectDate($('.due-date-filter .to-date', $container));
},
active: (value) => { return value; },
clearFilter: ($container) => {
if ($('.due-date-filter .to-date', $container).data('DateTimePicker')) {
$('.due-date-filter .to-date', $container).data('DateTimePicker').clear();
}
}
});
ExperimnetTable.filters.push({
name: 'archived_on_from',
init: () => {},
closeFilter: () => {},
apply: ($container) => {
return ExperimnetTable.selectDate($('.archived-on-filter .from-date', $container));
},
active: (value) => { return value; },
clearFilter: ($container) => {
if ($('.archived-on-filter .from-date', $container).data('DateTimePicker')) {
$('.archived-on-filter .from-date', $container).data('DateTimePicker').clear();
}
}
});
ExperimnetTable.filters.push({
name: 'archived_on_to',
init: () => {},
closeFilter: () => {},
apply: ($container) => {
return ExperimnetTable.selectDate($('.archived-on-filter .to-date', $container));
},
active: (value) => { return value; },
clearFilter: ($container) => {
if ($('.archived-on-filter .to-date', $container).data('DateTimePicker')) {
$('.archived-on-filter .to-date', $container).data('DateTimePicker').clear();
}
}
});
ExperimnetTable.filters.push({
name: 'assigned_users',
init: ($container) => {
dropdownSelector.init($('.assigned-filter', $container), {
optionClass: 'checkbox-icon users-dropdown-list',
optionLabel: (data) => {
return `<img class="item-avatar" src="${data.params.avatar_url}"/> ${data.label}`;
},
tagLabel: (data) => {
return `<img class="item-avatar" src="${data.params.avatar_url}"/> ${data.label}`;
},
labelHTML: true,
tagClass: 'users-dropdown-list'
});
},
closeFilter: ($container) => {
dropdownSelector.closeDropdown($('.assigned-filter', $container));
},
apply: ($container) => {
return dropdownSelector.getValues($('.assigned-filter', $container));
},
active: (value) => { return value && value.length !== 0; },
clearFilter: ($container) => {
dropdownSelector.clearData($('.assigned-filter', $container));
}
});
ExperimnetTable.filters.push({
name: 'statuses',
init: ($container) => {
dropdownSelector.init($('.status-filter', $container), {
singleSelect: true,
closeOnSelect: true,
selectAppearance: 'simple'
});
},
closeFilter: ($container) => {
dropdownSelector.closeDropdown($('.status-filter', $container));
},
apply: ($container) => {
return dropdownSelector.getValues($('.status-filter', $container));
},
active: (value) => { return value && value.length !== 0; },
clearFilter: ($container) => {
dropdownSelector.clearData($('.status-filter', $container));
}
});
ExperimnetTable.init();

View file

@ -89,155 +89,6 @@
});
}
// 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);
});
}
function checkStatusState() {
$.getJSON($('.status-flow-dropdown').data('status-check-url'), (statusData) => {
if (statusData.status_changing) {
@ -440,7 +291,6 @@
initTaskCollapseState();
applyTaskStatusChangeCallBack();
initTagsSelector();
bindEditTagsAjax();
initStartDatePicker();
initDueDatePicker();
initAssignedUsersSelector();

View file

@ -0,0 +1,158 @@
/* global dropdownSelector I18n */
/* eslint-disable no-use-before-define */
(function() {
// 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');
if ($('#experimentTable').length) {
let tags = $('.tag-show').length;
$(`#myModuleTags${$('#tags_modal_my_module_id').val()}`).html(
tags === 0 ? I18n.t('experiments.table.add_tag') : 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
$('#experimentTable, .my-modules-protocols-index')
.on('ajax:before', '.edit-tags-link', function() {
manageTagsModal.modal('show');
})
.on('ajax:success', '.edit-tags-link', function(e, data) {
$('#manage-module-tags-modal-module').text(data.my_module.name);
initTagsModalBody(data);
});
}
bindEditTagsAjax();
}());

View file

@ -332,6 +332,8 @@ function initializeFullZoom() {
commentMenu.position({ top: $(this).parent().position().top });
commentMenu.offset({ top: $(this).parent().offset().top + <%= Constants::DROPDOWN_TOP_OFFSET_PX %> });
});
initializeCanvasViewNavigator();
}
function destroyFullZoom() {
@ -370,6 +372,7 @@ function initializeMediumZoom() {
// Restore draggable position
restoreDraggablePosition($("#diagram"), $("#canvas-container"));
initializeCanvasViewNavigator();
}
function destroyMediumZoom() {
@ -397,6 +400,7 @@ function initializeSmallZoom() {
// Restore draggable position
restoreDraggablePosition($("#diagram"), $("#canvas-container"));
initializeCanvasViewNavigator();
}
function destroySmallZoom() {
@ -595,7 +599,7 @@ function resizeContainer() {
if (cont.length > 0) {
cont.css(
"height",
($(window).height() - cont.offset().top - 15) + "px"
($(window).height() - cont.offset().top) + "px"
);
}
}
@ -1127,7 +1131,7 @@ function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
module.find(".panel-title").html(name);
module.find(".ep").html($("#drag-connections-placeholder").text().trim());
module.find(".ep").html($("#drag-connections-placeholder").html());
// Add dropdown
var dropdown = document.createElement("div");
@ -2661,6 +2665,9 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) {
y_pos = y_el + (fastOffsetY - y_start);
x_start = fastOffsetX;
y_start = fastOffsetY;
drawRectangleCanvasNavigatorView(-x_pos, -y_pos)
if (draggable !== null) {
elLeft(draggable, x_pos);
elTop(draggable, y_pos);
@ -2913,6 +2920,74 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) {
}
})();
function drawCanvasViewNavigatorImage(image_src){
var canvasImage = $('.canvas-preview-img')[0];
var canvasRect = $('.canvas-preview-rect')[0];
var canvasImageTx = canvasImage.getContext('2d');
var canvasRectTx = canvasRect.getContext('2d');
var image = new Image();
image.onload = function() {
canvasImageTx.drawImage(image, 0, 0, canvasImage.width, canvasImage.height);
drawRectangleCanvasNavigatorView(-(draggable.offset().left - draggable.parent().offset().left),
-(draggable.offset().top - draggable.parent().offset().top));
canvasRectTx.stroke();
};
image.src = image_src;
}
function initializeCanvasViewNavigator() {
if ($('.canvas-preview-img').data('image-url')) {
drawCanvasViewNavigatorImage($('.canvas-preview-img').data('image-url'));
} else if ($('.canvas-preview-img').data('workflowimg-present') === false) {
let imgUrl = $('.canvas-preview-img').data('workflowimg-url');
$.ajax({
url: imgUrl,
type: 'GET',
dataType: 'json',
success: function(data) {
drawCanvasViewNavigatorImage($(data.workflowimg).attr('src'));
}
});
}
}
function drawRoundRectangle(ctx, xPos, yPos, width, height, radius) {
if (width < 2 * radius) radius = width / 2;
if (height < 2 * radius) radius = height / 2;
ctx.beginPath();
ctx.lineWidth = 4;
ctx.strokeStyle = '#104DA9';
ctx.moveTo(xPos + radius, yPos);
ctx.arcTo(xPos + width, yPos, xPos + width, yPos + height, radius);
ctx.arcTo(xPos + width, yPos + height, xPos, yPos + height, radius);
ctx.arcTo(xPos, yPos + height, xPos, yPos, radius);
ctx.arcTo(xPos, yPos, xPos + width, yPos, radius);
ctx.stroke();
ctx.closePath();
}
function drawRectangleCanvasNavigatorView(xPos, yPos) {
var adjustFactor = 10;
var canvasSize = calculateDraggableSize(draggable);
var ratioX = xPos / canvasSize.width;
var ratioY = yPos / canvasSize.height;
var canvasPreviewRect = $('.canvas-preview-rect')[0];
var canvasRectTx = canvasPreviewRect.getContext('2d');
var canvasWidth = canvasRectTx.canvas.width;
var canvasHeight = canvasRectTx.canvas.height;
var previewWidth = canvasWidth * ($('#diagram-container').width() / canvasSize.width);
var previewHeight = canvasHeight * ($('#diagram-container').height() / canvasSize.height);
canvasRectTx.clearRect(0, 0, canvasWidth, canvasHeight);
canvasRectTx.beginPath();
drawRoundRectangle(canvasRectTx, canvasWidth * ratioX + adjustFactor, canvasHeight * ratioY + adjustFactor,
previewWidth - adjustFactor, previewHeight - adjustFactor, 4)
}
/** prevent reload page */
var preventCanvasReloadOnSave = (function() {
'use strict';

View file

@ -519,7 +519,7 @@ var ProjectsIndex = (function() {
$(projectsPageSelector)
.on('ajax:success', '.change-projects-view-type-form', function(ev, data) {
$(cardsWrapper).removeClass('list').addClass(data.cards_view_type_class);
$(cardsWrapper).removeClass('list cards').addClass(data.cards_view_type_class);
$(projectsPageSelector).find('.cards-switch .button-to').removeClass('selected');
$(ev.target).find('.button-to').addClass('selected');
$(ev.target).parents('.dropdown.view-switch').removeClass('open');

View file

@ -55,7 +55,7 @@
function initProjectsViewModeSwitch() {
$(experimentsPage)
.on('ajax:success', '.change-experiments-view-type-form', function(ev, data) {
$(cardsWrapper).removeClass('list').addClass(data.cards_view_type_class);
$(cardsWrapper).removeClass('list cards').addClass(data.cards_view_type_class);
$(experimentsPage).find('.cards-switch .button-to').removeClass('selected');
$(ev.target).find('.button-to').addClass('selected');
$(ev.target).parents('.dropdown.view-switch').removeClass('open');

View file

@ -53,7 +53,7 @@ function initProtocolsTable() {
</div>`;
}
}, {
targets: [ 1, 2, 3, 4, 5 ],
targets: [ 1, 2, 3, 4, 5, 6 ],
searchable: true,
orderable: true
}],
@ -61,13 +61,14 @@ function initProtocolsTable() {
{ data: "0" },
{ data: "1" },
{ data: "2" },
{ data: "3" },
{
data: "3",
data: "4",
visible: repositoryType != "archive"
},
{ data: "4" },
{ data: "5" },
{ data: "6" }
{ data: "6" },
{ data: "7" }
],
oLanguage: {
sSearch: I18n.t('general.filter')

View file

@ -1020,7 +1020,7 @@ function reportHandsonTableConverter() {
});
// Project content
reportData.project_content = { experiments: [], repositories: [] };
reportData.project_content = { experiments: [] };
$.each($('.project-contents-container .experiment-element'), function(i, experiment) {
let expCheckbox = $(experiment).find('.report-experiment-checkbox');
if (!expCheckbox.prop('checked') && !expCheckbox.prop('indeterminate')) return;
@ -1034,10 +1034,6 @@ function reportHandsonTableConverter() {
reportData.project_content.experiments.push(experimentData);
});
$.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), function(i, e) {
reportData.project_content.repositories.push(parseInt(e.value, 10));
});
// Settings
reportData.report.settings.template = dropdownSelector.getValues('#templateSelector');
reportData.report.settings.all_tasks = $('.project-contents-container .select-all-my-modules-checkbox')
@ -1048,6 +1044,10 @@ function reportHandsonTableConverter() {
$.each($('.task-contents-container .content-element .task-setting'), function(i, e) {
reportData.report.settings.task[e.value] = e.checked;
});
reportData.report.settings.task.repositories = [];
$.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), function(i, e) {
reportData.report.settings.task.repositories.push(parseInt(e.value, 10));
});
reportData.report.settings.task.result_order = dropdownSelector.getValues('#taskResultsOrder');

View file

@ -135,7 +135,7 @@
if (data.archived) {
$(row).addClass('archived');
}
if (data['3'].processing || data['4'].processing) {
if (data['4'].processing || data['5'].processing) {
$(row).addClass('processing');
}
}
@ -225,7 +225,7 @@
var $table = $('#reports-table');
REPORTS_TABLE = $table.DataTable({
dom: "Rt<'pagination-row hidden'<'pagination-info'li><'pagination-actions'p>>",
order: [[8, 'desc']],
order: [[9, 'desc']],
sScrollX: '100%',
sScrollXInner: '100%',
processing: true,
@ -244,13 +244,13 @@
render: renderCheckboxHTML
},
{
targets: 3,
targets: 4,
searchable: false,
sWidth: '60',
render: renderPdfFile
},
{
targets: 4,
targets: 5,
searchable: false,
sWidth: '60',
render: renderDocxFile

View file

@ -71,6 +71,7 @@ var inlineEditing = (function() {
data: params,
success: function(result) {
var viewData;
var parentContainer = container.parent();
if (container.data('response-field')) {
// If we want to modify preview element on backend
// we can use this data field and we will take string from response
@ -95,11 +96,15 @@ var inlineEditing = (function() {
.attr('value', inputField(container).val());
appendAfterLabel(container);
container.trigger('inlineEditing::updated', [inputField(container).val(), viewData])
container.trigger('inlineEditing::updated', [inputField(container).val(), viewData]);
if (SIDEBAR_ITEM_TYPES.includes(paramsGroup)) {
updateSideBarNav(paramsGroup, itemId, viewData);
}
if (parentContainer.attr('data-original-title')) {
parentContainer.attr('data-original-title', inputField(container).val());
}
},
error: function(response) {
var error = response.responseJSON[fieldToUpdate];
@ -111,6 +116,7 @@ var inlineEditing = (function() {
container.find('.error-block').html(error.join(', '));
inputField(container).focus();
container.data('disabled', false);
$('.tooltip').hide();
}
});
return true;
@ -147,6 +153,7 @@ var inlineEditing = (function() {
.addClass('hidden')
.closest('.inline_scroll_block')
.scrollTop(container.offsetTop);
$('.tooltip').hide();
}
e.stopPropagation();
return true;

View file

@ -28,7 +28,7 @@ var CommentsSidebar = (function() {
var commentsAmount = $(SIDEBAR).find('.comments-list .comment-container').length;
if (commentsCounter.length) {
// Replace the number in comment element
commentsCounter.text(commentsCounter.text().replace(/\d+/g, commentsAmount));
commentsCounter.text(commentsCounter.text().replace(/[\d\\+]+/g, commentsAmount));
}
}
@ -37,6 +37,7 @@ var CommentsSidebar = (function() {
commentsCounter = $(`#comment-count-${$(this).data('objectId')}`);
closeCallback = $(this).data('closeCallback');
CommentsSidebar.open($(this).data('objectType'), $(this).data('objectId'));
$(this).parent().find('.unseen-comments').remove();
e.preventDefault();
});
}
@ -152,7 +153,7 @@ var CommentsSidebar = (function() {
open: function(objectType, objectId) {
$(SIDEBAR).find('.comments-subject-title').empty();
$(SIDEBAR).find('.comments-list').empty();
$(SIDEBAR).find('.comment-input-field').val('');
$(SIDEBAR).find('.comment-input-field').val('').focus();
$('.error-container').empty();
$(SIDEBAR).find('.sidebar-footer').removeClass('update');
$(SIDEBAR).data('object-type', objectType).data('object-id', objectId);

View file

@ -15,7 +15,7 @@
dt.data('DateTimePicker').show();
});
$(document).on('click', '[data-toggle="clear-date-time-picker"]', function() {
$(document).on('mousedown', '[data-toggle="clear-date-time-picker"]', function() {
let dt = $(`#${$(this).data('target')}`);
if (!dt.data('DateTimePicker')) dt.datetimepicker({ useCurrent: false });
dt.data('DateTimePicker').clear();

View file

@ -52,10 +52,10 @@ var filterDropdown = (function() {
$textFilter.click(function(e) {
e.stopPropagation();
$('#textSearchFilterHistory').toggle();
$(this).closest('.dropdown').toggleClass('open');
}).on('input', () => {
$(e.target).closest('.dropdown').toggleClass('open');
}).on('input', (e) => {
$('#textSearchFilterHistory').hide();
$(this).closest('.dropdown').removeClass('open');
$(e.target).closest('.dropdown').removeClass('open');
});
$filterContainer.on('click', '.projects-search-keyword', function(e) {

View file

@ -210,6 +210,20 @@ var MarvinJsEditorApi = (function() {
});
}
function createNewMarvinContainer(dataset) {
var objectId = dataset.objectId;
var objectType = dataset.objectType;
var marvinUrl = dataset.marvinUrl;
var container = dataset.sketchContainer;
MarvinJsEditor.open({
mode: 'new',
objectId: objectId,
objectType: objectType,
marvinUrl: marvinUrl,
container: container
});
}
// MarvinJS Methods
return {
@ -254,17 +268,13 @@ var MarvinJsEditorApi = (function() {
initNewButton: function(selector, saveCallback) {
$(selector).off('click').on('click', function() {
var objectId = this.dataset.objectId;
var objectType = this.dataset.objectType;
var marvinUrl = this.dataset.marvinUrl;
var container = this.dataset.sketchContainer;
MarvinJsEditor.open({
mode: 'new',
objectId: objectId,
objectType: objectType,
marvinUrl: marvinUrl,
container: container
});
createNewMarvinContainer(this.dataset);
});
$(selector).off('keypress').on('keypress', function(e) {
if (e.which === 13) {
createNewMarvinContainer(this.dataset);
}
});
MarvinJsEditor.saveCallback = saveCallback;

View file

@ -35,6 +35,7 @@
@import "protocols/*";
@import "dashboard/*";
@import "repository/*";
@import "experiment/*";
@import "repository_columns/*";
@import "label_templates/*";
@import "reports/*";

View file

@ -0,0 +1,71 @@
// scss-lint:disable SelectorDepth NestingDepth IdSelector
#canvas-container,
#module-archive {
.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: $brand-accent;
border-radius: $border-radius-tag;
color: $color-black;
font-size: 10px;
margin-left: -8px;
}
}
}
#canvas-container {
margin: 0 -28px;
}
.canvas-preview-img,
.canvas-preview-rect {
border-radius: 4px;
bottom: 24px;
box-shadow: 0 0 0 8px $color-white;
display: flex;
height: 64px;
position: absolute;
right: 24px;
width: 68px;
z-index: 999999;
&.empty {
background-color: $color-concrete;
box-shadow: inset 0 0 0 2px $brand-primary;
}
&.processing {
background-color: $color-concrete;
background-image: url("/images/medium/loading.svg");
background-position: center;
background-repeat: no-repeat;
}
}

View file

@ -0,0 +1,122 @@
// scss-lint:disable SelectorDepth NestingDepth IdSelector
#experimentTable,
#experiment-canvas {
.experimnet-name {
max-width: calc(100% - 300px);
}
}
#experiment-canvas {
[data-view-mode="archived"] {
display: none;
}
.toolbar-row {
align-items: center;
display: flex;
margin: 10px 0;
.toolbar-right-block {
align-items: center;
display: flex;
margin-left: auto;
}
.zoom-text {
margin-right: .5em;
}
}
}
#new-my-module-modal {
.form-control {
border-color: $color-silver-chalice;
}
.my-module-user-tags {
img {
border-radius: 50%;
display: inline;
margin-right: .5em;
max-height: 20px;
max-width: 20px;
}
}
.dropdown-selector-container {
.my-module-white-tags {
color: $color-white;
}
.my-module-tags-color {
border-radius: 8px;
display: inline-block;
height: 16px;
margin-right: 5px;
width: 16px;
}
.my-module-tags-create-new {
opacity: .6;
}
&.open {
.input-field {
border: 1px solid $color-alto;
}
}
&:not(.view-mode):hover {
.input-field {
border: 1px solid $color-alto;
}
}
}
.datetime-picker-container {
width: 45%;
.fa-calendar-alt {
color: $color-volcano !important;
font-size: 14px !important;
}
}
}
.dropdown-experiment-actions,
.my-module-menu {
.divider-label {
@include font-small;
color: $color-silver-chalice;
padding: .25em 1em;
&.footer {
border-top: 1px solid $color-concrete;
padding-top: .5em;
}
}
li {
@include font-button;
cursor: pointer;
padding: .5em 1em;
white-space: nowrap;
.fas {
display: inline-block;
margin-right: .25em;
width: 18px;
}
&:hover:not(.divider-label) {
background: $color-concrete;
}
a {
display: inline-block;
margin: -.5em -1em;
padding: .5em 1em;
width: calc(100% + 2em);
}
}
}

View file

@ -0,0 +1,478 @@
// scss-lint:disable SelectorDepth NestingDepth IdSelector
#experimentTable {
--content-header-size: 5em;
--toolbar-height: 4.5em;
position: relative;
.title-row {
.header-actions {
&.experiment-header {
column-gap: .25em;
}
.sort-task-menu {
&:not(.archived) {
[data-view-mode="archived"] {
display: none;
}
}
}
}
}
.experiment-table-container {
height: calc(100vh - var(--content-header-size) - var(--navbar-height) - var(--toolbar-height));
overflow: auto;
}
.toolbar-row {
align-items: center;
display: flex;
height: var(--toolbar-height);
.toolbar-left-block {
display: flex;
.btn {
margin-right: .25em;
}
}
.toolbar-right-block {
margin-left: auto;
}
}
.experiment-table {
display: grid;
grid-auto-rows: 3em 1px;
grid-template-columns: max-content repeat(calc(var(--columns-count)), minmax(max-content, auto)) max-content;
min-width: 100%;
.table-header-cell {
align-items: center;
background-color: $color-concrete;
border: 1px solid $color-white;
display: flex;
height: 3em;
padding: 0 .5em;
position: sticky;
position: -webkit-sticky;
top: 0;
z-index: 2;
&.select-all-checkboxes {
justify-content: center;
}
.fa-comment {
color: $color-silver-chalice;
}
}
.table-header {
display: contents;
height: 10px;
&::after {
content: "";
grid-column: 1/-1;
}
}
.table-body {
display: contents;
}
.loading-overlay {
display: none;
}
.table-row-provisioning {
.loading-overlay {
display: block;
}
.sci-checkbox-container {
height: 1.5em;
width: 1.5em;
.loading-overlay::after {
background-size: 1.5em;
cursor: default;
}
.sci-checkbox,
.sci-checkbox-label {
display: none;
}
}
}
.table-body-cell {
align-items: center;
display: flex;
padding: 0 .5em;
.my-module-users-link {
color: $color-silver-chalice;
&:hover {
text-decoration: none;
}
}
.global-avatar-container {
color: $color-silver-chalice;
height: 2em;
line-height: 2em;
margin-right: .25em;
width: 2em;
}
.more-users {
background: $color-volcano;
border-radius: 50%;
color: $color-white;
height: 2em;
line-height: 2em;
margin-right: .25em;
text-align: center;
text-decoration: none;
width: 2em;
}
.new-user {
background: $color-concrete;
text-align: center;
}
}
.archived-column {
display: none;
}
.table-row {
display: contents;
&:hover {
.table-body-cell {
background-color: $color-concrete;
}
}
&::after {
background: $color-concrete;
content: "";
display: inline-block;
grid-column: 1/-1;
height: 1px;
}
}
.assign-users-dropdown {
.dropdown-menu {
padding: .5em;
width: 280px;
}
.users-list {
max-height: 300px;
overflow: auto;
}
.user-container {
align-items: center;
display: flex;
padding: .5em;
.user-avatar {
padding: 0 .75em;
img {
border-radius: 50%;
}
}
}
.assigned-users-container {
cursor: pointer;
display: flex;
}
.avatar-container {
border: 1px solid $color-white;
border-radius: 50%;
display: inline-block;
height: 26px;
margin-right: -5px;
width: 26px;
img {
border-radius: 50%;
max-height: 100%;
max-width: 100%;
}
}
.more-users {
font-size: 10px;
line-height: 24px;
}
.new-user {
color: $color-silver-chalice;
line-height: 24px;
margin-left: 5px;
}
}
.my-module-status {
color: $color-white;
display: inline-block;
margin: 3px 0;
max-width: 100%;
overflow: hidden;
padding: 2px 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
.table-row-placeholder {
align-items: center;
background-color: $color-white;
border-radius: $border-radius-default;
box-shadow: $flyout-shadow;
display: contents;
.placeholder-cell {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-name: placeholder-pulsing;
background-color: $color-alto;
border-radius: $border-radius-default;
height: 18px;
margin: auto;
&.line-0 {
display: block;
grid-column: 2;
width: 90%;
}
&.line-1 {
display: block;
grid-column: 3;
width: 90%;
}
&.line-2 {
display: block;
grid-column: 4;
width: 90%;
}
&.line-3 {
display: block;
grid-column: 5;
width: 90%;
}
&.line-4 {
display: block;
grid-column: 6;
width: 90%;
}
&.line-5 {
display: block;
grid-column: 7;
width: 90%;
}
&.line-6 {
display: block;
grid-column: 9;
width: 90%;
}
&.line-7 {
display: block;
grid-column: 10;
width: 90%;
}
&.circle-0 {
border-radius: 100%;
display: block;
grid-column: 8;
height: 24px;
margin-left: 8px;
width: 24px;
}
@keyframes placeholder-pulsing {
0% {
opacity: 1;
}
50% {
opacity: .5;
}
100% {
opacity: 1;
}
}
}
}
&.last-page {
padding-bottom: 5em;
position: relative;
}
.experiment-table-list-end-placeholder {
align-items: center;
background-color: $color-concrete;
bottom: 1em;
display: flex;
height: 3em;
left: calc(50% - 150px);
margin: 0 auto;
padding: 1em;
position: absolute;
width: 300px;
> * {
flex-grow: 1;
text-align: center;
}
}
}
.unseen-comments {
@include font-small;
align-items: center;
background-color: $brand-complementary;
border: 2px solid $color-white;
border-radius: 50%;
color: $color-black;
display: flex;
font-weight: bold;
height: 16px;
justify-content: center;
margin-bottom: 10px;
margin-left: -1px;
min-width: 16px;
}
.datetime-container {
width: 100%;
.clear-date {
cursor: pointer;
left: 90%;
position: absolute;
text-align: center;
top: 0;
visibility: hidden;
width: 16px;
z-index: 999;
&.open {
visibility: visible;
}
}
.date-text {
display: block;
position: relative;
.alert-yellow {
color: $brand-warning;
margin-left: 4px;
}
.alert-red {
color: $brand-danger;
margin-left: 4px;
}
}
.datetime-picker-container {
left: 0;
position: absolute;
top: 0;
width: 100%;
.calendar-due-date {
opacity: 0;
}
}
&:hover {
.date-text[data-editable=true] {
background-color: $color-concrete;
border-radius: 4px;
}
}
}
.open-comments-sidebar {
display: contents;
margin-bottom: 0;
}
&.archived {
.table-body-cell {
background-color: $color-concrete;
}
.archived-column {
display: flex;
}
}
}
.task_name-column span {
color: $color-silver-chalice;
}
.table-display-modal {
.column-container {
align-items: center;
border-bottom: $border-default;
display: flex;
padding: .5em 1em;
&:not(.visible) {
color: $color-alto;
}
&:last-child {
border: 0;
}
.fas {
cursor: pointer;
margin-right: 1em;
&.disabled {
color: $color-alto;
pointer-events: none;
}
}
&.task_name {
padding-left: 3em;
.fas {
display: none;
}
}
}
}

View file

@ -48,12 +48,13 @@
.cards-wrapper {
--card-min-width: 350px;
--list-columns-number: 8;
grid-auto-rows: auto;
.card {
grid-row: span 7;
align-items: center;
.experiment-code-cell {
display: none;
display: block;
}
&.experiment-card {
@ -85,31 +86,25 @@
.experiment-name-cell {
@include font-h3;
-webkit-box-orient: vertical;
display: -webkit-box;
height: 3em;
-webkit-line-clamp: 2;
margin: .25em 1.75em -.25em;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 1em;
width: 100%;
a {
color: inherit;
display: inline-block;
line-height: 21px;
}
}
.actions {
position: absolute;
right: .2em;
top: .2em;
a {
-webkit-box-orient: vertical;
color: inherit;
display: -webkit-box;
height: 3em;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
}
.dates-and-img-container {
display: flex;
height: 7em;
height: 6em;
width: 100%;
.dates-container {
@ -117,11 +112,16 @@
}
}
.actions-cell {
height: 36px;
margin-left: auto;
}
.data-row {
display: flex;
line-height: 34px;
.card-label {
color: $color-silver-chalice;
width: 7em;
}
@ -156,7 +156,7 @@
&::after {
background: linear-gradient(to right, $color-transparent, $color-white 50%);
bottom: .75em;
bottom: .4em;
content: "";
height: 1.75em;
position: absolute;
@ -167,7 +167,7 @@
}
.more-button {
bottom: .8em;
bottom: .5em;
position: absolute;
right: 1em;
}
@ -248,7 +248,7 @@
}
&.list {
grid-auto-rows: 1px 5em;
grid-auto-rows: 1px minmax(3em, auto);
grid-template-columns: max-content minmax(100px, auto) minmax(80px, max-content) repeat(calc(var(--list-columns-number) - 4), minmax(100px, auto)) max-content;
grid-template-rows: 3em;
@ -262,9 +262,10 @@
.workflow-img-wrapper {
flex-shrink: 0;
height: 3.5em;
margin: .25em 1em .25em .5em;
width: 3.5em;
height: 3em;
margin: 0 .75em 0 .25em;
padding: .25em;
width: 3em;
.archived-icon-plceholder {
font-size: 2em;
@ -275,7 +276,7 @@
text-align: center;
img {
max-height: 3em;
max-height: 2.5em;
}
}
}
@ -308,6 +309,8 @@
.experiment-code-cell {
display: block;
grid-column: 3;
line-height: 1.25em;
padding-top: .5em;
}
.start-date-cell {
@ -327,8 +330,8 @@
position: relative;
.description-text {
height: 4.5em;
-webkit-line-clamp: 3;
height: 3em;
-webkit-line-clamp: 2;
&::after {
bottom: .5em;
@ -343,6 +346,7 @@
.actions-cell {
grid-column: 8;
height: 100%;
padding-top: 3px;
position: initial;
}
@ -468,7 +472,10 @@
.cards-wrapper {
.card.experiment-card {
.workflow-img-wrapper {
align-items: center;
background-color: $color-alto;
display: flex;
justify-content: center;
}
.progress-bar {

View file

@ -456,6 +456,7 @@
.calendar-input {
background-color: transparent !important;
border-color: $color-silver-chalice;
box-shadow: none;
color: inherit;
cursor: pointer;

View file

@ -52,6 +52,21 @@
}
.module-large {
.nav-tabs {
display: flex;
}
.task-code {
align-items: center;
color: $color-volcano;
display: flex;
height: 30px;
margin-left: auto;
padding-right: .5em;
text-align: end;
}
.description-label {
word-break: break-all;
word-wrap: break-word;
@ -63,6 +78,12 @@
}
}
.archived-task-card-code {
bottom: 30px;
position: absolute;
right: 25px;
}
// Create wopi file
.create-wopi-file-btn {
cursor: pointer;
@ -214,17 +235,16 @@
}
#experiment-canvas {
[data-view-mode="archived"] {
display: none;
}
}
#module-archive {
[data-view-mode="active"] {
display: none;
}
.toolbar {
margin-top: 1em;
}
.module-container {
min-width: 220px;

View file

@ -93,7 +93,7 @@
.repositories-dropdown-menu {
max-height: 250px;
overflow: auto;
.repository {
@include font-button;
cursor: pointer;
@ -420,9 +420,31 @@
max-width: 100vw;
width: 650px;
.dropdown-header,
.dropdown-body {
padding: 10px 32px;
}
.dropdown-header {
background: $color-white;
border-bottom: $border-tertiary;
.protocol-name {
color: $color-black;
margin-top: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.protocol-header-info {
color: $color-black;
font-size: .875em;
font-weight: 400;
}
}
.dropdown-body {
border-bottom: $border-tertiary;
padding: 10px 32px;
.info-line {
align-items: center;
@ -647,3 +669,8 @@
}
}
}
.task-details-code {
display: inline-block;
margin-left: 4px;
}

View file

@ -21,7 +21,7 @@ $color-module-hover: $brand-primary;
align-items: center;
display: flex;
#edit-canvas-button {
#edit-canvas-button, .new-my-module-button {
margin-right: 5px;
}
@ -48,9 +48,8 @@ $color-module-hover: $brand-primary;
}
#update-canvas {
.canvas-header {
margin-bottom: 5px;
padding: 1em 2em;
}
}
@ -89,6 +88,14 @@ $color-module-hover: $brand-primary;
overflow: hidden;
// for IE10+ touch devices
touch-action: none;
.empty-canvas {
color: $color-volcano;
display: flex;
font-size: 22px;
justify-content: center;
margin-top: 48px;
}
}
.diagram {
@ -192,12 +199,24 @@ path, ._jsPlumb_endpoint {
left: 0;
}
}
.panel-body {
height: 90px;
}
.ep {
font-style: italic;
.task-card-code {
color: $color-volcano;
font-style: normal;
font-weight: 400;
margin-bottom: 4px;
.new-my-module-canvas {
color: $color-silver-chalice;
}
}
}
.dropdown {
@ -597,17 +616,6 @@ li.module-hover {
}
}
}
.dropdown-option.users-dropdown-list {
padding: 8px 10px;
.item-avatar {
border-radius: 50%;
height: 32px;
margin: 0 16px 0 0;
width: 32px;
}
}
}
.projects-toolbar {
@ -666,7 +674,6 @@ li.module-hover {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
@ -689,18 +696,13 @@ li.module-hover {
.project-name-cell {
align-items: center;
display: flex;
height: 2em;
margin: 0 1.75em;
overflow: hidden;
a {
color: inherit;
overflow: hidden;
white-space: nowrap;
}
.name {
line-height: 2em;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
@ -713,6 +715,8 @@ li.module-hover {
top: .2em;
.project-actions-menu {
height: 40px;
a {
@include font-button;
padding: .5em 1em;
@ -775,6 +779,7 @@ li.module-hover {
height: 2em;
line-height: 2em;
margin-right: .25em;
position: unset;
width: 2em;
}
@ -854,7 +859,8 @@ li.module-hover {
}
&.list {
grid-template-columns: max-content repeat(calc(var(--list-columns-number) - 3), minmax(100px, auto)) max-content max-content;
grid-auto-rows: minmax(3em, auto) 1px;
grid-template-columns: max-content repeat(calc(var(--list-columns-number) - 2), minmax(100px, auto)) max-content max-content;
.projects-group {
display: contents;
@ -902,7 +908,7 @@ li.module-hover {
}
.name {
grid-column: 5 span;
grid-column: 6 span;
line-height: 3em;
&:before {
@ -926,28 +932,39 @@ li.module-hover {
grid-column: 2;
height: 100%;
margin: 0;
padding: 5px 0;
h3 {
line-height: 1.25em;
}
a:hover {
color: $brand-primary-light;
}
}
.start-date-cell {
.project-code-cell {
display: flex;
align-items: center;
grid-column: 3;
}
.visibility-cell {
.start-date-cell {
grid-column: 4;
}
.user-cell {
.visibility-cell {
grid-column: 5;
}
.user-cell {
grid-column: 6;
}
.actions-cell {
align-items: center;
display: flex;
grid-column: 6;
grid-column: 7;
position: initial;
}
}
@ -1017,8 +1034,11 @@ li.module-hover {
display: none !important;
}
.projects-container {
.project-actions-menu {
height: 40px;
.btn-light:hover {
background: $color-alto;
}
@ -1027,6 +1047,58 @@ li.module-hover {
.cards-wrapper {
grid-auto-rows: 2.5em;
.visibility-cell {
.value {
color: $color-black;
}
}
.start-date-cell {
.value {
color: $color-black;
}
}
.archived-date-cell {
z-index: 2;
.value {
color: $color-black;
}
}
&.last-page.cards {
.project-card {
.start-date-cell {
top: 36px;
.value {
color: $color-black;
}
}
.archived-date-cell {
top: 26px;
.value {
color: $color-black;
}
}
.visibility-cell {
top: 16px;
.value {
color: $color-black;
}
}
.user-cell {
top: 6px;
}
}
}
.card.project-card {
.data-row {
color: $color-silver-chalice;
@ -1046,25 +1118,33 @@ li.module-hover {
}
&.list {
--list-columns-number: 7;
grid-auto-rows: 3em 1px;
grid-template-columns: max-content repeat(calc(var(--list-columns-number) - 2), minmax(100px, auto)) max-content;
--list-columns-number: 8;
grid-auto-rows: minmax(3em, auto) 1px;
grid-template-columns: max-content repeat(calc(var(--list-columns-number) - 3), minmax(100px, auto)) max-content max-content;
.card {
&.folder-card {
.name {
grid-column: 6 span;
grid-column: 7 span;
}
}
.user-cell {
grid-column: 7;
}
.archived-date-cell {
grid-column: 6;
grid-column: 5;
}
.actions-cell {
grid-column: 7;
grid-column: 8;
position: initial;
}
.visibility-cell {
grid-column: 6;
}
}
}
}
@ -1082,3 +1162,56 @@ li.module-hover {
margin: 1em 0;
}
}
.cards-wrapper.cards {
grid-gap: 25px;
.project-card {
.project-name-cell {
align-items: start !important;
top: 12%;
min-height: 35px;
position: absolute;
left: 14px;
top: 42px;
a {
color: inherit;
}
.name {
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.project-code-cell {
height: min-content;
margin-top: 3px;
position: relative;
}
.data-row {
position: relative;
top: 5%;
.card-label {
color: #808080 !important;
}
&.start-date-cell {
top: 30px;
}
&.visibility-cell {
top: 18px;
}
&.user-cell {
top: 5px;
}
}
}
}

View file

@ -303,7 +303,6 @@
#dropdownAssetContextMenu {
background: $color-white;
&:focus,
&:active {
box-shadow: none;
}

View file

@ -59,9 +59,6 @@
display: flex;
height: 2em;
justify-content: center;
left: .5em;
position: absolute;
top: .5em;
width: 2em;
}
}

View file

@ -83,9 +83,24 @@
margin-right: .5em;
}
&:hover {
&:hover:not(.divider-label) {
background: $color-concrete;
}
a {
display: inline-block;
margin: -1em;
padding: 1em;
width: calc(100% + 2em);
&.selected::after {
@include font-awesome;
content: $font-fas-check;
margin-left: auto;
position: absolute;
right: 1em;
}
}
}
}

View file

@ -44,6 +44,20 @@
}
}
.item-avatar {
border-radius: 50%;
}
.dropdown-option.users-dropdown-list {
padding: 8px 10px;
.item-avatar {
height: 32px;
margin: 0 16px 0 0;
width: 32px;
}
}
.recent-searches {
border-top-left-radius: 0;
border-top-right-radius: 0;

View file

@ -25,10 +25,11 @@
.step {
.panel {
border: 0;
margin-left: 0;
.panel-body {
padding: 15px 5px;
padding: 15px 24px;
}
}
}

View file

@ -10,8 +10,8 @@
.enable-edit-mode {
cursor: pointer;
display: none;
justify-content: flex-end;
opacity: 0;
padding: 12px;
position: absolute;
right: 0;
@ -49,6 +49,7 @@
.enable-edit-mode {
display: flex;
opacity: 1;
}
}
}

View file

@ -28,7 +28,7 @@
$color-concrete 100%
);
border-radius: 4px;
display: none;
opacity: 0;
padding-left: 2em;
position: absolute;
right: 0;
@ -55,6 +55,7 @@
.buttons-container {
display: flex;
opacity: 1;
}
.step-element-grip {

View file

@ -686,47 +686,6 @@ ul.double-line > li {
}
}
#canvas-container,
#module-archive {
.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: $brand-accent;
border-radius: $border-radius-tag;
color: $color-black;
font-size: 10px;
margin-left: -8px;
}
}
}
.panel-options {
position: relative;
bottom: 8px;

View file

@ -218,7 +218,7 @@ class AssetsController < ApplicationController
log_step_activity(
:task_step_file_deleted,
@assoc,
@assoc.my_module.experiment.project,
@assoc.my_module.project,
my_module: @assoc.my_module.id,
file: @asset.file_name
)
@ -298,7 +298,7 @@ class AssetsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: step.protocol,
team: current_team,
team: step.protocol.team,
project: project,
message_items: message_items)
end
@ -308,8 +308,8 @@ class AssetsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: result,
team: result.my_module.experiment.project.team,
project: result.my_module.experiment.project,
team: result.my_module.team,
project: result.my_module.project,
message_items: {
result: result.id,
type_of_result: t('activities.result_type.text')

View file

@ -41,8 +41,8 @@ module AssetsActions
.call(activity_type: :edit_image_on_result,
owner: current_user,
subject: asset.result,
team: my_module.experiment.project.team,
project: my_module.experiment.project,
team: my_module.team,
project: my_module.project,
message_items: {
result: asset.result.id,
asset_name: { id: asset.id, value_for: 'file_name' },

View file

@ -104,8 +104,8 @@ module BioEddieActions
.call(activity_type: "#{activity}_molecule_on_result".to_sym,
owner: current_user,
subject: result,
team: my_module.experiment.project.team,
project: my_module.experiment.project,
team: my_module.team,
project: my_module.project,
message_items: message_items)
end
end

View file

@ -118,8 +118,8 @@ module MarvinJsActions
.call(activity_type: (activity + '_chemical_structure_on_result').to_sym,
owner: current_user,
subject: result,
team: my_module.experiment.project.team,
project: my_module.experiment.project,
team: my_module.team,
project: my_module.project,
message_items: message_items)
end
@ -137,8 +137,8 @@ module MarvinJsActions
.call(activity_type: (activity + '_chemical_structure_on_task').to_sym,
owner: current_user,
subject: my_module,
team: my_module.experiment.project.team,
project: my_module.experiment.project,
team: my_module.team,
project: my_module.project,
message_items: message_items)
end

View file

@ -55,6 +55,17 @@ module StepsActions
)
end
def step_text_annotation(step, step_text, old_text = nil)
smart_annotation_notification(
old_text: old_text,
new_text: step_text.text,
title: t('notifications.step_text_title',
user: current_user.full_name,
step: step.name),
message: annotation_message(step)
)
end
def checklist_name_annotation(step, checklist, old_text = nil)
smart_annotation_notification(
old_text: old_text,

View file

@ -12,12 +12,12 @@ class ExperimentsController < ApplicationController
before_action :check_read_permissions, except: %i(edit archive clone move new create archive_group restore_group)
before_action :check_canvas_read_permissions, only: %i(canvas)
before_action :check_create_permissions, only: %i(new create)
before_action :check_manage_permissions, only: %i(edit)
before_action :check_manage_permissions, only: %i(edit batch_clone_my_modules)
before_action :check_update_permissions, only: %i(update)
before_action :check_archive_permissions, only: :archive
before_action :check_clone_permissions, only: %i(clone_modal clone)
before_action :check_move_permissions, only: %i(move_modal move)
before_action :set_inline_name_editing, only: %i(canvas module_archive)
before_action :set_inline_name_editing, only: %i(canvas table module_archive)
layout 'fluid'
@ -88,6 +88,32 @@ class ExperimentsController < ApplicationController
.select('my_modules.*').group(:id)
end
def table
view_state = @experiment.current_view_state(current_user)
view_mode = params[:view_mode] || 'active'
@current_sort = view_state.state.dig('my_modules', view_mode, 'sort') || 'atoz'
@project = @experiment.project
@my_modules = if @experiment.archived?
@experiment.my_modules.order(:name)
elsif params[:view_mode] == 'archived'
@experiment.my_modules.archived.order(:name)
else
@experiment.my_modules.active.order(:name)
end
@my_module_visible_table_columns = current_user.my_module_visible_table_columns
end
def load_table
my_modules = @experiment.my_modules.readable_by_user(current_user)
unless @experiment.archived?
my_modules = params[:view_mode] == 'archived' ? my_modules.archived : my_modules.active
end
render json: Experiments::TableViewService.new(@experiment, my_modules, current_user, params).call
end
def edit
respond_to do |format|
format.json do
@ -208,7 +234,8 @@ class ExperimentsController < ApplicationController
format.json do
render json: {
html: render_to_string(
partial: 'clone_modal.html.erb'
partial: 'clone_modal.html.erb',
locals: { view_mode: params[:view_mode] }
)
}
end
@ -227,7 +254,8 @@ class ExperimentsController < ApplicationController
if service.succeed?
flash[:success] = t('experiments.clone.success_flash',
experiment: @experiment.name)
redirect_to canvas_experiment_path(service.cloned_experiment)
redirect_to canvas_experiment_path(service.cloned_experiment) if params[:view_mode] == 'canvas'
redirect_to table_experiment_path(service.cloned_experiment) if params[:view_mode] == 'table'
else
flash[:error] = t('experiments.clone.error_flash',
experiment: @experiment.name)
@ -249,6 +277,21 @@ class ExperimentsController < ApplicationController
end
end
def search_tags
tags = @experiment.project.tags.where.not(id: JSON.parse(params[:selected_tags]))
.where_attributes_like(:name, params[:query])
.select(:id, :name, :color)
tags = tags.map do |tag|
{ value: tag.id, label: sanitize_input(tag.name), params: { color: sanitize_input(tag.color) } }
end
if params[:query].present? && tags.select { |tag| tag[:label] == params[:query] }.blank?
tags << { value: 0, label: sanitize_input(params[:query]), params: { color: nil } }
end
render json: tags
end
# POST: move_experiment(id)
def move
service = Experiments::MoveToProjectService
@ -258,17 +301,54 @@ class ExperimentsController < ApplicationController
if service.succeed?
flash[:success] = t('experiments.move.success_flash',
experiment: @experiment.name)
path = canvas_experiment_url(@experiment)
status = :ok
else
message = service.errors.values.join(', ')
status = :unprocessable_entity
end
render json: { message: message, path: path }, status: status
render json: { message: message }, status: status
end
def move_modules_modal
@experiments = @experiment.project.experiments.active.where.not(id: @experiment)
.managable_by_user(current_user).order(name: :asc)
render json: {
html: render_to_string(
partial: 'move_modules_modal.html.erb'
)
}
end
def move_modules
modules_to_move = {}
dst_experiment = @experiment.project.experiments.find(params[:to_experiment_id])
return render_403 unless can_manage_experiment?(dst_experiment)
@experiment.with_lock do
params[:my_module_ids].each do |id|
my_module = @experiment.my_modules.find(id)
return render_403 unless can_move_my_module?(my_module)
modules_to_move[id] = dst_experiment.id
end
@experiment.move_modules(modules_to_move, current_user)
render json: { message: t('experiments.table.modal_move_modules.success_flash',
experiment: sanitize_input(dst_experiment.name)) }
rescue StandardError => e
Rails.logger.error(e.message)
Rails.logger.error(e.backtrace.join("\n"))
render json: {
message: t('experiments.table.modal_move_modules.error_flash', experiment: sanitize_input(dst_experiment.name))
}, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
rescue ActiveRecord::RecordNotFound
render_404
end
def module_archive
@project = @experiment.project
@my_modules = @experiment.archived_branch? ? @experiment.my_modules : @experiment.my_modules.archived
@my_modules = @my_modules.with_granted_permissions(current_user, MyModulePermissions::READ_ARCHIVED)
.left_outer_joins(:designated_users, :task_comments)
@ -321,6 +401,63 @@ class ExperimentsController < ApplicationController
end
end
def assigned_users_to_tasks
users = current_team.users.where(id: @experiment.my_modules.joins(:user_my_modules).select(:user_id))
.search(false, params[:query]).map do |u|
{ value: u.id, label: sanitize_input(u.name), params: { avatar_url: avatar_path(u, :icon_small) } }
end
render json: users, status: :ok
end
def archive_my_modules
my_modules = @experiment.my_modules.where(id: params[:my_modules])
counter = 0
my_modules.each do |my_module|
next unless can_archive_my_module?(my_module)
my_module.transaction do
my_module.archive!(current_user)
log_my_module_activity(:archive_module, my_module)
counter += 1
rescue StandardError => e
Rails.logger.error e.message
raise ActiveRecord::Rollback
end
end
if counter.positive?
render json: { message: t('experiments.table.archive_group.success_flash', number: counter) }
else
render json: { message: t('experiments.table.archive_group.error_flash') }, status: :unprocessable_entity
end
end
def batch_clone_my_modules
MyModule.transaction do
@my_modules =
@experiment.my_modules
.readable_by_user(current_user)
.where(id: params[:my_module_ids])
@my_modules.find_each do |my_module|
new_my_module = my_module.dup
new_my_module.update!(
{
provisioning_status: :in_progress,
name: my_module.next_clone_name
}.merge(new_my_module.get_new_position)
)
MyModules::CopyContentJob.perform_later(current_user, my_module.id, new_my_module.id)
end
end
render(
json: {
provisioning_status_urls: @my_modules.map { |m| provisioning_status_my_module_url(m) }
}
)
end
private
def load_experiment
@ -410,9 +547,19 @@ class ExperimentsController < ApplicationController
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
team: experiment.project.team,
team: experiment.team,
project: experiment.project,
subject: experiment,
message_items: { experiment: experiment.id })
end
def log_my_module_activity(type_of, my_module)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
team: my_module.experiment.project.team,
project: my_module.experiment.project,
subject: my_module,
message_items: { my_module: my_module.id })
end
end

View file

@ -135,7 +135,7 @@ class MyModuleRepositoriesController < ApplicationController
activity_type: :export_inventory_items_assigned_to_task,
owner: current_user,
subject: @my_module,
team: current_team,
team: @repository.team,
message_items: {
my_module: @my_module.id,
repository: @repository.id
@ -251,7 +251,7 @@ class MyModuleRepositoriesController < ApplicationController
owner: current_user,
subject: @my_module,
team: @repository.team,
project: @my_module.experiment.project,
project: @my_module.project,
message_items: {
repository: @repository.id,
repository_row: module_repository_row.repository_row_id,

View file

@ -71,12 +71,7 @@ class MyModuleRepositorySnapshotsController < ApplicationController
end
def full_view_sidebar
@repository = Repository.find_by(id: params[:repository_id])
if @repository
return render_403 unless can_read_repository?(@repository)
end
@repository = Repository.viewable_by_user(current_user, current_team).find_by(id: params[:repository_id])
@repository_snapshots = @my_module.repository_snapshots
.where(parent_id: params[:repository_id])
.order(created_at: :desc)
@ -114,7 +109,7 @@ class MyModuleRepositorySnapshotsController < ApplicationController
activity_type: :export_inventory_snapshot_items_assigned_to_task,
owner: current_user,
subject: @my_module,
team: current_team,
team: @my_module.team,
message_items: {
my_module: @my_module.id,
repository_snapshot: @repository_snapshot.id,

View file

@ -70,9 +70,8 @@ class MyModuleTagsController < ApplicationController
.call(activity_type: :add_task_tag,
owner: current_user,
subject: my_module,
project:
my_module.experiment.project,
team: current_team,
project: my_module.project,
team: my_module.team,
message_items: {
my_module: my_module.id,
tag: @mt.tag.id
@ -95,9 +94,8 @@ class MyModuleTagsController < ApplicationController
.call(activity_type: :remove_task_tag,
owner: current_user,
subject: @mt.my_module,
project:
@mt.my_module.experiment.project,
team: current_team,
project: @mt.my_module.project,
team: @mt.my_module.team,
message_items: {
my_module: @mt.my_module.id,
tag: @mt.tag.id
@ -139,9 +137,8 @@ class MyModuleTagsController < ApplicationController
.call(activity_type: :remove_task_tag,
owner: current_user,
subject: tag.my_module,
project:
tag.my_module.experiment.project,
team: current_team,
project: tag.my_module.project,
team: tag.my_module.team,
message_items: {
my_module: tag.my_module.id,
tag: tag.tag.id

View file

@ -5,19 +5,56 @@ class MyModulesController < ApplicationController
include Rails.application.routes.url_helpers
include ActionView::Helpers::UrlHelper
include ApplicationHelper
include MyModulesHelper
before_action :load_vars, except: %i(restore_group)
before_action :load_vars, except: %i(restore_group create new save_table_state)
before_action :load_experiment, only: %i(create new)
before_action :check_create_permissions, only: %i(new create)
before_action :check_archive_permissions, only: %i(update)
before_action :check_manage_permissions, only: %i(
description due_date update_description update_protocol_description update_protocol
)
before_action :check_read_permissions, except: %i(update update_description update_protocol_description restore_group)
before_action :check_read_permissions, except: %i(create new update update_description
update_protocol_description restore_group save_table_state)
before_action :check_update_state_permissions, only: :update_state
before_action :set_inline_name_editing, only: %i(protocols results activities archive)
before_action :load_experiment_my_modules, only: %i(protocols results activities archive)
layout 'fluid'.freeze
def new
@my_module = @experiment.my_modules.new
assigned_users = User.where(id: @experiment.user_assignments.select(:user_id))
render json: {
html: render_to_string(
partial: 'my_modules/modals/new_modal.html.erb', locals: { view_mode: params[:view_mode],
users: assigned_users }
)
}
end
def create
max_xy = @experiment.my_modules.select('MAX("my_modules"."x") AS x, MAX("my_modules"."y") AS y').take
x = max_xy.x ? (max_xy.x + 10) : 1
y = max_xy.y ? (max_xy.y + 10) : 1
@my_module = @experiment.my_modules.new(my_module_params)
@my_module.assign_attributes(created_by: current_user, last_modified_by: current_user, x: x, y: y)
@my_module.transaction do
if my_module_tags_params[:tag_ids].present?
@my_module.tags << @experiment.project.tags.where(id: JSON.parse(my_module_tags_params[:tag_ids]))
end
if my_module_designated_users_params[:user_ids].present?
@my_module.designated_users << @experiment.users.where(id: my_module_designated_users_params[:user_ids])
end
@my_module.save!
redirect_to canvas_experiment_path(@experiment) if params[:my_module][:view_mode] == 'canvas'
rescue ActiveRecord::RecordInvalid
render json: @my_module.errors, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
def show
respond_to do |format|
format.json {
@ -46,6 +83,11 @@ class MyModulesController < ApplicationController
end
end
def save_table_state
current_user.settings.update(visible_my_module_table_columns: params[:columns])
current_user.save!
end
def status_state
respond_to do |format|
format.json do
@ -195,6 +237,11 @@ class MyModulesController < ApplicationController
partial: 'my_modules/card_due_date_label.html.erb',
locals: { my_module: @my_module }
),
table_due_date_label: {
html: render_to_string(partial: 'experiments/table_due_date_label.html.erb',
locals: { my_module: @my_module, user: current_user }),
due_status: my_module_due_status(@my_module)
},
module_header_due_date: render_to_string(
partial: 'my_modules/module_header_due_date.html.erb',
locals: { my_module: @my_module }
@ -276,12 +323,14 @@ class MyModulesController < ApplicationController
def update_protocol
protocol = @my_module.protocol
old_description = protocol.description
ActiveRecord::Base.transaction do
protocol.update!(protocol_params)
log_activity(:protocol_name_in_task_edited) if protocol.saved_change_to_name?
log_activity(:protocol_description_in_task_edited) if protocol.saved_change_to_description?
TinyMceAsset.update_images(protocol, params[:tiny_mce_images], current_user)
protocol_annotation_notification(old_description)
end
render json: protocol, serializer: ProtocolSerializer, user: current_user
@ -296,10 +345,12 @@ class MyModulesController < ApplicationController
@results = @results.page(params[:page]).per(Constants::RESULTS_PER_PAGE_LIMIT)
@results = case @results_order
when 'old' then @results.order(updated_at: :asc)
when 'old' then @results.order(created_at: :asc)
when 'old_updated' then @results.order(updated_at: :asc)
when 'new_updated' then @results.order(updated_at: :desc)
when 'atoz' then @results.order(name: :asc)
when 'ztoa' then @results.order(name: :desc)
else @results.order(updated_at: :desc)
else @results.order(created_at: :desc)
end
end
@ -332,7 +383,12 @@ class MyModulesController < ApplicationController
else
flash[:error] = t('my_modules.restore_group.error_flash')
end
redirect_to module_archive_experiment_path(experiment)
if params[:view] == 'table'
redirect_to table_experiment_path(experiment, view_mode: :archived)
else
redirect_to module_archive_experiment_path(experiment)
end
end
def update_state
@ -347,6 +403,32 @@ class MyModulesController < ApplicationController
end
end
def permissions
if stale?(@my_module)
render json: {
editable: can_manage_my_module?(@my_module),
moveable: can_move_my_module?(@my_module),
archivable: can_archive_my_module?(@my_module),
restorable: can_restore_my_module?(@my_module)
}
end
end
def actions_dropdown
if stale?(@my_module)
render json: {
html: render_to_string(
partial: 'experiments/table_row_actions',
locals: { my_module: @my_module }
)
}
end
end
def provisioning_status
render json: { provisioning_status: @my_module.provisioning_status }
end
private
def load_vars
@ -359,6 +441,11 @@ class MyModulesController < ApplicationController
end
end
def load_experiment
@experiment = Experiment.preload(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @experiment
end
def load_experiment_my_modules
@experiment_my_modules = if @my_module.experiment.archived_branch?
@my_module.experiment.my_modules.order(:name)
@ -367,6 +454,10 @@ class MyModulesController < ApplicationController
end
end
def check_create_permissions
render_403 && return unless can_manage_experiment?(@experiment)
end
def check_manage_permissions
render_403 && return unless can_manage_my_module?(@my_module)
end
@ -399,18 +490,26 @@ class MyModulesController < ApplicationController
end
def my_module_params
update_params = params.require(:my_module).permit(:name, :description, :started_on, :due_date, :archived)
permitted_params = params.require(:my_module).permit(:name, :description, :started_on, :due_date, :archived)
if update_params[:started_on].present?
update_params[:started_on] =
Time.zone.strptime(update_params[:started_on], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M')
if permitted_params[:started_on].present?
permitted_params[:started_on] =
Time.zone.strptime(permitted_params[:started_on], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M')
end
if update_params[:due_date].present?
update_params[:due_date] =
Time.zone.strptime(update_params[:due_date], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M')
if permitted_params[:due_date].present?
permitted_params[:due_date] =
Time.zone.strptime(permitted_params[:due_date], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M')
end
update_params
permitted_params
end
def my_module_tags_params
params.require(:my_module).permit(:tag_ids)
end
def my_module_designated_users_params
params.require(:my_module).permit(user_ids: [])
end
def protocol_params
@ -456,8 +555,8 @@ class MyModulesController < ApplicationController
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
team: my_module.experiment.project.team,
project: my_module.experiment.project,
team: my_module.team,
project: my_module.project,
subject: my_module,
message_items: message_items)
end

View file

@ -13,7 +13,7 @@ class ProjectsController < ApplicationController
before_action :switch_team_with_param, only: :index
before_action :load_vars, only: %i(show permissions edit update notifications
sidebar experiments_cards view_type actions_dropdown)
sidebar experiments_cards view_type actions_dropdown create_tag)
before_action :load_current_folder, only: %i(index cards new show)
before_action :check_view_permissions, except: %i(index cards new create edit update archive_group restore_group
users_filter actions_dropdown)
@ -264,6 +264,24 @@ class ProjectsController < ApplicationController
end
end
def create_tag
render_403 unless can_manage_project_tags?(@project)
@tag = @project.tags.create(tag_params.merge({
created_by: current_user,
last_modified_by: current_user,
color: Constants::TAG_COLORS.sample
}))
render json: {
tag: {
id: @tag.id,
name: @tag.name,
color: @tag.color
}
}
end
def restore_group
projects = current_team.projects.archived.where(id: params[:projects_ids])
counter = 0
@ -287,9 +305,6 @@ class ProjectsController < ApplicationController
end
def show
# This is the "info" view
current_team_switch(@project.team)
view_state = @project.current_view_state(current_user)
@current_sort = view_state.state.dig('experiments', experiments_view_mode(@project), 'sort') || 'atoz'
@current_view_type = view_state.state.dig('experiments', 'view_type')
@ -372,11 +387,15 @@ class ProjectsController < ApplicationController
end
def load_vars
@project = Project.find_by(id: params[:id])
@project = Project.find_by(id: params[:id] || params[:project_id])
render_404 unless @project
end
def tag_params
params.require(:tag).permit(:name)
end
def load_current_folder
if current_team && params[:project_folder_id].present?
@current_folder = current_team.project_folders.find_by(id: params[:project_folder_id])
@ -386,6 +405,7 @@ class ProjectsController < ApplicationController
end
def check_view_permissions
current_team_switch(@project.team) if current_team != @project.team
render_403 unless can_read_project?(@project)
end

View file

@ -16,6 +16,7 @@ class ProtocolsController < ApplicationController
before_action :check_clone_permissions, only: [:clone]
before_action :check_view_permissions, only: %i(
show
edit
protocol_status_bar
updated_at_label
preview
@ -30,7 +31,6 @@ class ProtocolsController < ApplicationController
# For update_from_parent and update_from_parent_modal we don't need to check
# read permission for the parent protocol
before_action :check_manage_permissions, only: %i(
edit
update_keywords
update_description
update_name
@ -161,13 +161,10 @@ class ProtocolsController < ApplicationController
end
def edit
# Switch to correct team
current_team_switch(@protocol.team)
render :show
end
def show
# Switch to correct team
current_team_switch(@protocol.team)
respond_to do |format|
format.json { render json: @protocol, serializer: ProtocolSerializer, user: current_user }
@ -622,7 +619,7 @@ class ProtocolsController < ApplicationController
.call(activity_type: :import_protocol_in_repository,
owner: current_user,
subject: protocol,
team: current_team,
team: protocol.team,
message_items: {
protocol: protocol.id
})
@ -820,15 +817,15 @@ class ProtocolsController < ApplicationController
file_name = 'protocols.eln'
end
@protocols.each do |p|
@protocols.each do |protocol|
if params[:my_module_id]
my_module = MyModule.find(params[:my_module_id])
Activities::CreateActivityService
.call(activity_type: :export_protocol_from_task,
owner: current_user,
project: my_module.experiment.project,
project: my_module.project,
subject: my_module,
team: current_team,
team: my_module.team,
message_items: {
my_module: params[:my_module_id].to_i
})
@ -836,10 +833,10 @@ class ProtocolsController < ApplicationController
Activities::CreateActivityService
.call(activity_type: :export_protocol_in_repository,
owner: current_user,
subject: p,
team: current_team,
subject: protocol,
team: protocol.team,
message_items: {
protocol: p.id
protocol: protocol.id
})
end
end
@ -1102,6 +1099,7 @@ class ProtocolsController < ApplicationController
def check_view_permissions
@protocol = Protocol.find_by_id(params[:id])
current_team_switch(@protocol.team) if current_team != @protocol.team
unless @protocol.present? &&
(can_read_protocol_in_module?(@protocol) ||
can_read_protocol_in_repository?(@protocol))
@ -1232,7 +1230,7 @@ class ProtocolsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @protocol,
team: current_team,
team: @protocol.team,
project: project,
message_items: message_items)
end
@ -1245,7 +1243,7 @@ class ProtocolsController < ApplicationController
user: current_user.full_name,
protocol: @protocol.name),
message: t('notifications.protocol_description_annotation_message_html',
protocol: link_to(@protocol.name, edit_protocol_url(@protocol)))
protocol: link_to(@protocol.name, protocol_url(@protocol)))
)
end
end

View file

@ -110,7 +110,7 @@ class ReportsController < ApplicationController
@project_contents = {
experiments: @report.report_elements.order(:position).experiment.pluck(:experiment_id),
my_modules: @report.report_elements.order(:position).my_module.pluck(:my_module_id),
repositories: @report.report_elements.my_module_repository.distinct(:repository_id).pluck(:repository_id)
repositories: @report.settings.dig(:task, :repositories)
}
render :new
end

View file

@ -521,7 +521,7 @@ class RepositoriesController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @repository,
team: current_team,
team: @repository.team,
message_items: message_items)
end
end

View file

@ -167,7 +167,7 @@ class RepositoryColumnsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @repository,
team: current_team,
team: @repository.team,
message_items: {
repository_column: @repository_column.id,
repository: @repository.id

View file

@ -432,7 +432,7 @@ class RepositoryRowsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: repository_row,
team: current_team,
team: @repository.team,
message_items: {
repository_row: repository_row.id,
repository: @repository.id

View file

@ -191,8 +191,8 @@ class ResultAssetsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: result,
team: @my_module.experiment.project.team,
project: @my_module.experiment.project,
team: @my_module.team,
project: @my_module.project,
message_items: {
result: result.id,
type_of_result: t('activities.result_type.asset')

View file

@ -180,8 +180,8 @@ class ResultTablesController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @result,
team: @my_module.experiment.project.team,
project: @my_module.experiment.project,
team: @my_module.team,
project: @my_module.project,
message_items: {
result: @result.id,
type_of_result: t('activities.result_type.table')

View file

@ -206,8 +206,8 @@ class ResultTextsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @result,
team: @my_module.experiment.project.team,
project: @my_module.experiment.project,
team: @my_module.team,
project: @my_module.project,
message_items: {
result: @result.id,
type_of_result: t('activities.result_type.text')

View file

@ -14,8 +14,8 @@ class ResultsController < ApplicationController
.call(activity_type: :destroy_result,
owner: current_user,
subject: @result,
team: @my_module.experiment.project.team,
project: @my_module.experiment.project,
team: @my_module.team,
project: @my_module.project,
message_items: { result: @result.id,
type_of_result: result_type })
flash[:success] = t('my_modules.module_archive.delete_flash',

View file

@ -78,8 +78,8 @@ class StepCommentsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @protocol,
team: current_team,
project: @step.my_module.experiment.project,
team: @step.my_module.team,
project: @step.my_module.project,
message_items: {
my_module: @step.my_module.id,
step: @step.id,

View file

@ -40,8 +40,8 @@ module StepElements
Activities::CreateActivityService.call(
activity_type: "#{!@step.protocol.in_module? ? 'protocol_step_' : 'task_step_'}#{element_type_of}",
owner: current_user,
team: @protocol.in_module? ? @protocol.my_module.experiment.project.team : @protocol.team,
project: @protocol.in_module? ? @protocol.my_module.experiment.project : nil,
team: @protocol.team,
project: @protocol.in_module? ? @protocol.my_module.project : nil,
subject: @protocol,
message_items: {
step: @step.id,

View file

@ -3,6 +3,7 @@
module StepElements
class ChecklistItemsController < ApplicationController
include ApplicationHelper
include StepsActions
before_action :load_vars
before_action :load_checklist_item, only: %i(update toggle destroy)
@ -21,6 +22,7 @@ module StepElements
checklist_name: @checklist.name
}
)
checklist_item_annotation(@step, checklist_item)
end
render json: checklist_item, serializer: ChecklistItemSerializer, user: current_user
@ -31,6 +33,7 @@ module StepElements
end
def update
old_text = @checklist_item.text
@checklist_item.assign_attributes(
checklist_item_params.merge(last_modified_by: current_user)
)
@ -41,6 +44,7 @@ module StepElements
checklist_item: @checklist_item.text,
checklist_name: @checklist.name
)
checklist_item_annotation(@step, @checklist_item, old_text)
end
render json: @checklist_item, serializer: ChecklistItemSerializer, user: current_user
@ -129,6 +133,8 @@ module StepElements
@step = Step.find_by(id: params[:step_id])
return render_404 unless @step
@protocol = @step.protocol
@checklist = @step.checklists.find_by(id: params[:checklist_id])
return render_404 unless @checklist
end
@ -144,7 +150,7 @@ module StepElements
owner: current_user,
subject: @step.protocol,
team: @step.protocol.team,
project: @step.protocol.in_module? ? @step.protocol.my_module.experiment.project : nil,
project: @step.protocol.in_module? ? @step.protocol.my_module.project : nil,
message_items: message_items.merge(step_message_items)
)
end

View file

@ -2,8 +2,9 @@
module StepElements
class ChecklistsController < BaseController
include ApplicationHelper
include StepsActions
before_action :load_checklist, only: %i(update destroy duplicate)
def create
checklist = @step.checklists.build(
name: t('protocols.steps.checklist.default_name', position: @step.checklists.length + 1)
@ -11,6 +12,7 @@ module StepElements
ActiveRecord::Base.transaction do
create_in_step!(@step, checklist)
log_step_activity(:checklist_added, { checklist_name: checklist.name })
checklist_name_annotation(@step, checklist)
end
render_step_orderable_element(checklist)
rescue ActiveRecord::RecordInvalid
@ -18,9 +20,11 @@ module StepElements
end
def update
old_name = @checklist.name
ActiveRecord::Base.transaction do
@checklist.update!(checklist_params)
log_step_activity(:checklist_edited, { checklist_name: @checklist.name })
checklist_name_annotation(@step, @checklist, old_name)
end
render json: @checklist, serializer: ChecklistSerializer, user: current_user
@ -43,6 +47,7 @@ module StepElements
@step.step_orderable_elements.where('position > ?', position).order(position: :desc).each do |element|
element.update(position: element.position + 1)
end
@checklist.name += ' (1)'
new_checklist = @checklist.duplicate(@step, current_user, position + 1)
log_step_activity(:checklist_duplicated, { checklist_name: @checklist.name })
render_step_orderable_element(new_checklist)

View file

@ -48,6 +48,7 @@ module StepElements
@step.step_orderable_elements.where('position > ?', position).order(position: :desc).each do |element|
element.update(position: element.position + 1)
end
@table.name += ' (1)'
new_table = @table.duplicate(@step, current_user, position + 1)
log_step_activity(:table_duplicated, { table_name: new_table.name })
render_step_orderable_element(new_table.step_table)

View file

@ -2,6 +2,9 @@
module StepElements
class TextsController < BaseController
include ApplicationHelper
include StepsActions
before_action :load_step_text, only: %i(update destroy duplicate)
def create
@ -18,10 +21,12 @@ module StepElements
end
def update
old_text = @step_text.text
ActiveRecord::Base.transaction do
@step_text.update!(step_text_params)
TinyMceAsset.update_images(@step_text, params[:tiny_mce_images], current_user)
log_step_activity(:text_edited, { text_name: @step_text.name })
step_text_annotation(@step, @step_text, old_text)
end
render json: @step_text, serializer: StepTextSerializer, user: current_user

View file

@ -48,7 +48,7 @@ class StepOrderableElementsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @protocol,
team: current_team,
team: @protocol.team,
project: project,
message_items: message_items)
end

View file

@ -38,7 +38,7 @@ class StepsController < ApplicationController
@asset = @step.assets.create!(
created_by: current_user,
last_modified_by: current_user,
team: current_team,
team: @protocol.team,
view_mode: @step.assets_view_mode
)
@asset.file.attach(params[:signed_blob_id])
@ -323,7 +323,7 @@ class StepsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: @protocol,
team: current_team,
team: @protocol.team,
project: project,
message_items: message_items)
end

View file

@ -168,7 +168,7 @@ class TagsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: subject,
team: current_team,
team: @tag.project.team,
project: @tag.project,
message_items: message_items)
end

View file

@ -97,7 +97,7 @@ class TeamRepositoriesController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
subject: team_shared_object.shared_repository,
team: current_team,
team: @repository.team,
message_items: { repository: team_shared_object.shared_repository.id,
team: team_shared_object.team.id,
permission_level:

View file

@ -61,14 +61,22 @@ class UserMyModulesController < ApplicationController
respond_to do |format|
format.json do
render json: {
user: {
id: @um.user.id,
full_name: @um.user.full_name,
avatar_url: avatar_path(@um.user, :icon_small),
user_module_id: @um.id
}, status: :ok
}
if params[:table]
render json: {
html: render_to_string(partial: 'experiments/assigned_users.html.erb',
locals: { my_module: @my_module, user: current_user }),
unassign_url: my_module_user_my_module_path(@my_module, @um)
}
else
render json: {
user: {
id: @um.user.id,
full_name: @um.user.full_name,
avatar_url: avatar_path(@um.user, :icon_small),
user_module_id: @um.id
}, status: :ok
}
end
end
end
else
@ -88,7 +96,14 @@ class UserMyModulesController < ApplicationController
respond_to do |format|
format.json do
render json: {}, status: :ok
if params[:table]
render json: {
html: render_to_string(partial: 'experiments/assigned_users.html.erb',
locals: { my_module: @my_module, user: current_user })
}
else
render json: {}, status: :ok
end
end
end
else
@ -104,19 +119,36 @@ class UserMyModulesController < ApplicationController
def search
users = @my_module.users
.where.not(id: @my_module.designated_users.select(:id))
.joins("LEFT OUTER JOIN user_my_modules ON user_my_modules.user_id = users.id "\
"AND user_my_modules.my_module_id = #{@my_module.id}")
.search(false, params[:query])
.order(:full_name)
.limit(Constants::SEARCH_LIMIT)
.select('users.*', 'user_my_modules.id as user_my_module_id')
.select('CASE WHEN user_my_modules.id IS NOT NULL '\
'THEN true ELSE false END as designated')
users = users.map do |user|
{
next if params[:skip_assigned] && user.designated
user_hash = {
value: user.id,
label: sanitize_input(user.full_name),
params: { avatar_url: avatar_path(user, :icon_small) }
params: {
avatar_url: avatar_path(user, :icon_small),
designated: user.designated,
assign_url: my_module_user_my_modules_path(@my_module)
}
}
if user.designated
user_hash[:params][:unassign_url] = my_module_user_my_module_path(@my_module, user.user_my_module_id)
end
user_hash
end
render json: users
render json: users.compact
end
private

View file

@ -99,7 +99,7 @@ module Users
result[:status] = :user_exists_and_in_team
else
# Also generate user team relation
team.user_assignments.create(user: user, user_role: @user_role, assigned_by: current_user)
team.user_assignments.create!(user: user, user_role: @user_role, assigned_by: current_user)
generate_notification(
@user,

View file

@ -23,6 +23,7 @@ class ProtocolsDatatable < CustomDatatable
def sortable_columns
@sortable_columns ||= [
"Protocol.name",
"Protocol.id",
"protocol_keywords_str",
"Protocol.nr_of_linked_children",
"full_username_str",
@ -34,6 +35,7 @@ class ProtocolsDatatable < CustomDatatable
def searchable_columns
@searchable_columns ||= [
"Protocol.name",
"Protocol.#{Protocol::PREFIXED_ID_SQL}",
timestamp_db_column,
"Protocol.updated_at"
]
@ -56,9 +58,11 @@ class ProtocolsDatatable < CustomDatatable
# now the method checks if the column is the created_at or updated_at and generate a custom SQL to parse
# it back to the caller method
def new_search_condition(column, value)
model, column = column.split('.')
model, column = column.split('.', 2)
model = model.constantize
case column
when Protocol::PREFIXED_ID_SQL
casted_column = ::Arel::Nodes::SqlLiteral.new(Protocol::PREFIXED_ID_SQL)
when 'published_on'
casted_column = ::Arel::Nodes::NamedFunction.new('CAST',
[ Arel.sql("to_char( protocols.created_at, '#{ formated_date }' ) AS VARCHAR") ] )
@ -97,11 +101,12 @@ class ProtocolsDatatable < CustomDatatable
else
name_html(record)
end,
'2': keywords_html(record),
'3': modules_html(record),
'4': escape_input(record.full_username_str),
'5': timestamp_column_html(record),
'6': I18n.l(record.updated_at, format: :full)
'2': escape_input(record.code),
'3': keywords_html(record),
'4': modules_html(record),
'5': escape_input(record.full_username_str),
'6': timestamp_column_html(record),
'7': I18n.l(record.updated_at, format: :full)
}
end
result_data

View file

@ -7,6 +7,7 @@ class ReportDatatable < CustomDatatable
TABLE_COLUMNS = %w(
Report.project_name
Report.name
Report.code
Report.pdf_file
Report.docx_file
Report.created_by_name
@ -40,6 +41,9 @@ class ReportDatatable < CustomDatatable
records.left_joins(:pdf_file_attachment)
.order(active_storage_attachments: sort_direction(order_params))
.order(pdf_file_status: sort_direction(order_params) == 'ASC' ? :desc : :asc)
when 'reports.code'
sort_by = "reports.id #{sort_direction(order_params)}"
records.order(sort_by)
else
sort_by = "#{sort_column(order_params)} #{sort_direction(order_params)}"
records.order(sort_by)
@ -54,12 +58,13 @@ class ReportDatatable < CustomDatatable
'0' => record.id,
'1' => sanitize_input(record.project_name),
'2' => sanitize_input(record.name),
'3' => pdf_file(record),
'4' => docx_file(record),
'5' => sanitize_input(record.created_by_name),
'6' => sanitize_input(record.modified_by_name),
'7' => I18n.l(record.created_at, format: :full),
'8' => I18n.l(record.updated_at, format: :full),
'3' => sanitize_input(record.code),
'4' => pdf_file(record),
'5' => docx_file(record),
'6' => sanitize_input(record.created_by_name),
'7' => sanitize_input(record.modified_by_name),
'8' => I18n.l(record.created_at, format: :full),
'9' => I18n.l(record.updated_at, format: :full),
'archived' => record.project.archived?,
'edit' => edit_project_report_path(record.project_id, record.id),
'status' => status_project_report_path(record.project_id, record.id),
@ -106,7 +111,7 @@ class ReportDatatable < CustomDatatable
def filter_records(records)
records.where_attributes_like(
['project_name', 'reports.name', 'reports.description'],
['project_name', 'reports.name', 'reports.description', "('RP' || reports.id)"],
dt_params.dig(:search, :value)
)
end

View file

@ -2,6 +2,6 @@
module CardsViewHelper
def cards_view_type_class(view_type)
view_type == 'table' ? 'list' : ''
view_type == 'table' ? 'list' : 'cards'
end
end

View file

@ -278,4 +278,8 @@ module CommentHelper
def has_unseen_comments?(commentable)
commentable.comments.any? { |comment| comment.unseen_by.include?(current_user.id) }
end
def count_unseen_comments(commentable, current_user)
commentable.comments.count { |comment| comment.unseen_by.include?(current_user.id) }
end
end

View file

@ -100,4 +100,14 @@ module MyModulesHelper
my_module.experiment.project.archived_on
end
end
def my_module_due_status(my_module, datetime = DateTime.current)
if my_module.is_overdue?(datetime)
I18n.t('my_modules.details.overdue')
elsif my_module.is_one_day_prior?(datetime)
I18n.t('my_modules.details.due_soon')
else
''
end
end
end

View file

@ -14,7 +14,7 @@ module ProjectsHelper
end
def user_names_with_roles(user_assignments)
user_assignments.map { |up| user_name_with_role(up) }.join('&#013;').html_safe
user_assignments.map { |up| user_name_with_role(up) }.join('&#013;')
end
def user_name_with_role(user_assignment)
@ -48,6 +48,10 @@ module ProjectsHelper
records.sort_by { |c| c.name.downcase }
when 'ztoa'
records.sort_by { |c| c.name.downcase }.reverse!
when 'id_asc'
records.sort_by(&:id)
when 'id_desc'
records.sort_by(&:id).reverse!
when 'archived_old'
records.sort_by(&:archived_on)
when 'archived_new'

View file

@ -93,9 +93,13 @@ module ReportsHelper
when 'ztoa'
results.order(name: :desc)
when 'new'
results.order(created_at: :desc)
when 'old_updated'
results.order(updated_at: :asc)
when 'new_updated'
results.order(updated_at: :desc)
else
results.order(updated_at: :asc)
results.order(created_at: :asc)
end
end

View file

@ -14,14 +14,18 @@
</div>
<div class="actions-block">
<div class="protocol-buttons-group">
<a v-if="urls.add_step_url" class="btn btn-primary" @click="addStep(steps.length)" tabindex="0">
<a v-if="urls.add_step_url"
class="btn btn-primary"
@keyup.enter="addStep(steps.length)"
@click="addStep(steps.length)"
tabindex="0">
<span class="fas fa-plus" aria-hidden="true"></span>
<span>{{ i18n.t("protocols.steps.new_step") }}</span>
</a>
<a class="btn btn-secondary" data-toggle="modal" data-target="#print-protocol-modal" tabindex="0">
<button class="btn btn-secondary" data-toggle="modal" data-target="#print-protocol-modal" tabindex="0">
<span class="fas fa-print" aria-hidden="true"></span>
<span>{{ i18n.t("protocols.print.button") }}</span>
</a>
</button>
<ProtocolOptions
v-if="protocol.attributes && protocol.attributes.urls"
:protocol="protocol"
@ -67,7 +71,8 @@
{{ i18n.t("protocols.no_text_placeholder") }}
</div>
</div>
<a v-if="urls.add_step_url && protocol.attributes.in_repository" class="btn btn-primary repository-new-step" @click="addStep(steps.length)">
<a v-if="urls.add_step_url && protocol.attributes.in_repository" class="btn btn-primary repository-new-step"
@keyup.enter="addStep(steps.length)" @click="addStep(steps.length)" tabindex="0">
<span class="fas fa-plus" aria-hidden="true"></span>
<span>{{ i18n.t("protocols.steps.new_step") }}</span>
</a>

View file

@ -22,13 +22,13 @@
/>
</div>
<div class="step-element-controls">
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" @click="editingName = true" tabindex="-1">
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" @click="editingName = true" tabindex="0">
<i class="fas fa-pen"></i>
</button>
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="-1" @click="duplicateElement">
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="0" @click="duplicateElement">
<i class="fas fa-clone"></i>
</button>
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="-1">
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="0">
<i class="fas fa-trash"></i>
</button>
</div>

View file

@ -39,10 +39,10 @@
</div>
</div>
<div class="step-element-controls">
<button v-if="!checklistItem.attributes.urls || updateUrl" class="btn icon-btn btn-light" @click="enableTextEdit" tabindex="-1">
<button v-if="!checklistItem.attributes.urls || updateUrl" class="btn icon-btn btn-light" @click="enableTextEdit" tabindex="0">
<i class="fas fa-pen"></i>
</button>
<button v-if="!checklistItem.attributes.urls || deleteUrl" class="btn icon-btn btn-light" @click="deleteElement" tabindex="-1">
<button v-if="!checklistItem.attributes.urls || deleteUrl" class="btn icon-btn btn-light" @click="deleteElement" tabindex="0">
<i class="fas fa-trash"></i>
</button>
</div>

View file

@ -19,13 +19,13 @@
/>
</div>
<div class="step-element-controls">
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" @click="enableNameEdit" tabindex="-1">
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" @click="enableNameEdit" tabindex="0">
<i class="fas fa-pen"></i>
</button>
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="-1" @click="duplicateElement">
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="0" @click="duplicateElement">
<i class="fas fa-clone"></i>
</button>
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="-1">
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="0">
<i class="fas fa-trash"></i>
</button>
</div>
@ -35,7 +35,7 @@
tabindex="0"
@keyup.enter="!editingTable && enableTableEdit()">
<div class="enable-edit-mode" v-if="!editingTable && element.attributes.orderable.urls.update_url" @click="enableTableEdit">
<div class="enable-edit-mode__icon">
<div class="enable-edit-mode__icon" tabindex="0">
<i class="fas fa-pen"></i>
</div>
</div>

View file

@ -5,13 +5,13 @@
</div>
<div v-else class="step-element-grip-placeholder"></div>
<div class="buttons-container">
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" tabindex="-1" @click="enableEditMode($event)">
<button v-if="element.attributes.orderable.urls.update_url" class="btn icon-btn btn-light" tabindex="0" @click="enableEditMode($event)">
<i class="fas fa-pen"></i>
</button>
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="-1" @click="duplicateElement">
<button v-if="element.attributes.orderable.urls.duplicate_url" class="btn icon-btn btn-light" tabindex="0" @click="duplicateElement">
<i class="fas fa-clone"></i>
</button>
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="-1">
<button v-if="element.attributes.orderable.urls.delete_url" class="btn icon-btn btn-light" @click="showDeleteModal" tabindex="0">
<i class="fas fa-trash"></i>
</button>
</div>
@ -73,7 +73,7 @@
},
mounted() {
if (this.isNew) {
this.enableEditMode()
this.enableEditMode();
}
},
methods: {

View file

@ -7,13 +7,13 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p class="modal-title">
<template v-if="rows.length == 1">
{{ i18n.t('repository_row.modal_print_label.head_title', {repository_row: rows[0].attributes.name}) }}
<b>{{ i18n.t('repository_row.modal_print_label.head_title', {repository_row: rows[0].attributes.name}) }}</b>
<span class="id-label">
{{ i18n.t('repository_row.modal_print_label.id_label', {repository_row_id: rows[0].attributes.code}) }}
</span>
</template>
<template v-else>
{{ i18n.t('repository_row.modal_print_label.head_title_multiple', {repository_rows: rows.length}) }}
<b>{{ i18n.t('repository_row.modal_print_label.head_title_multiple', {repository_rows: rows.length}) }}</b>
</template>
</p>
</div>

View file

@ -150,7 +150,9 @@
return $(`meta[name=\'${name}\']`).attr('content');
},
wrapTables() {
$(this.$el).find('.tinymce-view table').wrapAll('<div style="overflow: auto"></div>');
this.$nextTick(() => {
$(this.$el).find('.tinymce-view table').css('float', 'none').wrapAll('<div style="overflow: auto"></div>');
});
},
initCharacterCount() {
if (!this.editorInstance()) return;

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module MyModules
class CopyContentJob < ApplicationJob
def perform(user, source_my_module_id, target_my_module_id)
MyModule.transaction do
target_my_module = MyModule.find(target_my_module_id)
MyModule.find(source_my_module_id).copy_content(user, target_my_module)
target_my_module.update!(provisioning_status: :done)
end
rescue StandardError => _e
target_my_module.update(provisioning_status: :failed)
end
end
end

View file

@ -8,8 +8,7 @@ class ChecklistItem < ApplicationRecord
validates :position, uniqueness: { scope: :checklist }, unless: -> { position.nil? }
belongs_to :checklist,
inverse_of: :checklist_items,
touch: true
inverse_of: :checklist_items
belongs_to :created_by,
foreign_key: 'created_by_id',
class_name: 'User',
@ -21,8 +20,21 @@ class ChecklistItem < ApplicationRecord
after_destroy :update_positions
# conditional touch excluding checked updates
after_destroy :touch_checklist
after_save :touch_checklist
private
def touch_checklist
# if only checklist item checked attribute changed, do not touch checklist
return if saved_changes.keys.sort == %w(checked updated_at)
# rubocop:disable Rails/SkipsModelValidations
checklist.touch
# rubocop:enable Rails/SkipsModelValidations
end
def update_positions
transaction do
checklist.checklist_items.order(position: :asc).each_with_index do |checklist_item, i|

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Cloneable
extend ActiveSupport::Concern
def next_clone_name
raise NotImplementedError, "Cloneable model must implement the '.parent' method!" unless respond_to?(:parent)
clone_label = I18n.t('general.clone_label')
last_clone_number =
parent.public_send(self.class.table_name)
.select("substring(#{self.class.table_name}.name, '(?:^#{clone_label} )(\\d+)')::int AS clone_number")
.where('name ~ ?', "^#{clone_label} \\d+ - #{name}$")
.order(clone_number: :asc)
.last&.clone_number
"#{clone_label} #{(last_clone_number || 0) + 1} - #{name}".truncate(Constants::NAME_MAX_LENGTH)
end
end

View file

@ -9,8 +9,10 @@ class Experiment < ApplicationRecord
include ArchivableModel
include SearchableModel
include SearchableByNameModel
include ViewableModel
include PermissionCheckableModel
include Assignable
include Cloneable
before_save -> { report_elements.destroy_all }, if: -> { !new_record? && project_id_changed? }
@ -93,6 +95,23 @@ class Experiment < ApplicationRecord
joins(:project).where(project: { team: teams })
end
def default_view_state
{
my_modules: {
active: { sort: 'atoz' },
archived: { sort: 'atoz' }
}
}
end
def validate_view_state(view_state)
if %w(atoz ztoa due_first due_last).exclude?(view_state.state.dig('my_modules', 'active', 'sort')) ||
%w(atoz ztoa due_first due_last
archived_old archived_new).exclude?(view_state.state.dig('my_modules', 'archived', 'sort'))
view_state.errors.add(:state, :wrong_state)
end
end
def connections
Connection.joins(
'LEFT JOIN my_modules AS inputs ON input_id = inputs.id'
@ -224,11 +243,13 @@ class Experiment < ApplicationRecord
.with_granted_permissions(current_user, ProjectPermissions::EXPERIMENTS_CREATE)
end
def permission_parent
def parent
project
end
private
def permission_parent
project
end
# Archive all modules. Receives an array of module integer IDs
# and current user.
@ -520,6 +541,8 @@ class Experiment < ApplicationRecord
true
end
private
def log_activity(type_of, current_user, my_module)
Activities::CreateActivityService
.call(activity_type: type_of,

View file

@ -1,7 +1,9 @@
# frozen_string_literal: true
class MyModule < ApplicationRecord
SEARCHABLE_ATTRIBUTES = ['my_modules.name', 'my_modules.description']
ID_PREFIX = 'TA'
include PrefixedIdModel
SEARCHABLE_ATTRIBUTES = ['my_modules.name', 'my_modules.description', PREFIXED_ID_SQL].freeze
include ArchivableModel
include SearchableModel
@ -9,10 +11,15 @@ class MyModule < ApplicationRecord
include TinyMceImages
include PermissionCheckableModel
include Assignable
include Cloneable
ID_PREFIX = 'TA'
include PrefixedIdModel
attr_accessor :transition_error_rollback
enum state: Extends::TASKS_STATES
enum provisioning_status: { done: 0, in_progress: 1, failed: 2 }
before_validation :archiving_and_restoring_extras, on: :update, if: :archived_changed?
before_save -> { report_elements.destroy_all }, if: -> { !new_record? && experiment_id_changed? }
@ -124,6 +131,10 @@ class MyModule < ApplicationRecord
joins(experiment: :project).where(experiment: { projects: { team: teams } })
end
def parent
experiment
end
def navigable?
!experiment.archived? && experiment.navigable?
end
@ -381,25 +392,29 @@ class MyModule < ApplicationRecord
clone.assign_user(current_user)
copy_content(current_user, clone)
clone
end
def copy_content(current_user, target_my_module)
# Remove the automatically generated protocol,
# & clone the protocol instead
clone.protocol.destroy
clone.reload
target_my_module.protocol.destroy
target_my_module.reload
# Update the cloned protocol if neccesary
clone_tinymce_assets(clone, clone.experiment.project.team)
clone.protocols << protocol.deep_clone_my_module(self, current_user)
clone.reload
clone_tinymce_assets(clone, target_my_module.experiment.project.team)
target_my_module.protocols << protocol.deep_clone_my_module(self, current_user)
target_my_module.reload
# fixes linked protocols
clone.protocols.each do |protocol|
target_my_module.protocols.each do |protocol|
next unless protocol.linked?
protocol.updated_at = protocol.parent_updated_at
protocol.save
end
clone
end
# Find an empty position for the restored module. It's

View file

@ -1,4 +1,10 @@
# frozen_string_literal: true
class Project < ApplicationRecord
ID_PREFIX = 'PR'
include PrefixedIdModel
SEARCHABLE_ATTRIBUTES = ['projects.name', PREFIXED_ID_SQL].freeze
include ArchivableModel
include SearchableModel
include SearchableByNameModel
@ -80,7 +86,7 @@ class Project < ApplicationRecord
)
new_query = Project.viewable_by_user(user, current_team || user.teams)
.where_attributes_like('projects.name', query, options)
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
new_query = new_query.active unless include_archived
# Show all results if needed
@ -127,8 +133,8 @@ class Project < ApplicationRecord
def validate_view_state(view_state)
if %w(cards table).exclude?(view_state.state.dig('experiments', 'view_type')) ||
%w(new old atoz ztoa).exclude?(view_state.state.dig('experiments', 'active', 'sort')) ||
%w(new old atoz ztoa archived_new archived_old).exclude?(view_state.state.dig('experiments', 'archived', 'sort'))
%w(new old atoz ztoa id_asc id_desc).exclude?(view_state.state.dig('experiments', 'active', 'sort')) ||
%w(new old atoz ztoa id_asc id_desc archived_new archived_old).exclude?(view_state.state.dig('experiments', 'archived', 'sort'))
view_state.errors.add(:state, :wrong_state)
end
end
@ -171,6 +177,8 @@ class Project < ApplicationRecord
when 'old' then { created_at: :asc }
when 'atoz' then { name: :asc }
when 'ztoa' then { name: :desc }
when 'id_asc' then { id: :asc }
when 'id_desc' then { id: :desc }
when 'archived_new' then { archived_on: :desc }
when 'archived_old' then { archived_on: :asc }
else { created_at: :desc }

View file

@ -1,6 +1,11 @@
# frozen_string_literal: true
class Protocol < ApplicationRecord
ID_PREFIX = 'PT'
include PrefixedIdModel
SEARCHABLE_ATTRIBUTES = ['protocols.name', 'protocols.description',
'protocols.authors', 'protocol_keywords.name', PREFIXED_ID_SQL].freeze
include SearchableModel
include RenamingUtil
include SearchableByNameModel
@ -180,15 +185,7 @@ class Protocol < ApplicationRecord
.joins('LEFT JOIN protocol_keywords ' \
'ON protocol_keywords.id = ' \
'protocol_protocol_keywords.protocol_keyword_id')
.where_attributes_like(
[
'protocols.name',
'protocols.description',
'protocols.authors',
'protocol_keywords.name'
],
query, options
)
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -1,6 +1,10 @@
# frozen_string_literal: true
class Report < ApplicationRecord
ID_PREFIX = 'RP'
include PrefixedIdModel
SEARCHABLE_ATTRIBUTES = ['reports.name', 'reports.description', PREFIXED_ID_SQL].freeze
include SettingsModel
include Assignable
include PermissionCheckableModel
@ -55,8 +59,9 @@ class Report < ApplicationRecord
table_results: true,
text_results: true,
result_comments: true,
result_order: 'atoz',
activities: true
result_order: 'new',
activities: true,
repositories: []
}
}.freeze
@ -74,7 +79,7 @@ class Report < ApplicationRecord
new_query = Report.distinct
.where(reports: { project_id: project_ids })
.where_attributes_like(%i(name description), query, options)
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -3,7 +3,7 @@
class ResultComment < Comment
before_create :fill_unseen_by
belongs_to :result, foreign_key: :associated_id, inverse_of: :result_comments, touch: true
belongs_to :result, foreign_key: :associated_id, inverse_of: :result_comments
validates :result, presence: true

View file

@ -24,9 +24,13 @@ class Step < ApplicationRecord
before_destroy :cascade_before_destroy
after_destroy :adjust_positions_after_destroy, unless: -> { skip_position_adjust }
# conditional touch excluding step completion updates
after_destroy :touch_protocol
after_save :touch_protocol
belongs_to :user, inverse_of: :steps
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true
belongs_to :protocol, inverse_of: :steps, touch: true
belongs_to :protocol, inverse_of: :steps
has_many :step_orderable_elements, inverse_of: :step, dependent: :destroy
has_many :checklists, inverse_of: :step, dependent: :destroy
has_many :step_comments, foreign_key: :associated_id, dependent: :destroy
@ -174,8 +178,18 @@ class Step < ApplicationRecord
new_step
end
end
private
def touch_protocol
# if only step completion attributes were changed, do not touch protocol
return if saved_changes.keys.sort == %w(completed completed_on updated_at)
# rubocop:disable Rails/SkipsModelValidations
protocol.touch
# rubocop:enable Rails/SkipsModelValidations
end
def adjust_positions_after_destroy
re_index_following_steps
protocol.steps.where('position > ?', position).order(:position).each do |step|

View file

@ -84,8 +84,8 @@ class Team < ApplicationRecord
end
def validate_view_state(view_state)
if %w(new old atoz ztoa).exclude?(view_state.state.dig('projects', 'active', 'sort')) ||
%w(new old atoz ztoa archived_new archived_old).exclude?(view_state.state.dig('projects', 'archived', 'sort')) ||
if %w(new old atoz ztoa id_asc id_desc).exclude?(view_state.state.dig('projects', 'active', 'sort')) ||
%w(new old atoz ztoa id_asc id_desc archived_new archived_old).exclude?(view_state.state.dig('projects', 'archived', 'sort')) ||
%w(cards table).exclude?(view_state.state.dig('projects', 'view_type'))
view_state.errors.add(:state, :wrong_state)
end

View file

@ -692,6 +692,11 @@ class User < ApplicationRecord
end
end
def my_module_visible_table_columns
settings['visible_my_module_table_columns'].presence ||
%w(id due_date age results status archived assigned tags comments)
end
protected
def confirmation_required?

View file

@ -21,6 +21,7 @@ Canaid::Permissions.register_for(MyModule) do
update_my_module_description
manage_my_module_tags
update_my_module_status
move_my_module
manage_my_module_steps
complete_my_module_steps
uncomplete_my_module_steps

View file

@ -4,7 +4,7 @@ Canaid::Permissions.register_for(RepositoryBase) do
# repository: read/export
can :read_repository do |user, repository|
if repository.is_a?(RepositorySnapshot)
user.teams.include?(repository.team)
can_read_my_module?(user, repository.my_module)
else
user.teams.include?(repository.team) || repository.shared_with?(user.current_team)
end

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