mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-06 21:24:23 +08:00
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:
parent
a84d228e0f
commit
4664ef1d9b
13 changed files with 183 additions and 29 deletions
|
@ -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>`;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
19
app/models/concerns/cloneable.rb
Normal file
19
app/models/concerns/cloneable.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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] =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue