Merge branch 'gc_SCI_7442' of github.com:G-Chubinidze/scinote-web into gc_SCI_7442

This commit is contained in:
Giga Chubinidze 2022-12-07 13:52:18 +04:00
commit 3c23465114
31 changed files with 967 additions and 169 deletions

View file

@ -1,7 +1,10 @@
/* global dropdownSelector */
(function() {
function initNewMyModuleModal() {
let experimentWrapper = '.experiment-new-my_module';
let newMyModuleModal = '#new-my-module-modal';
let myModuleUserSelector = '#my_module_user_ids';
// Modal's submit handler function
$(experimentWrapper)
@ -10,12 +13,19 @@
})
.on('ajax:error', newMyModuleModal, function(ev, data) {
$(this).renderFormErrors('my_module', data.responseJSON);
});
$(experimentWrapper)
.on('ajax:success', '.new-my-module-button', function(ev, data) {
})
.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(data.html));
$(experimentWrapper).append($.parseHTML(result.html));
$(newMyModuleModal).modal('show');
$(newMyModuleModal).find("input[type='text']").focus();
@ -23,6 +33,28 @@
$(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'));
});
}

View file

@ -1,4 +1,4 @@
/* global I18n GLOBAL_CONSTANTS InfiniteScroll filterDropdown dropdownSelector HelperModule */
/* global I18n GLOBAL_CONSTANTS InfiniteScroll initBSTooltips filterDropdown dropdownSelector HelperModule */
var ExperimnetTable = {
permissions: ['editable', 'archivable', 'restorable', 'moveable'],
@ -8,6 +8,7 @@ var ExperimnetTable = {
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');
@ -20,15 +21,19 @@ var ExperimnetTable = {
$(placeholder).insertAfter($(this.table).find('.table-body'));
},
appendRows: function(result) {
$.each(result, (id, data) => {
$.each(result, (_j, data) => {
let row;
// Checkbox selector
let row = `
<div class="table-body-cell">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox my-module-selector" data-my-module="${id}">
<span class="sci-checkbox-label"></span>
</div>
</div>`;
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 = '';
@ -53,10 +58,50 @@ var ExperimnetTable = {
</div>
</div>
</div>`;
$(`<div class="table-row" data-urls='${JSON.stringify(data.urls)}' data-id="${id}">${row}</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');
@ -70,6 +115,41 @@ var ExperimnetTable = {
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) => {
@ -84,6 +164,14 @@ var ExperimnetTable = {
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();
@ -92,23 +180,25 @@ var ExperimnetTable = {
initRenameModal: function() {
$('#editTask').on('click', () => {
$('#modal-edit-module').modal('show');
$('#edit-module-name-input').val($(`#taskName${this.selectedMyModules[0]}`).data('full-name'));
$('#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${this.selectedMyModules[0]}`).data('full-name')) {
if (newValue === $(`#taskName${id}`).data('full-name')) {
$('#modal-edit-module').modal('hide');
return false;
}
$.ajax({
url: this.getUrls(this.selectedMyModules[0]).name_update,
url: this.getUrls(id).name_update,
type: 'PATCH',
dataType: 'json',
data: { my_module: { name: $('#edit-module-name-input').val() } },
success: () => {
$(`#taskName${this.selectedMyModules[0]}`).text(newValue);
$(`#taskName${this.selectedMyModules[0]}`).data('full-name', newValue);
$(`#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');
},
@ -133,7 +223,13 @@ var ExperimnetTable = {
$(`
<div class="user-container">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox user-selector" value="${user.value}">
<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">
@ -147,6 +243,85 @@ var ExperimnetTable = {
});
});
});
$(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;
@ -229,7 +404,8 @@ var ExperimnetTable = {
$.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').length + 1);
$('.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);
@ -247,7 +423,8 @@ var ExperimnetTable = {
// 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').length + 1);
$('.experiment-table')[0].style
.setProperty('--columns-count', $('.table-display-modal .fa-eye:not(.disabled)').length + 1);
});
},
initNewTaskModal: function(table) {
@ -255,6 +432,18 @@ var ExperimnetTable = {
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');
@ -291,11 +480,16 @@ var ExperimnetTable = {
});
},
loadTable: function() {
var tableParams = {
filters: this.activeFilters,
sort: this.myModulesCurrentSort
};
var dataUrl = $(this.table).data('my-modules-url');
this.loadPlaceholder();
$.get(dataUrl, { filters: this.activeFilters }, (result) => {
$.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,
@ -305,30 +499,75 @@ var ExperimnetTable = {
lastPage: !result.next_page,
customResponse: (response) => {
this.appendRows(response.data);
this.initDueDatePicker(response.data);
},
customParams: (params) => {
return { ...params, ...{ filters: this.activeFilters } };
return { ...params, ...tableParams };
}
});
initBSTooltips();
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.updateExperimentToolbar();
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>`;
};
@ -339,7 +578,7 @@ ExperimnetTable.render.id = function(data) {
};
ExperimnetTable.render.due_date = function(data) {
return data;
return data.data;
};
ExperimnetTable.render.archived = function(data) {
@ -359,49 +598,7 @@ ExperimnetTable.render.status = function(data) {
};
ExperimnetTable.render.assigned = function(data) {
let users = '';
$.each(data.users, (i, user) => {
users += `
<span class="avatar-container" style="z-index: ${5 - i}">
<img src=${user.image_url} title=${user.title}>
</span>
`;
});
if (data.count > 4) {
users += `
<span class="more-users avatar-container" title="${data.more_users_title}">
+${data.count - 4}
</span>
`;
}
if (data.create_url) {
users += `
<span class="new-user avatar-container">
<i class="fas fa-plus"></i>
</span>
`;
}
return `
<div ref="dropdown"
class="assign-users-dropdown dropdown"
>
<div class="assigned-users-container" data-toggle="dropdown" >
${users}
</div>
<div class="dropdown-menu dropdown-menu-right">
<div class="sci-input-container left-icon">
<input type="text" class="sci-input-field" placeholder="${I18n.t('experiments.table.search')}"></input>
<i class="fas fa-search"></i>
</div>
<div class="users-list" data-list-url="${data.list_url}">
</div>
</div>
</div>
`;
return data.html;
};
ExperimnetTable.render.tags = function(data) {
@ -469,6 +666,36 @@ ExperimnetTable.filters.push({
}
});
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) => {

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

@ -21,3 +21,28 @@
}
}
}
#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;
}
}
.datetime-picker-container {
width: 45%;
.fa-calendar-alt {
color: $color-volcano !important;
font-size: 14px !important;
}
}
}

View file

@ -5,6 +5,22 @@
--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;
@ -69,6 +85,31 @@
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;
@ -137,7 +178,7 @@
}
.users-list {
max-height: 400px;
max-height: 300px;
overflow: auto;
}
@ -148,6 +189,10 @@
.user-avatar {
padding: 0 .75em;
img {
border-radius: 50%;
}
}
}
@ -324,6 +369,59 @@
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;
@ -374,16 +472,10 @@
}
}
.datetime-picker-container {
width: 45%;
.fa-calendar-alt {
color: $color-volcano !important;
font-size: 14px !important;
}
.task_name-column span {
color: $color-silver-chalice;
}
.table-display-modal {
.column-container {
align-items: center;
@ -402,6 +494,11 @@
.fas {
cursor: pointer;
margin-right: 1em;
&.disabled {
color: $color-alto;
pointer-events: none;
}
}
&.task_name {

View file

@ -49,10 +49,6 @@
display: flex;
flex-shrink: 0;
margin-left: auto;
&.experiment-header {
column-gap: .25em;
}
}
.view-switch {

View file

@ -12,7 +12,7 @@ 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)
@ -90,15 +90,19 @@ class ExperimentsController < ApplicationController
def table
redirect_to module_archive_experiment_path(@experiment) if @experiment.archived_branch?
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
@active_modules = @experiment.my_modules.active.order(:name)
@my_module_visible_table_columns = current_user.settings['visible_my_module_table_columns'].presence || []
end
def load_table
my_modules = @experiment.my_modules
my_modules = @experiment.my_modules.readable_by_user(current_user)
my_modules = params[:view_mode] == 'archived' ? my_modules.archived : my_modules.active
render json: Experiments::TableViewService.new(my_modules, current_user, params).call
render json: Experiments::TableViewService.new(@experiment, my_modules, current_user, params).call
end
def edit
@ -281,6 +285,43 @@ class ExperimentsController < ApplicationController
render json: { message: message, path: path }, 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
@ -366,6 +407,32 @@ class ExperimentsController < ApplicationController
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

View file

@ -5,16 +5,17 @@ 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 save_table_state)
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(
create 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 save_table_state
description due_date update_description update_protocol_description update_protocol
)
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)
@ -23,9 +24,12 @@ class MyModulesController < ApplicationController
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] }
partial: 'my_modules/modals/new_modal.html.erb', locals: { view_mode: params[:view_mode],
users: assigned_users }
)
}
end
@ -233,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 }
@ -377,7 +386,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
@ -414,6 +428,10 @@ class MyModulesController < ApplicationController
end
end
def provisioning_status
render json: { provisioning_status: @my_module.provisioning_status }
end
private
def load_vars
@ -426,6 +444,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)

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
@ -107,20 +122,33 @@ class UserMyModulesController < ApplicationController
.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.*')
.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), designated: user.designated }
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

@ -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

@ -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

@ -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

@ -9,10 +9,12 @@ class MyModule < ApplicationRecord
include TinyMceImages
include PermissionCheckableModel
include Assignable
include Cloneable
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 +126,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 +387,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

@ -21,7 +21,7 @@ module Experiments
ActiveRecord::Base.transaction do
@c_exp = Experiment.new(
name: find_uniq_name,
name: @exp.next_clone_name,
description: @exp.description,
created_by: @user,
last_modified_by: @user,
@ -54,15 +54,6 @@ module Experiments
private
def find_uniq_name
experiment_names = @project.experiments.map(&:name)
format = 'Clone %d - %s'
free_index = 1
free_index += 1 while experiment_names
.include?(format(format, free_index, @exp.name))
format(format, free_index, @exp.name).truncate(Constants::NAME_MAX_LENGTH)
end
def valid?
unless @exp && @project && @user
@errors[:invalid_arguments] =

View file

@ -7,7 +7,10 @@ module Experiments
include CommentHelper
include ProjectsHelper
include InputSanitizeHelper
include BootstrapFormHelper
include MyModulesHelper
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
COLUMNS = %i(
task_name
@ -32,20 +35,24 @@ module Experiments
experiment: :project
}
def initialize(my_modules, user, params)
def initialize(experiment, my_modules, user, params)
@my_modules = my_modules
@page = params[:page] || 1
@user = user
@filters = params[:filters] || []
@params = params
initialize_table_sorting(experiment)
end
def call
result = {}
result = []
my_module_list = @my_modules
@filters.each do |name, value|
my_module_list = __send__("#{name}_filter", my_module_list, value) if value.present?
end
my_module_list = sort_records(my_module_list)
my_module_list = my_module_list.includes(PRELOAD)
.select('my_modules.*')
.group('my_modules.id')
@ -64,15 +71,20 @@ module Experiments
experiment = my_module.experiment
project = experiment.project
result[my_module.id] = {
columns: prepared_my_module,
urls: {
permissions: permissions_my_module_path(my_module),
actions_dropdown: actions_dropdown_my_module_path(my_module),
name_update: my_module_path(my_module),
access: edit_access_permissions_project_experiment_my_module_path(project, experiment, my_module)
}
}
result.push({ id: my_module.id,
columns: prepared_my_module,
provisioning_status: my_module.provisioning_status,
urls: {
permissions: permissions_my_module_path(my_module),
actions_dropdown: actions_dropdown_my_module_path(my_module),
name_update: my_module_path(my_module),
restore: restore_my_modules_experiment_path(experiment),
provisioning_status:
my_module.provisioning_status == 'in_progress' &&
provisioning_status_my_module_url(my_module),
access: edit_access_permissions_project_experiment_my_module_path(project,
experiment, my_module)
} })
end
{
@ -87,6 +99,7 @@ module Experiments
{
id: my_module.id,
name: my_module.name,
provisioning_status: my_module.provisioning_status,
url: protocols_my_module_path(my_module)
}
end
@ -98,11 +111,14 @@ module Experiments
end
def due_date_presenter(my_module)
if my_module.due_date
I18n.l(my_module.due_date, format: :full_date)
else
''
end
{
id: my_module.id,
data: ApplicationController.renderer.render(
partial: 'experiments/table_due_date.html.erb',
locals: { my_module: my_module,
user: @user }
)
}
end
def archived_presenter(my_module)
@ -132,25 +148,10 @@ module Experiments
end
def assigned_presenter(my_module)
users = my_module.designated_users
result = {
count: users.length,
users: []
}
users[0..3].each do |user|
result[:users].push({
image_url: avatar_path(user, :icon_small),
title: user.full_name
})
end
result[:more_users_title] = users[4..].map(&:full_name).join('&#013;') if users.length > 4
result[:list_url] = search_my_module_user_my_module_path(my_module, my_module_id: my_module.id)
if can_manage_my_module_users?(@user, my_module)
result[:create_url] = my_module_user_my_modules_path(my_module_id: my_module.id)
end
result
{ html: ApplicationController.renderer.render(
partial: 'experiments/assigned_users.html.erb',
locals: { my_module: my_module, user: @user }
) }
end
def tags_presenter(my_module)
@ -181,12 +182,51 @@ module Experiments
my_modules.where('my_modules.due_date <= ?', value)
end
def archived_on_from_filter(my_modules, value)
my_modules.where('my_modules.archived_on >= ?', value)
end
def archived_on_to_filter(my_modules, value)
my_modules.where('my_modules.archived_on <= ?', value)
end
def assigned_users_filter(my_modules, value)
my_modules.joins(:user_my_modules).where(user_my_modules: { user_id: value })
end
def statuses_filter(my_modules, value)
my_modules.where('my_module_status_id IN (?)', value)
my_modules.where(my_module_status_id: value)
end
def initialize_table_sorting(experiment)
@view_state = experiment.current_view_state(@user)
@view_mode = @params[:view_mode] || 'active'
@sort = @view_state.state.dig('my_modules', @view_mode, 'sort') || 'atoz'
if @params[:sort] && @sort != @params[:sort] && %w(due_first due_last atoz ztoa
archived_old archived_new).include?(@params[:sort])
@view_state.state['my_modules'].merge!(Hash[@view_mode, { 'sort': @params[:sort] }.stringify_keys])
@view_state.save!
@sort = @view_state.state.dig('my_modules', @view_mode, 'sort')
end
end
def sort_records(records)
case @sort
when 'due_first'
records.order(:due_date)
when 'due_last'
records.order(Arel.sql("COALESCE(due_date, DATE '1900-01-01') DESC"))
when 'atoz'
records.order(:name)
when 'ztoa'
records.order(name: :desc)
when 'archived_old'
records.order(Arel.sql('COALESCE(my_modules.archived_on, my_modules.archived_on) ASC'))
when 'archived_new'
records.order(Arel.sql('COALESCE(my_modules.archived_on, my_modules.archived_on) DESC'))
else
records
end
end
end
end

View file

@ -0,0 +1,28 @@
<% users = my_module.designated_users.order(:full_name) %>
<div ref="dropdown" class="assign-users-dropdown dropdown">
<div class="assigned-users-container" data-toggle="dropdown">
<% users[0..3].each_with_index do |user, i| %>
<span class="avatar-container" style="z-index: <%= 5 - i %>">
<%= image_tag avatar_path(user, :icon_small), title: user.full_name %>
</span>
<% end %>
<% if users.length > 4 %>
<span class="more-users avatar-container" title="<%= sanitize_input(users[4..].map(&:full_name).join('&#013;')) %>">
+<%= users.length - 4 %>
</span>
<% end %>
<% if can_manage_my_module_users?(user, my_module) %>
<span class="new-user avatar-container">
<i class="fas fa-plus"></i>
</span>
<% end %>
</div>
<div class="dropdown-menu">
<div class="sci-input-container left-icon">
<input type="text" class="sci-input-field user-search" placeholder="<%= I18n.t('experiments.table.search') %>">
<i class="fas fa-search"></i>
</div>
<div class="users-list" data-list-url="<%= search_my_module_user_my_module_path(my_module, my_module_id: my_module.id) %>">
</div>
</div>
</div>

View file

@ -0,0 +1,31 @@
<div class="modal fade" id="modal-move-modules" tabindex="-1" role="dialog" aria-labelledby="modal-move-modules-label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="modal-move-modules-label"><%= t('experiments.table.modal_move_modules.title') %></h4>
</div>
<div class="modal-body">
<% if @experiments.present? %>
<%= bootstrap_form_tag do |f| %>
<%= f.select :experiment_id, @experiments.collect { |e| [ e.name, e.id ] }, {}, class: "form-control selectpicker", 'data-role': 'clear' %>
<% end %>
<% else %>
<div>
<em>
<%= t('experiments.table.modal_move_modules.no_experiments') %>
</em>
</div>
<% end %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('general.cancel') %></button>
<% if @experiments.present? %>
<button type="button" class="btn btn-primary" data-action="confirm">
<%= t('experiments.table.modal_move_modules.confirm') %>
</button>
<% end %>
</div>
</div>
</div>
</div>

View file

@ -63,6 +63,25 @@
<% end %>
<% if action_name == 'table' %>
<%= render partial: 'table_filters.html.erb' %>
<div class="dropdown sort-menu">
<button class="btn btn-light icon-btn dropdown-toggle" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fas fa-sort-amount-up"></i>
</button>
<ul id="sortMenuDropdown" class="dropdown-menu sort-task-menu <%= params[:view_mode] %> dropdown-menu-right" aria-labelledby="sortMenu">
<% %w(atoz ztoa due_first due_last archived_old archived_new).each_with_index do |sort, i| %>
<% if i.even? && i.positive? %>
<li class="divider" <%= i > 3 ? 'data-view-mode=archived' : '' %>></li>
<% end %>
<li <%= %w(archived_new archived_old).include?(sort) ? 'data-view-mode=archived' : '' %>>
<a class="<%= 'selected' if @current_sort == sort %>"
data-sort="<%= sort %>" >
<%= t("general.sort.#{sort}_html") %>
</a>
</li>
<% end %>
</ul>
</div>
<% end %>
</div>
</div>

View file

@ -11,11 +11,14 @@
<p><%= t("experiments.table.column_display_modal.description") %></p>
<% Experiments::TableViewService::COLUMNS.each do |col| %>
<div class="column-container <%= col %> visible">
<% unless col == :task_name %>
<% if col == :archived && params[:view_mode] != 'archived' %>
<i class="fas fa-eye disabled" data-column="<%= col %>"></i>
<% elsif col != :task_name %>
<i class="fas fa-<%= col.to_s.in?(@my_module_visible_table_columns) ? 'eye' : 'eye-slash' %>" data-column="<%= col %>"></i>
<% end %>
<%= t("experiments.table.column_display_modal.#{col}") %>
</div>
<% end %>
</div>
</div>

View file

@ -0,0 +1,32 @@
<% due_date_editable = can_update_my_module_due_date?(user, my_module)%>
<% due_status = my_module_due_status(my_module) %>
<div id="dueDateContainer" class="datetime-container"
data-update-url="<%= my_module_path(my_module, user, format: :json) %>">
<span class="date-text" data-editable="<%= due_date_editable %>"
data-toggle="tooltip" data-placement="top" title="<%= due_status %>" data-due-status="<%= due_status %>">
<span id="dueDateLabelContainer" class="view-block" >
<%= render partial: "experiments/table_due_date_label.html.erb" ,
locals: { my_module: my_module, user: user } %>
</span>
<% if due_date_editable %>
<div class="datetime-picker-container" id="due-date">
<input id="calendarDueDate<%= my_module.id %>"
type="datetime"
data-toggle='date-time-picker'
class="form-control calendar-input calendar-due-date"
readonly
placeholder="<%= t('my_modules.details.no_due_date_placeholder') %>"
data-date-format="<%= datetime_picker_format_full %>"
data-date-locale="<%= I18n.locale %>"
data-date-use-current="false"
data-date-orientation="left"
value="<%= my_module.due_date ? l(my_module.due_date, format: :full) : '' %>"/>
</div>
<div class="fas fa-times-circle clear-date"
data-toggle='clear-date-time-picker'
data-target='calendarDueDate<%= my_module.id %>'>
</div>
<% end %>
</span>
</div>

View file

@ -0,0 +1,17 @@
<span class="due-date-label" data-due-date="<%= my_module.due_date.present? %>" >
<% if my_module.is_one_day_prior? %>
<%= l(my_module.due_date, format: :full_date) %>
<span class="fas fa-exclamation-triangle <%= get_task_alert_color(my_module) %>"></span>
<% elsif my_module.is_overdue? %>
<%= l(my_module.due_date, format: :full_date) %>
<span class="fas fa-exclamation-triangle <%= get_task_alert_color(my_module) %>"></span>
<% elsif my_module.due_date %>
<%= l(my_module.due_date, format: :full_date) %>
<% elsif can_update_my_module_due_date?(user, my_module) %>
<a href='#'>
<%= t('my_modules.details.no_due_date_placeholder') %>
</a>
<% else %>
<%= t('my_modules.details.no_due_date') %>
<% end %>
</span>

View file

@ -5,6 +5,8 @@
} do %>
<%= render partial: 'shared/filter_dropdown/text_search', locals: {container_class: 'task-name-filter', label_text: t('experiments.table.filters.name')} %>
<%= render partial: 'shared/filter_dropdown/datetime_search', locals: {container_class: 'due-date-filter', label: t('experiments.table.filters.due_date'), view_mode: nil } %>
<%= render partial: 'shared/filter_dropdown/datetime_search', locals: {container_class: 'archived-on-filter', label: t("filters_modal.archived_on.label"), view_mode: 'archived' } if params[:view_mode] == 'archived' %>
<div class="select-block status-container">
<label><%= t('experiments.table.filters.status') %></label>
<select class="status-filter"

View file

@ -1,7 +1,7 @@
<li class="divider-label"><%= t("experiments.table.my_module_actions.title") %></li>
<% if can_manage_my_module?(my_module) %>
<li>
<a href="" class="edit-my-module">
<a href="" class="edit-my-module" data-id="<%= my_module.id %>">
<i class="fas fa-pen"></i>
<%= t("experiments.table.my_module_actions.edit") %>
</a>
@ -9,7 +9,7 @@
<% end %>
<% if can_manage_experiment?(my_module.experiment) && my_module.active? %>
<li>
<a href="" class="duplicate-my-module">
<a href="" class="duplicate-my-module" data-id="<%= my_module.id %>">
<i class="fas fa-copy"></i>
<%= t("experiments.table.my_module_actions.duplicate") %>
</a>
@ -17,7 +17,7 @@
<% end %>
<% if can_move_my_module?(my_module) %>
<li>
<a href="" class="move-my-module">
<a href="" class="move-my-module" data-id="<%= my_module.id %>">
<i class="fas fa-arrow-right"></i>
<%= t("experiments.table.my_module_actions.move") %>
</a>
@ -37,3 +37,11 @@
</a>
</li>
<% end %>
<% if can_restore_my_module?(my_module) %>
<li>
<a href="<%= restore_my_modules_experiment_path(my_module.experiment) %>" class="restore-my-module" data-id="<%= my_module.id %>">
<i class="fas fa-undo"></i>
<%= t("experiments.table.my_module_actions.restore") %>
</a>
</li>
<% end %>

View file

@ -15,7 +15,7 @@
<%= t("experiments.table.toolbar.edit") %>
</button>
<% if can_manage_experiment?(@experiment) %>
<button id="duplicateTask" class="btn btn-light multiple-object-action hidden only-active">
<button id="duplicateTasks" class="btn btn-light multiple-object-action hidden only-active" data-url="<%= batch_clone_my_modules_experiment_path(@experiment) %>">
<i class="fas fa-copy"></i>
<%= t("experiments.table.toolbar.duplicate") %>
</button>
@ -32,6 +32,10 @@
<i class="fas fa-archive"></i>
<%= t("experiments.table.toolbar.archive") %>
</button>
<button id="restoreTask" class="btn btn-light multiple-object-action hidden only-archive" data-url="<%= restore_my_modules_experiment_path(@experiment) %>" data-for="restorable">
<i class="fas fa-undo"></i>
<%= t("experiments.table.toolbar.restore") %>
</button>
</div>
<div class="toolbar-right-block">
<button id="taskDataDisplay" class="btn btn-light" data-toggle="modal" data-target="#tableDisplayModal">

View file

@ -14,6 +14,8 @@
<div class="experiment-table"
style="--columns-count: <%= Experiments::TableViewService::COLUMNS.length%>"
data-my-modules-url="<%= load_table_experiment_path(@experiment, view_mode: params[:view_mode]) %>"
data-move-modules-modal-url="<%= move_modules_modal_experiment_path(@experiment) %>"
data-move-modules-url="<%= move_modules_experiment_path(@experiment) %>"
>
<div class="table-header">
<div class="table-header-cell select-all-checkboxes">

View file

@ -1,4 +1,5 @@
<div class="modal" id="new-my-module-modal" tabindex="-1" role="dialog" data-create-url="<%= modules_experiment_path(@experiment) %>">
<div class="modal" id="new-my-module-modal" tabindex="-1" role="dialog"
data-create-url="<%= modules_experiment_path(@experiment) %>" data-user-id="<%= current_user.id %>">
<%= bootstrap_form_for @my_module, url: modules_experiment_path(@experiment), remote: true do |f| %>
<div class="modal-dialog" role="document">
<div class="modal-content">
@ -35,6 +36,26 @@
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<%= f.select 'user_ids',
options_for_select(users.map{ |user|
[
user.full_name,
user.id,
{'data-params' => {avatar_url: avatar_path(user, :icon_small) }.to_json}
]
}),
{
id: 'new-modal-assigned-users-selector',
label: t('experiments.canvas.new_my_module_modal.assigned_users')
}, {
:multiple => true
} %>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">

View file

@ -14,7 +14,7 @@
'data-project-id': my_module.experiment.project_id,
'data-placeholder': t('my_modules.details.no_assigned_users'),
'data-users-create-url': my_module_user_my_modules_path(my_module_id: my_module.id),
'data-ajax-url': search_my_module_user_my_module_path(my_module),
'data-ajax-url': search_my_module_user_my_module_path(my_module, skip_assigned: true),
'data-update-module-users-url': my_module_user_my_modules_url(my_module),
'data-view-mode': !can_manage_my_module_designated_users?(my_module)
} %>

View file

@ -951,6 +951,9 @@ en:
no_start_date_placeholder: "+ Add starting date"
due_date: "Due date:"
no_due_date_placeholder: "+ Add due date"
overdue: "Overdue"
due_soon: "Due soon"
no_due_date: "not set"
assigned_users: "Assigned to:"
no_assigned_users: "+ Assign task to a project member"
tags: "Tags:"
@ -1282,6 +1285,8 @@ en:
success_flash: 'Successfully duplicated experiment %{experiment} as template.'
error_flash: 'Could not duplicate the experiment as template.'
current_project: '(current project)'
duplicate_tasks:
success: 'Successfully duplicated %{count} task(s) as template.'
move:
modal_title: 'Move experiment %{experiment}'
notice: 'Moving is possible to projects, where you have permissions to create experiments and tasks.'
@ -1305,6 +1310,12 @@ en:
assigned_html: 'Assigned to'
tags_html: 'Tags'
comments_html: '<i class="fas fa-comment"></i>'
modal_move_modules:
title: "Move task(s) to experiment"
confirm: "Move"
no_experiments: "No experiments to move this task to."
success_flash: "Successfully moved task(s) to experiment %{experiment}."
error_flash: "Failed to move task(s) to experiment %{experiment}."
column_display_modal:
title: 'Task data display'
description: 'Click the eye buttons to hide or show columns in the table'
@ -1322,6 +1333,7 @@ en:
new: 'New task'
edit: 'Edit'
archive: 'Archive'
restore: 'Restore'
move: 'Move'
duplicate: 'Duplicate'
manage_access: 'Manage access'
@ -1333,6 +1345,7 @@ en:
move: 'Move to another experiment'
access: 'Task access'
archive: 'Archive'
restore: 'Restore'
filters:
name: 'Task name'
due_date: 'Due date'
@ -1364,6 +1377,7 @@ en:
name_placeholder: "e.g. My task"
due_date: "Due date (optional)"
due_date_placeholder: "+ Add due date"
assigned_users: "Assign task to (optional)"
tags: "Add tags (optional)"
create: "Create"
enter_placeholder: 'Enter...'
@ -3194,6 +3208,7 @@ en:
create: 'Create'
change: "Change"
remove: "Remove"
clone_label: "Clone"
# In order to use the strings 'yes' and 'no' as keys, you need to wrap them with quotes
'yes': "Yes"
'no': "No"
@ -3210,6 +3225,8 @@ en:
ztoa_html: "<i class=\"fas fa-sort-alpha-up\"></i>&nbsp;&nbsp;Name Z to A"
archived_new_html: "<span class=\"fa-stack\"><i class=\"fas fa-long-arrow-alt-up\"></i><i class=\"fas fa-archive\"></i></span>Archived last"
archived_old_html: "<span class=\"fa-stack\"><i class=\"fas fa-long-arrow-alt-down\"></i><i class=\"fas fa-archive\"></i></span>Archived first"
due_first_html: "<i class=\"fas fa-sort-numeric-up\"></i>&nbsp;&nbsp;Due first"
due_last_html: "<i class=\"fas fa-sort-numeric-up\"></i>&nbsp;&nbsp;Due last"
sort_new:
new: "Newest"
old: "Oldest"

View file

@ -357,6 +357,8 @@ Rails.application.routes.draw do
get 'actions_dropdown'
get :table
get :load_table
get :move_modules_modal
post :move_modules
get 'canvas' # Overview/structure for single experiment
# AJAX-loaded canvas edit mode (from canvas)
get 'canvas/edit', to: 'canvas#edit'
@ -379,6 +381,7 @@ Rails.application.routes.draw do
get 'sidebar'
get :assigned_users_to_tasks
post :archive_my_modules
post :batch_clone_my_modules
end
end
@ -390,6 +393,7 @@ Rails.application.routes.draw do
member do
get :permissions
get :actions_dropdown
get :provisioning_status
end
resources :my_module_tags, path: '/tags', only: [:index, :create, :destroy] do
collection do

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddProvisioningStatusToMyModules < ActiveRecord::Migration[6.1]
def change
add_column :my_modules, :provisioning_status, :integer
end
end