Implement task cloning in experiments table [SCI-7382] (#4653)

* Implement task cloning in experiments table [SCI-7382]

* Fix provisioning status polling [SCI-7382]

* Remove unused method [SCI-7382]

* Fix linter issues [SCI-7382]

* Fix fetching last clone number [SCI-7382]

* Fixing experiment duplication [SCI-7382]

* Add truncation to cloned name [SCI-7382]

* Add readable scope to batch clone action [SCI-7382]

* Move 'Clone' to translations, simplify JS [SCI-7382]
This commit is contained in:
artoscinote 2022-12-01 15:08:59 +01:00 committed by GitHub
parent a84d228e0f
commit 4664ef1d9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 183 additions and 29 deletions

View file

@ -21,14 +21,18 @@ var ExperimnetTable = {
},
appendRows: function(result) {
$.each(result, (id, 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="${id}">
<span class="sci-checkbox-label"></span>
</div>
</div>`;
// Task columns
$.each(data.columns, (_i, cell) => {
let hidden = '';
@ -53,7 +57,9 @@ 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="${id}">${row}</div>`)
.appendTo(`${this.table} .table-body`);
});
},
@ -71,6 +77,15 @@ var ExperimnetTable = {
this.archiveMyModules(e.target.href, e.target.dataset.id);
});
},
initDuplicateMyModules: function() {
$('#duplicateTasks').on('click', (e) => {
$.post(e.target.dataset.url, { my_module_ids: this.selectedMyModules }, () => {
this.loadTable();
}).error((data) => {
HelperModule.flashAlertMsg(data.responseJSON.message, 'danger');
});
});
},
initArchiveMyModules: function() {
$('#archiveTask').on('click', (e) => {
this.archiveMyModules(e.target.dataset.url, this.selectedMyModules);
@ -391,8 +406,43 @@ var ExperimnetTable = {
return { ...params, ...{ filters: this.activeFilters } };
}
});
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();
@ -400,6 +450,7 @@ var ExperimnetTable = {
this.loadTable();
this.initRenameModal();
this.initAccessModal();
this.initDuplicateMyModules();
this.initMoveModulesModal();
this.initArchiveMyModules();
this.initManageColumnsModal();
@ -411,6 +462,10 @@ var ExperimnetTable = {
};
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>`;
};

View file

@ -69,6 +69,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;
@ -387,6 +412,9 @@
}
}
.task_name-column span {
color: $color-silver-chalice;
}
.table-display-modal {
.column-container {

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)
@ -402,6 +402,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

@ -407,6 +407,10 @@ class MyModulesController < ApplicationController
end
end
def provisioning_status
render json: { provisioning_status: @my_module.provisioning_status }
end
private
def load_vars

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

@ -11,6 +11,7 @@ class Experiment < ApplicationRecord
include SearchableByNameModel
include PermissionCheckableModel
include Assignable
include Cloneable
before_save -> { report_elements.destroy_all }, if: -> { !new_record? && project_id_changed? }
@ -224,6 +225,10 @@ class Experiment < ApplicationRecord
.with_granted_permissions(current_user, ProjectPermissions::EXPERIMENTS_CREATE)
end
def parent
project
end
def permission_parent
project
end

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

@ -66,10 +66,13 @@ module Experiments
result[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),
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)
}
}
@ -87,6 +90,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
@ -179,7 +183,7 @@ module Experiments
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
end
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>

View file

@ -1282,6 +1282,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.'
@ -3200,6 +3202,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"

View file

@ -381,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 +391,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