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 @@
+ | <%= 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") %> | ++ |
---|