diff --git a/Gemfile b/Gemfile
index 4a57ae081..b4aca7f58 100644
--- a/Gemfile
+++ b/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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 9e87abfe0..e369124f7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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
diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb
index 909940fb5..e0f94b2e5 100644
--- a/app/assets/javascripts/application.js.erb
+++ b/app/assets/javascripts/application.js.erb
@@ -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
diff --git a/app/assets/javascripts/dashboard/calendar.js b/app/assets/javascripts/dashboard/calendar.js
new file mode 100644
index 000000000..8d074e829
--- /dev/null
+++ b/app/assets/javascripts/dashboard/calendar.js
@@ -0,0 +1,88 @@
+/* global I18n */
+/* eslint-disable no-underscore-dangle */
+
+var DasboardCalendarWidget = (function() {
+ function calendarTemplate() {
+ return ``;
+ }
+
+ 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();
+});
diff --git a/app/assets/javascripts/dashboard/current_tasks.js b/app/assets/javascripts/dashboard/current_tasks.js
new file mode 100644
index 000000000..33b420611
--- /dev/null
+++ b/app/assets/javascripts/dashboard/current_tasks.js
@@ -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 = `
+
${ I18n.t('dashboard.current_tasks.no_tasks.text_1') }
+
${ I18n.t('dashboard.current_tasks.no_tasks.text_2') }
+
+
`;
+
+ function generateTasksListHtml(json, container) {
+ $.each(json.data, (i, task) => {
+ var currentTaskItem = `
+ ${task.project}/ ${task.experiment}
+
+
${task.name}
+
+ ${I18n.t('dashboard.current_tasks.due_date', { date: task.due_date })}
+
+
+
+ `;
+ $(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();
+});
diff --git a/app/assets/javascripts/dashboard/quick_start.js b/app/assets/javascripts/dashboard/quick_start.js
new file mode 100644
index 000000000..809c3d72c
--- /dev/null
+++ b/app/assets/javascripts/dashboard/quick_start.js
@@ -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 `
+ ${I18n.t('dashboard.create_task_modal.filter_create_new')}
+ "${data.label}" `;
+ }
+ 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 `
+ ${I18n.t('dashboard.create_task_modal.filter_create_new')}
+ "${data.label}" `;
+ }
+ 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();
+});
diff --git a/app/assets/javascripts/protocols/index.js b/app/assets/javascripts/protocols/index.js
index 4a40e6bae..944bfd691 100644
--- a/app/assets/javascripts/protocols/index.js
+++ b/app/assets/javascripts/protocols/index.js
@@ -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();
diff --git a/app/assets/javascripts/reports/reports_datatable.js.erb b/app/assets/javascripts/reports/reports_datatable.js.erb
index a0c125d97..467685de5 100644
--- a/app/assets/javascripts/reports/reports_datatable.js.erb
+++ b/app/assets/javascripts/reports/reports_datatable.js.erb
@@ -186,6 +186,8 @@
initSelectPicker();
initRedirectToNewReportPage();
});
+
+ if ($('#content-reports-index').data('new-report')) $('#new-report-btn').click();
}
initDatatable();
diff --git a/app/assets/javascripts/sitewide/dropdown_selector.js b/app/assets/javascripts/sitewide/dropdown_selector.js
index c5bf4d78d..a2906788d 100644
--- a/app/assets/javascripts/sitewide/dropdown_selector.js
+++ b/app/assets/javascripts/sitewide/dropdown_selector.js
@@ -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;
},
diff --git a/app/assets/javascripts/sitewide/infinite_scroll.js b/app/assets/javascripts/sitewide/infinite_scroll.js
new file mode 100644
index 000000000..a062de0aa
--- /dev/null
+++ b/app/assets/javascripts/sitewide/infinite_scroll.js
@@ -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'));
+ }
+ }
+ };
+}());
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 152f59810..c840a574d 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -31,6 +31,7 @@
@import "my_modules/results/*";
@import "my_modules/*";
@import "protocols/*";
+@import "dashboard/*";
@import "repository/*";
@import "repository_columns/*";
@import "settings/*";
diff --git a/app/assets/stylesheets/dashboard/calendar.scss b/app/assets/stylesheets/dashboard/calendar.scss
new file mode 100644
index 000000000..83fae113e
--- /dev/null
+++ b/app/assets/stylesheets/dashboard/calendar.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/dashboard/create_task_modal.scss b/app/assets/stylesheets/dashboard/create_task_modal.scss
new file mode 100644
index 000000000..e627f8511
--- /dev/null
+++ b/app/assets/stylesheets/dashboard/create_task_modal.scss
@@ -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;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/dashboard/current_tasks.scss b/app/assets/stylesheets/dashboard/current_tasks.scss
new file mode 100644
index 000000000..f8f89b1e9
--- /dev/null
+++ b/app/assets/stylesheets/dashboard/current_tasks.scss
@@ -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;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/dashboard/quick_start.scss b/app/assets/stylesheets/dashboard/quick_start.scss
new file mode 100644
index 000000000..38b4f8dd5
--- /dev/null
+++ b/app/assets/stylesheets/dashboard/quick_start.scss
@@ -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;
+ }
+}
diff --git a/app/assets/stylesheets/dashboard/recent_work.scss b/app/assets/stylesheets/dashboard/recent_work.scss
new file mode 100644
index 000000000..e855cd00b
--- /dev/null
+++ b/app/assets/stylesheets/dashboard/recent_work.scss
@@ -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;
+ }
+}
diff --git a/app/assets/stylesheets/dashboard/show.scss b/app/assets/stylesheets/dashboard/show.scss
new file mode 100644
index 000000000..8dad1ce12
--- /dev/null
+++ b/app/assets/stylesheets/dashboard/show.scss
@@ -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;
+ }
+}
diff --git a/app/assets/stylesheets/shared/dropdown_selector.scss b/app/assets/stylesheets/shared/dropdown_selector.scss
index bc7976030..bf1ebba5e 100644
--- a/app/assets/stylesheets/shared/dropdown_selector.scss
+++ b/app/assets/stylesheets/shared/dropdown_selector.scss
@@ -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%;
}
}
diff --git a/app/assets/stylesheets/shared/my_modules_list_partial.scss b/app/assets/stylesheets/shared/my_modules_list_partial.scss
new file mode 100644
index 000000000..cf3be7bb5
--- /dev/null
+++ b/app/assets/stylesheets/shared/my_modules_list_partial.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/shared_styles/constants/borders.scss b/app/assets/stylesheets/shared_styles/constants/borders.scss
index 4de7169af..299aa2da7 100644
--- a/app/assets/stylesheets/shared_styles/constants/borders.scss
+++ b/app/assets/stylesheets/shared_styles/constants/borders.scss
@@ -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;
diff --git a/app/assets/stylesheets/shared_styles/elements/input_fields.scss b/app/assets/stylesheets/shared_styles/elements/input_fields.scss
index 2114c9a32..368883d14 100644
--- a/app/assets/stylesheets/shared_styles/elements/input_fields.scss
+++ b/app/assets/stylesheets/shared_styles/elements/input_fields.scss
@@ -30,6 +30,10 @@
&:disabled {
background: transparent;
}
+
+ &::placeholder {
+ color: $color-alto;
+ }
}
.fas {
diff --git a/app/assets/stylesheets/shared_styles/elements/navigation.scss b/app/assets/stylesheets/shared_styles/elements/navigation.scss
new file mode 100644
index 000000000..bfab2dde3
--- /dev/null
+++ b/app/assets/stylesheets/shared_styles/elements/navigation.scss
@@ -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%;
+ }
+ }
+ }
+}
diff --git a/app/controllers/dashboard/calendars_controller.rb b/app/controllers/dashboard/calendars_controller.rb
new file mode 100644
index 000000000..65afdd28d
--- /dev/null
+++ b/app/controllers/dashboard/calendars_controller.rb
@@ -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
diff --git a/app/controllers/dashboard/current_tasks_controller.rb b/app/controllers/dashboard/current_tasks_controller.rb
new file mode 100644
index 000000000..d6be71484
--- /dev/null
+++ b/app/controllers/dashboard/current_tasks_controller.rb
@@ -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
diff --git a/app/controllers/dashboard/quick_start_controller.rb b/app/controllers/dashboard/quick_start_controller.rb
new file mode 100644
index 000000000..6c30529d7
--- /dev/null
+++ b/app/controllers/dashboard/quick_start_controller.rb
@@ -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
diff --git a/app/controllers/dashboards_controller.rb b/app/controllers/dashboards_controller.rb
new file mode 100644
index 000000000..2e4880fad
--- /dev/null
+++ b/app/controllers/dashboards_controller.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class DashboardsController < ApplicationController
+ def show; end
+end
diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb
index 0addb3e1b..d228f4b9e 100644
--- a/app/controllers/experiments_controller.rb
+++ b/app/controllers/experiments_controller.rb
@@ -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
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index b0bd9cd07..37be8c8f3 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -13,6 +13,8 @@ module IconsHelper
when 'shared-read'
title = "#{t('repositories.icon_title.shared_read', team_name: team.name)} "
icon = ' '
+ when 'task-icon'
+ return ' '.html_safe
end
('' + title + icon + ' ').html_safe
end
diff --git a/app/helpers/left_menu_bar_helper.rb b/app/helpers/left_menu_bar_helper.rb
index 536ef7391..afcdd7bee 100644
--- a/app/helpers/left_menu_bar_helper.rb
+++ b/app/helpers/left_menu_bar_helper.rb
@@ -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
diff --git a/app/models/my_module.rb b/app/models/my_module.rb
index 486e30f10..c9c84c031 100644
--- a/app/models/my_module.rb
+++ b/app/models/my_module.rb
@@ -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
diff --git a/app/services/create_experiment_service.rb b/app/services/create_experiment_service.rb
new file mode 100644
index 000000000..c2d1e4d29
--- /dev/null
+++ b/app/services/create_experiment_service.rb
@@ -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
diff --git a/app/services/create_my_module_service.rb b/app/services/create_my_module_service.rb
new file mode 100644
index 000000000..144f53521
--- /dev/null
+++ b/app/services/create_my_module_service.rb
@@ -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
diff --git a/app/services/create_project_service.rb b/app/services/create_project_service.rb
new file mode 100644
index 000000000..75c4fd762
--- /dev/null
+++ b/app/services/create_project_service.rb
@@ -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
diff --git a/app/views/dashboards/_calendar.html.erb b/app/views/dashboards/_calendar.html.erb
new file mode 100644
index 000000000..8ad89763a
--- /dev/null
+++ b/app/views/dashboards/_calendar.html.erb
@@ -0,0 +1,21 @@
+
+
+
diff --git a/app/views/dashboards/_create_task_modal.html.erb b/app/views/dashboards/_create_task_modal.html.erb
new file mode 100644
index 000000000..9ec39df3e
--- /dev/null
+++ b/app/views/dashboards/_create_task_modal.html.erb
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ <%= t("dashboard.create_task_modal.description") %>
+
+
+ <%= t("dashboard.create_task_modal.project") %>
+ ">
+
+
+
+ <%= t("dashboard.create_task_modal.experiment") %>
+ "
+ data-placeholder="<%= t("dashboard.create_task_modal.experiment_placeholder") %>">
+
+
+
+
+
+
+
diff --git a/app/views/dashboards/_current_tasks.html.erb b/app/views/dashboards/_current_tasks.html.erb
new file mode 100644
index 000000000..f3a6165b2
--- /dev/null
+++ b/app/views/dashboards/_current_tasks.html.erb
@@ -0,0 +1,66 @@
+
diff --git a/app/views/dashboards/_quick_start.html.erb b/app/views/dashboards/_quick_start.html.erb
new file mode 100644
index 000000000..b4c86222c
--- /dev/null
+++ b/app/views/dashboards/_quick_start.html.erb
@@ -0,0 +1,20 @@
+
+<%= render "create_task_modal" %>
\ No newline at end of file
diff --git a/app/views/dashboards/_recent_work.html.erb b/app/views/dashboards/_recent_work.html.erb
new file mode 100644
index 000000000..0a3a1ed48
--- /dev/null
+++ b/app/views/dashboards/_recent_work.html.erb
@@ -0,0 +1,2 @@
+
+
diff --git a/app/views/dashboards/show.html.erb b/app/views/dashboards/show.html.erb
new file mode 100644
index 000000000..a4632c1d2
--- /dev/null
+++ b/app/views/dashboards/show.html.erb
@@ -0,0 +1,8 @@
+<% provide :head_title, t('nav.label.dashboard') %>
+
+
+ <%= render "calendar" %>
+ <%= render "current_tasks" %>
+ <%= render "recent_work" %>
+ <%= render "quick_start" %>
+
diff --git a/app/views/protocols/index.html.erb b/app/views/protocols/index.html.erb
index f405e3d09..571bf24b3 100644
--- a/app/views/protocols/index.html.erb
+++ b/app/views/protocols/index.html.erb
@@ -5,7 +5,7 @@
<% provide(:head_title, t("protocols.index.head_title")) %>
<% if current_team %>
-
+
>
">
<%= link_to t("protocols.index.navigation.public"), protocols_path(team: @current_team, type: :public) %>
diff --git a/app/views/reports/index.html.erb b/app/views/reports/index.html.erb
index 08acfca06..9debf5906 100644
--- a/app/views/reports/index.html.erb
+++ b/app/views/reports/index.html.erb
@@ -6,7 +6,7 @@
<%= stylesheet_link_tag 'datatables' %>
-
+
>
<% if can_manage_reports?(current_team) %>
diff --git a/app/views/shared/_left_menu_bar.html.erb b/app/views/shared/_left_menu_bar.html.erb
index 272d65375..1bf050ba8 100644
--- a/app/views/shared/_left_menu_bar.html.erb
+++ b/app/views/shared/_left_menu_bar.html.erb
@@ -2,6 +2,12 @@
"),this.calendarContainer=p(".clndr",this.element),this.bindEvents(),this.render(),this.options.ready&&this.options.ready.apply(this,[])},n.prototype.validateOptions=function(){(6
t.month()?(c+=" "+this.options.classes.adjacentMonth,p.isAdjacentMonth=!0,this._currentIntervalStart.year()===t.year()?c+=" "+this.options.classes.lastMonth:c+=" "+this.options.classes.nextMonth):this._currentIntervalStart.month()