Merge pull request #2460 from biosistemika/features/dashboard

Features/dashboard
This commit is contained in:
Miha Mencin 2020-03-10 18:06:23 +01:00 committed by GitHub
commit 26b687f2e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1994 additions and 17 deletions

View file

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

View file

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

View file

@ -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
@ -45,6 +46,7 @@
//= require marvinjslauncher
//= require_tree ./repositories/renderers
//= require_directory ./repositories/validators
//= require_directory ./dashboard
//= require_directory ./sitewide
//= require turbolinks

View 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();
});

View file

@ -0,0 +1,188 @@
/* global dropdownSelector I18n animateSpinner PerfectSb InfiniteScroll */
/* eslint-disable no-param-reassign */
var DasboardCurrentTasksWidget = (function() {
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 emptyState = `<div class="no-tasks">
<p class="text-1">${ I18n.t('dashboard.current_tasks.no_tasks.text_1') }</p>
<p class="text-2">${ I18n.t('dashboard.current_tasks.no_tasks.text_2') }</p>
<i class="fas fa-angle-double-down"></i>
</div>`;
function generateTasksListHtml(json, container) {
$.each(json.data, (i, task) => {
var currentTaskItem = ` <a class="current-task-item" href="${task.link}">
<div class="current-task-breadcrumbs">${task.project}<span class="slash">/</span>${task.experiment}</div>
<div class="item-row">
<div class="task-name">${task.name}</div>
<div class="task-due-date ${task.state.class} ${task.due_date ? '' : 'hidden'}">
<i class="fas fa-calendar-day"></i> ${I18n.t('dashboard.current_tasks.due_date', { date: task.due_date })}
</div>
<div class="task-progress-container ${task.state.class}">
<div class="task-progress" style="padding-left: ${task.steps_precentage}%"></div>
<div class="task-progress-label">${task.state.text}</div>
</div>
</div>
</a>`;
$(container).append(currentTaskItem);
});
}
function initInfiniteScroll() {
InfiniteScroll.init('.current-tasks-list', {
url: $('.current-tasks-list').data('tasksListUrl'),
customResponse: (json, container) => {
generateTasksListHtml(json, container);
},
customParams: (params) => {
params.project_id = dropdownSelector.getValues(projectFilter);
params.experiment_id = dropdownSelector.getValues(experimentFilter);
params.sort = dropdownSelector.getValues(sortFilter);
params.view = dropdownSelector.getValues(viewFilter);
params.query = $('.current-tasks-widget .task-search-field').val();
params.mode = $('.current-tasks-navbar .active').data('mode');
return params;
}
});
}
function loadCurrentTasksList(newList) {
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),
query: $('.current-tasks-widget .task-search-field').val(),
mode: $('.current-tasks-navbar .active').data('mode')
};
animateSpinner($currentTasksList, true);
$.get($currentTasksList.data('tasksListUrl'), params, function(result) {
$currentTasksList.find('.current-task-item, .no-tasks').remove();
// Toggle empty state
if (result.data.length === 0) {
$currentTasksList.append(emptyState);
}
generateTasksListHtml(result, $currentTasksList);
PerfectSb().update_all();
if (newList) InfiniteScroll.resetScroll('.current-tasks-list');
animateSpinner($currentTasksList, false);
});
}
function initFilters() {
$('.curent-tasks-filters .clear-button').click((e) => {
e.stopPropagation();
e.preventDefault();
dropdownSelector.selectValue(sortFilter, 'date_asc');
dropdownSelector.selectValue(viewFilter, 'uncompleted');
dropdownSelector.clearData(projectFilter);
dropdownSelector.clearData(experimentFilter);
});
dropdownSelector.init(sortFilter, {
noEmptyOption: true,
singleSelect: true,
closeOnSelect: true,
selectAppearance: 'simple',
disableSearch: true
});
dropdownSelector.init(viewFilter, {
noEmptyOption: true,
singleSelect: true,
closeOnSelect: true,
selectAppearance: 'simple',
disableSearch: true
});
dropdownSelector.init(projectFilter, {
singleSelect: true,
closeOnSelect: true,
emptyOptionAjax: true,
selectAppearance: 'simple',
ajaxParams: (params) => {
params.mode = $('.current-tasks-navbar .active').data('mode');
return params;
},
onChange: () => {
var selectedValue = dropdownSelector.getValues(projectFilter);
if (selectedValue > 0) {
dropdownSelector.enableSelector(experimentFilter);
} else {
dropdownSelector.disableSelector(experimentFilter);
}
dropdownSelector.clearData(experimentFilter);
}
});
dropdownSelector.init(experimentFilter, {
singleSelect: true,
closeOnSelect: true,
emptyOptionAjax: true,
selectAppearance: 'simple',
ajaxParams: (params) => {
params.mode = $('.current-tasks-navbar .active').data('mode');
params.project_id = dropdownSelector.getValues(projectFilter);
return params;
}
});
$('.curent-tasks-filters').click((e) => {
// Prevent filter window close
e.stopPropagation();
e.preventDefault();
dropdownSelector.closeDropdown(sortFilter);
dropdownSelector.closeDropdown(viewFilter);
dropdownSelector.closeDropdown(projectFilter);
dropdownSelector.closeDropdown(experimentFilter);
});
$('.curent-tasks-filters .apply-filters').click((e) => {
$('.curent-tasks-filters').dropdown('toggle');
e.stopPropagation();
e.preventDefault();
loadCurrentTasksList(true);
});
$('.filter-container').on('hide.bs.dropdown', () => {
loadCurrentTasksList(true);
});
}
function initNavbar() {
$('.navbar-assigned, .navbar-all').on('click', function(e) {
e.stopPropagation();
e.preventDefault();
$('.current-tasks-navbar').find('a').removeClass('active');
$(this).addClass('active');
loadCurrentTasksList(true);
});
}
function initSearch() {
$('.current-tasks-widget').on('change', '.task-search-field', () => {
loadCurrentTasksList();
});
}
return {
init: () => {
if ($('.current-tasks-widget').length) {
initNavbar();
initFilters();
initSearch();
loadCurrentTasksList();
initInfiniteScroll();
}
}
};
}());
$(document).on('turbolinks:load', function() {
DasboardCurrentTasksWidget.init();
});

View file

@ -0,0 +1,118 @@
/* global I18n dropdownSelector */
/* eslint-disable no-param-reassign */
var DasboardQuickStartWidget = (function() {
var projectFilter = '#create-task-modal .project-filter';
var experimentFilter = '#create-task-modal .experiment-filter';
var createTaskButton = '#create-task-modal .create-task-button';
var newProjectsVisibility = '#create-task-modal .new-projects-visibility';
function initNewTaskModal() {
$('.quick-start-widget .new-task').click(() => {
$('#create-task-modal').modal('show');
$('#create-task-modal .select-block').attr('data-error', '');
});
dropdownSelector.init(projectFilter, {
singleSelect: true,
closeOnSelect: true,
selectAppearance: 'simple',
optionLabel: (data) => {
if (data.value === 0) {
return `<i class="fas fa-plus"></i>
<span class="create-new">${I18n.t('dashboard.create_task_modal.filter_create_new')}</span>
<span>"${data.label}"</span>`;
}
return data.label;
},
onSelect: () => {
var selectedValue = dropdownSelector.getValues(projectFilter);
// Toggle project visibility button
if (selectedValue === '0') {
$(newProjectsVisibility).show();
} else {
$(newProjectsVisibility).hide();
}
// Enable/disable experiment filter
if (selectedValue >= 0) {
dropdownSelector.enableSelector(experimentFilter);
} else {
dropdownSelector.disableSelector(experimentFilter);
}
dropdownSelector.clearData(experimentFilter);
}
});
dropdownSelector.init(experimentFilter, {
singleSelect: true,
closeOnSelect: true,
selectAppearance: 'simple',
optionLabel: (data) => {
if (data.value === 0) {
return `<i class="fas fa-plus"></i>
<span class="create-new">${I18n.t('dashboard.create_task_modal.filter_create_new')}</span>
<span>"${data.label}"</span>`;
}
return data.label;
},
ajaxParams: (params) => {
if (dropdownSelector.getValues(projectFilter) === '0') {
params.project = {
name: dropdownSelector.getData(projectFilter)[0].label,
visibility: $('input[name="projects-visibility-selector"]:checked').val()
};
} else {
params.project = { id: dropdownSelector.getValues(projectFilter) };
}
return params;
},
onSelect: () => {
var selectedValue = dropdownSelector.getValues(experimentFilter);
if (selectedValue >= 0) {
$(createTaskButton).removeAttr('disabled');
} else {
$(createTaskButton).attr('disabled', true);
}
}
});
$(createTaskButton).click((e) => {
var params = {};
if (dropdownSelector.getValues(projectFilter) === '0') {
params.project = {
name: dropdownSelector.getData(projectFilter)[0].label,
visibility: $('input[name="projects-visibility-selector"]:checked').val()
};
} else {
params.project = { id: dropdownSelector.getValues(projectFilter) };
}
if (dropdownSelector.getValues(experimentFilter) === '0') {
params.experiment = { name: dropdownSelector.getData(experimentFilter)[0].label };
} else {
params.experiment = { id: dropdownSelector.getValues(experimentFilter) };
}
e.stopPropagation();
e.preventDefault();
$('#create-task-modal .select-block').attr('data-error', '');
$.post($(createTaskButton).data('ajaxUrl'), params, function(data) {
window.location.href = data.my_module_path;
}).error((response) => {
var errorsObject = response.responseJSON.error_object;
var errorsText = response.responseJSON.errors.name.join(' ');
$(`#create-task-modal .select-block[data-error-object="${errorsObject}"]`).attr('data-error', errorsText);
});
});
}
return {
init: () => {
if ($('.quick-start-widget').length) {
initNewTaskModal();
}
}
};
}());
$(document).on('turbolinks:load', function() {
DasboardQuickStartWidget.init();
});

View file

@ -329,6 +329,8 @@ function initCreateNewModal() {
});
});
if ($('#protocols-index').data('new-protocol')) link.click();
submitBtn.on("click", function() {
// Submit the form inside modal
$(this).closest(".modal").find(".modal-body form").submit();

View file

@ -186,6 +186,8 @@
initSelectPicker();
initRedirectToNewReportPage();
});
if ($('#content-reports-index').data('new-report')) $('#new-report-btn').click();
}
initDatatable();

View file

@ -380,7 +380,7 @@ var dropdownSelector = (function() {
}
} else {
// on Close we blur search field
dropdownContainer.find('.search-field').blur();
dropdownContainer.find('.search-field').blur().val('');
// onClose event
if (config.onClose) {
@ -681,7 +681,7 @@ var dropdownSelector = (function() {
// If we have alteast one tag, we need to remove placeholder from search field
if (selector.data('config').selectAppearance === 'simple') {
let selectedLabel = container.find('.tag-label');
container.find('.search-field').attr('placeholder',
container.find('.search-field').prop('placeholder',
selectedLabel.length && selectedLabel.text().trim() !== '' ? selectedLabel.text().trim() : selector.data('placeholder'));
} else {
searchFieldValue.attr('placeholder',
@ -837,7 +837,21 @@ var dropdownSelector = (function() {
setData: function(selector, data) {
if ($(selector).length === 0) return false;
setData($(selector), []);
setData($(selector), data);
return this;
},
// Select value
selectValue: function(selector, value) {
var $selector;
var option;
if ($(selector).length === 0) return false;
$selector = $(selector);
option = $selector.find(`option[value="${value}"]`)[0];
setData($selector, [convertOptionToJson(option)]);
return this;
},

View file

@ -0,0 +1,68 @@
/* eslint-disable no-unused-vars */
var InfiniteScroll = (function() {
function getScrollHeight($container) {
return $container[0].scrollHeight;
}
function scrollNotVisible($container) {
return (getScrollHeight($container) - $container.height() - 150 <= 0);
}
function loadData($container, page = 1) {
var customParams = $container.data('config').customParams;
var params = (customParams ? customParams({ page: page }) : { page: page });
if ($container.hasClass('loading') || $container.hasClass('last-page')) return;
$container.addClass('loading');
$.get($container.data('config').url, params, function(result) {
if ($container.data('config').customResponse) {
$container.data('config').customResponse(result, $container);
} else {
$(result.data).appendTo($container);
}
if (result.next_page) {
$container.data('next-page', result.next_page);
} else {
$container.addClass('last-page');
}
$container.removeClass('loading');
if (scrollNotVisible($container)) {
loadData($container, $container.data('next-page'));
}
});
}
function initScroll(object, config = {}) {
var $container = $(object);
$container.data('next-page', 2);
$container.data('config', config);
if (config.loadFirstPage) {
loadData($container, 1);
} else if (scrollNotVisible($container)) {
loadData($container, $container.data('next-page'));
}
$container.on('scroll', () => {
if ($container.scrollTop() + $container.height() > getScrollHeight($container) - 150 && !$container.hasClass('last-page')) {
loadData($container, $container.data('next-page'));
}
});
}
return {
init: (object, config) => {
initScroll(object, config);
},
resetScroll: (object) => {
$(object).data('next-page', 2).removeClass('last-page');
if (scrollNotVisible($(object))) {
loadData($(object), $(object).data('next-page'));
}
}
};
}());

View file

@ -31,6 +31,7 @@
@import "my_modules/results/*";
@import "my_modules/*";
@import "protocols/*";
@import "dashboard/*";
@import "repository/*";
@import "repository_columns/*";
@import "settings/*";

View file

@ -0,0 +1,141 @@
// 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;
}
&.event {
.event-day {
align-items: center;
border-radius: 50%;
cursor: pointer;
display: flex;
height: 30px;
justify-content: center;
width: 30px;
&:hover {
background: $color-concrete;
color: inherit;
}
}
&::after {
background: $brand-danger;
border-radius: 50%;
content: "";
height: 4px;
left: 14px;
position: absolute;
top: 24px;
width: 4px;
}
}
&.today {
border: $border-primary;
&.event {
&::after {
left: 13px;
top: 23px;
}
}
}
.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;
}
}
}
}

View file

@ -0,0 +1,92 @@
// scss-lint:disable SelectorDepth QualifyingElement NestingDepth
#create-task-modal {
.modal-dialog {
width: 360px;
.description {
margin-bottom: 20px;
}
.select-block {
display: inline-block;
padding-bottom: 16px;
position: relative;
width: 100%;
label {
@include font-small;
display: inline-block;
font-weight: bold;
margin-bottom: 5px;
user-select: none;
}
&::after {
display: block;
color: $brand-danger;
content: attr(data-error);
}
}
.dropdown-selector-container {
.create-new {
padding: 0 5px;
}
}
.new-projects-visibility {
position: relative;
}
.down-arrow {
background-color: $color-white;
box-shadow: -4px 4px 0 $color-black;
display: inline-block;
height: 38px;
margin: 0 11px 20px 14px;
opacity: .2;
width: 40px;
}
.down-arrow::before {
border-bottom: 6px solid transparent;
border-left: 8px solid $color-black;
border-top: 6px solid transparent;
content: "";
height: 0;
left: 46px;
position: absolute;
top: 36px;
width: 0;
}
.project-visibility-container {
display: inline-block;
width: 260px;
}
.project-visibility-title {
@include font-small;
display: inline-block;
font-weight: bold;
margin-bottom: 5px;
}
.sci-toggles-group {
.sci-toggle-item {
width: 130px;
}
.sci-toggle-item-label {
margin-left: -130px;
margin-bottom: 0;
width: 130px;
}
}
.modal-footer {
text-align: center;
}
}
}

View file

@ -0,0 +1,335 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
.dashboard-container .current-tasks-widget {
grid-column: 1 / span 9;
grid-row: 1 / span 6;
.title {
flex-shrink: 0;
}
.actions-container {
display: flex;
flex-grow: 1;
padding-left: 10px;
}
.search-container {
flex-basis: 36px;
.fa-search {
animation-timing-function: $timing-function-sharp;
color: $color-alto;
transition: .3s;
width: 26px;
}
.task-search-field {
background: transparent;
border: $border-default;
padding-left: 36px;
position: relative;
width: 200px;
z-index: 2;
&:placeholder-shown {
border: $border-transparent;
cursor: pointer;
width: 36px;
+ .fa-search {
color: $color-volcano;
}
}
&:hover {
border: $border-default;
}
&:focus {
border: $border-focus;
cursor: auto;
width: 200px;
+ .fa-search {
color: $color-alto;
}
}
}
}
.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%;
}
}
}
.no-tasks {
color: $color-alto;
margin-left: 8px;
margin-top: 16px;
.text-1 {
font-size: 24px;
font-weight: bold;
}
.text-2 {
color: $color-silver-chalice;
font-size: 16px;
}
.fas {
font-size: 32px;
margin-left: 100px;
margin-top: 50px;
}
}
.current-tasks-list {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 0 16px;
position: relative;
.current-task-item {
border-bottom: $border-tertiary;
color: $color-volcano;
padding: 6px;
text-decoration: none;
.current-task-breadcrumbs {
@include font-small;
color: $color-silver-chalice;
line-height: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.slash {
display: inline-block;
text-align: center;
width: 16px;
}
}
.item-row {
display: flex;
.task-name {
flex-grow: 1;
font-size: $font-size-base;
font-weight: bold;
overflow: hidden;
padding-right: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-due-date {
flex-basis: 280px;
flex-shrink: 0;
font-size: 14px;
.fas {
padding: 4px;
}
&.overdue {
color: $brand-danger;
}
&.day-prior {
color: $brand-warning;
}
&.completed {
color: $brand-success;
}
}
}
&:hover {
background: $color-concrete;
}
}
}
.task-progress-container {
height: 20px;
max-width: 250px;
min-width: 150px;
position: relative;
width: 100%;
&::after {
@include font-small;
@include font-awesome;
content: "";
line-height: 18px;
position: absolute;
right: 8px;
top: 1px;
}
.task-progress {
background: $brand-focus-light;
border: $border-tertiary;
border-radius: $border-radius-tag;
display: flex;
height: 20px;
position: relative;
&::after {
background: $color-white;
content: "";
height: 18px;
width: 100%;
}
}
.task-progress-label {
@include font-small;
font-weight: bold;
height: 20px;
left: 0;
line-height: 20px;
padding-left: 8px;
position: absolute;
top: 0;
width: calc(100% - 30px);
}
&.overdue {
.task-progress {
background: $brand-danger-light;
}
.task-progress-label {
color: $brand-danger;
}
&::after {
color: $brand-danger;
content: $font-fas-exclamation-triangle;
}
}
&.day-prior {
.task-progress-label {
color: $brand-warning;
}
}
&.completed {
.task-progress {
outline: $border-success;
}
.task-progress,
.task-progress::after {
background: $brand-success-light;
}
.task-progress-label {
color: $brand-success;
}
&::after {
color: $brand-success;
content: $font-fas-check;
}
}
}
}
@media (max-width: 1500px) {
.dashboard-container .current-tasks-widget {
.task-progress-container {
max-width: 200px;
}
}
}
@media (max-width: 1250px) {
.dashboard-container .current-tasks-widget {
grid-column: 1 / span 8;
.task-progress-container {
max-width: 150px;
}
.current-tasks-list {
.current-task-item {
.item-row {
.task-due-date {
flex-basis: 230px;
}
}
}
}
}
}
@media (max-width: 1000px) {
.dashboard-container .current-tasks-widget {
grid-column: 1 / span 12;
grid-row: 1 / span 4;
.no-tasks .fas {
margin-left: 500px;
}
}
}

View file

@ -0,0 +1,37 @@
.dashboard-container .quick-start-widget {
grid-column: 1 / span 2;
grid-row: 7 / span 6;
.widget-body {
padding: 16px;
.quick-start-description {
margin-bottom: 24px;
}
.btn-secondary {
margin-bottom: 8px;
text-align: left;
}
}
}
@media (max-width: 1700px) {
.dashboard-container .quick-start-widget {
grid-column: 1 / span 3;
}
}
@media (max-width: 1250px) {
.dashboard-container .quick-start-widget {
grid-column: 1 / span 4;
}
}
@media (max-width: 1000px) {
.dashboard-container .quick-start-widget {
grid-column: 7 / span 6;
grid-row: 5 / span 4;
}
}

View file

@ -0,0 +1,24 @@
.dashboard-container .recent-work-widget {
grid-column: 3 / span 7;
grid-row: 7 / span 6;
}
@media (max-width: 1700px) {
.dashboard-container .recent-work-widget {
grid-column: 4 / span 6;
}
}
@media (max-width: 1250px) {
.dashboard-container .recent-work-widget {
grid-column: 5 / span 8;
}
}
@media (max-width: 1000px) {
.dashboard-container .recent-work-widget {
grid-column: 1 / span 12;
grid-row: 9 / span 4;
}
}

View file

@ -0,0 +1,43 @@
.dashboard-container {
--widget-header-size: 44px;
--dashboard-widgets-gap: 30px;
display: grid;
grid-column-gap: var(--dashboard-widgets-gap);
grid-row-gap: var(--dashboard-widgets-gap);
grid-template-columns: repeat(12, 1fr);
grid-template-rows: repeat(12, 1fr);
min-height: calc(100vh - 51px);
padding: var(--dashboard-widgets-gap) calc(var(--dashboard-widgets-gap) - 15px);
width: 100%;
.basic-widget {
border-radius: $border-radius-modal;
box-shadow: $flyout-shadow;
position: relative;
.widget-header {
align-items: center;
border-bottom: $border-tertiary;
display: flex;
height: var(--widget-header-size);
padding-left: 16px;
.widget-title {
@include font-h2;
}
}
.widget-body {
height: calc(100% - var(--widget-header-size));
position: absolute;
width: 100%;
}
}
}
@media (max-width: 1300px) {
.dashboard-container {
--dashboard-widgets-gap: 16px;
}
}

View file

@ -40,6 +40,7 @@
}
.search-field {
@include font-button;
border: 0;
flex-basis: 0;
flex-grow: 2000;
@ -95,6 +96,10 @@
text-overflow: ellipsis;
white-space: nowrap;
width: auto;
&[data-ds-tag-id=""] {
opacity: .7;
}
}
.fas {
@ -108,6 +113,7 @@
}
.dropdown-container {
@include font-button;
background: $color-white;
border: 1px solid $color-alto;
border-radius: 0 0 4px 4px;
@ -172,7 +178,6 @@
color: $color-white;
opacity: 1;
}
}
.checkbox-icon {
@ -264,6 +269,7 @@
&[data-options-selected="0"] {
display: block;
width: 100%;
}
}

