diff --git a/Gemfile b/Gemfile index 92e9e4557..c86573f07 100644 --- a/Gemfile +++ b/Gemfile @@ -64,11 +64,11 @@ gem 'aspector' # Aspect-oriented programming for Rails gem 'auto_strip_attributes', '~> 2.1' # Removes unnecessary whitespaces AR gem 'bcrypt', '~> 3.1.10' gem 'caracal' # Build docx report -gem 'deface', '~> 1.0' +gem 'deface', '~> 1.9' gem 'down', '~> 5.0' gem 'faker' # Generate fake data gem 'fastimage' # Light gem to get image resolution -gem 'httparty', '~> 0.17.3' +gem 'httparty', '~> 0.21.0' gem 'i18n-js', '~> 3.6' # Localization in javascript files gem 'jbuilder' # JSON structures via a Builder-style DSL gem 'logging', '~> 2.0.0' @@ -97,15 +97,12 @@ gem 'devise-async', git: 'https://github.com/mhfs/devise-async.git', branch: 'devise-4.x' gem 'image_processing', '~> 1.12' +gem 'img2zpl', git: 'https://github.com/scinote-eln/img2zpl' gem 'rufus-scheduler', '~> 3.5' gem 'discard', '~> 1.0' gem 'graphviz' -gem 'tinymce-rails', '~> 4.9.10' # Rich text editor - SEE BELOW -# Any time you update tinymce-rails Gem, also update the cache_suffix parameter -# in sitewide/tiny_mce.js - to prevent browsers from loading old, cached .js -# TinyMCE files which might cause errors gem 'base62' # Used for smart annotations gem 'newrelic_rpm' diff --git a/Gemfile.lock b/Gemfile.lock index 5ccc6bfff..145cf6683 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,43 +39,50 @@ GIT devise-async (0.10.2) devise (>= 4.0) +GIT + remote: https://github.com/scinote-eln/img2zpl + revision: 23d61cfc3e90ac4caa62dd08546fa0d7590a5140 + specs: + img2zpl (1.0.1) + mini_magick (~> 4.9) + GEM remote: http://rubygems.org/ specs: - actioncable (6.1.6.1) - actionpack (= 6.1.6.1) - activesupport (= 6.1.6.1) + actioncable (6.1.7.1) + actionpack (= 6.1.7.1) + activesupport (= 6.1.7.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.6.1) - actionpack (= 6.1.6.1) - activejob (= 6.1.6.1) - activerecord (= 6.1.6.1) - activestorage (= 6.1.6.1) - activesupport (= 6.1.6.1) + actionmailbox (6.1.7.1) + actionpack (= 6.1.7.1) + activejob (= 6.1.7.1) + activerecord (= 6.1.7.1) + activestorage (= 6.1.7.1) + activesupport (= 6.1.7.1) mail (>= 2.7.1) - actionmailer (6.1.6.1) - actionpack (= 6.1.6.1) - actionview (= 6.1.6.1) - activejob (= 6.1.6.1) - activesupport (= 6.1.6.1) + actionmailer (6.1.7.1) + actionpack (= 6.1.7.1) + actionview (= 6.1.7.1) + activejob (= 6.1.7.1) + activesupport (= 6.1.7.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.6.1) - actionview (= 6.1.6.1) - activesupport (= 6.1.6.1) + actionpack (6.1.7.1) + actionview (= 6.1.7.1) + activesupport (= 6.1.7.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.6.1) - actionpack (= 6.1.6.1) - activerecord (= 6.1.6.1) - activestorage (= 6.1.6.1) - activesupport (= 6.1.6.1) + actiontext (6.1.7.1) + actionpack (= 6.1.7.1) + activerecord (= 6.1.7.1) + activestorage (= 6.1.7.1) + activesupport (= 6.1.7.1) nokogiri (>= 1.8.5) - actionview (6.1.6.1) - activesupport (= 6.1.6.1) + actionview (6.1.7.1) + activesupport (= 6.1.7.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -85,24 +92,24 @@ GEM activemodel (>= 4.1, < 6.2) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.6.1) - activesupport (= 6.1.6.1) + activejob (6.1.7.1) + activesupport (= 6.1.7.1) globalid (>= 0.3.6) - activemodel (6.1.6.1) - activesupport (= 6.1.6.1) - activerecord (6.1.6.1) - activemodel (= 6.1.6.1) - activesupport (= 6.1.6.1) + activemodel (6.1.7.1) + activesupport (= 6.1.7.1) + activerecord (6.1.7.1) + activemodel (= 6.1.7.1) + activesupport (= 6.1.7.1) activerecord-import (1.0.7) activerecord (>= 3.2) - activestorage (6.1.6.1) - actionpack (= 6.1.6.1) - activejob (= 6.1.6.1) - activerecord (= 6.1.6.1) - activesupport (= 6.1.6.1) + activestorage (6.1.7.1) + actionpack (= 6.1.7.1) + activejob (= 6.1.7.1) + activerecord (= 6.1.7.1) + activesupport (= 6.1.7.1) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.6.1) + activesupport (6.1.7.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -245,11 +252,13 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) + date (3.3.3) debug_inspector (1.0.0) - deface (1.6.1) + deface (1.9.0) + actionview (>= 5.2) nokogiri (>= 1.6) polyglot - rails (>= 5.2) + railties (>= 5.2) rainbow (>= 2.1.0) delayed_job (4.1.9) activesupport (>= 3.0, < 6.2) @@ -273,7 +282,7 @@ GEM railties (>= 5) down (5.2.0) addressable (~> 2.5) - erubi (1.10.0) + erubi (1.12.0) et-orbi (1.2.4) tzinfo execjs (2.7.0) @@ -298,17 +307,17 @@ GEM raabro (~> 1.4) generator (0.0.1) gherkin (5.1.0) - globalid (1.0.0) + globalid (1.0.1) activesupport (>= 5.0) graphviz (1.2.1) process-pipeline hammerjs-rails (2.0.8) hashdiff (1.0.1) hashie (5.0.0) - httparty (0.17.3) - mime-types (~> 3.0) + httparty (0.21.0) + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.11.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) i18n-js (3.8.0) i18n (>= 0.6.6) @@ -360,17 +369,20 @@ GEM loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.0.1) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp marcel (1.0.2) method_source (1.0.0) - mime-types (3.3.1) + mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2020.1104) + mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.2) + mini_portile2 (2.8.1) + minitest (5.17.0) momentjs-rails (2.17.1) railties (>= 3.1) msgpack (1.4.2) @@ -382,6 +394,15 @@ GEM coffee-rails (>= 3.2.1) jquery-rails rails (>= 3.2.0) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol newrelic_rpm (6.15.0) nio4r (2.5.8) nokogiri (1.13.10) @@ -441,8 +462,8 @@ GEM puma (5.6.4) nio4r (~> 2.0) raabro (1.4.0) - racc (1.6.1) - rack (2.2.4) + racc (1.6.2) + rack (2.2.6.2) rack-attack (6.4.0) rack (>= 1.0, < 3) rack-cors (1.1.1) @@ -451,20 +472,20 @@ GEM rack rack-test (2.0.2) rack (>= 1.3) - rails (6.1.6.1) - actioncable (= 6.1.6.1) - actionmailbox (= 6.1.6.1) - actionmailer (= 6.1.6.1) - actionpack (= 6.1.6.1) - actiontext (= 6.1.6.1) - actionview (= 6.1.6.1) - activejob (= 6.1.6.1) - activemodel (= 6.1.6.1) - activerecord (= 6.1.6.1) - activestorage (= 6.1.6.1) - activesupport (= 6.1.6.1) + rails (6.1.7.1) + actioncable (= 6.1.7.1) + actionmailbox (= 6.1.7.1) + actionmailer (= 6.1.7.1) + actionpack (= 6.1.7.1) + actiontext (= 6.1.7.1) + actionview (= 6.1.7.1) + activejob (= 6.1.7.1) + activemodel (= 6.1.7.1) + activerecord (= 6.1.7.1) + activestorage (= 6.1.7.1) + activesupport (= 6.1.7.1) bundler (>= 1.15.0) - railties (= 6.1.6.1) + railties (= 6.1.7.1) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -482,13 +503,13 @@ GEM rails (> 3.1) rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (6.1.6.1) - actionpack (= 6.1.6.1) - activesupport (= 6.1.6.1) + railties (6.1.7.1) + actionpack (= 6.1.7.1) + activesupport (= 6.1.7.1) method_source rake (>= 12.2) thor (~> 1.0) - rainbow (3.0.0) + rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.10.4) rb-inotify (0.10.1) @@ -579,9 +600,9 @@ GEM simplecov_json_formatter (0.1.2) spinjs-rails (1.4) rails (>= 3.1) - sprockets (4.1.1) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) + rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) @@ -591,12 +612,11 @@ GEM thor (1.2.1) tilt (2.0.10) timecop (0.9.2) - tinymce-rails (4.9.11) - railties (>= 3.1.1) + timeout (0.3.1) turbolinks (5.1.1) turbolinks-source (~> 5.1) turbolinks-source (5.2.0) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) @@ -624,7 +644,7 @@ GEM wkhtmltopdf-heroku (2.12.5.0) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.0) + zeitwerk (2.6.6) PLATFORMS ruby @@ -657,7 +677,7 @@ DEPENDENCIES caracal cucumber-rails (~> 1.8) database_cleaner - deface (~> 1.0) + deface (~> 1.9) delayed_job_active_record devise (~> 4.7.1) devise-async! @@ -671,9 +691,10 @@ DEPENDENCIES figaro graphviz hammerjs-rails - httparty (~> 0.17.3) + httparty (~> 0.21.0) i18n-js (~> 3.6) image_processing (~> 1.12) + img2zpl! jbuilder jquery-rails jquery-scrollto-rails! @@ -731,7 +752,6 @@ DEPENDENCIES sneaky-save! spinjs-rails timecop - tinymce-rails (~> 4.9.10) turbolinks (~> 5.1.1) tzinfo-data uglifier (>= 1.3.0) diff --git a/LICENSE.txt b/LICENSE.txt index c37f006e0..5036ffad9 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2016 BioSistemika USA, LLC +Copyright (c) 2016 SciNote, LLC SciNote is licensed under the following license: @@ -374,4 +374,4 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. \ No newline at end of file + defined by the Mozilla Public License, v. 2.0. diff --git a/VERSION b/VERSION index ea0928ced..8fe00a57f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.26.4 +1.26.5 diff --git a/app/assets/images/canvas/full_zoom.svg b/app/assets/images/canvas/full_zoom.svg new file mode 100644 index 000000000..aeb69784f --- /dev/null +++ b/app/assets/images/canvas/full_zoom.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/canvas/medium_zoom.svg b/app/assets/images/canvas/medium_zoom.svg new file mode 100644 index 000000000..a129eb6ac --- /dev/null +++ b/app/assets/images/canvas/medium_zoom.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/canvas/small_zoom.svg b/app/assets/images/canvas/small_zoom.svg new file mode 100644 index 000000000..a1eb3e807 --- /dev/null +++ b/app/assets/images/canvas/small_zoom.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 617f8f195..3ca820fe7 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -1,21 +1,6 @@ // turbolinks MUST BE THE LAST inclusion -//= require jquery -//= require jquery_ujs -//= require jquery.mousewheel.min -//= require jquery.scrollTo -//= require jquery.autosize -//= require jquery-ui/widget -//= require jquery-ui/widgets/mouse -//= require jquery-ui/widgets/draggable -//= require jquery-ui/widgets/droppable -//= require jquery.ui.touch-punch.min -//= require jquery-ui/effects/effect-slide -//= require jquery.caret.min -//= require jquery.atwho.min //= require hammer //= require js.cookie -//= require spin -//= require jquery.spin //= require bootstrap-sprockets //= require moment //= require bootstrap-datetimepicker @@ -25,8 +10,6 @@ //= require typeahead.bundle.min //= require nested_form_fields //= require highlight.pack -//= require tinymce-jquery -//= require_tree ./tinymce/plugins //= require jsPlumb-2.0.4-min //= require jsnetworkx //= require bootstrap-select @@ -257,9 +240,6 @@ var HelperModule = (function(){ $(document).on('turbolinks:load', function() { /* Fix .selectpicker (bootstrap-select) to work with Turbolinks 5.x */ $(window).trigger('load.bs.select.data-api'); - - /* Clean up TinyMCE */ - tinymce.remove(); }); // Show warning if page has unsaved data diff --git a/app/assets/javascripts/experiments/show.js b/app/assets/javascripts/experiments/show.js new file mode 100644 index 000000000..32765e5a4 --- /dev/null +++ b/app/assets/javascripts/experiments/show.js @@ -0,0 +1,129 @@ +/* global dropdownSelector initBSTooltips I18n */ + +(function() { + function initNewMyModuleModal() { + let experimentWrapper = '.experiment-new-my_module'; + let newMyModuleModal = '#new-my-module-modal'; + let myModuleUserSelector = '#my_module_user_ids'; + var myModuleTagsSelector = '#module-tags-selector'; + + + // Modal's submit handler function + $(experimentWrapper) + .on('ajax:success', newMyModuleModal, function() { + $(this).find('sci-input-container').removeClass('error'); + $(newMyModuleModal).modal('hide'); + }) + .on('ajax:error', newMyModuleModal, function(ev, data) { + let errors = data.responseJSON; + $(this).find('sci-input-container').removeClass('error'); + if (errors.name) { + $(this).find('#my_module_name') + .parent() + .addClass('error') + .attr('data-error-text', errors.name.join(', ')); + } + }) + .on('submit', newMyModuleModal, function() { + // To submit correct assigned user ids to new modal + // Clear default selected user in dropdown + $(`${myModuleUserSelector} option[value=${$('#new-my-module-modal').data('user-id')}]`) + .prop('selected', false); + $.map(dropdownSelector.getValues(myModuleUserSelector), function(val) { + $(`${myModuleUserSelector} option[value=${val}]`).prop('selected', true); + }); + }) + .on('ajax:success', '.new-my-module-button', function(ev, result) { + // Add and show modal + $(experimentWrapper).append($.parseHTML(result.html)); + $(newMyModuleModal).modal('show'); + $(newMyModuleModal).find("input[type='text']").focus(); + + // Remove modal when it gets closed + $(newMyModuleModal).on('hidden.bs.modal', function() { + $(newMyModuleModal).remove(); + }); + + // initiaize user assing dropdown menu + dropdownSelector.init(myModuleUserSelector, { + closeOnSelect: true, + labelHTML: true, + tagClass: 'my-module-user-tags', + tagLabel: (data) => { + return `${data.label} + ${data.label}`; + }, + optionLabel: (data) => { + if (data.params.avatar_url) { + return ` + ${data.label} + ${data.label}`; + } + + return data.label; + } + }); + + dropdownSelector.selectValues(myModuleUserSelector, $('#new-my-module-modal').data('user-id')); + + dropdownSelector.init($(myModuleTagsSelector), { + closeOnSelect: true, + tagClass: 'my-module-white-tags', + tagStyle: (data) => { + return `background: ${data.params.color}`; + }, + customDropdownIcon: () => { + return ''; + }, + optionLabel: (data) => { + if (data.value > 0) { + return ` + ${data.label}`; + } + return ` + ${data.label + ' '} + ${I18n.t('my_modules.details.create_new_tag')}`; + }, + ajaxParams: function(params) { + let newParams = params; + newParams.selected_tags = JSON.stringify(dropdownSelector.getValues(myModuleTagsSelector)); + return newParams; + }, + onSelect: function() { + var selectElement = $(myModuleTagsSelector); + var lastTag = selectElement.next().find('.ds-tags').last(); + var lastTagId = lastTag.find('.tag-label').data('ds-tag-id'); + + if (lastTagId > 0) { + $('#my_module_tag_ids').val(JSON.stringify(dropdownSelector.getValues(myModuleTagsSelector))); + } else { + let newTag = { + tag: { + name: lastTag.find('.tag-label').html(), + project_id: selectElement.data('project-id'), + color: null + }, + simple_creation: true + }; + $.post(selectElement.data('tags-create-url'), newTag, function(res) { + dropdownSelector.removeValue(myModuleTagsSelector, 0, '', true); + dropdownSelector.addValue(myModuleTagsSelector, { + value: res.tag.id, + label: res.tag.name, + params: { + color: res.tag.color + } + }, true); + $('#my_module_tag_ids').val(JSON.stringify(dropdownSelector.getValues(myModuleTagsSelector))); + }).fail(function() { + dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true); + }); + } + } + }); + }); + initBSTooltips(); + } + + initNewMyModuleModal(); +}()); diff --git a/app/assets/javascripts/experiments/table.js b/app/assets/javascripts/experiments/table.js new file mode 100644 index 000000000..e47606f88 --- /dev/null +++ b/app/assets/javascripts/experiments/table.js @@ -0,0 +1,832 @@ +/* global I18n GLOBAL_CONSTANTS InfiniteScroll + initBSTooltips filterDropdown dropdownSelector Sidebar HelperModule notTurbolinksPreview */ + +var ExperimnetTable = { + permissions: ['editable', 'archivable', 'restorable', 'moveable'], + selectedId: [], + table: '.experiment-table', + render: {}, + selectedMyModules: [], + activeFilters: {}, + filters: [], // Filter {name: '', init(), closeFilter(), apply(), active(), clearFilter()} + myModulesCurrentSort: '', + pageSize: GLOBAL_CONSTANTS.DEFAULT_ELEMENTS_PER_PAGE, + provisioningStatusTimeout: '', + getUrls: function(id) { + return $(`.table-row[data-id="${id}"]`).data('urls'); + }, + loadPlaceholder: function() { + let placeholder = ''; + $.each(Array(this.pageSize), function() { + placeholder += $('#experimentTablePlaceholder').html(); + }); + $(placeholder).insertAfter($(this.table).find('.table-body')); + }, + appendRows: function(result) { + $.each(result, (_j, data) => { + let row; + const isProvisioning = data.provisioning_status === 'in_progress'; + const provisioningTooltipAttrs = `title="${I18n.t('experiments.duplicate_tasks.duplicating')}" + data-toggle="tooltip"`; + + // Checkbox selector + row = ` +
+
+
+ + +
+
`; + + // Task columns + $.each(data.columns, (_i, cell) => { + let hidden = ''; + + if ($(`.table-display-modal .fa-eye-slash[data-column="${cell.column_type}"]`).length === 1) { + hidden = 'hidden'; + } + + row += ` +
+ ${ExperimnetTable.render[cell.column_type](cell.data)} +
+ `; + }); + // Menu + row += ` +
+ +
`; + + let tableRowClass = `table-row ${isProvisioning ? 'table-row-provisioning' : ''}`; + $(`
${row}
`) + .appendTo(`${this.table} .table-body`); + }); + }, + initDueDatePicker: function(data) { + // eslint-disable-next-line no-unused-vars + $.each(data, (_, row) => { + let element = `#calendarDueDate${row.id}`; + let dueDateContainer = $(element).closest('#dueDateContainer'); + let dateText = $(element).closest('.date-text'); + let clearDate = $(element).closest('.datetime-container').find('.clear-date'); + + $(element).on('dp.change', function() { + $.ajax({ + url: dueDateContainer.data('update-url'), + type: 'PATCH', + dataType: 'json', + data: { my_module: { due_date: $(element).val() } }, + success: function(result) { + dueDateContainer.find('#dueDateLabelContainer').html(result.table_due_date_label.html); + dateText.data('due-status', result.table_due_date_label.due_status); + + if ($(result.table_due_date_label.html).data('due-date')) { + clearDate.addClass('open'); + } + } + }); + }); + + $(element).on('dp.hide', function() { + dateText.attr('data-original-title', dateText.data('due-status')); + clearDate.removeClass('open'); + }); + + $(element).on('dp.show', function() { + var datePicker = $('.bootstrap-datetimepicker-widget.dropdown-menu')[0]; + + // show full datepicker menu for due date + if (datePicker.getBoundingClientRect().bottom > window.innerHeight) { + datePicker.scrollIntoView(false); + } else if (datePicker.getBoundingClientRect().top < 0) { + datePicker.scrollIntoView(); + } + + dateText.attr('data-original-title', '').tooltip('hide'); + if (dueDateContainer.find('.due-date-label').data('due-date')) { + clearDate.addClass('open'); + } + }); + }); + }, + initMyModuleActions: function() { + $(this.table).on('show.bs.dropdown', '.my-module-menu', (e) => { + let menu = $(e.target).find('.dropdown-menu'); + $.get(e.currentTarget.dataset.url, (result) => { + $(menu).find('li').remove(); + $(result.html).appendTo(menu); + }); + }); + + $(this.table).on('click', '.archive-my-module', (e) => { + e.preventDefault(); + this.archiveMyModules(e.currentTarget.href, e.currentTarget.dataset.id); + }); + + + $(this.table).on('click', '.restore-my-module', (e) => { + e.preventDefault(); + this.restoreMyModules(e.currentTarget.href, e.currentTarget.dataset.id); + }); + + $(this.table).on('click', '.duplicate-my-module', (e) => { + e.preventDefault(); + this.duplicateMyModules($('#duplicateTasks').data('url'), e.currentTarget.dataset.id); + }); + + $(this.table).on('click', '.move-my-module', (e) => { + e.preventDefault(); + this.openMoveModulesModal([e.currentTarget.dataset.id]); + }); + + $(this.table).on('click', '.edit-my-module', (e) => { + e.preventDefault(); + $('#modal-edit-module').modal('show'); + $('#modal-edit-module').data('id', e.currentTarget.dataset.id); + $('#edit-module-name-input').val($(`#taskName${$('#modal-edit-module').data('id')}`).data('full-name')); + }); + }, + initDuplicateMyModules: function() { + $('#duplicateTasks').on('click', (e) => { + this.duplicateMyModules(e.currentTarget.dataset.url, this.selectedMyModules); + }); + }, + duplicateMyModules: function(url, ids) { + $.post(url, { my_module_ids: ids }, () => { + this.loadTable(); + }).error((data) => { + HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); + }); + }, + initArchiveMyModules: function() { + $('#archiveTask').on('click', (e) => { + this.archiveMyModules(e.currentTarget.dataset.url, this.selectedMyModules); + }); + }, + archiveMyModules: function(url, ids) { + $.post(url, { my_modules: ids }, (data) => { + HelperModule.flashAlertMsg(data.message, 'success'); + this.loadTable(); + }).error((data) => { + HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); + }); + }, + initRestoreMyModules: function() { + $('#restoreTask').on('click', (e) => { + this.restoreMyModules(e.currentTarget.dataset.url, this.selectedMyModules); + }); + }, + restoreMyModules: function(url, ids) { + $.post(url, { my_modules_ids: ids, view: 'table' }); + }, + initRenameModal: function() { + $('#editTask').on('click', () => { + $('#modal-edit-module').modal('show'); + $('#modal-edit-module').data('id', this.selectedMyModules[0]); + $('#edit-module-name-input').val($(`#taskName${$('#modal-edit-module').data('id')}`).data('full-name')); + }); + + const handleRenameModal = () => { + let id = $('#modal-edit-module').data('id'); + let newValue = $('#edit-module-name-input').val(); + + $(`.my-module-selector[data-my-module="${id}"]`).trigger('click'); + + if (newValue === $(`#taskName${id}`).data('full-name')) { + $('#modal-edit-module').modal('hide'); + return false; + } + $.ajax({ + url: this.getUrls(id).name_update, + type: 'PATCH', + dataType: 'json', + data: { my_module: { name: $('#edit-module-name-input').val() } }, + success: () => { + $(`#taskName${id}`).text(newValue); + $(`#taskName${id}`).data('full-name', newValue); + $('#edit-module-name-input').closest('.sci-input-container').removeClass('error'); + $('#modal-edit-module').modal('hide'); + }, + error: function(response) { + let error = response.responseJSON.name.join(', '); + $('#edit-module-name-input') + .closest('.sci-input-container') + .addClass('error') + .attr('data-error-text', error); + } + }); + + if ($(`.my-module-selector[data-my-module="${id}"]`).prop('checked')) { + $(`.my-module-selector[data-my-module="${id}"]`).trigger('click'); + } + + this.clearRowTaskSelection(); + + return true; + }; + + $('#modal-edit-module') + .on('click', 'button[data-action="confirm"]', handleRenameModal) + .on('submit', 'form', (e) => { + e.preventDefault(); + handleRenameModal(); + }); + }, + initManageUsersDropdown: function() { + $(this.table).on('show.bs.dropdown', '.assign-users-dropdown', (e) => { + let usersList = $(e.target).find('.users-list'); + let isArchivedView = $('#experimentTable').hasClass('archived'); + let viewOnly = $(e.target).data('view-only'); + let checkbox = ''; + usersList.find('.user-container').remove(); + $.get(usersList.data('list-url'), (result) => { + $.each(result, (_i, user) => { + if (!isArchivedView && !viewOnly) { + checkbox = `
+ + +
`; + } + + $(` +
+ ${checkbox} +
+ +
+
+ ${user.label} +
+
+ `).appendTo(usersList); + }); + }); + }); + $(this.table).on('click', '.assign-users-dropdown .dropdown-menu', (e) => { + if (e.target.tagName === 'INPUT') return; + e.preventDefault(); + e.stopPropagation(); + }); + $(this.table).on('keyup', '.assigned-users-container, .open-my-module-menu, .calendar-input', (e) => { + if (e.keyCode === 13) { // Enter + e.currentTarget.click(); + } + }); + $(this.table).on('change keyup', '.assign-users-dropdown .user-search', (e) => { + let query = e.currentTarget.value; + let usersList = $(e.target).closest('.dropdown-menu').find('.user-container'); + $.each(usersList, (_i, user) => { + $(user).removeClass('hidden'); + if (query.length && !$(user).find('.user-name').text().toLowerCase() + .includes(query.toLowerCase())) { + $(user).addClass('hidden'); + } + }); + }); + $(this.table).on('change', '.assign-users-dropdown .user-selector', (e) => { + let checkbox = e.target; + if (checkbox.checked) { + $.post(checkbox.dataset.assignUrl, { + table: true, + user_my_module: { + my_module_id: $(checkbox).closest('.table-row').data('id'), + user_id: checkbox.value + } + }, (result) => { + checkbox.dataset.unassignUrl = result.unassign_url; + $(checkbox).closest('.table-row').find('.assigned-users-container') + .replaceWith($(result.html).find('.assigned-users-container')); + }).error((data) => { + HelperModule.flashAlertMsg(data.responseJSON.errors, 'danger'); + }); + } else { + $.ajax({ + url: checkbox.dataset.unassignUrl, + method: 'DELETE', + data: { table: true }, + success: (result) => { + $(checkbox).closest('.table-row').find('.assigned-users-container') + .replaceWith($(result.html).find('.assigned-users-container')); + }, + error: (data) => { + HelperModule.flashAlertMsg(data.responseJSON.errors, 'danger'); + } + }); + } + }); + }, + initMoveModulesModal: function() { + $('#moveTask').on('click', () => { + this.openMoveModulesModal(this.selectedMyModules); + }); + }, + openMoveModulesModal: function(ids) { + let table = $(this.table); + $.get(table.data('move-modules-modal-url'), (modalData) => { + if ($('#modal-move-modules').length > 0) { + $('#modal-move-modules').replaceWith(modalData.html); + } else { + $('#experimentTable').append(modalData.html); + } + $('#modal-move-modules').on('shown.bs.modal', function() { + $(this).find('.selectpicker').selectpicker().focus(); + }); + $('#modal-move-modules').on('click', 'button[data-action="confirm"]', () => { + let moveParams = { + to_experiment_id: $('#modal-move-modules').find('.selectpicker').val(), + my_module_ids: ids + }; + $.post(table.data('move-modules-url'), moveParams, (data) => { + HelperModule.flashAlertMsg(data.message, 'success'); + this.loadTable(); + }).error((data) => { + HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); + }); + $('#modal-move-modules').modal('hide'); + }); + $('#modal-move-modules').modal('show'); + }); + }, + checkActionPermission: function(permission) { + let allMyModules; + + allMyModules = this.selectedMyModules.every((id) => { + return $(`.table-row[data-id="${id}"]`).data(permission); + }); + + return allMyModules; + }, + initSelectAllCheckbox: function() { + $(this.table).on('click', '.select-all-checkboxes .sci-checkbox', (e1) => { + $.each($('.my-module-selector'), (_i, e2) => { + if (e1.target.checked !== e2.checked) e2.click(); + }); + }); + }, + loadPermission: function(id) { + let row = $(`.table-row[data-id="${id}"]`); + $.get(this.getUrls(id).permissions, (result) => { + this.permissions.forEach((permission) => { + row.data(permission, result[permission]); + }); + this.updateExperimentToolbar(); + }); + }, + initSelector: function() { + $(this.table).on('click', '.my-module-selector', (e) => { + let checkbox = e.target; + let myModuleId = checkbox.dataset.myModule; + let index = $.inArray(myModuleId, this.selectedMyModules); + + // If checkbox is checked and row ID is not in list of selected project IDs + if (checkbox.checked && index === -1) { + $(checkbox).closest('.table-row').addClass('selected'); + this.selectedMyModules.push(myModuleId); + // Otherwise, if checkbox is not checked and ID is in list of selected IDs + } else if (!this.checked && index !== -1) { + $(checkbox).closest('.table-row').removeClass('selected'); + this.selectedMyModules.splice(index, 1); + } + + if (checkbox.checked) { + this.loadPermission(myModuleId); + } else { + this.updateExperimentToolbar(); + } + }); + }, + updateExperimentToolbar: function() { + let experimentToolbar = $('.toolbar-row'); + + if (this.selectedMyModules.length === 0) { + experimentToolbar.find('.single-object-action, .multiple-object-action').addClass('hidden'); + } else if (this.selectedMyModules.length === 1) { + experimentToolbar.find('.single-object-action, .multiple-object-action').removeClass('hidden'); + } else { + experimentToolbar.find('.single-object-action').addClass('hidden'); + experimentToolbar.find('.multiple-object-action').removeClass('hidden'); + } + + this.permissions.forEach((permission) => { + if (!this.checkActionPermission(permission)) { + experimentToolbar.find(`.btn[data-for="${permission}"]`).addClass('hidden'); + } + }); + + if ($('#experimentTable').hasClass('archived')) { + experimentToolbar.find('.only-active').addClass('hidden'); + } + }, + selectDate: function($field) { + var datePicker = $field.data('DateTimePicker'); + if (datePicker && datePicker.date()) { + return datePicker.date()._d.toUTCString(); + } + return null; + }, + initManageColumnsModal: function() { + $.each($('.table-display-modal .fa-eye-slash'), (_i, column) => { + $(column).parent().removeClass('visible'); + }); + $('.experiment-table')[0].style + .setProperty('--columns-count', $('.table-display-modal .fa-eye:not(.disabled)').length + 1); + + $('.table-display-modal').on('click', '.column-container .fas', (e) => { + let icon = $(e.target); + if (icon.hasClass('fa-eye')) { + $(`.experiment-table .${icon.data('column')}-column`).addClass('hidden'); + icon.removeClass('fa-eye').addClass('fa-eye-slash'); + icon.parent().removeClass('visible'); + } else { + $(`.experiment-table .${icon.data('column')}-column`).removeClass('hidden'); + icon.addClass('fa-eye').removeClass('fa-eye-slash'); + icon.parent().addClass('visible'); + } + + let visibleColumns = $('.table-display-modal .fa-eye').map((_i, col) => col.dataset.column).toArray(); + // Update columns on backend - $.post('', { columns: visibleColumns }, () => {}); + $.post($('.table-display-modal').data('column-state-url'), { columns: visibleColumns }, () => {}); + + $('.experiment-table')[0].style + .setProperty('--columns-count', $('.table-display-modal .fa-eye:not(.disabled)').length + 1); + }); + }, + clearRowTaskSelection: function() { + this.selectedMyModules = []; + $('.select-all-checkboxes .sci-checkbox').prop('checked', false); + this.updateExperimentToolbar(); + }, + initNewTaskModal: function(table) { + $('.experiment-new-my_module').on('ajax:success', '#new-my-module-modal', function() { + table.loadTable(); + }); + }, + initSorting: function(table) { + $('#sortMenuDropdown a').click(function() { + if (table.myModulesCurrentSort !== $(this).data('sort')) { + $('#sortMenuDropdown a').removeClass('selected'); + // eslint-disable-next-line no-param-reassign + table.myModulesCurrentSort = $(this).data('sort'); + table.loadTable(); + $(this).addClass('selected'); + $('#sortMenu').dropdown('toggle'); + } + }); + }, + initFilters: function() { + this.filterDropdown = filterDropdown.init(); + let $experimentFilter = $('#experimentTable .my-modules-filters'); + + $.each(this.filters, (_i, filter) => { + filter.init($experimentFilter); + }); + + this.filterDropdown.on('filter:apply', () => { + $.each(this.filters, (_i, filter) => { + this.activeFilters[filter.name] = filter.apply($experimentFilter); + }); + + // filters are active if they have any non-empty value + let filtersEmpty = Object.values(this.activeFilters).every(value => /^\s+$/.test(value) || value === null || value === undefined || value && value.length === 0); + this.filtersActive = !filtersEmpty; + + filterDropdown.toggleFilterMark( + this.filterDropdown, + this.filters.some((filter) => { + return filter.active(this.activeFilters[filter.name]); + }) + ); + + this.loadTable(); + }); + + this.filterDropdown.on('filter:clickBody', () => { + $.each(this.filters, (_i, filter) => { + filter.closeFilter($experimentFilter); + }); + }); + + this.filterDropdown.on('filter:clear', () => { + $.each(this.filters, (_i, filter) => { + filter.clearFilter($experimentFilter); + }); + }); + }, + loadTable: function() { + var tableParams = { + filters: this.activeFilters, + sort: this.myModulesCurrentSort + }; + var dataUrl = $(this.table).data('my-modules-url'); + $(this.table).find('.table-row').remove(); + this.loadPlaceholder(); + + Sidebar.reload({ + sort: this.myModulesCurrentSort, + view_mode: $('#experimentTable').hasClass('archived') ? 'archived' : '' + }); + + $.get(dataUrl, tableParams, (result) => { + $(this.table).find('.table-row-placeholder, .table-row-placeholder-divider').remove(); + this.appendRows(result.data); + this.initDueDatePicker(result.data); + this.handleNoResults(); + + InfiniteScroll.init(this.table, { + url: dataUrl, + eventTarget: window, + placeholderTemplate: '#experimentTablePlaceholder', + endOfListTemplate: '#experimentTableEndOfList', + pageSize: this.pageSize, + lastPage: !result.next_page, + customResponse: (response) => { + this.appendRows(response.data); + this.initDueDatePicker(response.data); + this.initProvisioningStatusPolling(); + }, + customParams: (params) => { + return { ...params, ...tableParams }; + } + }); + + initBSTooltips(); + this.clearRowTaskSelection(); + this.initProvisioningStatusPolling(); + }); + }, + initProvisioningStatusPolling: function() { + let provisioningStatusUrls = $('.table-row-provisioning').toArray() + .map((u) => $(u).data('urls').provisioning_status) + .filter((u) => !!u); + + this.provisioningMyModulesCount = provisioningStatusUrls.length; + + if (this.provisioningMyModulesCount > 0) this.pollProvisioningStatuses(provisioningStatusUrls); + }, + handleNoResults: function() { + let tableRowLength = document.getElementsByClassName('table-row').length; + let noResultsContainer = document.getElementById('tasksNoResultsContainer'); + if (this.filtersActive && tableRowLength === 0) { + noResultsContainer.style.display = 'block'; + } else { + noResultsContainer.style.display = 'none'; + } + }, + pollProvisioningStatuses: function(provisioningStatusUrls) { + let remainingUrls = []; + + provisioningStatusUrls.forEach((url) => { + jQuery.ajax({ + url: url, + success: (data) => { + if (data.provisioning_status === 'in_progress') remainingUrls.push(url); + }, + async: false + }); + }); + + if (remainingUrls.length > 0) { + clearTimeout(this.provisioningStatusTimeout); + this.provisioningStatusTimeout = setTimeout(() => { + this.pollProvisioningStatuses(remainingUrls); + }, 10000); + } else { + HelperModule.flashAlertMsg( + I18n.t('experiments.duplicate_tasks.success', { count: this.provisioningMyModulesCount }), + 'success' + ); + this.loadTable(); + } + }, + init: function() { + this.initSelector(); + this.initSelectAllCheckbox(); + this.initFilters(); + this.initSorting(this); + this.loadTable(); + this.initRenameModal(); + this.initDuplicateMyModules(); + this.initMoveModulesModal(); + this.initArchiveMyModules(); + this.initManageColumnsModal(); + this.initNewTaskModal(this); + this.initMyModuleActions(); + this.initRestoreMyModules(); + this.initManageUsersDropdown(); + } +}; + +ExperimnetTable.render.task_name = function(data) { + let tooltip = ` title="${data.name}" data-toggle="tooltip" data-placement="bottom"`; + if (data.provisioning_status === 'in_progress') { + return `${data.name}`; + } + + return `${data.name}`; +}; + +ExperimnetTable.render.id = function(data) { + return ` +
${data.id}
+ `; +}; + +ExperimnetTable.render.due_date = function(data) { + return data.data; +}; + +ExperimnetTable.render.archived = function(data) { + return data; +}; + +ExperimnetTable.render.age = function(data) { + return data; +}; + +ExperimnetTable.render.results = function(data) { + return `${data.count}`; +}; + +ExperimnetTable.render.status = function(data) { + return `
${data.name}
`; +}; + +ExperimnetTable.render.assigned = function(data) { + return data.html; +}; + +ExperimnetTable.render.tags = function(data) { + const value = parseInt(data.tags, 10) === 0 ? I18n.t('experiments.table.add_tag') : data.tags; + + if (data.tags === 0 && !data.can_create) { + return `${I18n.t('experiments.table.not_set')}`; + } + + return `${value}`; +}; + +ExperimnetTable.render.comments = function(data) { + if (data.count === 0 && !data.can_create) return '0'; + return ` + ${data.count > 0 ? data.count : '+'} + ${data.count_unseen > 0 ? ` ${data.count_unseen} ` : ''} + `; +}; + +// Filters + +ExperimnetTable.filters.push({ + name: 'name', + init: () => {}, + closeFilter: ($container) => { + $('#textSearchFilterHistory').hide(); + $('#textSearchFilterInput', $container).closest('.dropdown').removeClass('open'); + }, + apply: ($container) => { + return $('#textSearchFilterInput', $container).val(); + }, + active: (value) => { return value; }, + clearFilter: ($container) => { + $('#textSearchFilterInput', $container).val(''); + } +}); + +ExperimnetTable.filters.push({ + name: 'due_date_from', + init: () => {}, + closeFilter: () => {}, + apply: ($container) => { + return ExperimnetTable.selectDate($('.due-date-filter .from-date', $container)); + }, + active: (value) => { return value; }, + clearFilter: ($container) => { + if ($('.due-date-filter .from-date', $container).data('DateTimePicker')) { + $('.due-date-filter .from-date', $container).data('DateTimePicker').clear(); + } + } +}); + +ExperimnetTable.filters.push({ + name: 'due_date_to', + init: () => {}, + closeFilter: () => {}, + apply: ($container) => { + return ExperimnetTable.selectDate($('.due-date-filter .to-date', $container)); + }, + active: (value) => { return value; }, + clearFilter: ($container) => { + if ($('.due-date-filter .to-date', $container).data('DateTimePicker')) { + $('.due-date-filter .to-date', $container).data('DateTimePicker').clear(); + } + } +}); + +ExperimnetTable.filters.push({ + name: 'archived_on_from', + init: () => {}, + closeFilter: () => {}, + apply: ($container) => { + return ExperimnetTable.selectDate($('.archived-on-filter .from-date', $container)); + }, + active: (value) => { return value; }, + clearFilter: ($container) => { + if ($('.archived-on-filter .from-date', $container).data('DateTimePicker')) { + $('.archived-on-filter .from-date', $container).data('DateTimePicker').clear(); + } + } +}); + +ExperimnetTable.filters.push({ + name: 'archived_on_to', + init: () => {}, + closeFilter: () => {}, + apply: ($container) => { + return ExperimnetTable.selectDate($('.archived-on-filter .to-date', $container)); + }, + active: (value) => { return value; }, + clearFilter: ($container) => { + if ($('.archived-on-filter .to-date', $container).data('DateTimePicker')) { + $('.archived-on-filter .to-date', $container).data('DateTimePicker').clear(); + } + } +}); + +ExperimnetTable.filters.push({ + name: 'assigned_users', + init: ($container) => { + dropdownSelector.init($('.assigned-filter', $container), { + optionClass: 'checkbox-icon users-dropdown-list', + optionLabel: (data) => { + return ` ${data.label}`; + }, + tagLabel: (data) => { + return ` ${data.label}`; + }, + labelHTML: true, + tagClass: 'users-dropdown-list' + }); + }, + closeFilter: ($container) => { + dropdownSelector.closeDropdown($('.assigned-filter', $container)); + }, + apply: ($container) => { + return dropdownSelector.getValues($('.assigned-filter', $container)); + }, + active: (value) => { return value && value.length !== 0; }, + clearFilter: ($container) => { + dropdownSelector.clearData($('.assigned-filter', $container)); + } +}); + +ExperimnetTable.filters.push({ + name: 'statuses', + init: ($container) => { + dropdownSelector.init($('.status-filter', $container), { + singleSelect: true, + closeOnSelect: true, + selectAppearance: 'simple' + }); + }, + closeFilter: ($container) => { + dropdownSelector.closeDropdown($('.status-filter', $container)); + }, + apply: ($container) => { + return dropdownSelector.getValues($('.status-filter', $container)); + }, + active: (value) => { return value && value.length !== 0; }, + clearFilter: ($container) => { + dropdownSelector.clearData($('.status-filter', $container)); + } +}); + +if (notTurbolinksPreview()) { + ExperimnetTable.init(); +} diff --git a/app/assets/javascripts/jquery_bundle.js b/app/assets/javascripts/jquery_bundle.js new file mode 100644 index 000000000..895fec7e1 --- /dev/null +++ b/app/assets/javascripts/jquery_bundle.js @@ -0,0 +1,15 @@ +//= require jquery +//= require jquery_ujs +//= require jquery.mousewheel.min +//= require jquery.scrollTo +//= require jquery.autosize +//= require jquery-ui/widget +//= require jquery-ui/widgets/mouse +//= require jquery-ui/widgets/draggable +//= require jquery-ui/widgets/droppable +//= require jquery.ui.touch-punch.min +//= require jquery-ui/effects/effect-slide +//= require jquery.caret.min +//= require jquery.atwho.min +//= require spin +//= require jquery.spin diff --git a/app/assets/javascripts/my_modules.js b/app/assets/javascripts/my_modules.js index 34fc4c5cc..f99afb8f0 100644 --- a/app/assets/javascripts/my_modules.js +++ b/app/assets/javascripts/my_modules.js @@ -89,155 +89,6 @@ }); } - - // Bind ajax for editing tags - function bindEditTagsAjax() { - var manageTagsModal = null; - var manageTagsModalBody = null; - - // Initialize reloading of manage tags modal content after posting new - // tag. - function initAddTagForm() { - manageTagsModalBody.find('.add-tag-form') - .submit(function() { - var selectOptions = manageTagsModalBody.find('#new_my_module_tag .dropdown-menu li').length; - if (selectOptions === 0 && this.id === 'new_my_module_tag') return false; - return true; - }) - .on('ajax:success', function(e, data) { - var newTag; - initTagsModalBody(data); - newTag = $('#manage-module-tags-modal .list-group-item').last(); - dropdownSelector.addValue('#module-tags-selector', { - value: newTag.data('tag-id'), - label: newTag.data('name'), - params: { - color: newTag.data('color') - } - }, true); - }); - } - - // Initialize edit tag & remove tag functionality from my_module links. - function initTagRowLinks() { - manageTagsModalBody.find('.edit-tag-link') - .on('click', function() { - var $this = $(this); - var li = $this.parents('li.list-group-item'); - var editDiv = $(li.find('div.tag-edit')); - - // Revert all rows to their original states - manageTagsModalBody.find('li.list-group-item').each(function() { - var li2 = $(this); - li2.css('background-color', li2.data('color')); - li2.find('.edit-tag-form').clearFormErrors(); - li2.find('input[type=text]').val(li2.data('name')); - }); - - // Hide all other edit divs, show all show divs - manageTagsModalBody.find('div.tag-edit').hide(); - manageTagsModalBody.find('div.tag-show').show(); - - editDiv.find('input[type=text]').val(li.data('name')); - editDiv.find('.edit-tag-color').colorselector('setColor', li.data('color')); - - li.find('div.tag-show').hide(); - editDiv.show(); - }); - manageTagsModalBody.find('div.tag-edit .dropdown-colorselector > .dropdown-menu li a') - .on('click', function() { - // Change background of the
  • - var $this = $(this); - var li = $this.parents('li.list-group-item'); - li.css('background-color', $this.data('value')); - }); - manageTagsModalBody.find('.remove-tag-link') - .on('ajax:success', function(e, data) { - dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true); - initTagsModalBody(data); - }); - manageTagsModalBody.find('.delete-tag-form') - .on('ajax:success', function(e, data) { - dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true); - initTagsModalBody(data); - }); - manageTagsModalBody.find('.edit-tag-form') - .on('ajax:success', function(e, data) { - var newTag; - initTagsModalBody(data); - dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true); - newTag = $('#manage-module-tags-modal .list-group-item[data-tag-id=' + this.dataset.tagId + ']'); - dropdownSelector.addValue('#module-tags-selector', { - value: newTag.data('tag-id'), - label: newTag.data('name'), - params: { - color: newTag.data('color') - } - }, true); - }) - .on('ajax:error', function(e, data) { - $(this).renderFormErrors('tag', data.responseJSON); - }); - manageTagsModalBody.find('.cancel-tag-link') - .on('click', function() { - var $this = $(this); - var li = $this.parents('li.list-group-item'); - - li.css('background-color', li.data('color')); - li.find('.edit-tag-form').clearFormErrors(); - - li.find('div.tag-edit').hide(); - li.find('div.tag-show').show(); - }); - } - - // Initialize ajax listeners and elements style on modal body. This - // function must be called when modal body is changed. - function initTagsModalBody(data) { - manageTagsModalBody.html(data.html); - manageTagsModalBody.find('.selectpicker').selectpicker(); - initAddTagForm(); - initTagRowLinks(); - } - - manageTagsModal = $('#manage-module-tags-modal'); - manageTagsModalBody = manageTagsModal.find('.modal-body'); - - // Reload tags HTML element when modal is closed - manageTagsModal.on('hide.bs.modal', function() { - var tagsEl = $('#module-tags'); - - // Load HTML - $.ajax({ - url: tagsEl.attr('data-module-tags-url'), - type: 'GET', - dataType: 'json', - success: function(data) { - var newOptions = $(data.html_module_header).find('option'); - $('#module-tags-selector').find('option').remove(); - $(newOptions).appendTo('#module-tags-selector').change(); - }, - error: function() { - // TODO - } - }); - }); - - // Remove modal content when modal window is closed. - manageTagsModal.on('hidden.bs.modal', function() { - manageTagsModalBody.html(''); - }); - // initialize my_module tab remote loading - $('.edit-tags-link') - .on('ajax:before', function() { - manageTagsModal.modal('show'); - }) - .on('ajax:success', function(e, data) { - $('#manage-module-tags-modal-module').text(data.my_module.name); - initTagsModalBody(data); - }); - } - function checkStatusState() { $.getJSON($('.status-flow-dropdown').data('status-check-url'), (statusData) => { if (statusData.status_changing) { @@ -297,9 +148,9 @@ return ` ${data.label}`; } - return ` - ${data.label + ' '} - (${I18n.t('my_modules.details.create_new_tag')})`; + return ` + ${data.label + ' '} + ${I18n.t('my_modules.details.create_new_tag')}`; }, onOpen: function() { $('.select-container .edit-button-container').removeClass('hidden'); @@ -440,7 +291,6 @@ initTaskCollapseState(); applyTaskStatusChangeCallBack(); initTagsSelector(); - bindEditTagsAjax(); initStartDatePicker(); initDueDatePicker(); initAssignedUsersSelector(); diff --git a/app/assets/javascripts/my_modules/protocols.js b/app/assets/javascripts/my_modules/protocols.js index 8a0e73b7f..f5f2a5af0 100644 --- a/app/assets/javascripts/my_modules/protocols.js +++ b/app/assets/javascripts/my_modules/protocols.js @@ -18,19 +18,21 @@ function initEditMyModuleDescription() { if ($(this).hasClass('record-info-link')) return; e.stopPropagation(); }); - TinyMCE.initIfHasDraft(viewObject); + + setTimeout(function() { + TinyMCE.wrapTables(viewObject); + }, 100); } function initEditProtocolDescription() { var viewObject = $('#protocol_description_view'); viewObject.on('click', function(e) { if ($(e.target).hasClass('record-info-link')) return; - TinyMCE.init('#protocol_description_textarea', refreshProtocolStatusBar); + TinyMCE.init('#protocol_description_textarea', { afterInitCallback: refreshProtocolStatusBar }); }).on('click', 'a', function(e) { if ($(this).hasClass('record-info-link')) return; e.stopPropagation(); }); - TinyMCE.initIfHasDraft(viewObject); } function initCopyToRepository() { diff --git a/app/assets/javascripts/my_modules/results.js b/app/assets/javascripts/my_modules/results.js index 10dc834c4..0fa29d8e7 100644 --- a/app/assets/javascripts/my_modules/results.js +++ b/app/assets/javascripts/my_modules/results.js @@ -138,6 +138,7 @@ } function processResult(ev, resultTypeEnum) { + var textWithoutImages; var $form = $(ev.target.form); $form.clearFormErrors(); @@ -153,9 +154,11 @@ .removeClass(GLOBAL_CONSTANTS.HAS_UNSAVED_DATA_CLASS_NAME); break; case ResultTypeEnum.TEXT: + textWithoutImages = TinyMCE.getContent().replaceAll(/src="(data:image\/[^;]+;base64[^"]+)"/i, ''); + textValidator( ev, $form.find('#result_text_attributes_textarea'), 1, - $form.data('rich-text-max-length'), false, TinyMCE.getContent() + $form.data('rich-text-max-length'), false, textWithoutImages ); break; default: diff --git a/app/assets/javascripts/my_modules/tags.js b/app/assets/javascripts/my_modules/tags.js new file mode 100644 index 000000000..ed1580ce2 --- /dev/null +++ b/app/assets/javascripts/my_modules/tags.js @@ -0,0 +1,158 @@ +/* global dropdownSelector I18n */ +/* eslint-disable no-use-before-define */ +(function() { + // Bind ajax for editing tags + function bindEditTagsAjax() { + var manageTagsModal = null; + var manageTagsModalBody = null; + + // Initialize reloading of manage tags modal content after posting new + // tag. + function initAddTagForm() { + manageTagsModalBody.find('.add-tag-form') + .submit(function() { + var selectOptions = manageTagsModalBody.find('#new_my_module_tag .dropdown-menu li').length; + if (selectOptions === 0 && this.id === 'new_my_module_tag') return false; + return true; + }) + .on('ajax:success', function(e, data) { + var newTag; + initTagsModalBody(data); + newTag = $('#manage-module-tags-modal .list-group-item').last(); + dropdownSelector.addValue('#module-tags-selector', { + value: newTag.data('tag-id'), + label: newTag.data('name'), + params: { + color: newTag.data('color') + } + }, true); + }); + } + // Initialize edit tag & remove tag functionality from my_module links. + function initTagRowLinks() { + manageTagsModalBody.find('.edit-tag-link') + .on('click', function() { + var $this = $(this); + var li = $this.parents('li.list-group-item'); + var editDiv = $(li.find('div.tag-edit')); + + // Revert all rows to their original states + manageTagsModalBody.find('li.list-group-item').each(function() { + var li2 = $(this); + li2.css('background-color', li2.data('color')); + li2.find('.edit-tag-form').clearFormErrors(); + li2.find('input[type=text]').val(li2.data('name')); + }); + + // Hide all other edit divs, show all show divs + manageTagsModalBody.find('div.tag-edit').hide(); + manageTagsModalBody.find('div.tag-show').show(); + + editDiv.find('input[type=text]').val(li.data('name')); + editDiv.find('.edit-tag-color').colorselector('setColor', li.data('color')); + + li.find('div.tag-show').hide(); + editDiv.show(); + }); + manageTagsModalBody.find('div.tag-edit .dropdown-colorselector > .dropdown-menu li a') + .on('click', function() { + // Change background of the
  • + var $this = $(this); + var li = $this.parents('li.list-group-item'); + li.css('background-color', $this.data('value')); + }); + manageTagsModalBody.find('.remove-tag-link') + .on('ajax:success', function(e, data) { + dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true); + initTagsModalBody(data); + }); + manageTagsModalBody.find('.delete-tag-form') + .on('ajax:success', function(e, data) { + dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true); + initTagsModalBody(data); + }); + manageTagsModalBody.find('.edit-tag-form') + .on('ajax:success', function(e, data) { + var newTag; + initTagsModalBody(data); + dropdownSelector.removeValue('#module-tags-selector', this.dataset.tagId, '', true); + newTag = $('#manage-module-tags-modal .list-group-item[data-tag-id=' + this.dataset.tagId + ']'); + dropdownSelector.addValue('#module-tags-selector', { + value: newTag.data('tag-id'), + label: newTag.data('name'), + params: { + color: newTag.data('color') + } + }, true); + }) + .on('ajax:error', function(e, data) { + $(this).renderFormErrors('tag', data.responseJSON); + }); + manageTagsModalBody.find('.cancel-tag-link') + .on('click', function() { + var $this = $(this); + var li = $this.parents('li.list-group-item'); + + li.css('background-color', li.data('color')); + li.find('.edit-tag-form').clearFormErrors(); + + li.find('div.tag-edit').hide(); + li.find('div.tag-show').show(); + }); + } + + // Initialize ajax listeners and elements style on modal body. This + // function must be called when modal body is changed. + function initTagsModalBody(data) { + manageTagsModalBody.html(data.html); + manageTagsModalBody.find('.selectpicker').selectpicker(); + initAddTagForm(); + initTagRowLinks(); + } + + manageTagsModal = $('#manage-module-tags-modal'); + manageTagsModalBody = manageTagsModal.find('.modal-body'); + + // Reload tags HTML element when modal is closed + manageTagsModal.on('hide.bs.modal', function() { + var tagsEl = $('#module-tags'); + + if ($('#experimentTable').length) { + let tags = $('.tag-show').length; + $(`#myModuleTags${$('#tags_modal_my_module_id').val()}`).html( + tags === 0 ? I18n.t('experiments.table.add_tag') : tags + ); + } + + // Load HTML + $.ajax({ + url: tagsEl.attr('data-module-tags-url'), + type: 'GET', + dataType: 'json', + success: function(data) { + var newOptions = $(data.html_module_header).find('option'); + $('#module-tags-selector').find('option').remove(); + $(newOptions).appendTo('#module-tags-selector').change(); + }, + error: function() { + // TODO + } + }); + }); + // Remove modal content when modal window is closed. + manageTagsModal.on('hidden.bs.modal', function() { + manageTagsModalBody.html(''); + }); + // initialize my_module tab remote loading + $('#experimentTable, .my-modules-protocols-index') + .on('ajax:before', '.edit-tags-link', function() { + manageTagsModal.modal('show'); + }) + .on('ajax:success', '.edit-tags-link', function(e, data) { + $('#manage-module-tags-modal-module').text(data.my_module.name); + initTagsModalBody(data); + }); + } + + bindEditTagsAjax(); +}()); diff --git a/app/assets/javascripts/projects/canvas.js.erb b/app/assets/javascripts/projects/canvas.js.erb index ae60f5f7a..a014dc1d0 100644 --- a/app/assets/javascripts/projects/canvas.js.erb +++ b/app/assets/javascripts/projects/canvas.js.erb @@ -332,6 +332,8 @@ function initializeFullZoom() { commentMenu.position({ top: $(this).parent().position().top }); commentMenu.offset({ top: $(this).parent().offset().top + <%= Constants::DROPDOWN_TOP_OFFSET_PX %> }); }); + + initializeCanvasViewNavigator(); } function destroyFullZoom() { @@ -370,6 +372,7 @@ function initializeMediumZoom() { // Restore draggable position restoreDraggablePosition($("#diagram"), $("#canvas-container")); + initializeCanvasViewNavigator(); } function destroyMediumZoom() { @@ -397,6 +400,7 @@ function initializeSmallZoom() { // Restore draggable position restoreDraggablePosition($("#diagram"), $("#canvas-container")); + initializeCanvasViewNavigator(); } function destroySmallZoom() { @@ -595,7 +599,7 @@ function resizeContainer() { if (cont.length > 0) { cont.css( "height", - ($(window).height() - cont.offset().top - 15) + "px" + ($(window).height() - cont.offset().top) + "px" ); } } @@ -2064,7 +2068,9 @@ function cloneModule(originalModule, gridDistX, gridDistY, left, top) { var newModule = createVirtualModule(); elLeft(newModule, left); elTop(newModule, top); - updateModuleHtml(newModule, id, originalModule.data("module-name"), gridDistX, gridDistY); + updateModuleHtml(newModule, id, + `${I18n.t('experiments.canvas.edit.clone_prefix')} ${originalModule.data('module-name')}`, + gridDistX, gridDistY); newModule.removeClass("new"); // Add the cloned module id into the hidden input field @@ -2661,6 +2667,9 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) { y_pos = y_el + (fastOffsetY - y_start); x_start = fastOffsetX; y_start = fastOffsetY; + + drawRectangleCanvasNavigatorView(-x_pos, -y_pos) + if (draggable !== null) { elLeft(draggable, x_pos); elTop(draggable, y_pos); @@ -2913,6 +2922,79 @@ function initJsPlumb(containerSel, containerChildSel, modulesSel, params) { } })(); +function drawCanvasViewNavigatorImage(image_src){ + var canvasImage = $('.canvas-preview-img')[0]; + var canvasRect = $('.canvas-preview-rect')[0]; + var canvasImageTx = canvasImage.getContext('2d'); + var canvasRectTx = canvasRect.getContext('2d'); + var image = new Image(); + + image.onload = function() { + canvasImageTx.drawImage(image, 0, 0, canvasImage.width, canvasImage.height); + drawRectangleCanvasNavigatorView(-(draggable.offset().left - draggable.parent().offset().left), + -(draggable.offset().top - draggable.parent().offset().top)); + canvasRectTx.stroke(); + }; + image.src = image_src; + +} + +function initializeCanvasViewNavigator() { + if ($('.canvas-preview-img').data('image-url')) { + drawCanvasViewNavigatorImage($('.canvas-preview-img').data('image-url')); + } else if ($('.canvas-preview-img').data('workflowimg-present') === false) { + let imgUrl = $('.canvas-preview-img').data('workflowimg-url'); + $.ajax({ + url: imgUrl, + type: 'GET', + dataType: 'json', + success: function(data) { + drawCanvasViewNavigatorImage($(data.workflowimg).attr('src')); + } + }); + } +} + +function drawRoundRectangle(ctx, xPos, yPos, width, height, radius) { + width = Math.max(width, 0) + height = Math.max(height, 0) + if (width < 2 * radius) radius = width / 2; + if (height < 2 * radius) radius = height / 2; + + ctx.beginPath(); + ctx.lineWidth = 4; + ctx.strokeStyle = '#104DA9'; + ctx.moveTo(xPos + radius, yPos); + ctx.arcTo(xPos + width, yPos, xPos + width, yPos + height, radius); + ctx.arcTo(xPos + width, yPos + height, xPos, yPos + height, radius); + ctx.arcTo(xPos, yPos + height, xPos, yPos, radius); + ctx.arcTo(xPos, yPos, xPos + width, yPos, radius); + ctx.stroke(); + ctx.closePath(); +} + +function drawRectangleCanvasNavigatorView(xPos, yPos) { + var adjustFactor = 10; + var canvasSize = calculateDraggableSize(draggable); + var ratioX = xPos / canvasSize.width; + var ratioY = yPos / canvasSize.height; + + var canvasPreviewRect = $('.canvas-preview-rect')[0]; + + if (canvasPreviewRect) { + var canvasRectTx = canvasPreviewRect.getContext('2d'); + var canvasWidth = canvasRectTx.canvas.width; + var canvasHeight = canvasRectTx.canvas.height; + var previewWidth = canvasWidth * ($('#diagram-container').width() / canvasSize.width); + var previewHeight = canvasHeight * ($('#diagram-container').height() / canvasSize.height); + + canvasRectTx.clearRect(0, 0, canvasWidth, canvasHeight); + canvasRectTx.beginPath(); + drawRoundRectangle(canvasRectTx, canvasWidth * ratioX + adjustFactor, canvasHeight * ratioY + adjustFactor, + previewWidth - adjustFactor, previewHeight - adjustFactor, 4) + } +} + /** prevent reload page */ var preventCanvasReloadOnSave = (function() { 'use strict'; diff --git a/app/assets/javascripts/projects/show.js b/app/assets/javascripts/projects/show.js index 9826f0fbd..c5a8efe49 100644 --- a/app/assets/javascripts/projects/show.js +++ b/app/assets/javascripts/projects/show.js @@ -317,11 +317,16 @@ function initNewExperimentToolbarButton() { let forms = '.new-experiment-form'; $(experimentsPage) + .on('submit', forms, function() { + $(this).find("button[type='submit']").prop('disabled', true); + }) .on('ajax:success', forms, function(ev, data) { appendActionModal($(data.html)); + $(this).find("button[type='submit']").prop('disabled', false); }) .on('ajax:error', forms, function(ev, data) { HelperModule.flashAlertMsg(data.responseJSON.message, 'danger'); + $(this).find("button[type='submit']").prop('disabled', false); }); } diff --git a/app/assets/javascripts/protocols/header.js b/app/assets/javascripts/protocols/header.js index 13f678e7d..fd17b98e7 100644 --- a/app/assets/javascripts/protocols/header.js +++ b/app/assets/javascripts/protocols/header.js @@ -29,7 +29,6 @@ var ProtocolRepositoryHeader = (function() { if ($(this).hasClass('record-info-link')) return; e.stopPropagation(); }); - TinyMCE.initIfHasDraft(viewObject); } return { diff --git a/app/assets/javascripts/protocols/import_export/import.js b/app/assets/javascripts/protocols/import_export/import.js index c9d8ee579..4cb415f01 100644 --- a/app/assets/javascripts/protocols/import_export/import.js +++ b/app/assets/javascripts/protocols/import_export/import.js @@ -72,20 +72,8 @@ function importProtocolFromFile( return template; } - function addChildToPreviewElement(parentEl, name, childEl) { - parentEl.find("[data-hold='" + name + "']").append(childEl); - } - - function hidePartOfElement(element, name) { - element.find("[data-toggle='" + name + "']").hide(); - } - - function showPartOfElement(element, name) { - element.find("[data-toggle='" + name + "']").show(); - } - function newAssetElement(folder, stepGuid, fileRef, fileName, fileType) { - var html = '
  • '; + var html = '
  • '; var assetBytes; if ($.inArray(fileType, ['image/png', 'image/jpeg', 'image/gif', 'image/bmp']) > 0) { assetBytes = getAssetBytes(folder, stepGuid, fileRef); @@ -176,6 +164,8 @@ function importProtocolFromFile( var stepName = node.children('name').text(); var checklistNodes; var tableNodes; + var assetNodes; + var fileHeader; var stepDescription = displayTinyMceAssetInDescription( node, protocolFolders[position], @@ -191,39 +181,6 @@ function importProtocolFromFile( } ); - // Iterate through step assets - var assetNodes = node.find('assets > asset'); - if (assetNodes.length > 0) { - assetNodes.each(function() { - var fileRef = $(this).attr('fileRef'); - var fileName = $(this).children('fileName').text(); - var fileType = $(this).children('fileType').text(); - - var assetEl = newAssetElement( - protocolFolders[position], - stepGuid, - fileRef, - fileName, - fileType - ); - - // Append asset element to step - addChildToPreviewElement(stepEl, 'assets', assetEl); - }); - } else { - hidePartOfElement(stepEl, 'assets'); - } - - // Iterate through step tables - tableNodes = node.find('elnTables > elnTable'); - if (tableNodes.length > 0) { - tableNodes.each(function() { - addTablePreview(stepEl, this); - }); - } else { - hidePartOfElement(stepEl, 'tables'); - } - // Iterate through step checklists checklistNodes = node.find('checklists > checklist'); if (checklistNodes.length > 0) { @@ -232,21 +189,26 @@ function importProtocolFromFile( }); } + // Iterate through step tables + tableNodes = node.find('elnTables > elnTable'); + if (tableNodes.length > 0) { + tableNodes.each(function() { + addTablePreview(stepEl, this); + }); + } + // Parse step elements - $(this).find('stepElements > stepElement').each(function() { + $(this).find('stepElements > stepElement').sort(stepComparator).each(function() { $element = $(this); switch ($(this).attr('type')) { case 'Checklist': addChecklistPreview(stepEl, $(this).find('checklist')); - showPartOfElement(stepEl, 'checklists'); break; case 'StepTable': addTablePreview(stepEl, $(this).find('elnTable')); - showPartOfElement(stepEl, 'tables'); break; case 'StepText': addStepTextPreview(stepEl, $(this).find('stepText'), protocolFolders[position], stepGuid); - showPartOfElement(stepEl, 'step-texts'); break; default: // nothing to do @@ -254,6 +216,32 @@ function importProtocolFromFile( } }); + // Iterate through step assets + assetNodes = node.find('assets > asset'); + if (assetNodes.length > 0) { + fileHeader = newPreviewElement('asset-file-name', null); + + stepEl.append(fileHeader); + + assetNodes.each(function() { + var fileRef = $(this).attr('fileRef'); + var fileName = $(this).children('fileName').text(); + var fileType = $(this).children('fileType').text(); + var assetEl; + + assetEl = newAssetElement( + protocolFolders[position], + stepGuid, + fileRef, + fileName, + fileType + ); + + // Append asset element to step + stepEl.append(assetEl); + }); + } + // Append step element to preview container previewContainer.append(stepEl); }); @@ -270,10 +258,10 @@ function importProtocolFromFile( { name: tableName } ); var elnTableEl = generateElnTable(tableId, tableContent); - addChildToPreviewElement(tableEl, 'table', elnTableEl); + tableEl.append(elnTableEl); // Now, append table element to step - addChildToPreviewElement(stepEl, 'tables', tableEl); + stepEl.append(tableEl); } function addChecklistPreview(stepEl, checklistNode) { @@ -294,11 +282,11 @@ function importProtocolFromFile( 'checklist-item', { text: itemText } ); - addChildToPreviewElement(checklistEl, 'checklist-items', itemEl); + checklistEl.append(itemEl); }); // Now, add checklist item to step - addChildToPreviewElement(stepEl, 'checklists', checklistEl); + stepEl.append(stepEl, checklistEl); } function addStepTextPreview(stepEl, stepTextNode, folder, stepGuid) { @@ -311,7 +299,7 @@ function importProtocolFromFile( { text: itemText } ); - addChildToPreviewElement(stepEl, 'step-texts', textEl); + stepEl.append(textEl); } // display tiny_mce_assets in step description @@ -743,7 +731,7 @@ function importProtocolFromFile( // Parse step elements stepJson.stepElements = []; - $(this).find('stepElements > stepElement').each(function() { + $(this).find('stepElements > stepElement').sort(stepComparator).each(function() { stepJson.stepElements.push(stepElementJson($(this), index, stepGuid)); }); diff --git a/app/assets/javascripts/protocols/steps.js.erb b/app/assets/javascripts/protocols/steps.js.erb index 8e44967ed..252b21520 100644 --- a/app/assets/javascripts/protocols/steps.js.erb +++ b/app/assets/javascripts/protocols/steps.js.erb @@ -42,7 +42,11 @@ // On init initHandsOnTable($(document)); - TinyMCE.highlight(); + + $('[class*=language]').each((i, block) => { + hljs.highlightBlock(block); + }); + SmartAnnotation.preventPropagation('.atwho-user-popover'); $(function () { @@ -116,4 +120,4 @@ reorderAttachmentsInit(); initAssetViewModeToggle(); -})(); +}); diff --git a/app/assets/javascripts/reports/new.js b/app/assets/javascripts/reports/new.js index 07b4f7e84..963a6123b 100644 --- a/app/assets/javascripts/reports/new.js +++ b/app/assets/javascripts/reports/new.js @@ -1020,7 +1020,7 @@ function reportHandsonTableConverter() { }); // Project content - reportData.project_content = { experiments: [], repositories: [] }; + reportData.project_content = { experiments: [] }; $.each($('.project-contents-container .experiment-element'), function(i, experiment) { let expCheckbox = $(experiment).find('.report-experiment-checkbox'); if (!expCheckbox.prop('checked') && !expCheckbox.prop('indeterminate')) return; @@ -1034,10 +1034,6 @@ function reportHandsonTableConverter() { reportData.project_content.experiments.push(experimentData); }); - $.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), function(i, e) { - reportData.project_content.repositories.push(parseInt(e.value, 10)); - }); - // Settings reportData.report.settings.template = dropdownSelector.getValues('#templateSelector'); reportData.report.settings.all_tasks = $('.project-contents-container .select-all-my-modules-checkbox') @@ -1048,6 +1044,10 @@ function reportHandsonTableConverter() { $.each($('.task-contents-container .content-element .task-setting'), function(i, e) { reportData.report.settings.task[e.value] = e.checked; }); + reportData.report.settings.task.repositories = []; + $.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), function(i, e) { + reportData.report.settings.task.repositories.push(parseInt(e.value, 10)); + }); reportData.report.settings.task.result_order = dropdownSelector.getValues('#taskResultsOrder'); diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index 4732b783b..a5c248e0f 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -412,10 +412,10 @@ var RepositoryDatatable = (function(global) { // Adjust columns width in table header function adjustTableHeader() { TABLE.columns.adjust(); - $('.dropdown-menu').parent() - .on('shown.bs.dropdown hidden.bs.dropdown', function() { - TABLE.columns.adjust(); - }); + // $('.dropdown-menu').parent() + // .on('shown.bs.dropdown hidden.bs.dropdown', function() { + // TABLE.columns.adjust(); + // }); } function checkSnapshottingStatus() { @@ -664,9 +664,9 @@ var RepositoryDatatable = (function(global) { initActiveRemindersFilter(); renderFiltersDropdown(); - setTimeout(function() { - adjustTableHeader(); - }, 500); + // setTimeout(function() { + // adjustTableHeader(); + // }, 500); } }); @@ -699,11 +699,11 @@ var RepositoryDatatable = (function(global) { }) initRowSelection(); - $(window).resize(() => { - setTimeout(() => { - adjustTableHeader(); - }, 500); - }); + // $(window).resize(() => { + // setTimeout(() => { + // adjustTableHeader(); + // }, 500); + // }); return TABLE; } @@ -822,7 +822,7 @@ var RepositoryDatatable = (function(global) { }); changeToEditMode(); - adjustTableHeader(); + // adjustTableHeader(); }) .on('click', '#deleteRepositoryRecords', function() { $('#deleteRepositoryRecord').modal('show'); @@ -909,9 +909,9 @@ var RepositoryDatatable = (function(global) { document.documentElement.style.setProperty('--repository-sidebar-margin', '363px'); }); - $('#wrapper').on('sideBar::hidden sideBar::shown', function() { - adjustTableHeader(); - }); + // $('#wrapper').on('sideBar::hidden sideBar::shown', function() { + // adjustTableHeader(); + // }); } function renderFiltersDropdown() { diff --git a/app/assets/javascripts/shared/inline_editing.js b/app/assets/javascripts/shared/inline_editing.js index 31a451869..1ad371d8b 100644 --- a/app/assets/javascripts/shared/inline_editing.js +++ b/app/assets/javascripts/shared/inline_editing.js @@ -71,6 +71,7 @@ var inlineEditing = (function() { data: params, success: function(result) { var viewData; + var parentContainer = container.parent(); if (container.data('response-field')) { // If we want to modify preview element on backend // we can use this data field and we will take string from response @@ -95,11 +96,15 @@ var inlineEditing = (function() { .attr('value', inputField(container).val()); appendAfterLabel(container); - container.trigger('inlineEditing::updated', [inputField(container).val(), viewData]) + container.trigger('inlineEditing::updated', [inputField(container).val(), viewData]); if (SIDEBAR_ITEM_TYPES.includes(paramsGroup)) { updateSideBarNav(paramsGroup, itemId, viewData); } + + if (parentContainer.attr('data-original-title')) { + parentContainer.attr('data-original-title', inputField(container).val()); + } }, error: function(response) { var error = response.responseJSON[fieldToUpdate]; @@ -111,6 +116,7 @@ var inlineEditing = (function() { container.find('.error-block').html(error.join(', ')); inputField(container).focus(); container.data('disabled', false); + $('.tooltip').hide(); } }); return true; @@ -127,26 +133,38 @@ var inlineEditing = (function() { $(document) .off('click', editBlocks) + .off('keyup', `${editBlocks}`) .off('click', `${editBlocks} .save-button`) .off('click', `${editBlocks} .cancel-button`) .off('blur', `${editBlocks} textarea, ${editBlocks} input`) + .on('keyup', `${editBlocks}`, function(e) { + var container = $(this); + if (e.keyCode === 27) { + $(`${editBlocks} .cancel-button`).click(); + } // Esc + if (e.keyCode === 13 && !container.find('.view-mode').hasClass('hidden')) { + $(editBlocks).click(); + } + }) .on('click', editBlocks, function(e) { // 'A' mean that, if we click on element we will not go in edit mode var container = $(this); if (e.target.tagName === 'A') return true; if (inputField(container).attr('disabled')) { saveAllEditFields(); - - inputField(container) - .attr('disabled', false) + let input = inputField(container); + input.attr('disabled', false) .removeClass('hidden') .focus(); + input[0].selectionStart = input[0].value.length; + input[0].selectionEnd = input[0].value.length; container .attr('data-edit-mode', '1'); container.find('.view-mode') .addClass('hidden') .closest('.inline_scroll_block') .scrollTop(container.offsetTop); + $('.tooltip').hide(); } e.stopPropagation(); return true; diff --git a/app/assets/javascripts/sitewide/comments_sidebar.js b/app/assets/javascripts/sitewide/comments_sidebar.js index 3f6b5e3f4..f59f5075f 100644 --- a/app/assets/javascripts/sitewide/comments_sidebar.js +++ b/app/assets/javascripts/sitewide/comments_sidebar.js @@ -20,6 +20,7 @@ var CommentsSidebar = (function() { $(SIDEBAR).find('.comment-input-container').removeClass('hidden'); } else { $(SIDEBAR).find('.comment-input-container').addClass('hidden'); + $(SIDEBAR).find('.comment-input-container').addClass('update-only'); } }); } @@ -28,7 +29,8 @@ var CommentsSidebar = (function() { var commentsAmount = $(SIDEBAR).find('.comments-list .comment-container').length; if (commentsCounter.length) { // Replace the number in comment element - commentsCounter.text(commentsCounter.text().replace(/\d+/g, commentsAmount)); + commentsCounter.text(commentsCounter.text().replace(/[\d\\+]+/g, commentsAmount)); + commentsCounter.removeClass('hidden'); } } @@ -37,6 +39,7 @@ var CommentsSidebar = (function() { commentsCounter = $(`#comment-count-${$(this).data('objectId')}`); closeCallback = $(this).data('closeCallback'); CommentsSidebar.open($(this).data('objectType'), $(this).data('objectId')); + $(this).parent().find('.unseen-comments').remove(); e.preventDefault(); }); } @@ -86,6 +89,9 @@ var CommentsSidebar = (function() { } $(SIDEBAR).find('.comment-input-field').val(''); $(SIDEBAR).find('.sidebar-footer').removeClass('update'); + if ($(SIDEBAR).find('.comment-input-container').hasClass('update-only')) { + $(SIDEBAR).find('.comment-input-container').addClass('hidden'); + } $('.error-container').empty(); updateCounter(); }, @@ -100,6 +106,9 @@ var CommentsSidebar = (function() { $(document).on('click', `${SIDEBAR} .cancel-button`, function() { $(SIDEBAR).find('.comment-input-field').val(''); $(SIDEBAR).find('.sidebar-footer').removeClass('update'); + if ($(SIDEBAR).find('.comment-input-container').hasClass('update-only')) { + $(SIDEBAR).find('.comment-input-container').addClass('hidden'); + } }); } @@ -127,6 +136,9 @@ var CommentsSidebar = (function() { $('.comment-container').removeClass('edit'); $(this).closest('.comment-container').addClass('edit'); $(SIDEBAR).find('.sidebar-footer').addClass('update'); + if ($(SIDEBAR).find('.comment-input-container').hasClass('hidden')) { + $(SIDEBAR).find('.comment-input-container').removeClass('hidden'); + } $(SIDEBAR).find('.comment-input-field') .val($(this).data('comment-raw')) .data('update-url', $(this).data('update-url')); @@ -152,7 +164,7 @@ var CommentsSidebar = (function() { open: function(objectType, objectId) { $(SIDEBAR).find('.comments-subject-title').empty(); $(SIDEBAR).find('.comments-list').empty(); - $(SIDEBAR).find('.comment-input-field').val(''); + $(SIDEBAR).find('.comment-input-field').val('').focus(); $('.error-container').empty(); $(SIDEBAR).find('.sidebar-footer').removeClass('update'); $(SIDEBAR).data('object-type', objectType).data('object-id', objectId); diff --git a/app/assets/javascripts/sitewide/date_time_picker.js b/app/assets/javascripts/sitewide/date_time_picker.js index ce64dddb3..0e896a3c2 100644 --- a/app/assets/javascripts/sitewide/date_time_picker.js +++ b/app/assets/javascripts/sitewide/date_time_picker.js @@ -6,16 +6,21 @@ ev.stopPropagation(); let dt = $(this); + let options = { ignoreReadonly: true }; if (dt.data('DateTimePicker')) { dt.data('DateTimePicker').destroy(); } - dt.datetimepicker({ ignoreReadonly: true }); + if (dt.data('positioningVertical')) { + options.widgetPositioning = { vertical: dt.data('positioningVertical') }; + } + + dt.datetimepicker(options); dt.data('DateTimePicker').show(); }); - $(document).on('click', '[data-toggle="clear-date-time-picker"]', function() { + $(document).on('mousedown', '[data-toggle="clear-date-time-picker"]', function() { let dt = $(`#${$(this).data('target')}`); if (!dt.data('DateTimePicker')) dt.datetimepicker({ useCurrent: false }); dt.data('DateTimePicker').clear(); diff --git a/app/assets/javascripts/sitewide/filter_dropdown.js b/app/assets/javascripts/sitewide/filter_dropdown.js index a380485e6..994b62d8f 100644 --- a/app/assets/javascripts/sitewide/filter_dropdown.js +++ b/app/assets/javascripts/sitewide/filter_dropdown.js @@ -44,18 +44,20 @@ var filterDropdown = (function() { } catch (error) { console.error(error); } - }).on('hide.bs.dropdown', function() { - $('#textSearchFilterHistory').hide(); - $('.apply-filters', $filterContainer).click(); + }).on('hide.bs.dropdown', function(e) { + if (e.target === e.currentTarget) { + $('#textSearchFilterHistory').hide(); + $('.apply-filters', $filterContainer).click(); + } }); $textFilter.click(function(e) { e.stopPropagation(); $('#textSearchFilterHistory').toggle(); - $(this).closest('.dropdown').toggleClass('open'); - }).on('input', () => { + $(e.currentTarget).closest('.dropdown').toggleClass('open'); + }).on('input', (e) => { $('#textSearchFilterHistory').hide(); - $(this).closest('.dropdown').removeClass('open'); + $(e.currentTarget).closest('.dropdown').removeClass('open'); }); $filterContainer.on('click', '.projects-search-keyword', function(e) { diff --git a/app/assets/javascripts/sitewide/marvinjs_editor.js b/app/assets/javascripts/sitewide/marvinjs_editor.js index 7dd069383..c58217f7e 100644 --- a/app/assets/javascripts/sitewide/marvinjs_editor.js +++ b/app/assets/javascripts/sitewide/marvinjs_editor.js @@ -161,7 +161,7 @@ var MarvinJsEditorApi = (function() { } else if (config.objectType === 'Result') { location.reload(); } else if (config.objectType === 'TinyMceAsset') { - json = tinymce.util.JSON.parse(result); + json = JSON.parse(result); config.editor.execCommand('mceInsertContent', false, TinyMceBuildHTML(json)); TinyMCE.updateImages(config.editor); } @@ -210,6 +210,20 @@ var MarvinJsEditorApi = (function() { }); } + function createNewMarvinContainer(dataset) { + var objectId = dataset.objectId; + var objectType = dataset.objectType; + var marvinUrl = dataset.marvinUrl; + var container = dataset.sketchContainer; + MarvinJsEditor.open({ + mode: 'new', + objectId: objectId, + objectType: objectType, + marvinUrl: marvinUrl, + container: container + }); + } + // MarvinJS Methods return { @@ -254,17 +268,13 @@ var MarvinJsEditorApi = (function() { initNewButton: function(selector, saveCallback) { $(selector).off('click').on('click', function() { - var objectId = this.dataset.objectId; - var objectType = this.dataset.objectType; - var marvinUrl = this.dataset.marvinUrl; - var container = this.dataset.sketchContainer; - MarvinJsEditor.open({ - mode: 'new', - objectId: objectId, - objectType: objectType, - marvinUrl: marvinUrl, - container: container - }); + createNewMarvinContainer(this.dataset); + }); + + $(selector).off('keypress').on('keypress', function(e) { + if (e.which === 13) { + createNewMarvinContainer(this.dataset); + } }); MarvinJsEditor.saveCallback = saveCallback; @@ -280,47 +290,6 @@ var MarvinJsEditorApi = (function() { }; }); -// TinyMCE plugin - -(function() { - 'use strict'; - - tinymce.PluginManager.requireLangPack('MarvinJsPlugin'); - - tinymce.create('tinymce.plugins.MarvinJsPlugin', { - MarvinJsPlugin: function(ed) { - var editor = ed; - - function openMarvinJs() { - MarvinJsEditor.open({ - mode: 'new-tinymce', - marvinUrl: '/tiny_mce_assets/marvinjs', - editor: editor - }); - } - // Add a button that opens a window - editor.addButton('marvinjsplugin', { - tooltip: I18n.t('marvinjs.new_button'), - icon: 'marvinjs', - onclick: openMarvinJs - }); - - // Adds a menu item to the tools menu - editor.addMenuItem('marvinjsplugin', { - text: I18n.t('marvinjs.new_button'), - icon: 'marvinjs', - context: 'insert', - onclick: openMarvinJs - }); - } - }); - - tinymce.PluginManager.add( - 'marvinjsplugin', - tinymce.plugins.MarvinJsPlugin - ); -})(); - // Initialization $(document).on('click', '.marvinjs-edit-button', function() { var editButton = $(this); @@ -337,7 +306,7 @@ $(document).on('click', '.marvinjs-edit-button', function() { $(document).on('turbolinks:load', function() { MarvinJsEditor = MarvinJsEditorApi(); if (MarvinJsEditor.enabled()) { - if ($('#marvinjs-editor')[0].dataset.marvinjsMode === 'remote' && typeof(ChemicalizeMarvinJs) !== 'undefined') { + if ($('#marvinjs-editor')[0].dataset.marvinjsMode === 'remote' && typeof (ChemicalizeMarvinJs) !== 'undefined') { ChemicalizeMarvinJs.createEditor('#marvinjs-sketch').then(function(marvin) { marvin.setDisplaySettings({ toolbars: 'reporting' }); marvinJsRemoteEditor = marvin; diff --git a/app/assets/javascripts/sitewide/tiny_mce.js b/app/assets/javascripts/sitewide/tiny_mce.js index 516966694..e69de29bb 100644 --- a/app/assets/javascripts/sitewide/tiny_mce.js +++ b/app/assets/javascripts/sitewide/tiny_mce.js @@ -1,446 +0,0 @@ -/* global _ hljs tinyMCE SmartAnnotation I18n GLOBAL_CONSTANTS HelperModule */ -/* eslint-disable no-unused-vars */ - -var TinyMCE = (function() { - 'use strict'; - - function initHighlightjs() { - $('[class*=language]').each(function(i, block) { - hljs.highlightBlock(block); - }); - } - - function initHighlightjsIframe(iframe) { - $('[class*=language]', iframe).each(function(i, block) { - hljs.highlightBlock(block); - }); - } - - // Get LocalStorage auto save path - function getAutoSavePrefix(editor) { - var prefix = editor.getParam('autosave_prefix', 'tinymce-autosave-{path}{query}{hash}-{id}-'); - - prefix = prefix.replace(/\{path\}/g, document.location.pathname); - prefix = prefix.replace(/\{query\}/g, document.location.search); - prefix = prefix.replace(/\{hash\}/g, document.location.hash); - prefix = prefix.replace(/\{id\}/g, editor.id); - - return prefix; - } - - // Handles autosave notification if draft is available in the local storage - function restoreDraftNotification(selector, editor) { - var prefix = getAutoSavePrefix(editor); - var lastDraftTime = parseInt(tinyMCE.util.LocalStorage.getItem(prefix + 'time'), 10); - var lastUpdated = $(selector).data('last-updated'); - var notificationBar; - var restoreBtn = $(''); - var cancelBtn = $(''); - - // Check whether we have draft stored - if (editor.plugins.autosave.hasDraft()) { - notificationBar = $('
    '); - - if (lastDraftTime < lastUpdated) { - notificationBar.html(`${I18n.t('tiny_mce.older_version_available')}`); - } else { - notificationBar.html(`${I18n.t('tiny_mce.newer_version_available')}`); - } - - // Add notification bar - $(notificationBar).append(restoreBtn).append(cancelBtn); - $(editor.contentAreaContainer).before(notificationBar); - - $(restoreBtn).click(function() { - editor.plugins.autosave.restoreDraft(); - makeItDirty(editor); - notificationBar.remove(); - }); - - $(cancelBtn).click(function() { - notificationBar.remove(); - }); - } - } - - function initImageToolBar(editor) { - var editorIframe = $('#' + editor.id).prev().find('.mce-edit-area iframe'); - var primaryColor = '#104da9'; - editorIframe.contents().find('head').append(``); - } - - function makeItDirty(editor) { - var editorForm = $(editor.getContainer()).closest('form'); - editorForm.find('.tinymce-status-badge').addClass('hidden'); - $(editor.getContainer()).find('.tinymce-save-button').removeClass('hidden'); - } - - function draftLocation() { - return 'tinymce-drafts-' + document.location.pathname; - } - - function removeDraft(editor, textAreaObject) { - var location = draftLocation(); - var storedDrafts = JSON.parse(sessionStorage.getItem(location) || '[]'); - var draftId = storedDrafts.indexOf(textAreaObject.data('tinymce-object')); - if (draftId > -1) { - storedDrafts.splice(draftId, 1); - } - - if (storedDrafts.length) { - sessionStorage.setItem(location, JSON.stringify(storedDrafts)); - } else { - sessionStorage.removeItem(location); - } - } - - // Update scroll position after exit - function updateScrollPosition(editorForm) { - if (editorForm.offset().top < $(window).scrollTop()) { - $(window).scrollTop(editorForm.offset().top - 150); - } - } - - function saveAction(editor) { - var editorForm = $(editor.getContainer()).closest('form'); - editorForm.clearFormErrors(); - editor.setProgressState(1); - editor.save(); - editorForm.submit(); - updateScrollPosition(editorForm); - } - - // returns a public API for TinyMCE editor - return Object.freeze({ - init: function(selector, onSaveCallback) { - var editorInstancePromise; - var tinyMceContainer; - var tinyMceInitSize; - var plugins; - var textAreaObject = $(selector); - if (typeof tinyMCE !== 'undefined') { - // Hide element containing HTML view of RTE field - tinyMceContainer = $(selector).closest('form').find('.tinymce-view'); - tinyMceInitSize = tinyMceContainer.height(); - $(selector).closest('.form-group') - .before('
    '); - tinyMceContainer.addClass('hidden'); - plugins = 'custom_image_toolbar table autosave autoresize customimageuploader link advlist codesample autolink lists charmap hr anchor searchreplace wordcount visualblocks visualchars insertdatetime nonbreaking save directionality paste textcolor colorpicker textpattern placeholder'; - if (typeof (MarvinJsEditor) !== 'undefined') plugins += ' marvinjsplugin'; - - if (textAreaObject.data('objectType') === 'step' - || textAreaObject.data('objectType') === 'result_text') { - document.location.hash = textAreaObject.data('objectType') + '_' + textAreaObject.data('objectId'); - } - - editorInstancePromise = tinyMCE.init({ - cache_suffix: '?v=4.9.10', // This suffix should be changed any time library is updated - selector: selector, - convert_urls: false, - menubar: 'file edit view insert format table', - toolbar: 'undo redo restoredraft | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table | link | forecolor backcolor | customimageuploader marvinjsplugin | codesample', - plugins: plugins, - autoresize_bottom_margin: 20, - codesample_languages: [ - { text: 'R', value: 'r' }, - { text: 'MATLAB', value: 'matlab' }, - { text: 'Python', value: 'python' }, - { text: 'JSON', value: 'javascript' }, - { text: 'HTML/XML', value: 'markup' }, - { text: 'JavaScript', value: 'javascript' }, - { text: 'CSS', value: 'css' }, - { text: 'PHP', value: 'php' }, - { text: 'Ruby', value: 'ruby' }, - { text: 'Java', value: 'java' }, - { text: 'C', value: 'c' }, - { text: 'C#', value: 'csharp' }, - { text: 'C++', value: 'cpp' } - ], - browser_spellcheck: true, - branding: false, - fixed_toolbar_container: '#mytoolbar', - autosave_restore_when_empty: false, - autosave_interval: '1s', - autosave_retention: '1440m', - removed_menuitems: 'newdocument', - object_resizing: true, - elementpath: false, - forced_root_block: 'div', - force_p_newlines: false, - default_link_target: '_blank', - target_list: [ - { title: 'New page', value: '_blank' }, - { title: 'Same page', value: '_self' } - ], - style_formats: [ - { - title: 'Headers', - items: [ - { title: 'Header 1', format: 'h1' }, - { title: 'Header 2', format: 'h2' }, - { title: 'Header 3', format: 'h3' }, - { title: 'Header 4', format: 'h4' }, - { title: 'Header 5', format: 'h5' }, - { title: 'Header 6', format: 'h6' } - ] - }, - { - title: 'Inline', - items: [ - { title: 'Bold', icon: 'bold', format: 'bold' }, - { title: 'Italic', icon: 'italic', format: 'italic' }, - { title: 'Underline', icon: 'underline', format: 'underline' }, - { title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough' }, - { title: 'Superscript', icon: 'superscript', format: 'superscript' }, - { title: 'Subscript', icon: 'subscript', format: 'subscript' }, - { title: 'Code', icon: 'code', format: 'code' } - ] - }, - { - title: 'Blocks', - items: [ - { title: 'Paragraph', format: 'p' }, - { title: 'Blockquote', format: 'blockquote' } - ] - }, - { - title: 'Alignment', - items: [ - { title: 'Left', icon: 'alignleft', format: 'alignleft' }, - { title: 'Center', icon: 'aligncenter', format: 'aligncenter' }, - { title: 'Right', icon: 'alignright', format: 'alignright' }, - { title: 'Justify', icon: 'alignjustify', format: 'alignjustify' } - ] - } - ], - init_instance_callback: function(editor) { - var editorForm = $(editor.getContainer()).closest('form'); - var menuBar = editorForm.find('.mce-menubar.mce-toolbar.mce-first .mce-flow-layout'); - var editorToolbar = editorForm.find('.mce-top-part'); - - var editorToolbaroffset; - - $('.tinymce-placeholder').css('height', $(editor.editorContainer).height() + 'px'); - setTimeout(() => { - $(editor.editorContainer).addClass('show'); - $('.tinymce-placeholder').remove(); - updateScrollPosition($(editor.editorContainer).closest('.tinymce-container')); - }, 400); - - // Init saved status label - if (editor.getContent() !== '') { - editorForm.find('.tinymce-status-badge').removeClass('hidden'); - } - - if ($('.navbar-secondary').length) { - editorToolbaroffset = $('.navbar-secondary').position().top + $('.navbar-secondary').height(); - } else if ($('#main-nav').length) { - editorToolbaroffset = $('#main-nav').height(); - } else { - editorToolbaroffset = 0; - } - - if (GLOBAL_CONSTANTS.IS_SAFARI) { - editorToolbar.css('position', '-webkit-sticky'); - } else { - editorToolbar.css('position', 'sticky'); - } - editorToolbar.css('top', editorToolbaroffset + 'px').css('z-index', '100'); - - // Init image toolbar - initImageToolBar(editor); - - // Init Save button - editorForm - .find('.tinymce-save-button') - .clone() - .appendTo(menuBar) - .on('click', function(event) { - event.preventDefault(); - saveAction(editor); - }); - - // After save action - editorForm - .on('ajax:success', function(ev, data) { - editor.save(); - editor.setProgressState(0); - editorForm.find('.tinymce-status-badge').removeClass('hidden'); - editor.remove(); - editorForm.find('.tinymce-view').html(data.html).removeClass('hidden'); - editor.plugins.autosave.removeDraft(); - removeDraft(editor, textAreaObject); - if (onSaveCallback) { onSaveCallback(data); } - }).on('ajax:error', function(ev, data) { - var model = editor.getElement().dataset.objectType; - if (data.status === 422 && 'description' in data.responseJSON) { - // eslint-disable-next-line no-param-reassign - data.responseJSON.description = data.responseJSON.description.toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ' '); - } - $(this).renderFormErrors(model, data.responseJSON); - editor.setProgressState(0); - if (data.status === 403) { - HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger'); - } - }); - - // Init Cancel button - editorForm - .find('.tinymce-cancel-button') - .clone() - .appendTo(menuBar) - .on('click', function(event) { - $(editorForm).find('.form-group').removeClass('has-error'); - $(editorForm).find('.help-block').remove(); - - event.preventDefault(); - if (editor.isDirty()) { - editor.setContent($(selector).val()); - } - editorForm.find('.tinymce-status-badge').addClass('hidden'); - editorForm.find('.tinymce-view').removeClass('hidden'); - editor.remove(); - updateScrollPosition(editorForm); - if (onSaveCallback) { onSaveCallback(); } - }) - .removeClass('hidden'); - - // Set cursor to the end of the content - if (editor.settings.id !== 'step_description_textarea') { - editor.focus(); - } - editor.selection.select(editor.getBody(), true); - editor.selection.collapse(false); - - SmartAnnotation.init($(editor.contentDocument.activeElement)); - SmartAnnotation.preventPropagation('.atwho-user-popover'); - initHighlightjsIframe($(this.iframeElement).contents()); - }, - setup: function(editor) { - editor.on('keydown', function(e) { - if (e.keyCode === 13 && $(editor.contentDocument.activeElement).atwho('isSelecting')) { - return false; - } - return true; - }); - - editor.on('NodeChange', function(e) { - var node = e.element; - setTimeout(function() { - if ($(node).is('pre') && !editor.isHidden()) { - initHighlightjsIframe($(editor.iframeElement).contents()); - } - }, 200); - }); - - editor.on('Dirty', function() { - makeItDirty(editor); - }); - - editor.on('StoreDraft', function() { - var location = draftLocation(); - var storedDrafts = JSON.parse(sessionStorage.getItem(location) || '[]'); - var draftName = textAreaObject.data('tinymce-object'); - if (storedDrafts.includes(draftName) || !draftName) return; - storedDrafts.push(draftName); - sessionStorage.setItem(location, JSON.stringify(storedDrafts)); - }); - - editor.on('remove', function() { - var menuBar = $(editor.getContainer()).find('.mce-menubar.mce-toolbar.mce-first .mce-flow-layout'); - menuBar.find('.tinymce-save-button').remove(); - menuBar.find('.tinymce-cancel-button').remove(); - }); - - editor.on('blur', function(e) { - if (editor.blurDisabled) return false; - - if ($('.atwho-view:visible').length || $('#MarvinJsModal:visible').length) return false; - setTimeout(() => { - if (editor.isNotDirty === false) { - $(editor.container).find('.tinymce-save-button').click(); - } else { - $(editor.container).find('.tinymce-cancel-button').click(); - } - }, 0); - return true; - }); - - editor.on('init', function(e) { - restoreDraftNotification(selector, editor); - }); - }, - codesample_content_css: $(selector).data('highlightjs-path'), - save_onsavecallback: function(editor) { saveAction(editor); } - }); - } - - return editorInstancePromise; - }, - destroyAll: function() { - _.each(tinyMCE.editors, function(editor) { - if (editor) { - editor.remove(); - initHighlightjs(); - } - }); - }, - refresh: function() { - this.destroyAll(); - this.init(); - }, - getContent: function() { - return tinyMCE.editors[0].getContent(); - }, - updateImages(editor) { - var images; - var iframe = $('#' + editor.id).prev().find('.mce-edit-area iframe').contents(); - images = $.map($('img', iframe), e => { - return e.dataset.mceToken; - }); - $('#' + editor.id).next()[0].value = JSON.stringify(images); - return JSON.stringify(images); - }, - makeItDirty: function(editor) { - makeItDirty(editor); - }, - highlight: initHighlightjs, - initIfHasDraft: function(viewObject) { - var storedDrafts = sessionStorage.getItem(draftLocation()); - var draftName = viewObject.data('tinymce-init'); - if (storedDrafts && JSON.parse(storedDrafts)[0] === draftName) { - let top = viewObject.offset().top; - setTimeout(() => { - viewObject.click(); - }, 0); - setTimeout(() => { - window.scrollTo(0, top - 150); - }, 2000); - } - } - }); -}()); - -$(document).on('turbolinks:before-visit', function(e) { - _.each(tinyMCE.editors, function(editor) { - if (editor.isNotDirty === false) { - if (confirm(I18n.t('tiny_mce.leaving_warning'))) { - return false; - } - e.preventDefault(); - return false; - } - return false; - }); -}); diff --git a/app/assets/javascripts/sitewide/tiny_mce_file_upload_plugin.js b/app/assets/javascripts/sitewide/tiny_mce_file_upload_plugin.js deleted file mode 100644 index 4a6a95edf..000000000 --- a/app/assets/javascripts/sitewide/tiny_mce_file_upload_plugin.js +++ /dev/null @@ -1,133 +0,0 @@ -/* eslint no-underscore-dangle: "off" */ -/* eslint no-use-before-define: "off" */ -/* eslint no-restricted-syntax: ["off", "BinaryExpression[operator='in']"] */ -/* global tinymce I18n HelperModule validateFileSize */ -(function() { - 'use strict'; - - tinymce.PluginManager.requireLangPack('customimageuploader'); - - tinymce.create('tinymce.plugins.CustomImageUploader', { - CustomImageUploader: function(ed) { - var iframe; - var editor = ed; - var textAreaElement = $('#' + ed.id); - - function loadFiles() { - let $fileInput; - let hitFileLimit; - $('#tinymce_current_upload').remove(); - $fileInput = $('').prependTo(editor.container); - $fileInput.click(); - - $fileInput.change(function() { - let formData = new FormData(); - let files = $('#tinymce_current_upload')[0].files; - - Array.from(files).forEach(file => formData.append('files[]', file, file.name)); - - Array.from(files).every(file => { - if (!validateFileSize(file, true)) { - hitFileLimit = true; - return false; - } - }); - - if (hitFileLimit) { - return; - } - - $.post({ - url: textAreaElement.data('tinymce-asset-path'), - data: formData, - processData: false, - contentType: false, - success: function(data) { - handleResponse(data); - $('#tinymce_current_upload').remove(); - }, - error: function(response) { - HelperModule.flashAlertMsg(response.responseJSON.errors, 'danger'); - $('#tinymce_current_upload').remove(); - } - }); - }); - } - - function handleResponse(response) { - if (response.errors) { - handleError(response.errors.join('
    ')); - } else { - response.images.forEach(el => editor.execCommand('mceInsertContent', false, buildHTML(el))); - updateActiveImages(ed); - } - } - - function handleError(error) { - HelperModule.flashAlertMsg(error, 'danger'); - } - - function buildHTML(image) { - return `description-${image.token}`; - } - - // Create hidden field for images - function createImageHiddenField() { - textAreaElement.parent().find('input#tiny-mce-images').remove(); - $('').insertAfter(textAreaElement); - } - - // Finding images in text - function updateActiveImages() { - var images; - var imageContainer = $('#' + editor.id).next()[0]; - iframe = $('#' + editor.id).prev().find('.mce-edit-area iframe').contents(); - images = $.map($('img', iframe), e => { - return e.dataset.mceToken; - }); - if (imageContainer === undefined) { - createImageHiddenField(); - } - - // Small fix for ResultText when you cancel after change MarvinJS - if (imageContainer === undefined) return []; - - imageContainer.value = JSON.stringify(images); - return JSON.stringify(images); - } - - // Add a button that opens a window - editor.addButton('customimageuploader', { - tooltip: I18n.t('tiny_mce.upload_window_label'), - icon: 'image', - onclick: loadFiles - }); - - // Adds a menu item to the tools menu - editor.addMenuItem('customimageuploader', { - text: I18n.t('tiny_mce.upload_window_label'), - icon: 'image', - context: 'insert', - onclick: loadFiles - }); - - ed.on('NodeChange', function() { - // Check editor status - if (this.initialized) { - updateActiveImages(ed); - } - }); - - createImageHiddenField(); - } - - - }); - - tinymce.PluginManager.add( - 'customimageuploader', - tinymce.plugins.CustomImageUploader - ); -}()); diff --git a/app/assets/javascripts/tinymce/plugins/placeholder/plugin.js b/app/assets/javascripts/tinymce/plugins/placeholder/plugin.js index efae49143..7e7007445 100644 --- a/app/assets/javascripts/tinymce/plugins/placeholder/plugin.js +++ b/app/assets/javascripts/tinymce/plugins/placeholder/plugin.js @@ -1,11 +1,12 @@ /* global tinymce */ + tinymce.PluginManager.add('placeholder', function(editor) { var Label = function() { var editorForm = $(editor.getContainer()).closest('form'); var editorToolbar = editorForm.find('.mce-top-part'); var placeholderText = editor.getElement().getAttribute('placeholder') || editor.settings.placeholder; - var placeholderAttrs = editor.settings.placeholder_attrs || { - style: { + var placeholderAttrs = { + style: ` position: 'absolute', top: (editorToolbar.height()) + 'px', left: 0, @@ -14,7 +15,7 @@ tinymce.PluginManager.add('placeholder', function(editor) { width: 'calc(100% - 50px)', overflow: 'hidden', 'white-space': 'pre-wrap' - } + ` }; var contentAreaContainer = editor.getContentAreaContainer(); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 586fb2321..19576eb99 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -35,6 +35,7 @@ @import "protocols/*"; @import "dashboard/*"; @import "repository/*"; +@import "experiment/*"; @import "repository_columns/*"; @import "label_templates/*"; @import "reports/*"; diff --git a/app/assets/stylesheets/experiment/canvas.scss b/app/assets/stylesheets/experiment/canvas.scss new file mode 100644 index 000000000..672d3c2e8 --- /dev/null +++ b/app/assets/stylesheets/experiment/canvas.scss @@ -0,0 +1,75 @@ +// scss-lint:disable SelectorDepth NestingDepth IdSelector + +#canvas-container, +#module-archive { + .experimnet-name { + max-width: calc(100% - 300px); + } + + .panel-heading { + padding: 10px 15px 4px; + } + + .panel-body { + padding: 6px 15px; + + .status-label { + background-color: var(--state-color); + color: $color-white; + display: inline-block; + margin: 3px 0; + max-width: 100%; + overflow: hidden; + padding: 2px 8px; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .panel-footer { + .nav > li > a { + padding: 6px 15px; + } + + .btn { + height: 30px; + } + + .badge-indicator { + background: $brand-accent; + border-radius: $border-radius-tag; + color: $color-black; + font-size: 10px; + margin-left: -8px; + } + } +} + +#canvas-container { + margin: 0 -28px; +} + +.canvas-preview-img, +.canvas-preview-rect { + border-radius: 4px; + bottom: 24px; + box-shadow: 0 0 0 8px $color-white; + display: flex; + height: 64px; + position: absolute; + right: 24px; + width: 68px; + z-index: 9999; + + &.empty { + background-color: $color-concrete; + box-shadow: inset 0 0 0 2px $brand-primary; + } + + &.processing { + background-color: $color-concrete; + background-image: url("/images/medium/loading.svg"); + background-position: center; + background-repeat: no-repeat; + } +} diff --git a/app/assets/stylesheets/experiment/show.scss b/app/assets/stylesheets/experiment/show.scss new file mode 100644 index 000000000..19fc0840c --- /dev/null +++ b/app/assets/stylesheets/experiment/show.scss @@ -0,0 +1,133 @@ +// scss-lint:disable SelectorDepth NestingDepth IdSelector + +#experimentTable, +#experiment-canvas, +#module-archive { + .experimnet-name { + max-width: calc(100% - 300px); + } +} + +#experimentTable { + &.archived { + [data-view-mode="active"] { + display: none !important; + } + } +} + +#experiment-canvas { + [data-view-mode="archived"] { + display: none; + } + + .toolbar-row { + align-items: center; + display: flex; + margin: 10px 0; + + .toolbar-right-block { + align-items: center; + display: flex; + margin-left: auto; + } + + .zoom-text { + margin-right: .5em; + } + } +} + +#new-my-module-modal { + .my-module-user-tags { + img { + border-radius: 50%; + display: inline; + margin-right: .5em; + max-height: 20px; + max-width: 20px; + } + } + .dropdown-selector-container { + .my-module-white-tags { + color: $color-white; + } + + .my-module-tags-color { + align-items: center; + border-radius: 8px; + display: inline-flex; + height: 16px; + justify-content: center; + margin-right: 5px; + width: 16px; + + &.new { + color: $color-silver-chalice; + } + } + + .my-module-tags-create-new { + margin-left: 3px; + } + + &.open { + .input-field { + border: 1px solid $color-alto; + } + } + + &:not(.view-mode):hover { + .input-field { + border: 1px solid $color-alto; + } + } + } + + .datetime-picker-container { + width: 45%; + + .fa-calendar-alt { + color: $color-volcano !important; + font-size: 14px !important; + } + } +} + +.dropdown-experiment-actions, +.my-module-menu { + .divider-label { + @include font-small; + color: $color-silver-chalice; + padding: .25em 1em; + + &.footer { + border-top: 1px solid $color-concrete; + padding-top: .5em; + } + } + + li { + @include font-button; + cursor: pointer; + padding: .5em 1em; + white-space: nowrap; + + .fas { + display: inline-block; + margin-right: .25em; + width: 18px; + } + + &:hover:not(.divider-label) { + background: $color-concrete; + } + + a { + display: inline-block; + margin: -.5em -1em; + padding: .5em 1em; + width: calc(100% + 2em); + } + } +} diff --git a/app/assets/stylesheets/experiment/table.scss b/app/assets/stylesheets/experiment/table.scss new file mode 100644 index 000000000..affc6d027 --- /dev/null +++ b/app/assets/stylesheets/experiment/table.scss @@ -0,0 +1,469 @@ +// scss-lint:disable SelectorDepth NestingDepth IdSelector + +#experimentTable { + --content-header-size: 5em; + --toolbar-height: 4.5em; + position: relative; + + .title-row { + .header-actions { + &.experiment-header { + column-gap: .25em; + } + + .sort-task-menu { + &:not(.archived) { + [data-view-mode="archived"] { + display: none; + } + } + } + } + } + + .experiment-table-container { + height: calc(100vh - var(--content-header-size) - var(--navbar-height) - var(--toolbar-height)); + overflow: auto; + } + + .toolbar-row { + align-items: center; + display: flex; + height: var(--toolbar-height); + + .toolbar-left-block { + display: flex; + + .btn { + margin-right: .25em; + } + } + + .toolbar-right-block { + margin-left: auto; + } + } + + .experiment-table { + display: grid; + grid-auto-rows: 3em 1px; + grid-template-columns: max-content repeat(calc(var(--columns-count)), minmax(max-content, auto)) max-content; + min-width: 100%; + + .table-header-cell { + align-items: center; + background-color: $color-concrete; + border: 1px solid $color-white; + display: flex; + height: 3em; + padding: 0 .5em; + position: sticky; + position: -webkit-sticky; + top: 0; + z-index: 7; + + &.select-all-checkboxes { + justify-content: center; + } + + .fa-comment { + color: $color-silver-chalice; + } + } + + .table-header { + display: contents; + height: 10px; + + &::after { + content: ""; + grid-column: 1/-1; + } + } + + .table-body { + display: contents; + } + + .loading-overlay { + display: none; + } + + .table-row-provisioning { + .loading-overlay { + display: block; + } + + .sci-checkbox-container { + height: 1.5em; + width: 1.5em; + + .loading-overlay::after { + background-size: 1.5em; + cursor: default; + } + + .sci-checkbox, + .sci-checkbox-label { + display: none; + } + } + } + + .table-body-cell { + align-items: center; + display: flex; + padding: 0 .5em; + + .my-module-users-link { + color: $color-silver-chalice; + + &:hover { + text-decoration: none; + } + } + + .global-avatar-container { + color: $color-silver-chalice; + height: 2em; + line-height: 2em; + margin-right: .25em; + width: 2em; + } + + .more-users { + background: $color-volcano; + border-radius: 50%; + color: $color-white; + height: 2em; + line-height: 2em; + margin-right: .25em; + text-align: center; + text-decoration: none; + width: 2em; + } + + .new-user { + background: $color-concrete; + text-align: center; + } + } + + .archived-column { + display: none; + } + + .comments-column .disabled { + color: $color-silver-chalice; + } + + .table-row { + display: contents; + + &:hover { + .table-body-cell { + background-color: $color-concrete; + } + } + + &::after { + background: $color-concrete; + content: ""; + display: inline-block; + grid-column: 1/-1; + height: 1px; + } + } + + .open-my-module-menu:focus { + box-shadow: 0 0 0 1px $brand-focus; + } + + .assign-users-dropdown { + .dropdown-menu { + padding: .5em; + width: 280px; + } + + .users-list { + max-height: 300px; + overflow: auto; + } + + .user-container { + align-items: center; + display: flex; + padding: .5em; + + .user-avatar { + padding: 0 .75em; + + &.archived { + padding-left: 0; + } + + img { + border-radius: 50%; + } + } + } + + .assigned-users-container { + cursor: pointer; + display: flex; + } + + .avatar-container { + border: 1px solid $color-white; + border-radius: 50%; + display: inline-block; + height: 26px; + margin-right: -5px; + width: 26px; + + img { + border-radius: 50%; + max-height: 100%; + max-width: 100%; + } + } + + .more-users { + font-size: 10px; + line-height: 24px; + } + + .new-user { + color: $color-silver-chalice; + line-height: 24px; + + &:not(:first-child) { + margin-left: 5px; + } + } + } + + .my-module-status { + color: $color-white; + display: inline-block; + margin: 3px 0; + max-width: 100%; + overflow: hidden; + padding: 2px 8px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .table-row-placeholder-divider { + background: $color-concrete; + display: inline-block; + grid-column: 1/-1; + height: 1px; + } + + .table-row-placeholder { + align-items: center; + background-color: $color-white; + border-radius: $border-radius-default; + box-shadow: $flyout-shadow; + display: grid; + grid-column: 1 / -1; + grid-template-columns: 32px repeat(9, minmax(max-content, auto)); + + .placeholder-cell { + animation-duration: 2s; + animation-iteration-count: infinite; + animation-name: placeholder-pulsing; + background-color: $color-alto; + border-radius: $border-radius-default; + display: block; + height: 18px; + margin: auto; + width: 90%; + + &.circle-0 { + border-radius: 100%; + height: 24px; + width: 24px; + } + + + @keyframes placeholder-pulsing { + 0% { + opacity: 1; + } + + 50% { + opacity: .5; + } + + 100% { + opacity: 1; + } + } + } + } + + &.last-page { + padding-bottom: 5em; + position: relative; + } + + .experiment-table-list-end-placeholder { + align-items: center; + background-color: $color-concrete; + bottom: 1em; + display: flex; + height: 3em; + left: calc(50% - 150px); + margin: 0 auto; + padding: 1em; + position: absolute; + width: 300px; + + > * { + flex-grow: 1; + text-align: center; + } + } + } + + .unseen-comments { + @include font-small; + align-items: center; + background-color: $brand-complementary; + border: 2px solid $color-white; + border-radius: 50%; + color: $color-black; + display: flex; + font-weight: bold; + height: 16px; + justify-content: center; + margin-bottom: 10px; + margin-left: -1px; + min-width: 16px; + } + + .datetime-container { + width: 100%; + + .clear-date { + cursor: pointer; + left: calc(100% - 16px); + position: absolute; + text-align: center; + top: 0; + visibility: hidden; + width: 16px; + + &.open { + visibility: visible; + } + } + + .date-text { + display: block; + position: relative; + + .alert-yellow { + color: $brand-warning; + margin-left: 4px; + } + + .alert-red { + color: $brand-danger; + margin-left: 4px; + } + } + + .datetime-picker-container { + left: 0; + position: absolute; + top: 0; + width: calc(100% - 16px); + + .calendar-due-date { + opacity: 0; + } + } + + &:hover { + .date-text[data-editable=true] { + background-color: $color-concrete; + border-radius: 4px; + + } + } + } + + .open-comments-sidebar { + margin-bottom: 0; + } + + &.archived { + .table-body-cell { + background-color: $color-concrete; + } + + .archived-column { + display: flex; + } + } + + .task_name-column { + a { + display: inline-block; + max-width: 320px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + span { + color: $color-silver-chalice; + } + } +} + +.table-display-modal { + .column-container { + align-items: center; + border-bottom: $border-default; + display: flex; + padding: .5em 1em; + + &:not(.visible) { + color: $color-alto; + } + + &:last-child { + border: 0; + } + + .fas { + cursor: pointer; + margin-right: 1em; + + &.disabled { + color: $color-alto; + pointer-events: none; + } + } + + &.task_name { + padding-left: 3em; + + .fas { + display: none; + } + } + } +} + +@media (max-width: 1000px) { + .toolbar-row { + .button-text { + display: none; + } + } +} diff --git a/app/assets/stylesheets/experiments.scss b/app/assets/stylesheets/experiments.scss index 93672ad55..3566fd19b 100644 --- a/app/assets/stylesheets/experiments.scss +++ b/app/assets/stylesheets/experiments.scss @@ -21,6 +21,14 @@ } } + .project-show-toolbar { + display: flex; + + .btn { + margin-right: .25em; + } + } + .content-header { .project-name { align-items: center; @@ -541,3 +549,28 @@ } } } + +.tasks-no-results-container { + grid-column: 1 / -1; + grid-row: 8; + display: none; +} + +.no-results-img { + display: block; + margin: auto; + max-height: 230px; +} + +.no-results-title { + @include font-h1; + margin-bottom: .25em; + margin-top: 1.25em; + text-align: center; +} + +.no-results-description { + @include font-main; + color: $color-silver-chalice; + text-align: center; +} diff --git a/app/assets/stylesheets/global_activities.scss b/app/assets/stylesheets/global_activities.scss index 5c8ff6820..a3ccb1b87 100644 --- a/app/assets/stylesheets/global_activities.scss +++ b/app/assets/stylesheets/global_activities.scss @@ -455,18 +455,19 @@ } .calendar-input { + @include font-button; background-color: transparent !important; + border-color: $color-silver-chalice; box-shadow: none; color: inherit; cursor: pointer; - font-size: 13px; - padding-left: 5px; + padding-left: 10px; padding-right: 34px; position: relative; z-index: 3; &::placeholder { - color: $color-silver-chalice; + color: $color-alto; } } } diff --git a/app/assets/stylesheets/label_templates/show.scss b/app/assets/stylesheets/label_templates/show.scss index 97c01d1d0..8d25a15e4 100644 --- a/app/assets/stylesheets/label_templates/show.scss +++ b/app/assets/stylesheets/label_templates/show.scss @@ -139,7 +139,16 @@ } } - .inser-field-dropdown { + .insert-field-dropdown { + .dimensions-container { + align-items: center; + display: flex; + + img { + margin-top: 27px; + } + } + .open-dropdown-button:not(.collapsed) { .fas { @include rotate(-180deg); @@ -171,7 +180,12 @@ display: flex; padding: 10px 10px 10px 24px; - .fas { + .fas:not(.fa-plus-square) { + margin-left: -1.25em; + margin-right: .25em; + } + + .fa-plus-square { @include font-main; display: none; margin-left: auto; @@ -180,7 +194,7 @@ &:hover { background-color: $color-concrete; - .fas { + .fa-plus-square { display: inline-block; } } diff --git a/app/assets/stylesheets/my_modules.scss b/app/assets/stylesheets/my_modules.scss index 84dc6a309..d7f0e74e6 100644 --- a/app/assets/stylesheets/my_modules.scss +++ b/app/assets/stylesheets/my_modules.scss @@ -235,17 +235,16 @@ } -#experiment-canvas { - [data-view-mode="archived"] { - display: none; - } -} #module-archive { [data-view-mode="active"] { display: none; } + .toolbar { + margin-top: 1em; + } + .module-container { min-width: 220px; diff --git a/app/assets/stylesheets/my_modules/protocols/index.scss b/app/assets/stylesheets/my_modules/protocols/index.scss index 53d604c6c..f6a4e2079 100644 --- a/app/assets/stylesheets/my_modules/protocols/index.scss +++ b/app/assets/stylesheets/my_modules/protocols/index.scss @@ -239,15 +239,21 @@ } .my-module-tags-color { + align-items: center; border-radius: 8px; - display: inline-block; + display: inline-flex; height: 16px; + justify-content: center; margin-right: 5px; width: 16px; + + &.new { + color: $color-silver-chalice; + } } .my-module-tags-create-new { - opacity: .6; + margin-left: 3px; } .input-field { diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss index c3a22ccb6..773a8c1d1 100644 --- a/app/assets/stylesheets/projects.scss +++ b/app/assets/stylesheets/projects.scss @@ -21,7 +21,7 @@ $color-module-hover: $brand-primary; align-items: center; display: flex; - #edit-canvas-button { + #edit-canvas-button, .new-my-module-button { margin-right: 5px; } @@ -48,9 +48,8 @@ $color-module-hover: $brand-primary; } #update-canvas { - .canvas-header { - margin-bottom: 5px; + padding: 1em 2em; } } @@ -89,6 +88,14 @@ $color-module-hover: $brand-primary; overflow: hidden; // for IE10+ touch devices touch-action: none; + + .empty-canvas { + color: $color-volcano; + display: flex; + font-size: 22px; + justify-content: center; + margin-top: 48px; + } } .diagram { @@ -542,6 +549,26 @@ li.module-hover { margin-right: 15px; margin-top: 10px; } + + #manage-module-tags-modal-intro { + padding-left: 15px; + border-top: 0; + width: 568px; + height: 30px; + + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 21px; + + color: $color-volcano; + + flex: none; + order: 1; + align-self: stretch; + flex-grow: 0; + } } @@ -609,17 +636,6 @@ li.module-hover { } } } - - .dropdown-option.users-dropdown-list { - padding: 8px 10px; - - .item-avatar { - border-radius: 50%; - height: 32px; - margin: 0 16px 0 0; - width: 32px; - } - } } .projects-toolbar { diff --git a/app/assets/stylesheets/shared/assets.scss b/app/assets/stylesheets/shared/assets.scss index 73c7bc52a..e2925b5f8 100644 --- a/app/assets/stylesheets/shared/assets.scss +++ b/app/assets/stylesheets/shared/assets.scss @@ -303,7 +303,6 @@ #dropdownAssetContextMenu { background: $color-white; - &:focus, &:active { box-shadow: none; } diff --git a/app/assets/stylesheets/shared/cards.scss b/app/assets/stylesheets/shared/cards.scss index 5377b702a..504e8d7e3 100644 --- a/app/assets/stylesheets/shared/cards.scss +++ b/app/assets/stylesheets/shared/cards.scss @@ -58,8 +58,8 @@ align-items: center; display: flex; height: 2em; - justify-content: center; - width: 2em; + justify-content: left; + width: 1.5em; } } @@ -111,6 +111,7 @@ .checkbox-cell { grid-column: 1; + justify-content: center; position: initial; } } diff --git a/app/assets/stylesheets/shared/comments_sidebar.scss b/app/assets/stylesheets/shared/comments_sidebar.scss index 372989073..3a6a08b2c 100644 --- a/app/assets/stylesheets/shared/comments_sidebar.scss +++ b/app/assets/stylesheets/shared/comments_sidebar.scss @@ -9,7 +9,7 @@ top: var(--navbar-height); transition: width .3s; width: 0; - z-index: 1000; + z-index: 10000; &.open { width: var(--comments-sidebar-width); @@ -131,6 +131,10 @@ .update-buttons { display: block; } + + .send-comment { + display: none; + } } } } diff --git a/app/assets/stylesheets/shared/content_pane.scss b/app/assets/stylesheets/shared/content_pane.scss index a45fd4441..c0914267b 100644 --- a/app/assets/stylesheets/shared/content_pane.scss +++ b/app/assets/stylesheets/shared/content_pane.scss @@ -66,7 +66,7 @@ .dropdown-menu { @include font-button; - min-width: auto; + min-width: 190px; .divider-label { @include font-small; @@ -74,18 +74,41 @@ padding: .25em 1em; } + .divider { + margin: 0; + } + li { cursor: pointer; - padding: 1em; + padding: .5em 1em; white-space: nowrap; .button-icon { margin-right: .5em; } - &:hover { + &:hover:not(.divider-label) { background: $color-concrete; } + + .btn { + height: 36px; + } + + a { + display: inline-block; + margin: -1em; + padding: .5em 1em; + width: calc(100% + 2em); + + &.selected::after { + @include font-awesome; + content: $font-fas-check; + margin-left: auto; + position: absolute; + right: 1em; + } + } } } diff --git a/app/assets/stylesheets/shared/dropdown_selector.scss b/app/assets/stylesheets/shared/dropdown_selector.scss index a0fc99232..812ed63cd 100644 --- a/app/assets/stylesheets/shared/dropdown_selector.scss +++ b/app/assets/stylesheets/shared/dropdown_selector.scss @@ -60,7 +60,7 @@ line-height: 28px; min-width: 0; outline: 0; - padding: 0 0 0 5px; + padding: 0 0 0 10px; &::placeholder { opacity: .7; diff --git a/app/assets/stylesheets/shared/filter_dropdown.scss b/app/assets/stylesheets/shared/filter_dropdown.scss index f1b251247..6840c6f9c 100644 --- a/app/assets/stylesheets/shared/filter_dropdown.scss +++ b/app/assets/stylesheets/shared/filter_dropdown.scss @@ -44,6 +44,20 @@ } } + .item-avatar { + border-radius: 50%; + } + + .dropdown-option.users-dropdown-list { + padding: 8px 10px; + + .item-avatar { + height: 32px; + margin: 0 16px 0 0; + width: 32px; + } + } + .recent-searches { border-top-left-radius: 0; border-top-right-radius: 0; diff --git a/app/assets/stylesheets/shared_styles/constants/fonts.scss b/app/assets/stylesheets/shared_styles/constants/fonts.scss index 8d4982d0a..d02929a38 100644 --- a/app/assets/stylesheets/shared_styles/constants/fonts.scss +++ b/app/assets/stylesheets/shared_styles/constants/fonts.scss @@ -23,6 +23,7 @@ $font-fas-angle-double-left: "\f100"; $font-fas-angle-double-right: "\f101"; $font-fas-exclamation-circle: "\f06a"; $font-fas-caret-up: "\f0d8"; +$font-fas-plus: "\f02b"; @mixin font-h1 { font-size: 24px; diff --git a/app/assets/stylesheets/shared_styles/elements/dropdown.scss b/app/assets/stylesheets/shared_styles/elements/dropdown.scss index 6489c3e44..6b57e2b5f 100644 --- a/app/assets/stylesheets/shared_styles/elements/dropdown.scss +++ b/app/assets/stylesheets/shared_styles/elements/dropdown.scss @@ -75,7 +75,8 @@ } .change-projects-view-type-form, - .change-experiments-view-type-form { + .change-experiments-view-type-form, + .change-my-modules-view-type-form { .button-to { float: unset !important; height: 48px; diff --git a/app/assets/stylesheets/shared_styles/elements/input_fields.scss b/app/assets/stylesheets/shared_styles/elements/input_fields.scss index 12f121c36..9b671e401 100644 --- a/app/assets/stylesheets/shared_styles/elements/input_fields.scss +++ b/app/assets/stylesheets/shared_styles/elements/input_fields.scss @@ -81,6 +81,10 @@ &.error { padding-bottom: 6px; + label { + color: $brand-danger; + } + .sci-input-field { border: $border-danger; } diff --git a/app/assets/stylesheets/shared_styles/elements/toggles.scss b/app/assets/stylesheets/shared_styles/elements/toggles.scss index ab8820633..a93c27ac3 100644 --- a/app/assets/stylesheets/shared_styles/elements/toggles.scss +++ b/app/assets/stylesheets/shared_styles/elements/toggles.scss @@ -95,6 +95,11 @@ input[type="checkbox"].sci-toggle-checkbox { transition: .2s; width: 48px; + svg, + svg path { + fill: $color-black; + } + &:first-of-type { border-left-color: $color-silver-chalice; border-radius: $border-radius-default 0 0 $border-radius-default; @@ -109,6 +114,11 @@ input[type="checkbox"].sci-toggle-checkbox { background: $brand-primary; border: 1px solid $brand-primary; color: $color-white; + + svg, + svg path { + fill: $color-white; + } } } } diff --git a/app/assets/stylesheets/steps.scss b/app/assets/stylesheets/steps.scss index f65fc8b85..61220b967 100644 --- a/app/assets/stylesheets/steps.scss +++ b/app/assets/stylesheets/steps.scss @@ -25,10 +25,11 @@ .step { .panel { + border: 0; margin-left: 0; .panel-body { - padding: 15px 5px; + padding: 15px 24px; } } } diff --git a/app/assets/stylesheets/steps/components/table.scss b/app/assets/stylesheets/steps/components/table.scss index 9c4f74dec..a5854a815 100644 --- a/app/assets/stylesheets/steps/components/table.scss +++ b/app/assets/stylesheets/steps/components/table.scss @@ -10,8 +10,8 @@ .enable-edit-mode { cursor: pointer; - display: none; justify-content: flex-end; + opacity: 0; padding: 12px; position: absolute; right: 0; @@ -49,6 +49,7 @@ .enable-edit-mode { display: flex; + opacity: 1; } } } diff --git a/app/assets/stylesheets/steps/components/text.scss b/app/assets/stylesheets/steps/components/text.scss index 1e56e8c56..2e52e9c70 100644 --- a/app/assets/stylesheets/steps/components/text.scss +++ b/app/assets/stylesheets/steps/components/text.scss @@ -28,7 +28,7 @@ $color-concrete 100% ); border-radius: 4px; - display: none; + opacity: 0; padding-left: 2em; position: absolute; right: 0; @@ -55,6 +55,7 @@ .buttons-container { display: flex; + opacity: 1; } .step-element-grip { diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 5a6a8a430..9717a9fd8 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -201,16 +201,6 @@ mark,.mark { text-align: right; } -a[data-toggle="tooltip"] { - color: inherit; - border-bottom: 1px dashed $color-emperor; - - &:hover { - text-decoration: none; - cursor: help; - } -} - .nav-tabs { margin-bottom: 15px; @@ -686,47 +676,6 @@ ul.double-line > li { } } -#canvas-container, -#module-archive { - .panel-heading { - padding: 10px 15px 4px; - } - - .panel-body { - padding: 6px 15px; - - .status-label { - background-color: var(--state-color); - color: $color-white; - display: inline-block; - margin: 3px 0; - max-width: 100%; - overflow: hidden; - padding: 2px 8px; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - .panel-footer { - .nav > li > a { - padding: 6px 15px; - } - - .btn { - height: 30px; - } - - .badge-indicator { - background: $brand-accent; - border-radius: $border-radius-tag; - color: $color-black; - font-size: 10px; - margin-left: -8px; - } - } -} - .panel-options { position: relative; bottom: 8px; diff --git a/app/assets/stylesheets/tiny_mce.scss b/app/assets/stylesheets/tiny_mce.scss index 399ab7e81..2643ceb28 100644 --- a/app/assets/stylesheets/tiny_mce.scss +++ b/app/assets/stylesheets/tiny_mce.scss @@ -1,4 +1,4 @@ -// scss-lint:disable ImportantRule +// scss-lint:disable ImportantRule SelectorDepth @import "constants"; @font-face { @@ -22,6 +22,11 @@ color: $color-silver-chalice; content: attr(data-placeholder); } + + p { + margin: 0; + padding: 0; + } } .mce-tinymce { @@ -38,6 +43,16 @@ position: relative !important; } +.tox.tox-tinymce { + left: -100000px; + position: absolute; + + &.tox-tinymce--loaded { + left: 0; + position: relative; + } +} + .tinymce-placeholder { background: $color-concrete; opacity: .7; @@ -45,8 +60,15 @@ width: 100%; } -.tinymce-save-button, -.tinymce-cancel-button { +.tox-edit-area { + label { + color: $color-silver-chalice !important; + padding: 5px !important; + } +} + +.tinymce-save-button.tox-mbtn, +.tinymce-cancel-button.tox-mbtn { cursor: pointer; .fas { @@ -54,6 +76,15 @@ font-weight: 900; margin-top: 3px; } + + &:hover { + background: transparent !important; + } +} + +.tinymce-save-controls { + display: flex; + margin-left: auto !important; } .tinymce-status-badge { @@ -69,18 +100,17 @@ background: $color-white !important; } - - .restore-draft-notification { align-items: center; background: $state-info-bg !important; display: flex; + flex-basis: 100%; height: 30px !important; - padding: 0 10px !important; + padding: 10px !important; .notification-text { flex-grow: 1; - max-width: 75%; + max-width: 85%; overflow: hidden; text-overflow: ellipsis; } @@ -152,25 +182,6 @@ &::after { display: none; } - - .mce-container-body.mce-abs-layout { - background: $brand-primary; - position: relative; - top: -10px; - - .mce-container, - .mce-widget { - background: transparent !important; - } - - .mce-btn:hover { - border-color: transparent; - } - - .mce-ico { - color: $color-white; - } - } } .mce-window { @@ -193,4 +204,62 @@ } } -// scss-lint:enable ImportantRule +// fix for TinyMCE 6 vs Boostrap 3 .show conflict +.tox.tox-tinymce.show { + display: flex !important; +} + +.tox .tox-pop { + margin-top: -12px; + + &::after, + &::before { + display: none !important; + } + + .tox-pop__dialog { + border: 0; + border-radius: 0 0 3px 3px; + box-shadow: none; + } + + .tox-toolbar { + background: $brand-primary !important; + top: -10px; + + button { + color: $color-white; + } + + .tox-icon svg { + fill: $color-white; + } + } +} + +.tox-edit-area__iframe { + background-color: transparent !important; + z-index: 1; +} + +.tox-sidebar-wrap { + flex-direction: column !important; + + .restore-draft-notification { + flex-basis: 30px; + } +} + +.tox-editor-header { + z-index: 2 !important; +} + +.tox-dialog-wrap { + .tox-dialog__body-nav { + .tox-dialog__body-nav-item:nth-child(3) { + display: none; + } + } +} + +// scss-lint:enable ImportantRule SelectorDepth diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 1f0836d97..8206d342c 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -3,8 +3,16 @@ module Api module V1 class UsersController < BaseController + before_action :load_team, only: :index before_action :load_user, only: :show + def index + users = @team.users + .page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + render jsonapi: users, each_serializer: UserSerializer + end + def show render jsonapi: @user, serializer: UserSerializer end diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 5fef41b75..9330e153a 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -218,7 +218,7 @@ class AssetsController < ApplicationController log_step_activity( :task_step_file_deleted, @assoc, - @assoc.my_module.experiment.project, + @assoc.my_module.project, my_module: @assoc.my_module.id, file: @asset.file_name ) @@ -298,7 +298,7 @@ class AssetsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: step.protocol, - team: current_team, + team: step.protocol.team, project: project, message_items: message_items) end @@ -308,8 +308,8 @@ class AssetsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: result, - team: result.my_module.experiment.project.team, - project: result.my_module.experiment.project, + team: result.my_module.team, + project: result.my_module.project, message_items: { result: result.id, type_of_result: t('activities.result_type.text') diff --git a/app/controllers/concerns/assets_actions.rb b/app/controllers/concerns/assets_actions.rb index a7c769730..9971d2433 100644 --- a/app/controllers/concerns/assets_actions.rb +++ b/app/controllers/concerns/assets_actions.rb @@ -41,8 +41,8 @@ module AssetsActions .call(activity_type: :edit_image_on_result, owner: current_user, subject: asset.result, - team: my_module.experiment.project.team, - project: my_module.experiment.project, + team: my_module.team, + project: my_module.project, message_items: { result: asset.result.id, asset_name: { id: asset.id, value_for: 'file_name' }, diff --git a/app/controllers/concerns/bio_eddie_actions.rb b/app/controllers/concerns/bio_eddie_actions.rb index 8926853f4..5905650dd 100644 --- a/app/controllers/concerns/bio_eddie_actions.rb +++ b/app/controllers/concerns/bio_eddie_actions.rb @@ -104,8 +104,8 @@ module BioEddieActions .call(activity_type: "#{activity}_molecule_on_result".to_sym, owner: current_user, subject: result, - team: my_module.experiment.project.team, - project: my_module.experiment.project, + team: my_module.team, + project: my_module.project, message_items: message_items) end end diff --git a/app/controllers/concerns/marvin_js_actions.rb b/app/controllers/concerns/marvin_js_actions.rb index a7711da89..981678c2a 100644 --- a/app/controllers/concerns/marvin_js_actions.rb +++ b/app/controllers/concerns/marvin_js_actions.rb @@ -118,8 +118,8 @@ module MarvinJsActions .call(activity_type: (activity + '_chemical_structure_on_result').to_sym, owner: current_user, subject: result, - team: my_module.experiment.project.team, - project: my_module.experiment.project, + team: my_module.team, + project: my_module.project, message_items: message_items) end @@ -137,8 +137,8 @@ module MarvinJsActions .call(activity_type: (activity + '_chemical_structure_on_task').to_sym, owner: current_user, subject: my_module, - team: my_module.experiment.project.team, - project: my_module.experiment.project, + team: my_module.team, + project: my_module.project, message_items: message_items) end diff --git a/app/controllers/concerns/steps_actions.rb b/app/controllers/concerns/steps_actions.rb index a8de5ca0b..73b40842b 100644 --- a/app/controllers/concerns/steps_actions.rb +++ b/app/controllers/concerns/steps_actions.rb @@ -55,6 +55,17 @@ module StepsActions ) end + def step_text_annotation(step, step_text, old_text = nil) + smart_annotation_notification( + old_text: old_text, + new_text: step_text.text, + title: t('notifications.step_text_title', + user: current_user.full_name, + step: step.name), + message: annotation_message(step) + ) + end + def checklist_name_annotation(step, checklist, old_text = nil) smart_annotation_notification( old_text: old_text, @@ -85,7 +96,7 @@ module StepsActions ), experiment: link_to( step.my_module.experiment.name, - canvas_experiment_url(step.my_module.experiment) + my_modules_experiment_url(step.my_module.experiment) ), my_module: link_to( step.my_module.name, diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index 07cd60704..13eb02177 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -12,12 +12,12 @@ class ExperimentsController < ApplicationController before_action :check_read_permissions, except: %i(edit archive clone move new create archive_group restore_group) before_action :check_canvas_read_permissions, only: %i(canvas) before_action :check_create_permissions, only: %i(new create) - before_action :check_manage_permissions, only: %i(edit) + before_action :check_manage_permissions, only: %i(edit batch_clone_my_modules) before_action :check_update_permissions, only: %i(update) before_action :check_archive_permissions, only: :archive before_action :check_clone_permissions, only: %i(clone_modal clone) before_action :check_move_permissions, only: %i(move_modal move) - before_action :set_inline_name_editing, only: %i(canvas module_archive) + before_action :set_inline_name_editing, only: %i(canvas table module_archive) layout 'fluid' @@ -46,7 +46,7 @@ class ExperimentsController < ApplicationController experiment: @experiment.name) respond_to do |format| format.json do - render json: { path: canvas_experiment_url(@experiment) }, status: :ok + render json: { path: my_modules_experiment_url(@experiment) }, status: :ok end end else @@ -88,6 +88,37 @@ class ExperimentsController < ApplicationController .select('my_modules.*').group(:id) end + def table + @project = @experiment.project + @experiment.current_view_state(current_user) + @my_module_visible_table_columns = current_user.my_module_visible_table_columns + end + + def load_table + my_modules = @experiment.my_modules.readable_by_user(current_user) + + unless @experiment.archived_branch? + my_modules = params[:view_mode] == 'archived' ? my_modules.archived : my_modules.active + end + + render json: Experiments::TableViewService.new(@experiment, my_modules, current_user, params).call + end + + def my_modules + view_state = @experiment.current_view_state(current_user) + view_type = view_state.state['my_modules']['view_type'] || 'canvas' + + redirect_to view_mode_redirect_url(view_type) + end + + def view_type + view_state = @experiment.current_view_state(current_user) + view_state.state['my_modules']['view_type'] = view_type_params + view_state.save! + + redirect_to view_mode_redirect_url(view_type_params) + end + def edit respond_to do |format| format.json do @@ -208,7 +239,8 @@ class ExperimentsController < ApplicationController format.json do render json: { html: render_to_string( - partial: 'clone_modal.html.erb' + partial: 'clone_modal.html.erb', + locals: { view_mode: params[:view_mode] } ) } end @@ -249,6 +281,21 @@ class ExperimentsController < ApplicationController end end + def search_tags + tags = @experiment.project.tags.where.not(id: JSON.parse(params[:selected_tags])) + .where_attributes_like(:name, params[:query]) + .select(:id, :name, :color) + + tags = tags.map do |tag| + { value: tag.id, label: sanitize_input(tag.name), params: { color: sanitize_input(tag.color) } } + end + + if params[:query].present? && tags.select { |tag| tag[:label] == params[:query] }.blank? + tags << { value: 0, label: sanitize_input(params[:query]), params: { color: nil } } + end + render json: tags + end + # POST: move_experiment(id) def move service = Experiments::MoveToProjectService @@ -258,8 +305,10 @@ class ExperimentsController < ApplicationController if service.succeed? flash[:success] = t('experiments.move.success_flash', experiment: @experiment.name) - path = canvas_experiment_url(@experiment) status = :ok + view_state = @experiment.current_view_state(current_user) + view_type = view_state.state['my_modules']['view_type'] || 'canvas' + path = view_mode_redirect_url(view_type) else message = service.errors.values.join(', ') status = :unprocessable_entity @@ -268,7 +317,53 @@ class ExperimentsController < ApplicationController render json: { message: message, path: path }, status: status end + def move_modules_modal + @experiments = @experiment.project.experiments.active.where.not(id: @experiment) + .managable_by_user(current_user).order(name: :asc) + render json: { + html: render_to_string( + partial: 'move_modules_modal.html.erb' + ) + } + end + + def move_modules + modules_to_move = {} + dst_experiment = @experiment.project.experiments.find(params[:to_experiment_id]) + return render_403 unless can_manage_experiment?(dst_experiment) + + @experiment.transaction do + params[:my_module_ids].each do |id| + my_module = @experiment.my_modules.find(id) + return render_403 unless can_move_my_module?(my_module) + + modules_to_move[id] = dst_experiment.id + end + # Make sure that locks are acquired always in the same order + if dst_experiment.id < @experiment.id + dst_experiment.lock! && @experiment.lock! + else + @experiment.lock! && dst_experiment.lock! + end + @experiment.move_modules(modules_to_move, current_user) + @experiment.workflowimg.purge + + render json: { message: t('experiments.table.modal_move_modules.success_flash', + experiment: sanitize_input(dst_experiment.name)) } + rescue StandardError => e + Rails.logger.error(e.message) + Rails.logger.error(e.backtrace.join("\n")) + render json: { + message: t('experiments.table.modal_move_modules.error_flash', experiment: sanitize_input(dst_experiment.name)) + }, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + rescue ActiveRecord::RecordNotFound + render_404 + end + def module_archive + @project = @experiment.project @my_modules = @experiment.archived_branch? ? @experiment.my_modules : @experiment.my_modules.archived @my_modules = @my_modules.with_granted_permissions(current_user, MyModulePermissions::READ_ARCHIVED) .left_outer_joins(:designated_users, :task_comments) @@ -299,11 +394,27 @@ class ExperimentsController < ApplicationController end def sidebar + view_state = @experiment.current_view_state(current_user) + view_mode = params[:view_mode].presence || 'active' + default_sort = view_state.state.dig('my_modules', view_mode, 'sort') || 'atoz' + my_modules = if @experiment.archived_branch? + @experiment.my_modules + elsif params[:view_mode] == 'archived' + @experiment.my_modules.archived + else + @experiment.my_modules.active + end + + my_modules = sort_my_modules(my_modules, params[:sort].presence || default_sort) respond_to do |format| format.json do render json: { html: render_to_string( - partial: 'shared/sidebar/my_modules.html.erb', locals: { experiment: @experiment } + partial: if params[:view_mode] == 'archived' + 'shared/sidebar/archived_my_modules.html.erb' + else + 'shared/sidebar/my_modules.html.erb' + end, locals: { experiment: @experiment, my_modules: my_modules } ) } end @@ -321,6 +432,73 @@ class ExperimentsController < ApplicationController end end + def assigned_users_to_tasks + users = current_team.users.where(id: @experiment.my_modules.joins(:user_my_modules).select(:user_id)) + .search(false, params[:query]).map do |u| + { value: u.id, label: sanitize_input(u.name), params: { avatar_url: avatar_path(u, :icon_small) } } + end + + render json: users, status: :ok + end + + def archive_my_modules + my_modules = @experiment.my_modules.where(id: params[:my_modules]) + counter = 0 + my_modules.each do |my_module| + next unless can_archive_my_module?(my_module) + + my_module.transaction do + connect_my_modules_before_archive(my_module) + + my_module.archive!(current_user) + log_my_module_activity(:archive_module, my_module) + counter += 1 + rescue StandardError => e + Rails.logger.error e.message + raise ActiveRecord::Rollback + end + end + if counter.positive? + render json: { message: t('experiments.table.archive_group.success_flash', number: counter) } + else + render json: { message: t('experiments.table.archive_group.error_flash') }, status: :unprocessable_entity + end + end + + def batch_clone_my_modules + MyModule.transaction do + @my_modules = + @experiment.my_modules + .readable_by_user(current_user) + .where(id: params[:my_module_ids]) + + @my_modules.find_each do |my_module| + new_my_module = my_module.dup + new_my_module.my_module_status = MyModuleStatus.first + new_my_module.update!( + { + provisioning_status: :in_progress, + name: my_module.next_clone_name, + created_by: current_user, + due_date: nil, + started_on: nil, + state: 'uncompleted', + completed_on: nil + }.merge(new_my_module.get_new_position) + ) + new_my_module.designated_users << current_user + MyModules::CopyContentJob.perform_later(current_user, my_module.id, new_my_module.id) + end + @experiment.workflowimg.purge + end + + render( + json: { + provisioning_status_urls: @my_modules.map { |m| provisioning_status_my_module_url(m) } + } + ) + end + private def load_experiment @@ -341,10 +519,14 @@ class ExperimentsController < ApplicationController params.require(:experiment).require(:project_id) end + def view_type_params + params.require(:experiment).require(:view_type) + end + def check_read_permissions current_team_switch(@experiment.project.team) if current_team != @experiment.project.team render_403 unless can_read_experiment?(@experiment) || - @experiment.archived? && can_read_archived_experiment?(@experiment) + (@experiment.archived? && can_read_archived_experiment?(@experiment)) end def check_canvas_read_permissions @@ -402,7 +584,7 @@ class ExperimentsController < ApplicationController project: link_to(@experiment.project.name, project_url(@experiment.project)), experiment: link_to(@experiment.name, - canvas_experiment_url(@experiment))) + my_modules_experiment_url(@experiment))) ) end @@ -410,9 +592,61 @@ class ExperimentsController < ApplicationController Activities::CreateActivityService .call(activity_type: type_of, owner: current_user, - team: experiment.project.team, + team: experiment.team, project: experiment.project, subject: experiment, message_items: { experiment: experiment.id }) end + + def log_my_module_activity(type_of, my_module) + Activities::CreateActivityService + .call(activity_type: type_of, + owner: current_user, + team: my_module.experiment.project.team, + project: my_module.experiment.project, + subject: my_module, + message_items: { my_module: my_module.id }) + end + + def view_mode_redirect_url(view_type) + if params[:view_mode] == 'archived' || @experiment.archived_branch? + case view_type + when 'canvas' + module_archive_experiment_path(@experiment) + else + table_experiment_path(@experiment, view_mode: :archived) + end + else + view_type == 'canvas' ? canvas_experiment_path(@experiment) : table_experiment_path(@experiment) + end + end + + def sort_my_modules(records, sort) + case sort + when 'due_first' + records.order(:due_date, :name) + when 'due_last' + records.order(Arel.sql("COALESCE(due_date, DATE '2100-01-01') DESC"), :name) + when 'atoz' + records.order(:name) + when 'ztoa' + records.order(name: :desc) + when 'archived_old' + records.order(Arel.sql('COALESCE(my_modules.archived_on, my_modules.archived_on) ASC')) + when 'archived_new' + records.order(Arel.sql('COALESCE(my_modules.archived_on, my_modules.archived_on) DESC')) + else + records + end + end + + def connect_my_modules_before_archive(my_module) + return if my_module.my_modules.empty? || my_module.my_module_antecessors.empty? + + my_module.my_modules.each do |destination_my_module| + my_module.my_module_antecessors.each do |source_my_module| + Connection.create!(input_id: destination_my_module.id, output_id: source_my_module.id) + end + end + end end diff --git a/app/controllers/label_templates_controller.rb b/app/controllers/label_templates_controller.rb index a1d235a94..1577219df 100644 --- a/app/controllers/label_templates_controller.rb +++ b/app/controllers/label_templates_controller.rb @@ -7,7 +7,7 @@ class LabelTemplatesController < ApplicationController before_action :check_view_permissions, except: %i(create duplicate set_default delete update) before_action :check_manage_permissions, only: %i(create duplicate set_default delete update) before_action :load_label_templates, only: %i(index datatable) - before_action :load_label_template, only: %i(show set_default update) + before_action :load_label_template, only: %i(show set_default update template_tags) layout 'fluid' @@ -125,7 +125,7 @@ class LabelTemplatesController < ApplicationController end def template_tags - render json: LabelTemplates::TagService.new(current_team).tags + render json: LabelTemplates::TagService.new(current_team, @label_template).tags end def zpl_preview diff --git a/app/controllers/my_module_repositories_controller.rb b/app/controllers/my_module_repositories_controller.rb index 22d81c158..5b5bee11f 100644 --- a/app/controllers/my_module_repositories_controller.rb +++ b/app/controllers/my_module_repositories_controller.rb @@ -135,7 +135,7 @@ class MyModuleRepositoriesController < ApplicationController activity_type: :export_inventory_items_assigned_to_task, owner: current_user, subject: @my_module, - team: current_team, + team: @repository.team, message_items: { my_module: @my_module.id, repository: @repository.id @@ -240,7 +240,7 @@ class MyModuleRepositoriesController < ApplicationController user: current_user.full_name), message: t('notifications.my_module_consumption_comment_annotation_message_html', project: link_to(@my_module.experiment.project.name, project_url(@my_module.experiment.project)), - experiment: link_to(@my_module.experiment.name, canvas_experiment_url(@my_module.experiment)), + experiment: link_to(@my_module.experiment.name, my_modules_experiment_url(@my_module.experiment)), my_module: link_to(@my_module.name, protocols_my_module_url(@my_module))) ) end @@ -251,7 +251,7 @@ class MyModuleRepositoriesController < ApplicationController owner: current_user, subject: @my_module, team: @repository.team, - project: @my_module.experiment.project, + project: @my_module.project, message_items: { repository: @repository.id, repository_row: module_repository_row.repository_row_id, diff --git a/app/controllers/my_module_repository_snapshots_controller.rb b/app/controllers/my_module_repository_snapshots_controller.rb index 0bce20eb6..f3b5430e1 100644 --- a/app/controllers/my_module_repository_snapshots_controller.rb +++ b/app/controllers/my_module_repository_snapshots_controller.rb @@ -109,7 +109,7 @@ class MyModuleRepositorySnapshotsController < ApplicationController activity_type: :export_inventory_snapshot_items_assigned_to_task, owner: current_user, subject: @my_module, - team: current_team, + team: @my_module.team, message_items: { my_module: @my_module.id, repository_snapshot: @repository_snapshot.id, diff --git a/app/controllers/my_module_tags_controller.rb b/app/controllers/my_module_tags_controller.rb index a62025545..0235505d5 100644 --- a/app/controllers/my_module_tags_controller.rb +++ b/app/controllers/my_module_tags_controller.rb @@ -70,9 +70,8 @@ class MyModuleTagsController < ApplicationController .call(activity_type: :add_task_tag, owner: current_user, subject: my_module, - project: - my_module.experiment.project, - team: current_team, + project: my_module.project, + team: my_module.team, message_items: { my_module: my_module.id, tag: @mt.tag.id @@ -95,9 +94,8 @@ class MyModuleTagsController < ApplicationController .call(activity_type: :remove_task_tag, owner: current_user, subject: @mt.my_module, - project: - @mt.my_module.experiment.project, - team: current_team, + project: @mt.my_module.project, + team: @mt.my_module.team, message_items: { my_module: @mt.my_module.id, tag: @mt.tag.id @@ -139,9 +137,8 @@ class MyModuleTagsController < ApplicationController .call(activity_type: :remove_task_tag, owner: current_user, subject: tag.my_module, - project: - tag.my_module.experiment.project, - team: current_team, + project: tag.my_module.project, + team: tag.my_module.team, message_items: { my_module: tag.my_module.id, tag: tag.tag.id diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 231077b07..f2ac67a1c 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -5,19 +5,69 @@ class MyModulesController < ApplicationController include Rails.application.routes.url_helpers include ActionView::Helpers::UrlHelper include ApplicationHelper + include MyModulesHelper - before_action :load_vars, except: %i(restore_group) + before_action :load_vars, except: %i(restore_group create new save_table_state) + before_action :load_experiment, only: %i(create new) + before_action :check_create_permissions, only: %i(new create) before_action :check_archive_permissions, only: %i(update) before_action :check_manage_permissions, only: %i( description due_date update_description update_protocol_description update_protocol ) - before_action :check_read_permissions, except: %i(update update_description update_protocol_description restore_group) + before_action :check_read_permissions, except: %i(create new update update_description + update_protocol_description restore_group save_table_state) before_action :check_update_state_permissions, only: :update_state before_action :set_inline_name_editing, only: %i(protocols results activities archive) before_action :load_experiment_my_modules, only: %i(protocols results activities archive) layout 'fluid'.freeze + def new + @my_module = @experiment.my_modules.new + assigned_users = User.where(id: @experiment.user_assignments.select(:user_id)) + + render json: { + html: render_to_string( + partial: 'my_modules/modals/new_modal.html.erb', locals: { view_mode: params[:view_mode], + users: assigned_users } + ) + } + end + + def create + @my_module = @experiment.my_modules.new(my_module_params) + new_pos = @my_module.get_new_position + @my_module.assign_attributes( + created_by: current_user, + last_modified_by: current_user, + x: new_pos[:x], + y: new_pos[:y] + ) + @my_module.transaction do + if my_module_tags_params[:tag_ids].present? + @my_module.tags << @experiment.project.tags.where(id: JSON.parse(my_module_tags_params[:tag_ids])) + end + if my_module_designated_users_params[:user_ids].present? && can_designate_users_to_new_task?(@experiment) + @my_module.designated_users << @experiment.users.where(id: my_module_designated_users_params[:user_ids]) + elsif !can_designate_users_to_new_task?(@experiment) + @my_module.designated_users << current_user + end + @my_module.save! + Activities::CreateActivityService.call( + activity_type: :create_module, + owner: current_user, + team: @my_module.experiment.project.team, + project: @my_module.experiment.project, + subject: @my_module, + message_items: { my_module: @my_module.id } + ) + redirect_to canvas_experiment_path(@experiment) if params[:my_module][:view_mode] == 'canvas' + rescue ActiveRecord::RecordInvalid + render json: @my_module.errors, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + end + def show respond_to do |format| format.json { @@ -46,6 +96,11 @@ class MyModulesController < ApplicationController end end + def save_table_state + current_user.settings.update(visible_my_module_table_columns: params[:columns]) + current_user.save! + end + def status_state respond_to do |format| format.json do @@ -195,6 +250,11 @@ class MyModulesController < ApplicationController partial: 'my_modules/card_due_date_label.html.erb', locals: { my_module: @my_module } ), + table_due_date_label: { + html: render_to_string(partial: 'experiments/table_due_date_label.html.erb', + locals: { my_module: @my_module, user: current_user }), + due_status: my_module_due_status(@my_module) + }, module_header_due_date: render_to_string( partial: 'my_modules/module_header_due_date.html.erb', locals: { my_module: @my_module } @@ -276,12 +336,14 @@ class MyModulesController < ApplicationController def update_protocol protocol = @my_module.protocol + old_description = protocol.description ActiveRecord::Base.transaction do protocol.update!(protocol_params) log_activity(:protocol_name_in_task_edited) if protocol.saved_change_to_name? log_activity(:protocol_description_in_task_edited) if protocol.saved_change_to_description? TinyMceAsset.update_images(protocol, params[:tiny_mce_images], current_user) + protocol_annotation_notification(old_description) end render json: protocol, serializer: ProtocolSerializer, user: current_user @@ -334,7 +396,12 @@ class MyModulesController < ApplicationController else flash[:error] = t('my_modules.restore_group.error_flash') end - redirect_to module_archive_experiment_path(experiment) + + if params[:view] == 'table' + redirect_to table_experiment_path(experiment, view_mode: :archived) + else + redirect_to module_archive_experiment_path(experiment) + end end def update_state @@ -349,6 +416,32 @@ class MyModulesController < ApplicationController end end + def permissions + if stale?(@my_module) + render json: { + editable: can_manage_my_module?(@my_module), + moveable: can_move_my_module?(@my_module), + archivable: can_archive_my_module?(@my_module), + restorable: can_restore_my_module?(@my_module) + } + end + end + + def actions_dropdown + if stale?(@my_module) + render json: { + html: render_to_string( + partial: 'experiments/table_row_actions', + locals: { my_module: @my_module } + ) + } + end + end + + def provisioning_status + render json: { provisioning_status: @my_module.provisioning_status } + end + private def load_vars @@ -361,6 +454,11 @@ class MyModulesController < ApplicationController end end + def load_experiment + @experiment = Experiment.preload(user_assignments: %i(user user_role)).find_by(id: params[:id]) + render_404 unless @experiment + end + def load_experiment_my_modules @experiment_my_modules = if @my_module.experiment.archived_branch? @my_module.experiment.my_modules.order(:name) @@ -369,6 +467,10 @@ class MyModulesController < ApplicationController end end + def check_create_permissions + render_403 && return unless can_manage_experiment?(@experiment) + end + def check_manage_permissions render_403 && return unless can_manage_my_module?(@my_module) end @@ -401,18 +503,26 @@ class MyModulesController < ApplicationController end def my_module_params - update_params = params.require(:my_module).permit(:name, :description, :started_on, :due_date, :archived) + permitted_params = params.require(:my_module).permit(:name, :description, :started_on, :due_date, :archived) - if update_params[:started_on].present? - update_params[:started_on] = - Time.zone.strptime(update_params[:started_on], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M') + if permitted_params[:started_on].present? + permitted_params[:started_on] = + Time.zone.strptime(permitted_params[:started_on], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M') end - if update_params[:due_date].present? - update_params[:due_date] = - Time.zone.strptime(update_params[:due_date], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M') + if permitted_params[:due_date].present? + permitted_params[:due_date] = + Time.zone.strptime(permitted_params[:due_date], I18n.backend.date_format.dup.gsub(/%-/, '%') + ' %H:%M') end - update_params + permitted_params + end + + def my_module_tags_params + params.require(:my_module).permit(:tag_ids) + end + + def my_module_designated_users_params + params.require(:my_module).permit(user_ids: []) end def protocol_params @@ -458,8 +568,8 @@ class MyModulesController < ApplicationController Activities::CreateActivityService .call(activity_type: type_of, owner: current_user, - team: my_module.experiment.project.team, - project: my_module.experiment.project, + team: my_module.team, + project: my_module.project, subject: my_module, message_items: message_items) end @@ -479,7 +589,7 @@ class MyModulesController < ApplicationController user: current_user.full_name), message: t('notifications.my_module_description_annotation_message_html', project: link_to(@my_module.experiment.project.name, project_url(@my_module.experiment.project)), - experiment: link_to(@my_module.experiment.name, canvas_experiment_url(@my_module.experiment)), + experiment: link_to(@my_module.experiment.name, my_modules_experiment_url(@my_module.experiment)), my_module: link_to(@my_module.name, protocols_my_module_url(@my_module))) ) end @@ -493,7 +603,7 @@ class MyModulesController < ApplicationController user: current_user.full_name), message: t('notifications.my_module_protocol_annotation_message_html', project: link_to(@my_module.experiment.project.name, project_url(@my_module.experiment.project)), - experiment: link_to(@my_module.experiment.name, canvas_experiment_url(@my_module.experiment)), + experiment: link_to(@my_module.experiment.name, my_modules_experiment_url(@my_module.experiment)), my_module: link_to(@my_module.name, protocols_my_module_url(@my_module))) ) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 126f6f0a7..2a449de32 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -13,7 +13,7 @@ class ProjectsController < ApplicationController before_action :switch_team_with_param, only: :index before_action :load_vars, only: %i(show permissions edit update notifications - sidebar experiments_cards view_type actions_dropdown) + sidebar experiments_cards view_type actions_dropdown create_tag) before_action :load_current_folder, only: %i(index cards new show) before_action :check_view_permissions, except: %i(index cards new create edit update archive_group restore_group users_filter actions_dropdown) @@ -264,6 +264,24 @@ class ProjectsController < ApplicationController end end + def create_tag + render_403 unless can_manage_project_tags?(@project) + + @tag = @project.tags.create(tag_params.merge({ + created_by: current_user, + last_modified_by: current_user, + color: Constants::TAG_COLORS.sample + })) + + render json: { + tag: { + id: @tag.id, + name: @tag.name, + color: @tag.color + } + } + end + def restore_group projects = current_team.projects.archived.where(id: params[:projects_ids]) counter = 0 @@ -287,9 +305,6 @@ class ProjectsController < ApplicationController end def show - # This is the "info" view - current_team_switch(@project.team) - view_state = @project.current_view_state(current_user) @current_sort = view_state.state.dig('experiments', experiments_view_mode(@project), 'sort') || 'atoz' @current_view_type = view_state.state.dig('experiments', 'view_type') @@ -372,11 +387,15 @@ class ProjectsController < ApplicationController end def load_vars - @project = Project.find_by(id: params[:id]) + @project = Project.find_by(id: params[:id] || params[:project_id]) render_404 unless @project end + def tag_params + params.require(:tag).permit(:name) + end + def load_current_folder if current_team && params[:project_folder_id].present? @current_folder = current_team.project_folders.find_by(id: params[:project_folder_id]) @@ -386,6 +405,7 @@ class ProjectsController < ApplicationController end def check_view_permissions + current_team_switch(@project.team) if current_team != @project.team render_403 unless can_read_project?(@project) end diff --git a/app/controllers/protocols_controller.rb b/app/controllers/protocols_controller.rb index 04aaa2e02..97ce37c15 100644 --- a/app/controllers/protocols_controller.rb +++ b/app/controllers/protocols_controller.rb @@ -1,4 +1,3 @@ - class ProtocolsController < ApplicationController include RenamingUtil include ActionView::Helpers::TextHelper @@ -16,6 +15,7 @@ class ProtocolsController < ApplicationController before_action :check_clone_permissions, only: [:clone] before_action :check_view_permissions, only: %i( show + edit protocol_status_bar updated_at_label preview @@ -30,7 +30,6 @@ class ProtocolsController < ApplicationController # For update_from_parent and update_from_parent_modal we don't need to check # read permission for the parent protocol before_action :check_manage_permissions, only: %i( - edit update_keywords update_description update_name @@ -161,14 +160,10 @@ class ProtocolsController < ApplicationController end def edit - # Switch to correct team - current_team_switch(@protocol.team) + render :show end def show - # Switch to correct team - current_team_switch(@protocol.team) - respond_to do |format| format.json { render json: @protocol, serializer: ProtocolSerializer, user: current_user } format.html @@ -256,9 +251,7 @@ class ProtocolsController < ApplicationController end def delete_steps - @protocol.my_module.lock! - - Protocol.transaction do + @protocol.with_lock do team = @protocol.team previous_size = 0 @protocol.steps.each do |step| @@ -276,7 +269,6 @@ class ProtocolsController < ApplicationController # skip adjusting positions after destroy as this is a bulk delete step.skip_position_adjust = true - step.destroy! end @@ -622,7 +614,7 @@ class ProtocolsController < ApplicationController .call(activity_type: :import_protocol_in_repository, owner: current_user, subject: protocol, - team: current_team, + team: protocol.team, message_items: { protocol: protocol.id }) @@ -820,15 +812,15 @@ class ProtocolsController < ApplicationController file_name = 'protocols.eln' end - @protocols.each do |p| + @protocols.each do |protocol| if params[:my_module_id] my_module = MyModule.find(params[:my_module_id]) Activities::CreateActivityService .call(activity_type: :export_protocol_from_task, owner: current_user, - project: my_module.experiment.project, + project: my_module.project, subject: my_module, - team: current_team, + team: my_module.team, message_items: { my_module: params[:my_module_id].to_i }) @@ -836,10 +828,10 @@ class ProtocolsController < ApplicationController Activities::CreateActivityService .call(activity_type: :export_protocol_in_repository, owner: current_user, - subject: p, - team: current_team, + subject: protocol, + team: protocol.team, message_items: { - protocol: p.id + protocol: protocol.id }) end end @@ -1102,6 +1094,7 @@ class ProtocolsController < ApplicationController def check_view_permissions @protocol = Protocol.find_by_id(params[:id]) + current_team_switch(@protocol.team) if current_team != @protocol.team unless @protocol.present? && (can_read_protocol_in_module?(@protocol) || can_read_protocol_in_repository?(@protocol)) @@ -1232,7 +1225,7 @@ class ProtocolsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @protocol, - team: current_team, + team: @protocol.team, project: project, message_items: message_items) end @@ -1245,7 +1238,7 @@ class ProtocolsController < ApplicationController user: current_user.full_name, protocol: @protocol.name), message: t('notifications.protocol_description_annotation_message_html', - protocol: link_to(@protocol.name, edit_protocol_url(@protocol))) + protocol: link_to(@protocol.name, protocol_url(@protocol))) ) end end diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 04c738e77..b629dce3b 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -110,7 +110,7 @@ class ReportsController < ApplicationController @project_contents = { experiments: @report.report_elements.order(:position).experiment.pluck(:experiment_id), my_modules: @report.report_elements.order(:position).my_module.pluck(:my_module_id), - repositories: @report.report_elements.my_module_repository.distinct(:repository_id).pluck(:repository_id) + repositories: @report.settings.dig(:task, :repositories) } render :new end diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 139502711..e68ec7dfd 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -521,7 +521,7 @@ class RepositoriesController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @repository, - team: current_team, + team: @repository.team, message_items: message_items) end end diff --git a/app/controllers/repository_columns_controller.rb b/app/controllers/repository_columns_controller.rb index 254dd5d21..811b724e3 100644 --- a/app/controllers/repository_columns_controller.rb +++ b/app/controllers/repository_columns_controller.rb @@ -167,7 +167,7 @@ class RepositoryColumnsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @repository, - team: current_team, + team: @repository.team, message_items: { repository_column: @repository_column.id, repository: @repository.id diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index da8ec005d..35cfaca3f 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -432,7 +432,7 @@ class RepositoryRowsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: repository_row, - team: current_team, + team: @repository.team, message_items: { repository_row: repository_row.id, repository: @repository.id diff --git a/app/controllers/result_assets_controller.rb b/app/controllers/result_assets_controller.rb index bfd636875..1045e113c 100644 --- a/app/controllers/result_assets_controller.rb +++ b/app/controllers/result_assets_controller.rb @@ -191,8 +191,8 @@ class ResultAssetsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: result, - team: @my_module.experiment.project.team, - project: @my_module.experiment.project, + team: @my_module.team, + project: @my_module.project, message_items: { result: result.id, type_of_result: t('activities.result_type.asset') diff --git a/app/controllers/result_tables_controller.rb b/app/controllers/result_tables_controller.rb index 110a4571b..4c386e680 100644 --- a/app/controllers/result_tables_controller.rb +++ b/app/controllers/result_tables_controller.rb @@ -180,8 +180,8 @@ class ResultTablesController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @result, - team: @my_module.experiment.project.team, - project: @my_module.experiment.project, + team: @my_module.team, + project: @my_module.project, message_items: { result: @result.id, type_of_result: t('activities.result_type.table') diff --git a/app/controllers/result_texts_controller.rb b/app/controllers/result_texts_controller.rb index 280610e43..0e6f0b21f 100644 --- a/app/controllers/result_texts_controller.rb +++ b/app/controllers/result_texts_controller.rb @@ -192,8 +192,8 @@ class ResultTextsController < ApplicationController .experiment .project)), experiment: link_to(@result.my_module.experiment.name, - canvas_experiment_url(@result.my_module - .experiment)), + my_modules_experiment_url(@result.my_module + .experiment)), my_module: link_to(@result.my_module.name, protocols_my_module_url( @result.my_module @@ -206,8 +206,8 @@ class ResultTextsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @result, - team: @my_module.experiment.project.team, - project: @my_module.experiment.project, + team: @my_module.team, + project: @my_module.project, message_items: { result: @result.id, type_of_result: t('activities.result_type.text') diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb index c231786fd..220ee813d 100644 --- a/app/controllers/results_controller.rb +++ b/app/controllers/results_controller.rb @@ -14,8 +14,8 @@ class ResultsController < ApplicationController .call(activity_type: :destroy_result, owner: current_user, subject: @result, - team: @my_module.experiment.project.team, - project: @my_module.experiment.project, + team: @my_module.team, + project: @my_module.project, message_items: { result: @result.id, type_of_result: result_type }) flash[:success] = t('my_modules.module_archive.delete_flash', diff --git a/app/controllers/step_comments_controller.rb b/app/controllers/step_comments_controller.rb index 714c232a0..c7ecfa5f0 100644 --- a/app/controllers/step_comments_controller.rb +++ b/app/controllers/step_comments_controller.rb @@ -78,8 +78,8 @@ class StepCommentsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @protocol, - team: current_team, - project: @step.my_module.experiment.project, + team: @step.my_module.team, + project: @step.my_module.project, message_items: { my_module: @step.my_module.id, step: @step.id, diff --git a/app/controllers/step_elements/base_controller.rb b/app/controllers/step_elements/base_controller.rb index 2c41d5479..4ceed0c1d 100644 --- a/app/controllers/step_elements/base_controller.rb +++ b/app/controllers/step_elements/base_controller.rb @@ -40,8 +40,8 @@ module StepElements Activities::CreateActivityService.call( activity_type: "#{!@step.protocol.in_module? ? 'protocol_step_' : 'task_step_'}#{element_type_of}", owner: current_user, - team: @protocol.in_module? ? @protocol.my_module.experiment.project.team : @protocol.team, - project: @protocol.in_module? ? @protocol.my_module.experiment.project : nil, + team: @protocol.team, + project: @protocol.in_module? ? @protocol.my_module.project : nil, subject: @protocol, message_items: { step: @step.id, diff --git a/app/controllers/step_elements/checklist_items_controller.rb b/app/controllers/step_elements/checklist_items_controller.rb index 2a4fa806f..fd979a9f6 100644 --- a/app/controllers/step_elements/checklist_items_controller.rb +++ b/app/controllers/step_elements/checklist_items_controller.rb @@ -3,6 +3,7 @@ module StepElements class ChecklistItemsController < ApplicationController include ApplicationHelper + include StepsActions before_action :load_vars before_action :load_checklist_item, only: %i(update toggle destroy) @@ -21,6 +22,7 @@ module StepElements checklist_name: @checklist.name } ) + checklist_item_annotation(@step, checklist_item) end render json: checklist_item, serializer: ChecklistItemSerializer, user: current_user @@ -31,6 +33,7 @@ module StepElements end def update + old_text = @checklist_item.text @checklist_item.assign_attributes( checklist_item_params.merge(last_modified_by: current_user) ) @@ -41,6 +44,7 @@ module StepElements checklist_item: @checklist_item.text, checklist_name: @checklist.name ) + checklist_item_annotation(@step, @checklist_item, old_text) end render json: @checklist_item, serializer: ChecklistItemSerializer, user: current_user @@ -129,6 +133,8 @@ module StepElements @step = Step.find_by(id: params[:step_id]) return render_404 unless @step + @protocol = @step.protocol + @checklist = @step.checklists.find_by(id: params[:checklist_id]) return render_404 unless @checklist end @@ -144,7 +150,7 @@ module StepElements owner: current_user, subject: @step.protocol, team: @step.protocol.team, - project: @step.protocol.in_module? ? @step.protocol.my_module.experiment.project : nil, + project: @step.protocol.in_module? ? @step.protocol.my_module.project : nil, message_items: message_items.merge(step_message_items) ) end diff --git a/app/controllers/step_elements/checklists_controller.rb b/app/controllers/step_elements/checklists_controller.rb index 540fad27c..aa6ee30e8 100644 --- a/app/controllers/step_elements/checklists_controller.rb +++ b/app/controllers/step_elements/checklists_controller.rb @@ -2,8 +2,9 @@ module StepElements class ChecklistsController < BaseController + include ApplicationHelper + include StepsActions before_action :load_checklist, only: %i(update destroy duplicate) - def create checklist = @step.checklists.build( name: t('protocols.steps.checklist.default_name', position: @step.checklists.length + 1) @@ -11,6 +12,7 @@ module StepElements ActiveRecord::Base.transaction do create_in_step!(@step, checklist) log_step_activity(:checklist_added, { checklist_name: checklist.name }) + checklist_name_annotation(@step, checklist) end render_step_orderable_element(checklist) rescue ActiveRecord::RecordInvalid @@ -18,9 +20,11 @@ module StepElements end def update + old_name = @checklist.name ActiveRecord::Base.transaction do @checklist.update!(checklist_params) log_step_activity(:checklist_edited, { checklist_name: @checklist.name }) + checklist_name_annotation(@step, @checklist, old_name) end render json: @checklist, serializer: ChecklistSerializer, user: current_user @@ -43,6 +47,7 @@ module StepElements @step.step_orderable_elements.where('position > ?', position).order(position: :desc).each do |element| element.update(position: element.position + 1) end + @checklist.name += ' (1)' new_checklist = @checklist.duplicate(@step, current_user, position + 1) log_step_activity(:checklist_duplicated, { checklist_name: @checklist.name }) render_step_orderable_element(new_checklist) diff --git a/app/controllers/step_elements/tables_controller.rb b/app/controllers/step_elements/tables_controller.rb index 25957cb86..8d8f43486 100644 --- a/app/controllers/step_elements/tables_controller.rb +++ b/app/controllers/step_elements/tables_controller.rb @@ -48,6 +48,7 @@ module StepElements @step.step_orderable_elements.where('position > ?', position).order(position: :desc).each do |element| element.update(position: element.position + 1) end + @table.name += ' (1)' new_table = @table.duplicate(@step, current_user, position + 1) log_step_activity(:table_duplicated, { table_name: new_table.name }) render_step_orderable_element(new_table.step_table) diff --git a/app/controllers/step_elements/texts_controller.rb b/app/controllers/step_elements/texts_controller.rb index b33bad5b2..06ebe2293 100644 --- a/app/controllers/step_elements/texts_controller.rb +++ b/app/controllers/step_elements/texts_controller.rb @@ -2,6 +2,9 @@ module StepElements class TextsController < BaseController + include ApplicationHelper + include StepsActions + before_action :load_step_text, only: %i(update destroy duplicate) def create @@ -18,10 +21,12 @@ module StepElements end def update + old_text = @step_text.text ActiveRecord::Base.transaction do @step_text.update!(step_text_params) TinyMceAsset.update_images(@step_text, params[:tiny_mce_images], current_user) log_step_activity(:text_edited, { text_name: @step_text.name }) + step_text_annotation(@step, @step_text, old_text) end render json: @step_text, serializer: StepTextSerializer, user: current_user diff --git a/app/controllers/step_orderable_elements_controller.rb b/app/controllers/step_orderable_elements_controller.rb index 70e7509be..801c6fbac 100644 --- a/app/controllers/step_orderable_elements_controller.rb +++ b/app/controllers/step_orderable_elements_controller.rb @@ -48,7 +48,7 @@ class StepOrderableElementsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @protocol, - team: current_team, + team: @protocol.team, project: project, message_items: message_items) end diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 9bbfedf45..f9dbe66c5 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -38,7 +38,7 @@ class StepsController < ApplicationController @asset = @step.assets.create!( created_by: current_user, last_modified_by: current_user, - team: current_team, + team: @protocol.team, view_mode: @step.assets_view_mode ) @asset.file.attach(params[:signed_blob_id]) @@ -323,7 +323,7 @@ class StepsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: @protocol, - team: current_team, + team: @protocol.team, project: project, message_items: message_items) end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index e561e1d4d..29c84a8df 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -168,7 +168,7 @@ class TagsController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: subject, - team: current_team, + team: @tag.project.team, project: @tag.project, message_items: message_items) end diff --git a/app/controllers/team_repositories_controller.rb b/app/controllers/team_repositories_controller.rb index e0bf0ed48..3840fbeea 100644 --- a/app/controllers/team_repositories_controller.rb +++ b/app/controllers/team_repositories_controller.rb @@ -97,7 +97,7 @@ class TeamRepositoriesController < ApplicationController .call(activity_type: type_of, owner: current_user, subject: team_shared_object.shared_repository, - team: current_team, + team: @repository.team, message_items: { repository: team_shared_object.shared_repository.id, team: team_shared_object.team.id, permission_level: diff --git a/app/controllers/user_my_modules_controller.rb b/app/controllers/user_my_modules_controller.rb index e65eabdd7..37125a1c1 100644 --- a/app/controllers/user_my_modules_controller.rb +++ b/app/controllers/user_my_modules_controller.rb @@ -61,14 +61,22 @@ class UserMyModulesController < ApplicationController respond_to do |format| format.json do - render json: { - user: { - id: @um.user.id, - full_name: @um.user.full_name, - avatar_url: avatar_path(@um.user, :icon_small), - user_module_id: @um.id - }, status: :ok - } + if params[:table] + render json: { + html: render_to_string(partial: 'experiments/assigned_users.html.erb', + locals: { my_module: @my_module, user: current_user, skip_unassigned: false }), + unassign_url: my_module_user_my_module_path(@my_module, @um) + } + else + render json: { + user: { + id: @um.user.id, + full_name: @um.user.full_name, + avatar_url: avatar_path(@um.user, :icon_small), + user_module_id: @um.id + }, status: :ok + } + end end end else @@ -88,7 +96,14 @@ class UserMyModulesController < ApplicationController respond_to do |format| format.json do - render json: {}, status: :ok + if params[:table] + render json: { + html: render_to_string(partial: 'experiments/assigned_users.html.erb', + locals: { my_module: @my_module, user: current_user, skip_unassigned: false }) + } + else + render json: {}, status: :ok + end end end else @@ -104,19 +119,37 @@ class UserMyModulesController < ApplicationController def search users = @my_module.users - .where.not(id: @my_module.designated_users.select(:id)) + .joins("LEFT OUTER JOIN user_my_modules ON user_my_modules.user_id = users.id "\ + "AND user_my_modules.my_module_id = #{@my_module.id}") .search(false, params[:query]) + .order(:full_name) .limit(Constants::SEARCH_LIMIT) + .select('users.*', 'user_my_modules.id as user_my_module_id') + .select('CASE WHEN user_my_modules.id IS NOT NULL '\ + 'THEN true ELSE false END as designated') users = users.map do |user| - { + next if params[:skip_assigned] && user.designated + next if ActiveModel::Type::Boolean.new.cast(params[:skip_unassigned]) && !user.designated + + user_hash = { value: user.id, label: sanitize_input(user.full_name), - params: { avatar_url: avatar_path(user, :icon_small) } + params: { + avatar_url: avatar_path(user, :icon_small), + designated: user.designated, + assign_url: my_module_user_my_modules_path(@my_module) + } } + + if user.designated + user_hash[:params][:unassign_url] = my_module_user_my_module_path(@my_module, user.user_my_module_id) + end + + user_hash end - render json: users + render json: users.compact end private diff --git a/app/helpers/comment_helper.rb b/app/helpers/comment_helper.rb index 346747dae..2a17dc6af 100644 --- a/app/helpers/comment_helper.rb +++ b/app/helpers/comment_helper.rb @@ -157,8 +157,7 @@ module CommentHelper .experiment .project)), experiment: link_to(result.my_module.experiment.name, - canvas_experiment_url(result.my_module - .experiment)), + my_modules_experiment_url(result.my_module.experiment)), my_module: link_to(result.my_module.name, protocols_my_module_url( result.my_module @@ -193,8 +192,7 @@ module CommentHelper .experiment .project)), experiment: link_to(step.my_module.experiment.name, - canvas_experiment_url(step.my_module - .experiment)), + my_modules_experiment_url(step.my_module.experiment)), my_module: link_to(step.my_module.name, protocols_my_module_url( step.my_module @@ -218,8 +216,7 @@ module CommentHelper .experiment .project)), experiment: link_to(my_module.experiment.name, - canvas_experiment_url(my_module - .experiment)), + my_modules_experiment_url(my_module.experiment)), my_module: link_to(my_module.name, protocols_my_module_url( my_module @@ -278,4 +275,8 @@ module CommentHelper def has_unseen_comments?(commentable) commentable.comments.any? { |comment| comment.unseen_by.include?(current_user.id) } end + + def count_unseen_comments(commentable, current_user) + commentable.comments.count { |comment| comment.unseen_by.include?(current_user.id) } + end end diff --git a/app/helpers/global_activities_helper.rb b/app/helpers/global_activities_helper.rb index 7511e0c29..76bfb9f7e 100644 --- a/app/helpers/global_activities_helper.rb +++ b/app/helpers/global_activities_helper.rb @@ -72,12 +72,12 @@ module GlobalActivitiesHelper when Experiment return current_value unless obj.navigable? - path = obj.archived? ? project_path(obj.project, view_mode: :archived) : canvas_experiment_path(obj) + path = obj.archived? ? project_path(obj.project, view_mode: :archived) : my_modules_experiment_path(obj) when MyModule return current_value unless obj.navigable? path = if obj.archived? - module_archive_experiment_path(obj.experiment) + my_modules_experiment_path(obj.experiment, view_mode: :archived) else protocols_my_module_path(obj) end diff --git a/app/helpers/input_sanitize_helper.rb b/app/helpers/input_sanitize_helper.rb index 0408df878..883310d05 100644 --- a/app/helpers/input_sanitize_helper.rb +++ b/app/helpers/input_sanitize_helper.rb @@ -28,11 +28,14 @@ module InputSanitizeHelper base64_encoded_imgs = options.fetch(:base64_encoded_imgs, false) text = sanitize_input(text, tags) text = simple_format(text, {}, format_opt) if simple_f + if text =~ SmartAnnotations::TagToHtml::USER_REGEX || text =~ SmartAnnotations::TagToHtml::REGEX + text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository) + end auto_link( - custom_link_open_new_tab(smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository)), + text, + html: { target: '_blank' }, link: :urls, - sanitize: false, - html: { target: '_blank' } + sanitize: false ).html_safe end end diff --git a/app/helpers/my_modules_helper.rb b/app/helpers/my_modules_helper.rb index 1627a63dc..b2014275c 100644 --- a/app/helpers/my_modules_helper.rb +++ b/app/helpers/my_modules_helper.rb @@ -31,7 +31,7 @@ module MyModulesHelper def get_task_alert_color(my_module) alert = '' - if my_module.active? && !my_module.completed? + if !my_module.archived_branch? && !my_module.completed? alert = ' alert-yellow' if my_module.is_one_day_prior? alert = ' alert-red' if my_module.is_overdue? end @@ -100,4 +100,16 @@ module MyModulesHelper my_module.experiment.project.archived_on end end + + def my_module_due_status(my_module, datetime = DateTime.current) + return if my_module.archived_branch? || my_module.completed? + + if my_module.is_overdue?(datetime) + I18n.t('my_modules.details.overdue') + elsif my_module.is_one_day_prior?(datetime) + I18n.t('my_modules.details.due_soon') + else + '' + end + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ab062316a..1fb5f6432 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -14,7 +14,7 @@ module ProjectsHelper end def user_names_with_roles(user_assignments) - user_assignments.map { |up| user_name_with_role(up) }.join(' ').html_safe + user_assignments.map { |up| user_name_with_role(up) }.join(' ') end def user_name_with_role(user_assignment) diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb index 7cd7b203b..9d3e66036 100644 --- a/app/helpers/repository_datatable_helper.rb +++ b/app/helpers/repository_datatable_helper.rb @@ -6,22 +6,25 @@ module RepositoryDatatableHelper def prepare_row_columns(repository_rows, repository, columns_mappings, team, options = {}) has_stock_management = repository.has_stock_management? reminders_enabled = Repository.reminders_enabled? - reminder_row_ids = reminders_enabled ? repository_reminder_row_ids(repository_rows, repository) : [] + repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows repository_rows.map do |record| - default_cells = public_send("#{repository.class.name.underscore}_default_columns", record) - row = { + row = public_send("#{repository.class.name.underscore}_default_columns", record) + row.merge!( DT_RowId: record.id, DT_RowAttr: { 'data-state': row_style(record) }, recordInfoUrl: Rails.application.routes.url_helpers.repository_repository_row_path(repository, record), - hasActiveReminders: reminder_row_ids.include?(record.id), rowRemindersUrl: Rails.application.routes.url_helpers .active_reminder_repository_cells_repository_repository_row_url( repository, record ) - }.merge(default_cells) + ) + + if reminders_enabled + row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders + end if has_stock_management row['manageStockUrl'] = if record.has_stock? @@ -100,7 +103,7 @@ module RepositoryDatatableHelper def prepare_simple_view_row_columns(repository_rows, repository, my_module, options = {}) has_stock_management = repository.has_stock_management? reminders_enabled = Repository.reminders_enabled? - reminder_row_ids = reminders_enabled ? repository_reminder_row_ids(repository_rows, repository) : [] + repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows repository_rows.map do |record| row = { @@ -108,7 +111,6 @@ module RepositoryDatatableHelper DT_RowAttr: { 'data-state': row_style(record) }, '0': escape_input(record.name), recordInfoUrl: Rails.application.routes.url_helpers.repository_repository_row_path(record.repository, record), - hasActiveReminders: reminder_row_ids.include?(record.id), rowRemindersUrl: Rails.application.routes.url_helpers .active_reminder_repository_cells_repository_repository_row_url( @@ -117,6 +119,10 @@ module RepositoryDatatableHelper ) } + if reminders_enabled + row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders + end + if has_stock_management stock_present = record.repository_stock_cell.present? # Always disabled in a simple view @@ -275,14 +281,33 @@ module RepositoryDatatableHelper '' end - def repository_reminder_row_ids(repository_rows, repository) - # don't load reminders for archived repositories - return [] if repository_rows.blank? || repository.archived? + def with_reminders_status(repository_rows, repository) + # don't load reminders for archived repositories or snapshots + if repository.archived? || repository.is_a?(RepositorySnapshot) + return repository_rows.select('FALSE AS has_active_stock_reminders') + .select('FALSE AS has_active_datetime_reminders') + end - # don't load reminders for snapshots - return [] if repository.is_a?(RepositorySnapshot) + repository_cells = RepositoryCell.joins( + "INNER JOIN repository_columns ON repository_columns.id = repository_cells.repository_column_id " \ + "AND repository_columns.repository_id = #{repository.id}" + ) - repository_rows.active.with_active_reminders(current_user).to_a.pluck(:id).uniq + repository_rows + .joins( + "LEFT OUTER JOIN (#{RepositoryCell.stock_reminder_repository_cells_scope(repository_cells, current_user) + .select(:id, :repository_row_id).to_sql}) " \ + "AS repository_cells_with_active_stock_reminders " \ + "ON repository_cells_with_active_stock_reminders.repository_row_id = repository_rows.id" + ) + .joins( + "LEFT OUTER JOIN (#{RepositoryCell.date_time_reminder_repository_cells_scope(repository_cells, current_user) + .select(:id, :repository_row_id).to_sql}) " \ + "AS repository_cells_with_active_datetime_reminders " \ + "ON repository_cells_with_active_datetime_reminders.repository_row_id = repository_rows.id" + ) + .select('COUNT(repository_cells_with_active_stock_reminders.id) > 0 AS has_active_stock_reminders') + .select('COUNT(repository_cells_with_active_datetime_reminders.id) > 0 AS has_active_datetime_reminders') end def stock_consumption_permitted?(repository, my_module) diff --git a/app/javascript/packs/tiny_mce.js b/app/javascript/packs/tiny_mce.js new file mode 100644 index 000000000..a8b884a9a --- /dev/null +++ b/app/javascript/packs/tiny_mce.js @@ -0,0 +1,489 @@ +/* global I18n hljs GLOBAL_CONSTANTS HelperModule SmartAnnotation TinyMCE */ + +import tinyMCE from 'tinymce/tinymce'; +import 'tinymce/models/dom'; +import 'tinymce/icons/default'; +import 'tinymce/themes/silver'; + +import 'tinymce/plugins/table'; +import 'tinymce/plugins/autosave'; +import 'tinymce/plugins/autoresize'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/advlist'; +import 'tinymce/plugins/codesample'; +import 'tinymce/plugins/autolink'; +import 'tinymce/plugins/lists'; +import 'tinymce/plugins/charmap'; +import 'tinymce/plugins/anchor'; +import 'tinymce/plugins/searchreplace'; +import 'tinymce/plugins/wordcount'; +import 'tinymce/plugins/visualblocks'; +import 'tinymce/plugins/visualchars'; +import 'tinymce/plugins/insertdatetime'; +import 'tinymce/plugins/nonbreaking'; +import 'tinymce/plugins/save'; +import 'tinymce/plugins/help'; +import 'tinymce/plugins/quickbars'; +import 'tinymce/plugins/directionality'; +import './tinymce/custom_image_uploader/plugin'; +import './tinymce/marvinjs/plugin'; +import './tinymce/image_toolbar/plugin'; + +// Content styles, including inline UI like fake cursors +// All the above CSS files are loaded on to the page but these two must +// be loaded into the editor iframe so they are loaded as strings and passed +// to the init function. +import 'raw-loader'; +import contentCss from '!!raw-loader!tinymce/skins/content/default/content.min.css'; +import contentUiCss from '!!raw-loader!tinymce/skins/ui/tinymce-5/content.min.css'; + +const contentPStyle = 'p { margin: 0; padding: 0 }'; +const contentStyle = [contentCss, contentUiCss, contentPStyle].map((s) => s.toString()).join('\n'); + +window.TinyMCE = (() => { + function initHighlightjs() { + $('[class*=language]').each((i, block) => { + hljs.highlightBlock(block); + }); + } + + function initHighlightjsIframe(iframe) { + $('[class*=language]', iframe).each((i, block) => { + hljs.highlightBlock(block); + }); + } + + function makeItDirty(editor) { + const editorForm = $(editor.getContainer()).closest('form'); + editorForm.find('.tinymce-status-badge').addClass('hidden'); + $(editor.getContainer()).find('.tinymce-save-button').removeClass('hidden'); + } + + // Get LocalStorage auto save path + function getAutoSavePrefix(editor) { + let prefix = editor.getParam('autosave_prefix', 'tinymce-autosave-{path}{query}{hash}-{id}-'); + + prefix = prefix.replace(/\{path\}/g, document.location.pathname); + prefix = prefix.replace(/\{query\}/g, document.location.search); + prefix = prefix.replace(/\{hash\}/g, document.location.hash); + prefix = prefix.replace(/\{id\}/g, editor.id); + + return prefix; + } + + // Handles autosave notification if draft is available in the local storage + function restoreDraftNotification(selector, editor) { + const prefix = getAutoSavePrefix(editor); + const lastDraftTime = parseInt(tinyMCE.util.LocalStorage.getItem(`${prefix}time`), 10); + const lastUpdated = $(selector).data('last-updated'); + let notificationBar; + const restoreBtn = $(''); + const cancelBtn = $(''); + + // Check whether we have draft stored + + if (editor.plugins.autosave.hasDraft()) { + notificationBar = $('
    '); + + if (lastDraftTime < lastUpdated) { + notificationBar.html(`${I18n.t('tiny_mce.older_version_available')}`); + } else { + notificationBar.html(`${I18n.t('tiny_mce.newer_version_available')}`); + } + + // Add notification bar + $(notificationBar).append(restoreBtn).append(cancelBtn); + $(editor.contentAreaContainer).before(notificationBar); + + // Prevents save on blur if clicking draft notification + $('.restore-draft-notification').on('mousedown', () => { + editor.isBlurTempDisabled = true; + setTimeout(() => { + editor.isBlurTempDisabled = false; + }, 500); + }); + + $(restoreBtn).click(() => { + editor.plugins.autosave.restoreDraft(); + makeItDirty(editor); + notificationBar.remove(); + }); + + $(cancelBtn).click(() => { + notificationBar.remove(); + }); + } + + setTimeout(() => { tinyMCE.activeEditor.execCommand('mceAutoResize') }, 500); + } + + function initImageToolBar(editor) { + const editorIframe = $(`#${editor.id}`).next().find('.tox-edit-area iframe'); + const primaryColor = '#104da9'; + editorIframe.contents().find('head').append(``); + } + + function draftLocation() { + return `tinymce-drafts-${document.location.pathname}`; + } + + function removeDraft(editor, textAreaObject) { + const location = draftLocation(); + const storedDrafts = JSON.parse(sessionStorage.getItem(location) || '[]'); + const draftId = storedDrafts.indexOf(textAreaObject.data('tinymce-object')); + if (draftId > -1) { + storedDrafts.splice(draftId, 1); + } + + if (storedDrafts.length) { + sessionStorage.setItem(location, JSON.stringify(storedDrafts)); + } else { + sessionStorage.removeItem(location); + } + } + + // Update scroll position after exit + function updateScrollPosition(editorForm) { + if (editorForm.offset().top < $(window).scrollTop()) { + $(window).scrollTop(editorForm.offset().top - 150); + } + } + + function saveAction(editor) { + const editorForm = $(editor.getContainer()).closest('form'); + editorForm.clearFormErrors(); + editor.setProgressState(1); + editor.save(); + editorForm.submit(); + updateScrollPosition(editorForm); + } + + // returns a public API for TinyMCE editor + return { + init: (selector, options = {}) => { + const textAreaObject = $(selector); + let editorToolbaroffset = 0; + + if (typeof tinyMCE !== 'undefined') { + // Hide element containing HTML view of RTE field + const tinyMceContainer = $(selector).closest('form').find('.tinymce-view'); + const tinyMceInitSize = tinyMceContainer.height(); + $(selector).closest('.form-group') + .before(`
    `); + tinyMceContainer.addClass('hidden'); + const plugins = ` + table autosave autoresize link advlist codesample autolink lists + charmap anchor searchreplace wordcount visualblocks visualchars + insertdatetime nonbreaking save directionality customimageuploader + marvinjs custom_image_toolbar help quickbars + `; + // if (typeof (MarvinJsEditor) !== 'undefined') plugins += ' marvinjsplugin'; + + if (textAreaObject.data('objectType') === 'step' + || textAreaObject.data('objectType') === 'result_text') { + document.location.hash = `${textAreaObject.data('objectType')}_${textAreaObject.data('objectId')}`; + } + + if ($('.navbar-secondary').length) { + editorToolbaroffset = $('.navbar-secondary').position().top + $('.navbar-secondary').height(); + } else if ($('#main-nav').length) { + editorToolbaroffset = $('#main-nav').height(); + } + + return tinyMCE.init({ + cache_suffix: '?v=6.3.1', // This suffix should be changed any time library is updated + selector, + skin: false, + content_css: false, + content_style: contentStyle, + convert_urls: false, + promotion: false, + menu: { + insert: { title: 'Insert', items: 'link codesample inserttable | charmap hr | nonbreaking anchor | insertdatetime customimageuploader marvinjs' }, + }, + menubar: 'file edit view insert format table', + toolbar: 'undo redo restoredraft | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table | link | forecolor backcolor | codesample | customimageuploader marvinjs | help', + plugins, + autoresize_bottom_margin: 20, + placeholder: options.placeholder, + toolbar_sticky: true, + toolbar_sticky_offset: editorToolbaroffset, + codesample_languages: [ + { text: 'R', value: 'r' }, + { text: 'MATLAB', value: 'matlab' }, + { text: 'Python', value: 'python' }, + { text: 'JSON', value: 'javascript' }, + { text: 'HTML/XML', value: 'markup' }, + { text: 'JavaScript', value: 'javascript' }, + { text: 'CSS', value: 'css' }, + { text: 'PHP', value: 'php' }, + { text: 'Ruby', value: 'ruby' }, + { text: 'Java', value: 'java' }, + { text: 'C', value: 'c' }, + { text: 'C#', value: 'csharp' }, + { text: 'C++', value: 'cpp' } + ], + browser_spellcheck: true, + branding: false, + fixed_toolbar_container: '#mytoolbar', + autosave_restore_when_empty: false, + autosave_interval: '1s', + autosave_retention: '1440m', + removed_menuitems: 'newdocument', + object_resizing: true, + elementpath: false, + quickbars_insert_toolbar: false, + default_link_target: '_blank', + target_list: [ + { title: 'New page', value: '_blank' }, + { title: 'Same page', value: '_self' } + ], + style_formats: [ + { + title: 'Headers', + items: [ + { title: 'Header 1', format: 'h1' }, + { title: 'Header 2', format: 'h2' }, + { title: 'Header 3', format: 'h3' }, + { title: 'Header 4', format: 'h4' }, + { title: 'Header 5', format: 'h5' }, + { title: 'Header 6', format: 'h6' } + ] + }, + { + title: 'Inline', + items: [ + { title: 'Bold', icon: 'bold', format: 'bold' }, + { title: 'Italic', icon: 'italic', format: 'italic' }, + { title: 'Underline', icon: 'underline', format: 'underline' }, + { title: 'Strikethrough', icon: 'strike-through', format: 'strikethrough' }, + { title: 'Superscript', icon: 'superscript', format: 'superscript' }, + { title: 'Subscript', icon: 'subscript', format: 'subscript' }, + { title: 'Code', icon: 'sourcecode', format: 'code' } + ] + }, + { + title: 'Blocks', + items: [ + { title: 'Paragraph', format: 'p' }, + { title: 'Blockquote', format: 'blockquote' } + ] + }, + { + title: 'Alignment', + items: [ + { title: 'Left', icon: 'align-left', format: 'alignleft' }, + { title: 'Center', icon: 'align-center', format: 'aligncenter' }, + { title: 'Right', icon: 'align-right', format: 'alignright' }, + { title: 'Justify', icon: 'align-justify', format: 'alignjustify' } + ] + } + ], + init_instance_callback: (editor) => { + + const editorContainer = $(editor.getContainer()); + const editorForm = editorContainer.closest('form'); + const menuBar = editorForm.find('.tox-menubar'); + + $('.tinymce-placeholder').css('height', `${$(editor.editorContainer).height()}px`); + setTimeout(() => { + editorContainer.addClass('tox-tinymce--loaded'); + $('.tinymce-placeholder').remove(); + }, 400); + + // Init saved status label + if (editor.getContent() !== '') { + editorForm.find('.tinymce-status-badge').removeClass('hidden'); + } + + // Init image toolbar + initImageToolBar(editor); + + // Init save/cancel button wrapper + $('
    ').appendTo(menuBar); + + // Init Save button + editorForm + .find('.tinymce-save-button') + .clone() + .appendTo(menuBar.find('.tinymce-save-controls')) + .on('click', (event) => { + event.preventDefault(); + saveAction(editor); + }); + + // After save action + editorForm + .on('ajax:success', (_ev, data) => { + editor.save(); + editor.setProgressState(0); + editorForm.find('.tinymce-status-badge').removeClass('hidden'); + editor.remove(); + editorForm.find('.tinymce-view').html(data.html).removeClass('hidden'); + TinyMCE.wrapTables(editorForm.find('.tinymce-view')); + editor.plugins.autosave.removeDraft(); + removeDraft(editor, textAreaObject); + if (options.onSaveCallback) { options.onSaveCallback(data); } + }).on('ajax:error', (_ev, data) => { + const model = editor.getElement().dataset.objectType; + $(this).renderFormErrors(model, data.responseJSON); + editor.setProgressState(0); + if (data.status === 403) { + HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger'); + } + }); + + // Init Cancel button + editorForm + .find('.tinymce-cancel-button') + .clone() + .prependTo(menuBar.find('.tinymce-save-controls')) + .on('click', (event) => { + $(editorForm).find('.form-group').removeClass('has-error'); + $(editorForm).find('.help-block').remove(); + + event.preventDefault(); + if (editor.isDirty()) { + editor.setContent($(selector).val()); + } + editorForm.find('.tinymce-status-badge').addClass('hidden'); + editorForm.find('.tinymce-view').removeClass('hidden'); + editor.remove(); + + updateScrollPosition(editorForm); + if (options.onSaveCallback) { options.onSaveCallback($(selector).val()); } + }) + .removeClass('hidden'); + + editor.selection.select(editor.getBody(), true); + editor.selection.collapse(false); + + SmartAnnotation.init($(editor.contentDocument.activeElement)); + SmartAnnotation.preventPropagation('.atwho-user-popover'); + initHighlightjsIframe($(editor.iframeElement).contents()); + + if (options.afterInitCallback) { options.afterInitCallback(); } + }, + setup: (editor) => { + editor.isBlurTempDisabled = false; + + editor.on('keydown', (e) => { + if (e.key === 'Enter' && $(editor.contentDocument.activeElement).atwho('isSelecting')) { + return false; + } + return true; + }); + + editor.on('NodeChange', (e) => { + const node = e.element; + setTimeout(() => { + if ($(node).is('pre') && !editor.isHidden()) { + initHighlightjsIframe($(editor.iframeElement).contents()); + } + }, 200); + }); + + editor.on('Dirty', () => { + makeItDirty(editor); + }); + + editor.on('StoreDraft', () => { + const location = draftLocation(); + const storedDrafts = JSON.parse(sessionStorage.getItem(location) || '[]'); + const draftName = textAreaObject.data('tinymce-object'); + if (storedDrafts.includes(draftName) || !draftName) return; + storedDrafts.push(draftName); + sessionStorage.setItem(location, JSON.stringify(storedDrafts)); + }); + + editor.on('remove', () => { + const menuBar = $(editor.getContainer()).find('.tox-menubar'); + menuBar.find('.tinymce-save-button').remove(); + menuBar.find('.tinymce-cancel-button').remove(); + }); + + editor.on('blur', () => { + if (editor.isBlurTempDisabled || editor.blurDisabled) return false; + + if ($('.atwho-view:visible').length || $('#MarvinJsModal:visible').length) return false; + setTimeout(() => { + if (editor.isNotDirty === false) { + $(editor.container).find('.tinymce-save-button').click(); + } else { + $(editor.container).find('.tinymce-cancel-button').click(); + } + }, 0); + return true; + }); + + editor.on('init', () => { + restoreDraftNotification(selector, editor); + }); + }, + codesample_content_css: $(selector).data('highlightjs-path'), + save_onsavecallback: (editor) => { saveAction(editor); } + }); + } + + return null; + }, + destroyAll: () => { + if (tinyMCE.activeEditor) { + tinyMCE.activeEditor.remove(); + initHighlightjs(); + } + }, + refresh: () => { + this.destroyAll(); + this.init(); + }, + getContent: () => tinyMCE.activeEditor && tinyMCE.activeEditor.getContent(), + updateImages: (editor) => { + const iframe = $(`#${editor.id}`).next().find('.tox-edit-area iframe').contents(); + const images = $.map($('img', iframe), e => e.dataset.mceToken); + $(`#${editor.id}`).parent().find('input.tiny-mce-images').val(JSON.stringify(images)); + return JSON.stringify(images); + }, + makeItDirty: (editor) => { + makeItDirty(editor); + }, + highlight: initHighlightjs, + wrapTables: (container) => { + container.find('table').toArray().forEach((table) => { + if ($(table).parent().hasClass('table-wrapper')) return; + + $(table).css('float', 'none').wrapAll(` +
    + `); + }); + } + }; +})(); + +$(document).on('turbolinks:before-visit', (e) => { + const editor = tinyMCE.activeEditor; + + if (editor === null) return true; + + if (editor.isDirty()) { + // eslint-disable-next-line no-alert + if (confirm(I18n.t('tiny_mce.leaving_warning'))) { + $('.atwho-container').remove(); + tinyMCE.activeEditor.remove(); + return true; + } + e.preventDefault(); + return false; + } + return true; +}); diff --git a/app/javascript/packs/tiny_mce_styles.scss b/app/javascript/packs/tiny_mce_styles.scss new file mode 100644 index 000000000..474e8e936 --- /dev/null +++ b/app/javascript/packs/tiny_mce_styles.scss @@ -0,0 +1 @@ +@import "tinymce/skins/ui/tinymce-5/skin.min.css"; diff --git a/app/javascript/packs/tinymce/custom_image_uploader/plugin.js b/app/javascript/packs/tinymce/custom_image_uploader/plugin.js new file mode 100644 index 000000000..64e83280b --- /dev/null +++ b/app/javascript/packs/tinymce/custom_image_uploader/plugin.js @@ -0,0 +1,122 @@ +/* eslint no-underscore-dangle: "off" */ +/* eslint no-use-before-define: "off" */ +/* eslint no-restricted-syntax: ["off", "BinaryExpression[operator='in']"] */ +/* global tinymce I18n HelperModule validateFileSize */ + +tinymce.PluginManager.add('customimageuploader', (editor) => { + var iframe; + var textAreaElement = $('#' + editor.id); + + function loadFiles() { + let $fileInput; + let hitFileLimit; + $('#tinymce_current_upload').remove(); + $fileInput = $('') + .prependTo(editor.container); + $fileInput.click(); + + $fileInput.change(function() { + let formData = new FormData(); + let files = $('#tinymce_current_upload')[0].files; + + Array.from(files).forEach(file => formData.append('files[]', file, file.name)); + + Array.from(files).every(file => { + if (!validateFileSize(file, true)) { + hitFileLimit = true; + return false; + } + }); + + if (hitFileLimit) { + return; + } + + $.post({ + url: textAreaElement.data('tinymce-asset-path'), + data: formData, + processData: false, + contentType: false, + success: function(data) { + handleResponse(data); + $('#tinymce_current_upload').remove(); + }, + error: function(response) { + HelperModule.flashAlertMsg(response.responseJSON.errors, 'danger'); + $('#tinymce_current_upload').remove(); + } + }); + }); + } + + function handleResponse(response) { + if (response.errors) { + handleError(response.errors.join('
    ')); + } else { + response.images.forEach(el => editor.execCommand('mceInsertContent', false, buildHTML(el))); + updateActiveImages(); + } + } + + function handleError(error) { + HelperModule.flashAlertMsg(error, 'danger'); + } + + function buildHTML(image) { + return `description-${image.token}`; + } + + // Create hidden field for images + function createImageHiddenField() { + textAreaElement.parent().find('input.tiny-mce-images').remove(); + $('').appendTo(textAreaElement.parent()); + } + + // Finding images in text + function updateActiveImages() { + const imageContainer = $(`#${editor.id}`).parent().find('input.tiny-mce-images'); + iframe = $(`#${editor.id}`).next().find('.tox-edit-area iframe').contents(); + const images = $.map($('img', iframe), e => e.dataset.mceToken); + if (imageContainer === undefined) { + createImageHiddenField(); + } + + // Small fix for ResultText when you cancel after change MarvinJS + if (imageContainer === undefined) return []; + + imageContainer.val(JSON.stringify(images)); + return JSON.stringify(images); + } + + // Add a button that opens a window + editor.ui.registry.addButton('customimageuploader', { + tooltip: I18n.t('tiny_mce.upload_window_label'), + icon: 'image', + onAction: loadFiles + }); + + // Adds a menu item to the tools menu + editor.ui.registry.addMenuItem('customimageuploader', { + text: I18n.t('tiny_mce.upload_window_label'), + icon: 'image', + context: 'insert', + onAction: loadFiles + }); + + editor.on('NodeChange', function() { + // Check editor status + if (this.initialized) { + updateActiveImages(); + } + }); + + createImageHiddenField(); + + return { + getMetadata: () => ({ + name: 'Custom Image Uploader Plugin' + }) + }; +}); diff --git a/app/javascript/packs/tinymce/image_toolbar/plugin.js b/app/javascript/packs/tinymce/image_toolbar/plugin.js new file mode 100644 index 000000000..3ecad2ae6 --- /dev/null +++ b/app/javascript/packs/tinymce/image_toolbar/plugin.js @@ -0,0 +1,55 @@ +/* global tinymce MarvinJsEditor */ +tinymce.PluginManager.add('custom_image_toolbar', (editor) => { + + editor.ui.registry.addIcon( + 'download', + ` + + ` + ); + + editor.ui.registry.addButton('image_download', { + icon: 'download', + onAction: () => { + const editorIframe = $(`#${editor.id}`).next().find('.tox-edit-area iframe'); + const image = editorIframe.contents().find('img[data-mce-selected="1"]'); + + window.open(`/tiny_mce_assets/${image.data('mce-token')}/download`, '_blank'); + } + }); + + editor.ui.registry.addButton('marvinjs_edit', { + icon: 'edit-block', + onAction: () => { + const editorIframe = $(`#${editor.id}`).next().find('.tox-edit-area iframe'); + const image = editorIframe.contents().find('img[data-mce-selected="1"]'); + MarvinJsEditor.open({ + mode: 'edit-tinymce', + marvinUrl: `/tiny_mce_assets/${image[0].dataset.mceToken}/marvinjs`, + editor, + image + }); + } + }); + + function isImage(elem) { + return editor.dom.is(elem, 'img') && elem.dataset.mceToken; + } + function isMarvinJs(elem) { + return elem.dataset.sourceType === 'marvinjs'; + } + + editor.ui.registry.addContextToolbar('marvinJsToolbar', { + predicate: (node) => isMarvinJs(node), + items: 'marvinjs_edit', + position: 'node', + scope: 'node' + }); + + editor.ui.registry.addContextToolbar('ImageToolbar', { + predicate: (node) => isImage(node), + items: 'image_download', + position: 'node', + scope: 'node' + }); +}); diff --git a/app/javascript/packs/tinymce/marvinjs/plugin.js b/app/javascript/packs/tinymce/marvinjs/plugin.js new file mode 100644 index 000000000..1b845d2f2 --- /dev/null +++ b/app/javascript/packs/tinymce/marvinjs/plugin.js @@ -0,0 +1,41 @@ +/* global tinymce I18n MarvinJsEditor */ +// TinyMCE plugin +tinymce.PluginManager.add('marvinjs', (editor) => { + function openMarvinJs() { + MarvinJsEditor.open({ + mode: 'new-tinymce', + marvinUrl: '/tiny_mce_assets/marvinjs', + editor + }); + } + + // Add marvinjs button + + editor.ui.registry.addIcon( + 'marvinjs', + ` + + ` + ); + + // Add a button that opens a window + editor.ui.registry.addButton('marvinjs', { + tooltip: I18n.t('marvinjs.new_button'), + icon: 'marvinjs', + onAction: openMarvinJs + }); + + // Adds a menu item to the tools menu + editor.ui.registry.addMenuItem('marvinjs', { + text: I18n.t('marvinjs.new_button'), + icon: 'marvinjs', + context: 'insert', + onAction: openMarvinJs + }); + + return { + getMetadata: () => ({ + name: 'MarvinJs Plugin' + }) + }; +}); diff --git a/app/javascript/vue/label_template/components/logo_insert_modal.vue b/app/javascript/vue/label_template/components/logo_insert_modal.vue new file mode 100644 index 000000000..25f13e622 --- /dev/null +++ b/app/javascript/vue/label_template/components/logo_insert_modal.vue @@ -0,0 +1,72 @@ + + diff --git a/app/javascript/vue/label_template/container.vue b/app/javascript/vue/label_template/container.vue index 59bbe2d3b..b0502e98f 100644 --- a/app/javascript/vue/label_template/container.vue +++ b/app/javascript/vue/label_template/container.vue @@ -59,7 +59,7 @@