Dashboard current tasks visual layout and backend

This commit is contained in:
Mojca Lorber 2020-02-21 10:41:42 +01:00
parent 3b679bc53d
commit 9bd2e8bec4
9 changed files with 358 additions and 113 deletions

View file

@ -67,7 +67,7 @@ gem 'down', '~> 5.0'
gem 'faker' # Generate fake data
gem 'fastimage' # Light gem to get image resolution
gem 'httparty', '~> 0.13.1'
gem 'i18n-js', '~> 3.0' # Localization in javascript files
gem 'i18n-js', '~> 3.6' # Localization in javascript files
gem 'jbuilder' # JSON structures via a Builder-style DSL
gem 'logging', '~> 2.0.0'
gem 'nested_form_fields'

View file

@ -1,16 +1,65 @@
/* global dropdownSelector */
/* global dropdownSelector I18n animateSpinner */
/* eslint-disable no-param-reassign */
var DasboardCurrentTasksWidget = (function() {
function initFilters() {
var sortFilter = '.curent-tasks-filters .sort-filter';
var viewFilter = '.curent-tasks-filters .view-filter';
var projectFilter = '.curent-tasks-filters .project-filter';
var experimentFilter = '.curent-tasks-filters .experiment-filter';
var sortFilter = '.curent-tasks-filters .sort-filter';
var viewFilter = '.curent-tasks-filters .view-filter';
var projectFilter = '.curent-tasks-filters .project-filter';
var experimentFilter = '.curent-tasks-filters .experiment-filter';
$('.curent-tasks-filters .clear-button').click(() => {
function loadCurrentTasksList() {
var $currentTasksList = $('.current-tasks-list');
var params = {
project_id: dropdownSelector.getValues(projectFilter),
experiment_id: dropdownSelector.getValues(experimentFilter),
sort: dropdownSelector.getValues(sortFilter),
view: dropdownSelector.getValues(viewFilter),
mode: $('.current-tasks-navbar .active').data('mode')
};
animateSpinner($currentTasksList, true);
$.get($currentTasksList.attr('data-tasks-list-url'), params, function(data) {
// Clear the list
$currentTasksList.find('.current-task-item').remove();
$.each(data.tasks_list, (i, task) => {
var currentTaskItem;
var stepsPercentage = (task.steps_state === 0) ? '' : task.steps_state.percentage + '%';
var stateText;
var dueDate = (task.due_date !== null) ? '<i class="fas fa-calendar-day"></i>'
+ I18n.t('dashboard.current_tasks.due_date', { date: task.due_date }) : '';
var overdue = (task.overdue) ? 'overdue' : '';
if (task.state === 'completed') {
stateText = I18n.t('dashboard.current_tasks.progress_bar.completed');
} else {
stateText = I18n.t('dashboard.current_tasks.progress_bar.in_progress');
if (task.overdue) { stateText = I18n.t('dashboard.current_tasks.progress_bar.overdue'); }
if (task.steps_state !== 0) {
stateText += I18n.t('dashboard.current_tasks.progress_bar.completed_steps',
{ steps: task.steps_state.completed_steps, total_steps: task.steps_state.all_steps });
}
}
currentTaskItem = `<a class="current-task-item" href="${task.link}">
<div class="current-task-breadcrumbs">${task.project}/${task.experiment}</div>
<div class="item-row">
<div class="task-name">${task.name}</div>
<div class="task-due-date ${overdue}">${dueDate}</div>
<div class="task-progress-container ${task.state} ${overdue}">
<div class="task-progress" style="padding-left: ${stepsPercentage}"></div>
<div class="task-progress-label">${stateText}</div>
</div>
</div>
</a>`;
$currentTasksList.append(currentTaskItem);
});
animateSpinner($currentTasksList, false);
});
}
function initFilters() {
$('.curent-tasks-filters .clear-button').click((e) => {
e.stopPropagation();
e.preventDefault();
dropdownSelector.selectValue(sortFilter, 'date_asc');
dropdownSelector.selectValue(viewFilter, 'active');
dropdownSelector.selectValue(viewFilter, 'uncompleted');
dropdownSelector.clearData(projectFilter);
dropdownSelector.clearData(experimentFilter);
});
@ -37,7 +86,7 @@ var DasboardCurrentTasksWidget = (function() {
emptyOptionAjax: true,
selectAppearance: 'simple',
ajaxParams: (params) => {
params.mode = 'team';
params.mode = $('.current-tasks-navbar .active').data('mode');
return params;
},
onChange: () => {
@ -57,7 +106,7 @@ var DasboardCurrentTasksWidget = (function() {
emptyOptionAjax: true,
selectAppearance: 'simple',
ajaxParams: (params) => {
params.mode = 'team';
params.mode = $('.current-tasks-navbar .active').data('mode');
params.project_id = dropdownSelector.getValues(projectFilter);
return params;
}
@ -72,12 +121,29 @@ var DasboardCurrentTasksWidget = (function() {
dropdownSelector.closeDropdown(projectFilter);
dropdownSelector.closeDropdown(experimentFilter);
});
$('.curent-tasks-filters .apply-filters').click((e) => {
$('.curent-tasks-filters').dropdown('toggle');
e.stopPropagation();
e.preventDefault();
loadCurrentTasksList();
});
}
function initNavbar() {
$('.navbar-assigned, .navbar-all').on('click', function() {
$('.current-tasks-navbar').find('a').removeClass('active');
$(this).addClass('active');
loadCurrentTasksList();
});
}
return {
init: () => {
if ($('.current-tasks-widget').length) {
initNavbar();
initFilters();
loadCurrentTasksList();
}
}
};

View file

@ -5,19 +5,117 @@
grid-column: 1 / span 9;
grid-row: 1 / span 6;
.title {
flex-shrink: 0;
}
.actions-container {
display: flex;
flex-grow: 1;
}
.filter-container {
height: 36px;
margin-right: 4px;
width: 36px;
.curent-tasks-filters {
padding: 0;
width: 230px;
.header {
align-items: center;
border-bottom: $border-default;
display: flex;
height: 44px;
margin-bottom: 16px;
padding: 0 5px 0 16px;
.title {
@include font-h2;
flex-grow: 1;
user-select: none;
}
}
.select-block {
display: inline-block;
padding: 0 16px 16px;
position: relative;
width: 100%;
label {
@include font-small;
display: inline-block;
font-weight: bold;
margin-bottom: 5px;
user-select: none;
}
}
.footer {
align-items: center;
border-top: $border-default;
display: flex;
height: 68px;
justify-content: center;
position: relative;
width: 100%;
}
}
}
.current-tasks-list {
display: flex;
flex-direction: column;
height: 293px;
overflow-y: auto;
padding: 0 16px;
.current-task-item {
border-bottom: $border-tertiary;
color: $color-volcano;
padding: 6px 0;
text-decoration: none;
.current-task-breadcrumbs {
@include font-small;
color: $color-silver-chalice;
line-height: 14px;
}
.item-row{
display: flex;
.task-name {
flex-grow: 1;
font-size: $font-size-base; //not in styleguide
font-weight: bold;
}
.task-due-date {
flex-basis: 260px;
font-size: 14px;
.fas {
padding: 4px;
}
&.overdue {
color: $brand-danger;
}
}
}
}
}
.task-progress-container {
max-width: 300px;
max-width: 250px;
min-width: 150px;
position: relative;
width: 100%;
// Example
//
//<div class="task-progress-container">
// <div class="task-progress" style="padding-left: 45%"></div>
// <div class="task-progress-label">t('...')</div>
//</div>
&::after {
@include font-small;
@include font-awesome;
@ -106,57 +204,6 @@
}
}
}
.filter-container {
height: 36px;
width: 36px;
.curent-tasks-filters {
padding: 0;
width: 230px;
.header {
align-items: center;
border-bottom: $border-default;
display: flex;
height: 44px;
margin-bottom: 16px;
padding: 0 5px 0 16px;
.title {
@include font-h2;
flex-grow: 1;
user-select: none;
}
}
.select-block {
display: inline-block;
padding: 0 16px 16px;
position: relative;
width: 100%;
label {
@include font-small;
display: inline-block;
font-weight: bold;
margin-bottom: 5px;
user-select: none;
}
}
.footer {
align-items: center;
border-top: $border-default;
display: flex;
height: 68px;
justify-content: center;
position: relative;
width: 100%;
}
}
}
}
@media (max-width: 1000px) {

View file

@ -14,6 +14,19 @@
border-radius: $border-radius-modal;
box-shadow: $flyout-shadow;
position: relative;
.widget-header {
align-items: center;
border-bottom: $border-tertiary;
display: flex;
height: 44px;
h2 {
line-height: 18px;
margin: 10px 16px;
padding: 4px 0;
}
}
}
}

View file

@ -0,0 +1,35 @@
.sci-secondary-navbar {
display: flex;
height: 100%;
a {
@include font-small;
align-items: center;
color: $color-silver-chalice;
display: flex;
height: 100%;
padding: 0 16px;
position: relative;
text-decoration: none;
text-transform: uppercase;
&:hover {
color: $color-volcano;
}
&.active {
color: $color-volcano;
font-weight: bold;
&::before {
background: $brand-primary;
bottom: 0;
content: "";
height: 4px;
left: 0;
position: absolute;
width: 100%;
}
}
}
}

View file

@ -4,7 +4,54 @@ module Dashboard
class CurrentTasksController < ApplicationController
include InputSanitizeHelper
def show; end
def show
if params[:project_id]
if params[:experiment_id]
tasks = MyModule.active.joins(:experiment).where('experiments.id': params[:experiment_id])
else
tasks = MyModule.active.joins(:experiment).where('experiments.project_id': params[:project_id])
end
else
tasks = MyModule.active.joins(experiment: :project).where('projects.team_id': current_team)
end
if params[:mode] == 'assigned'
tasks = tasks.left_outer_joins(:user_my_modules).where('user_my_modules.user_id': current_user.id)
end
tasks = tasks.where('my_modules.state': params[:view])
case params[:sort]
when 'date_desc'
tasks = tasks.order('my_modules.due_date': :desc)
when 'date_asc'
tasks = tasks.order('my_modules.due_date': :asc)
when 'atoz'
tasks = tasks.order('my_modules.name': :asc)
when 'ztoa'
tasks = tasks.order('my_modules.name': :desc)
else
tasks
end
respond_to do |format|
format.json do
render json: {
tasks_list: tasks.map do |task|
due_date = I18n.l(task.due_date, format: :full_with_comma) if task.due_date.present?
{ id: task.id,
link: protocols_my_module_path(task.id),
experiment: task.experiment.name,
project: task.experiment.project.name,
name: escape_input(task.name),
due_date: due_date,
overdue: task.is_overdue?,
state: task.state,
steps_state: task.completed_steps_percentage }
end,
status: :ok
}
end
end
end
def project_filter
projects = current_team.projects.search(current_user, false, params[:query], 1, current_team).select(:id, :name)

View file

@ -492,6 +492,18 @@ class MyModule < ApplicationRecord
state == 'completed'
end
def completed_steps_percentage
if protocol && protocol.steps.count.positive?
{
completed_steps: protocol.steps.count(&:completed),
all_steps: protocol.steps.count,
percentage: (protocol.steps.count(&:completed).fdiv(protocol.steps.count) * 100).round
}
else
0
end
end
# Check if my_module is ready to become completed
def check_completness_status
if protocol && protocol.steps.count > 0

View file

@ -1,44 +1,63 @@
<div class="current-tasks-widget basic-widget">
<div class="filter-container dropdown">
<div class="btn btn-light icon-btn filter-button" data-toggle="dropdown"><i class="fas fa-filter"></i></div>
<div class="dropdown-menu curent-tasks-filters" role="menu">
<div class="header">
<div class="title"><%= t("dashboard.current_tasks.filter.title") %></div>
<div class="btn btn-light clear-button"><i class="fas fa-times-circle"></i><%= t("dashboard.current_tasks.filter.clear") %></div>
<div class="widget-header">
<h2 class="title"><%= t("dashboard.current_tasks.title") %></h2>
<div class="actions-container">
<div class="filter-container dropdown">
<div class="btn btn-light icon-btn filter-button" data-toggle="dropdown"><i class="fas fa-filter"></i></div>
<div class="dropdown-menu curent-tasks-filters" role="menu">
<div class="header">
<div class="title"><%= t("dashboard.current_tasks.filter.title") %></div>
<div class="btn btn-light clear-button"><i class="fas fa-times-circle"></i><%= t("dashboard.current_tasks.filter.clear") %></div>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.sort") %></label>
<select class="sort-filter">
<option value="date_asc" ><%= t("dashboard.current_tasks.filter.date_asc") %></option>
<option value="date_desc" ><%= t("dashboard.current_tasks.filter.date_desc") %></option>
<option value="atoz" ><%= t("dashboard.current_tasks.filter.atoz") %></option>
<option value="ztoa" ><%= t("dashboard.current_tasks.filter.ztoa") %></option>
</select>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.display") %></label>
<select class="view-filter">
<option value="uncompleted" ><%= t("dashboard.current_tasks.filter.uncompleted_tasks") %></option>
<option value="completed" ><%= t("dashboard.current_tasks.filter.completed_tasks") %></option>
</select>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.project") %></label>
<select class="project-filter"
data-ajax-url="<%= project_filter_dashboard_current_tasks_path %>"
data-placeholder="<%= t("dashboard.current_tasks.filter.select_project") %>"></select>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.experiment") %></label>
<select class="experiment-filter"
data-ajax-url="<%= experiment_filter_dashboard_current_tasks_path %>"
data-disable-on-load="true"
data-disable-placeholder="<%= t("dashboard.current_tasks.filter.select_experiment") %>"
data-placeholder="<%= t("dashboard.current_tasks.filter.select_experiment") %>"></select>
</div>
<div class="footer">
<div class="btn btn-primary apply-filters"><%= t("dashboard.current_tasks.filter.apply") %></div>
</div>
</div>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.sort") %></label>
<select class="sort-filter">
<option value="date_asc" ><%= t("dashboard.current_tasks.filter.date_asc") %></option>
<option value="date_desc" ><%= t("dashboard.current_tasks.filter.date_desc") %></option>
<option value="atoz" ><%= t("dashboard.current_tasks.filter.atoz") %></option>
<option value="ztoa" ><%= t("dashboard.current_tasks.filter.ztoa") %></option>
</select>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.display") %></label>
<select class="view-filter">
<option value="active" ><%= t("dashboard.current_tasks.filter.active_tasks") %></option>
<option value="completed" ><%= t("dashboard.current_tasks.filter.locked_tasks") %></option>
</select>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.project") %></label>
<select class="project-filter"
data-ajax-url="<%= project_filter_dashboard_current_tasks_path %>"
data-placeholder="<%= t("dashboard.current_tasks.filter.select_project") %>"></select>
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.experiment") %></label>
<select class="experiment-filter"
data-ajax-url="<%= experiment_filter_dashboard_current_tasks_path %>"
data-disable-on-load="true"
data-disable-placeholder="<%= t("dashboard.current_tasks.filter.select_experiment") %>"
data-placeholder="<%= t("dashboard.current_tasks.filter.select_experiment") %>"></select>
</div>
<div class="footer">
<div class="btn btn-primary"><%= t("dashboard.current_tasks.filter.apply") %></div>
<div class="search-container">
<div class="btn btn-light icon-btn search-button"><i class="fas fa-search"></i></div>
</div>
</div>
<div class="sci-secondary-navbar current-tasks-navbar">
<a class="navbar-assigned active" href="" data-remote="true" data-mode="assigned"><%= t("dashboard.current_tasks.navbar.assigned") %></a>
<a class="navbar-all" href="" data-remote="true" data-mode="team"><%= t("dashboard.current_tasks.navbar.all") %></a>
</div>
</div>
<div class="widger-body">
<div class="current-tasks-list"
data-tasks-list-url="<%= dashboard_current_tasks_path %>">
</div>
</div>
</div>

View file

@ -1,6 +1,10 @@
en:
dashboard:
current_tasks:
title: "Current tasks"
navbar:
assigned: "Assigned to me"
all: "All"
filter:
title: "Filters"
clear: "Clear"
@ -10,15 +14,17 @@ en:
atoz: "From A to Z"
ztoa: "From Z to A"
display: "Display"
active_tasks: "Only active tasks"
locked_tasks: "Only completed and locked tasks"
uncompleted_tasks: "Only in progress tasks"
completed_tasks: "Only completed tasks"
project: "Project"
select_project: "Select Project"
experiment: "Experiment"
select_experiment: "Select Experiment"
apply: "Apply"
due_date: "Due date: %{date}"
progress_bar:
in_progress: "In progress: %{steps} / %{total_steps}"
overdue: "Overdue: %{steps} / %{total_steps}"
in_progress: "In progress"
overdue: "Overdue"
completed_steps: ": %{steps} / %{total_steps}"
completed: "Completed"
locked: "Locked"