View file

@ -0,0 +1,59 @@
// 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 {
flex-shrink: 0;
margin-right: 9px;
path {
fill: $brand-primary;
}
}
.task-link {
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}

View file

@ -8,6 +8,8 @@ $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;
$border-transparent: 1px solid transparent;

View file

@ -30,6 +30,10 @@
&:disabled {
background: transparent;
}
&::placeholder {
color: $color-alto;
}
}
.fas {

View file

@ -0,0 +1,35 @@
.sci-secondary-navbar {
display: flex;
height: 100%;
.navbar-link {
@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%;
}
}
}
}

View file

@ -0,0 +1,35 @@
# 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
.joins(experiment: :project)
.where(experiments: { archived: false })
.where(projects: { archived: false })
.where('my_modules.due_date > ? AND my_modules.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
.joins(experiment: :project)
.where(experiments: { archived: false })
.where(projects: { archived: false })
.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

View file

@ -0,0 +1,133 @@
# frozen_string_literal: true
module Dashboard
class CurrentTasksController < ApplicationController
include InputSanitizeHelper
before_action :load_project, only: %i(show experiment_filter)
before_action :load_experiment, only: :show
before_action :check_task_view_permissions, only: :show
def show
tasks = if @experiment
@experiment.my_modules.active
elsif @project
MyModule.active.where(projects: { id: @project.id })
else
MyModule.active.viewable_by_user(current_user, current_team)
end
tasks = tasks.joins(experiment: :project)
.where(experiments: { archived: false })
.where(projects: { archived: false })
if task_filters[:mode] == 'assigned'
tasks = tasks.left_outer_joins(:user_my_modules).where(user_my_modules: { user_id: current_user.id })
end
tasks = filter_by_state(tasks)
case task_filters[:sort]
when 'date_desc'
tasks = tasks.order('my_modules.due_date': :desc).order('my_modules.name': :asc)
when 'date_asc'
tasks = tasks.order('my_modules.due_date': :asc).order('my_modules.name': :asc)
when 'atoz'
tasks = tasks.order('my_modules.name': :asc)
when 'ztoa'
tasks = tasks.order('my_modules.name': :desc)
else
tasks
end
page = (params[:page] || 1).to_i
tasks = tasks.with_step_statistics.search_by_name(current_user, current_team, task_filters[:query])
.preload(experiment: :project).page(page).per(Constants::INFINITE_SCROLL_LIMIT)
tasks_list = tasks.map do |task|
{ id: task.id,
link: protocols_my_module_path(task.id),
experiment: escape_input(task.experiment.name),
project: escape_input(task.experiment.project.name),
name: escape_input(task.name),
due_date: task.due_date.present? ? I18n.l(task.due_date, format: :full_date) : nil,
state: task_state(task),
steps_precentage: task.steps_completed_percentage }
end
render json: { data: tasks_list, next_page: tasks.next_page }
end
def project_filter
projects = current_team.projects
.where(archived: false)
.viewable_by_user(current_user, current_team)
.search_by_name(current_user, current_team, params[:query]).select(:id, :name)
unless params[:mode] == 'team'
projects = projects.where(id: current_user.my_modules.joins(:experiment)
.group(:project_id).select(:project_id).pluck(:project_id))
end
render json: projects.map { |i| { value: i.id, label: escape_input(i.name) } }, status: :ok
end
def experiment_filter
unless @project
render json: []
return false
end
experiments = @project.experiments
.where(archived: false)
.viewable_by_user(current_user, current_team)
.search_by_name(current_user, current_team, params[:query]).select(:id, :name)
unless params[:mode] == 'team'
experiments = experiments.where(id: current_user.my_modules
.group(:experiment_id).select(:experiment_id).pluck(:experiment_id))
end
render json: experiments.map { |i| { value: i.id, label: escape_input(i.name) } }, status: :ok
end
private
def task_state(task)
if task.state == 'completed'
task_state_class = task.state
task_state_text = t('dashboard.current_tasks.progress_bar.completed')
else
task_state_text = t('dashboard.current_tasks.progress_bar.in_progress')
task_state_class = 'day-prior' if task.is_one_day_prior?
if task.is_overdue?
task_state_text = t('dashboard.current_tasks.progress_bar.overdue')
task_state_class = 'overdue'
end
if task.steps_total.positive?
task_state_text += t('dashboard.current_tasks.progress_bar.completed_steps',
steps: task.steps_completed, total_steps: task.steps_total)
end
end
{ text: task_state_text, class: task_state_class }
end
def filter_by_state(tasks)
tasks.where(my_modules: { state: task_filters[:view] })
end
def task_filters
params.permit(:project_id, :experiment_id, :mode, :view, :sort, :query, :page)
end
def load_project
@project = current_team.projects.find_by(id: params[:project_id])
end
def load_experiment
@experiment = @project.experiments.find_by(id: params[:experiment_id]) if @project
end
def check_task_view_permissions
render_403 if @project && !can_read_project?(@project)
render_403 if @experiment && !can_read_experiment?(@experiment)
end
end
end

View file

@ -0,0 +1,80 @@
# frozen_string_literal: true
module Dashboard
class QuickStartController < ApplicationController
include InputSanitizeHelper
before_action :load_project, only: %i(create_task experiment_filter)
before_action :load_experiment, only: :create_task
before_action :check_task_create_permissions, only: :create_task
def create_task
my_module = CreateMyModuleService.new(current_user, current_team,
project: @project || create_project_params,
experiment: @experiment || create_experiment_params).call
if my_module.errors.empty?
render json: { my_module_path: protocols_my_module_path(my_module) }
else
render json: { errors: my_module.errors, error_object: my_module.class.name }, status: :unprocessable_entity
end
end
def project_filter
projects = current_team.projects.search(current_user, false, params[:query], 1, current_team)
.where('user_projects.role <= 1')
.select(:id, :name)
projects = projects.map { |i| { value: i.id, label: escape_input(i.name) } }
if (projects.map { |i| i[:label] }.exclude? params[:query]) && params[:query].present?
projects = [{ value: 0, label: params[:query] }] + projects
end
render json: projects, status: :ok
end
def experiment_filter
if create_project_params.present? && params[:query].present?
experiments = [{ value: 0, label: params[:query] }]
elsif @project
experiments = @project.experiments
.search(current_user, false, params[:query], 1, current_team)
.select(:id, :name)
experiments = experiments.map { |i| { value: i.id, label: escape_input(i.name) } }
if (experiments.map { |i| i[:label] }.exclude? params[:query]) && params[:query].present?
experiments = [{ value: 0, label: params[:query] }] + experiments
end
end
render json: experiments || [], status: :ok
end
private
def create_project_params
params.require(:project).permit(:name, :visibility)
end
def create_experiment_params
params.require(:experiment).permit(:name)
end
def load_project
@project = current_team.projects.find_by(id: params.dig(:project, :id))
end
def load_experiment
@experiment = @project.experiments.find_by(id: params.dig(:experiment, :id)) if @project
end
def check_task_create_permissions
unless @project
render_403 unless can_create_projects?(current_user, current_team)
return
end
unless @experiment
render_403 unless can_create_experiments?(current_user, @project)
return
end
render_403 unless can_manage_experiment?(current_user, @experiment)
end
end
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class DashboardsController < ApplicationController
def show; end
end

View file

@ -95,7 +95,7 @@ class ExperimentsController < ApplicationController
@experiment.last_modified_by = current_user
if @experiment.save
experiment_annotation_notification(old_text)
experiment_annotation_notification(old_text) if old_text
activity_type = if experiment_params[:archived] == 'false'
:restore_experiment

View file

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

View file

@ -1,4 +1,9 @@
module LeftMenuBarHelper
def dashboard_are_selected?
controller_name == 'dashboards'
end
def projects_are_selected?
controller_name.in? %w(projects experiments my_modules)
end

View file

@ -80,6 +80,16 @@ class MyModule < ApplicationRecord
end)
scope :workflow_ordered, -> { order(workflow_order: :asc) }
scope :uncomplete, -> { where(state: 'uncompleted') }
scope :with_step_statistics, (lambda do
left_outer_joins(protocols: :steps)
.group(:id)
.select('my_modules.*')
.select('COUNT(steps.id) AS steps_total')
.select('COUNT(steps.id) FILTER (where steps.completed = true) AS steps_completed')
.select('CASE COUNT(steps.id) WHEN 0 THEN 0 ELSE'\
'((COUNT(steps.id) FILTER (where steps.completed = true)) * 100 / COUNT(steps.id)) '\
'END AS steps_completed_percentage')
end)
# A module takes this much space in canvas (x, y) in database
WIDTH = 30
@ -514,6 +524,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

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
class CreateExperimentService
def initialize(user, team, params)
@params = params
@user = user
@team = team
end
def call
new_experiment = nil
ActiveRecord::Base.transaction do
unless @params[:project].class == Project
@params[:project] = CreateProjectService.new(@user, @team, @params[:project]).call
end
unless @params[:project]&.errors&.empty?
new_experiment = @params[:project]
raise ActiveRecord::Rollback
end
@params[:created_by] = @user
@params[:last_modified_by] = @user
@experiment = @params[:project].experiments.new(@params)
create_experiment_activity if @experiment.save
new_experiment = @experiment
end
new_experiment
end
private
def create_experiment_activity
Activities::CreateActivityService
.call(activity_type: :create_experiment,
owner: @user,
subject: @experiment,
team: @team,
project: @experiment.project,
message_items: { experiment: @experiment.id })
end
end

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
class CreateMyModuleService
def initialize(user, team, params)
@params = params
@my_module_params = params[:my_module] || {}
@user = user
@team = team
end
def call
new_my_module = nil
ActiveRecord::Base.transaction do
unless @params[:experiment].class == Experiment
@params[:experiment][:project] = @params[:project]
@params[:experiment] = CreateExperimentService.new(@user, @team, @params[:experiment]).call
end
unless @params[:experiment]&.errors&.empty?
new_my_module = @params[:experiment]
raise ActiveRecord::Rollback
end
@my_module_params[:x] ||= 0
@my_module_params[:y] ||= 0
@my_module_params[:name] ||= I18n.t('create_task_service.default_task_name')
@my_module = @params[:experiment].my_modules.new(@my_module_params)
new_pos = @my_module.get_new_position
@my_module.x = new_pos[:x]
@my_module.y = new_pos[:y]
@my_module.save!
create_my_module_activity
@params[:experiment].generate_workflow_img
new_my_module = @my_module
end
new_my_module
end
private
def create_my_module_activity
Activities::CreateActivityService
.call(activity_type: :create_module,
owner: @user,
team: @team,
project: @params[:experiment].project,
subject: @my_module,
message_items: { my_module: @my_module.id })
end
end

View file

@ -0,0 +1,42 @@
# frozen_string_literal: true
class CreateProjectService
def initialize(user, team, params)
@params = params
@user = user
@team = team
end
def call
new_project = nil
ActiveRecord::Base.transaction do
@params[:created_by] = @user
@params[:last_modified_by] = @user
@project = @team.projects.new(@params)
if @project.save
@project.user_projects.create!(role: :owner, user: @user)
create_project_activity
new_project = @project
else
new_project = @project
raise ActiveRecord::Rollback
end
end
new_project
end
private
def create_project_activity
Activities::CreateActivityService
.call(activity_type: :create_project,
owner: @user,
subject: @project,
team: @team,
project: @project,
message_items: { project: @project.id })
end
end

View file

@ -0,0 +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>

View file

@ -0,0 +1,57 @@
<div class="modal" id="create-task-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span></button>
<h2 class="modal-title">
<%= t("dashboard.create_task_modal.title") %>
</h2>
</div>
<div class="modal-body">
<div class="description">
<%= t("dashboard.create_task_modal.description") %>
</div>
<div class="select-block" data-error-object="Project" data-error="">
<label><%= t("dashboard.create_task_modal.project") %></label>
<select class="project-filter"
data-ajax-url="<%= dashboard_quick_start_project_filter_path %>"
data-placeholder="<%= t("dashboard.create_task_modal.project_placeholder") %>"></select>
</div>
<div class="new-projects-visibility" style="display: none;">
<div class="down-arrow"></div>
<div class="project-visibility-container">
<div class="project-visibility-title"><%= t("dashboard.create_task_modal.project_visibility_label") %></div>
<div class="sci-toggles-group" data-toggle="buttons">
<input type="radio" name="projects-visibility-selector" class="sci-toggle-item" value="hidden" checked="checked">
<label class="sci-toggle-item-label">
<%= t("dashboard.create_task_modal.project_visibility_members") %>
</label>
<input type="radio" name="projects-visibility-selector" class="sci-toggle-item" value="visible">
<label class="sci-toggle-item-label">
<%= t("dashboard.create_task_modal.project_visibility_all") %>
</label>
</div>
</div>
</div>
<div class="select-block" data-error-object="Experiment" data-error="">
<label><%= t("dashboard.create_task_modal.experiment") %></label>
<select class="experiment-filter"
data-ajax-url="<%= dashboard_quick_start_experiment_filter_path %>"
data-disable-on-load="true"
data-disable-placeholder="<%= t("dashboard.create_task_modal.experiment_disabled_placeholder") %>"
data-placeholder="<%= t("dashboard.create_task_modal.experiment_placeholder") %>"></select>
</div>
</div>
<div class="modal-footer">
<button type="button"
class="create-task-button btn btn-primary"
data-ajax-url="<%= dashboard_quick_start_create_task_path %>"
data-dismiss="modal"
disabled>
<%= t("dashboard.create_task_modal.create_task") %>
</button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,66 @@
<div class="current-tasks-widget basic-widget">
<div class="widget-header">
<div class="widget-title"><%= t("dashboard.current_tasks.title") %></div>
<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="search-container">
<div class="sci-input-container left-icon ">
<input type="text" class="sci-input-field task-search-field" placeholder="<%= t("dashboard.current_tasks.search") %>"></input>
<i class="fas fa-search"></i>
</div>
</div>
</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>
</div>
</div>
<div class="widget-body">
<div class="current-tasks-list perfect-scrollbar"
data-tasks-list-url="<%= dashboard_current_tasks_path %>">
</div>
</div>
</div>

