From 2901c7b6271761cb87524b4e9f9b9d675a74bb2a Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 28 Nov 2022 12:44:15 +0100 Subject: [PATCH 01/12] Add restore action to experiment table [SCI-7453] --- app/assets/javascripts/experiments/table.js | 14 ++++++++++++++ app/controllers/my_modules_controller.rb | 7 ++++++- app/services/experiments/table_view_service.rb | 3 ++- app/views/experiments/_table_row_actions.html.erb | 8 ++++++++ app/views/experiments/_table_toolbar.html.erb | 4 ++++ config/locales/en.yml | 2 ++ 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/experiments/table.js b/app/assets/javascripts/experiments/table.js index 7ea9a3280..c3f357017 100644 --- a/app/assets/javascripts/experiments/table.js +++ b/app/assets/javascripts/experiments/table.js @@ -70,6 +70,11 @@ 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); + }); }, initArchiveMyModules: function() { $('#archiveTask').on('click', (e) => { @@ -84,6 +89,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(); @@ -299,6 +312,7 @@ var ExperimnetTable = { this.initNewTaskModal(this); this.initMyModuleActions(); this.updateExperimentToolbar(); + this.initRestoreMyModules(); } }; diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 4924fe264..b274d7f6d 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -362,7 +362,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 diff --git a/app/services/experiments/table_view_service.rb b/app/services/experiments/table_view_service.rb index 5780fab6e..00727298a 100644 --- a/app/services/experiments/table_view_service.rb +++ b/app/services/experiments/table_view_service.rb @@ -70,7 +70,8 @@ module Experiments 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) + access: edit_access_permissions_project_experiment_my_module_path(project, experiment, my_module), + restore: restore_my_modules_experiment_path(experiment) } } end diff --git a/app/views/experiments/_table_row_actions.html.erb b/app/views/experiments/_table_row_actions.html.erb index a06cd58fe..3ae2f51a9 100644 --- a/app/views/experiments/_table_row_actions.html.erb +++ b/app/views/experiments/_table_row_actions.html.erb @@ -37,3 +37,11 @@ <% end %> +<% if can_restore_my_module?(my_module) %> +
  • + + + <%= t("experiments.table.my_module_actions.restore") %> + +
  • +<% end %> diff --git a/app/views/experiments/_table_toolbar.html.erb b/app/views/experiments/_table_toolbar.html.erb index 94e73a12e..d74f460a2 100644 --- a/app/views/experiments/_table_toolbar.html.erb +++ b/app/views/experiments/_table_toolbar.html.erb @@ -32,6 +32,10 @@ <%= t("experiments.table.toolbar.archive") %> +
    + +
    + + + + + diff --git a/app/views/experiments/table.html.erb b/app/views/experiments/table.html.erb index ad7f0f45c..2d67bb95d 100644 --- a/app/views/experiments/table.html.erb +++ b/app/views/experiments/table.html.erb @@ -13,7 +13,9 @@
    + 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) %>" >
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 945191b58..4231cfe74 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1305,6 +1305,12 @@ en: assigned_html: 'Assigned to' tags_html: 'Tags' comments_html: '' + 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' diff --git a/config/routes.rb b/config/routes.rb index e84625045..5c49a9d7d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' From 4664ef1d9b0b6d2a6ba3913e327787b4e049902a Mon Sep 17 00:00:00 2001 From: artoscinote <85488244+artoscinote@users.noreply.github.com> Date: Thu, 1 Dec 2022 15:08:59 +0100 Subject: [PATCH 06/12] 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] --- app/assets/javascripts/experiments/table.js | 71 ++++++++++++++++--- app/assets/stylesheets/experiment/table.scss | 28 ++++++++ app/controllers/experiments_controller.rb | 28 +++++++- app/controllers/my_modules_controller.rb | 4 ++ app/models/concerns/cloneable.rb | 19 +++++ app/models/experiment.rb | 5 ++ app/models/my_module.rb | 26 ++++--- .../copy_experiment_as_template_service.rb | 11 +-- .../experiments/table_view_service.rb | 6 +- app/views/experiments/_table_toolbar.html.erb | 2 +- config/locales/en.yml | 3 + config/routes.rb | 2 + ...7_add_provisioning_status_to_my_modules.rb | 7 ++ 13 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 app/models/concerns/cloneable.rb create mode 100644 db/migrate/20221122132857_add_provisioning_status_to_my_modules.rb diff --git a/app/assets/javascripts/experiments/table.js b/app/assets/javascripts/experiments/table.js index 0ed5c7671..b4a90f495 100644 --- a/app/assets/javascripts/experiments/table.js +++ b/app/assets/javascripts/experiments/table.js @@ -21,14 +21,18 @@ var ExperimnetTable = { }, appendRows: function(result) { $.each(result, (id, data) => { + let row; + // Checkbox selector - let row = ` -
    -
    - - -
    -
    `; + row = ` +
    +
    +
    + + +
    +
    `; + // Task columns $.each(data.columns, (_i, cell) => { let hidden = ''; @@ -53,7 +57,9 @@ var ExperimnetTable = {
    `; - $(`
    ${row}
    `) + + let tableRowClass = `table-row ${data.provisioning_status === 'in_progress' ? 'table-row-provisioning' : ''}`; + $(`
    ${row}
    `) .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 `${data.name}`; + } + return `${data.name}`; }; diff --git a/app/assets/stylesheets/experiment/table.scss b/app/assets/stylesheets/experiment/table.scss index ea1ea0e4b..b0f118302 100644 --- a/app/assets/stylesheets/experiment/table.scss +++ b/app/assets/stylesheets/experiment/table.scss @@ -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 { diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index 23e791df8..d3e1cb57f 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -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 diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index b54ee9077..71a486dca 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -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 diff --git a/app/models/concerns/cloneable.rb b/app/models/concerns/cloneable.rb new file mode 100644 index 000000000..498a10636 --- /dev/null +++ b/app/models/concerns/cloneable.rb @@ -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 diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 77dc0e217..d8aea4823 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -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 diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 456f1a636..adc49f81d 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -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 diff --git a/app/services/experiments/copy_experiment_as_template_service.rb b/app/services/experiments/copy_experiment_as_template_service.rb index be4c3b2ed..7988d17df 100644 --- a/app/services/experiments/copy_experiment_as_template_service.rb +++ b/app/services/experiments/copy_experiment_as_template_service.rb @@ -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] = diff --git a/app/services/experiments/table_view_service.rb b/app/services/experiments/table_view_service.rb index 1e3616f3a..b07b2616f 100644 --- a/app/services/experiments/table_view_service.rb +++ b/app/services/experiments/table_view_service.rb @@ -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 diff --git a/app/views/experiments/_table_toolbar.html.erb b/app/views/experiments/_table_toolbar.html.erb index 94e73a12e..47722fc97 100644 --- a/app/views/experiments/_table_toolbar.html.erb +++ b/app/views/experiments/_table_toolbar.html.erb @@ -15,7 +15,7 @@ <%= t("experiments.table.toolbar.edit") %> <% if can_manage_experiment?(@experiment) %> - diff --git a/config/locales/en.yml b/config/locales/en.yml index 4231cfe74..0f5461083 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index 5c49a9d7d..3822c30c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20221122132857_add_provisioning_status_to_my_modules.rb b/db/migrate/20221122132857_add_provisioning_status_to_my_modules.rb new file mode 100644 index 000000000..b795d4cf0 --- /dev/null +++ b/db/migrate/20221122132857_add_provisioning_status_to_my_modules.rb @@ -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 From 882178a2ca3806d9062b486cce8d71922e8d0c18 Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Fri, 2 Dec 2022 09:45:02 +0100 Subject: [PATCH 07/12] Add missing job [SCI-7382] --- app/jobs/my_modules/copy_content_job.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/jobs/my_modules/copy_content_job.rb diff --git a/app/jobs/my_modules/copy_content_job.rb b/app/jobs/my_modules/copy_content_job.rb new file mode 100644 index 000000000..ed6f98421 --- /dev/null +++ b/app/jobs/my_modules/copy_content_job.rb @@ -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 From 12f141b82ed72a4b0279afb69e52552dfa9cabce Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 1 Dec 2022 09:34:29 +0100 Subject: [PATCH 08/12] Experiment table connect task action in dropdown [SCI-7525] --- app/assets/javascripts/experiments/table.js | 101 +++++++++++------- app/assets/stylesheets/experiment/table.scss | 5 + app/controllers/experiments_controller.rb | 2 +- .../experiments/_table_display_modal.html.erb | 5 +- .../experiments/_table_row_actions.html.erb | 6 +- 5 files changed, 77 insertions(+), 42 deletions(-) diff --git a/app/assets/javascripts/experiments/table.js b/app/assets/javascripts/experiments/table.js index b4a90f495..3b553514d 100644 --- a/app/assets/javascripts/experiments/table.js +++ b/app/assets/javascripts/experiments/table.js @@ -76,14 +76,34 @@ var ExperimnetTable = { e.preventDefault(); this.archiveMyModules(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) => { - $.post(e.target.dataset.url, { my_module_ids: this.selectedMyModules }, () => { - this.loadTable(); - }).error((data) => { - HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); - }); + 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() { @@ -107,23 +127,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'); }, @@ -216,33 +238,36 @@ var ExperimnetTable = { } }); }, - initMoveModulesModal: function () { - let table = $(this.table); + initMoveModulesModal: function() { $('#moveTask').on('click', () => { - $.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: this.selectedMyModules - }; - $.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'); + 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) { @@ -326,7 +351,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); @@ -343,7 +369,8 @@ var ExperimnetTable = { let visibleColumns = $('.table-display-modal .fa-eye').map((_i, col) => col.dataset.column).toArray(); // Update columns on backend - $.post('', { 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) { diff --git a/app/assets/stylesheets/experiment/table.scss b/app/assets/stylesheets/experiment/table.scss index b0f118302..cddb4d39d 100644 --- a/app/assets/stylesheets/experiment/table.scss +++ b/app/assets/stylesheets/experiment/table.scss @@ -434,6 +434,11 @@ .fas { cursor: pointer; margin-right: 1em; + + &.disabled { + color: $color-alto; + pointer-events: none; + } } &.task_name { diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index d3e1cb57f..45fc7f78a 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -95,7 +95,7 @@ class ExperimentsController < ApplicationController 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 end diff --git a/app/views/experiments/_table_display_modal.html.erb b/app/views/experiments/_table_display_modal.html.erb index c3f382ba1..29a7b49c2 100644 --- a/app/views/experiments/_table_display_modal.html.erb +++ b/app/views/experiments/_table_display_modal.html.erb @@ -11,11 +11,14 @@

    <%= t("experiments.table.column_display_modal.description") %>

    <% Experiments::TableViewService::COLUMNS.each do |col| %>
    - <% unless col == :task_name %> + <% if col == :archived && params[:view_mode] != 'archived' %> + + <% elsif col != :task_name %> <% end %> <%= t("experiments.table.column_display_modal.#{col}") %>
    + <% end %>
    diff --git a/app/views/experiments/_table_row_actions.html.erb b/app/views/experiments/_table_row_actions.html.erb index a06cd58fe..3fe0aca39 100644 --- a/app/views/experiments/_table_row_actions.html.erb +++ b/app/views/experiments/_table_row_actions.html.erb @@ -1,7 +1,7 @@
  • <%= t("experiments.table.my_module_actions.title") %>
  • <% if can_manage_my_module?(my_module) %>
  • - + <%= t("experiments.table.my_module_actions.edit") %> @@ -9,7 +9,7 @@ <% end %> <% if can_manage_experiment?(my_module.experiment) && my_module.active? %>
  • - + <%= t("experiments.table.my_module_actions.duplicate") %> @@ -17,7 +17,7 @@ <% end %> <% if can_move_my_module?(my_module) %>
  • - + <%= t("experiments.table.my_module_actions.move") %> From 8ff822c7e5747334557240fcfe742d48f501cff0 Mon Sep 17 00:00:00 2001 From: ajugo Date: Fri, 2 Dec 2022 11:01:40 +0100 Subject: [PATCH 09/12] Implement due date column in experiment table view [SCI-7406] (#4646) * Implement due date column in experiment table view [SCI-7406] * Fix due date [SCI-7406] * Move text to translation [SCI-7406] * Fix table due date partial [SCI-7406] * Fix table due date partial [SCI-7406] --- app/assets/javascripts/experiments/table.js | 45 +++++++++++++++- .../javascripts/sitewide/date_time_picker.js | 2 +- app/assets/stylesheets/experiment/table.scss | 53 +++++++++++++++++++ app/controllers/my_modules_controller.rb | 6 +++ app/helpers/my_modules_helper.rb | 10 ++++ .../experiments/table_view_service.rb | 16 ++++-- .../experiments/_table_due_date.html.erb | 32 +++++++++++ .../_table_due_date_label.html.erb | 17 ++++++ config/locales/en.yml | 3 ++ 9 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 app/views/experiments/_table_due_date.html.erb create mode 100644 app/views/experiments/_table_due_date_label.html.erb diff --git a/app/assets/javascripts/experiments/table.js b/app/assets/javascripts/experiments/table.js index b4a90f495..7ea4edff2 100644 --- a/app/assets/javascripts/experiments/table.js +++ b/app/assets/javascripts/experiments/table.js @@ -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'], @@ -63,6 +63,44 @@ var ExperimnetTable = { .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'); @@ -392,6 +430,7 @@ var ExperimnetTable = { $.get(dataUrl, { filters: this.activeFilters }, (result) => { $(this.table).find('.table-row').remove(); this.appendRows(result.data); + this.initDueDatePicker(result.data); InfiniteScroll.init(this.table, { url: dataUrl, eventTarget: window, @@ -401,12 +440,14 @@ var ExperimnetTable = { lastPage: !result.next_page, customResponse: (response) => { this.appendRows(response.data); + this.initDueDatePicker(response.data); }, customParams: (params) => { return { ...params, ...{ filters: this.activeFilters } }; } }); + initBSTooltips(); this.initProvisioningStatusPolling(); }); }, @@ -476,7 +517,7 @@ ExperimnetTable.render.id = function(data) { }; ExperimnetTable.render.due_date = function(data) { - return data; + return data.data; }; ExperimnetTable.render.archived = function(data) { diff --git a/app/assets/javascripts/sitewide/date_time_picker.js b/app/assets/javascripts/sitewide/date_time_picker.js index ce64dddb3..94260e1cb 100644 --- a/app/assets/javascripts/sitewide/date_time_picker.js +++ b/app/assets/javascripts/sitewide/date_time_picker.js @@ -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(); diff --git a/app/assets/stylesheets/experiment/table.scss b/app/assets/stylesheets/experiment/table.scss index b0f118302..ea9d7317c 100644 --- a/app/assets/stylesheets/experiment/table.scss +++ b/app/assets/stylesheets/experiment/table.scss @@ -353,6 +353,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; diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 71a486dca..1189f811f 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -5,6 +5,7 @@ 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 :check_create_permissions, only: %i(new create) @@ -226,6 +227,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 } diff --git a/app/helpers/my_modules_helper.rb b/app/helpers/my_modules_helper.rb index 1627a63dc..a091675d7 100644 --- a/app/helpers/my_modules_helper.rb +++ b/app/helpers/my_modules_helper.rb @@ -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 diff --git a/app/services/experiments/table_view_service.rb b/app/services/experiments/table_view_service.rb index b07b2616f..854b5be2f 100644 --- a/app/services/experiments/table_view_service.rb +++ b/app/services/experiments/table_view_service.rb @@ -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 @@ -102,11 +105,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) diff --git a/app/views/experiments/_table_due_date.html.erb b/app/views/experiments/_table_due_date.html.erb new file mode 100644 index 000000000..20b59fd25 --- /dev/null +++ b/app/views/experiments/_table_due_date.html.erb @@ -0,0 +1,32 @@ +<% due_date_editable = can_update_my_module_due_date?(user, my_module)%> +<% due_status = my_module_due_status(my_module) %> + +
    + + + <%= render partial: "experiments/table_due_date_label.html.erb" , + locals: { my_module: my_module, user: user } %> + + <% if due_date_editable %> +
    + +
    +
    +
    + <% end %> +
    +
    diff --git a/app/views/experiments/_table_due_date_label.html.erb b/app/views/experiments/_table_due_date_label.html.erb new file mode 100644 index 000000000..cb309ed34 --- /dev/null +++ b/app/views/experiments/_table_due_date_label.html.erb @@ -0,0 +1,17 @@ + + <% if my_module.is_one_day_prior? %> + <%= l(my_module.due_date, format: :full_date) %> + + <% elsif my_module.is_overdue? %> + <%= l(my_module.due_date, format: :full_date) %> + + <% elsif my_module.due_date %> + <%= l(my_module.due_date, format: :full_date) %> + <% elsif can_update_my_module_due_date?(user, my_module) %> + + <%= t('my_modules.details.no_due_date_placeholder') %> + + <% else %> + <%= t('my_modules.details.no_due_date') %> + <% end %> + diff --git a/config/locales/en.yml b/config/locales/en.yml index 0f5461083..d7b511375 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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:" From 02f039ffed45f14ea82939b944d255526f9a451f Mon Sep 17 00:00:00 2001 From: ajugo Date: Mon, 5 Dec 2022 10:30:20 +0100 Subject: [PATCH 10/12] Implement sorting for experiment table view [SCI-7451] [SCI-7497] (#4659) * Implement experiment table sort flyout [SCI-7451] * Implement archive sort options for experiment table [SCI-7497] * Fix hound [SCI-7451] * Clean code for experiment table view sorting [SCI-7497] * Fix hound [SCI-7451] --- app/assets/javascripts/experiments/table.js | 28 ++++++-- app/assets/stylesheets/experiment/table.scss | 16 +++++ .../stylesheets/shared/content_pane.scss | 4 -- app/controllers/experiments_controller.rb | 6 +- app/models/experiment.rb | 18 +++++ .../experiments/table_view_service.rb | 67 ++++++++++++++----- app/views/experiments/_show_header.html.erb | 19 ++++++ config/locales/en.yml | 2 + 8 files changed, 134 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/experiments/table.js b/app/assets/javascripts/experiments/table.js index 74243f882..758ef621d 100644 --- a/app/assets/javascripts/experiments/table.js +++ b/app/assets/javascripts/experiments/table.js @@ -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,7 +21,7 @@ var ExperimnetTable = { $(placeholder).insertAfter($(this.table).find('.table-body')); }, appendRows: function(result) { - $.each(result, (id, data) => { + $.each(result, (_j, data) => { let row; // Checkbox selector @@ -28,7 +29,7 @@ var ExperimnetTable = {
    - +
    `; @@ -59,7 +60,7 @@ var ExperimnetTable = { `; let tableRowClass = `table-row ${data.provisioning_status === 'in_progress' ? 'table-row-provisioning' : ''}`; - $(`
    ${row}
    `) + $(`
    ${row}
    `) .appendTo(`${this.table} .table-body`); }); }, @@ -430,6 +431,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'); @@ -466,9 +479,13 @@ 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); @@ -484,7 +501,7 @@ var ExperimnetTable = { this.initDueDatePicker(response.data); }, customParams: (params) => { - return { ...params, ...{ filters: this.activeFilters } }; + return { ...params, ...tableParams }; } }); @@ -529,6 +546,7 @@ var ExperimnetTable = { this.initSelector(); this.initSelectAllCheckbox(); this.initFilters(); + this.initSorting(this); this.loadTable(); this.initRenameModal(); this.initAccessModal(); diff --git a/app/assets/stylesheets/experiment/table.scss b/app/assets/stylesheets/experiment/table.scss index a1f8ea912..555049c90 100644 --- a/app/assets/stylesheets/experiment/table.scss +++ b/app/assets/stylesheets/experiment/table.scss @@ -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; diff --git a/app/assets/stylesheets/shared/content_pane.scss b/app/assets/stylesheets/shared/content_pane.scss index 1c1e4e3b5..9cee83fd3 100644 --- a/app/assets/stylesheets/shared/content_pane.scss +++ b/app/assets/stylesheets/shared/content_pane.scss @@ -49,10 +49,6 @@ display: flex; flex-shrink: 0; margin-left: auto; - - &.experiment-header { - column-gap: .25em; - } } .view-switch { diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index 45fc7f78a..6f68f4f8c 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -90,6 +90,10 @@ 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) end @@ -97,7 +101,7 @@ class ExperimentsController < ApplicationController def load_table 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 diff --git a/app/models/experiment.rb b/app/models/experiment.rb index d8aea4823..290e8744f 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -9,6 +9,7 @@ class Experiment < ApplicationRecord include ArchivableModel include SearchableModel include SearchableByNameModel + include ViewableModel include PermissionCheckableModel include Assignable include Cloneable @@ -94,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' diff --git a/app/services/experiments/table_view_service.rb b/app/services/experiments/table_view_service.rb index 0db15bab0..ef23a8f28 100644 --- a/app/services/experiments/table_view_service.rb +++ b/app/services/experiments/table_view_service.rb @@ -35,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') @@ -67,20 +71,20 @@ module Experiments experiment = my_module.experiment project = experiment.project - 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), - 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) - - } - } + 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 { @@ -193,5 +197,36 @@ module Experiments def statuses_filter(my_modules, 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 diff --git a/app/views/experiments/_show_header.html.erb b/app/views/experiments/_show_header.html.erb index 2d77efb0c..3e52a2c17 100644 --- a/app/views/experiments/_show_header.html.erb +++ b/app/views/experiments/_show_header.html.erb @@ -63,6 +63,25 @@ <% end %> <% if action_name == 'table' %> <%= render partial: 'table_filters.html.erb' %> + + <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 93a982006..fa6290568 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3224,6 +3224,8 @@ en: ztoa_html: "  Name Z to A" archived_new_html: "Archived last" archived_old_html: "Archived first" + due_first_html: "  Due first" + due_last_html: "  Due last" sort_new: new: "Newest" old: "Oldest" From 4b83f77c5b7484bb05a590f4fbc5970ba2233347 Mon Sep 17 00:00:00 2001 From: ajugo Date: Mon, 5 Dec 2022 12:55:57 +0100 Subject: [PATCH 11/12] Add new task modal field for Assign users for experiment views [SCI-7450] (#4670) * Add new task modal field for Assign users for experiment views [SCI-7450] * Fix loading users assigned to the experiment [SCI-7450] * Fix task creation on experiment if none exists [SCI-7450] --- app/assets/javascripts/experiments/show.js | 42 ++++++++++++++++--- app/assets/stylesheets/experiment/show.scss | 25 +++++++++++ app/assets/stylesheets/experiment/table.scss | 9 ---- app/controllers/my_modules_controller.rb | 22 +++++++--- .../my_modules/modals/_new_modal.html.erb | 23 +++++++++- config/locales/en.yml | 1 + 6 files changed, 101 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/experiments/show.js b/app/assets/javascripts/experiments/show.js index c4832498c..9de3cd6a1 100644 --- a/app/assets/javascripts/experiments/show.js +++ b/app/assets/javascripts/experiments/show.js @@ -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 `${data.label} + ${data.label}`; + }, + optionLabel: (data) => { + if (data.params.avatar_url) { + return ` + ${data.label} + ${data.label}`; + } + + return data.label; + } + }); + + dropdownSelector.selectValues(myModuleUserSelector, $('#new-my-module-modal').data('user-id')); }); } diff --git a/app/assets/stylesheets/experiment/show.scss b/app/assets/stylesheets/experiment/show.scss index 71991edc2..5da5c4285 100644 --- a/app/assets/stylesheets/experiment/show.scss +++ b/app/assets/stylesheets/experiment/show.scss @@ -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; + } + } +} diff --git a/app/assets/stylesheets/experiment/table.scss b/app/assets/stylesheets/experiment/table.scss index 555049c90..997944650 100644 --- a/app/assets/stylesheets/experiment/table.scss +++ b/app/assets/stylesheets/experiment/table.scss @@ -472,15 +472,6 @@ } } -.datetime-picker-container { - width: 45%; - - .fa-calendar-alt { - color: $color-volcano !important; - font-size: 14px !important; - } -} - .task_name-column span { color: $color-silver-chalice; } diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index d7d3973e8..1e7bfcfb7 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -7,13 +7,15 @@ class MyModulesController < ApplicationController include ApplicationHelper include MyModulesHelper - before_action :load_vars, except: %i(restore_group) + before_action :load_vars, except: %i(restore_group create new) + 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 + 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) 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) @@ -22,17 +24,20 @@ 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 def create max_xy = @experiment.my_modules.select('MAX("my_modules"."x") AS x, MAX("my_modules"."y") AS y').take - x = max_xy ? (max_xy.x + 10) : 1 - y = max_xy ? (max_xy.y + 10) : 1 + 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 @@ -434,6 +439,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) diff --git a/app/views/my_modules/modals/_new_modal.html.erb b/app/views/my_modules/modals/_new_modal.html.erb index 9ff71149c..d17753e89 100644 --- a/app/views/my_modules/modals/_new_modal.html.erb +++ b/app/views/my_modules/modals/_new_modal.html.erb @@ -1,4 +1,5 @@ -