diff --git a/.gitignore b/.gitignore index a16f03f0c5..4e459907b1 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,7 @@ public/marvin4js-license.cxl /app/assets/builds/* !/app/assets/builds/.keep + +# Ignore automatically generated js-routes files. +/app/javascript/routes.js +/app/javascript/routes.d.ts diff --git a/.travis.yml b/.travis.yml index c04c0e859b..01e9638774 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin + - sudo chown --recursive 1000 . - make docker-ci script: - make tests-ci diff --git a/Dockerfile b/Dockerfile index 4dc87507a3..da11bb47fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.2.2-bookworm +FROM ruby:3.2-bookworm MAINTAINER SciNote # additional dependecies @@ -20,14 +20,15 @@ RUN apt-get update -qq && \ fonts-wqy-microhei \ fonts-wqy-zenhei \ libfile-mimeinfo-perl \ - chromium-driver \ + chromium \ + chromium-sandbox \ yarnpkg && \ ln -s /usr/lib/x86_64-linux-gnu/libvips.so.42 /usr/lib/x86_64-linux-gnu/libvips.so && \ rm -rf /var/lib/apt/lists/* ENV PATH=/usr/share/nodejs/yarn/bin:$PATH -RUN yarn add puppeteer@npm:puppeteer-core +RUN yarn add puppeteer@npm:puppeteer-core@^22.15.0 ENV BUNDLE_PATH /usr/local/bundle/ @@ -35,6 +36,10 @@ ENV BUNDLE_PATH /usr/local/bundle/ ENV APP_HOME /usr/src/app ENV PATH $APP_HOME/bin:$PATH RUN mkdir $APP_HOME +RUN adduser --uid 1000 scinote +RUN chown scinote:scinote $APP_HOME +USER scinote +ENV CHROMIUM_PATH=$APP_HOME/bin/chromium WORKDIR $APP_HOME CMD rails s -b 0.0.0.0 diff --git a/Dockerfile.production b/Dockerfile.production index 90847ab02c..2a8af22f0b 100644 --- a/Dockerfile.production +++ b/Dockerfile.production @@ -1,5 +1,5 @@ # Building stage -FROM ruby:3.2.2-bookworm AS builder +FROM ruby:3.2-bookworm AS builder RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache RUN \ @@ -23,7 +23,7 @@ COPY . $APP_HOME RUN rm -f $APP_HOME/config/application.yml $APP_HOME/production.env WORKDIR $APP_HOME RUN \ - --mount=target=$APP_HOME/tmp/bundle,type=cache \ + --mount=target=/usr/src/app/tmp/bundle,type=cache \ bundle config set without 'development test' && \ bundle config set path '/usr/src/app/tmp/bundle' && \ bundle install --jobs `nproc` && \ @@ -34,14 +34,14 @@ RUN \ RUN \ --mount=type=cache,target=/usr/local/share/.cache/yarn/v6,sharing=locked \ - --mount=type=cache,target=$APP_HOME/node_modules,sharing=locked \ + --mount=type=cache,target=/usr/src/app/node_modules,sharing=locked \ DATABASE_URL=postgresql://postgres@db/scinote_production \ SECRET_KEY_BASE=dummy \ DEFACE_ENABLED=true \ - bash -c "rake assets:precompile && rake deface:precompile" + bash -c "rake assets:precompile && rake deface:precompile && rm -rf ./tmp/cache" # Final stage -FROM ruby:3.2.2-bookworm AS runner +FROM ruby:3.2-bookworm AS runner MAINTAINER SciNote RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache @@ -76,6 +76,7 @@ RUN \ libvips42 \ graphviz \ chromium \ + chromium-sandbox \ libfile-mimeinfo-perl \ yarnpkg && \ /usr/share/nodejs/yarn/bin/yarn add puppeteer@npm:puppeteer-core@^22.15.0 && \ @@ -88,6 +89,8 @@ RUN \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ touch /etc/build-${BUILD_TIMESTAMP} && \ DEBIAN_FRONTEND=noninteractive \ + apt-get remove -y *-dev && \ + apt-get autoremove -y && \ apt-get update -qq && \ apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/* @@ -98,7 +101,10 @@ ENV GEM_HOME=$APP_HOME/vendor/bundle/ruby/3.2.0 ENV PATH=$GEM_HOME/bin:$PATH ENV BUNDLE_APP_CONFIG=.bundle -COPY --from=builder $APP_HOME $APP_HOME +RUN adduser --uid 1000 scinote +USER scinote + +COPY --from=builder --chown=scinote:scinote $APP_HOME $APP_HOME WORKDIR $APP_HOME diff --git a/Gemfile b/Gemfile index 453dea286a..710cbf891d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,8 @@ # frozen_string_literal: true -source 'http://rubygems.org' +source 'https://rubygems.org' -ruby '3.2.2' +ruby '~> 3.2.2' gem 'activerecord-session_store' gem 'bootsnap', require: false @@ -62,6 +62,7 @@ gem 'logging', '~> 2.0.0' gem 'nested_form_fields' gem 'nokogiri', '~> 1.16.5' # HTML/XML parser gem 'noticed' +gem 'oj' gem 'rails_autolink', '~> 1.1', '>= 1.1.6' gem 'rgl' # Graph framework for project diagram calculations gem 'roo', '~> 2.10.0' # Spreadsheet parser @@ -81,9 +82,6 @@ gem 'aws-sdk-lambda' gem 'aws-sdk-rails' gem 'aws-sdk-s3' gem 'delayed_job_active_record' -gem 'devise-async', - git: 'https://github.com/mhfs/devise-async.git', - branch: 'devise-4.x' gem 'image_processing' gem 'img2zpl', git: 'https://github.com/scinote-eln/img2zpl' gem 'rufus-scheduler' @@ -94,6 +92,7 @@ gem 'graphviz' gem 'cssbundling-rails' gem 'jsbundling-rails' +gem 'js-routes' gem 'tailwindcss-rails', '~> 2.4' diff --git a/Gemfile.lock b/Gemfile.lock index 5a29368f28..956bc482e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,14 +5,6 @@ GIT sneaky-save (0.1.3) activerecord (>= 3.2.0) -GIT - remote: https://github.com/mhfs/devise-async.git - revision: 177f6363a002f7ff28f1d289c8cab7ad8d9cb8c5 - branch: devise-4.x - specs: - devise-async (0.10.2) - devise (>= 4.0) - GIT remote: https://github.com/scinote-eln/canaid revision: bba1b817d1c9b0c7e0440a83d0f62848aabc0a1b @@ -50,64 +42,64 @@ GIT GEM remote: http://rubygems.org/ specs: - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailbox (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.4) - actionpack (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailer (7.0.8.5) + actionpack (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.4) - actionview (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionpack (7.0.8.5) + actionview (= 7.0.8.5) + activesupport (= 7.0.8.5) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.4) - actionpack (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actiontext (7.0.8.5) + actionpack (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.0.8.5) + activesupport (= 7.0.8.5) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_model_serializers (0.10.13) - actionpack (>= 4.1, < 7.1) - activemodel (>= 4.1, < 7.1) + active_model_serializers (0.10.14) + actionpack (>= 4.1) + activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.3.6) - activemodel (7.0.8.4) - activesupport (= 7.0.8.4) - activerecord (7.0.8.4) - activemodel (= 7.0.8.4) - activesupport (= 7.0.8.4) + activemodel (7.0.8.5) + activesupport (= 7.0.8.5) + activerecord (7.0.8.5) + activemodel (= 7.0.8.5) + activesupport (= 7.0.8.5) activerecord-import (1.4.1) activerecord (>= 4.2) activerecord-session_store (2.1.0) @@ -117,14 +109,14 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 4) railties (>= 6.1) - activestorage (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activesupport (= 7.0.8.4) + activestorage (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activesupport (= 7.0.8.5) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.0.8.5) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -193,6 +185,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) + bigdecimal (3.1.8) bindata (2.5.0) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -200,7 +193,7 @@ GEM msgpack (~> 1.2) brakeman (6.1.2) racc - builder (3.2.4) + builder (3.3.0) bullet (7.0.7) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) @@ -242,7 +235,7 @@ GEM combine_pdf (1.0.23) matrix ruby-rc4 (>= 0.1.5) - concurrent-ruby (1.3.1) + concurrent-ruby (1.3.4) crack (0.4.5) rexml crass (1.0.6) @@ -318,8 +311,8 @@ GEM railties (>= 5) down (5.4.1) addressable (~> 2.8) - erubi (1.12.0) - et-orbi (1.2.7) + erubi (1.13.0) + et-orbi (1.2.11) tzinfo execjs (2.8.1) factory_bot (6.2.1) @@ -342,8 +335,8 @@ GEM rake figaro (1.2.0) thor (>= 0.14.0, < 2) - fugit (1.8.1) - et-orbi (~> 1, >= 1.2.7) + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) @@ -366,7 +359,7 @@ GEM httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-js (3.9.2) i18n (>= 0.6.6) @@ -374,7 +367,7 @@ GEM mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) iniparse (1.5.0) - jbuilder (2.11.5) + jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) @@ -382,6 +375,8 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + js-routes (2.2.8) + railties (>= 4) jsbundling-rails (1.1.1) railties (>= 6.0.0) json (2.6.3) @@ -427,7 +422,7 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) matrix (0.4.2) method_source (1.0.0) mime-types (3.4.1) @@ -435,8 +430,7 @@ GEM mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.7) - minitest (5.23.1) + minitest (5.25.1) msgpack (1.7.1) multi_json (1.15.0) multi_test (1.1.0) @@ -447,23 +441,20 @@ GEM rails (>= 3.2.0) net-http (0.4.1) uri - net-imap (0.4.10) + net-imap (0.4.17) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.4.0.1) + net-smtp (0.5.0) net-protocol - newrelic_rpm (9.2.2) - nio4r (2.7.0) - nokogiri (1.16.5) - mini_portile2 (~> 2.8.2) + newrelic_rpm (9.14.0) + nio4r (2.7.3) + nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.5-arm64-darwin) - racc (~> 1.4) - nokogiri (1.16.5-x86_64-linux) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) noticed (1.6.3) http (>= 4.0.0) @@ -475,7 +466,10 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - omniauth (2.1.1) + oj (3.16.6) + bigdecimal (>= 3.0) + ostruct (>= 0.2) + omniauth (2.1.2) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection @@ -489,9 +483,9 @@ GEM omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.1.0) - omniauth (~> 2.0) - ruby-saml (~> 1.12) + omniauth-saml (2.2.1) + omniauth (~> 2.1) + ruby-saml (~> 1.17) omniauth_openid_connect (0.7.1) omniauth (>= 1.9, < 3) openid_connect (~> 2.2) @@ -509,6 +503,7 @@ GEM validate_url webfinger (~> 2.0) orm_adapter (0.5.0) + ostruct (0.6.0) overcommit (0.60.0) childprocess (>= 0.6.3, < 5) iniparse (~> 1.4) @@ -539,11 +534,11 @@ GEM pry (>= 0.10.4) psych (3.3.4) public_suffix (5.0.1) - puma (6.4.2) + puma (6.4.3) nio4r (~> 2.0) raabro (1.4.0) - racc (1.8.0) - rack (2.2.9) + racc (1.8.1) + rack (2.2.10) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (2.0.2) @@ -555,24 +550,25 @@ GEM faraday-follow_redirects json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (3.0.6) - rack + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.4) - actioncable (= 7.0.8.4) - actionmailbox (= 7.0.8.4) - actionmailer (= 7.0.8.4) - actionpack (= 7.0.8.4) - actiontext (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activemodel (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rails (7.0.8.5) + actioncable (= 7.0.8.5) + actionmailbox (= 7.0.8.5) + actionmailer (= 7.0.8.5) + actionpack (= 7.0.8.5) + actiontext (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activemodel (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.0.8.5) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -593,9 +589,9 @@ GEM railties (> 3.1) rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + railties (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) method_source rake (>= 12.2) thor (~> 1.0) @@ -611,8 +607,7 @@ GEM responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.3) - strscan + rexml (3.3.9) rgl (0.6.3) pairing_heap (>= 0.3.0) rexml (~> 3.2, >= 3.2.4) @@ -664,7 +659,7 @@ GEM rubocop (>= 1.33.0, < 2.0) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) - ruby-saml (1.16.0) + ruby-saml (1.17.0) nokogiri (>= 1.13.10) rexml ruby-vips (2.1.4) @@ -704,7 +699,6 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) stream (0.5.5) - strscan (3.1.0) swd (2.0.2) activesupport (>= 3) attr_required (>= 0.0.5) @@ -712,8 +706,6 @@ GEM faraday-follow_redirects sys-uname (1.2.3) ffi (~> 1.1) - tailwindcss-rails (2.4.0) - railties (>= 6.0.0) tailwindcss-rails (2.4.0-arm64-darwin) railties (>= 6.0.0) tailwindcss-rails (2.4.0-x86_64-linux) @@ -773,7 +765,6 @@ GEM PLATFORMS arm64-darwin - ruby x86_64-linux DEPENDENCIES @@ -807,7 +798,6 @@ DEPENDENCIES deface (~> 1.9) delayed_job_active_record devise (~> 4.8.1) - devise-async! devise_invitable discard doorkeeper (>= 4.6) @@ -823,6 +813,7 @@ DEPENDENCIES image_processing img2zpl! jbuilder + js-routes jsbundling-rails json-jwt json_matchers @@ -835,6 +826,7 @@ DEPENDENCIES newrelic_rpm nokogiri (~> 1.16.5) noticed + oj omniauth (~> 2.1) omniauth-azure-activedirectory-v2 omniauth-linkedin-oauth2 @@ -893,4 +885,4 @@ RUBY VERSION ruby 3.2.2p53 BUNDLED WITH - 2.4.10 + 2.5.11 diff --git a/Makefile b/Makefile index c413735b86..928acecb2a 100644 --- a/Makefile +++ b/Makefile @@ -22,13 +22,13 @@ heroku: @echo "Set environment variables, DATABASE_URL, RAILS_SERVE_STATIC_FILES, RAKE_ENV, RAILS_ENV, SECRET_KEY_BASE" docker: - @docker-compose build + @docker-compose --progress plain build docker-ci: @docker-compose --progress plain build web docker-production: - @docker-compose -f docker-compose.production.yml build --build-arg BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) + @docker-compose --progress plain -f docker-compose.production.yml build --build-arg BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) config-production: ifeq (production.env,$(wildcard production.env)) diff --git a/Rakefile b/Rakefile index 9f98587c32..c172d03df4 100644 --- a/Rakefile +++ b/Rakefile @@ -5,3 +5,5 @@ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks Doorkeeper::Rake.load_tasks +# Update js-routes file before javascript build +task 'javascript:build' => 'js:routes:typescript' diff --git a/VERSION b/VERSION index defae51814..bf50e910e6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.36.0.1 +1.37.0 diff --git a/app/assets/javascripts/my_modules/protocols.js b/app/assets/javascripts/my_modules/protocols.js index 7d74d1e411..70dbc55933 100644 --- a/app/assets/javascripts/my_modules/protocols.js +++ b/app/assets/javascripts/my_modules/protocols.js @@ -29,7 +29,8 @@ function initEditMyModuleDescription() { }); setTimeout(function() { - TinyMCE.wrapTables(viewObject); + const notesContainerEl = document.getElementById('notes-container'); + window.wrapTables(notesContainerEl); }, 100); } @@ -327,16 +328,6 @@ function initAccessModal() { }); } -function initWrapTables() { - const viewMode = new URLSearchParams(window.location.search).get('view_mode'); - if (['archived', 'locked'].includes(viewMode)) { - setTimeout(() => { - const notesContainerEl = document.getElementById('notes-container'); - window.wrapTables(notesContainerEl); - }, 100); - } -} - /** * Initializes page */ @@ -348,7 +339,6 @@ function init() { initProtocolSectionOpenEvent(); initDetailsDropdown(); initAccessModal(); - initWrapTables(); } init(); diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index 9850f40af9..d030666cea 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -317,9 +317,7 @@ var RepositoryDatatable = (function(global) { checkAvailableColumns(); - RepositoryDatatableRowEditor.switchRowToEditMode(row); - - changeToEditMode(); + RepositoryDatatableRowEditor.switchRowToEditMode(row, changeToEditMode); }); } @@ -330,6 +328,40 @@ var RepositoryDatatable = (function(global) { }); } + function initDeleteAssetValueConfirmModal() { + $('#deleteRepositoryAssetValueModal').on('shown.bs.modal', function() { + let $fileBtn = $(this).data('cellFileBtn'); + let $input = $(this).data('cellInput'); + let $label = $(this).data('cellLabel'); + + $('#confirmAssetValueDelete').one('click', function() { + $fileBtn.addClass('new-file'); + $label.text(''); + $input.val(''); + $fileBtn.removeClass('error'); + + if (!$input.data('is-empty')) { // set hidden field for deletion only if original value has been set on rendering + $input + .prev('.file-hidden-field-container') + .html(``); + } + + $('#deleteRepositoryAssetValueModal').modal('hide'); + }); + }); + + $('#deleteRepositoryAssetValueModal').on('hidden.bs.modal', function() { + const $deleteRepositoryAssetValueModal = $('#deleteRepositoryAssetValueModal'); + + $deleteRepositoryAssetValueModal.data('cellFileBtn', null); + $deleteRepositoryAssetValueModal.data('cellInput', null); + $deleteRepositoryAssetValueModal.data('cellLabel', null); + }); + } + function initActiveRemindersFilter() { $(TABLE_WRAPPER_ID).find('#only_reminders').on('change', function() { var $activeRemindersFilter = $(this).closest('.active-reminders-filter'); @@ -692,6 +724,7 @@ var RepositoryDatatable = (function(global) { }, rowCallback: function(row, data) { $(row).attr('data-editable', data.recordEditable); + $(row).attr('data-info-url', data.recordInfoUrl); $(row).attr('data-manage-stock-url', data.manageStockUrl); // Get row ID let rowId = data.DT_RowId; @@ -805,6 +838,7 @@ var RepositoryDatatable = (function(global) { initSaveButton(); initCancelButton(); initBSTooltips(); + initDeleteAssetValueConfirmModal(); window.initRepositoryStateMenu(); DataTableHelpers.initLengthAppearance($(TABLE_ID).closest('.dataTables_wrapper')); @@ -1003,10 +1037,8 @@ var RepositoryDatatable = (function(global) { $(TABLE_ID).find('.repository-row-edit-icon').remove(); rowsSelected.forEach(function(rowNumber) { - RepositoryDatatableRowEditor.switchRowToEditMode(TABLE.row('#' + rowNumber)); + RepositoryDatatableRowEditor.switchRowToEditMode(TABLE.row('#' + rowNumber), changeToEditMode); }); - - changeToEditMode(); }) .on('click', '#assignRepositoryRecords', function(e) { e.preventDefault(); diff --git a/app/assets/javascripts/repositories/row_editor.js b/app/assets/javascripts/repositories/row_editor.js index 48a9517c8a..422b01acfd 100644 --- a/app/assets/javascripts/repositories/row_editor.js +++ b/app/assets/javascripts/repositories/row_editor.js @@ -80,25 +80,17 @@ var RepositoryDatatableRowEditor = (function() { $fileBtn.removeClass('error'); }); - deleteButtons.on('click', function() { + const $deleteRepositoryAssetValueModal = $('#deleteRepositoryAssetValueModal'); let $fileBtn = $(this).parent(); let $input = $fileBtn.prev('input[type=file]'); let $label = $fileBtn.find('label'); - $fileBtn.addClass('new-file'); - $label.text(''); - $input.val(''); - $fileBtn.removeClass('error'); + $deleteRepositoryAssetValueModal.data('cellFileBtn', $fileBtn); + $deleteRepositoryAssetValueModal.data('cellInput', $input); + $deleteRepositoryAssetValueModal.data('cellLabel', $label); - if (!$input.data('is-empty')) { // set hidden field for deletion only if original value has been set on rendering - $input - .prev('.file-hidden-field-container') - .html(``); - } + $('#deleteRepositoryAssetValueModal').modal('show'); }); } @@ -173,11 +165,17 @@ var RepositoryDatatableRowEditor = (function() { TABLE.columns.adjust(); } - function switchRowToEditMode(row) { + function enableEditMode(row, isEditable) { + if (!isEditable) { + HelperModule.flashAlertMsg(I18n.t('repositories.table.row_locked'), 'danger'); + return false; + } + let $row = $(row.node()); let itemId = row.id(); let formId = `repositoryRowForm${itemId}`; let requestUrl = $(TABLE.table().node()).data('current-uri'); + let rowForm = $(`
{ + if (enableEditMode(row, data.editable)) editEnabledCallback(); + } + }); } return Object.freeze({ diff --git a/app/assets/javascripts/shareable_links/handson_table_wraping.js b/app/assets/javascripts/shareable_links/handson_table_wraping.js index 084c6d1ae5..68c91b104d 100644 --- a/app/assets/javascripts/shareable_links/handson_table_wraping.js +++ b/app/assets/javascripts/shareable_links/handson_table_wraping.js @@ -1,17 +1,12 @@ /* global */ (function () { - const rtf = $('.rtf-view').toArray(); - for (let i = 0; i < rtf.length; i += 1) { - const container = $(rtf[i]).find('table').toArray(); - - for (let j = 0; j < container.length; j += 1) { - const table = $(container[j]); - if ($(table).parent().hasClass('table-wrapper')) return; - - $(table).wrap(` -
- `); - } - } + $('.rtf-view').toArray().forEach((rtf) => { + $(rtf).find('table').toArray().forEach((table) => { + if ($(table).parents('table').length === 0) { + $(table).css('float', 'none') + .wrapAll('
'); + } + }); + }); }()); diff --git a/app/assets/javascripts/sitewide/active_storage_previews.js b/app/assets/javascripts/sitewide/active_storage_previews.js index fe0ab846ed..d1085dd332 100644 --- a/app/assets/javascripts/sitewide/active_storage_previews.js +++ b/app/assets/javascripts/sitewide/active_storage_previews.js @@ -22,8 +22,10 @@ var ActiveStoragePreviews = (function() { if (!$(img).parent().hasClass('processing')) $(img).parent().addClass('processing'); setTimeout(() => { - img.src = src; - img.retryCount += 1; + if (document.body.contains(img)) { + img.src = src; + img.retryCount += 1; + } }, RETRY_DELAY); }, showPreview: function(ev) { diff --git a/app/assets/javascripts/sitewide/constants.js.erb b/app/assets/javascripts/sitewide/constants.js.erb index ff1416e1e1..bcbd62513b 100644 --- a/app/assets/javascripts/sitewide/constants.js.erb +++ b/app/assets/javascripts/sitewide/constants.js.erb @@ -19,5 +19,6 @@ const GLOBAL_CONSTANTS = { ASSET_POLLING_INTERVAL: <%= Constants::ASSET_POLLING_INTERVAL %>, ASSET_SYNC_URL: '<%= Constants::ASSET_SYNC_URL %>', GLOBAL_SEARCH_PREVIEW_LIMIT: <%= Constants::GLOBAL_SEARCH_PREVIEW_LIMIT %>, - SEARCH_LIMIT: <%= Constants::SEARCH_LIMIT %> + SEARCH_LIMIT: <%= Constants::SEARCH_LIMIT %>, + SCINOTE_EDIT_RESTRICTED_EXTENSIONS: <%= Constants::SCINOTE_EDIT_RESTRICTED_EXTENSIONS %> }; diff --git a/app/assets/javascripts/sitewide/utils.js b/app/assets/javascripts/sitewide/utils.js index d4ae794627..bfd02c99f2 100644 --- a/app/assets/javascripts/sitewide/utils.js +++ b/app/assets/javascripts/sitewide/utils.js @@ -142,23 +142,14 @@ $.fn.initSubmitModal = function(modalID, modelName) { * @returns {string} - HTML with tables wrapped. */ function wrapTables(htmlStringOrDomEl) { - if (typeof htmlStringOrDomEl === 'string') { - const container = $(`${htmlStringOrDomEl}`); - container.find('table').toArray().forEach((table) => { - if ($(table).parent().hasClass('table-wrapper')) return; - $(table).css('float', 'none').wrapAll(` -
- `); - }); - return container.prop('outerHTML'); - } - // Check if the value is a DOM element - if (htmlStringOrDomEl instanceof Element) { - const tableElement = $(htmlStringOrDomEl).find('table'); - if (tableElement.length > 0) { - tableElement.wrap('
'); - const updatedHtml = $(htmlStringOrDomEl).html(); - $(htmlStringOrDomEl).replaceWith(updatedHtml); + const htmlContent = `${htmlStringOrDomEl}`; + const container = typeof htmlStringOrDomEl === 'string' ? $(htmlContent) : $(htmlStringOrDomEl); + + container.find('table').toArray().forEach((table) => { + if ($(table).parents('table').length === 0) { + $(table).css('float', 'none') + .wrapAll('
'); } - } + }); + return container.prop('outerHTML'); } diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 400c99bdf9..9e83df130c 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -2,6 +2,7 @@ @import "tailwind/buttons"; @import "tailwind/modals"; @import "tailwind/flyouts"; +@import "tailwind/radio"; @import "tailwind/loader.css"; @tailwind base; @@ -69,6 +70,6 @@ html { @keyframes shine-lines { 0% { background-position: -150px } - + 40%, 100% { background-position: 320px } } diff --git a/app/assets/stylesheets/layouts/reminders.scss b/app/assets/stylesheets/layouts/reminders.scss index 69a0000b3f..126981812e 100644 --- a/app/assets/stylesheets/layouts/reminders.scss +++ b/app/assets/stylesheets/layouts/reminders.scss @@ -26,5 +26,11 @@ border-width: 0; height: 1px; margin: 0 16px 10px; - } } +} + +.reminders-view-mode { + .row-reminders-footer { + display: none; + } +} diff --git a/app/assets/stylesheets/navigation/notifications.scss b/app/assets/stylesheets/navigation/notifications.scss index 688a949953..c7049d6efa 100644 --- a/app/assets/stylesheets/navigation/notifications.scss +++ b/app/assets/stylesheets/navigation/notifications.scss @@ -30,7 +30,7 @@ flex-direction: column; height: calc(100vh - 8rem); padding: 1.5rem; - width: 400px; + width: 600px; .sci--navigation--notificaitons-flyout-title { @include font-h2; diff --git a/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss b/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss index b940efe9e0..5b7fd31ca1 100644 --- a/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss +++ b/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss @@ -1,5 +1,5 @@ // scss-lint:disable SelectorDepth QualifyingElement - +/* :root { --sci-radio-size: 16px; } @@ -85,3 +85,4 @@ input[type="radio"].sci-radio { } } } +*/ diff --git a/app/assets/stylesheets/tailwind/radio.css b/app/assets/stylesheets/tailwind/radio.css new file mode 100644 index 0000000000..1971193979 --- /dev/null +++ b/app/assets/stylesheets/tailwind/radio.css @@ -0,0 +1,42 @@ +@layer components { + + .sci-radio-container { + @apply inline-block h-4 w-4 relative; + } + + input[type="radio"].sci-radio { + @apply cursor-pointer shrink-0 h-4 w-4 m-0 opacity-0 relative z-[2]; + } + + input[type="radio"].sci-radio + .sci-radio-label { + @apply inline-block shrink-0 h-4 w-4 absolute left-0; + } + + input[type="radio"].sci-radio + .sci-radio-label::before { + @apply border-[1px] border-solid border-sn-black rounded-full text-white text-center transition-all + h-4 w-4 left-0 absolute; + content: ""; + } + + input[type="radio"].sci-radio + .sci-radio-label::after{ + @apply bg-white rounded-full text-white text-center transition-all + absolute w-2.5 h-2.5 top-[3px] left-[3px] ; + content: ""; + } + + input[type="radio"].sci-radio:checked + .sci-radio-label::before { + @apply !border-sn-blue; + } + + input[type="radio"].sci-radio:checked + .sci-radio-label::after { + @apply !bg-sn-science-blue; + } + + input[type="radio"].sci-radio:disabled + .sci-radio-label::before { + @apply !border-sn-sleepy-grey; + } + + input[type="radio"].sci-radio:checked:disabled + .sci-radio-label::after { + @apply !bg-sn-sleepy-grey; + } +} diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 7116779807..fd7f60180e 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -1362,6 +1362,10 @@ th.custom-field .modal-tooltiptext { cursor: pointer; } +.tooltip { + z-index: 9999; +} + .tooltip-open { background-color: $color-concrete; color: $color-black; diff --git a/app/controllers/api/api_controller.rb b/app/controllers/api/api_controller.rb index 27141eded2..3eb460f188 100644 --- a/app/controllers/api/api_controller.rb +++ b/app/controllers/api/api_controller.rb @@ -8,6 +8,8 @@ class ApiController < ActionController::API before_action :authenticate_request!, except: %i(status health) + newrelic_ignore only: %i(health status) + rescue_from StandardError do |e| logger.error e.message logger.error e.backtrace.join("\n") diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 72eb1780eb..213e4bc568 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -180,6 +180,9 @@ def load_team(key = :team_id) def load_inventory(key = :inventory_id) @inventory = @team.repositories.find(params.require(key)) + + @inventory.unlock! if @inventory.is_a?(SoftLockedRepository) + raise PermissionError.new(Repository, :read) unless can_read_repository?(@inventory) end diff --git a/app/controllers/api/v1/results_controller.rb b/app/controllers/api/v1/results_controller.rb index c26c2ef02e..4665f46a8d 100644 --- a/app/controllers/api/v1/results_controller.rb +++ b/app/controllers/api/v1/results_controller.rb @@ -124,7 +124,7 @@ def update_file_result Result.transaction do old_checksum = asset.file.blob.checksum if @form_multipart_upload - asset.file.attach(result_file_params[:file]) + asset.attach_file_version(result_file_params[:file]) else blob = create_blob_from_params asset.update!(file: blob) diff --git a/app/controllers/api/v2/inventory_item_child_relationships_controller.rb b/app/controllers/api/v2/inventory_item_child_relationships_controller.rb index 8d60437535..47571b8832 100644 --- a/app/controllers/api/v2/inventory_item_child_relationships_controller.rb +++ b/app/controllers/api/v2/inventory_item_child_relationships_controller.rb @@ -18,7 +18,7 @@ def show end def create - inventory_item_to_link = RepositoryRow.where(repository: Repository.accessible_by_teams(@team)) + inventory_item_to_link = RepositoryRow.where(repository: Repository.viewable_by_user(current_user, @team)) .find(connection_params[:child_id]) child_connection = @inventory_item.child_connections.create!( child: inventory_item_to_link, diff --git a/app/controllers/api/v2/inventory_item_parent_relationships_controller.rb b/app/controllers/api/v2/inventory_item_parent_relationships_controller.rb index dec716a7da..bf0c663d52 100644 --- a/app/controllers/api/v2/inventory_item_parent_relationships_controller.rb +++ b/app/controllers/api/v2/inventory_item_parent_relationships_controller.rb @@ -20,7 +20,7 @@ def show end def create - inventory_item_to_link = RepositoryRow.where(repository: Repository.accessible_by_teams(@team)) + inventory_item_to_link = RepositoryRow.where(repository: Repository.viewable_by_user(current_user, @team)) .find(connection_params[:parent_id]) parent_connection = @inventory_item.parent_connections.create!( parent: inventory_item_to_link, diff --git a/app/controllers/asset_sync_controller.rb b/app/controllers/asset_sync_controller.rb index 54c6e7440e..968503cd77 100644 --- a/app/controllers/asset_sync_controller.rb +++ b/app/controllers/asset_sync_controller.rb @@ -16,7 +16,8 @@ def show asset_sync_token = current_user.asset_sync_tokens.find_or_create_by(asset_id: params[:asset_id]) unless asset_sync_token.token_valid? - asset_sync_token = current_user.asset_sync_tokens.create(asset_id: params[:asset_id]) + asset_sync_token = + current_user.asset_sync_tokens.create(asset_id: params[:asset_id]) end render json: AssetSyncTokenSerializer.new(asset_sync_token).as_json @@ -27,34 +28,32 @@ def download end def update - if @asset_sync_token.conflicts?(request.headers['VersionToken']) - ActiveRecord::Base.transaction do - conflict_response = AssetSyncTokenSerializer.new(conflicting_asset_copy_token).as_json - error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) } - log_activity(:create) - render json: conflict_response.merge(error_message), status: :conflict - end - - return - end - - orig_file_size = @asset.file_size + asset_conflicts = @asset_sync_token.conflicts?(request.headers['VersionToken']) ActiveRecord::Base.transaction do @asset.update(last_modified_by: current_user) if wopi_file?(@asset) @asset.update_contents(request.body) else - @asset.file.attach(io: request.body, filename: @asset.file.filename) + @asset.attach_file_version(io: request.body, filename: @asset.file.filename) @asset.touch end - @asset.team.release_space(orig_file_size) @asset.post_process_file log_activity(:edit) end + if asset_conflicts + ActiveRecord::Base.transaction do + conflict_response = AssetSyncTokenSerializer.new(@asset_sync_token).as_json + error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) } + render json: conflict_response.merge(error_message), status: :conflict + end + + return + end + render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json end @@ -94,7 +93,7 @@ def conflicting_asset_copy_token metadata: @asset.blob.metadata ) - new_asset.file.attach(blob) + new_asset.attach_file_version(blob) case @asset.parent when Step diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 383964890b..5f49147597 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -19,15 +19,14 @@ class AssetsController < ApplicationController before_action :load_vars, except: :create_wopi_file before_action :check_read_permission, except: %i(edit destroy duplicate create_wopi_file toggle_view_mode) before_action :check_manage_permission, only: %i(edit destroy duplicate rename toggle_view_mode) + before_action :check_restore_permission, only: :restore_version def file_preview - editable = can_manage_asset?(@asset) && (@asset.repository_asset_value.blank? || - !@asset.repository_cell.repository_row.repository.is_a?(SoftLockedRepository)) render json: { html: render_to_string( partial: 'shared/file_preview/content', locals: { asset: @asset, - can_edit: editable, + can_edit: can_manage_asset?(@asset), gallery: params[:gallery], preview: params[:preview] }, @@ -197,7 +196,7 @@ def update_image return render_403 unless can_read_team?(@asset.team) @asset.last_modified_by = current_user - @asset.file.attach(io: params.require(:image), filename: orig_file_name) + @asset.attach_file_version(io: params.require(:image), filename: orig_file_name) @asset.save! create_edit_image_activity(@asset, current_user, :finish_editing) # release previous image space @@ -242,9 +241,9 @@ def create_wopi_file # Asset validation asset = Asset.new(created_by: current_user, team: current_team) - asset.file.attach(io: StringIO.new, - filename: "#{params[:file_name]}.#{params[:file_type]}", - content_type: wopi_content_type(params[:file_type])) + asset.attach_file_version(io: StringIO.new, + filename: "#{params[:file_name]}.#{params[:file_type]}", + content_type: wopi_content_type(params[:file_type])) unless asset.valid?(:wopi_file_creation) render json: { @@ -397,13 +396,66 @@ def checksum render json: { checksum: @asset.file.blob.checksum } end + def versions + blobs = + [@asset.file.blob] + + @asset.previous_files.map(&:blob).sort_by { |b| -1 * b.metadata['version'].to_i }[0..(VersionedAttachments.enabled? ? -1 : 1)] + render( + json: ActiveModel::SerializableResource.new( + blobs, + each_serializer: ActiveStorage::BlobSerializer, + user: current_user + ).as_json.merge( + enabled: VersionedAttachments.enabled?, + enable_url: ENV.fetch('SCINOTE_FILE_VERSIONING_ENABLE_URL', nil), + disabled_disclaimer: VersionedAttachments.disabled_disclaimer + ) + ) + end + + def restore_version + render_403 unless VersionedAttachments.enabled? + + @asset.restore_file_version(params[:version].to_i) + @asset.restore_preview_image_version(params[:version].to_i) if @asset.preview_image.attached? + + message_items = { + version: params[:version].to_i, + file: @asset.file_name + } + + case @asset.parent + when Step + if @asset.parent.protocol.in_module? + message_items.merge!({ my_module: @assoc.protocol.my_module.id, step: @asset.parent.id }) + log_restore_activity(:task_step_restore_asset_version, @assoc.protocol, + @assoc.protocol.team, @assoc.my_module&.project, message_items) + else + message_items.merge!({ protocol: @assoc.protocol.id, step: @asset.parent.id }) + log_restore_activity(:protocol_step_restore_asset_version, @assoc.protocol, + @assoc.protocol.team, nil, message_items) + end + when Result + message_items.merge!({ result: @assoc.id, my_module: @assoc.my_module.id }) + log_restore_activity(:task_result_restore_asset_version, @assoc, + @assoc.my_module.team, @assoc.my_module.project, message_items) + when RepositoryCell + message_items.merge!({ repository_column: @assoc.repository_column.id, repository: @repository.id }) + log_restore_activity(:repository_column_restore_asset_version, @repository, + @repository.team, nil, message_items) + end + + render json: @asset.file.blob + end + private def load_vars @asset = Asset.find_by(id: params[:id]) return render_404 unless @asset - current_user.permission_team = @asset.team + # don't overwrite permission team if asset is in a repositoy, since then sharing rules may apply and depend on user's current team + current_user.permission_team = @asset.team unless @asset.repository_cell @assoc ||= @asset.step @assoc ||= @asset.result @@ -426,6 +478,10 @@ def check_manage_permission render_403 and return unless can_manage_asset?(@asset) end + def check_restore_permission + render_403 and return unless can_restore_asset?(@asset) + end + def append_wd_params(url) exclude_params = %w(wdPreviousSession wdPreviousCorrelation) wd_params = params.as_json.select { |key, _value| key[/^wd.*/] && !(exclude_params.include? key) }.to_query @@ -472,4 +528,14 @@ def log_result_activity(type_of, result, message_items) result: result.id }.merge(message_items)) end + + def log_restore_activity(type_of, subject, team, project = nil, message_items = {}) + Activities::CreateActivityService + .call(activity_type: type_of, + owner: current_user, + subject: subject, + team: team, + project: project, + message_items: message_items) + end end diff --git a/app/controllers/at_who_controller.rb b/app/controllers/at_who_controller.rb index f6a801d115..f48645f81c 100644 --- a/app/controllers/at_who_controller.rb +++ b/app/controllers/at_who_controller.rb @@ -27,7 +27,7 @@ def rep_items if params[:repository_id].present? Repository.find_by(id: params[:repository_id]) else - Repository.active.accessible_by_teams(@team).first + Repository.active.viewable_by_user(current_user, @team).first end items = [] @@ -54,8 +54,8 @@ def rep_items end def menu - repositories = Repository.active.accessible_by_teams(@team) - render json: { + repositories = Repository.active.viewable_by_user(current_user, @team) + render json: { html: render_to_string(partial: 'shared/smart_annotation/menu', locals: { repositories: repositories }, formats: :html) diff --git a/app/controllers/concerns/active_storage/check_blob_permissions.rb b/app/controllers/concerns/active_storage/check_blob_permissions.rb index 2c99d9b7e2..d23952ff6f 100644 --- a/app/controllers/concerns/active_storage/check_blob_permissions.rb +++ b/app/controllers/concerns/active_storage/check_blob_permissions.rb @@ -28,6 +28,8 @@ def check_attachment_read_permissions(attachment) check_tinymce_asset_read_permissions(attachment.record) when 'Experiment' can_read_experiment?(attachment.record) + when 'StorageLocation' + can_read_storage_location?(attachment.record) when 'Report' can_read_project?(attachment.record.project) when 'User' diff --git a/app/controllers/dashboard/calendars_controller.rb b/app/controllers/dashboard/calendars_controller.rb index 93217a54c2..50c101638e 100644 --- a/app/controllers/dashboard/calendars_controller.rb +++ b/app/controllers/dashboard/calendars_controller.rb @@ -9,7 +9,7 @@ def show date = params[:date].in_time_zone(current_user.time_zone) start_date = date.at_beginning_of_month.utc - 8.days end_date = date.at_end_of_month.utc + 15.days - due_dates = current_user.my_modules.active.uncomplete + due_dates = current_user.my_modules.readable_by_user(current_user).active.uncomplete .joins(experiment: :project) .where(experiments: { archived: false }) .where(projects: { archived: false }) @@ -23,7 +23,7 @@ def day date = params[:date].in_time_zone(current_user.time_zone) start_date = date.utc end_date = date.end_of_day.utc - my_modules = current_user.my_modules.active.uncomplete + my_modules = current_user.my_modules.readable_by_user(current_user).active.uncomplete .joins(experiment: :project) .where(experiments: { archived: false }) .where(projects: { archived: false }) diff --git a/app/controllers/gene_sequence_assets_controller.rb b/app/controllers/gene_sequence_assets_controller.rb index a706a200fe..69526e9a60 100644 --- a/app/controllers/gene_sequence_assets_controller.rb +++ b/app/controllers/gene_sequence_assets_controller.rb @@ -72,15 +72,12 @@ def save_asset! ensure_asset! - @asset.file.purge - @asset.preview_image.purge - - @asset.file.attach( + @asset.attach_file_version( io: StringIO.new(params[:sequence_data].to_json), filename: "#{params[:sequence_name]}.json" ) - @asset.preview_image.attach( + @asset.attach_preview_image_version( io: StringIO.new(Base64.decode64(params[:base64_image].split(',').last)), filename: "#{params[:sequence_name]}.png" ) diff --git a/app/controllers/global_activities_controller.rb b/app/controllers/global_activities_controller.rb index b531c5ac13..b84085994b 100644 --- a/app/controllers/global_activities_controller.rb +++ b/app/controllers/global_activities_controller.rb @@ -151,7 +151,25 @@ def get_objects(subject) end def activity_filter_params - params.permit(:name, filter: {}) + params.permit( + :name, + filter: [ + :to_date, + :from_date, + { types: [] }, + { subjects: { + 'Report' => [], + 'Project' => [], + 'MyModule' => [], + 'Protocol' => [], + 'Experiment' => [], + 'RepositoryRow' => [], + 'RepositoryBase' => [] + } }, + { users: [] }, + { teams: [] } + ] + ) end def activity_filters diff --git a/app/controllers/hidden_repository_cell_reminders_controller.rb b/app/controllers/hidden_repository_cell_reminders_controller.rb index 5ed53b5f11..ad7472d8ce 100644 --- a/app/controllers/hidden_repository_cell_reminders_controller.rb +++ b/app/controllers/hidden_repository_cell_reminders_controller.rb @@ -15,7 +15,7 @@ def create private def load_repository - @repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id]) + @repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id]) render_404 unless @repository end diff --git a/app/controllers/my_module_repositories_controller.rb b/app/controllers/my_module_repositories_controller.rb index 1cb81a6c06..d89dcb42bc 100644 --- a/app/controllers/my_module_repositories_controller.rb +++ b/app/controllers/my_module_repositories_controller.rb @@ -15,7 +15,14 @@ def index_dt @draw = params[:draw].to_i per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i page = (params[:start].to_i / per_page) + 1 - datatable_service = RepositoryDatatableService.new(@repository, params, current_user, @my_module) + if params[:simple_view] + rows_view = 'repository_rows/simple_view_index' + preload_cells = false + else + rows_view = 'repository_rows/index' + preload_cells = true + end + datatable_service = RepositoryDatatableService.new(@repository, params, current_user, @my_module, preload_cells: preload_cells) @datatable_params = { view_mode: params[:view_mode], @@ -26,19 +33,9 @@ def index_dt @all_rows_count = datatable_service.all_count @columns_mappings = datatable_service.mappings - - if params[:simple_view] - repository_rows = datatable_service.repository_rows - rows_view = 'repository_rows/simple_view_index' - else - repository_rows = datatable_service.repository_rows - .preload(:repository_columns, - :created_by, - repository_cells: { value: @repository.cell_preload_includes }) - rows_view = 'repository_rows/index' - end + repository_rows = datatable_service.repository_rows @repository_rows = repository_rows.page(page).per(per_page) - + @filtered_rows_count = @repository_rows.load.take&.filtered_count || 0 render rows_view end @@ -145,7 +142,7 @@ def assign_repository_records_modal end def repositories_list_html - @assigned_repositories = @my_module.live_and_snapshot_repositories_list + @assigned_repositories = @my_module.readable_live_and_snapshot_repositories_list(current_user) render json: { html: render_to_string(partial: 'my_modules/repositories/repositories_list'), assigned_rows_count: @assigned_repositories.map(&:assigned_rows_count).sum @@ -162,7 +159,7 @@ def full_view_table end def repositories_dropdown_list - @repositories = Repository.accessible_by_teams(current_team).joins(" + @repositories = Repository.viewable_by_user(current_user).joins(" LEFT OUTER JOIN repository_rows ON repository_rows.repository_id = repositories.id LEFT OUTER JOIN my_module_repository_rows ON diff --git a/app/controllers/my_module_repository_snapshots_controller.rb b/app/controllers/my_module_repository_snapshots_controller.rb index 2d0c6a128e..6d1cff5d34 100644 --- a/app/controllers/my_module_repository_snapshots_controller.rb +++ b/app/controllers/my_module_repository_snapshots_controller.rb @@ -16,20 +16,16 @@ def index_dt @all_rows_count = datatable_service.all_count @columns_mappings = datatable_service.mappings + repository_rows = datatable_service.repository_rows + if params[:simple_view] - repository_rows = datatable_service.repository_rows @repository = @repository_snapshot rows_view = 'repository_rows/simple_view_index' else - repository_rows = - datatable_service.repository_rows - .preload(:repository_columns, - :created_by, - repository_cells: { value: @repository_snapshot.cell_preload_includes }) rows_view = 'repository_rows/snapshot_index' end @repository_rows = repository_rows.page(page).per(per_page) - + @filtered_rows_count = @repository_rows.load.take&.filtered_count || 0 render rows_view end diff --git a/app/controllers/my_module_shareable_links_controller.rb b/app/controllers/my_module_shareable_links_controller.rb index 225825ab02..594d6cf8b8 100644 --- a/app/controllers/my_module_shareable_links_controller.rb +++ b/app/controllers/my_module_shareable_links_controller.rb @@ -62,7 +62,7 @@ def repository_index_dt @draw = params[:draw].to_i per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i page = (params[:start].to_i / per_page) + 1 - datatable_service = RepositoryDatatableService.new(@repository, params, nil, @my_module) + datatable_service = RepositoryDatatableService.new(@repository, params, nil, @my_module, preload_cells: false, disable_reminders: true) @datatable_params = { view_mode: params[:view_mode], @@ -76,6 +76,7 @@ def repository_index_dt @columns_mappings = datatable_service.mappings @repository_rows = datatable_service.repository_rows.page(page).per(per_page) + @filtered_rows_count = @repository_rows.load.take&.filtered_count || 0 render 'repository_rows/simple_view_index' end @@ -84,13 +85,14 @@ def repository_snapshot_index_dt @draw = params[:draw].to_i per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i page = (params[:start].to_i / per_page) + 1 - datatable_service = RepositorySnapshotDatatableService.new(@repository_snapshot, params, nil, @my_module) + datatable_service = RepositorySnapshotDatatableService.new(@repository_snapshot, params, nil, @my_module, preload_cells: false) @all_rows_count = datatable_service.all_count @columns_mappings = datatable_service.mappings @repository = @repository_snapshot @repository_rows = datatable_service.repository_rows.page(page).per(per_page) + @filtered_rows_count = @repository_rows.load.take&.filtered_count || 0 render 'repository_rows/simple_view_index' end diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 807c5261d9..a9e941cd28 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -304,7 +304,7 @@ def update_protocol_description def protocols @protocol = @my_module.protocol - @assigned_repositories = @my_module.live_and_snapshot_repositories_list + @assigned_repositories = @my_module.readable_live_and_snapshot_repositories_list(current_user) end def protocol @@ -651,7 +651,7 @@ def set_navigator @navigator = { url: tree_navigator_my_module_path(@my_module), - archived: params[:view_mode] == 'archived', + archived: @my_module.archived_branch? || params[:view_mode] == 'archived', id: @my_module.code } end diff --git a/app/controllers/protocols_controller.rb b/app/controllers/protocols_controller.rb index b0ade7fb48..63a782d595 100644 --- a/app/controllers/protocols_controller.rb +++ b/app/controllers/protocols_controller.rb @@ -909,7 +909,6 @@ def move_protocols(action) end def set_inline_name_editing - return unless @protocol.initial_draft? return unless can_manage_protocol_draft_in_repository?(@protocol) @inline_editable_title_config = { diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 2e5b20a393..3618b9cfb6 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -348,7 +348,7 @@ def load_vars_nested def load_wizard_vars @templates = Extends::REPORT_TEMPLATES @docx_templates = Extends::DOCX_REPORT_TEMPLATES - live_repositories = Repository.accessible_by_teams(current_team).sort_by { |r| r.name.downcase } + live_repositories = Repository.viewable_by_user(current_user).sort_by { |r| r.name.downcase } snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository) .where(team: current_team) .where.not(original_repository: live_repositories) @@ -384,7 +384,7 @@ def check_manage_permissions def load_available_repositories @available_repositories = [] repositories = Repository.active - .accessible_by_teams(current_team) + .viewable_by_user(current_user) .name_like(search_params[:query]) .limit(Constants::SEARCH_LIMIT) repositories.each do |repository| @@ -397,7 +397,7 @@ def load_available_repositories def report_params params.require(:report) - .permit(:name, :description, :grouped_by, :report_contents, settings: {}) + .permit(:name, :description, :grouped_by, :report_contents, settings: permit_report_settings_structure(Report::DEFAULT_SETTINGS)) end def search_params diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 8e5ead106c..e17486afcf 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -10,18 +10,17 @@ class RepositoriesController < ApplicationController include MyModulesHelper before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar - export_modal export_repositories) - before_action :load_repositories, only: :index + export_modal export_repositories list) + before_action :load_repositories, only: %i(index list) before_action :load_repositories_for_archiving, only: :archive before_action :load_repositories_for_restoring, only: :restore - before_action :check_view_all_permissions, only: %i(index sidebar) + before_action :check_view_all_permissions, only: %i(index sidebar list) before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet import_records sidebar archive restore actions_toolbar - export_modal export_repositories) + export_modal export_repositories list) before_action :check_manage_permissions, only: %i(rename_modal update) before_action :check_delete_permissions, only: %i(destroy destroy_modal) before_action :check_archive_permissions, only: %i(archive restore) - before_action :check_share_permissions, only: :share_modal before_action :check_create_permissions, only: %i(create_modal create) before_action :check_copy_permissions, only: %i(copy_modal copy) before_action :set_inline_name_editing, only: %i(show) @@ -44,6 +43,34 @@ def index end end + def list + results = @repositories.select(:id, :name, 'LOWER(repositories.name)') + results = results.name_like(params[:query]) if params[:query].present? + results = results.joins(:repository_rows).distinct if params[:non_empty].present? + results = results.active if params[:active].present? + + render json: { data: results.order('LOWER(repositories.name) asc').map { |r| [r.id, r.name] } } + end + + def rows_list + results = @repository.repository_rows + if params[:query].present? + results = results.where_attributes_like( + ['repository_rows.name', RepositoryRow::PREFIXED_ID_SQL], + params[:query] + ) + end + results = results.active if params[:active].present? + + results = results.order('LOWER(repository_rows.name) asc').page(params[:page]) + + render json: { + paginated: true, + next_page: results.next_page, + data: results.map { |r| [r.id, r.name] } + } + end + def sidebar render json: { html: render_to_string(partial: 'repositories/sidebar', locals: { @@ -101,15 +128,6 @@ def create_modal } end - def share_modal - render json: { html: render_to_string(partial: 'share_repository_modal', formats: :html) } - end - - def shareable_teams - teams = current_user.teams.order(:name) - [@repository.team] - render json: teams, each_serializer: ShareableTeamSerializer, repository: @repository - end - def hide_reminders # synchronously hide currently visible reminders if params[:visible_reminder_repository_row_ids].present? @@ -298,14 +316,14 @@ def parse_sheet end def import_records - render_403 unless can_create_repository_rows?(Repository.accessible_by_teams(current_team) + render_403 unless can_create_repository_rows?(Repository.viewable_by_user(current_user) .find_by(id: import_params[:id])) # Check if there exist mapping for repository record (it's mandatory) if import_params[:mappings].present? && import_params[:mappings].value?('-1') status = ImportRepository::ImportRecords .new( temp_file: TempFile.find_by(id: import_params[:file_id]), - repository: Repository.accessible_by_teams(current_team).find_by(id: import_params[:id]), + repository: Repository.viewable_by_user(current_user).find_by(id: import_params[:id]), mappings: import_params[:mappings], session: session, user: current_user, @@ -452,12 +470,12 @@ def actions_toolbar def load_repository repository_id = params[:id] || params[:repository_id] - @repository = Repository.accessible_by_teams(current_user.teams).find_by(id: repository_id) + @repository = Repository.viewable_by_user(current_user).find_by(id: repository_id) render_404 unless @repository end def load_repositories - @repositories = Repository.accessible_by_teams(current_team) + @repositories = Repository.viewable_by_user(current_user) end def load_repositories_for_archiving @@ -477,7 +495,7 @@ def load_repository_row end def set_inline_name_editing - return unless can_manage_repository?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless can_manage_repository?(@repository) @inline_editable_title_config = { name: 'title', @@ -522,10 +540,6 @@ def check_delete_permissions render_403 unless can_delete_repository?(@repository) end - def check_share_permissions - render_403 unless can_share_repository?(@repository) - end - def repository_params params.require(:repository).permit(:name) end diff --git a/app/controllers/repository_columns_controller.rb b/app/controllers/repository_columns_controller.rb index 7b6e640dcc..707a6beb8b 100644 --- a/app/controllers/repository_columns_controller.rb +++ b/app/controllers/repository_columns_controller.rb @@ -107,7 +107,7 @@ def available_columns AvailableRepositoryColumn = Struct.new(:id, :name) def load_repository - @repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id]) + @repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id]) render_404 unless @repository end diff --git a/app/controllers/repository_row_connections_controller.rb b/app/controllers/repository_row_connections_controller.rb index 61e1c79b39..f3b3d998d2 100644 --- a/app/controllers/repository_row_connections_controller.rb +++ b/app/controllers/repository_row_connections_controller.rb @@ -56,7 +56,7 @@ def destroy end def repositories - repositories = Repository.accessible_by_teams(current_team) + repositories = Repository.viewable_by_user(current_user) .search_by_name_and_id(current_user, current_user.teams, params[:query]) .order(name: :asc) .page(params[:page] || 1) @@ -69,7 +69,7 @@ def repositories end def repository_rows - selected_repository = Repository.accessible_by_teams(current_team).find(params[:selected_repository_id]) + selected_repository = Repository.viewable_by_user(current_user).find(params[:selected_repository_id]) repository_rows = selected_repository.repository_rows .where.not(id: @repository_row.id) @@ -93,14 +93,14 @@ def load_create_vars return render_422(t('.invalid_params')) unless @relation_type - @connection_repository = Repository.accessible_by_teams(current_team) + @connection_repository = Repository.viewable_by_user(current_user) .find_by(id: connection_params[:connection_repository_id]) return render_404 unless @connection_repository return render_403 unless can_connect_repository_rows?(@connection_repository) end def load_repository - @repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id]) + @repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id]) render_404 unless @repository end diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index a9c8b11e14..2e31001dc7 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -4,6 +4,7 @@ class RepositoryRowsController < ApplicationController include ApplicationHelper include MyModulesHelper include RepositoryDatatableHelper + include StorageLocationsHelper before_action :load_repository, except: %i(show print rows_to_print print_zpl validate_label_template_columns) before_action :load_repository_or_snapshot, only: %i(show print rows_to_print print_zpl @@ -27,15 +28,10 @@ def index @all_rows_count = datatable_service.all_count @columns_mappings = datatable_service.mappings - @repository_rows = datatable_service.repository_rows - .preload(:repository_columns, - :created_by, - :archived_by, - repository_cells: { value: @repository.cell_preload_includes }) - .page(page) - .per(per_page) - - @repository_rows = @repository_rows.where(archived: params[:archived]) unless @repository.archived? + repository_rows = datatable_service.repository_rows + repository_rows = repository_rows.where(archived: params[:archived]) unless @repository.archived? + @repository_rows = repository_rows.page(page).per(per_page) + @filtered_rows_count = @repository_rows.load.take&.filtered_count || 0 rescue RepositoryFilters::ColumnNotFoundException render json: { custom_error: I18n.t('repositories.show.repository_filter.errors.column_not_found') } rescue RepositoryFilters::ValueNotFoundException @@ -328,7 +324,7 @@ def restore_records def active_reminder_repository_cells reminder_cells = @repository_row.repository_cells.with_active_reminder(current_user).distinct render json: { - html: render_to_string(partial: 'shared/repository_row_reminder', locals: { + html: render_to_string(partial: 'shared/repository_row_reminder', formats: :html, locals: { reminders: reminder_cells }) } @@ -358,14 +354,14 @@ def relationships AvailableRepositoryRow = Struct.new(:id, :name, :has_file_attached) def load_repository - @repository = Repository.accessible_by_teams(current_team) + @repository = Repository.viewable_by_user(current_user) .eager_load(:repository_columns) .find_by(id: params[:repository_id]) render_404 unless @repository end def load_repository_or_snapshot - @repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id]) || + @repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id]) || RepositorySnapshot.find_by(id: params[:repository_id]) return render_404 unless @repository end diff --git a/app/controllers/repository_table_filters_controller.rb b/app/controllers/repository_table_filters_controller.rb index 8ad3123c5f..a5009529af 100644 --- a/app/controllers/repository_table_filters_controller.rb +++ b/app/controllers/repository_table_filters_controller.rb @@ -70,7 +70,7 @@ def destroy private def load_repository - @repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id]) + @repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id]) render_403 unless can_read_repository?(@repository) end diff --git a/app/controllers/result_assets_controller.rb b/app/controllers/result_assets_controller.rb index e2c429260f..400ae671e2 100644 --- a/app/controllers/result_assets_controller.rb +++ b/app/controllers/result_assets_controller.rb @@ -125,7 +125,7 @@ def create_multiple_results ActiveRecord::Base.transaction do params[:results_files].each do |index, file| asset = Asset.create!(created_by: current_user, last_modified_by: current_user, team: current_team) - asset.file.attach(file[:signed_blob_id]) + asset.attach_file_version(file[:signed_blob_id]) result = Result.create!(user: current_user, my_module: @my_module, name: params[:results_names][index], diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb index 075b2b669c..3bc46e3d21 100644 --- a/app/controllers/results_controller.rb +++ b/app/controllers/results_controller.rb @@ -90,7 +90,7 @@ def upload_attachment team: @my_module.team, view_mode: @result.assets_view_mode ) - @asset.file.attach(params[:signed_blob_id]) + @asset.attach_file_version(params[:signed_blob_id]) @asset.post_process_file end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index a86d04a82a..afa8fd4e4b 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -145,7 +145,10 @@ def new def quick results = if params[:filter].present? - object_quick_search(params[:filter].singularize) + class_name = params[:filter].singularize + return render_422(t('general.invalid_params')) unless Constants::QUICK_SEARCH_SEARCHABLE_OBJECTS.include?(class_name) + + object_quick_search(class_name) else Constants::QUICK_SEARCH_SEARCHABLE_OBJECTS.filter_map do |object| next if object == 'label_template' && !LabelTemplate.enabled? diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 40f2845f55..ce97a4cc20 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -41,7 +41,7 @@ def upload_attachment team: @protocol.team, view_mode: @step.assets_view_mode ) - @asset.file.attach(params[:signed_blob_id]) + @asset.attach_file_version(params[:signed_blob_id]) @asset.post_process_file default_message_items = { diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb new file mode 100644 index 0000000000..a35aa8cbfa --- /dev/null +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +class StorageLocationRepositoryRowsController < ApplicationController + before_action :check_storage_locations_enabled, except: :destroy + before_action :load_storage_location_repository_row, only: %i(update destroy move) + before_action :load_storage_location + before_action :load_repository_row, only: %i(create update destroy move) + before_action :check_read_permissions, except: %i(create actions_toolbar) + before_action :check_manage_permissions, only: %i(create update destroy move) + + def index + storage_location_repository_row = Lists::StorageLocationRepositoryRowsService.new( + current_team, params + ).call + render json: storage_location_repository_row, + each_serializer: Lists::StorageLocationRepositoryRowSerializer, + meta: (pagination_dict(storage_location_repository_row) unless @storage_location.with_grid?) + end + + def create + ActiveRecord::Base.transaction do + @storage_location_repository_row = StorageLocationRepositoryRow.new( + repository_row: @repository_row, + storage_location: @storage_location, + metadata: storage_location_repository_row_params[:metadata] || {}, + created_by: current_user + ) + + @storage_location_repository_row.with_lock do + if @storage_location_repository_row.save + log_activity(:storage_location_repository_row_created) + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer + else + render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity + end + end + end + end + + def update + ActiveRecord::Base.transaction do + @storage_location_repository_row.update(storage_location_repository_row_params) + + if @storage_location_repository_row.save + log_activity(:storage_location_repository_row_moved) + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer + else + render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity + end + end + end + + def move + ActiveRecord::Base.transaction do + @original_storage_location = @storage_location_repository_row.storage_location + @original_position = @storage_location_repository_row.human_readable_position + + @storage_location_repository_row.discard + @storage_location_repository_row = StorageLocationRepositoryRow.create!( + repository_row: @repository_row, + storage_location: @storage_location, + metadata: storage_location_repository_row_params[:metadata] || {}, + created_by: current_user + ) + log_activity( + :storage_location_repository_row_moved, + { + storage_location_original: @original_storage_location.id, + position_original: @original_position + } + ) + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer + rescue ActiveRecord::RecordInvalid => e + render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + end + + def destroy + ActiveRecord::Base.transaction do + if @storage_location_repository_row.discard + log_activity(:storage_location_repository_row_deleted) + render json: {} + else + render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity + end + end + end + + def actions_toolbar + render json: { + actions: Toolbars::StorageLocationRepositoryRowsService.new( + current_user, + items_ids: JSON.parse(params[:items]).pluck('id') + ).actions + } + end + + private + + def check_storage_locations_enabled + render_403 unless StorageLocation.storage_locations_enabled? + end + + def load_storage_location_repository_row + @storage_location_repository_row = StorageLocationRepositoryRow.find( + storage_location_repository_row_params[:id] + ) + render_404 unless @storage_location_repository_row + end + + def load_storage_location + @storage_location = StorageLocation.find( + storage_location_repository_row_params[:storage_location_id] + ) + render_404 unless can_read_storage_location?(@storage_location) + end + + def load_repository_row + @repository_row = RepositoryRow.find(storage_location_repository_row_params[:repository_row_id]) + render_404 unless @repository_row + end + + def storage_location_repository_row_params + params.permit(:id, :storage_location_id, :repository_row_id, + metadata: { position: [] }) + end + + def check_read_permissions + render_403 unless can_read_storage_location?(@storage_location) + end + + def check_manage_permissions + render_403 unless can_manage_storage_location_repository_rows?(@storage_location) + end + + def log_activity(type_of, message_items = {}) + Activities::CreateActivityService + .call(activity_type: type_of, + owner: current_user, + team: @storage_location.team, + subject: @storage_location_repository_row.storage_location, + message_items: { + storage_location: @storage_location_repository_row.storage_location_id, + repository_row: @storage_location_repository_row.repository_row_id, + position: @storage_location_repository_row.human_readable_position, + user: current_user.id + }.merge(message_items)) + end +end diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb new file mode 100644 index 0000000000..3bae95dd88 --- /dev/null +++ b/app/controllers/storage_locations_controller.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true + +class StorageLocationsController < ApplicationController + include ActionView::Helpers::TextHelper + include ApplicationHelper + include TeamsHelper + include Rails.application.routes.url_helpers + + before_action :switch_team_with_param, only: %i(index show) + before_action :check_storage_locations_enabled, except: :unassign_rows + before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows export_container import_container) + before_action :check_read_permissions, except: %i(index create tree actions_toolbar import_container unassign_rows) + before_action :check_manage_repository_rows_permissions, only: %i(import_container unassign_rows) + before_action :check_create_permissions, only: :create + before_action :check_manage_permissions, only: %i(update destroy duplicate move) + before_action :set_breadcrumbs_items, only: %i(index show) + + def index + @parent_location = StorageLocation.find(storage_location_params[:parent_id]) if storage_location_params[:parent_id] + + render_403 if @parent_location && !can_read_storage_location?(@parent_location) + + respond_to do |format| + format.html + format.json do + storage_locations = Lists::StorageLocationsService.new(current_user, current_team, params).call + render json: storage_locations, + each_serializer: Lists::StorageLocationSerializer, + user: current_user, + meta: pagination_dict(storage_locations), + shared_object: + @parent_location && + StorageLocation.select('*').select(StorageLocation.shared_sql_select(current_user)).find(@parent_location.root_storage_location.id) + end + end + end + + def show; end + + def create + ActiveRecord::Base.transaction do + @storage_location = StorageLocation.new( + storage_location_params.merge({ created_by: current_user }) + ) + + @storage_location.team = @storage_location.root_storage_location.team || current_team + + @storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id] + + if @storage_location.save + log_activity('storage_location_created') + storage_location_annotation_notification + render json: @storage_location, serializer: Lists::StorageLocationSerializer + else + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + end + end + end + + def update + ActiveRecord::Base.transaction do + @storage_location.image.purge if params[:file_name].blank? + @storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id] + old_description = @storage_location.description + @storage_location.update(storage_location_params) + + if @storage_location.save + log_activity('storage_location_edited') + storage_location_annotation_notification(old_description) if old_description != @storage_location.description + render json: @storage_location, serializer: Lists::StorageLocationSerializer + else + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + end + end + end + + def destroy + ActiveRecord::Base.transaction do + if @storage_location.discard + log_activity('storage_location_deleted') + render json: {} + else + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + end + end + end + + def duplicate + ActiveRecord::Base.transaction do + new_storage_location = @storage_location.duplicate!(current_user, current_team) + if new_storage_location + @storage_location = new_storage_location + log_activity('storage_location_created') + render json: @storage_location, serializer: Lists::StorageLocationSerializer + else + render json: { errors: :failed }, status: :unprocessable_entity + end + end + end + + def move + ActiveRecord::Base.transaction do + original_storage_location = @storage_location.parent + destination_storage_location = + if move_params[:destination_storage_location_id] == 'root_storage_location' + nil + else + StorageLocation.find(move_params[:destination_storage_location_id]) + end + + render_403 and return if destination_storage_location && !can_manage_storage_location?(destination_storage_location) + + @storage_location.update!(parent: destination_storage_location) + + log_activity('storage_location_moved', { + storage_location_original: original_storage_location&.id, # nil if moved from root + storage_location_destination: destination_storage_location&.id # nil if moved to root + }) + end + + render json: { message: I18n.t('storage_locations.index.move_modal.success_flash') } + rescue StandardError => e + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") + render json: { error: I18n.t('storage_locations.index.move_modal.error_flash') }, status: :bad_request + end + + def tree + records = StorageLocation.viewable_by_user(current_user, current_team) + .where( + parent: nil, + container: [false, params[:container] == 'true'] + ) + records = records.where(team_id: params[:team_id]) if params[:team_id] + + render json: { + locations: storage_locations_recursive_builder(records), + movable_to_root: params[:team_id] && current_team.id == params[:team_id].to_i + } + end + + def available_positions + render json: { positions: @storage_location.available_positions } + end + + def unassign_rows + ActiveRecord::Base.transaction do + @storage_location_repository_rows = @storage_location.storage_location_repository_rows.where(id: params[:ids]) + @storage_location_repository_rows.each(&:discard) + log_unassign_activities + end + + render json: { status: :ok } + end + + def export_container + xlsx = StorageLocations::ExportService.new(@storage_location, current_user).to_xlsx + + send_data( + xlsx, + filename: "#{@storage_location.name.gsub(/\s/, '_')}_export_#{Date.current}.xlsx", + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + end + + def import_container + result = StorageLocations::ImportService.new(@storage_location, params[:file], current_user).import_items + if result[:status] == :ok + if (result[:assigned_count] + result[:unassigned_count]).positive? + log_activity( + :storage_location_imported, + { + assigned_count: result[:assigned_count], + unassigned_count: result[:unassigned_count] + } + ) + end + + render json: result + else + render json: result, status: :unprocessable_entity + end + end + + def actions_toolbar + render json: { + actions: + Toolbars::StorageLocationsService.new( + current_user, + storage_location_ids: JSON.parse(params[:items]).pluck('id') + ).actions + } + end + + private + + def check_storage_locations_enabled + render_403 unless StorageLocation.storage_locations_enabled? + end + + def storage_location_params + params.permit(:id, :parent_id, :name, :container, :description, + metadata: [:display_type, { dimensions: [], parent_coordinations: [] }]) + end + + def move_params + params.permit(:id, :destination_storage_location_id) + end + + def load_storage_location + @storage_location = StorageLocation.find(storage_location_params[:id]) + @parent_location = @storage_location.parent + render_404 unless can_read_storage_location?(@storage_location) + end + + def check_read_permissions + render_403 unless can_read_storage_location?(@storage_location) + end + + def check_create_permissions + render_403 if @parent_location && !can_manage_storage_location?(@parent_location.team) + + if storage_location_params[:container] + render_403 unless can_create_storage_location_containers?(current_team) + else + render_403 unless can_create_storage_locations?(current_team) + end + end + + def check_manage_permissions + render_403 unless can_manage_storage_location?(@storage_location) + end + + def check_manage_repository_rows_permissions + render_403 unless can_manage_storage_location_repository_rows?(@storage_location) + end + + def set_breadcrumbs_items + @breadcrumbs_items = [] + + @breadcrumbs_items.push({ + label: t('breadcrumbs.inventories') + }) + + @breadcrumbs_items.push({ + label: t('breadcrumbs.locations'), + url: storage_locations_path + }) + + storage_locations = [] + if params[:parent_id] || @storage_location + location = StorageLocation.find_by(id: params[:parent_id]) || @storage_location + if location + storage_locations.unshift(breadcrumbs_item(location)) + while location.parent + location = location.parent + storage_locations.unshift(breadcrumbs_item(location)) + end + end + end + @breadcrumbs_items += storage_locations + end + + def breadcrumbs_item(location) + { + label: location.name, + url: storage_locations_path(parent_id: location.id) + } + end + + def storage_locations_recursive_builder(storage_locations) + storage_locations.order('LOWER(storage_locations.name) ASC').map do |storage_location| + { + storage_location: storage_location, + can_manage: (can_manage_storage_location?(storage_location) unless storage_location.parent_id), + children: storage_locations_recursive_builder( + storage_location.storage_locations.where(container: [false, params[:container] == 'true']) + ) + } + end + end + + def log_activity(type_of, message_items = {}) + Activities::CreateActivityService + .call(activity_type: "#{'container_' if @storage_location.container}#{type_of}", + owner: current_user, + team: @storage_location.team, + subject: @storage_location, + message_items: { + storage_location: @storage_location.id, + user: current_user.id + }.merge(message_items)) + end + + def storage_location_annotation_notification(old_text = nil) + url = if @storage_location.container + storage_location_path(@storage_location.id) + else + storage_locations_path(parent_id: @storage_location.id) + end + + smart_annotation_notification( + old_text: old_text, + new_text: @storage_location.description, + subject: @storage_location, + title: t('notifications.storage_location_annotation_title', + storage_location: @storage_location.name, + user: current_user.full_name), + message: t('notifications.storage_location_annotation_message_html', + storage_location: link_to(@storage_location.name, url)) + ) + end + + def log_unassign_activities + @storage_location_repository_rows.each do |storage_location_repository_row| + Activities::CreateActivityService + .call(activity_type: :storage_location_repository_row_deleted, + owner: current_user, + team: @storage_location.team, + subject: storage_location_repository_row.repository_row, + message_items: { + storage_location: storage_location_repository_row.storage_location_id, + repository_row: storage_location_repository_row.repository_row_id, + position: storage_location_repository_row.human_readable_position, + user: current_user.id + }) + end + end +end diff --git a/app/controllers/team_shared_objects_controller.rb b/app/controllers/team_shared_objects_controller.rb new file mode 100644 index 0000000000..3276b57210 --- /dev/null +++ b/app/controllers/team_shared_objects_controller.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +class TeamSharedObjectsController < ApplicationController + before_action :load_vars + before_action :check_sharing_permissions + + def update + ActiveRecord::Base.transaction do + @activities_to_log = [] + + global_permission_level = + if params[:select_all_teams] + params[:select_all_write_permission] ? :shared_write : :shared_read + else + :not_shared + end + + # Global share + if @model.globally_shareable? + @model.permission_level = global_permission_level + + if @model.permission_level_changed? + @model.save! + @model.team_shared_objects.each(&:destroy!) unless global_permission_level == :not_shared + case @model + when Repository + setup_repository_global_share_activity + end + end + end + + # Share to specific teams + params[:team_share_params].each do |t| + next unless t['private_shared_with'] + + team_shared_object = @model.team_shared_objects.find_or_initialize_by(team_id: t['id']) + + new_record = team_shared_object.new_record? + + team_shared_object.update!( + permission_level: t['private_shared_with_write'] ? :shared_write : :shared_read + ) + + setup_team_share_activity(team_shared_object, new_record) if team_shared_object.saved_changes? + end + + # Unshare + @model.team_shared_objects.where.not( + team_id: params[:team_share_params].filter { |t| t['private_shared_with'] }.pluck('id') + ).each do |team_shared_object| + team_shared_object.destroy! + setup_team_share_activity(team_shared_object, false) + end + + log_activities + end + end + + def shareable_teams + teams = (Team.order(:name).all - [@model.team]).filter { |t| can_read_team?(t) || @model.private_shared_with?(t) } + render json: teams, each_serializer: ShareableTeamSerializer, model: @model + end + + private + + def load_vars + case params[:object_type] + when 'Repository' + @model = Repository.viewable_by_user(current_user).find_by(id: params[:object_id]) + when 'StorageLocation' + @model = StorageLocation.viewable_by_user(current_user).find_by(id: params[:object_id]) + end + + render_404 unless @model + end + + def create_params + params.permit(:team_id, :object_type, :object_id, :target_team_id, :permission_level) + end + + def destroy_params + params.permit(:team_id, :id) + end + + def update_params + params.permit(permission_changes: {}, share_team_ids: [], write_permissions: []) + end + + def check_sharing_permissions + object_name = @model.is_a?(RepositoryBase) ? 'repository' : @model.model_name.param_key + render_403 unless public_send("can_share_#{object_name}?", @model) + render_403 if !@model.shareable_write? && update_params[:write_permissions].present? + end + + def share_all_params + { + shared_with_all: params[:select_all_teams].present?, + shared_permissions_level: params[:select_all_write_permission].present? ? 'shared_write' : 'shared_read' + } + end + + def setup_team_share_activity(team_shared_object, new_record) + type = + case @model + when Repository + if team_shared_object.destroyed? + :unshare_inventory + elsif new_record + :share_inventory + else + :update_share_inventory + end + when StorageLocation + if team_shared_object.destroyed? + "#{'container_' if @model.container?}storage_location_unshared" + elsif new_record + "#{'container_' if @model.container?}storage_location_shared" + else + "#{'container_' if @model.container?}storage_location_sharing_updated" + end + end + + @activities_to_log << { + type: type, + message_items: { + @model.model_name.param_key.to_sym => team_shared_object.shared_object.id, + team: team_shared_object.team.id, + permission_level: Extends::SHARED_INVENTORIES_PL_MAPPINGS[team_shared_object.permission_level.to_sym] + } + } + end + + def setup_repository_global_share_activity + message_items = { + repository: @model.id, + team: @model.team.id, + permission_level: Extends::SHARED_INVENTORIES_PL_MAPPINGS[@model.permission_level.to_sym] + } + + activity_params = + if @model.saved_changes['permission_level'][0] == 'not_shared' + { type: :share_inventory_with_all, message_items: message_items } + elsif @model.saved_changes['permission_level'][1] == 'not_shared' + { type: :unshare_inventory_with_all, message_items: message_items } + else + { type: :update_share_with_all_permission_level, message_items: message_items } + end + + @activities_to_log << activity_params + end + + def log_activities + @activities_to_log.each do |activity_params| + Activities::CreateActivityService + .call(activity_type: activity_params[:type], + owner: current_user, + team: @model.team, + subject: @model, + message_items: { + user: current_user.id + }.merge(activity_params[:message_items])) + end + end +end diff --git a/app/controllers/user_notifications_controller.rb b/app/controllers/user_notifications_controller.rb index ca05c3bbce..127d35289a 100644 --- a/app/controllers/user_notifications_controller.rb +++ b/app/controllers/user_notifications_controller.rb @@ -5,11 +5,18 @@ class UserNotificationsController < ApplicationController def index page = (params.dig(:page, :number) || 1).to_i - notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT) + notifications = load_notifications - render json: notifications, each_serializer: NotificationSerializer + case params[:tab] + when 'read' + notifications = notifications.where.not(read_at: nil) + when 'unread' + notifications = notifications.where(read_at: nil) + end + + notifications = notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT) - notifications.mark_as_read! + render json: notifications, each_serializer: NotificationSerializer end def unseen_counter @@ -18,6 +25,17 @@ def unseen_counter } end + def mark_all_read + load_notifications.mark_as_read! + render json: { success: true } + end + + def toggle_read + notification = current_user.notifications.find(params[:id]) + notification.update(read_at: (params[:mark_as_read] ? DateTime.now : nil)) + render json: notification, serializer: NotificationSerializer + end + private def load_notifications @@ -25,5 +43,4 @@ def load_notifications .in_app .order(created_at: :desc) end - end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 6fefaf3c79..ad62346c3f 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -8,7 +8,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_before_action :verify_authenticity_token before_action :sign_up_with_provider_enabled?, only: :linkedin - before_action :check_sso_status, only: %i(customazureactivedirectory okta openid_connect) + before_action :check_sso_status, only: %i(customazureactivedirectory okta openid_connect saml) # You should configure your model like this: # devise :omniauthable, omniauth_providers: [:twitter] @@ -38,7 +38,7 @@ def customazureactivedirectory if email.blank? # No email in the token so can not link or create user - error_message = I18n.t('devise.azure.errors.no_email') + missing_attribute = 'Email' return redirect_to after_omniauth_failure_path_for(resource_name) end @@ -47,7 +47,11 @@ def customazureactivedirectory if user.blank? # Create new user and identity user = create_user_from_auth(email, auth) - sign_in_and_redirect(user, event: :authentication) + if user.errors.present? + redirect_to after_omniauth_failure_path_for(resource_name) + else + sign_in_and_redirect(user, event: :authentication) + end elsif provider_conf['auto_link_on_sign_in'] # Link to existing local account user.user_identities.create!(provider: auth.provider, uid: auth.uid) @@ -65,7 +69,10 @@ def customazureactivedirectory error_message ||= I18n.t('devise.azure.errors.generic') redirect_to after_omniauth_failure_path_for(resource_name) ensure - if error_message + if user&.errors.present? || missing_attribute.present? + missing_attribute ||= user.errors.first.attribute.capitalize + set_flash_message(:alert, :missing_attribute, attribute: missing_attribute) + elsif error_message set_flash_message(:alert, :failure, kind: I18n.t('devise.azure.provider_name'), reason: error_message) else set_flash_message(:notice, :success, kind: I18n.t('devise.azure.provider_name')) @@ -107,10 +114,7 @@ def linkedin email: auth_hash['info']['email'], password: generate_user_password ) - if auth_hash['info']['picture_url'] - avatar = URI.open(auth_hash['info']['picture_url']) - @user.avatar.attach(io: avatar, filename: 'linkedin_avatar.jpg') - end + @user.avatar.attach(io: URI(auth_hash['info']['picture_url']).open, filename: 'linkedin_avatar.jpg') if auth_hash['info']['picture_url'] user_identity = UserIdentity.new(user: @user, provider: auth_hash['provider'], uid: auth_hash['uid']) @@ -137,13 +141,18 @@ def okta user = User.find_by(email: auth.info.email.downcase) if user.blank? - user = create_user_from_auth(email, auth) + user = create_user_from_auth(auth.info.email.downcase, auth) + if user.errors.present? + redirect_to after_omniauth_failure_path_for(resource_name) + else + sign_in_and_redirect(user, event: :authentication) + end else # Link to existing local account user.user_identities.create!(provider: auth.provider, uid: auth.uid) user.update!(confirmed_at: user.created_at) if user.confirmed_at.blank? + sign_in_and_redirect(user, event: :authentication) end - sign_in_and_redirect(user, event: :authentication) rescue StandardError => e Rails.logger.error e.message Rails.logger.error e.backtrace.join("\n") @@ -151,7 +160,9 @@ def okta error_message ||= I18n.t('devise.okta.errors.generic') redirect_to after_omniauth_failure_path_for(resource_name) ensure - if error_message + if user&.errors.present? + set_flash_message(:alert, :missing_attribute, attribute: user.errors.first.attribute.capitalize) + elsif error_message set_flash_message(:alert, :failure, kind: I18n.t('devise.okta.provider_name'), reason: error_message) else set_flash_message(:notice, :success, kind: I18n.t('devise.okta.provider_name')) @@ -175,7 +186,7 @@ def openid_connect if email.blank? # No email in the token so can not link or create user - error_message = I18n.t('devise.openid_connect.errors.no_email') + missing_attribute = 'Email' return redirect_to after_omniauth_failure_path_for(resource_name) end @@ -184,7 +195,11 @@ def openid_connect if user.blank? # Create new user and identity user = create_user_from_auth(email, auth) - sign_in_and_redirect(user) + if user.errors.present? + redirect_to after_omniauth_failure_path_for(resource_name) + else + sign_in_and_redirect(user) + end elsif provider_conf['auto_link_on_sign_in'] # Link to existing local account user.user_identities.create!(provider: auth.provider, uid: auth.uid) @@ -202,7 +217,10 @@ def openid_connect error_message ||= I18n.t('devise.openid_connect.errors.generic') redirect_to after_omniauth_failure_path_for(resource_name) ensure - if error_message + if user&.errors.present? || missing_attribute.present? + missing_attribute ||= user.errors.first.attribute.capitalize + set_flash_message(:alert, :missing_attribute, attribute: missing_attribute) + elsif error_message set_flash_message(:alert, :failure, kind: I18n.t('devise.openid_connect.provider_name'), reason: error_message) else set_flash_message(:notice, :success, kind: I18n.t('devise.openid_connect.provider_name')) @@ -226,7 +244,7 @@ def saml if email.blank? # No email in the token so can not link or create user - error_message = I18n.t('devise.saml.errors.no_email') + missing_attribute = 'Email' return redirect_to after_omniauth_failure_path_for(resource_name) end @@ -234,7 +252,11 @@ def saml if user.blank? user = create_user_from_auth(email, auth) - sign_in_and_redirect(user) + if user.errors.present? + redirect_to after_omniauth_failure_path_for(resource_name) + else + sign_in_and_redirect(user) + end elsif provider_conf['auto_link_on_sign_in'] # Link to existing local account user.user_identities.create!(provider: auth.provider, uid: auth.uid) @@ -252,7 +274,10 @@ def saml error_message ||= I18n.t('devise.saml.errors.generic') redirect_to after_omniauth_failure_path_for(resource_name) ensure - if error_message + if user&.errors.present? || missing_attribute.present? + missing_attribute ||= user.errors.first.attribute.to_s.capitalize + set_flash_message(:alert, :missing_attribute, attribute: missing_attribute) + elsif error_message set_flash_message(:alert, :failure, kind: I18n.t('devise.saml.provider_name'), reason: error_message) else set_flash_message(:notice, :success, kind: I18n.t('devise.saml.provider_name')) @@ -306,20 +331,10 @@ def create_user_from_auth(email, auth) user.save! user.user_identities.create!(provider: auth.provider, uid: auth.uid) user.update!(confirmed_at: user.created_at) - end - user - end - - def create_user_from_auth(email, auth) - full_name = "#{auth.info.first_name} #{auth.info.last_name}" - user = User.new(full_name: full_name, - initials: generate_initials(full_name), - email: email, - password: generate_user_password) - User.transaction do - user.save! - user.user_identities.create!(provider: auth.provider, uid: auth.uid) - user.update!(confirmed_at: user.created_at) + rescue StandardError => e + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") + raise ActiveRecord::Rollback end user end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index fb4d4d496c..17c936933c 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -208,11 +208,12 @@ def two_factor_qr_code end def regenerate_api_key - current_user.regenerate_api_key! + token = current_user.regenerate_api_key! redirect_to(edit_user_registration_path(anchor: 'api-key'), flash: { - success: t('users.registrations.edit.api_key.generated') + success: t('users.registrations.edit.api_key.generated'), + token: token }) end diff --git a/app/controllers/users/settings/user_settings_controller.rb b/app/controllers/users/settings/user_settings_controller.rb index 2b106bc178..04620a4964 100644 --- a/app/controllers/users/settings/user_settings_controller.rb +++ b/app/controllers/users/settings/user_settings_controller.rb @@ -17,8 +17,8 @@ def update next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s) case key.to_s - when 'task_step_states' - update_task_step_states(data) + when 'task_step_states', 'result_states' + update_object_states(data, key.to_s) else current_user.settings[key] = data end @@ -34,18 +34,18 @@ def update private - def update_task_step_states(task_step_states_data) - current_states = current_user.settings.fetch('task_step_states', {}) + def update_object_states(object_states_data, object_state_key) + current_states = current_user.settings.fetch(object_state_key, {}) - task_step_states_data.each do |step_id, collapsed| + object_states_data.each do |object_id, collapsed| if collapsed - current_states[step_id] = true + current_states[object_id] = true else - current_states.delete(step_id) + current_states.delete(object_id) end end - current_user.settings['task_step_states'] = current_states + current_user.settings[object_state_key] = current_states end end end diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb index 6dbe4051eb..a34b01d278 100644 --- a/app/controllers/wopi_controller.rb +++ b/app/controllers/wopi_controller.rb @@ -200,7 +200,6 @@ def put_file if @asset.lock == lock logger.warn 'WOPI: replacing file' - @team.release_space(@asset.estimated_size) @asset.last_modified_by = @user @asset.update_contents(request.body) @asset.save @@ -220,7 +219,6 @@ def put_file elsif !@asset.file_size.nil? && @asset.file_size.zero? logger.warn 'WOPI: initializing empty file' - @team.release_space(@asset.estimated_size) @asset.update_contents(request.body) @asset.last_modified_by = @user @asset.save diff --git a/app/helpers/active_storage_helper.rb b/app/helpers/active_storage_helper.rb new file mode 100644 index 0000000000..58ee7f4d6f --- /dev/null +++ b/app/helpers/active_storage_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ActiveStorageHelper + def image_preview_format(blob) + if ['image/jpeg', 'image/jpg'].include?(blob&.content_type) + :jpeg + else + :png + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 513a90ee76..5802404e7c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -141,10 +141,9 @@ def smart_annotation_filter_resources(text, team, preview_repository: false) # Check if text have smart annotations of users # and outputs a popover with user information def smart_annotation_filter_users(text, team, base64_encoded_imgs: false) - sa_user = /\[\@(.*?)~([0-9a-zA-Z]+)\]/ - text.gsub(sa_user) do |el| - match = el.match(sa_user) - user = User.find_by_id(match[2].base62_decode) + text.gsub(SmartAnnotations::TagToHtml::USER_REGEX) do |el| + match = el.match(SmartAnnotations::TagToHtml::USER_REGEX) + user = User.find_by(id: match[2].base62_decode) next unless user popover_for_user_name(user, team, false, false, base64_encoded_imgs) diff --git a/app/helpers/global_activities_helper.rb b/app/helpers/global_activities_helper.rb index 608d3d6de6..4d09aaee14 100644 --- a/app/helpers/global_activities_helper.rb +++ b/app/helpers/global_activities_helper.rb @@ -3,6 +3,7 @@ module GlobalActivitiesHelper include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::UrlHelper + include Canaid::Helpers::PermissionsHelper include InputSanitizeHelper def generate_activity_content(activity, no_links: false, no_custom_links: false) @@ -60,6 +61,9 @@ def generate_link(message_item, activity) when Repository path = repository_path(obj, team: obj.team.id) when RepositoryRow + # Handle private repository rows + return I18n.t('storage_locations.show.hidden') unless can_read_repository?(obj.repository) + return current_value unless obj.repository path = repository_path(obj.repository, team: obj.repository.team.id) @@ -87,7 +91,7 @@ def generate_link(message_item, activity) end when Protocol if obj.my_module.nil? - path = protocols_path(team: obj.team.id) + path = protocol_path(obj) elsif obj.my_module.navigable? path = protocols_my_module_path(obj.my_module) else @@ -108,6 +112,12 @@ def generate_link(message_item, activity) else project_folder_path(obj, team: obj.team.id) end + when StorageLocation + path = if obj.new_record? + storage_locations_path(team: activity.team.id) + else + storage_location_path(obj, team: activity.team.id) + end else return current_value end @@ -121,8 +131,12 @@ def generate_name(message_item) message_item['type'].constantize.new end + return I18n.t('storage_locations.show.hidden') if obj.is_a?(RepositoryRow) && !can_read_repository?(obj.repository) + return I18n.t('projects.index.breadcrumbs_root') if obj.is_a?(ProjectFolder) && obj.new_record? + return I18n.t('storage_locations.index.breadcrumbs_root') if obj.is_a?(StorageLocation) && obj.new_record? + return message_item['value'] unless obj value = obj.public_send(message_item['value_for'] || 'name') diff --git a/app/helpers/input_sanitize_helper.rb b/app/helpers/input_sanitize_helper.rb index e30a236661..73c9eaca28 100644 --- a/app/helpers/input_sanitize_helper.rb +++ b/app/helpers/input_sanitize_helper.rb @@ -46,9 +46,8 @@ def custom_auto_link(text, options = {}) sanitizer_config = Constants::INPUT_SANITIZE_CONFIG.deep_dup text = sanitize_input(text, tags, sanitizer_config: sanitizer_config) - if text =~ SmartAnnotations::TagToHtml::USER_REGEX || text =~ SmartAnnotations::TagToHtml::REGEX - text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository) - end + text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository) if text.match?(SmartAnnotations::TagToHtml::ALL_REGEX) + auto_link( text, html: { target: '_blank' }, diff --git a/app/helpers/left_menu_bar_helper.rb b/app/helpers/left_menu_bar_helper.rb index f8a77be758..b6a2d22d27 100644 --- a/app/helpers/left_menu_bar_helper.rb +++ b/app/helpers/left_menu_bar_helper.rb @@ -19,8 +19,16 @@ def left_menu_elements url: repositories_path, name: t('left_menu_bar.repositories'), icon: 'sn-icon-inventory', - active: repositories_are_selected?, - submenu: [] + active: repositories_are_selected? || storage_locations_are_selected?, + submenu: [{ + url: repositories_path, + name: t('left_menu_bar.items'), + active: repositories_are_selected? + }, { + url: storage_locations_path, + name: t('left_menu_bar.locations'), + active: storage_locations_are_selected? + }] }, { url: "#", name: t('left_menu_bar.templates'), @@ -63,6 +71,10 @@ def repositories_are_selected? controller_name == 'repositories' end + def storage_locations_are_selected? + controller_name == 'storage_locations' + end + def protocols_are_selected? controller_name == 'protocols' end diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb index 07e3eec422..2abd4fdc2c 100644 --- a/app/helpers/reports_helper.rb +++ b/app/helpers/reports_helper.rb @@ -107,13 +107,16 @@ def report_experiment_descriptions(report) end end - def assigned_to_report_repository_items(report, repository_name) - repository = Repository.accessible_by_teams(report.team).where(name: repository_name).take - return RepositoryRow.none if repository.blank? - - my_modules = MyModule.joins(:experiment) - .where(experiment: { project: report.project }) - .where(id: report.report_elements.my_module.select(:my_module_id)) - repository.repository_rows.joins(:my_modules).where(my_modules: my_modules) + def permit_report_settings_structure(settings_definition) + settings_definition.each_with_object([]) do |(key, value), permitted| + permitted << case value + when Hash + { key => permit_report_settings_structure(value) } + when Array + { key => [] } + else + key + end + end end end diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb index b865caca7a..a1f4364d22 100644 --- a/app/helpers/repository_datatable_helper.rb +++ b/app/helpers/repository_datatable_helper.rb @@ -5,42 +5,37 @@ module RepositoryDatatableHelper include Rails.application.routes.url_helpers def prepare_row_columns(repository_rows, repository, columns_mappings, team, options = {}) + # repository_rows collection is already preloaded in controllers, do not modify scopes or query params + # otherwise it will result in duplicated SQL queries has_stock_management = repository.has_stock_management? + stock_management_column_exists = repository.repository_columns.stock_type.exists? + repository_row_connections_enabled = Repository.repository_row_connections_enabled? reminders_enabled = Repository.reminders_enabled? - repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows stock_managable = has_stock_management && !options[:disable_stock_management] && - can_manage_repository_stock?(repository) && - !repository.is_a?(SoftLockedRepository) + can_manage_repository_stock?(repository) stock_consumption_permitted = has_stock_management && options[:include_stock_consumption] && options[:my_module] && stock_consumption_permitted?(repository, options[:my_module]) + default_columns_method_name = "#{repository.class.name.underscore}_default_columns" repository_rows.map do |record| - row = public_send("#{repository.class.name.underscore}_default_columns", record) - row.merge!( - DT_RowId: record.id, - DT_RowAttr: { 'data-state': row_style(record), 'data-e2e': "e2e-TR-invInventory-bodyRow-#{record.id}" }, - recordInfoUrl: Rails.application.routes.url_helpers.repository_repository_row_path(repository, record), - rowRemindersUrl: - Rails.application.routes.url_helpers - .active_reminder_repository_cells_repository_repository_row_url( - repository, - record - ), - relationshipsUrl: - Rails.application.routes.url_helpers - .relationships_repository_repository_row_url(record.repository_id, record.id), - relationships_enabled: repository_row_connections_enabled, - code: record.code - ) - - if reminders_enabled - row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders - end - - unless options[:view_mode] || repository.is_a?(SoftLockedRepository) + row = public_send(default_columns_method_name, record) + row['code'] = record.code + row['DT_RowId'] = record.id + row['DT_RowAttr'] = { 'data-state': row_style(record), 'data-e2e': "e2e-TR-invInventory-bodyRow-#{record.id}" } + row['recordInfoUrl'] = Rails.application.routes.url_helpers.repository_repository_row_path(repository.id, record.id) + row['rowRemindersUrl'] = Rails.application.routes.url_helpers + .active_reminder_repository_cells_repository_repository_row_url(repository.id, record.id) + row['relationshipsUrl'] = Rails.application.routes.url_helpers + .relationships_repository_repository_row_url(record.repository_id, record.id) + row['relationships_enabled'] = repository_row_connections_enabled + row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled + + unless options[:view_mode] row['recordUpdateUrl'] = Rails.application.routes.url_helpers.repository_repository_row_path(repository, record) - row['recordEditable'] = record.editable? + + # if the editable? property will be checked in a separate request, we can default it to true + row['recordEditable'] = options[:omit_editable] ? true : record.editable? end row['0'] = record[:row_assigned] if options[:my_module] @@ -49,13 +44,7 @@ def prepare_row_columns(repository_rows, repository, columns_mappings, team, opt custom_cells = record.repository_cells.filter { |cell| cell.value_type != 'RepositoryStockValue' } custom_cells.each do |cell| - row[columns_mappings[cell.repository_column.id]] = - serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled) - end - - if repository.repository_columns.stock_type.exists? - stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' } - row['stock'] = serialize_repository_cell_value(record.repository_stock_cell, team, repository) if stock_cell.present? + row[columns_mappings[cell.repository_column_id]] = serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled) end if has_stock_management @@ -98,6 +87,9 @@ def prepare_row_columns(repository_rows, repository, columns_mappings, team, opt } } end + elsif stock_management_column_exists + stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' } + row['stock'] = serialize_repository_cell_value(record.repository_stock_cell, team, repository) if stock_cell.present? end row @@ -105,9 +97,10 @@ def prepare_row_columns(repository_rows, repository, columns_mappings, team, opt end def prepare_simple_view_row_columns(repository_rows, repository, my_module, options = {}) + # repository_rows collection is already preloaded in controllers, do not modify scopes or query params + # otherwise it will result in duplicated SQL queries has_stock_management = repository.has_stock_management? reminders_enabled = !options[:disable_reminders] && Repository.reminders_enabled? - repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows # Always disabled in a simple view stock_managable = false stock_consumption_permitted = has_stock_management && stock_consumption_permitted?(repository, my_module) @@ -126,9 +119,7 @@ def prepare_simple_view_row_columns(repository_rows, repository, my_module, opti ) } - if reminders_enabled - row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders - end + row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled if has_stock_management stock_present = record.repository_stock_cell.present? @@ -193,15 +184,14 @@ def prepare_snapshot_row_columns(repository_rows, columns_mappings, team, reposi '1': record.code, '2': escape_input(record.name), '3': I18n.l(record.created_at, format: :full), - '4': escape_input(record.created_by.full_name), + '4': escape_input(record.created_by_full_name), 'recordInfoUrl': Rails.application.routes.url_helpers .repository_repository_row_path(repository_snapshot, record) } # Add custom columns record.repository_cells.each do |cell| - row[columns_mappings[cell.repository_column.id]] = - serialize_repository_cell_value(cell, team, repository_snapshot) + row[columns_mappings[cell.repository_column_id]] = serialize_repository_cell_value(cell, team, repository_snapshot) end if has_stock_management @@ -239,11 +229,11 @@ def repository_default_columns(record) '3': escape_input(record.name), '4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}", '5': I18n.l(record.created_at, format: :full), - '6': escape_input(record.created_by.full_name), + '6': escape_input(record.created_by_full_name), '7': (record.updated_at ? I18n.l(record.updated_at, format: :full) : ''), - '8': escape_input(record.last_modified_by.full_name), + '8': escape_input(record.last_modified_by_full_name), '9': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''), - '10': escape_input(record.archived_by&.full_name) + '10': escape_input(record.archived_by_full_name) } end @@ -254,9 +244,9 @@ def soft_locked_repository_default_columns(record) '3': escape_input(record.name), '4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}", '5': I18n.l(record.created_at, format: :full), - '6': escape_input(record.created_by.full_name), + '6': escape_input(record.created_by_full_name), '7': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''), - '8': escape_input(record.archived_by&.full_name) + '8': escape_input(record.archived_by_full_name) } end @@ -266,15 +256,35 @@ def linked_repository_default_columns(record) '2': record.code, '3': escape_input(record.name), '4': I18n.l(record.created_at, format: :full), - '5': escape_input(record.created_by.full_name), + '5': escape_input(record.created_by_full_name), '6': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''), - '7': escape_input(record.archived_by&.full_name), + '7': escape_input(record.archived_by_full_name), '8': escape_input(record.external_id) } end def serialize_repository_cell_value(cell, team, repository, options = {}) - serializer_class = "RepositoryDatatable::#{cell.repository_column.data_type}Serializer".constantize + # case/when is used because it is much faster then .constantize + serializer_class = + case cell.repository_column.data_type + when 'RepositoryTextValue' then RepositoryDatatable::RepositoryTextValueSerializer + when 'RepositoryNumberValue' then RepositoryDatatable::RepositoryNumberValueSerializer + when 'RepositoryListValue' then RepositoryDatatable::RepositoryListValueSerializer + when 'RepositoryChecklistValue' then RepositoryDatatable::RepositoryChecklistValueSerializer + when 'RepositoryStatusValue' then RepositoryDatatable::RepositoryStatusValueSerializer + when 'RepositoryTimeValue' then RepositoryDatatable::RepositoryTimeValueSerializer + when 'RepositoryDateValue' then RepositoryDatatable::RepositoryDateValueSerializer + when 'RepositoryDateTimeValue' then RepositoryDatatable::RepositoryDateTimeValueSerializer + when 'RepositoryDateRangeValue' then RepositoryDatatable::RepositoryDateRangeValueSerializer + when 'RepositoryTimeRangeValue' then RepositoryDatatable::RepositoryTimeRangeValueSerializer + when 'RepositoryDateTimeRangeValue' then RepositoryDatatable::RepositoryDateTimeRangeValueSerializer + when 'RepositoryAssetValue' then RepositoryDatatable::RepositoryAssetValueSerializer + when 'RepositoryStockValue' then RepositoryDatatable::RepositoryStockValueSerializer + when 'RepositoryStockConsumptionValue' then RepositoryDatatable::RepositoryStockConsumptionValueSerializer + else + Extends::REPOSITORY_EXTRA_VALUE_SERIALIZERS[cell.value_type] + end + serializer_class.new( cell.value, scope: { @@ -293,35 +303,6 @@ def row_style(row) '' end - 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 - - 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 - .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) return false unless repository.is_a?(Repository) && current_user @@ -338,8 +319,4 @@ def stock_consumption_managable?(record, repository, my_module) def display_stock_warnings?(repository) !repository.is_a?(RepositorySnapshot) end - - def repository_row_connections_enabled - Repository.repository_row_connections_enabled? - end end diff --git a/app/helpers/storage_locations_helper.rb b/app/helpers/storage_locations_helper.rb new file mode 100644 index 0000000000..8754ad523f --- /dev/null +++ b/app/helpers/storage_locations_helper.rb @@ -0,0 +1,9 @@ +module StorageLocationsHelper + def storage_locations_placeholder + return if StorageLocation.storage_locations_enabled? + + "
+ #{I18n.t('storage_locations.storage_locations_disabled')} +
" + end +end diff --git a/app/javascript/packs/tiny_mce.js b/app/javascript/packs/tiny_mce.js index 723d3ab0a7..cc041eb4c8 100644 --- a/app/javascript/packs/tiny_mce.js +++ b/app/javascript/packs/tiny_mce.js @@ -489,11 +489,10 @@ window.TinyMCE = (() => { }, wrapTables: (container) => { container.find('table').toArray().forEach((table) => { - if ($(table).parent().hasClass('table-wrapper')) return; - - $(table).css('float', 'none').wrapAll(` -
- `); + if ($(table).parents('table').length === 0) { + $(table).css('float', 'none') + .wrapAll('
'); + } }); } }; diff --git a/app/javascript/packs/vue/design_system/breadcrumbs.js b/app/javascript/packs/vue/design_system/breadcrumbs.js new file mode 100644 index 0000000000..207ba469a2 --- /dev/null +++ b/app/javascript/packs/vue/design_system/breadcrumbs.js @@ -0,0 +1,22 @@ +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import Breadcrumbs from '../../../vue/shared/breadcrumbs.vue'; +import { mountWithTurbolinks } from '../helpers/turbolinks.js'; + +const app = createApp({ + computed: { + breadcrumbs() { + return [ + { name: 'Home', url: '/' }, + { name: 'Very very very long name ', url: '' }, + { name: 'Data', url: '' }, + { name: 'Very very very very very very very very very very long name ', url: '' }, + { name: 'Very very very very very very very long name ', url: '' }, + { name: 'Very very very very very long name ', url: '' }, + { name: 'Very very very very long name ', url: '' } + ]; + } + } +}); +app.component('Breadcrumbs', Breadcrumbs); +app.config.globalProperties.i18n = window.I18n; +mountWithTurbolinks(app, '#breadcrumbs'); diff --git a/app/javascript/packs/vue/legacy/tags_modal.js b/app/javascript/packs/vue/legacy/tags_modal.js index 176b2305dd..7fb0bfc981 100644 --- a/app/javascript/packs/vue/legacy/tags_modal.js +++ b/app/javascript/packs/vue/legacy/tags_modal.js @@ -10,6 +10,7 @@ const app = createApp({ myModuleParams: null, myModuleUrl: null, tagsModalOpen: false, + tagDeleted: false }; }, mounted() { @@ -35,6 +36,10 @@ const app = createApp({ this.myModuleParams = null; this.myModuleUrl = null; this.tagsModalOpen = false; + + if ($('#canvas-container').length && this.tagDeleted) { + window.location.reload(); + } }, syncTags(tags) { // My module page diff --git a/app/javascript/packs/vue/storage_locations_container.js b/app/javascript/packs/vue/storage_locations_container.js new file mode 100644 index 0000000000..e7f2e62592 --- /dev/null +++ b/app/javascript/packs/vue/storage_locations_container.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import PerfectScrollbar from 'vue3-perfect-scrollbar'; +import StorageLocationsContainer from '../../vue/storage_locations/container.vue'; +import { mountWithTurbolinks } from './helpers/turbolinks.js'; + +const app = createApp(); +app.component('StorageLocationsContainer', StorageLocationsContainer); +app.config.globalProperties.i18n = window.I18n; +app.use(PerfectScrollbar); +window.StorageLocationsContainer = mountWithTurbolinks(app, '#StorageLocationsContainer'); diff --git a/app/javascript/packs/vue/storage_locations_table.js b/app/javascript/packs/vue/storage_locations_table.js new file mode 100644 index 0000000000..e83db78249 --- /dev/null +++ b/app/javascript/packs/vue/storage_locations_table.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import PerfectScrollbar from 'vue3-perfect-scrollbar'; +import StorageLocations from '../../vue/storage_locations/table.vue'; +import { mountWithTurbolinks } from './helpers/turbolinks.js'; + +const app = createApp(); +app.component('StorageLocations', StorageLocations); +app.config.globalProperties.i18n = window.I18n; +app.use(PerfectScrollbar); +mountWithTurbolinks(app, '#storageLocationsTable'); diff --git a/app/javascript/vue/label_template/renderers/default.vue b/app/javascript/vue/label_template/renderers/default.vue new file mode 100644 index 0000000000..0081fe32a0 --- /dev/null +++ b/app/javascript/vue/label_template/renderers/default.vue @@ -0,0 +1,14 @@ + + diff --git a/app/javascript/vue/label_template/renderers/format.vue b/app/javascript/vue/label_template/renderers/format.vue new file mode 100644 index 0000000000..de61fb3052 --- /dev/null +++ b/app/javascript/vue/label_template/renderers/format.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/javascript/vue/label_template/renderers/name.vue b/app/javascript/vue/label_template/renderers/name.vue new file mode 100644 index 0000000000..ac969e84f0 --- /dev/null +++ b/app/javascript/vue/label_template/renderers/name.vue @@ -0,0 +1,16 @@ + + + diff --git a/app/javascript/vue/label_template/table.vue b/app/javascript/vue/label_template/table.vue index e39071a457..778b77c46d 100644 --- a/app/javascript/vue/label_template/table.vue +++ b/app/javascript/vue/label_template/table.vue @@ -30,28 +30,34 @@ import axios from '../../packs/custom_axios.js'; import DataTable from '../shared/datatable/table.vue'; import DeleteModal from '../shared/confirmation_modal.vue'; +import NameRenderer from './renderers/name.vue'; +import DefaultRenderer from './renderers/default.vue'; +import FormatRenderer from './renderers/format.vue'; export default { name: 'LabelTemplatesTable', components: { DataTable, DeleteModal, + NameRenderer, + DefaultRenderer, + FormatRenderer }, props: { dataSource: { type: String, - required: true, + required: true }, actionsUrl: { type: String, - required: true, + required: true }, createUrl: { - type: String, + type: String }, syncFluicsUrl: { - type: String, - }, + type: String + } }, data() { return { @@ -60,40 +66,40 @@ export default { { field: 'default', headerName: this.i18n.t('label_templates.index.default_label'), - cellRenderer: this.defaultRenderer, - sortable: true, + cellRenderer: 'DefaultRenderer', + sortable: true }, { field: 'name', headerName: this.i18n.t('label_templates.index.thead_name'), - cellRenderer: this.labelNameRenderer, - sortable: true, + cellRenderer: 'NameRenderer', + sortable: true }, { field: 'format', headerName: this.i18n.t('label_templates.index.format'), sortable: true, - cellRenderer: ({ data: { format, icon_url: iconUrl } }) => `${iconUrl} ${format}` + cellRenderer: 'FormatRenderer' }, { field: 'description', headerName: this.i18n.t('label_templates.index.description'), - sortable: true, + sortable: true }, { field: 'modified_by', headerName: this.i18n.t('label_templates.index.updated_by'), - sortable: true, + sortable: true }, { field: 'updated_at', headerName: this.i18n.t('label_templates.index.updated_at'), - sortable: true, + sortable: true }, { field: 'created_by', headerName: this.i18n.t('label_templates.index.created_by'), - sortable: true, + sortable: true }, { field: 'created_at', headerName: this.i18n.t('label_templates.index.created_at'), - sortable: true, - }, - ], + sortable: true + } + ] }; }, computed: { @@ -106,7 +112,7 @@ export default { label: this.i18n.t('label_templates.index.toolbar.new'), type: 'emit', path: this.createUrl, - buttonStyle: 'btn btn-primary', + buttonStyle: 'btn btn-primary' }); } if (this.syncFluicsUrl) { @@ -121,21 +127,11 @@ export default { } return { left, - right: [], + right: [] }; - }, + } }, methods: { - labelNameRenderer(params) { - const editUrl = params.data.urls.show; - return ` - ${params.data.name} - `; - }, - defaultRenderer(params) { - const defaultSelected = params.data.default; - return defaultSelected ? '' : ''; - }, setDefault(action) { axios.post(action.path).then((response) => { this.reloadingTable = true; @@ -175,8 +171,8 @@ export default { HelperModule.flashAlertMsg(error.response.data.error, 'danger'); }); } - }, - }, + } + } }; diff --git a/app/javascript/vue/my_modules/list.vue b/app/javascript/vue/my_modules/list.vue index 8d6ff6acd4..4904aee57f 100644 --- a/app/javascript/vue/my_modules/list.vue +++ b/app/javascript/vue/my_modules/list.vue @@ -16,6 +16,7 @@ :hiddenDataMessage="i18n.t('experiments.empty_state.no_active_modules_archived_branch')" scrollMode="infinite" @tableReloaded="reloadingTable = false" + @reloadTable="reloadingTable = true" @create="newModalOpen = true" @edit="edit" @move="move" @@ -56,6 +57,9 @@ import axios from '../../packs/custom_axios.js'; import DataTable from '../shared/datatable/table.vue'; import ConfirmationModal from '../shared/confirmation_modal.vue'; +import NameRenderer from './renderers/name.vue'; +import ResultsRenderer from './renderers/results.vue'; +import StatusRenderer from './renderers/status.vue'; import DueDateRenderer from './renderers/due_date.vue'; import DesignatedUsers from './renderers/designated_users.vue'; import TagsModal from './modals/tags.vue'; @@ -77,7 +81,10 @@ export default { NewModal, EditModal, MoveModal, - AccessModal + AccessModal, + NameRenderer, + ResultsRenderer, + StatusRenderer }, props: { dataSource: { type: String, required: true }, @@ -115,7 +122,7 @@ export default { field: 'name', headerName: this.i18n.t('experiments.table.column.task_name_html'), sortable: true, - cellRenderer: this.nameRenderer + cellRenderer: NameRenderer }, { field: 'code', @@ -133,7 +140,7 @@ export default { field: 'results', headerName: this.i18n.t('experiments.table.column.results_html'), sortable: true, - cellRenderer: this.resultsRenderer + cellRenderer: ResultsRenderer }, { field: 'age', @@ -144,7 +151,7 @@ export default { field: 'status', headerName: this.i18n.t('experiments.table.column.status_html'), sortable: true, - cellRenderer: this.statusRenderer, + cellRenderer: StatusRenderer, minWidth: 120 } ]; @@ -321,53 +328,6 @@ export default { roles_path: this.userRolesUrl }; }, - checkProvisioning(params) { - if (params.data.provisioning_status === 'done') return; - - axios.get(params.data.urls.provisioning_status).then((response) => { - const provisioningStatus = response.data.provisioning_status; - if (provisioningStatus === 'done') { - this.reloadingTable = true; - } else { - setTimeout(() => { - this.checkProvisioning(params); - }, 5000); - } - }); - }, - // Renderers - nameRenderer(params) { - const { name, urls } = params.data; - const provisioningStatus = params.data.provisioning_status; - if (provisioningStatus === 'in_progress') { - setTimeout(() => { - this.checkProvisioning(params); - }, 5000); - return ` - -
- ${name} -
`; - } - - return `${name}`; - }, - statusRenderer(params) { - const { status } = params.data; - - return ` - ${status.name} - `; - }, - resultsRenderer(params) { - const { results, urls } = params.data; - - return `${results}`; - }, usersFilterRenderer(option) { return `
diff --git a/app/javascript/vue/my_modules/modals/tags.vue b/app/javascript/vue/my_modules/modals/tags.vue index 05515daf8f..5e69d9f7ef 100644 --- a/app/javascript/vue/my_modules/modals/tags.vue +++ b/app/javascript/vue/my_modules/modals/tags.vue @@ -147,7 +147,7 @@ import ConfirmationModal from '../../shared/confirmation_modal.vue'; export default { name: 'TagsModal', - emits: ['close', 'tagsLoaded'], + emits: ['close', 'tagsLoaded', 'tagDeleted'], props: { params: { required: true @@ -288,6 +288,8 @@ export default { color: tag.attributes.color }, my_module_id: this.params.id + }).then(() => { + this.$emit('tagsLoaded', this.allTags); }); }, createTag() { @@ -309,6 +311,7 @@ export default { } }).then(() => { this.loadAlltags(); + this.$emit('tagDeleted', tag); document.body.style.overflow = 'hidden'; }); } else { diff --git a/app/javascript/vue/my_modules/renderers/name.vue b/app/javascript/vue/my_modules/renderers/name.vue new file mode 100644 index 0000000000..22725492ee --- /dev/null +++ b/app/javascript/vue/my_modules/renderers/name.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/javascript/vue/my_modules/renderers/results.vue b/app/javascript/vue/my_modules/renderers/results.vue new file mode 100644 index 0000000000..bcb21bad53 --- /dev/null +++ b/app/javascript/vue/my_modules/renderers/results.vue @@ -0,0 +1,14 @@ + + + diff --git a/app/javascript/vue/my_modules/renderers/status.vue b/app/javascript/vue/my_modules/renderers/status.vue new file mode 100644 index 0000000000..33753fc8cc --- /dev/null +++ b/app/javascript/vue/my_modules/renderers/status.vue @@ -0,0 +1,20 @@ + + + diff --git a/app/javascript/vue/navigation/notifications/notification_item.vue b/app/javascript/vue/navigation/notifications/notification_item.vue index b187636f1f..15aea8d55e 100644 --- a/app/javascript/vue/navigation/notifications/notification_item.vue +++ b/app/javascript/vue/navigation/notifications/notification_item.vue @@ -1,41 +1,63 @@ diff --git a/app/javascript/vue/projects/renderers/users.vue b/app/javascript/vue/projects/renderers/users.vue index 4516936963..b07a5ce8f0 100644 --- a/app/javascript/vue/projects/renderers/users.vue +++ b/app/javascript/vue/projects/renderers/users.vue @@ -19,8 +19,8 @@ export default { name: 'UsersRenderer', props: { params: { - required: true, - }, + required: true + } }, computed: { users() { diff --git a/app/javascript/vue/protocol/container.vue b/app/javascript/vue/protocol/container.vue index d03b02909a..a85f124d5e 100644 --- a/app/javascript/vue/protocol/container.vue +++ b/app/javascript/vue/protocol/container.vue @@ -225,6 +225,7 @@ :title="i18n.t('protocols.reorder_steps.modal.title')" :items="steps" :includeNumbers="true" + dataE2e="protocol-templateSteps-reorder" @reorder="updateStepOrder" @close="closeStepReorderModal" /> diff --git a/app/javascript/vue/protocol/step.vue b/app/javascript/vue/protocol/step.vue index 88a7dd2b04..0546f3f042 100644 --- a/app/javascript/vue/protocol/step.vue +++ b/app/javascript/vue/protocol/step.vue @@ -162,7 +162,7 @@ diff --git a/app/javascript/vue/protocols/modals/new.vue b/app/javascript/vue/protocols/modals/new.vue index f97c78ae40..1ca7c71e78 100644 --- a/app/javascript/vue/protocols/modals/new.vue +++ b/app/javascript/vue/protocols/modals/new.vue @@ -31,7 +31,12 @@
- +
+ + + + ; diff --git a/app/javascript/vue/shared/content/attachments/attachment_actions.vue b/app/javascript/vue/shared/content/attachments/attachment_actions.vue index 71561f7c39..6147efa336 100644 --- a/app/javascript/vue/shared/content/attachments/attachment_actions.vue +++ b/app/javascript/vue/shared/content/attachments/attachment_actions.vue @@ -27,6 +27,7 @@ @attachment:changed="$emit('attachment:changed', $event)" @attachment:update="$emit('attachment:update', $event)" @menu-toggle="$emit('attachment:toggle_menu', $event)" + @attachment:versionRestored="$emit('attachment:versionRestored', $event)" :withBorder="withBorder" /> diff --git a/app/javascript/vue/shared/content/attachments/context_menu.vue b/app/javascript/vue/shared/content/attachments/context_menu.vue index bb17af66cf..49135586b2 100644 --- a/app/javascript/vue/shared/content/attachments/context_menu.vue +++ b/app/javascript/vue/shared/content/attachments/context_menu.vue @@ -34,6 +34,7 @@ @duplicate="duplicate" @viewMode="changeViewMode" @move="showMoveModal" + @fileVersionsModal="fileVersionsModal = true" @menu-toggle="$emit('menu-toggle', $event)" > @@ -55,6 +56,13 @@ :targets_url="attachment.attributes.urls.move_targets" @confirm="moveAttachment($event)" @cancel="closeMoveModal" /> + @@ -65,6 +73,7 @@ import deleteAttachmentModal from './delete_modal.vue'; import MoveAssetModal from '../modal/move.vue'; import MoveMixin from './mixins/move.js'; import MenuDropdown from '../../menu_dropdown.vue'; +import FileVersionsModal from '../../file_versions_modal.vue'; import axios from '../../../../packs/custom_axios.js'; export default { @@ -73,6 +82,7 @@ export default { RenameAttachmentModal, deleteAttachmentModal, MoveAssetModal, + FileVersionsModal, MenuDropdown }, mixins: [MoveMixin], @@ -91,7 +101,8 @@ export default { return { viewModeOptions: ['inline', 'thumbnail', 'list'], deleteModal: false, - renameModal: false + renameModal: false, + fileVersionsModal: false }; }, computed: { @@ -124,6 +135,12 @@ export default { data_e2e: 'e2e-BT-attachmentOptions-delete' }); } + if (this.attachment.attributes.urls.versions) { + menu.push({ + text: this.i18n.t('assets.context_menu.versions'), + emit: 'fileVersionsModal' + }); + } if (this.attachment.attributes.urls.toggle_view_mode) { this.viewModeOptions.forEach((viewMode, i) => { menu.push({ diff --git a/app/javascript/vue/shared/content/attachments/inline.vue b/app/javascript/vue/shared/content/attachments/inline.vue index 14788fc32c..d1fec5fb78 100644 --- a/app/javascript/vue/shared/content/attachments/inline.vue +++ b/app/javascript/vue/shared/content/attachments/inline.vue @@ -38,6 +38,7 @@ @attachment:delete="deleteAttachment" @attachment:moved="attachmentMoved" @attachment:uploaded="reloadAttachments" + @attachment:versionRestored="reloadAttachments" @attachment:changed="$emit('attachment:changed', $event)" @attachment:update="$emit('attachment:update', $event)" @attachment:toggle_menu="toggleMenuDropdown" diff --git a/app/javascript/vue/shared/content/attachments/list.vue b/app/javascript/vue/shared/content/attachments/list.vue index d8cdc5ad6f..2a2aeb03b2 100644 --- a/app/javascript/vue/shared/content/attachments/list.vue +++ b/app/javascript/vue/shared/content/attachments/list.vue @@ -37,6 +37,7 @@ @attachment:delete="deleteAttachment" @attachment:moved="attachmentMoved" @attachment:uploaded="reloadAttachments" + @attachment:versionRestored="reloadAttachments" @attachment:changed="$emit('attachment:changed', $event)" @attachment:update="$emit('attachment:update', $event)" @attachment:toggle_menu="toggleMenuDropdown" diff --git a/app/javascript/vue/shared/content/attachments/mixins/open_locally.js b/app/javascript/vue/shared/content/attachments/mixins/open_locally.js index 2a34f6e4a3..b383a762a9 100644 --- a/app/javascript/vue/shared/content/attachments/mixins/open_locally.js +++ b/app/javascript/vue/shared/content/attachments/mixins/open_locally.js @@ -1,7 +1,10 @@ +/* global GLOBAL_CONSTANTS */ + import axios from '../../../../../packs/custom_axios.js'; import { satisfies } from 'compare-versions'; import editLaunchingApplicationModal from '../../modal/edit_launching_application_modal.vue'; import NoPredefinedAppModal from '../../modal/no_predefined_app_modal.vue'; +import RestrictedExtensionModal from '../../modal/restricted_extension_modal.vue'; import UpdateVersionModal from '../../modal/update_version_modal.vue'; export default { @@ -10,15 +13,17 @@ export default { localAppName: null, scinoteEditRunning: null, showNoPredefinedAppModal: false, + showRestrictedExtensionModal: false, showUpdateVersionModal: false, editAppModal: false, - pollingInterval: null, + pollingInterval: null }; }, components: { editLaunchingApplicationModal, NoPredefinedAppModal, - UpdateVersionModal + UpdateVersionModal, + RestrictedExtensionModal }, computed: { attributes() { @@ -93,9 +98,16 @@ export default { } }, async openLocally() { + const restrictedExtension = GLOBAL_CONSTANTS.SCINOTE_EDIT_RESTRICTED_EXTENSIONS.includes( + this.attributes.file_extension.toUpperCase() + ); + if (this.isWrongVersion(window.scinoteEditVersion)) { this.showUpdateVersionModal = true; return; + } else if (restrictedExtension) { + this.showRestrictedExtensionModal = true; + return; } else if (this.localAppName === null) { this.showNoPredefinedAppModal = true; return; diff --git a/app/javascript/vue/shared/content/attachments/open_locally_menu.vue b/app/javascript/vue/shared/content/attachments/open_locally_menu.vue index c758cd8fc6..0db3edd45a 100644 --- a/app/javascript/vue/shared/content/attachments/open_locally_menu.vue +++ b/app/javascript/vue/shared/content/attachments/open_locally_menu.vue @@ -1,8 +1,8 @@ @@ -51,14 +63,19 @@ import OpenLocallyMixin from './mixins/open_locally.js'; import MenuDropdown from '../../menu_dropdown.vue'; import UpdateVersionModal from '../modal/update_version_modal.vue'; +import FileVersionsModal from '../../file_versions_modal.vue'; export default { name: 'OpenLocallyMenu', mixins: [OpenLocallyMixin], - components: { MenuDropdown, UpdateVersionModal }, + components: { MenuDropdown, UpdateVersionModal, FileVersionsModal }, props: { - attachment: { type: Object, required: true }, - disableLocalOpen: { type: Boolean, default: false } + attachment: { type: Object, required: true } + }, + data() { + return { + fileVersionsModal: false + }; }, created() { this.fetchLocalAppInfo(); @@ -88,7 +105,7 @@ export default { }); } - if (this.canOpenLocally && !this.disableLocalOpen) { + if (this.canOpenLocally) { const text = this.localAppName ? this.i18n.t('attachments.open_locally_in', { application: this.localAppName }) : this.i18n.t('attachments.open_locally'); @@ -112,6 +129,13 @@ export default { methods: { openImageEditor() { document.getElementById('editImageButton').click(); + }, + refreshPreview() { + const imageElement = document.querySelector('.file-preview-container .asset-image'); + + if (!imageElement) return; + + $('#filePreviewModal').modal('hide'); } } }; diff --git a/app/javascript/vue/shared/content/attachments/open_menu.vue b/app/javascript/vue/shared/content/attachments/open_menu.vue index d4b3dac000..2080e86b87 100644 --- a/app/javascript/vue/shared/content/attachments/open_menu.vue +++ b/app/javascript/vue/shared/content/attachments/open_menu.vue @@ -93,6 +93,10 @@ :fileName="attachment.attributes.file_name" @close="showNoPredefinedAppModal = false" /> + + + + + diff --git a/app/javascript/vue/shared/datatable/action_toolbar.vue b/app/javascript/vue/shared/datatable/action_toolbar.vue index 6d97bfb6cc..d7cd7a80ca 100644 --- a/app/javascript/vue/shared/datatable/action_toolbar.vue +++ b/app/javascript/vue/shared/datatable/action_toolbar.vue @@ -11,6 +11,8 @@ diff --git a/app/javascript/vue/shared/reorderable_items_modal.vue b/app/javascript/vue/shared/reorderable_items_modal.vue index d3218e9061..c4fa0e7628 100644 --- a/app/javascript/vue/shared/reorderable_items_modal.vue +++ b/app/javascript/vue/shared/reorderable_items_modal.vue @@ -1,10 +1,12 @@ @@ -70,7 +70,7 @@ {{ i18n.t('general.select_all') }} - +