View file

@ -0,0 +1,20 @@
<div class="quick-start-widget basic-widget">
<div class="widget-header">
<div class="widget-title">
<%= t("dashboard.quick_start.title") %>
</div>
</div>
<div class="widget-body">
<div class="quick-start-description">
<%= t("dashboard.quick_start.description") %>
</div>
<div class="new-task btn btn-secondary btn-block"><i class="fas fa-plus"></i><%= t("dashboard.quick_start.new_task") %></div>
<%= link_to protocols_path(new_protocol: true), {class: "new-protocol btn btn-secondary btn-block"} do %>
<i class="fas fa-edit"></i><%= t("dashboard.quick_start.new_protocol") %>
<% end %>
<%= link_to reports_path(new_report: true), {class: "new-report btn btn-secondary btn-block"} do %>
<i class="fas fa-clipboard-check"></i><%= t("dashboard.quick_start.new_report") %>
<% end %>
</div>
</div>
<%= render "create_task_modal" %>

View file

@ -0,0 +1,2 @@
<div class="recent-work-widget basic-widget">
</div>

View file

@ -0,0 +1,8 @@
<% provide :head_title, t('nav.label.dashboard') %>
<div class="dashboard-container">
<%= render "calendar" %>
<%= render "current_tasks" %>
<%= render "recent_work" %>
<%= render "quick_start" %>
</div>

