mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-26 09:42:46 +08:00
Dashboard current tasks visual layout and backend
This commit is contained in:
parent
3b679bc53d
commit
9bd2e8bec4
9 changed files with 358 additions and 113 deletions
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue