From e729aa043e48c594cc7104a4c36185aff1551bb7 Mon Sep 17 00:00:00 2001 From: aignatov-bio Date: Fri, 6 Mar 2020 13:51:18 +0100 Subject: [PATCH] Add recent work widget --- .../javascripts/dashboard/current_tasks.js | 6 +- .../javascripts/dashboard/recent_work.js | 64 +++++++++ .../javascripts/sitewide/infinite_scroll.js | 6 +- .../stylesheets/dashboard/quick_start.scss | 2 +- .../stylesheets/dashboard/recent_work.scss | 74 +++++++++- .../shared_styles/elements/navigation.scss | 1 + .../dashboard/recent_works_controller.rb | 12 ++ app/services/dashboard/recent_work_service.rb | 135 ++++++++++++++++++ app/views/dashboards/_current_tasks.html.erb | 4 +- app/views/dashboards/_recent_work.html.erb | 15 ++ config/locales/dashboard/en.yml | 18 +++ config/routes.rb | 2 + 12 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/dashboard/recent_work.js create mode 100644 app/controllers/dashboard/recent_works_controller.rb create mode 100644 app/services/dashboard/recent_work_service.rb diff --git a/app/assets/javascripts/dashboard/current_tasks.js b/app/assets/javascripts/dashboard/current_tasks.js index 441cd06e8..553f53c8e 100644 --- a/app/assets/javascripts/dashboard/current_tasks.js +++ b/app/assets/javascripts/dashboard/current_tasks.js @@ -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); }); diff --git a/app/assets/javascripts/dashboard/recent_work.js b/app/assets/javascripts/dashboard/recent_work.js new file mode 100644 index 000000000..0106efa0c --- /dev/null +++ b/app/assets/javascripts/dashboard/recent_work.js @@ -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 = ` +
${item.name}
+
${I18n.t('dashboard.recent_work.subject_type.' + item.subject_type)}
+
${item.last_change}
+
`; + $(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(`
+
${I18n.t('dashboard.recent_work.no_results.title')}
+
${I18n.t('dashboard.recent_work.no_results.description')}
+
+ +
+
`); + } + } + }); + } + + + return { + init: () => { + if ($('.recent-work-widget').length) { + initNavbar(); + initRecentWork(); + } + } + }; +}()); + +$(document).on('turbolinks:load', function() { + DasboardRecentWorkWidget.init(); +}); diff --git a/app/assets/javascripts/sitewide/infinite_scroll.js b/app/assets/javascripts/sitewide/infinite_scroll.js index a062de0aa..0df4498b7 100644 --- a/app/assets/javascripts/sitewide/infinite_scroll.js +++ b/app/assets/javascripts/sitewide/infinite_scroll.js @@ -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')); } diff --git a/app/assets/stylesheets/dashboard/quick_start.scss b/app/assets/stylesheets/dashboard/quick_start.scss index 38b4f8dd5..b26c7a290 100644 --- a/app/assets/stylesheets/dashboard/quick_start.scss +++ b/app/assets/stylesheets/dashboard/quick_start.scss @@ -23,7 +23,7 @@ } -@media (max-width: 1250px) { +@media (max-width: 1300px) { .dashboard-container .quick-start-widget { grid-column: 1 / span 4; } diff --git a/app/assets/stylesheets/dashboard/recent_work.scss b/app/assets/stylesheets/dashboard/recent_work.scss index e855cd00b..9bbff7ed9 100644 --- a/app/assets/stylesheets/dashboard/recent_work.scss +++ b/app/assets/stylesheets/dashboard/recent_work.scss @@ -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; + } + } } } diff --git a/app/assets/stylesheets/shared_styles/elements/navigation.scss b/app/assets/stylesheets/shared_styles/elements/navigation.scss index bfab2dde3..bc2c5442a 100644 --- a/app/assets/stylesheets/shared_styles/elements/navigation.scss +++ b/app/assets/stylesheets/shared_styles/elements/navigation.scss @@ -6,6 +6,7 @@ @include font-small; align-items: center; color: $color-silver-chalice; + cursor: pointer; display: flex; height: 100%; padding: 0 16px; diff --git a/app/controllers/dashboard/recent_works_controller.rb b/app/controllers/dashboard/recent_works_controller.rb new file mode 100644 index 000000000..41e91ee26 --- /dev/null +++ b/app/controllers/dashboard/recent_works_controller.rb @@ -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 diff --git a/app/services/dashboard/recent_work_service.rb b/app/services/dashboard/recent_work_service.rb new file mode 100644 index 000000000..50bf5464e --- /dev/null +++ b/app/services/dashboard/recent_work_service.rb @@ -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 diff --git a/app/views/dashboards/_current_tasks.html.erb b/app/views/dashboards/_current_tasks.html.erb index f3a6165b2..530abc381 100644 --- a/app/views/dashboards/_current_tasks.html.erb +++ b/app/views/dashboards/_current_tasks.html.erb @@ -53,8 +53,8 @@
- <%= t("dashboard.current_tasks.navbar.assigned") %> - <%= t("dashboard.current_tasks.navbar.all") %> + <%= t("dashboard.current_tasks.navbar.assigned") %> + <%= t("dashboard.current_tasks.navbar.all") %>
diff --git a/app/views/dashboards/_recent_work.html.erb b/app/views/dashboards/_recent_work.html.erb index 0a3a1ed48..1c48bc9d6 100644 --- a/app/views/dashboards/_recent_work.html.erb +++ b/app/views/dashboards/_recent_work.html.erb @@ -1,2 +1,17 @@
+
+
+ <%= t('dashboard.recent_work.title') %> +
+
+ <%= t('dashboard.recent_work.modes.all') %> + <%= t('dashboard.recent_work.modes.projects') %> + <%= t('dashboard.recent_work.modes.repositories') %> + <%= t('dashboard.recent_work.modes.protocols') %> + <%= t('dashboard.recent_work.modes.reports') %> +
+
+
+
+
diff --git a/config/locales/dashboard/en.yml b/config/locales/dashboard/en.yml index d15749e15..8b79e52be 100644 --- a/config/locales/dashboard/en.yml +++ b/config/locales/dashboard/en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index 92b2f05eb..75194f557 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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