View file

@ -5,7 +5,7 @@
<% provide(:head_title, t("protocols.index.head_title")) %>
<% if current_team %>
<div class="content-pane" id="protocols-index">
<div class="content-pane" id="protocols-index" <%= "data-new-protocol=true" if params[:new_protocol] %>>
<ul class="nav nav-tabs nav-settings">
<li role="presentation" class="<%= "active" if @type == :public %>">
<%= link_to t("protocols.index.navigation.public"), protocols_path(team: @current_team, type: :public) %>

View file

@ -6,7 +6,7 @@
<%= stylesheet_link_tag 'datatables' %>
<div class="content-pane">
<div id="content-reports-index">
<div id="content-reports-index" <%= "data-new-report=true" if params[:new_report] %>>
<div class="row">
<div class="col-md-12">
<% if can_manage_reports?(current_team) %>

View file

@ -2,6 +2,12 @@
<div class="scroll-wrapper">
<ul class="nav">
<% if current_user.teams.empty? %>
<li class="disabled">
<span>
<span class="fas fa-thumbtack"></span>
<span><%= t('left_menu_bar.dashboard') %></span>
</span>
</li>
<li class="disabled">
<span>
<span class="fas fa-folder"></span>
@ -27,6 +33,12 @@
</span>
</li>
<% else %>
<li class="<%= "active" if dashboard_are_selected? %>">
<%= link_to dashboard_path, id: "dashboard-link", title: t('left_menu_bar.dashboard') do %>
<span class="fas fa-thumbtack"></span>
<span><%= t('left_menu_bar.dashboard') %></span>
<% end %>
</li>
<li class="<%= "active" if projects_are_selected? %>">
<%= link_to projects_path, id: "projects-link", title: t('left_menu_bar.projects') do %>
<span class="fas fa-folder"></span>

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

View file

@ -56,16 +56,14 @@ class Constants
COMMENTS_SEARCH_LIMIT = 10
# Activity limited query/display elements for pages
ACTIVITY_AND_NOTIF_SEARCH_LIMIT = 20
# Infinite Scroll load limit (elements per page)
INFINITE_SCROLL_LIMIT = 20
# Maximum number of users that can be invited in a single action
INVITE_USERS_LIMIT = 20
# Maximum nr. of search results for atwho (smart annotations)
ATWHO_SEARCH_LIMIT = 5
# Max characters for repository name in Atwho modal
ATWHO_REP_NAME_LIMIT = 16
# Number of protocols in recent protocol dropdown
RECENT_PROTOCOL_LIMIT = 14

View file

@ -0,0 +1,62 @@
en:
dashboard:
current_tasks:
title: "Current tasks"
search: "Search tasks"
navbar:
assigned: "Assigned to me"
all: "All"
filter:
title: "Filters"
clear: "Clear"
sort: "Sort by"
date_asc: "Due Date Ascending"
date_desc: "Due Date Descending"
atoz: "From A to Z"
ztoa: "From Z to A"
display: "Display"
uncompleted_tasks: "Tasks in progress"
completed_tasks: "Tasks completed"
project: "Project"
select_project: "All Projects"
experiment: "Experiment"
select_experiment: "All Experiments"
apply: "Apply"
no_tasks:
text_1: "Looks like you completed all of your tasks. Good job!"
text_2: "Why not use use the Quick start section to create your next task"
due_date: "Due date: %{date}"
progress_bar:
in_progress: "In progress"
overdue: "Overdue"
completed_steps: ": %{steps} / %{total_steps}"
completed: "Completed"
calendar:
due_on: Due on
dow:
su: 'Su'
mo: 'Mo'
tu: 'Tu'
we: 'We'
th: 'Th'
fr: 'Fr'
sa: 'Sa'
quick_start:
title: "Quick start"
description: "Quickly start a new task, create a new project or a new report."
new_task: "New task"
new_protocol: "New protocol"
new_report: "New report"
create_task_modal:
title: "Create a new Task"
description: "Simply type in the fields bellow to find or create space for your new task to live in"
project: "Project"
project_visibility_label: "Visible to"
project_visibility_members: "Project members"
project_visibility_all: "All team members"
experiment: "Experiment"
project_placeholder: "Enter project name (New or Existing)"
experiment_placeholder: "Enter experiment name (New or Existing)"
experiment_disabled_placeholder: "Select Project to enable Experiments"
filter_create_new: "Create"
create_task: "Create Task"

View file

@ -134,7 +134,8 @@ en:
none: "No activities!"
label:
scinote: "SciNote"
projects: "Home"
dashboard: "Overview"
projects: "Projects"
protocols: "Protocols"
calendar: "Calendar"
activities: "Activities"
@ -151,6 +152,7 @@ en:
addon_versions: "Addon versions"
left_menu_bar:
dashboard: "Overview"
projects: "Projects"
repositories: "Inventories"
templates: "Protocols"
@ -258,7 +260,7 @@ en:
projects:
index:
head_title: "Home"
head_title: "Projects"
archive: "Archive"
archived: "Archived"
active: "Active"
@ -2113,6 +2115,9 @@ en:
dropdown_selector:
nothing_found: "Nothing found..."
create_task_service:
default_task_name: "New Task"
zip_export:
modal_label: 'Export inventory'
notification_title: 'Your requested export package is ready!'

View file

@ -16,7 +16,7 @@ Rails.application.routes.draw do
confirmations: 'users/confirmations',
omniauth_callbacks: 'users/omniauth_callbacks' }
root 'projects#index'
root 'dashboards#show'
# # Client APP endpoints
# get '/settings', to: 'client_api/settings#index'
@ -240,6 +240,23 @@ Rails.application.routes.draw do
defaults: { format: 'json' }
post 'reports/destroy', to: 'reports#destroy'
resource :dashboard, only: :show do
resource :current_tasks, module: 'dashboard', only: :show do
get :project_filter
get :experiment_filter
end
namespace :quick_start, module: :dashboard, controller: :quick_start do
get :project_filter
get :experiment_filter
post :create_task
end
resource :calendar, module: 'dashboard', only: [:show] do
get :day
end
end
resources :projects, except: [:new, :destroy] do
resources :user_projects, path: '/users',
only: [:create, :index, :update, :destroy]

File diff suppressed because one or more lines are too long