Add recent work widget

This commit is contained in:
aignatov-bio 2020-03-06 13:51:18 +01:00
parent d940c0a122
commit e729aa043e
12 changed files with 330 additions and 9 deletions

View file

@ -154,10 +154,8 @@ var DasboardCurrentTasksWidget = (function() {
}
function initNavbar() {
$('.navbar-assigned, .navbar-all').on('click', function(e) {
e.stopPropagation();
e.preventDefault();
$('.current-tasks-navbar').find('a').removeClass('active');
$('.current-tasks-navbar .navbar-link').on('click', function() {
$(this).parent().find('.navbar-link').removeClass('active');
$(this).addClass('active');
loadCurrentTasksList(true);
});

View file

@ -0,0 +1,64 @@
/* global I18n PerfectSb InfiniteScroll */
/* eslint-disable no-param-reassign */
var DasboardRecentWorkWidget = (function() {
function initNavbar() {
$('.recent-work-navbar .navbar-link').on('click', function() {
$(this).parent().find('.navbar-link').removeClass('active');
$(this).addClass('active');
$('.recent-work-container').empty();
InfiniteScroll.resetScroll('.recent-work-container');
PerfectSb().update_all();
});
}
function renderRecentWorkItem(json, container) {
$.each(json.data, (i, item) => {
var recentWorkItem = `<a href="${item.url}" class="recent-work-item">
<div class="object-name">${item.name}</div>
<div class="object-type">${I18n.t('dashboard.recent_work.subject_type.' + item.subject_type)}</div>
<div class="object-changed">${item.last_change}</div>
</a>`;
$(container).append(recentWorkItem);
});
}
function initRecentWork() {
InfiniteScroll.init('.recent-work-container', {
url: $('.recent-work-container').data('url'),
loadFirstPage: true,
customResponse: (json, container) => {
renderRecentWorkItem(json, container);
},
customParams: (params) => {
params.mode = $('.recent-work-navbar .active').data('mode');
return params;
},
afterAction: (json, container) => {
if (json.data.length === 0) {
$(container).append(` <div class="no-results">
<div class="no-results-title">${I18n.t('dashboard.recent_work.no_results.title')}</div>
<div class="no-results-description">${I18n.t('dashboard.recent_work.no_results.description')}</div>
<div class="no-results-arrow">
<i class="fas fa-angle-double-left"></i>
</div>
</div>`);
}
}
});
}
return {
init: () => {
if ($('.recent-work-widget').length) {
initNavbar();
initRecentWork();
}
}
};
}());
$(document).on('turbolinks:load', function() {
DasboardRecentWorkWidget.init();
});

View file

@ -30,6 +30,10 @@ var InfiniteScroll = (function() {
}
$container.removeClass('loading');
if ($container.data('config').afterAction) {
$container.data('config').afterAction(result, $container);
}
if (scrollNotVisible($container)) {
loadData($container, $container.data('next-page'));
}
@ -59,7 +63,7 @@ var InfiniteScroll = (function() {
initScroll(object, config);
},
resetScroll: (object) => {
$(object).data('next-page', 2).removeClass('last-page');
$(object).data('next-page', $(object).data('config').loadFirstPage ? 1 : 2).removeClass('last-page');
if (scrollNotVisible($(object))) {
loadData($(object), $(object).data('next-page'));
}

View file

@ -23,7 +23,7 @@
}
@media (max-width: 1250px) {
@media (max-width: 1300px) {
.dashboard-container .quick-start-widget {
grid-column: 1 / span 4;
}

View file

@ -1,6 +1,72 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
.dashboard-container .recent-work-widget {
grid-column: 3 / span 7;
grid-row: 7 / span 6;
.widget-title {
flex-grow: 1;
}
.recent-work-container {
height: 100%;
padding: 0 8px;
position: relative;
.recent-work-item {
color: $color-volcano;
cursor: pointer;
display: flex;
line-height: 28px;
padding: 0 8px;
text-decoration: none;
.object-name {
flex-grow: 1;
font-weight: bold;
overflow: hidden;
padding-right: 15px;
text-overflow: ellipsis;
white-space: nowrap;
}
.object-type {
@include font-small;
color: $color-silver-chalice;
flex-basis: 120px;
flex-shrink: 0;
}
.object-changed {
@include font-small;
flex-basis: 160px;
flex-shrink: 0;
}
&:hover {
background: $color-concrete;
}
}
.no-results {
color: $color-alto;
padding: 24px;
.no-results-title {
@include font-h1;
}
.no-results-description {
@include font-main;
}
.no-results-arrow {
font-size: 32px;
padding-top: 50px;
}
}
}
}
@media (max-width: 1700px) {
@ -10,7 +76,7 @@
}
@media (max-width: 1250px) {
@media (max-width: 1300px) {
.dashboard-container .recent-work-widget {
grid-column: 5 / span 8;
}
@ -20,5 +86,11 @@
.dashboard-container .recent-work-widget {
grid-column: 1 / span 12;
grid-row: 9 / span 4;
.no-results {
.no-results-arrow {
display: none;
}
}
}
}

View file

@ -6,6 +6,7 @@
@include font-small;
align-items: center;
color: $color-silver-chalice;
cursor: pointer;
display: flex;
height: 100%;
padding: 0 16px;

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Dashboard
class RecentWorksController < ApplicationController
def show
activities = Dashboard::RecentWorkService.new(current_user, current_team, params[:mode]).call
page = (params[:page] || 1).to_i
activities = Kaminari.paginate_array(activities).page(page).per(Constants::INFINITE_SCROLL_LIMIT)
render json: { data: activities, next_page: activities.next_page }
end
end
end

View file

@ -0,0 +1,135 @@
# frozen_string_literal: true
module Dashboard
class RecentWorkService
include InputSanitizeHelper
include Rails.application.routes.url_helpers
def initialize(user, team, mode)
@user = user
@team = team
@mode = mode
end
def call
visible_projects = Project.viewable_by_user(@user, @team)
visible_by_team = Activity.where(project: nil, team_id: @team.id)
.where('created_at > ?', (DateTime.now - 1.month))
.select('MAX(created_at) as last_change,
percentile_disc(0) WITHIN GROUP (ORDER BY values) as values,
subject_id,
subject_type')
.group(:subject_id, :subject_type)
.order(last_change: :desc)
visible_by_projects = Activity.where(project_id: visible_projects.pluck(:id))
.where('created_at > ?', (DateTime.now - 1.month))
.select('MAX(created_at) as last_change,
percentile_disc(0) WITHIN GROUP (ORDER BY values) as values,
subject_id,
subject_type')
.group(:subject_id, :subject_type)
.order(last_change: :desc)
query = Activity.from("((#{visible_by_team.to_sql}) UNION ALL (#{visible_by_projects.to_sql})) AS activities")
# Join subjects
if %w(all projects).include? @mode
query = query.joins("
LEFT JOIN projects ON
subject_type = 'Project'
AND subject_id = projects.id
AND projects.archived = 'false'
LEFT JOIN experiments ON
subject_type = 'Experiment'
AND subject_id = experiments.id
AND experiments.archived = 'false'
LEFT JOIN my_modules ON
subject_type = 'MyModule'
AND subject_id = my_modules.id
AND my_modules.archived = 'false'
LEFT JOIN results ON
subject_type = 'Result'
AND subject_id = results.id
LEFT JOIN my_modules my_modules_result ON
my_modules_result.id = results.my_module_id
AND my_modules_result.archived = 'false'
LEFT JOIN my_modules my_modules_protocol ON
subject_type = 'Protocol'
AND (values #>> '{message_items, my_module, id}') :: BIGINT = my_modules_protocol.id
AND my_modules_protocol.archived = 'false'
").select('
projects.name as project_name,
experiments.name as experiment_name,
my_modules.name as my_module_name,
my_modules_protocol.name as my_module_protocol_name,
my_modules_protocol.id as my_module_protocol_id,
my_modules_result.name as my_module_result_name,
my_modules_result.id as my_module_result_id
')
end
if %w(all protocols).include? @mode
query = query.joins("LEFT JOIN protocols ON subject_type = 'Protocol'
AND subject_id = protocols.id AND protocols.my_module_id IS NULL
AND protocols.protocol_type != 4")
.select('protocols.name as protocol_name')
end
if %w(all repositories).include? @mode
query = query.joins("LEFT JOIN repositories ON subject_type = 'Repository' AND subject_id = repositories.id")
.select('repositories.name as repository_name')
end
if %w(all reports).include? @mode
query = query.joins("LEFT JOIN reports ON subject_type = 'Report' AND subject_id = reports.id")
.select('reports.name as report_name, reports.project_id as report_project_id')
end
query = query.select(:subject_id, :subject_type, :last_change).order(last_change: :desc)
activities = query.as_json.map do |activity|
activity.deep_symbolize_keys!
object_name = nil
activity.delete_if { |_k, v| v.nil? }
if activity[:my_module_protocol_name]
activity[:subject_type] = 'MyModule'
activity[:subject_id] = activity.delete :my_module_protocol_id
end
if activity[:my_module_result_name]
activity[:subject_type] = 'MyModule'
activity[:subject_id] = activity.delete :my_module_result_id
end
activity.each do |key, _value|
object_name = activity.delete key if key.to_s.include? 'name'
end
activity[:last_change] = I18n.l(DateTime.parse(activity[:last_change]), format: :full_with_comma)
activity[:name] = escape_input(object_name)
activity[:url] = generate_url(activity)
activity unless activity[:name].empty?
end.compact
activities.uniq! { |activity| [activity[:subject_type], activity[:subject_id]].join(':') }
activities
end
private
def generate_url(activity)
case activity[:subject_type]
when 'MyModule'
protocols_my_module_path(activity[:subject_id])
when 'Experiment'
canvas_experiment_path(activity[:subject_id])
when 'Project'
project_path(activity[:subject_id])
when 'Protocol'
edit_protocol_path(activity[:subject_id])
when 'Repository'
repository_path(activity[:subject_id])
when 'Report'
edit_project_report_path(activity[:report_project_id], activity[:subject_id]) if activity[:report_project_id]
end
end
end
end

View file

@ -53,8 +53,8 @@
</div>
<div class="sci-secondary-navbar current-tasks-navbar">
<a class="navbar-link navbar-assigned active" href="" data-remote="true" data-mode="assigned"><%= t("dashboard.current_tasks.navbar.assigned") %></a>
<a class="navbar-link navbar-all" href="" data-remote="true" data-mode="team"><%= t("dashboard.current_tasks.navbar.all") %></a>
<span class="navbar-link active" data-mode="assigned"><%= t("dashboard.current_tasks.navbar.assigned") %></span>
<span class="navbar-link" data-mode="team"><%= t("dashboard.current_tasks.navbar.all") %></span>
</div>
</div>

View file

@ -1,2 +1,17 @@
<div class="recent-work-widget basic-widget">
<div class="widget-header">
<div class="widget-title">
<%= t('dashboard.recent_work.title') %>
</div>
<div class="sci-secondary-navbar recent-work-navbar">
<span class="navbar-link active" data-mode="all"><%= t('dashboard.recent_work.modes.all') %></span>
<span class="navbar-link" data-mode="projects"><%= t('dashboard.recent_work.modes.projects') %></span>
<span class="navbar-link" data-mode="repositories"><%= t('dashboard.recent_work.modes.repositories') %></span>
<span class="navbar-link" data-mode="protocols"><%= t('dashboard.recent_work.modes.protocols') %></span>
<span class="navbar-link" data-mode="reports"><%= t('dashboard.recent_work.modes.reports') %></span>
</div>
</div>
<div class="widget-body">
<div class="recent-work-container perfect-scrollbar" data-url="<%= dashboard_recent_works_path %>"></div>
</div>
</div>

View file

@ -60,3 +60,21 @@ en:
experiment_disabled_placeholder: "Select Project to enable Experiments"
filter_create_new: "Create"
create_task: "Create Task"
recent_work:
title: "Recent work"
no_results:
title: "You have not worked on anything recently."
description: "Use the Quick start section to begin creating."
modes:
all: "All"
projects: "PROJECTS"
repositories: "INVENTORIES"
protocols: "PROTOCOLS"
reports: "REPORTS"
subject_type:
Project: "Project"
Experiment: "Experiment"
MyModule: "Task"
Repository: "Inventory"
Protocol: "Protocol"
Report: "Report"

View file

@ -255,6 +255,8 @@ Rails.application.routes.draw do
resource :calendar, module: 'dashboard', only: [:show] do
get :day
end
resource :recent_works, module: 'dashboard', only: [:show]
end
resources :projects, except: [:new, :destroy] do