From 492a5de19e40ad4cb3fce37af52c5abaf0ee5919 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Mon, 1 Oct 2018 15:39:42 +0200 Subject: [PATCH 1/2] Add Projects view - list/table view [SCI-2731] --- app/assets/javascripts/projects/index.js | 391 +++++++++++++++--- app/assets/stylesheets/themes/projects.scss | 14 + app/controllers/projects_controller.rb | 5 +- app/services/projects_overview_service.rb | 2 +- app/views/projects/index.html.erb | 4 +- app/views/projects/index/_project.html.erb | 35 +- .../index/_project_actions_dropdown.html.erb | 37 ++ .../index/_team_projects_table.html.erb | 21 + app/views/projects/index_dt.json.jbuilder | 52 ++- .../shared/_secondary_navigation.html.erb | 4 +- config/locales/en.yml | 8 + config/routes.rb | 2 +- 12 files changed, 452 insertions(+), 123 deletions(-) create mode 100644 app/assets/stylesheets/themes/projects.scss create mode 100644 app/views/projects/index/_project_actions_dropdown.html.erb create mode 100644 app/views/projects/index/_team_projects_table.html.erb diff --git a/app/assets/javascripts/projects/index.js b/app/assets/javascripts/projects/index.js index 20e95bebf..9e1605d9e 100644 --- a/app/assets/javascripts/projects/index.js +++ b/app/assets/javascripts/projects/index.js @@ -7,7 +7,8 @@ // - refresh project users tab after manage user modal is closed // - refactor view handling using library, ex. backbone.js -/* global Comments CounterBadge animateSpinner initFormSubmitLinks HelperModule */ +/* global Comments CounterBadge animateSpinner initFormSubmitLinks HelperModule + I18n */ //= require comments (function() { @@ -28,8 +29,15 @@ var projectsViewMode = 'cards'; var projectsViewFilter = $('.projects-view-filter.active').data('filter'); + var projectsViewFilterChanged = false; + var projectsChanged = false; var projectsViewSort = 'new'; + var TABLE; + + // Array with selected project IDs shared between both views + var selectedProjects = []; + /** * Initialize the JS for new project modal to work. */ @@ -61,9 +69,11 @@ .on('ajax:beforeSend', function() { animateSpinner(newProjectModalBody); }) - .on('ajax:success', function(data, status) { - // Redirect to response page - $(location).attr('href', status.url); + .on('ajax:success', function(ev, data) { + projectsChanged = true; + refreshCurrentView(); + newProjectModal.modal('hide'); + HelperModule.flashAlertMsg(data.message, 'success'); }) .on('ajax:error', function(jqxhr, status) { $(this).renderFormErrors('project', status.responseJSON); @@ -79,6 +89,65 @@ }); } + // init project archive/restore function + function initArchiveRestoreButton(el) { + el.find('form.edit_project') + .on('ajax:beforeSend', function() { + animateSpinner($('#projects-cards-view').closest('.tab-content')); + }) + .on('ajax:success', function(ev, data) { + projectsChanged = true; + HelperModule.flashAlertMsg(data.message, 'success'); + // Project saved, reload view + refreshCurrentView(); + }) + .on('ajax:error', function(ev, data) { + HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); + }) + .on('ajax:complete', function() { + animateSpinner($('#projects-cards-view').closest('.tab-content'), false); + }); + } + + function initEditProjectButton(el) { + el.find(".dropdown-menu a[data-action='edit']") + .on('ajax:success', function(ev, data) { + // Update modal title + editProjectModalTitle.html(data.title); + + // Set modal body + editProjectModalBody.html(data.html); + + // Add modal body's submit handler + editProjectModal.find('form') + .on('ajax:beforeSend', function() { + animateSpinner(this); + }) + .on('ajax:success', function(ev2, data2) { + projectsChanged = true; + // Hide modal + editProjectModal.modal('hide'); + + HelperModule.flashAlertMsg(data2.message, 'success'); + + // Project saved, reload view + refreshCurrentView(); + }) + .on('ajax:error', function(ev2, data2) { + $(this).renderFormErrors('project', data2.responseJSON.errors); + }) + .on('ajax:complete', function() { + animateSpinner(this, false); + }); + + // Show the modal + editProjectModal.modal('show'); + }) + .on('ajax:error', function() { + // TODO + }); + } + /** * Initialize the JS for edit project modal to work. */ @@ -93,42 +162,6 @@ editProjectModal.on('hidden.bs.modal', function() { editProjectModalBody.html(''); }); - - $(".panel-project a[data-action='edit']") - .on('ajax:success', function(ev, data) { - // Update modal title - editProjectModalTitle.html(data.title); - - // Set modal body - editProjectModalBody.html(data.html); - - // Add modal body's submit handler - editProjectModal.find('form') - .on('ajax:beforeSend', function() { - animateSpinner(this); - }) - .on('ajax:success', function(ev2, data2) { - // Hide modal - editProjectModal.modal('hide'); - - HelperModule.flashAlertMsg(data2.message, 'success'); - - // Project saved, reload cards view - loadCardsView(); - }) - .on('ajax:error', function(ev2, data2) { - $(this).renderFormErrors('project', data2.responseJSON.errors); - }) - .on('ajax:complete', function() { - animateSpinner(this, false); - }); - - // Show the modal - editProjectModal.modal('show'); - }) - .on('ajax:error', function() { - // TODO - }); } function initManageUsersModal() { @@ -147,6 +180,7 @@ data.counter, data.project_id, 'users' ); initUsersEditLink(projectEl); + projectsChanged = true; }, error: function() { // TODO @@ -232,8 +266,18 @@ initUserRoleForms(); } + function updateSelectedCards() { + $('.panel-project').removeClass('selected'); + $('.project-card-selector').prop('checked', false); + $.each(selectedProjects, function(index, value) { + var selectedCard = $('.panel-project[id=' + value + ']'); + selectedCard.addClass('selected'); + selectedCard.find('.project-card-selector').prop('checked', true); + }); + } + /** - * Initializes page + * Initializes cards view */ function init() { newProjectModal = $('#new-project-modal'); @@ -251,6 +295,7 @@ projectActionsModalBody = projectActionsModal.find('.modal-body'); projectActionsModalFooter = projectActionsModal.find('.modal-footer'); + updateSelectedCards(); initNewProjectModal(); initEditProjectModal(); initManageUsersModal(); @@ -258,31 +303,25 @@ Comments.initEditComments('.panel-project .tab-content'); Comments.initDeleteComments('.panel-project .tab-content'); + initEditProjectButton($('.panel-project')); + initArchiveRestoreButton($('.panel-project')); + $('.project-card-selector').click(function() { - if (this.checked) { + var projectId = $(this).closest('.panel-project').data('id'); + // Determine whether ID is in the list of selected project IDs + var index = $.inArray(projectId, selectedProjects); + + // If checkbox is checked and row ID is not in list of selected project IDs + if (this.checked && index === -1) { $(this).closest('.panel-project').addClass('selected'); - } else { + selectedProjects.push(projectId); + // Otherwise, if checkbox is not checked and ID is in list of selected IDs + } else if (!this.checked && index !== -1) { $(this).closest('.panel-project').removeClass('selected'); + selectedProjects.splice(index, 1); } }); - // init project archive/restore function - $('.panel-project .panel-heading form') - .on('ajax:beforeSend', function() { - animateSpinner($('#projects-cards-view')); - }) - .on('ajax:success', function(ev, data) { - HelperModule.flashAlertMsg(data.message, 'success'); - // Project saved, reload cards view - loadCardsView(); - }) - .on('ajax:error', function(ev, data) { - HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); - }) - .on('ajax:complete', function() { - animateSpinner($('#projects-cards-view'), false); - }); - // initialize project tab remote loading $('.panel-project .active').removeClass('active'); $('.panel-project .panel-footer [role=tab]') @@ -322,6 +361,14 @@ }); } + function refreshCurrentView() { + if (projectsViewMode === 'cards') { + loadCardsView(); + } else { + TABLE.draw(); + } + } + function loadCardsView() { // Load HTML with projects list var viewContainer = $('#projects-cards-view'); @@ -349,14 +396,18 @@ $('.projects-view-filter').click(function(event) { event.preventDefault(); event.stopPropagation(); - $('.projects-view-filter').removeClass('active'); - $(this).addClass('active'); if ($(this).data('filter') === projectsViewFilter) { return; } + $('.projects-view-filter').removeClass('active'); + $(this).addClass('active'); + selectedProjects = []; projectsViewFilter = $(this).data('filter'); + projectsViewFilterChanged = true; if ($('#projects-cards-view').hasClass('active')) { loadCardsView(); + } else if (!$.isEmptyObject(TABLE)) { + TABLE.draw(); } }); } @@ -367,6 +418,10 @@ return; } projectsViewMode = $(this).val(); + if (projectsChanged) { + refreshCurrentView(); + } + projectsChanged = false; }); } @@ -384,6 +439,218 @@ }); } + // Updates "Select all" control in a data table + function updateDataTableSelectAllCtrl() { + var $table = TABLE.table().node(); + var $header = TABLE.table().header(); + var $chkboxAll = $('.project-row-selector', $table); + var $chkboxChecked = $('.project-row-selector:checked', $table); + var chkboxSelectAll = $('input[name="select_all"]', $header).get(0); + + // If none of the checkboxes are checked + if ($chkboxChecked.length === 0) { + chkboxSelectAll.checked = false; + if ('indeterminate' in chkboxSelectAll) { + chkboxSelectAll.indeterminate = false; + } + + // If all of the checkboxes are checked + } else if ($chkboxChecked.length === $chkboxAll.length) { + chkboxSelectAll.checked = true; + if ('indeterminate' in chkboxSelectAll) { + chkboxSelectAll.indeterminate = false; + } + + // If some of the checkboxes are checked + } else { + chkboxSelectAll.checked = true; + if ('indeterminate' in chkboxSelectAll) { + chkboxSelectAll.indeterminate = true; + } + } + } + + function initRowSelection() { + // Handle clicks on checkbox + $('.dt-body-center .project-row-selector').change(function(e) { + // Get row ID + var $row = $(this).closest('tr'); + var data = TABLE.row($row).data(); + var rowId = data.DT_RowId; + + // Determine whether row ID is in the list of selected project IDs + var index = $.inArray(rowId, selectedProjects); + + // If checkbox is checked and row ID is not in list of selected project IDs + if (this.checked && index === -1) { + selectedProjects.push(rowId); + // Otherwise, if checkbox is not checked and ID is in list of selected IDs + } else if (!this.checked && index !== -1) { + selectedProjects.splice(index, 1); + } + + updateDataTableSelectAllCtrl(); + e.stopPropagation(); + }); + + // Handle click on "Select all" control + $('.dataTables_scrollHead input[name="select_all"]').change(function(e) { + if (this.checked) { + $('.project-row-selector:not(:checked)').trigger('click'); + } else { + $('.project-row-selector:checked').trigger('click'); + } + // Prevent click event from propagating to parent + e.stopPropagation(); + }); + } + + function updateSelectedRows() { + TABLE.rows().every(function() { + var rowSelector = $(this.node()).find('input[type="checkbox"]'); + var rowId = this.data().DT_RowId; + + if ($.inArray(rowId, selectedProjects) !== -1) { + rowSelector.prop('checked', true); + } else { + rowSelector.prop('checked', false); + } + }); + + updateDataTableSelectAllCtrl(); + } + + function dataTableInit() { + var TABLE_ID = '#projects-overview-table'; + TABLE = $(TABLE_ID).DataTable({ + dom: "R<'row'<'col-sm-9-custom toolbar'l><'col-sm-3-custom'f>>tpi", + stateSave: false, + processing: true, + serverSide: true, + scrollY: '64vh', + scrollCollapse: true, + destroy: true, + ajax: { + url: $(TABLE_ID).data('source'), + global: false, + type: 'POST', + data: function(params) { + params.filter = projectsViewFilter; + // return { ...params, ...{ filter: projectsViewFilter } }; + } + }, + colReorder: { + fixedColumnsLeft: 9 + }, + columnDefs: [{ + // Checkbox column needs special handling + targets: 0, + searchable: false, + orderable: false, + className: 'dt-body-center', + sWidth: '1%', + render: function() { + return ""; + } + }, { + targets: 8, + searchable: false, + orderable: false, + className: 'dt-body-center', + sWidth: '1%' + }], + oLanguage: { + sSearch: I18n.t('general.filter') + }, + rowCallback: function(row, data) { + // Get row ID + var rowId = data.DT_RowId; + var dropdown = $(row).find('.dropdown'); + var dropdownCell = dropdown.closest('td'); + // If row ID is in the list of selected row IDs + if ($.inArray(rowId, selectedProjects) !== -1) { + $(row).find('input[type="checkbox"]').prop('checked', true); + } + + initEditProjectButton($(row)); + initArchiveRestoreButton($(row)); + + dropdown.on('show.bs.dropdown', function() { + $('body').append(dropdown.css({ + left: dropdown.offset().left, + position: 'absolute', + top: dropdown.offset().top + }).detach()); + }); + dropdown.on('hidden.bs.dropdown', function() { + dropdownCell.append(dropdown.removeAttr('style').detach()); + }); + }, + order: [[2, 'asc']], + columns: [ + { data: 'checkbox' }, + { data: 'status' }, + { data: 'name' }, + { data: 'start' }, + { data: 'visibility' }, + { data: 'users' }, + { data: 'experiments' }, + { data: 'tasks' }, + { data: 'actions' } + ], + fnDrawCallback: function() { + animateSpinner(this, false); + updateDataTableSelectAllCtrl(); + initRowSelection(); + initFormSubmitLinks($(this)); + }, + stateLoadCallback: function() { + // to be implemented + }, + stateSaveCallback: function(settings, data) { + // to be implemented + }, + fnInitComplete: function() { + // to be implemented + } + }); + + // Handle click on table cells with checkboxes + $(TABLE_ID).on('click', 'tbody td', function(e) { + if ($(e.target).is( + '.project-row-selector, .active-project-link, button, span' + )) { + // Skip if clicking on selector checkbox, links and buttons + return; + } + $(this).parent().find('.project-row-selector').trigger('click'); + }); + + return TABLE; + } + + $('.projects-view-mode-switch a').on('shown.bs.tab', function(event) { + if ($(event.target).data('mode') === 'table') { + // table tab + $('#sortMenu').hide(); + if ($.isEmptyObject(TABLE)) { + dataTableInit(); + } else if (projectsViewFilterChanged) { + TABLE.draw(); + } else { + updateSelectedRows(); + } + } else { + // cards tab + $('#sortMenu').show(); + if (projectsViewFilterChanged) { + loadCardsView(); + } + updateSelectedCards(); + } + projectsViewFilterChanged = false; + }); + initProjectsViewFilter(); initProjectsViewModeSwitch(); initSorting(); diff --git a/app/assets/stylesheets/themes/projects.scss b/app/assets/stylesheets/themes/projects.scss new file mode 100644 index 000000000..c308e0699 --- /dev/null +++ b/app/assets/stylesheets/themes/projects.scss @@ -0,0 +1,14 @@ +@import "constants"; + +// Projects overview table + +.projects-overview-table { + .fas { + color: $color-silver-chalice; + margin-right: 5px; + } + + .archived { + background-color: $color-concrete; + } +} diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3d3e59ff7..1e0e2c714 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -49,6 +49,7 @@ class ProjectsController < ApplicationController end def index_dt + @draw = params[:draw].to_i respond_to do |format| format.json do @current_team = current_team if current_team @@ -93,10 +94,10 @@ class ProjectsController < ApplicationController ) ) - flash[:success] = t("projects.create.success_flash", name: @project.name) + message = t('projects.create.success_flash', name: @project.name) respond_to do |format| format.json { - render json: { url: projects_path }, status: :ok + render json: { message: message }, status: :ok } end else diff --git a/app/services/projects_overview_service.rb b/app/services/projects_overview_service.rb index 921d0bb52..407f60042 100644 --- a/app/services/projects_overview_service.rb +++ b/app/services/projects_overview_service.rb @@ -121,7 +121,7 @@ class ProjectsOverviewService def sort(records, params) order = params[:order]&.values&.first if order - dir = order[:dir] == 'DESC' ? 'DESC' : 'ASC' + dir = order[:dir] == 'desc' ? 'DESC' : 'ASC' column_index = order[:column] else dir = 'ASC' diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 324b5eed4..402d85ba1 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -121,7 +121,9 @@
-
Table view
+
+ <%= render partial: "projects/index/team_projects_table" %> +
<% end %> diff --git a/app/views/projects/index/_project.html.erb b/app/views/projects/index/_project.html.erb index 32c14cbfb..956aadf0f 100644 --- a/app/views/projects/index/_project.html.erb +++ b/app/views/projects/index/_project.html.erb @@ -5,40 +5,7 @@ data-project-users-tab-url="<%= url_for project_user_projects_path(project_id: project.id, format: :json) %>">
- <% if (active && can_manage_project?(project)) || (!active && can_restore_project?(project)) %> - - <% end %> + <%= render partial: "projects/index/project_actions_dropdown.html.erb", locals: { project: project, view_mode: 'cards' } %>
diff --git a/app/views/projects/index/_project_actions_dropdown.html.erb b/app/views/projects/index/_project_actions_dropdown.html.erb new file mode 100644 index 000000000..2a1b21d4f --- /dev/null +++ b/app/views/projects/index/_project_actions_dropdown.html.erb @@ -0,0 +1,37 @@ +<% cache project do %> + <% active = !project.archived %> + <% if (true || active && can_manage_project?(project)) || (!active && can_restore_project?(project)) %> + + <% end %> +<% end %> diff --git a/app/views/projects/index/_team_projects_table.html.erb b/app/views/projects/index/_team_projects_table.html.erb new file mode 100644 index 000000000..65c1c8fb3 --- /dev/null +++ b/app/views/projects/index/_team_projects_table.html.erb @@ -0,0 +1,21 @@ +
+ + + + + + + + + + + + + + + +
<%= t("projects.table.status") %><%= t("projects.table.name") %><%= t("projects.table.start") %><%= t("projects.table.visibility") %><%= t("projects.table.users") %><%= t("projects.table.experiments") %><%= t("projects.table.tasks") %>
+
+ +<%= stylesheet_link_tag 'datatables' %> diff --git a/app/views/projects/index_dt.json.jbuilder b/app/views/projects/index_dt.json.jbuilder index ccc92d125..55706a6d4 100644 --- a/app/views/projects/index_dt.json.jbuilder +++ b/app/views/projects/index_dt.json.jbuilder @@ -1,28 +1,40 @@ # frozen_string_literal: true +json.draw @draw json.recordsTotal @projects.total_count -json.recordsFiltered @projects.length +json.recordsFiltered @projects.total_count json.data do json.array! @projects do |project| json.set! 'DT_RowId', project.id - json.set! '1', if project.archived - '' + - I18n.t('projects.index.archived') - else - '' + - I18n.t('projects.index.active') - end - json.set! '2', project.name - json.set! '3', I18n.l(project.created_at, format: :full) - json.set! '4', if project.visibility == 'hidden' - '' + - I18n.t('projects.index.hidden') - else - '' + - I18n.t('projects.index.visible') - end - json.set! '5', project.user_count - json.set! '6', project.experiment_count - json.set! '7', project.task_count + json.set! 'DT_RowClass', project.archived ? 'archived' : '' + json.set! 'status', if project.archived + '' + + I18n.t('projects.index.archived') + else + '' + + I18n.t('projects.index.active') + end + json.set! 'name', if project.archived + escape_input(project.name) + else + link_to(escape_input(project.name), + project_path(project), + class: 'active-project-link') + end + json.set! 'start', I18n.l(project.created_at, format: :full) + json.set! 'visibility', if project.visibility == 'hidden' + '' + + I18n.t('projects.index.hidden') + else + '' + + I18n.t('projects.index.visible') + end + json.set! 'users', project.user_count + json.set! 'experiments', project.experiment_count + json.set! 'tasks', project.task_count + json.set! 'actions', render( + partial: 'projects/index/project_actions_dropdown.html.erb', + locals: { project: project, view_mode: 'table' } + ) end end diff --git a/app/views/shared/_secondary_navigation.html.erb b/app/views/shared/_secondary_navigation.html.erb index 75bc3538f..5c825c129 100644 --- a/app/views/shared/_secondary_navigation.html.erb +++ b/app/views/shared/_secondary_navigation.html.erb @@ -38,11 +38,11 @@
  • - + - + diff --git a/config/locales/en.yml b/config/locales/en.yml index ed03db18d..a93217fb5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -283,6 +283,14 @@ en: invite_users_link: "Invite users" invite_users_details: "to team %{team}." contact_admins: "To invite additional users to team %{team}, contact its administrator/s." + table: + status: "Status" + name: "Project name" + start: "Start date" + visibility: "Visible to" + users: "Users" + experiments: "Experiments" + tasks: "Tasks" create: success_flash: "Project %{name} successfully created." update: diff --git a/config/routes.rb b/config/routes.rb index e52433274..e57749087 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -188,7 +188,7 @@ Rails.application.routes.draw do end get 'projects/archive', to: 'projects#archive', as: 'projects_archive' - get 'projects/index_dt', to: 'projects#index_dt', as: 'projects_index_dt' + post 'projects/index_dt', to: 'projects#index_dt', as: 'projects_index_dt' resources :reports, only: :index get 'reports/datatable', to: 'reports#datatable' From 7a591273a31c73e3423b9c270f1d6be560985dae Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Mon, 8 Oct 2018 13:45:26 +0200 Subject: [PATCH 2/2] Remove unused variables [SCI-2731] --- app/views/projects/index/_project.html.erb | 2 +- app/views/projects/index_dt.json.jbuilder | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/index/_project.html.erb b/app/views/projects/index/_project.html.erb index 8b6877a09..da464a3ec 100644 --- a/app/views/projects/index/_project.html.erb +++ b/app/views/projects/index/_project.html.erb @@ -5,7 +5,7 @@ data-project-users-tab-url="<%= url_for project_user_projects_path(project_id: project.id, format: :json) %>">
    - <%= render partial: "projects/index/project_actions_dropdown.html.erb", locals: { project: project, view_mode: 'cards' } %> + <%= render partial: "projects/index/project_actions_dropdown.html.erb", locals: { project: project } %>
    diff --git a/app/views/projects/index_dt.json.jbuilder b/app/views/projects/index_dt.json.jbuilder index 55706a6d4..9516f674c 100644 --- a/app/views/projects/index_dt.json.jbuilder +++ b/app/views/projects/index_dt.json.jbuilder @@ -34,7 +34,7 @@ json.data do json.set! 'tasks', project.task_count json.set! 'actions', render( partial: 'projects/index/project_actions_dropdown.html.erb', - locals: { project: project, view_mode: 'table' } + locals: { project: project } ) end end