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