diff --git a/app/assets/javascripts/projects/show.js b/app/assets/javascripts/projects/show.js index ad2af0384..449e87ae3 100644 --- a/app/assets/javascripts/projects/show.js +++ b/app/assets/javascripts/projects/show.js @@ -1,5 +1,11 @@ -/* global filterDropdown */ +/* global filterDropdown Sidebar Turbolinks HelperModule */ (function() { + const PERMISSIONS = ['editable', 'archivable', 'restorable', 'moveable']; + var cardsWrapper = '#cardsWrapper'; + var experimentsPage = '#projectShowWrapper'; + + var selectedExperiments = []; + let experimentsCurrentSort; let experimentsViewSearch; let startedOnFromFilter; @@ -9,6 +15,77 @@ let archivedOnFromFilter; let archivedOnToFilter; + + function checkActionPermission(permission) { + return selectedExperiments.every(function(experimentId) { + return $(`.experiment-card[data-id="${experimentId}"]`).data(permission); + }); + } + + function updateExperimentsToolbar() { + let experimentsToolbar = $('#projectShowToolbar'); + + if (selectedExperiments.length === 0) { + experimentsToolbar.find('.single-object-action, .multiple-object-action').addClass('hidden'); + return; + } + + if (selectedExperiments.length === 1) { + experimentsToolbar.find('.single-object-action, .multiple-object-action').removeClass('hidden'); + } else { + experimentsToolbar.find('.single-object-action').addClass('hidden'); + experimentsToolbar.find('.multiple-object-action').removeClass('hidden'); + } + PERMISSIONS.forEach((permission) => { + if (!checkActionPermission(permission)) { + experimentsToolbar.find(`.btn[data-for="${permission}"]`).addClass('hidden'); + } + }); + } + + function initProjectsViewModeSwitch() { + $(experimentsPage).on('click', '.archive-switch', function() { + Turbolinks.visit($(this).data('url')); + }); + } + + function loadCardsView() { + var viewContainer = $(cardsWrapper); + $.ajax({ + url: viewContainer.data('experiments-cards-url'), + type: 'GET', + dataType: 'json', + data: { + view_mode: $(experimentsPage).data('view-mode'), + sort: experimentsCurrentSort, + search: experimentsViewSearch, + created_on_from: startedOnFromFilter, + created_on_to: startedOnToFilter, + updated_on_from: modifiedOnFromFilter, + updated_on_to: modifiedOnToFilter, + archived_on_from: archivedOnFromFilter, + archived_on_to: archivedOnToFilter + }, + success: function(data) { + viewContainer.find('.card').remove(); + viewContainer.append(data.cards_html); + selectedExperiments.length = 0; + updateExperimentsToolbar(); + }, + error: function() { + viewContainer.html('Error loading project list'); + } + }); + } + + function refreshCurrentView() { + loadCardsView(); + Sidebar.reload({ + sort: experimentsCurrentSort, + view_mode: $(experimentsPage).data('view-mode') + }); + } + function initExperimentsFilters() { var $filterDropdown = filterDropdown.init(); @@ -41,7 +118,7 @@ archivedOnToFilter = $archivedOnToFilter.val(); experimentsViewSearch = $textFilter.val(); appliedFiltersMark(); - //refreshCurrentView(); + refreshCurrentView(); }); // Clear filters @@ -56,6 +133,61 @@ }); } + function initSorting() { + $('#sortMenuDropdown a').click(function() { + if (experimentsCurrentSort !== $(this).data('sort')) { + $('#sortMenuDropdown a').removeClass('selected'); + experimentsCurrentSort = $(this).data('sort'); + refreshCurrentView(); + $(this).addClass('selected'); + $('#sortMenu').dropdown('toggle'); + } + }); + } + + function initExperimentsSelector() { + $(cardsWrapper).on('click', '.experiment-card-selector', function() { + let card = $(this).closest('.experiment-card'); + let experimentId = card.data('id'); + // Determine whether ID is in the list of selected project IDs + let index = $.inArray(experimentId, selectedExperiments); + + // If checkbox is checked and row ID is not in list of selected project IDs + if (this.checked && index === -1) { + $(this).closest('.experiment-card').addClass('selected'); + selectedExperiments.push(experimentId); + // Otherwise, if checkbox is not checked and ID is in list of selected IDs + } else if (!this.checked && index !== -1) { + $(this).closest('.experiment-card').removeClass('selected'); + selectedExperiments.splice(index, 1); + } + updateExperimentsToolbar(); + }); + } + + function initArchiveRestoreToolbarButtons() { + $(experimentsPage) + .on('ajax:before', '.archive-experiments-form, .restore-experiments-form', function() { + let buttonForm = $(this); + buttonForm.find('input[name="experiments_ids[]"]').remove(); + selectedExperiments.forEach(function(id) { + $('').attr({ + type: 'hidden', + name: 'experiments_ids[]', + value: id + }).appendTo(buttonForm); + }); + }) + .on('ajax:success', '.archive-experiments-form, .restore-experiments-form', function(ev, data) { + HelperModule.flashAlertMsg(data.message, 'success'); + // Project saved, reload view + refreshCurrentView(); + }) + .on('ajax:error', '.archive-experiments-form, .restore-experiments-form', function(ev, data) { + HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); + }); + } + function init() { $('.workflowimg-container').each(function() { let container = $(this); @@ -77,6 +209,11 @@ }); initExperimentsFilters(); + initSorting(); + loadCardsView(); + initProjectsViewModeSwitch(); + initExperimentsSelector(); + initArchiveRestoreToolbarButtons(); } init(); diff --git a/app/assets/stylesheets/experiments.scss b/app/assets/stylesheets/experiments.scss index df5bd30a9..21962028c 100644 --- a/app/assets/stylesheets/experiments.scss +++ b/app/assets/stylesheets/experiments.scss @@ -7,14 +7,38 @@ // New experiments page .projects-show { + + &.active { + [data-view-mode="archived"] { + display: none !important; + } + } + + &.archived { + [data-view-mode="active"] { + display: none !important; + } + } + .content-header { .project-name { + align-items: center; + display: flex; max-width: calc(100% - 7em); + .inline-editing-container { + width: calc(100% - 3em); + } + .fas { margin-right: .5em; } } + + .archive-experiments-form, + .restore-experiments-form { + display: inline-block; + } } .project-show-container { diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index 2f1b2fbdd..74c4c61c1 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -7,9 +7,9 @@ class ExperimentsController < ApplicationController include ApplicationHelper include Rails.application.routes.url_helpers - before_action :load_project, only: %i(new create) - before_action :load_experiment, except: %i(new create) - before_action :check_view_permissions, except: %i(edit archive clone move new create) + before_action :load_project, only: %i(new create archive_group restore_group) + before_action :load_experiment, except: %i(new create archive_group restore_group) + before_action :check_view_permissions, except: %i(edit archive clone move new create archive_group restore_group) before_action :check_create_permissions, only: %i(new create) before_action :check_manage_permissions, only: %i(edit) before_action :check_archive_permissions, only: :archive @@ -39,7 +39,7 @@ class ExperimentsController < ApplicationController @experiment.project = @project if @experiment.save experiment_annotation_notification - log_activity(:create_experiment) + log_activity(:create_experiment, @experiment) flash[:success] = t('experiments.create.success_flash', experiment: @experiment.name) respond_to do |format| @@ -98,7 +98,7 @@ class ExperimentsController < ApplicationController else :edit_experiment end - log_activity(activity_type) + log_activity(activity_type, @experiment) respond_to do |format| format.json do @@ -106,7 +106,7 @@ class ExperimentsController < ApplicationController end format.html do flash[:success] = t('experiments.update.success_flash', - experiment: @experiment.name) + experiment: @experiment.name) redirect_to project_path(@experiment.project) end end @@ -128,7 +128,7 @@ class ExperimentsController < ApplicationController @experiment.archived_by = current_user @experiment.archived_on = DateTime.now if @experiment.save - log_activity(:archive_experiment) + log_activity(:archive_experiment, @experiment) flash[:success] = t('experiments.archive.success_flash', experiment: @experiment.name) @@ -139,6 +139,51 @@ class ExperimentsController < ApplicationController end end + def archive_group + experiments = @project.experiments.active.where(id: params[:experiments_ids]) + counter = 0 + experiments.each do |experiment| + next unless can_archive_experiment?(experiment) + + experiment.transaction do + experiment.archived_on = DateTime.now + experiment.archive!(current_user) + log_activity(:archive_experiment, experiment) + counter += 1 + rescue StandardError => e + Rails.logger.error e.message + raise ActiveRecord::Rollback + end + end + if counter.positive? + render json: { message: t('experiments.archive_group.success_flash', number: counter) } + else + render json: { message: t('experiments.archive_group.error_flash') }, status: :unprocessable_entity + end + end + + def restore_group + experiments = @project.experiments.archived.where(id: params[:experiments_ids]) + counter = 0 + experiments.each do |experiment| + next unless can_restore_experiment?(experiment) + + experiment.transaction do + experiment.restore!(current_user) + log_activity(:restore_experiment, experiment) + counter += 1 + rescue StandardError => e + Rails.logger.error e.message + raise ActiveRecord::Rollback + end + end + if counter.positive? + render json: { message: t('experiments.restore_group.success_flash', number: counter) } + else + render json: { message: t('experiments.restore_group.error_flash') }, status: :unprocessable_entity + end + end + # GET: clone_modal_experiment_path(id) def clone_modal @projects = @experiment.projects_with_role_above_user(current_user) @@ -287,6 +332,7 @@ class ExperimentsController < ApplicationController def set_inline_name_editing return unless can_manage_experiment?(@experiment) + @inline_editable_title_config = { name: 'title', params_group: 'experiment', @@ -311,13 +357,13 @@ class ExperimentsController < ApplicationController ) end - def log_activity(type_of) + def log_activity(type_of, experiment) Activities::CreateActivityService .call(activity_type: type_of, owner: current_user, - team: @experiment.project.team, - project: @experiment.project, - subject: @experiment, - message_items: { experiment: @experiment.id }) + team: experiment.project.team, + project: experiment.project, + subject: experiment, + message_items: { experiment: experiment.id }) end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e2121f07c..bb9981a79 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -5,14 +5,15 @@ class ProjectsController < ApplicationController include TeamsHelper include InputSanitizeHelper include ProjectsHelper + include ExperimentsHelper attr_reader :current_folder helper_method :current_folder before_action :switch_team_with_param, only: :index - before_action :load_vars, only: %i(show edit update notifications experiment_archive sidebar) + before_action :load_vars, only: %i(show edit update notifications experiment_archive sidebar experiments_cards) before_action :load_current_folder, only: %i(index cards new show experiment_archive) - before_action :check_view_permissions, only: %i(show notifications experiment_archive sidebar) + before_action :check_view_permissions, only: %i(show notifications experiment_archive sidebar experiments_cards) before_action :check_create_permissions, only: %i(new create) before_action :check_manage_permissions, only: :edit before_action :set_inline_name_editing, only: %i(show) @@ -53,15 +54,15 @@ class ProjectsController < ApplicationController end def sidebar - respond_to do |format| - format.json do - render json: { - html: render_to_string( - partial: 'shared/sidebar/experiments.html.erb', locals: { project: @project } - ) + @current_sort = @project.current_view_state(current_user).state.dig('experiments', params[:view_mode], 'sort') + render json: { + html: render_to_string( + partial: 'shared/sidebar/experiments.html.erb', locals: { + project: @project, + view_mode: experiments_view_mode(@project) } - end - end + ) + } end def new @@ -258,6 +259,17 @@ class ProjectsController < ApplicationController current_team_switch(@project.team) end + def experiments_cards + overview_service = ExperimentsOverviewService.new(@project, current_user, params) + render json: { + cards_html: render_to_string( + partial: 'projects/show/experiments_list.html.erb', + locals: { cards: overview_service.experiments } + ) + } + + end + def notifications @modules = @project .assigned_modules(current_user) diff --git a/app/helpers/experiments_helper.rb b/app/helpers/experiments_helper.rb index eda5a0230..25a755ce8 100644 --- a/app/helpers/experiments_helper.rb +++ b/app/helpers/experiments_helper.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true module ExperimentsHelper + def experiments_view_mode(project) + return 'archived' if project.archived? + + params[:view_mode] == 'archived' ? 'archived' : 'active' + end + def grouped_by_prj(experiments) ungrouped_experiments = experiments.joins(:project) .select('projects.name as project_name, diff --git a/app/services/experiments_overview_service.rb b/app/services/experiments_overview_service.rb index 762723c0a..d06ab1caa 100644 --- a/app/services/experiments_overview_service.rb +++ b/app/services/experiments_overview_service.rb @@ -46,6 +46,11 @@ class ExperimentsOverviewService records = records.where('experiments.created_at > ?', @params[:created_on_from]) end records = records.where('experiments.created_at < ?', @params[:created_on_to]) if @params[:created_on_to].present? + if @params[:updated_on_from].present? + records = records.where('experiments.updated_at > ?', @params[:updated_on_from]) + end + records = records.where('experiments.updated_at < ?', @params[:updated_on_to]) if @params[:updated_on_to].present? + if @params[:archived_on_from].present? records = records.where('COALESCE(experiments.archived_on, projects.archived_on) > ?', @params[:archived_on_from]) end diff --git a/app/views/projects/index/_header.html.erb b/app/views/projects/index/_header.html.erb index 3434cc89d..766e8ee10 100644 --- a/app/views/projects/index/_header.html.erb +++ b/app/views/projects/index/_header.html.erb @@ -48,27 +48,26 @@ <%= t("projects.index.filters_modal.folders.popover_html") %> - - <% end %> - - diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 6f48d6516..65e3b6131 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -1,21 +1,21 @@ <% provide(:head_title, t("projects.show.head_title", project: h(@project.name)).html_safe) %> <% provide(:sidebar_title, t("sidebar.experiments.sidebar_title")) %> -<% provide(:sidebar_url, project_sidebar_path(@project)) %> +<% provide(:sidebar_url, sidebar_project_path(@project)) %> <% provide(:container_class, 'no-second-nav-container') %> <%= content_for :sidebar do %> - <%= render partial: 'shared/sidebar/experiments.html.erb', locals: { project: @project, archived: false } %> + <%= render partial: 'shared/sidebar/experiments.html.erb', locals: { project: @project, view_mode: experiments_view_mode(@project)} %> <% end %> <% content_for :breadcrumbs do %> <%= render partial: 'projects/index/breadcrumbs', locals: { target_folder: @project.project_folder } %> <% end %> -
+
<%= render partial: 'projects/show/header' %>
-
+
@@ -31,11 +31,6 @@
<%= t('experiments.card.description') %>
- - <% @project.sorted_experiments(@current_sort).each do |experiment| %> - <%= render partial: 'projects/show/experiment_card', - locals: { experiment: experiment } %> - <% end %>
diff --git a/app/views/projects/show/_experiment_card.html.erb b/app/views/projects/show/_experiment_card.html.erb index 449bc9077..3ab6e63b9 100644 --- a/app/views/projects/show/_experiment_card.html.erb +++ b/app/views/projects/show/_experiment_card.html.erb @@ -1,4 +1,11 @@ -
+
+>
diff --git a/app/views/projects/show/_experiments_list.html.erb b/app/views/projects/show/_experiments_list.html.erb new file mode 100644 index 000000000..13009eb54 --- /dev/null +++ b/app/views/projects/show/_experiments_list.html.erb @@ -0,0 +1,5 @@ +<% cards.each do |card| %> + <% cache [current_user, card] do %> + <%= render partial: 'projects/show/experiment_card', locals: { experiment: card } %> + <% end %> +<% end %> diff --git a/app/views/projects/show/_header.html.erb b/app/views/projects/show/_header.html.erb index 4ad677fa0..4ca7b154e 100644 --- a/app/views/projects/show/_header.html.erb +++ b/app/views/projects/show/_header.html.erb @@ -1,6 +1,7 @@
diff --git a/app/views/projects/show/_toolbar.html.erb b/app/views/projects/show/_toolbar.html.erb index 55c0c2d1f..233231bd5 100644 --- a/app/views/projects/show/_toolbar.html.erb +++ b/app/views/projects/show/_toolbar.html.erb @@ -4,6 +4,7 @@ <%= link_to new_project_experiment_url(@project), remote: true, type: "button", + data: {view_mode: :active}, id: 'new-experiment', class: 'btn btn-primary' do %> @@ -11,23 +12,38 @@ <% end %> <% end %> - + - + - + - + <%= button_to archive_group_project_experiments_path(@project), + class: 'btn btn-light archive-experiments-btn multiple-object-action hidden', + form_class: 'archive-experiments-form', + data: { for: :archivable, view_mode: 'active' }, + remote: true, + method: :post do %> - - + + <% end %> + + <%= button_to restore_group_project_experiments_path(@project), + class: 'btn btn-light restore-experiments-btn multiple-object-action hidden', + form_class: 'restore-experiments-form', + data: { for: :restorable, view_mode: 'archived' }, + remote: true, + method: :post do %> + + + <% end %>
diff --git a/app/views/shared/sidebar/_experiments.html.erb b/app/views/shared/sidebar/_experiments.html.erb index 164c970c2..5af45586c 100644 --- a/app/views/shared/sidebar/_experiments.html.erb +++ b/app/views/shared/sidebar/_experiments.html.erb @@ -2,24 +2,24 @@ - <% project.sorted_experiments(@current_sort, archived).each do |experiment| %> + <% project.sorted_experiments(@current_sort, view_mode == 'archived').each do |experiment| %> <% end %> - <% unless archived %> + <% if view_mode == 'active' %>