Merge pull request #1321 from okriuchykhin/ok_SCI_2731

Add Projects view - list/table view [SCI-2731]
This commit is contained in:
Alex Kriuchykhin 2018-10-08 17:53:35 +02:00 committed by GitHub
commit 16b29b50a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 454 additions and 124 deletions

View file

@ -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 "<input class='project-row-selector' type='checkbox'>";
}
}, {
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();

View file

@ -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;
}
}

View file

@ -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

View file

@ -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'

View file

@ -121,7 +121,9 @@
<div class="tab-content">
<div class="tab-pane active" id="projects-cards-view" data-projects-url="<%= projects_path %>"></div>
<div class="tab-pane" id="projects-table-view">Table view</div>
<div class="tab-pane" id="projects-table-view">
<%= render partial: "projects/index/team_projects_table" %>
</div>
</div>
<% end %>

View file

@ -5,41 +5,7 @@
data-project-users-tab-url="<%= url_for project_user_projects_path(project_id: project.id, format: :json) %>">
<div class="panel-heading">
<% if (active && (can_manage_project?(project) || can_archive_project?(project))) || (!active && can_restore_project?(project)) %>
<div class="dropdown pull-right">
<button class="btn btn-link dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="caret"></span>
</button>
<% project_form = nil %>
<%= form_for project, format: :json, method: :put, remote: true do |f| %>
<% project_form = f %>
<%= f.hidden_field :archived, value: active %>
<% end %>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu1" style="top: 105%;">
<li class="dropdown-header"><%= t('projects.index.options_header') %></li>
<% if active && can_manage_project?(project) %>
<li>
<%= link_to t("projects.index.edit_option"), edit_project_path(project, format: :json), remote: true, "data-action" => "edit" %>
</li>
<% elsif active && can_archive_project?(project) %>
<li>
<a href="#"
class="form-submit-link"
data-turbolinks="false"
data-submit-form="<%= project_form.options[:html][:id] %>"
data-confirm-form="<%= t("projects.index.archive_confirm") %>"><%= t 'projects.index.archive_option' %></a>
</li>
<% else %>
<li>
<a href="#"
class="form-submit-link"
data-turbolinks="false"
data-submit-form="<%= project_form.options[:html][:id] %>"><%= t 'projects.index.restore_option' %></a>
</li>
<% end %>
</ul>
</div>
<% end %>
<%= render partial: "projects/index/project_actions_dropdown.html.erb", locals: { project: project } %>
<div class="pull-right">
<input class="project-card-selector" type="checkbox" name="project-<%= project.id %>">

View file

@ -0,0 +1,39 @@
<% cache project do %>
<% active = !project.archived %>
<% if (active && (can_manage_project?(project) || can_archive_project?(project))) || (!active && can_restore_project?(project)) %>
<div class="dropdown pull-right">
<button class="btn btn-link dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="caret"></span>
</button>
<% project_form = nil %>
<%= form_for project, format: :json, method: :put, remote: true do |f| %>
<% project_form = f %>
<%= f.hidden_field :archived, value: active %>
<% end %>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu1" style="top: 105%;">
<li class="dropdown-header"><%= t('projects.index.options_header') %></li>
<% if active && can_manage_project?(project) %>
<li>
<%= link_to t("projects.index.edit_option"), edit_project_path(project, format: :json), remote: true, "data-action" => "edit" %>
</li>
<% end %>
<% if active && can_archive_project?(project) %>
<li>
<a href="#"
class="form-submit-link"
data-turbolinks="false"
data-submit-form="<%= project_form.options[:html][:id] %>"
data-confirm-form="<%= t("projects.index.archive_confirm") %>"><%= t 'projects.index.archive_option' %></a>
</li>
<% elsif !active %>
<li>
<a href="#"
class="form-submit-link"
data-turbolinks="false"
data-submit-form="<%= project_form.options[:html][:id] %>"><%= t 'projects.index.restore_option' %></a>
</li>
<% end %>
</ul>
</div>
<% end %>
<% end %>

View file

@ -0,0 +1,21 @@
<div class="projects-overview-table">
<table id="projects-overview-table" class="table"
data-source="<%= projects_index_dt_path %>">
<thead>
<tr>
<th><input name="select_all" value="1" type="checkbox"></th>
<th><%= t("projects.table.status") %></th>
<th><%= t("projects.table.name") %></th>
<th><%= t("projects.table.start") %></th>
<th><%= t("projects.table.visibility") %></th>
<th><%= t("projects.table.users") %></th>
<th><%= t("projects.table.experiments") %></th>
<th><%= t("projects.table.tasks") %></th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<%= stylesheet_link_tag 'datatables' %>

View file

@ -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
'<i class="fas fa-archive"></i>' +
I18n.t('projects.index.archived')
else
'<i class="fas fa-arrow-alt-circle-right"></i>' +
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'
'<i class="fas fa-eye-slash"></i>' +
I18n.t('projects.index.hidden')
else
'<i class="fas fa-eye"></i>' +
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
'<i class="fas fa-archive"></i>' +
I18n.t('projects.index.archived')
else
'<i class="fas fa-arrow-alt-circle-right"></i>' +
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'
'<i class="fas fa-eye-slash"></i>' +
I18n.t('projects.index.hidden')
else
'<i class="fas fa-eye"></i>' +
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 }
)
end
end

View file

@ -38,11 +38,11 @@
<ul class="list-unstyled">
<li>
<div class="btn-group projects-view-mode-switch" data-toggle="buttons">
<a class="btn btn-toggle active" href="#projects-cards-view" data-toggle="tab">
<a class="btn btn-toggle active" href="#projects-cards-view" data-toggle="tab" data-mode="cards">
<input type="radio" name="projects-view-mode-selector" value="cards">
<i class="fas fa-th-large"></i>
</a>
<a class="btn btn-toggle" href="#projects-table-view" data-toggle="tab">
<a class="btn btn-toggle" href="#projects-table-view" data-toggle="tab" data-mode="table">
<input type="radio" name="projects-view-mode-selector" value="table">
<i class="fas fa-th-list"></i>
</a>

View file

@ -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 <strong>%{name}</strong> successfully created."
update:

View file

@ -193,7 +193,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'