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 })} +
+
+
+
${task.state.text}
+
+
+
`; + $(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 @@ + 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 @@ +
+
+
<%= t("dashboard.current_tasks.title") %>
+
+ +
+
+ "> + +
+
+
+ + +
+ +
+
+
+
+
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 @@ +
+
+
+ <%= t("dashboard.quick_start.title") %> +
+
+
+
+ <%= t("dashboard.quick_start.description") %> +
+
<%= t("dashboard.quick_start.new_task") %>
+ <%= link_to protocols_path(new_protocol: true), {class: "new-protocol btn btn-secondary btn-block"} do %> + <%= t("dashboard.quick_start.new_protocol") %> + <% end %> + <%= link_to reports_path(new_report: true), {class: "new-report btn btn-secondary btn-block"} do %> + <%= t("dashboard.quick_start.new_report") %> + <% end %> +
+
+<%= 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 %> -
+
>