mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-12-10 06:06:24 +08:00
Merge pull request #2424 from aignatov-bio/ai-sci-add-calendar-widget
Add calendar widget [SCI-4373][SCI-4371][SCI-4372]
This commit is contained in:
commit
df51140d2d
16 changed files with 381 additions and 3 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'
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ GEM
|
|||
multi_xml (>= 0.5.2)
|
||||
i18n (1.6.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.3.0)
|
||||
i18n-js (3.6.0)
|
||||
i18n (>= 0.6.6)
|
||||
image_processing (1.9.3)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
|
|
@ -631,7 +631,7 @@ DEPENDENCIES
|
|||
figaro
|
||||
hammerjs-rails
|
||||
httparty (~> 0.13.1)
|
||||
i18n-js (~> 3.0)
|
||||
i18n-js (~> 3.6)
|
||||
image_processing (~> 1.2)
|
||||
jbuilder
|
||||
jquery-rails
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
//= require bootstrap-select
|
||||
//= require_directory ./repository_columns/columns_initializers
|
||||
//= require datatables
|
||||
//= require clndr.min
|
||||
//= require ajax-bootstrap-select.min
|
||||
//= require underscore
|
||||
//= require i18n.js
|
||||
|
|
|
|||
88
app/assets/javascripts/dashboard/calendar.js
Normal file
88
app/assets/javascripts/dashboard/calendar.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/* global I18n */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
var DasboardCalendarWidget = (function() {
|
||||
function calendarTemplate() {
|
||||
return `<script id="calendar-template" type="text/template">
|
||||
<div class="controls">
|
||||
<div class="clndr-previous-button">
|
||||
<div class="btn btn-light icon-btn"><i class="fas fa-angle-double-left"></i></div>
|
||||
</div>
|
||||
<div class="clndr-title"><%= month %> <%= year %></div>
|
||||
<div class="clndr-next-button">
|
||||
<div class="btn btn-light icon-btn"><i class="fas fa-angle-double-right"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="days-container">
|
||||
<% _.each(daysOfTheWeek, function(day) { %>
|
||||
<div class="day-header"><%= day %></div>
|
||||
<% }); %>
|
||||
<% _.each(days, function(day) { %>
|
||||
<% if (day.classes.includes('event')){ %>
|
||||
<div class="<%= day.classes %>" id="<%= day.id %>">
|
||||
<div class="event-day" data-toggle="dropdown"><%= day.day %></div>
|
||||
<div class="dropdown-menu events-container dropdown-menu-right" role="menu">
|
||||
<div class="title">${I18n.t('dashboard.calendar.due_on')} <%= day.date.format(formatJS) %></div>
|
||||
<div class="tasks"></div>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="<%= day.classes %>" id="<%= day.id %>"><%= day.day %></div>
|
||||
<% } %>
|
||||
<% }); %>
|
||||
</div>
|
||||
</script>`;
|
||||
}
|
||||
|
||||
function getMonthEventsList(date, clndrInstance) {
|
||||
var getUrl = $('.dashboard-calendar').data('month-events-url');
|
||||
$.get(getUrl, { date: date }, function(result) {
|
||||
clndrInstance.setEvents(result.events);
|
||||
});
|
||||
}
|
||||
|
||||
function initCalendar() {
|
||||
var dayOfWeek = [
|
||||
I18n.t('dashboard.calendar.dow.su'),
|
||||
I18n.t('dashboard.calendar.dow.mo'),
|
||||
I18n.t('dashboard.calendar.dow.tu'),
|
||||
I18n.t('dashboard.calendar.dow.we'),
|
||||
I18n.t('dashboard.calendar.dow.th'),
|
||||
I18n.t('dashboard.calendar.dow.fr'),
|
||||
I18n.t('dashboard.calendar.dow.sa')
|
||||
];
|
||||
var clndrInstance = $('.dashboard-calendar').clndr({
|
||||
template: $(calendarTemplate()).html(),
|
||||
daysOfTheWeek: dayOfWeek,
|
||||
forceSixRows: true,
|
||||
clickEvents: {
|
||||
click: function(target) {
|
||||
var getDayUrl = $('.dashboard-calendar').data('day-events-url');
|
||||
if ($(target.element).hasClass('event')) {
|
||||
$.get(getDayUrl, { date: target.date._i }, function(result) {
|
||||
$(target.element).find('.tasks').html(result.html);
|
||||
});
|
||||
}
|
||||
},
|
||||
onMonthChange: function(month) {
|
||||
getMonthEventsList(month._d, clndrInstance);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
getMonthEventsList((new Date()), clndrInstance);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
init: () => {
|
||||
if ($('.current-tasks-widget').length) {
|
||||
initCalendar();
|
||||
}
|
||||
}
|
||||
};
|
||||
}());
|
||||
|
||||
$(document).on('turbolinks:load', function() {
|
||||
DasboardCalendarWidget.init();
|
||||
});
|
||||
|
|
@ -1,11 +1,134 @@
|
|||
// scss-lint:disable SelectorDepth
|
||||
// scss-lint:disable NestingDepth
|
||||
|
||||
.dashboard-container .calendar-widget {
|
||||
grid-column: 10 / span 3;
|
||||
grid-row: 1 / span 6;
|
||||
|
||||
.dashboard-calendar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clndr {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.controls {
|
||||
border-bottom: $border-default;
|
||||
display: flex;
|
||||
flex-basis: 42px;
|
||||
padding: 3px;
|
||||
|
||||
.clndr-title {
|
||||
@include font-h3;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.days-container {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
grid-column-gap: 6px;
|
||||
grid-row-gap: 6px;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
justify-items: center;
|
||||
padding: 6px;
|
||||
|
||||
.day-header {
|
||||
@include font-button;
|
||||
color: $color-silver-chalice;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.day {
|
||||
@include font-button;
|
||||
align-items: center;
|
||||
animation-timing-function: $timing-function-sharp;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
transition: .3s;
|
||||
user-select: none;
|
||||
width: 32px;
|
||||
|
||||
&.adjacent-month {
|
||||
color: $color-alto;
|
||||
}
|
||||
|
||||
&.today {
|
||||
border: $border-primary;
|
||||
}
|
||||
|
||||
&.event {
|
||||
|
||||
.event-day {
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
|
||||
&:hover {
|
||||
background: $color-alto;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&::after {
|
||||
background: $brand-danger;
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
height: 4px;
|
||||
left: 14px;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.events-container {
|
||||
color: $color-black;
|
||||
padding: 8px;
|
||||
width: 280px;
|
||||
|
||||
.title {
|
||||
@include font-h3;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1250px) {
|
||||
.dashboard-container .calendar-widget {
|
||||
grid-column: 9 / span 4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.dashboard-container .calendar-widget {
|
||||
grid-column: 1 / span 6;
|
||||
grid-row: 5 / span 4;
|
||||
|
||||
.clndr {
|
||||
.events-container {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1250px) {
|
||||
.dashboard-container .current-tasks-widget {
|
||||
grid-column: 1 / span 8;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.dashboard-container .current-tasks-widget {
|
||||
grid-column: 1 / span 12;
|
||||
|
|
|
|||
55
app/assets/stylesheets/shared/my_modules_list_partial.scss
Normal file
55
app/assets/stylesheets/shared/my_modules_list_partial.scss
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// scss-lint:disable SelectorDepth
|
||||
// scss-lint:disable NestingDepth
|
||||
|
||||
.my-modules-list-partial {
|
||||
width: 100%;
|
||||
|
||||
.task-group:not(:first-child) {
|
||||
border-top: $border-tertiary;
|
||||
}
|
||||
|
||||
.header {
|
||||
@include font-small;
|
||||
align-items: center;
|
||||
color: $color-silver-chalice;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
|
||||
.project,
|
||||
.experiment {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.slash {
|
||||
flex-basis: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tasks {
|
||||
@include font-button;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.task {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
line-height: 25px;
|
||||
|
||||
.task-icon {
|
||||
margin-right: 9px;
|
||||
|
||||
path {
|
||||
fill: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.task-link {
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ $border-default: 1px solid $color-alto;
|
|||
$border-secondary: 1px solid $color-silver-chalice;
|
||||
$border-tertiary: 1px solid $color-concrete;
|
||||
|
||||
$border-primary: 1px solid $brand-primary;
|
||||
$border-focus: 1px solid $brand-focus;
|
||||
$border-success: 1px solid $brand-success;
|
||||
$border-danger: 1px solid $brand-danger;
|
||||
|
|
|
|||
29
app/controllers/dashboard/calendars_controller.rb
Normal file
29
app/controllers/dashboard/calendars_controller.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Dashboard
|
||||
class CalendarsController < ApplicationController
|
||||
include IconsHelper
|
||||
|
||||
def show
|
||||
date = DateTime.parse(params[:date])
|
||||
start_date = date.at_beginning_of_month.utc - 7.days
|
||||
end_date = date.at_end_of_month.utc + 14.days
|
||||
due_dates = current_user.my_modules.active.uncomplete
|
||||
.where('due_date > ? AND due_date < ?', start_date, end_date)
|
||||
.joins(:protocols).where('protocols.team_id = ?', current_team.id)
|
||||
.pluck(:due_date)
|
||||
render json: { events: due_dates.map { |i| { date: i } } }
|
||||
end
|
||||
|
||||
def day
|
||||
date = DateTime.parse(params[:date]).utc
|
||||
my_modules = current_user.my_modules.active.uncomplete
|
||||
.where('DATE(my_modules.due_date) = DATE(?)', date)
|
||||
.where('projects.team_id = ?', current_team.id)
|
||||
.my_modules_list_partial
|
||||
render json: {
|
||||
html: render_to_string(partial: 'shared/my_modules_list_partial.html.erb', locals: { task_groups: my_modules })
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -13,6 +13,8 @@ module IconsHelper
|
|||
when 'shared-read'
|
||||
title = "<title>#{t('repositories.icon_title.shared_read', team_name: team.name)}</title>"
|
||||
icon = '<path fill="#A0A0A0" fill-rule="evenodd" d="M6.6 8.922c-.688 0-1.34-.172-1.925-.515a3.478 3.478 0 0 1-1.41-1.41 3.758 3.758 0 0 1-.515-1.925c0-.687.172-1.306.516-1.925.343-.584.79-1.065 1.409-1.41A3.758 3.758 0 0 1 6.6 1.223a3.85 3.85 0 0 1 1.925.516 3.955 3.955 0 0 1 1.41 1.41 3.85 3.85 0 0 1 .515 1.924c0 .688-.172 1.34-.516 1.925-.343.619-.825 1.066-1.409 1.41a3.85 3.85 0 0 1-1.925.515zm2.647 1.1c.687 0 1.34.207 1.96.55.618.344 1.1.825 1.443 1.444.281.506.47 1.036.53 1.588a7.217 7.217 0 0 0-2.564 2.568 2.64 2.64 0 0 0-.218.45H1.65c-.481 0-.86-.137-1.169-.481-.343-.31-.481-.687-.481-1.169v-.997c0-.687.172-1.34.516-1.959.343-.619.825-1.1 1.443-1.444.62-.343 1.272-.55 1.994-.55h.275c.756.378 1.547.55 2.372.55a5.43 5.43 0 0 0 2.372-.55h.275zm11.43 3.546c.442.263.85.562 1.22.898.068-.18.103-.376.103-.594a3.85 3.85 0 0 0-.516-1.925 3.955 3.955 0 0 0-1.409-1.41 3.849 3.849 0 0 0-1.925-.515h-.137a4.156 4.156 0 0 1-1.513.275c-.481 0-.997-.069-1.512-.275h-.138c-.688 0-1.306.172-1.925.516.412.481.756 1.031.997 1.615.125.306.223.62.288.94a7.268 7.268 0 0 1 2.748-.527c1.326 0 2.577.34 3.704.994l.014.008zM16.5 8.922c-.928 0-1.719-.31-2.338-.962-.653-.619-.962-1.41-.962-2.338 0-.894.31-1.684.963-2.337a3.216 3.216 0 0 1 2.337-.963c.894 0 1.684.344 2.337.963.62.653.963 1.443.963 2.337 0 .928-.344 1.719-.963 2.338a3.193 3.193 0 0 1-2.337.962zm5.347 8.6a.932.932 0 0 0-.119-.407 5.658 5.658 0 0 0-1.986-1.969 5.473 5.473 0 0 0-2.784-.747 5.428 5.428 0 0 0-2.784.747 5.39 5.39 0 0 0-1.986 1.97.75.75 0 0 0-.119.407c0 .152.034.288.12.407a5.148 5.148 0 0 0 1.985 1.97c.85.509 1.766.746 2.784.746 1.002 0 1.936-.237 2.784-.747a5.39 5.39 0 0 0 1.986-1.969.818.818 0 0 0 .12-.407zm-3.734 2.004a2.303 2.303 0 0 1-1.155.305c-.424 0-.814-.101-1.154-.305a2.264 2.264 0 0 1-.849-.849 2.214 2.214 0 0 1-.305-1.154c0-.408.101-.798.305-1.155.204-.34.492-.628.849-.831.34-.204.73-.323 1.154-.323.408 0 .798.119 1.155.323.34.203.628.492.831.831.204.357.323.747.323 1.155 0 .424-.119.815-.323 1.154a2.346 2.346 0 0 1-.831.849zm.085-3.242c.339.34.526.764.526 1.239 0 .492-.187.916-.526 1.256-.34.34-.764.51-1.24.51-.492 0-.916-.17-1.256-.51-.34-.34-.51-.764-.51-1.256 0-.289.069-.56.205-.832 0 .204.067.39.203.526a.73.73 0 0 0 .527.204.691.691 0 0 0 .509-.204.745.745 0 0 0 .22-.526.706.706 0 0 0-.22-.51.706.706 0 0 0-.51-.22c.255-.136.527-.204.832-.204.476 0 .9.187 1.24.527z" clip-rule="evenodd"/>'
|
||||
when 'task-icon'
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="task-icon"><path d="M6.646 14V7.215H4V5h7.938v2.215H9.292V14h4.144c.26 0 .434-.009.542-.022.013-.107.022-.282.022-.542V2.564c0-.26-.009-.435-.022-.542A4.762 4.762 0 0 0 13.436 2H2.564c-.26 0-.435.009-.542.022A4.762 4.762 0 0 0 2 2.564v10.872c0 .26.009.434.022.542.107.013.282.022.542.022h4.082zM2.564 0h10.872c.892 0 1.215.093 1.54.267.327.174.583.43.757.756.174.326.267.65.267 1.54v10.873c0 .892-.093 1.215-.267 1.54-.174.327-.43.583-.756.757-.326.174-.65.267-1.54.267H2.563c-.892 0-1.215-.093-1.54-.267a1.817 1.817 0 0 1-.757-.756C.093 14.65 0 14.327 0 13.437V2.563c0-.892.093-1.215.267-1.54.174-.327.43-.583.756-.757C1.35.093 1.673 0 2.563 0z" fill="#B3B3B3" fill-rule="evenodd"/></svg>'.html_safe
|
||||
end
|
||||
('<svg class="fas-custom" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22">' + title + icon + '</svg>').html_safe
|
||||
end
|
||||
|
|
|
|||
|
|
@ -514,6 +514,21 @@ class MyModule < ApplicationRecord
|
|||
self.completed_on = nil
|
||||
end
|
||||
|
||||
def self.my_modules_list_partial
|
||||
ungrouped_tasks = joins(experiment: :project)
|
||||
.select('experiments.name as experiment_name,
|
||||
projects.name as project_name,
|
||||
my_modules.name as task_name,
|
||||
my_modules.id')
|
||||
ungrouped_tasks.group_by { |i| [i[:project_name], i[:experiment_name]] }.map do |group, tasks|
|
||||
{
|
||||
project_name: group[0],
|
||||
experiment_name: group[1],
|
||||
tasks: tasks.map { |task| { id: task.id, task_name: task.task_name } }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_blank_protocol
|
||||
|
|
|
|||
|
|
@ -1,2 +1,21 @@
|
|||
<div class="calendar-widget basic-widget">
|
||||
<div class="dashboard-calendar"
|
||||
data-month-events-url="<%= dashboard_calendar_path %>"
|
||||
data-day-events-url="<%= day_dashboard_calendar_path %>"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
<%
|
||||
js_format = I18n.backend.date_format.dup
|
||||
js_format.gsub!(/%-d/, 'D')
|
||||
js_format.gsub!(/%d/, 'DD')
|
||||
js_format.gsub!(/%-m/, 'M')
|
||||
js_format.gsub!(/%m/, 'MM')
|
||||
js_format.gsub!(/%b/, 'MMM')
|
||||
js_format.gsub!(/%B/, 'MMMM')
|
||||
js_format.gsub!('%Y', 'YYYY')
|
||||
%>
|
||||
|
||||
var formatJS = "<%= js_format %>"
|
||||
</script>
|
||||
|
|
|
|||
19
app/views/shared/_my_modules_list_partial.html.erb
Normal file
19
app/views/shared/_my_modules_list_partial.html.erb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<div class="my-modules-list-partial">
|
||||
<% task_groups.each do |task_group| %>
|
||||
<div class="task-group">
|
||||
<div class="header">
|
||||
<span class="project" title="<%= task_group[:project_name] %>"><%= task_group[:project_name] %></span>
|
||||
<span class="slash">/</span>
|
||||
<span class="experiment" title="<%= task_group[:experiment_name] %>"><%= task_group[:experiment_name] %></span>
|
||||
</div>
|
||||
<div class="tasks">
|
||||
<% task_group[:tasks].each do |task| %>
|
||||
<div class="task">
|
||||
<%= draw_custom_icon('task-icon') %>
|
||||
<%= link_to(task[:task_name], protocols_my_module_path(task[:id]), {class: "task-link", title: task[:task_name]}) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -22,3 +22,13 @@ en:
|
|||
overdue: "Overdue: %{steps} / %{total_steps}"
|
||||
completed: "Completed"
|
||||
locked: "Locked"
|
||||
calendar:
|
||||
due_on: Due on
|
||||
dow:
|
||||
su: 'Su'
|
||||
mo: 'Mo'
|
||||
tu: 'Tu'
|
||||
we: 'We'
|
||||
th: 'Th'
|
||||
fr: 'Fr'
|
||||
sa: 'Sa'
|
||||
|
|
|
|||
|
|
@ -245,6 +245,10 @@ Rails.application.routes.draw do
|
|||
get :project_filter
|
||||
get :experiment_filter
|
||||
end
|
||||
|
||||
resource :calendar, module: 'dashboard', only: [:show] do
|
||||
get :day
|
||||
end
|
||||
end
|
||||
|
||||
resources :projects, except: [:new, :destroy] do
|
||||
|
|
|
|||
6
vendor/assets/javascripts/clndr.min.js
vendored
Normal file
6
vendor/assets/javascripts/clndr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue