Merge pull request #8816 from scinote-eln/develop

August 2025 Release
This commit is contained in:
Martin Artnik 2025-08-19 14:03:38 +02:00 committed by GitHub
commit 200dc89dcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
345 changed files with 5197 additions and 3798 deletions

View file

@ -38,7 +38,7 @@ gem 'rack-cors'
gem 'uglifier', '>= 1.3.0'
gem 'activerecord-import'
gem 'activerecord-import', '~> 2.2.0'
gem 'acts_as_list'
gem 'ajax-datatables-rails', '~> 0.3.1'
gem 'aspector' # Aspect-oriented programming for Rails
@ -70,7 +70,6 @@ gem 'rubyzip', '>= 2.3.0' # will load new rubyzip version
gem 'scenic', '~> 1.4'
gem 'sdoc', '~> 1.0', group: :doc
gem 'silencer' # Silence certain Rails logs
gem 'sneaky-save', git: 'https://github.com/einzige/sneaky-save'
gem 'turbolinks', '~> 5.2.0'
gem 'underscore-rails'
gem 'wicked_pdf'
@ -95,8 +94,12 @@ gem 'js-routes'
gem 'tailwindcss-rails', '~> 2.4'
gem 'base62' # Used for smart annotations
gem 'datadog'
gem 'newrelic_rpm'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-pg'
gem 'opentelemetry-instrumentation-rails'
gem 'opentelemetry-propagator-xray'
gem 'opentelemetry-sdk'
# Permission helper Gem
gem 'canaid', git: 'https://github.com/scinote-eln/canaid'

View file

@ -1,10 +1,3 @@
GIT
remote: https://github.com/einzige/sneaky-save
revision: ee71d0a00cd4ecdd575bd2a9aa8b8693915f4871
specs:
sneaky-save (0.1.3)
activerecord (>= 3.2.0)
GIT
remote: https://github.com/scinote-eln/canaid
revision: bba1b817d1c9b0c7e0440a83d0f62848aabc0a1b
@ -43,29 +36,29 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
actioncable (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
actionmailbox (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
mail (>= 2.8.0)
actionmailer (7.2.2.1)
actionpack (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activesupport (= 7.2.2.1)
actionmailer (7.2.2.2)
actionpack (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activesupport (= 7.2.2.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.2.1)
actionview (= 7.2.2.1)
activesupport (= 7.2.2.1)
actionpack (7.2.2.2)
actionview (= 7.2.2.2)
activesupport (= 7.2.2.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
@ -74,15 +67,15 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.2.1)
actionpack (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
actiontext (7.2.2.2)
actionpack (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.2.1)
activesupport (= 7.2.2.1)
actionview (7.2.2.2)
activesupport (= 7.2.2.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@ -92,16 +85,16 @@ GEM
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.2.2.1)
activesupport (= 7.2.2.1)
activejob (7.2.2.2)
activesupport (= 7.2.2.2)
globalid (>= 0.3.6)
activemodel (7.2.2.1)
activesupport (= 7.2.2.1)
activerecord (7.2.2.1)
activemodel (= 7.2.2.1)
activesupport (= 7.2.2.1)
activemodel (7.2.2.2)
activesupport (= 7.2.2.2)
activerecord (7.2.2.2)
activemodel (= 7.2.2.2)
activesupport (= 7.2.2.2)
timeout (>= 0.4.0)
activerecord-import (1.4.1)
activerecord-import (2.2.0)
activerecord (>= 4.2)
activerecord-session_store (2.1.0)
actionpack (>= 6.1)
@ -110,13 +103,13 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 4)
railties (>= 6.1)
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activesupport (= 7.2.2.1)
activestorage (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activesupport (= 7.2.2.2)
marcel (~> 1.0)
activesupport (7.2.2.1)
activesupport (7.2.2.2)
base64
benchmark (>= 0.3)
bigdecimal
@ -195,14 +188,14 @@ GEM
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
base62 (1.0.0)
base64 (0.2.0)
base64 (0.3.0)
bcrypt (3.1.18)
benchmark (0.4.0)
benchmark (0.4.1)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.2.0)
bigdecimal (3.2.2)
bindata (2.5.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
@ -292,13 +285,6 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
datadog (2.14.0)
datadog-ruby_core_source (~> 3.4)
libdatadog (~> 16.0.1.1.0)
libddwaf (~> 1.21.0.0.1)
logger
msgpack
datadog-ruby_core_source (3.4.0)
date (3.4.1)
debug_inspector (1.1.0)
deface (1.9.0)
@ -361,6 +347,14 @@ GEM
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (4.31.1-arm64-darwin)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86_64-linux-gnu)
bigdecimal
rake (>= 13)
googleapis-common-protos-types (1.20.0)
google-protobuf (>= 3.18, < 5.a)
graphviz (1.2.1)
process-pipeline
grover (1.2.3)
@ -430,12 +424,6 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
libdatadog (16.0.1.1.0)
libdatadog (16.0.1.1.0-x86_64-linux)
libddwaf (1.21.0.0.1-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.21.0.0.1-x86_64-linux)
ffi (~> 1.0)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
@ -536,6 +524,82 @@ GEM
validate_email
validate_url
webfinger (~> 2.0)
opentelemetry-api (1.5.0)
opentelemetry-common (0.22.0)
opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.30.0)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.2)
opentelemetry-semantic_conventions
opentelemetry-helpers-sql (0.1.1)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation (0.3.0)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.4.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-action_pack (0.12.3)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (~> 0.21)
opentelemetry-instrumentation-action_view (0.9.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_job (0.8.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_record (0.9.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_storage (0.1.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_support (0.8.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.22.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-pg (0.30.1)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (0.26.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rails (0.36.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.4.0)
opentelemetry-instrumentation-action_pack (~> 0.12.0)
opentelemetry-instrumentation-action_view (~> 0.9.0)
opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_record (~> 0.9.0)
opentelemetry-instrumentation-active_storage (~> 0.1.0)
opentelemetry-instrumentation-active_support (~> 0.8.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-propagator-xray (0.24.0)
opentelemetry-api (~> 1.0)
opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.8.0)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.11.0)
opentelemetry-api (~> 1.0)
orm_adapter (0.5.0)
ostruct (0.6.0)
overcommit (0.60.0)
@ -597,20 +661,20 @@ GEM
rackup (1.0.1)
rack (< 3)
webrick
rails (7.2.2.1)
actioncable (= 7.2.2.1)
actionmailbox (= 7.2.2.1)
actionmailer (= 7.2.2.1)
actionpack (= 7.2.2.1)
actiontext (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activemodel (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
rails (7.2.2.2)
actioncable (= 7.2.2.2)
actionmailbox (= 7.2.2.2)
actionmailer (= 7.2.2.2)
actionpack (= 7.2.2.2)
actiontext (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activemodel (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
bundler (>= 1.15.0)
railties (= 7.2.2.1)
railties (= 7.2.2.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -626,9 +690,9 @@ GEM
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
railties (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@ -698,7 +762,7 @@ GEM
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
ruby-progressbar (1.13.0)
ruby-saml (1.18.0)
ruby-saml (1.18.1)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.1.4)
@ -810,7 +874,7 @@ PLATFORMS
DEPENDENCIES
active_model_serializers (~> 0.10.15)
activerecord-import
activerecord-import (~> 2.2.0)
activerecord-session_store
acts_as_list
ajax-datatables-rails (~> 0.3.1)
@ -836,7 +900,6 @@ DEPENDENCIES
cssbundling-rails
cucumber-rails
database_cleaner
datadog
deface (~> 1.9)
delayed_job_active_record
devise (~> 4.9.4)
@ -876,6 +939,11 @@ DEPENDENCIES
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml
omniauth_openid_connect
opentelemetry-exporter-otlp
opentelemetry-instrumentation-pg
opentelemetry-instrumentation-rails
opentelemetry-propagator-xray
opentelemetry-sdk
overcommit
pg (~> 1.5)
pg_search
@ -907,7 +975,6 @@ DEPENDENCIES
shoulda-matchers
silencer
simplecov
sneaky-save!
sprockets-rails
tailwindcss-rails (~> 2.4)
timecop

View file

@ -1 +1 @@
1.43.0.2
1.44.0

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="23" height="23" rx="11.5" fill="#F9F9F9"/>
<rect x="0.5" y="0.5" width="23" height="23" rx="11.5" stroke="#EAECF0"/>
<path d="M4.50001 17.8331V16.6663C4.50001 16.2744 4.60001 15.9412 4.80001 15.6662C5.00001 15.3912 5.26689 15.1663 5.60001 14.9994C6.32501 14.6494 7.04189 14.3744 7.75814 14.1744C8.47439 13.9744 9.33314 13.8744 10.3331 13.8744C11.3331 13.8744 12.1913 13.9744 12.9081 14.1744C13.625 14.3744 14.3413 14.6494 15.0663 14.9994C15.3994 15.1663 15.6663 15.3912 15.8663 15.6662C16.0663 15.9412 16.1663 16.2744 16.1663 16.6663V17.8331H4.50001ZM10.3331 12C9.64126 12 9.05001 11.7581 8.56626 11.2669C8.08314 10.7837 7.83314 10.1919 7.83314 9.5C7.83314 8.80813 8.07501 8.23313 8.56626 7.74188C9.04939 7.25 9.64126 7 10.3331 7C11.025 7 11.6 7.25 12.0913 7.74188C12.5831 8.23375 12.8331 8.825 12.8331 9.5C12.8331 10.175 12.5831 10.7831 12.0913 11.2669C11.5994 11.75 11.0081 12 10.3331 12ZM16.3919 9.5C16.3919 10.1919 16.1419 10.7831 15.65 11.2669C15.1581 11.7506 14.5669 12 13.8919 12C13.85 12 13.8 12 13.75 11.9919C13.6919 11.9919 13.65 11.9837 13.625 11.9837C13.9 11.6337 14.1169 11.2506 14.275 10.8256C14.425 10.4006 14.5 9.95875 14.5 9.50063C14.5 9.0425 14.4169 8.62563 14.2581 8.20875C14.1 7.79188 13.8831 7.40063 13.625 7.04188C13.6669 7.025 13.7081 7.01687 13.75 7.00875C13.7919 7.00875 13.8419 7.00063 13.8919 7.00063C14.575 7.00063 15.1588 7.25062 15.65 7.7425C16.1413 8.23437 16.3919 8.82563 16.3919 9.50063V9.5ZM5.33314 17H15.3331V16.6669C15.3331 16.475 15.2831 16.3 15.1831 16.1587C15.0831 16.0087 14.9163 15.8669 14.6663 15.7338C14.0413 15.4006 13.3831 15.1506 12.6994 14.9756C12.0075 14.8006 11.2244 14.7175 10.3325 14.7175C9.44064 14.7175 8.65752 14.8006 7.96564 14.9756C7.27376 15.1506 6.62376 15.4006 5.99876 15.7338C5.74876 15.8756 5.57376 16.0169 5.48189 16.1587C5.38189 16.3006 5.34001 16.4756 5.34001 16.6669V17H5.33314ZM10.3331 11.1669C10.7913 11.1669 11.1831 11 11.5081 10.675C11.8331 10.35 12 9.95812 12 9.5C12 9.04188 11.8331 8.65 11.5081 8.325C11.1831 8 10.7913 7.83313 10.3331 7.83313C9.87501 7.83313 9.48314 8 9.15814 8.325C8.83314 8.65 8.66627 9.04188 8.66627 9.5C8.66627 9.95812 8.83314 10.35 9.15814 10.675C9.48314 11 9.87501 11.1669 10.3331 11.1669ZM17.25 16.3169V17.8337H19.5V16.3169C19.5 15.9587 19.4 15.6337 19.2081 15.325C19.0163 15.025 18.7413 14.775 18.3913 14.575C18.0331 14.3831 17.6663 14.2 17.2994 14.0419C16.9325 13.875 16.5494 13.75 16.1663 13.6669C16.5163 13.9919 16.7831 14.3837 16.9663 14.85C17.1494 15.3081 17.25 15.8 17.25 16.3169Z" fill="#1D2939"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,100 @@
<svg width="399" height="240" viewBox="0 0 399 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2686_13418)">
<path d="M180.722 32.686L199.497 31.6412L194.696 17.9363L176.738 16.3633L180.722 32.686Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M176.55 16.2012L194.802 17.8L199.698 31.7755L180.61 32.8377L176.55 16.2012ZM176.927 16.5256L180.834 32.5345L199.297 31.5071L194.59 18.0728L176.927 16.5256Z" fill="#B7BBCE"/>
<path d="M182.287 30.0739H196.894L193.763 19.6313L179.162 18.5864L182.287 30.0739Z" fill="#074C9C"/>
<path d="M233.47 32.541L213.385 52.6832C213.385 52.6832 212.625 47.3719 211.488 41.6543C210.213 35.1879 208.462 28.2049 206.815 27.7812C203.707 26.9801 196.749 25.8192 197.433 27.4097C198.117 29.0001 201.312 29.6909 201.312 29.6909C201.312 29.6909 199.26 35.1705 203.475 35.8555C203.475 35.8555 203.197 38.0264 202.895 41.3467C201.892 51.4584 200.46 72.2566 205.215 74.7874C211.593 78.1483 229.707 71.8212 229.707 71.8212L233.47 32.541Z" fill="#DBDBDC"/>
<path d="M173.485 169.653L215.42 185.866L218.191 213.722L194.238 226.051L195.838 215.545L173.485 169.653Z" fill="#4175BA"/>
<path d="M216.904 200.795L218.192 213.722L194.238 226.051L195.839 215.545L189.53 202.6L216.904 200.795Z" fill="#DBDBDC"/>
<path d="M205.215 162.461H241.113V239.083H213.385L219.879 230.277C219.879 230.277 206.589 180.937 205.191 162.461" fill="#6693CD"/>
<path d="M228.333 4.51001L234.131 21.094L226.779 23.9905C226.527 24.0921 226.255 24.1334 225.985 24.111C225.714 24.0887 225.453 24.0033 225.221 23.8617C224.99 23.7201 224.795 23.5263 224.651 23.2957C224.508 23.065 224.42 22.804 224.396 22.5335C223.967 18.2206 223.659 8.89254 228.333 4.53903" fill="#DBDBDC"/>
<path d="M231.661 16.9668L230.623 16.7753C230.623 16.7753 231.864 15.3938 231.064 8.88673C230.264 2.3797 239.646 0.59766 244.748 6.24561C252.292 14.5985 247.131 34.4389 247.131 34.4389H233.117C233.117 34.4389 235.587 26.0976 235.587 23.2707C235.587 23.2707 239.147 23.8222 240.985 21.8079C231.064 24.2923 231.661 16.9436 231.661 16.9436" fill="#4071B7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M233.559 14.1123C233.839 13.832 234.219 13.6746 234.615 13.6746C235.012 13.6746 235.392 13.832 235.672 14.1123L235.582 14.2026C235.326 13.9463 234.978 13.8023 234.615 13.8023C234.253 13.8023 233.905 13.9463 233.649 14.2026L233.559 14.1123Z" fill="#20499A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M232.969 17.8259H235.294L235.185 17.9349C235.047 18.0735 234.883 18.1835 234.702 18.2585C234.521 18.3336 234.327 18.3722 234.131 18.3722C233.936 18.3722 233.742 18.3336 233.561 18.2585C233.38 18.1835 233.216 18.0735 233.077 17.9349L232.969 17.8259ZM233.29 17.9536C233.388 18.03 233.495 18.093 233.61 18.1406C233.775 18.2092 233.952 18.2445 234.131 18.2445C234.31 18.2445 234.488 18.2092 234.653 18.1406C234.768 18.093 234.875 18.03 234.972 17.9536H233.29Z" fill="#20499A"/>
<path d="M236.236 8.25414C236.236 7.98132 236.236 11.1565 231.325 13.3971C226.414 15.6377 224.175 10.8198 227.266 5.93808C230.357 1.05635 237.274 -0.969483 241.623 0.458467C245.972 1.88642 253.075 6.26314 248.859 20.1944L244.192 16.8799C244.192 16.8799 246.355 13.9137 243.96 13.0024C241.565 12.091 241.171 14.5406 241.171 14.5406C241.171 14.5406 236.491 11.8008 236.236 8.23673" fill="#F0F0F6"/>
<path d="M244.232 27.6127C256.989 29.6676 251.115 69.9868 251.115 69.9868H215.82C218.099 38.9318 231.441 25.5753 244.232 27.6127Z" fill="#F0F0F6"/>
<path d="M215.82 69.9695H251.098L248.871 190.202C248.871 190.202 222.987 205.294 159.354 178.9L215.82 69.9695Z" fill="#F0F0F6"/>
<path d="M215.82 69.9695L246.801 44.2664C246.801 44.2664 253.011 75.4258 204.809 91.2029L215.82 69.9695Z" fill="#DBDBDC"/>
<path d="M184.775 29.2148C184.241 27.4444 182.49 28.1583 182.49 28.1583C182.49 28.1583 179.817 23.7642 178.397 24.0486C176.976 24.3331 174.129 31.0142 178.054 37.9044L185.749 33.6902C185.499 32.183 185.174 30.6894 184.775 29.2148Z" fill="#4071B7"/>
<path d="M243.49 35.5652C243.49 35.5652 252.292 64.5886 224.355 78.6127C196.418 92.6368 179.527 47.935 177.388 39.228L186.439 33.9283L211.285 57.0774C211.285 57.0774 233.847 14.5523 243.49 35.571" fill="#F0F0F6"/>
<path d="M241.113 223.886V239.094H213.385L219.879 230.277C219.879 230.277 218.719 226.028 217.078 219.567L241.113 223.886Z" fill="#DBDBDC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M228.938 32.9135C229.254 32.6653 229.533 32.4871 229.762 32.4043L229.861 32.6773C229.676 32.7439 229.427 32.8984 229.117 33.1418C228.81 33.3831 228.453 33.7045 228.054 34.0952C227.256 34.8765 226.298 35.9283 225.246 37.1555C223.144 39.6095 220.677 42.7575 218.399 45.8286C216.121 48.9 214.033 51.8915 212.685 54.0325C212.01 55.105 211.526 55.9568 211.295 56.4968C211.246 56.6115 211.209 56.7096 211.185 56.791L211.547 57.1074L211.348 57.2024C211.257 57.2456 211.156 57.2788 211.063 57.2615C211.01 57.2518 210.961 57.2264 210.924 57.1824C210.888 57.1405 210.871 57.0915 210.864 57.0466C210.857 57.0003 210.858 56.9495 210.865 56.8963L201.229 48.4795L201.419 48.2608L210.948 56.5842C210.971 56.5213 210.998 56.4539 211.028 56.3826C211.269 55.8195 211.765 54.9507 212.44 53.8778C213.793 51.728 215.886 48.7299 218.166 45.6556C220.447 42.581 222.918 39.4273 225.026 36.9665C226.08 35.7364 227.045 34.6775 227.852 33.8878C228.255 33.493 228.62 33.1637 228.938 32.9135Z" fill="white"/>
<path d="M202.988 40.1798C203.249 37.5387 203.452 35.8844 203.452 35.8844C199.393 35.1878 201.364 29.6385 201.364 29.6385C201.364 29.6385 198.291 28.942 197.63 27.3167C196.969 25.6913 203.672 26.8755 206.67 27.6881C208.137 28.0887 209.691 34.172 210.891 40.2727L202.988 40.1798Z" fill="#4071B7"/>
<path d="M363.843 84.0574C363.843 84.0574 370.163 115.937 347.688 120.772C325.213 125.607 309.998 111.09 309.998 111.09C309.998 111.09 306.995 112.396 303.73 108.187C303.73 108.187 300.332 109.534 300.46 107.485C300.587 105.436 304.455 94.5871 341.177 104.275L349.184 85.3983L363.843 84.0574Z" fill="#DBDBDC"/>
<path d="M368.319 71.6354C361.674 69.0407 352.663 76.2094 348.245 87.633L372.303 97.0134C376.744 85.5898 374.964 74.2243 368.319 71.6354Z" fill="#DBDBDC"/>
<path d="M320.337 152.32L355.667 136.108L365.315 81.4569C365.315 81.4569 362.677 57.0192 379.928 65.1225C397.178 73.2258 410.474 144.618 381.603 161.561C352.733 178.505 300.616 163.976 320.337 152.344" fill="#1F3563"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M332.808 167.374C346.939 170.33 367.105 169.719 381.457 161.311L381.75 161.812C367.231 170.318 346.904 170.915 332.689 167.942C329.132 167.198 325.949 166.228 323.329 165.099C320.713 163.971 318.64 162.675 317.319 161.269C315.995 159.858 315.402 158.303 315.844 156.696C316.28 155.112 317.7 153.562 320.19 152.094L320.484 152.594C318.044 154.033 316.779 155.481 316.403 156.85C316.033 158.197 316.502 159.551 317.742 160.871C318.985 162.196 320.978 163.453 323.558 164.566C326.135 165.676 329.279 166.635 332.808 167.374Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M350.327 235.356V169.235H350.907V235.356H350.327Z" fill="#B7BBCE"/>
<path d="M314.185 228.164L310.259 208.057L329.58 207.052L329.365 231.751L311.836 232.657L314.185 228.164Z" fill="#1F3563"/>
<path d="M335.169 234.265L329.435 214.599L348.581 211.842L350.617 236.459L333.244 238.955L335.169 234.265Z" fill="#0B4B9B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M365.124 240H283.325V239.129H365.124V240Z" fill="#B7BBCE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M242.4 240H160.601V239.129H242.4V240Z" fill="#B7BBCE"/>
<path d="M367.27 68.4834C345.056 71.0317 345.873 124.371 345.873 124.371H384.653C381.754 83.3956 383.726 66.6027 367.258 68.4834" fill="#F0F0F6"/>
<path d="M379.493 131.122L344.238 123.744C344.238 123.744 288.231 127.128 307.285 209.74L330.304 214.564L335.697 164.609C335.697 164.609 377.684 176.067 379.493 131.11" fill="#4175BA"/>
<path d="M372.883 116.285L347.114 108.28C347.114 108.28 356.108 112.785 359.111 96.2763C360.886 86.4896 374.187 92.718 374.187 92.718L372.883 116.285Z" fill="#DBDBDC"/>
<path d="M385.198 123.686H346.418C346.418 123.686 288.156 138.627 326.42 216.358H351.736L346.418 165.857C346.418 165.857 393.189 168.486 385.198 123.674" fill="#6693CD"/>
<path d="M338.016 123.726L384.665 120.168C384.665 120.168 401.098 157.08 347.346 172.201C347.346 172.201 334.908 171.754 317.136 166.861C295.328 160.859 306.032 150.852 306.032 150.852C306.032 150.852 310.132 135.249 338.016 123.726Z" fill="#F0F0F6"/>
<path d="M366.469 110.399L383.088 114.602C383.088 114.602 387.408 125.903 371.798 131.087C356.189 136.27 331.082 112.872 331.082 112.872L366.469 110.399Z" fill="#DBDBDC"/>
<path d="M387.524 88.4224C389.698 95.7398 389.062 103.606 385.74 110.479C382.419 117.351 376.652 122.732 369.571 125.567C362.491 128.402 354.607 128.486 347.468 125.802C340.329 123.118 334.449 117.861 330.983 111.061C330.983 111.061 327.979 112.372 324.709 108.158C324.709 108.158 321.317 109.511 321.444 107.462C321.572 105.413 326.541 99.2655 363.263 108.953L361.836 89.4208L387.524 88.4224Z" fill="#F0F0F6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M323.03 105.733C322.03 106.407 321.759 107.1 321.734 107.503C321.72 107.721 321.755 107.87 321.807 107.972C321.858 108.073 321.934 108.147 322.038 108.204C322.261 108.324 322.601 108.354 323.001 108.316C323.391 108.278 323.791 108.179 324.098 108.088C324.251 108.042 324.378 107.999 324.467 107.968C324.511 107.952 324.546 107.939 324.569 107.93L324.595 107.92L324.601 107.918L324.602 107.918L324.804 107.837L324.938 108.009C326.535 110.064 328.043 110.745 329.122 110.931C329.665 111.024 330.109 110.994 330.414 110.942C330.567 110.916 330.684 110.884 330.762 110.86C330.8 110.848 330.829 110.838 330.847 110.831C330.856 110.828 330.862 110.826 330.866 110.824L330.868 110.823C330.868 110.823 330.868 110.823 330.983 111.09C331.099 111.356 331.098 111.356 331.098 111.356L331.096 111.357L331.093 111.358L331.083 111.362C331.075 111.366 331.064 111.37 331.05 111.375C331.022 111.385 330.983 111.399 330.933 111.415C330.834 111.446 330.691 111.483 330.511 111.514C330.151 111.575 329.64 111.609 329.023 111.503C327.815 111.295 326.239 110.559 324.612 108.532C324.52 108.564 324.401 108.603 324.264 108.644C323.939 108.741 323.499 108.851 323.057 108.893C322.625 108.935 322.141 108.918 321.763 108.715C321.567 108.609 321.4 108.453 321.29 108.234C321.18 108.019 321.137 107.761 321.155 107.467C321.194 106.845 321.592 106.002 322.707 105.251C323.818 104.502 325.651 103.836 328.602 103.544C334.503 102.958 344.966 103.855 363.337 108.702L363.189 109.263C344.838 104.422 334.456 103.546 328.66 104.121C325.762 104.409 324.034 105.057 323.03 105.733ZM330.869 110.823C330.869 110.823 330.869 110.823 330.869 110.823V110.823Z" fill="white"/>
<path d="M330.919 103.7C322.801 103.863 321.166 106.342 321.062 107.508C320.934 109.552 324.332 108.205 324.332 108.205C327.597 112.39 330.6 111.107 330.6 111.107L330.919 103.7Z" fill="#4071B7"/>
<path d="M308.468 101.019C301.016 104.24 300.442 107.16 300.814 108.263C301.469 110.207 304.101 107.682 304.101 107.682C308.74 110.318 310.996 107.967 310.996 107.967L308.468 101.019Z" fill="#4071B7"/>
<path d="M292.626 111.554L296.453 107.984H334.381L337.222 113.882L282.763 113.777C282.55 113.752 282.355 113.645 282.221 113.478C282.086 113.31 282.023 113.097 282.044 112.883C282.024 112.67 282.087 112.458 282.22 112.29C282.353 112.122 282.545 112.013 282.757 111.984L292.626 111.554Z" fill="#0B4B9B"/>
<path d="M313.588 104.438L251.776 103.422L242.504 62.824L306.374 61.2974L313.588 104.438Z" fill="#0B4B9B"/>
<path d="M287.512 113.481H278.142L273.213 92.9966H282.577L287.512 113.481Z" fill="#0B4B9B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M272.845 92.7063H282.806L287.88 113.771H277.913L272.845 92.7063ZM273.581 93.2868L278.37 113.191H287.144L282.349 93.2868H273.581Z" fill="white"/>
<path d="M353.475 55.3009L352.408 55.2719C352.408 55.2719 353.458 53.8033 351.759 47.8361C350.06 41.8689 359.21 38.8215 365.095 43.3433C373.793 50.0245 373.793 68.9942 373.793 68.9942L359.042 71.1767C359.042 71.1767 358.653 63.2708 358.265 60.6296C358.265 60.6296 361.906 60.6296 363.483 58.4645C353.893 62.2549 353.475 55.3009 353.475 55.3009Z" fill="#4071B7"/>
<path d="M353.04 55.2894C353.04 55.2894 351.741 63.027 355.261 64.6697C358.781 66.3124 366.568 64.1937 366.04 57.4603C365.513 50.7269 365.849 49.8214 367.415 50.1522C368.98 50.4831 368.041 52.4218 368.041 52.4218L371.41 53.5247C371.41 53.5247 368.284 40.232 363.129 39.3091C357.975 38.3861 347.52 45.8684 347.52 45.8684C347.52 45.8684 349.741 47.8304 351.776 47.4182L348.877 48.5327C348.877 48.5327 352.414 52.5321 361.054 49.8388L363.373 51.1564C363.373 51.1564 363.756 56.3052 361.923 56.3806C360.091 56.4561 357.285 54.1284 353.052 55.3126" fill="#1F3563"/>
<path d="M357.059 52.0852C356.744 51.8755 356.363 51.7902 355.989 51.846C355.615 51.9018 355.275 52.0946 355.035 52.3871L357.059 52.0852Z" fill="white"/>
<path d="M355.064 55.9454C355.379 56.1551 355.76 56.2404 356.134 56.1846C356.508 56.1289 356.848 55.936 357.088 55.6436L355.064 55.9454Z" fill="#0B4B9B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M357.789 55.2454L357.312 55.8277C357.026 56.1757 356.621 56.4052 356.176 56.4716C355.731 56.538 355.278 56.4365 354.903 56.1869L354.277 55.7693L357.789 55.2454Z" fill="#2354A2"/>
<path d="M374.628 63.4973L356.722 69.4065C356.722 69.4065 356.253 77.4227 356.224 77.6027L377.73 73.8122L374.628 63.4973Z" fill="#DBDBDC"/>
<path d="M372.906 68.9827C380.009 68.3616 386.631 77.7594 387.692 89.9782L361.97 92.2246C360.909 80.0058 365.803 69.5864 372.906 69.0059" fill="#F0F0F6"/>
<path d="M36.7404 85.6245C36.7404 85.6245 31.1681 115.101 50.9988 119.576C70.8295 124.051 84.2703 110.619 84.2703 110.619C84.2703 110.619 86.926 111.827 89.8078 107.955C89.8078 107.955 92.8056 109.203 92.707 107.311C92.6085 105.418 89.1816 95.3822 56.7566 104.339L48.3373 86.3095L36.7404 85.6245Z" fill="#DBDBDC"/>
<path d="M31.5103 75.4492C37.6567 73.0518 45.9717 79.6808 50.0654 90.2511L27.8109 98.9233C23.6998 88.3529 25.3524 77.8465 31.4929 75.4492" fill="#DBDBDC"/>
<path d="M72.7894 149.517L31.1913 83.9934C31.1913 83.9934 33.6325 61.3958 17.6751 68.9013C1.71779 76.4067 -10.5749 142.406 16.1269 158.038C42.8288 173.67 91.0255 160.273 72.7894 149.517Z" fill="#0B4B9B"/>
<path d="M123.155 203.622L115.884 186.133L102.315 192.866L115.124 211.772L127.26 205.88L123.155 203.622Z" fill="#4175BA"/>
<path d="M154.142 178.9L138.509 168.219L130.814 181.274L151.851 190.114L158.815 178.546L154.142 178.9Z" fill="#0B4B9B"/>
<path d="M52.5413 112.233L76.3729 159.843L131.4 182.882L141.495 168.538L52.5413 112.233Z" fill="#1F3563"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M100.877 239.53H25.2246V238.659H100.877V239.53Z" fill="#B7BBCE"/>
<path d="M52.5412 112.222H18.9681C18.9681 112.222 9.62101 156.68 74.4187 159.82L103.075 196.227L118.052 187.102C118.052 187.102 100.999 129.578 52.5296 112.222" fill="#0B4B9B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.3538 239.094V165.474H45.2235V239.094H44.3538Z" fill="#B7BBCE"/>
<path d="M57.638 114.108H19.3334C19.3334 114.108 9.54559 157.91 76.2336 161.184C76.2336 161.184 73.4735 208.933 79.2488 201.097C102.773 169.171 92.5331 136.891 92.5331 136.891C92.5331 136.891 81.4522 123.947 57.638 114.108Z" fill="#F0F0F6"/>
<path d="M32.1249 109.54L19.4553 113.104C19.4553 113.104 16.8634 125.508 29.4055 132.416C41.9475 139.324 59.9111 114.776 59.9111 114.776L32.1249 109.54Z" fill="#DBDBDC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.759 104.403C73.1904 104.943 74.5916 106.872 74.6676 108.053C74.6853 108.328 74.6451 108.569 74.5421 108.773C74.4379 108.979 74.2794 109.127 74.0947 109.226C73.7393 109.417 73.2861 109.431 72.8852 109.392C72.474 109.352 72.0651 109.249 71.7635 109.159C71.6425 109.123 71.5373 109.088 71.4543 109.06C69.9516 110.919 68.4936 111.595 67.3737 111.786C66.801 111.883 66.3262 111.852 65.9911 111.794C65.8236 111.765 65.6909 111.73 65.598 111.701C65.5516 111.687 65.5151 111.674 65.4891 111.664C65.4761 111.659 65.4658 111.655 65.4581 111.652L65.4487 111.648L65.4455 111.647L65.4443 111.647C65.444 111.646 65.4433 111.646 65.5588 111.38C65.6743 111.114 65.6741 111.114 65.6739 111.114L65.6756 111.114C65.6787 111.115 65.6843 111.118 65.6924 111.121C65.7085 111.127 65.7346 111.136 65.7701 111.147C65.8409 111.169 65.9488 111.198 66.0889 111.222C66.3689 111.27 66.7773 111.298 67.2766 111.213C68.2681 111.045 69.6566 110.424 71.1288 108.537L71.2624 108.366L71.464 108.446L71.4653 108.446L71.471 108.448L71.4948 108.458C71.5162 108.466 71.5481 108.477 71.589 108.492C71.6709 108.521 71.7884 108.561 71.9293 108.603C72.2132 108.688 72.5824 108.779 72.9416 108.814C73.3112 108.85 73.6208 108.822 73.8205 108.715C73.9133 108.665 73.9803 108.599 74.0247 108.511C74.0703 108.421 74.1017 108.288 74.0889 108.09C74.0431 107.378 73.0873 105.516 67.7016 104.98C62.3454 104.447 52.7472 105.255 35.7765 109.733L35.6287 109.172C52.6195 104.688 62.2982 103.859 67.759 104.403Z" fill="white"/>
<path d="M48.8244 74.1721C51.6424 71.8908 43.3507 69.8534 46.8239 63.1432C50.2972 56.433 46.0817 51.7661 46.0817 51.7661L34.6994 49.5312L33.8006 59.7533L27.6484 65.0181C27.6484 65.0181 35.5575 75.2692 41.6401 81.0332C48.9172 87.9234 54.2285 77.2892 48.5345 74.2533L48.8244 74.1721Z" fill="#1F3563"/>
<path d="M38.88 59.9796L39.831 59.8055C39.831 59.8055 38.6713 58.5284 39.4251 52.5264C40.1789 46.5244 31.5102 44.8874 26.7902 50.1117C19.8321 57.8203 24.5752 76.1456 24.5752 76.1456H37.5232C37.5232 76.1456 35.2386 68.4544 35.2386 65.8423C35.2386 65.8423 31.9567 66.3473 30.2577 64.4898C39.4135 66.8117 38.8684 60.0028 38.8684 60.0028" fill="#4071B7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.149 57.3119C35.8758 57.3119 35.6137 57.4204 35.4202 57.6135L35.0107 57.2025C35.3129 56.9009 35.7223 56.7314 36.149 56.7314C36.5758 56.7314 36.9851 56.9009 37.2873 57.2025L36.8778 57.6135C36.6844 57.4204 36.4223 57.3119 36.149 57.3119Z" fill="#D2D2D2"/>
<path d="M37.529 60.8503C37.2804 61.0964 36.945 61.2343 36.5954 61.2343C36.2459 61.2343 35.9104 61.0964 35.6619 60.8503H37.529Z" fill="#0B4B9B"/>
<path d="M28.9589 58.0001C28.9589 58.0001 38.2944 54.7902 39.4425 51.3132C39.4425 51.3132 39.541 56.6883 45.4844 56.7928C51.4279 56.8973 42.6432 44.3069 33.7484 43.9354C24.8535 43.5639 15.2571 52.3638 23.0328 62.0054L26.3553 59.4223C26.3553 59.4223 25.4102 58.3368 26.0654 57.4081C26.7206 56.4793 27.7528 56.3458 28.9646 57.9885" fill="#1F3563"/>
<path d="M25.5146 71.9255C13.3378 73.8759 18.9681 112.233 18.9681 112.233H52.5412C50.3726 82.6815 37.6739 69.9693 25.5146 71.9255Z" fill="#F0F0F6"/>
<path d="M82.2351 100.438C91.2691 101.93 92.6317 105.419 92.4983 106.939C92.2664 109.615 88.7235 107.328 88.7235 107.328C84.3051 112.239 81.1855 110.062 81.1855 110.062L82.2351 100.438Z" fill="#4071B7"/>
<path d="M115.286 80.0288L100.471 108.28H76.8078L73.5491 113.731H135.308L151.103 80.0288H115.286Z" fill="#6693CD"/>
<path d="M21.3281 69.447L21.85 76.6216L36.8274 78.0321L37.8247 71.5135L21.3281 69.447Z" fill="#DBDBDC"/>
<path d="M25.5552 72.4017C18.9623 72.6223 13.6451 80.3715 14.022 91.708L38.1726 92.5265C37.7899 81.19 32.1422 72.2101 25.5552 72.4017Z" fill="#F0F0F6"/>
<path d="M25.0508 113.383L47.0501 106.545C47.0501 106.545 39.3672 110.393 36.8101 96.2936C35.2909 87.9406 23.9143 93.2577 23.9143 93.2577L25.0508 113.383Z" fill="#DBDBDC"/>
<path d="M14.0337 91.3945C17.1417 114.445 23.5779 122.589 30.0838 124.667C43.2289 128.87 66.1386 111.363 66.1386 111.363C66.1386 111.363 68.9276 112.57 71.937 108.698C71.937 108.698 75.0914 109.946 74.9696 108.054C74.8479 106.162 70.2381 100.473 36.12 109.43C35.7566 103.42 35.3933 97.4101 35.0299 91.4003L14.0337 91.3945Z" fill="#F0F0F6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M73.2618 106.052C74.2811 106.739 74.6492 107.513 74.685 108.088C74.7019 108.36 74.6617 108.598 74.5596 108.8C74.4564 109.004 74.2996 109.149 74.1171 109.248C73.7659 109.437 73.3188 109.451 72.9239 109.413C72.5188 109.374 72.1163 109.274 71.8194 109.185C71.7019 109.15 71.5995 109.117 71.5182 109.089C70.0414 110.916 68.6064 111.581 67.5031 111.768C66.9389 111.863 66.4707 111.832 66.1403 111.775C65.9751 111.747 65.8442 111.712 65.7526 111.683C65.7068 111.669 65.6708 111.656 65.6451 111.647C65.6323 111.642 65.622 111.638 65.6145 111.635L65.6051 111.631L65.602 111.63L65.6008 111.629C65.6005 111.629 65.5998 111.629 65.7153 111.363C65.8308 111.096 65.8306 111.096 65.8304 111.096L65.832 111.097C65.835 111.098 65.8405 111.1 65.8484 111.103C65.8643 111.109 65.89 111.118 65.9248 111.129C65.9945 111.151 66.1006 111.179 66.2384 111.203C66.5139 111.25 66.9155 111.278 67.4065 111.195C68.3813 111.031 69.7465 110.421 71.1924 108.566L71.3259 108.395L71.5276 108.475L71.5289 108.475L71.5343 108.477L71.5578 108.486C71.5787 108.494 71.6101 108.506 71.6503 108.52C71.7307 108.549 71.8463 108.588 71.9847 108.629C72.2637 108.712 72.6266 108.801 72.9796 108.835C73.3429 108.87 73.6467 108.842 73.8424 108.736C73.9333 108.688 73.999 108.623 74.0425 108.537C74.0872 108.449 74.1183 108.319 74.1062 108.124C74.0841 107.768 73.8433 107.144 72.938 106.534C72.0298 105.922 70.462 105.333 67.8285 105.071C62.5599 104.547 53.1182 105.342 36.4258 109.745L36.2781 109.184C52.9905 104.775 62.5127 103.959 67.8858 104.493C70.5731 104.761 72.2454 105.368 73.2618 106.052Z" fill="white"/>
<path d="M159.963 62.2028L163.46 60.1015L161.935 58.2324L158.438 60.3337L159.963 62.2028Z" fill="#0B4B9B"/>
<path d="M162.822 67.752L167.727 64.8032L164.272 59.7996L159.372 62.7483L162.822 67.752Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M164.397 59.2161L168.349 64.9375L162.696 68.3356L158.751 62.6142L164.397 59.2161ZM159.993 62.8827L162.948 67.1685L167.106 64.669L164.146 60.3832L159.993 62.8827Z" fill="#B7BBCE"/>
<path d="M179.736 92.242L184.636 89.299L181.18 84.2954L176.28 87.2384L179.736 92.242Z" fill="#0B4B9B"/>
<path d="M177.498 90.5005L183.859 86.6753L165.884 60.6355L159.517 64.4608L177.498 90.5005Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M165.926 60.4412L184.066 86.7201L177.456 90.6952L159.31 64.4161L165.926 60.4412ZM159.724 64.5057L177.54 90.3062L183.652 86.6306L165.842 60.8301L159.724 64.5057Z" fill="#DBDBDC"/>
<path d="M169.241 75.2807C169.206 75.7725 169.32 76.2635 169.567 76.6896C169.815 77.1158 170.185 77.4574 170.629 77.6701C171.074 77.8828 171.572 77.9566 172.059 77.882C172.545 77.8074 172.999 77.5877 173.359 77.2517C173.719 76.9157 173.97 76.4789 174.079 75.998C174.188 75.5172 174.15 75.0147 173.97 74.5559C173.789 74.0971 173.475 73.7033 173.068 73.4257C172.661 73.1481 172.18 72.9996 171.688 72.9995C171.061 72.9776 170.452 73.2055 169.993 73.6332C169.534 74.061 169.263 74.6535 169.241 75.2807Z" fill="#0B4B9B"/>
<path d="M179.307 101.21L186.99 97.4604L186.578 96.7349L178.901 100.485L179.307 101.21Z" fill="#2757A0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.01 60.052L184.48 86.8094L177.372 91.0839L158.896 64.3263L166.01 60.052ZM160.138 64.595L177.624 89.9169L183.238 86.5409L165.758 61.2188L160.138 64.595Z" fill="#B7BBCE"/>
<path d="M176.64 105.007L177.277 106.464L184.868 102.754L182.977 99.3818L177.254 102.18C171.572 99.7243 168.325 96.6884 167.6 93.1534C166.44 87.4822 171.781 82.3973 171.838 82.3393L170.012 79.7156C169.734 79.9826 162.897 86.4838 164.376 93.7106C164.909 96.2879 166.376 98.6156 168.765 100.676H168.649V109.795H178.437L177.277 106.469L177.19 106.51L176.64 105.007Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M170.078 79.0491L172.404 82.3893L172.146 82.6471C172.145 82.6478 172.144 82.6497 172.14 82.6527C172.042 82.7485 170.768 83.9852 169.635 85.8662C168.462 87.813 167.478 90.3856 168.026 93.0658C168.699 96.3496 171.716 99.2767 177.244 101.7L183.156 98.8101L185.472 102.943L177.814 106.686L179.05 110.231H168.215V100.771C165.924 98.7176 164.486 96.3886 163.95 93.7989L163.95 93.7979C163.179 90.0282 164.58 86.4793 166.164 83.8766C167.734 81.296 169.522 79.5828 169.706 79.4066C169.708 79.4046 169.71 79.4028 169.711 79.4012L170.078 79.0491ZM169.084 101.112H169.936L169.049 100.346C166.718 98.3356 165.312 96.0882 164.802 93.6227C164.095 90.1658 165.372 86.8508 166.907 84.3296C168.061 82.4335 169.338 81.0224 169.952 80.3899L171.275 82.2905C171.121 82.4512 170.908 82.6815 170.659 82.9727C170.156 83.5595 169.504 84.3973 168.89 85.4165C167.669 87.4444 166.562 90.2497 167.174 93.2406C167.943 96.9908 171.362 100.108 177.082 102.579L177.266 102.659L182.799 99.9534L184.263 102.565L177.494 105.873L177.038 104.832L176.232 105.156L176.945 107.105L177.026 107.067L177.825 109.36H169.084V101.112Z" fill="#B7BBCE"/>
<path d="M186.375 109.766H161.714V114.033H186.375V109.766Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M161.28 109.331H186.81V114.468H161.28V109.331ZM162.149 110.202V113.597H185.94V110.202H162.149Z" fill="#B7BBCE"/>
<path d="M170.163 104.385C170.127 104.881 170.24 105.376 170.489 105.805C170.738 106.235 171.11 106.58 171.558 106.795C172.005 107.01 172.507 107.085 172.997 107.01C173.488 106.935 173.945 106.715 174.308 106.377C174.671 106.038 174.925 105.599 175.035 105.114C175.145 104.63 175.107 104.124 174.926 103.661C174.744 103.199 174.428 102.802 174.018 102.522C173.608 102.242 173.123 102.093 172.627 102.092C172.315 102.081 172.003 102.131 171.71 102.24C171.418 102.349 171.149 102.515 170.92 102.727C170.691 102.94 170.507 103.196 170.377 103.481C170.247 103.765 170.174 104.073 170.163 104.385Z" fill="#0B4B9B"/>
<path d="M66.0283 104.333C73.6069 104.478 75.1319 106.852 75.2015 107.955C75.3232 109.911 72.1515 108.623 72.1515 108.623C69.1015 112.616 66.2951 111.368 66.2951 111.368L66.0283 104.333Z" fill="#4071B7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.387 113.609H336.567V114.48H62.387V113.609Z" fill="#B7BBCE"/>
</g>
<defs>
<clipPath id="clip0_2686_13418">
<rect width="398" height="240" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -82,13 +82,9 @@
id: data.data.id,
type: data.data.type
};
const { rolesUrl } = container.dataset;
const params = {
object,
roles_path: rolesUrl
};
const modal = $('#accessModalComponent').data('accessModal');
modal.params = params;
modal.params = { object };
modal.open();
});
});

View file

@ -3,17 +3,6 @@
const protocolModal = '#newProtocolModal';
const submitButton = $('.create-protocol-button');
let roleSelector = `${protocolModal} #protocol_role_selector`;
dropdownSelector.init(roleSelector, {
noEmptyOption: true,
singleSelect: true,
closeOnSelect: true,
selectAppearance: 'simple',
onChange: function() {
$('#protocol_default_public_user_role_id').val(dropdownSelector.getValues(roleSelector));
}
});
$(protocolModal)
.on('input', '#protocol_name', function() {
if ($(this).val().length >= GLOBAL_CONSTANTS.NAME_MIN_LENGTH) {
@ -22,11 +11,6 @@
submitButton.attr('disabled', 'disabled');
}
})
.on('change', '#protocol_visibility', function() {
let checked = $(this)[0].checked;
$('#roleSelectWrapper').toggleClass('hidden', !checked);
$('#protocol_default_public_user_role_id').prop('disabled', !checked);
})
.on('submit', function() {
submitButton.attr('disabled', 'disabled');
})

View file

@ -198,15 +198,6 @@ var protocolsIO = function() {
e.stopPropagation();
animateSpinner(modal, true);
const visibility = $('#protocol-preview-modal .modal-footer #visibility').prop('checked');
const defaultPublicUserRoleId = $('#protocol-preview-modal .modal-footer #default_public_user_role_id')
.prop('value');
const visibilityField = $('#protocol-preview-modal #protocol_visibility');
const defaultPublicUserRoleIdField = $('#protocol-preview-modal #protocol_default_public_user_role_id');
visibilityField.prop('value', visibility ? 'visible' : 'hidden');
defaultPublicUserRoleIdField.prop('value', defaultPublicUserRoleId);
$.ajax({
type: 'POST',
url: url,
@ -291,24 +282,6 @@ var protocolsIO = function() {
$('form.protocols-search-bar').submit();
function initProtocolModalPreview() {
$('#protocol-preview-modal').on('change', '#visibility', function() {
const checkbox = this;
$('#protocol-preview-modal #roleSelectWrapper').toggleClass('hidden', !checkbox.checked);
});
const roleSelector = '#protocol-preview-modal #role_selector';
dropdownSelector.init(roleSelector, {
noEmptyOption: true,
singleSelect: true,
closeOnSelect: true,
selectAppearance: 'simple',
onChange: function() {
$('#protocol-preview-modal #default_public_user_role_id').val(dropdownSelector.getValues(roleSelector));
}
});
$('#protocol-preview-modal')
.on('ajax:error', 'form', function(e, error) {
let msg = error.responseJSON.error;

View file

@ -12,7 +12,9 @@ let inlineEditing = (function() {
}
if ($(container).data('params-group') === 'protocol' && $(container).hasClass('inline-editing-container')) {
$('.view-mode').text(I18n.t('protocols.draft_name', { name: $('.view-mode').text() }));
container.find('.view-mode').text(I18n.t('protocols.draft_name', { name: container.find('.view-mode').text() }));
} else if ($(container).data('params-group') === 'user_group' && $(container).hasClass('inline-editing-container')) {
container.find('.view-mode').text(`${I18n.t('user_groups.show.title')} ${container.find('.view-mode').text()}`);
}
}

View file

@ -150,7 +150,11 @@
if (data.status === 'done') {
// Reload the whole table
HelperModule.flashAlertMsg(jobData.success_message, 'success');
usersDatatable.ajax.reload();
if(jobData.redirect_url) {
window.location.href = jobData.redirect_url;
} else {
usersDatatable.ajax.reload();
}
animateSpinner(null, false);
$('#destroy-user-team-modal').modal('hide');
clearInterval(jobStatusInterval);

View file

@ -17,6 +17,14 @@
.fixed-content-body {
height: calc(100vh - var(--content-header-size) - var(--navbar-height));
width: 100%;
&.user-groups-table-container {
height: calc(100vh - var(--content-header-size) - var(--navbar-height) - 72px);
}
&.user-group-table-container {
height: calc(100vh - var(--content-header-size) - var(--navbar-height) - 104px);
}
}
.content-header {

View file

@ -4,10 +4,10 @@ module Reports
class RepositoriesInputComponent < TemplateValueComponent
def initialize(report:, name:, label:, placeholder: nil, editing: true, displayed_field: :name, user: nil)
super(report: report, name: name, label: label, placeholder: placeholder, editing: editing)
live_repositories = Repository.viewable_by_user(user, report.team).sort_by { |r| r.name.downcase }
live_repositories = Repository.readable_by_user(user, report.team).sort_by { |r| r.name.downcase }
snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository)
.where(team: report.team)
.where.not(original_repository: live_repositories)
.where.not(original_repository: report.team.repositories)
.select('DISTINCT ON ("repositories"."parent_id") "repositories".*')
.sort_by { |r| r.name.downcase }
@repositories = live_repositories + snapshots_of_deleted

View file

@ -0,0 +1,200 @@
# frozen_string_literal: true
module AccessPermissions
class BaseController < ApplicationController
include InputSanitizeHelper
include UserRolesHelper
before_action :set_model
before_action :set_assignment, only: %i(create update destroy)
before_action :check_read_permissions, only: %i(show show_user_group_assignments user_roles)
before_action :check_manage_permissions, except: %i(show show_user_group_assignments user_roles)
before_action :load_available_users, only: %i(new create)
def show
render json: @model.user_assignments.where(team: current_team).includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer, user: current_user
end
def new
render json: @available_users, each_serializer: UserSerializer, user: current_user
end
def edit; end
def create
ActiveRecord::Base.transaction do
@assignment.update!(
user_role_id: permitted_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
case assignment_type
when :team
log_activity(:"#{model_parameter}_access_granted_all_team_members", team: @assignment.team.id, role: @assignment.user_role.name)
when :user
log_activity(:"#{model_parameter}_access_granted", user_target: @assignment.user.id, role: @assignment.user_role.name)
when :user_group
log_activity(:"#{model_parameter}_access_granted_user_group", user_group: @assignment.user_group.id, role: @assignment.user_role.name)
end
propagate_job
@message = if assignment_type == :team
t('access_permissions.create.success', member_name: t('access_permissions.all_team'))
else
t('access_permissions.create.success', member_name: escape_input(assignment_type == :user_group ? @assignment.user_group.name : @assignment.user.name))
end
render json: { message: @message }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
errors = @model.errors.present? ? @model.errors&.map(&:message)&.join(',') : e.message
render json: { flash: errors }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
def update
# prevent role change if it would result in no manually assigned users having the user management permission
new_user_role = UserRole.find(permitted_params[:user_role_id])
if permitted_params[:user_id].present? && permitted_params[:user_id] != 'all' && !new_user_role.has_permission?(manage_permission_constant) &&
@assignment.last_with_permission?(manage_permission_constant, assigned: :manually)
raise ActiveRecord::RecordInvalid
end
@assignment.update!(user_role_id: permitted_params[:user_role_id], assigned_by: current_user)
case assignment_type
when :team
log_activity(:"#{model_parameter}_access_changed_all_team_members", team: @assignment.team.id, role: @assignment.user_role.name)
when :user
log_activity(:"#{model_parameter}_access_changed", user_target: @assignment.user.id, role: @assignment.user_role.name)
when :user_group
log_activity(:"#{model_parameter}_access_changed_user_group", user_group: @assignment.user_group.id, role: @assignment.user_role.name)
end
propagate_job
render json: { user_role_id: @assignment.user_role_id }, status: :ok
rescue ActiveRecord::RecordInvalid
render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity
end
def destroy
# prevent deletion of last manually assigned user that can manage users
if params[:user_id].present? && params[:user_id] != 'all' && @assignment.last_with_permission?(manage_permission_constant, assigned: :manually)
raise ActiveRecord::RecordInvalid
end
UserAssignments::PropagateAssignmentJob.perform_now(@assignment, destroy: true)
case assignment_type
when :team
@assigned_name = @assignment.team.name
log_activity(:"#{model_parameter}_access_revoked_all_team_members", team: @assignment.team.id, role: @assignment.user_role.name)
when :user_group
@assigned_name = @assignment.user_group.name
log_activity(:"#{model_parameter}_access_revoked_user_group", user_group: @assignment.user_group.id, role: @assignment.user_role.name)
when :user
@assigned_name = @assignment.user.full_name
log_activity(:"#{model_parameter}_access_revoked", user_target: @assignment.user.id, role: @assignment.user_role.name)
end
render json: { message: t('access_permissions.destroy.success', member_name: escape_input(@assigned_name)) }
rescue ActiveRecord::RecordInvalid
render json: { message: t('access_permissions.destroy.failure') },
status: :unprocessable_entity
end
def show_user_group_assignments
render json: @model.user_group_assignments.where(team: current_team).includes(:user_role, :user_group).order('user_groups.name ASC'),
each_serializer: UserGroupAssignmentSerializer, user: current_user
end
def unassigned_user_groups
render json: current_team.user_groups.where.not(id: @model.user_group_assignments.select(:user_group_id)),
each_serializer: UserGroupSerializer, user: current_user
end
def user_roles
render json: { data: user_roles_collection(@model).map(&:reverse) }
end
private
def model_parameter
@model.class.permission_class.model_name.param_key
end
def manage_permission_constant
"#{@model.class.permission_class.name}Permissions::USERS_MANAGE".constantize
end
def permitted_default_public_user_role_params
params.require(:object).permit(:default_public_user_role_id)
end
def permitted_params
params.require(:user_assignment)
.permit(%i(user_role_id user_id user_group_id team_id))
end
def load_available_users
@available_users = current_team.users.where.not(id: @model.user_assignments.where(team: current_team).select(:user_id)).order(users: { full_name: :asc })
end
def propagate_job(destroy: false)
return unless @model.has_permission_children?
UserAssignments::PropagateAssignmentJob.perform_later(
@assignment,
destroy: destroy
)
end
def check_manage_permissions
raise NotImplementedError
end
def check_read_permissions
raise NotImplementedError
end
def log_activity(type_of, message_items = {})
message_items = { model_parameter => @model.id }.merge(message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: @model,
team: @model.team,
project: @project,
message_items: message_items)
end
def set_assignment
case assignment_type
when :user, :user_group
@assignment = @model.public_send(:"#{assignment_type}_assignments").find_or_initialize_by(
"#{assignment_type}_id": permitted_params[:"#{assignment_type}_id"],
team: current_team
)
when :team
@assignment =
@model.team_assignments
.find_or_initialize_by(team: current_team, assignable: @model)
end
end
def assignment_type
if permitted_params[:team_id].present? || permitted_params[:user_id] == 'all'
:team
elsif permitted_params[:user_group_id].present?
:user_group
elsif permitted_params[:user_id].present?
:user
end
end
end
end

View file

@ -1,90 +1,61 @@
# frozen_string_literal: true
module AccessPermissions
class ExperimentsController < ApplicationController
before_action :set_experiment
class ExperimentsController < BaseController
before_action :set_project
before_action :check_read_permissions, only: %i(show)
before_action :check_manage_permissions, only: %i(edit update)
def show
render json: @experiment.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer, user: current_user
end
def new
render json: @available_users, each_serializer: UserSerializer, user: current_user
end
def edit; end
def update
user_id = permitted_update_params[:user_id]
@user_assignment = @experiment.user_assignments.find_by(user_id: user_id, team: current_team)
if permitted_params[:user_role_id] == 'reset'
parent_assignment = @project.public_send(:"#{assignment_type}_assignments").find_or_initialize_by(
"#{assignment_type}_id": permitted_params[:"#{assignment_type}_id"] || current_team.id,
team: current_team
)
if permitted_update_params[:user_role_id] == 'reset'
@user_assignment.update!(
user_role_id: @project.user_assignments.find_by(user_id: user_id, team: current_team).user_role_id,
@assignment.update!(
user_role_id: parent_assignment.user_role_id,
assigned_by: current_user,
assigned: :automatically
)
else
@user_assignment.update!(
user_role_id: permitted_update_params[:user_role_id],
@assignment.update!(
user_role_id: permitted_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
end
UserAssignments::PropagateAssignmentJob.perform_later(
@experiment,
@user_assignment.user.id,
@user_assignment.user_role,
current_user.id
)
UserAssignments::PropagateAssignmentJob.perform_later(@assignment)
log_change_activity
case assignment_type
when :team
log_activity(:experiment_access_changed_all_team_members, team: @assignment.team.id, role: @assignment.user_role.name)
when :user_group
log_activity(:experiment_access_changed_user_group, user_group: @assignment.user_group.id, role: @assignment.user_role.name)
when :user
log_activity(:change_user_role_on_experiment, user_target: @assignment.user.id, role: @assignment.user_role.name)
end
render json: {}, status: :ok
render json: { user_role_id: @assignment.user_role_id }, status: :ok
end
private
def permitted_update_params
params.require(:user_assignment)
.permit(%i(user_role_id user_id))
end
def set_project
@project = @experiment.project
@project = @model.project
end
def set_experiment
@experiment = Experiment.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
def set_model
@model = Experiment.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @experiment
render_404 unless @model
end
def check_manage_permissions
render_403 unless can_manage_experiment_users?(@experiment)
render_403 unless can_manage_experiment_users?(@model)
end
def check_read_permissions
render_403 unless can_read_experiment?(@experiment)
end
def log_change_activity
Activities::CreateActivityService.call(
activity_type: :change_user_role_on_experiment,
owner: current_user,
subject: @experiment,
team: @project.team,
project: @project,
message_items: {
experiment: @experiment.id,
user_target: @user_assignment.user_id,
role: @user_assignment.user_role.name
}
)
render_403 unless can_read_experiment?(@model)
end
end
end

View file

@ -1,186 +1,21 @@
# frozen_string_literal: true
module AccessPermissions
class FormsController < ApplicationController
include InputSanitizeHelper
before_action :set_form
before_action :check_read_permissions, only: %i(show)
before_action :check_manage_permissions, except: %i(show)
before_action :available_users, only: %i(new create)
def show
render json: @form.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer, user: current_user
end
def new
render json: @available_users, each_serializer: UserSerializer, user: current_user
end
def edit; end
def create
ActiveRecord::Base.transaction do
created_count = 0
if permitted_create_params[:user_id] == 'all'
@form.update!(visibility: :visible, default_public_user_role_id: permitted_create_params[:user_role_id])
log_activity(:form_access_granted_all_team_members,
{ team: @form.team.id, role: @form.default_public_user_role&.name })
else
user_assignment = UserAssignment.find_or_initialize_by(
assignable: @form,
user_id: permitted_create_params[:user_id],
team: current_team
)
user_assignment.update!(
user_role_id: permitted_create_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
log_activity(:form_access_granted, { user_target: user_assignment.user.id,
role: user_assignment.user_role.name })
created_count += 1
end
@message = if created_count.zero?
t('access_permissions.create.success', member_name: t('access_permissions.all_team'))
else
t('access_permissions.create.success', member_name: escape_input(user_assignment.user.name))
end
render json: { message: @message }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
errors = @form.errors.present? ? @form.errors&.map(&:message)&.join(',') : e.message
render json: { flash: errors }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
def update
@user_assignment = @form.user_assignments.find_by(
user_id: permitted_update_params[:user_id],
team: current_team
)
# prevent role change if it would result in no manually assigned users having the user management permission
new_user_role = UserRole.find(permitted_update_params[:user_role_id])
if !new_user_role.has_permission?(FormPermissions::USERS_MANAGE) &&
@user_assignment.last_with_permission?(FormPermissions::USERS_MANAGE, assigned: :manually)
raise ActiveRecord::RecordInvalid
end
@user_assignment.update!(permitted_update_params)
log_activity(:form_access_changed, { user_target: @user_assignment.user.id,
role: @user_assignment.user_role.name })
render :form_member
rescue ActiveRecord::RecordInvalid
render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity
end
def destroy
user = @form.assigned_users.find(params[:user_id])
user_assignment = @form.user_assignments.find_by(user: user, team: current_team)
# prevent deletion of last manually assigned user that can manage users
raise ActiveRecord::RecordInvalid if user_assignment.last_with_permission?(FormPermissions::USERS_MANAGE, assigned: :manually)
Protocol.transaction do
if @form.visible?
user_assignment.update!(
user_role: @form.default_public_user_role,
assigned: :automatically
)
else
user_assignment.destroy!
end
log_activity(:form_access_revoked, { user_target: user_assignment.user.id,
role: user_assignment.user_role.name })
end
render json: { message: t('access_permissions.destroy.success', member_name: user.full_name) }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
render json: { message: t('access_permissions.destroy.failure') }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
def update_default_public_user_role
ActiveRecord::Base.transaction do
current_role = @form.default_public_user_role.name
@form.update!(permitted_default_public_user_role_params)
# revoke all team members access
if permitted_default_public_user_role_params[:default_public_user_role_id].blank?
log_activity(:form_access_revoked_all_team_members,
{ team: @form.team.id, role: current_role })
render json: { flash: t('access_permissions.update.revoke_all_team_members') }, status: :ok
else
# update all team members access
log_activity(:form_access_changed_all_team_members,
{ team: @form.team.id, role: @form.default_public_user_role&.name })
end
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
render json: { flash: @form.errors&.map(&:message)&.join(',') }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
class FormsController < BaseController
private
def permitted_default_public_user_role_params
params.require(:object).permit(:default_public_user_role_id)
end
def set_model
@model = current_team.forms.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
def permitted_update_params
params.require(:user_assignment)
.permit(%i(user_role_id user_id))
end
def permitted_create_params
params.require(:user_assignment)
.permit(%i(user_id user_role_id))
end
def available_users
# automatically assigned or not assigned to project
@available_users = current_team.users.where(
id: @form.user_assignments.automatically_assigned.select(:user_id)
).or(
current_team.users.where.not(id: @form.users.select(:id))
).order('users.full_name ASC')
end
def set_form
@form = current_team.forms.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
return render_404 unless @form
@form = @form.parent if @form.parent_id
render_404 unless @model
end
def check_manage_permissions
render_403 unless can_manage_form_users?(@form)
render_403 unless can_manage_form_users?(@model)
end
def check_read_permissions
render_403 unless can_read_form?(@form) || can_manage_team?(@form.team)
end
def log_activity(type_of, message_items = {})
message_items = { form: @form.id }.merge(message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: @form,
team: @form.team,
project: nil,
message_items: message_items)
render_403 unless can_read_form?(@model) || can_manage_team?(@model.team)
end
end
end

View file

@ -1,86 +1,64 @@
# frozen_string_literal: true
module AccessPermissions
class MyModulesController < ApplicationController
before_action :set_my_module
class MyModulesController < BaseController
before_action :set_experiment
before_action :set_project
before_action :check_read_permissions, only: %i(show)
before_action :check_manage_permissions, only: %i(edit update)
def show
render json: @my_module.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer, user: current_user
end
def new
render json: @available_users, each_serializer: UserSerializer, user: current_user
end
def edit; end
def update
user_id = permitted_update_params[:user_id]
@user_assignment = @my_module.user_assignments.find_by(user_id: user_id, team: current_team)
if permitted_params[:user_role_id] == 'reset'
parent_assignment = @experiment.public_send(:"#{assignment_type}_assignments").find_or_initialize_by(
"#{assignment_type}_id": permitted_params[:"#{assignment_type}_id"] || current_team.id,
team: current_team
)
if permitted_update_params[:user_role_id] == 'reset'
@user_assignment.update!(
user_role_id: @experiment.user_assignments.find_by(user_id: user_id, team: current_team).user_role_id,
@assignment.update!(
user_role_id: parent_assignment.user_role_id,
assigned_by: current_user,
assigned: :automatically
)
else
@user_assignment.update!(
user_role_id: permitted_update_params[:user_role_id],
@assignment.update!(
user_role_id: permitted_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
end
log_change_activity
case assignment_type
when :team
log_activity(:my_module_access_changed_all_team_members, team: @assignment.team.id, role: @assignment.user_role.name)
when :user_group
log_activity(:my_module_access_changed_user_group, user_group: @assignment.user_group.id, role: @assignment.user_role.name)
when :user
log_activity(:change_user_role_on_my_module, user_target: @assignment.user.id, role: @assignment.user_role.name)
end
render :my_module_member
render json: { user_role_id: @assignment.user_role_id }, status: :ok
end
private
def permitted_update_params
params.require(:user_assignment)
.permit(%i(user_role_id user_id))
end
def set_project
@project = @experiment.project
end
def set_experiment
@experiment = @my_module.experiment
@experiment = @model.experiment
end
def set_my_module
@my_module = MyModule.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
def set_model
@model = MyModule.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @my_module
render_404 unless @model
end
def check_manage_permissions
render_403 unless can_manage_my_module_users?(@my_module)
render_403 unless can_manage_my_module_users?(@model)
end
def check_read_permissions
render_403 unless can_read_my_module?(@my_module)
end
def log_change_activity
Activities::CreateActivityService.call(
activity_type: :change_user_role_on_my_module,
owner: current_user,
subject: @my_module,
team: @project.team,
project: @project,
message_items: {
my_module: @my_module.id,
user_target: @user_assignment.user_id,
role: @user_assignment.user_role.name
}
)
render_403 unless can_read_my_module?(@model)
end
end
end

View file

@ -1,206 +1,36 @@
# frozen_string_literal: true
module AccessPermissions
class ProjectsController < ApplicationController
include InputSanitizeHelper
before_action :set_project
before_action :check_read_permissions, only: %i(show)
before_action :check_manage_permissions, except: %i(show)
before_action :available_users, only: %i(new create)
def show
render json: @project.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer, user: current_user
end
def new
render json: @available_users, each_serializer: UserSerializer, user: current_user
end
def edit; end
def create
ActiveRecord::Base.transaction do
created_count = 0
if permitted_create_params[:user_id] == 'all'
@project.update!(visibility: :visible, default_public_user_role_id: permitted_create_params[:user_role_id])
log_activity(:project_grant_access_to_all_team_members,
{ visibility: t('projects.activity.visibility_visible'),
role: @project.default_public_user_role.name,
team: @project.team.id })
else
user_assignment = UserAssignment.find_or_initialize_by(
assignable: @project,
user_id: permitted_create_params[:user_id],
team: current_team
)
user_assignment.update!(
user_role_id: permitted_create_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
log_activity(:assign_user_to_project, { user_target: user_assignment.user.id,
role: user_assignment.user_role.name })
created_count += 1
propagate_job(user_assignment)
end
@message = if created_count.zero?
t('access_permissions.create.success', member_name: t('access_permissions.all_team'))
else
t('access_permissions.create.success', member_name: escape_input(user_assignment.user.name))
end
render json: { message: @message }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
errors = @project.errors.present? ? @project.errors&.map(&:message)&.join(',') : e.message
render json: { flash: errors }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
def update
@user_assignment = @project.user_assignments.find_by(
user_id: permitted_update_params[:user_id],
team: current_team
)
# prevent role change if it would result in no manually assigned users having the user management permission
new_user_role = UserRole.find(permitted_update_params[:user_role_id])
if !new_user_role.has_permission?(ProjectPermissions::USERS_MANAGE) &&
@user_assignment.last_with_permission?(ProjectPermissions::USERS_MANAGE, assigned: :manually)
raise ActiveRecord::RecordInvalid
end
@user_assignment.update!(permitted_update_params)
log_activity(:change_user_role_on_project, { user_target: @user_assignment.user.id,
role: @user_assignment.user_role.name })
propagate_job(@user_assignment)
render :project_member
rescue ActiveRecord::RecordInvalid
render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity
end
def destroy
user = @project.assigned_users.find(params[:user_id])
user_assignment = @project.user_assignments.find_by(user: user, team: current_team)
# prevent deletion of last manually assigned user that can manage users
if user_assignment.last_with_permission?(ProjectPermissions::USERS_MANAGE, assigned: :manually)
raise ActiveRecord::RecordInvalid
end
UserAssignments::PropagateAssignmentJob.perform_now(
@project,
user_assignment.user.id,
user_assignment.user_role,
current_user.id,
destroy: true
)
log_activity(:unassign_user_from_project, { user_target: user_assignment.user.id,
role: user_assignment.user_role.name })
render json: { message: t('access_permissions.destroy.success', member_name: escape_input(user.full_name)) }
rescue ActiveRecord::RecordInvalid
render json: { message: t('access_permissions.destroy.failure') },
status: :unprocessable_entity
end
def update_default_public_user_role
Project.transaction do
@project.visibility_will_change!
@project.last_modified_by = current_user
if permitted_default_public_user_role_params[:default_public_user_role_id].blank?
# revoke all team members access
@project.visibility = :hidden
previous_user_role_name = @project.default_public_user_role.name
@project.default_public_user_role_id = nil
@project.save!
log_activity(:project_remove_access_from_all_team_members,
{ visibility: t('projects.activity.visibility_hidden'),
role: previous_user_role_name,
team: @project.team.id })
render json: { message: t('access_permissions.update.revoke_all_team_members') }
else
# update all team members access
@project.visibility = :visible
@project.assign_attributes(permitted_default_public_user_role_params)
@project.save!
log_activity(:project_access_changed_all_team_members,
{ team: @project.team.id, role: @project.default_public_user_role&.name })
end
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
render json: { flash: @project.errors&.map(&:message)&.join(',') }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
class ProjectsController < AccessPermissions::BaseController
# legacy activity map
ACTIVITY_TYPE_MAP = {
project_access_granted: :assign_user_to_project,
project_access_changed: :change_user_role_on_project,
project_access_revoked: :unassign_user_from_project,
project_access_granted_all_team_members: :project_grant_access_to_all_team_members,
project_access_changed_all_team_members: :project_access_changed_all_team_members,
project_access_revoked_all_team_members: :project_remove_access_from_all_team_members
}.freeze
private
def permitted_default_public_user_role_params
params.require(:object).permit(:default_public_user_role_id)
end
def set_model
@model = current_team.projects.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
@project = @model
def permitted_update_params
params.require(:user_assignment)
.permit(%i(user_role_id user_id))
end
def permitted_create_params
params.require(:user_assignment)
.permit(%i(user_id user_role_id))
end
def set_project
@project = current_team.projects.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @project
end
def propagate_job(user_assignment, destroy: false)
UserAssignments::PropagateAssignmentJob.perform_later(
@project,
user_assignment.user.id,
user_assignment.user_role,
current_user.id,
destroy: destroy
)
render_404 unless @model
end
def check_manage_permissions
render_403 unless can_manage_project_users?(@project)
render_403 unless can_manage_project_users?(@model)
end
def check_read_permissions
render_403 unless can_read_project_users?(@project)
end
def available_users
# automatically assigned or not assigned to project
@available_users = current_team.users.where(
id: @project.user_assignments.automatically_assigned.select(:user_id)
).or(
current_team.users.where.not(id: @project.users.select(:id))
).order('users.full_name ASC')
render_403 unless can_read_project_users?(@model)
end
def log_activity(type_of, message_items = {})
message_items = { project: @project.id }.merge(message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: @project,
team: @project.team,
project: @project,
message_items: message_items)
super(ACTIVITY_TYPE_MAP[type_of] || type_of, message_items)
end
end
end

View file

@ -1,188 +1,41 @@
# frozen_string_literal: true
module AccessPermissions
class ProtocolsController < ApplicationController
include InputSanitizeHelper
before_action :set_protocol
before_action :check_read_permissions, only: %i(show)
before_action :check_manage_permissions, except: %i(show)
before_action :available_users, only: %i(new create)
def show
render json: @protocol.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer, user: current_user
end
def new
render json: @available_users, each_serializer: UserSerializer, user: current_user
end
def edit; end
def create
ActiveRecord::Base.transaction do
created_count = 0
if permitted_create_params[:user_id] == 'all'
@protocol.update!(visibility: :visible, default_public_user_role_id: permitted_create_params[:user_role_id])
log_activity(:protocol_template_access_granted_all_team_members,
{ team: @protocol.team.id, role: @protocol.default_public_user_role&.name })
else
user_assignment = UserAssignment.find_or_initialize_by(
assignable: @protocol,
user_id: permitted_create_params[:user_id],
team: current_team
)
user_assignment.update!(
user_role_id: permitted_create_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
log_activity(:protocol_template_access_granted, { user_target: user_assignment.user.id,
role: user_assignment.user_role.name })
created_count += 1
end
@message = if created_count.zero?
t('access_permissions.create.success', member_name: t('access_permissions.all_team'))
else
t('access_permissions.create.success', member_name: escape_input(user_assignment.user.name))
end
render json: { message: @message }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
errors = @protocol.errors.present? ? @protocol.errors&.map(&:message)&.join(',') : e.message
render json: { flash: errors }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
def update
@user_assignment = @protocol.user_assignments.find_by(
user_id: permitted_update_params[:user_id],
team: current_team
)
# prevent role change if it would result in no manually assigned users having the user management permission
new_user_role = UserRole.find(permitted_update_params[:user_role_id])
if !new_user_role.has_permission?(ProtocolPermissions::USERS_MANAGE) &&
@user_assignment.last_with_permission?(ProtocolPermissions::USERS_MANAGE, assigned: :manually)
raise ActiveRecord::RecordInvalid
end
@user_assignment.update!(permitted_update_params)
log_activity(:protocol_template_access_changed, { user_target: @user_assignment.user.id,
role: @user_assignment.user_role.name })
render :protocol_member
rescue ActiveRecord::RecordInvalid
render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity
end
def destroy
user = @protocol.assigned_users.find(params[:user_id])
user_assignment = @protocol.user_assignments.find_by(user: user, team: current_team)
# prevent deletion of last manually assigned user that can manage users
if user_assignment.last_with_permission?(ProtocolPermissions::USERS_MANAGE, assigned: :manually)
raise ActiveRecord::RecordInvalid
end
Protocol.transaction do
if @protocol.visible?
user_assignment.update!(
user_role: @protocol.default_public_user_role,
assigned: :automatically
)
else
user_assignment.destroy!
end
log_activity(:protocol_template_access_revoked, { user_target: user_assignment.user.id,
role: user_assignment.user_role.name })
end
render json: { message: t('access_permissions.destroy.success', member_name: user.full_name) }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
render json: { message: t('access_permissions.destroy.failure') }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
def update_default_public_user_role
ActiveRecord::Base.transaction do
current_role = @protocol.default_public_user_role.name
@protocol.update!(permitted_default_public_user_role_params)
# revoke all team members access
if permitted_default_public_user_role_params[:default_public_user_role_id].blank?
log_activity(:protocol_template_access_revoked_all_team_members,
{ team: @protocol.team.id, role: current_role })
render json: { flash: t('access_permissions.update.revoke_all_team_members') }, status: :ok
else
# update all team members access
log_activity(:protocol_template_access_changed_all_team_members,
{ team: @protocol.team.id, role: @protocol.default_public_user_role&.name })
end
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
render json: { flash: @protocol&.errors&.map(&:message)&.join(',') }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
class ProtocolsController < BaseController
# protocol template activity naming is inconsistent with model name (model_parameter in BaseController),
# so we need to map them for now
ACTIVITY_TYPE_MAP = {
protocol_access_granted_all_team_members: :protocol_template_access_granted_all_team_members,
protocol_access_revoked_all_team_members: :protocol_template_access_revoked_all_team_members,
protocol_access_changed_all_team_members: :protocol_template_access_changed_all_team_members,
protocol_access_granted: :protocol_template_access_granted,
protocol_access_revoked: :protocol_template_access_revoked,
protocol_access_changed: :protocol_template_access_changed,
protocol_access_granted_user_group: :protocol_template_access_granted_user_group,
protocol_access_revoked_user_group: :protocol_template_access_revoked_user_group,
protocol_access_changed_user_group: :protocol_template_access_changed_user_group
}.freeze
private
def permitted_default_public_user_role_params
params.require(:object).permit(:default_public_user_role_id)
end
def set_model
@model = current_team.protocols.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
def permitted_update_params
params.require(:user_assignment)
.permit(%i(user_role_id user_id))
end
return render_404 unless @model
def permitted_create_params
params.require(:user_assignment)
.permit(%i(user_id user_role_id))
end
def available_users
# automatically assigned or not assigned to project
@available_users = current_team.users.where(
id: @protocol.user_assignments.automatically_assigned.select(:user_id)
).or(
current_team.users.where.not(id: @protocol.users.select(:id))
).order('users.full_name ASC')
end
def set_protocol
@protocol = current_team.protocols.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
return render_404 unless @protocol
@protocol = @protocol.parent if @protocol.parent_id
@model = @model.parent if @model.parent_id
end
def check_manage_permissions
render_403 unless can_manage_protocol_users?(@protocol)
render_403 unless can_manage_protocol_users?(@model)
end
def check_read_permissions
render_403 unless can_read_protocol_in_repository?(@protocol) || can_manage_team?(@protocol.team)
render_403 unless can_read_protocol_in_repository?(@model) || can_manage_team?(@model.team)
end
def log_activity(type_of, message_items = {})
message_items = { protocol: @protocol.id }.merge(message_items)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: @protocol,
team: @protocol.team,
project: nil,
message_items: message_items)
super(ACTIVITY_TYPE_MAP[type_of] || type_of, message_items)
end
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module AccessPermissions
class RepositoriesController < BaseController
private
def set_model
@model = Repository.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @model
end
def check_manage_permissions
render_403 unless can_manage_repository_users?(@model)
end
def check_read_permissions
render_403 unless can_manage_repository_users?(@model) || can_read_repository?(@model)
end
end
end

View file

@ -35,12 +35,7 @@ module Api
@user_assignment.update!(user_assignment_params.merge(assigned: :manually))
UserAssignments::PropagateAssignmentJob.perform_later(
@experiment,
@user_assignment.user_id,
@user_assignment.user_role,
current_user.id
)
UserAssignments::PropagateAssignmentJob.perform_later(@user_assignment)
log_change_activity

View file

@ -13,6 +13,7 @@ module Api
def index
inventories =
timestamps_filter(@team.repositories).active
.readable_by_user(current_user)
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))

View file

@ -110,10 +110,7 @@ module Api
def propagate_job(user_assignment, destroy: false)
UserAssignments::PropagateAssignmentJob.perform_later(
@project,
user_assignment.user.id,
user_assignment.user_role,
current_user.id,
user_assignment,
destroy: destroy
)
end

View file

@ -11,7 +11,13 @@ module Api
before_action :load_project_for_managing, only: %i(update)
def index
projects = @team.projects.visible_to(current_user, @team)
projects =
if can_manage_team?(@team)
# Team owners see all projects in the team
@team.projects
else
@team.projects.readable_by_user(current_user, @team)
end
projects = metadata_filter(timestamps_filter(archived_filter(projects)))
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
@ -26,32 +32,57 @@ module Api
def create
raise PermissionError.new(Project, :create) unless can_create_projects?(@team)
project = @team.projects.build(project_params.merge!(created_by: current_user))
ActiveRecord::Base.transaction do
project = @team.projects.build(project_params.merge!(created_by: current_user))
if project.visible? # set default viewer role for public projects
project.default_public_user_role = UserRole.predefined.find_by(name: I18n.t('user_roles.predefined.viewer'))
project.save!
if project_params[:visibility] == 'visible'
project.team_assignments.create!(
team: project.team,
user_role: UserRole.find_predefined_viewer_role,
assigned_by: current_user,
assigned: :manually
)
end
render jsonapi: project, serializer: ProjectSerializer, scope: { metadata: params['with-metadata'] == 'true' }, status: :created
end
project.save!
render jsonapi: project, serializer: ProjectSerializer, scope: { metadata: params['with-metadata'] == 'true' }, status: :created
end
def update
@project.assign_attributes(project_params)
return render body: nil, status: :no_content unless @project.changed?
return render body: nil, status: :no_content if !@project.changed? && project_params[:visibility].blank?
if @project.archived_changed?
if @project.archived?
@project.archived_by = current_user
else
@project.restored_by = current_user
ActiveRecord::Base.transaction do
if @project.archived_changed?
if @project.archived?
@project.archived_by = current_user
else
@project.restored_by = current_user
end
end
@project.last_modified_by = current_user
@project.save!
if project_params[:visibility].present?
team_assignment = @project.team_assignments.find_by(team: @team)
if project_params[:visibility] == 'hidden' && team_assignment.present?
team_assignment.destroy!
elsif project_params[:visibility] == 'visible' && team_assignment.blank?
@project.team_assignments.create!(
team: @project.team,
user_role: UserRole.find_predefined_viewer_role,
assigned_by: current_user,
assigned: :manually
)
end
end
render jsonapi: @project, serializer: ProjectSerializer, scope: { metadata: params['with-metadata'] == 'true' }, status: :ok
end
@project.last_modified_by = current_user
@project.save!
render jsonapi: @project, serializer: ProjectSerializer, scope: { metadata: params['with-metadata'] == 'true' }, status: :ok
end
def activities

View file

@ -11,13 +11,11 @@ module Api
end
def index
protocol_templates =
timestamps_filter(
Protocol.latest_available_versions(@team)
)
.viewable_by_user(current_user, @team)
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
protocol_templates = timestamps_filter(Protocol.latest_available_versions(@team))
# Team owners see all protocol templates in the team
protocol_templates = protocol_templates.readable_by_user(current_user, @team) unless can_manage_team?(@team)
protocol_templates = protocol_templates.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
render jsonapi: protocol_templates,
each_serializer: ProtocolTemplateSerializer, rte_rendering: render_rte?, team: @team

View file

@ -7,16 +7,18 @@ module Api
before_action :load_project
before_action :load_experiment
before_action :load_task
before_action :load_my_module_repository_row, only: :update
before_action :load_inventory_item, only: %i(show destroy update)
before_action :load_task_inventory_item, only: %i(update destroy)
before_action :check_repository_view_permissions, only: :show
before_action :check_stock_consumption_update_permissions, only: :update
before_action :check_task_assign_permissions, only: %i(create destroy)
def index
items =
timestamps_filter(@task.repository_rows).includes(repository_cells: :repository_column)
.preload(repository_cells: :value)
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
items = @task.repository_rows.where(repository_id: Repository.readable_by_user(current_user).select(:id))
items = timestamps_filter(items).includes(repository_cells: :repository_column)
.preload(repository_cells: :value)
.page(params.dig(:page, :number))
.per(params.dig(:page, :size))
render jsonapi: items,
each_serializer: TaskInventoryItemSerializer,
show_repository: true,
@ -25,7 +27,7 @@ module Api
end
def show
render jsonapi: @task.repository_rows.find(params.require(:id)),
render jsonapi: @inventory_item,
serializer: TaskInventoryItemSerializer,
show_repository: true,
my_module: @task,
@ -39,21 +41,21 @@ module Api
@task.my_module_repository_rows.create!(repository_row: @inventory_item, assigned_by: current_user)
render jsonapi: @task.repository_rows,
each_serializer: TaskInventoryItemSerializer,
render jsonapi: @inventory_item,
serializer: TaskInventoryItemSerializer,
show_repository: true,
my_module: @task,
include: include_params
end
def update
@my_module_repository_row.consume_stock(
@task_inventory_item.consume_stock(
current_user,
repository_row_params[:attributes][:stock_consumption],
repository_row_params[:attributes][:stock_consumption_comment]
)
render jsonapi: @my_module_repository_row.repository_row,
render jsonapi: @inventory_item,
serializer: TaskInventoryItemSerializer,
show_repository: true,
my_module: @task,
@ -61,29 +63,27 @@ module Api
end
def destroy
@inventory_item = @task.repository_rows.find(params.require(:id))
raise PermissionError.new(Repository, :read) unless @inventory_item && can_read_repository?(@inventory_item.repository)
@task.my_module_repository_rows.find_by(repository_row: @inventory_item).destroy!
@task_inventory_item.destroy!
render body: nil
end
private
def load_my_module_repository_row
@my_module_repository_row = @task.repository_rows
.find(params.require(:id))
.my_module_repository_rows
.find_by(my_module: @task)
def load_inventory_item
@inventory_item = @task.repository_rows.find(params.require(:id))
end
def load_task_inventory_item
@task_inventory_item = @task.my_module_repository_rows.find_by!(repository_row: @inventory_item)
end
def check_repository_view_permissions
raise PermissionError.new(RepositoryRow, :read_repository) unless can_read_repository?(@inventory_item.repository)
end
def check_stock_consumption_update_permissions
unless can_update_my_module_stock_consumption?(@task) &&
can_manage_repository_rows?(@my_module_repository_row.repository_row.repository)
raise PermissionError.new(RepositoryRow, :update_stock_consumption)
end
raise PermissionError.new(RepositoryRow, :update_stock_consumption) if @inventory_item.archived? || !can_update_my_module_stock_consumption?(@task)
end
def check_task_assign_permissions

View file

@ -21,7 +21,7 @@ module Api
end
def create
inventory_item_to_link = RepositoryRow.where(repository: Repository.viewable_by_user(current_user, @team))
inventory_item_to_link = RepositoryRow.where(repository: Repository.readable_by_user(current_user, @team))
.find(connection_params[:child_id])
child_connection = @inventory_item.child_connections.create!(
child: inventory_item_to_link,

View file

@ -21,7 +21,7 @@ module Api
end
def create
inventory_item_to_link = RepositoryRow.where(repository: Repository.viewable_by_user(current_user, @team))
inventory_item_to_link = RepositoryRow.where(repository: Repository.readable_by_user(current_user, @team))
.find(connection_params[:parent_id])
parent_connection = @inventory_item.parent_connections.create!(
parent: inventory_item_to_link,

View file

@ -27,7 +27,7 @@ class AtWhoController < ApplicationController
if params[:repository_id].present?
Repository.find_by(id: params[:repository_id])
else
Repository.active.viewable_by_user(current_user, @team).first
Repository.active.readable_by_user(current_user, @team).first
end
items = []
@ -36,7 +36,7 @@ class AtWhoController < ApplicationController
if repository && can_read_repository?(repository)
assignable_my_module =
if params[:assignable_my_module_id].present?
MyModule.viewable_by_user(current_user, @team).find_by(id: params[:assignable_my_module_id])
MyModule.readable_by_user(current_user, @team).find_by(id: params[:assignable_my_module_id])
end
items = SmartAnnotation.new(current_user, current_team, @query)
.repository_rows(repository, assignable_my_module&.id)
@ -54,7 +54,7 @@ class AtWhoController < ApplicationController
end
def menu
repositories = Repository.active.viewable_by_user(current_user, @team)
repositories = Repository.active.readable_by_user(current_user, @team)
render json: {
html: render_to_string(partial: 'shared/smart_annotation/menu',
locals: { repositories: repositories },

View file

@ -48,7 +48,7 @@ module StepsActions
smart_annotation_notification(
old_text: old_text,
new_text: checklist_item.text,
subject: step.protocol,
subject: step,
title: t('notifications.checklist_title',
user: current_user.full_name,
step: step.name),
@ -60,7 +60,7 @@ module StepsActions
smart_annotation_notification(
old_text: old_text,
new_text: step_text.text,
subject: step.protocol,
subject: step,
title: t('notifications.step_text_title',
user: current_user.full_name,
step: step.name),
@ -72,7 +72,7 @@ module StepsActions
smart_annotation_notification(
old_text: old_text,
new_text: checklist.name,
subject: step.protocol,
subject: step,
title: t('notifications.checklist_title',
user: current_user.full_name,
step: step.name),
@ -84,7 +84,7 @@ module StepsActions
smart_annotation_notification(
old_text: old_text,
new_text: step.description,
subject: step.protocol,
subject: step,
title: t('notifications.step_description_title',
user: current_user.full_name,
step: step.name),
@ -96,7 +96,7 @@ module StepsActions
smart_annotation_notification(
old_text: old_content,
new_text: table.contents,
subject: step.protocol,
subject: step,
title: t(table.metadata['plateTemplate'] ? 'notifications.step_well_plate_title' : 'notifications.step_table_title',
user: current_user.full_name,
step: step.name),

View file

@ -30,7 +30,7 @@ module Dashboard
def project_filter
projects = current_team.projects
.where(archived: false)
.viewable_by_user(current_user, current_team)
.readable_by_user(current_user, current_team)
.search_by_name(current_user, current_team, params[:query]).select(:id, :name)
unless params[:mode] == 'team'
@ -47,7 +47,7 @@ module Dashboard
end
experiments = @project.experiments
.where(archived: false)
.viewable_by_user(current_user, current_team)
.readable_by_user(current_user, current_team)
.search_by_name(current_user, current_team, params[:query]).select(:id, :name)
unless params[:mode] == 'team'
@ -103,7 +103,7 @@ module Dashboard
MyModule.active
end
tasks = tasks.viewable_by_user(current_user, current_team)
tasks = tasks.readable_by_user(current_user, current_team)
tasks = tasks.joins(experiment: :project)
.where(experiments: { archived: false })

View file

@ -60,7 +60,7 @@ module Dashboard
end
def create_project_params
params.require(:project).permit(:name, :visibility, :default_public_user_role_id)
params.require(:project).permit(:name)
end
def create_experiment_params

View file

@ -47,7 +47,7 @@ class ExperimentsController < ApplicationController
end
def assigned_users
render json: User.where(id: @experiment.user_assignments.select(:user_id)),
render json: @experiment.users,
each_serializer: UserSerializer,
user: current_user
end
@ -72,10 +72,13 @@ class ExperimentsController < ApplicationController
def canvas
@project = @experiment.project
@active_modules = unless @experiment.archived_branch?
@experiment.my_modules.active.order(:name)
@experiment.my_modules
.active
.readable_by_user(current_user)
.left_outer_joins(:designated_users, :task_comments)
.preload(:tags, outputs: :to)
.preload(:my_module_status, :my_module_group, user_assignments: %i(user user_role))
.order(:name)
.select('COUNT(DISTINCT users.id) as designated_users_count')
.select('COUNT(DISTINCT comments.id) as task_comments_count')
.select('my_modules.*').group(:id)
@ -202,7 +205,7 @@ class ExperimentsController < ApplicationController
def projects_to_clone
projects = @experiment.project.team.projects.active
.with_user_permission(current_user, ProjectPermissions::EXPERIMENTS_CREATE)
.with_granted_permissions(current_user, ProjectPermissions::EXPERIMENTS_CREATE)
.where('trim_html_tags(projects.name) ILIKE ?',
"%#{ActiveRecord::Base.sanitize_sql_like(params['query'])}%")
.map { |p| [p.id, p.name] }
@ -355,10 +358,10 @@ class ExperimentsController < ApplicationController
end
def inventory_assigning_experiment_filter
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
viewable_experiments = Experiment.readable_by_user(current_user, current_team)
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
project = Project.viewable_by_user(current_user, current_team)
project = Project.readable_by_user(current_user, current_team)
.joins(experiments: :my_modules)
.where(experiments: { id: viewable_experiments })
.where(my_modules: { id: assignable_my_modules })

View file

@ -118,7 +118,7 @@ class ExternalProtocolsController < ApplicationController
def create_protocol_params
params
.require(:protocol)
.permit(:name, :authors, :published_on, :protocol_type, :description, :visibility, :default_public_user_role_id)
.permit(:name, :authors, :published_on, :protocol_type, :description)
.except(:steps)
end

View file

@ -18,7 +18,7 @@ class FormFieldValuesController < ApplicationController
log_form_field_value_create_activity
form_field_value_annotation if @form_field_value.is_a?(FormTextFieldValue)
render json: @form_field_value, serializer: FormFieldValueSerializer, user: current_user
render json: @form_field_value, serializer: FormFieldValueSerializer, scope: { user: current_user }
end
private
@ -52,7 +52,7 @@ class FormFieldValuesController < ApplicationController
smart_annotation_notification(
old_text: @form_field_value.text_previously_was,
new_text: @form_field_value.text,
subject: step.protocol,
subject: step,
title: t('notifications.form_field_value_title',
user: current_user.full_name,
field: @form_field_value.form_field.name,

View file

@ -2,9 +2,9 @@
class FormsController < ApplicationController
include InputSanitizeHelper
include UserRolesHelper
before_action :check_forms_enabled
before_action :load_forms, only: %i(index actions_toolbar)
before_action :load_form, only: %i(show update publish unpublish export_form_responses duplicate)
before_action :set_breadcrumbs_items, only: %i(index show)
before_action :check_manage_permissions, only: :update
@ -14,11 +14,11 @@ class FormsController < ApplicationController
respond_to do |format|
format.html
format.json do
forms = Lists::FormsService.new(current_user, current_team, params).call
render json: forms,
forms_list = Lists::FormsService.new(@forms, params, user: current_user).call
render json: forms_list,
each_serializer: Lists::FormSerializer,
user: current_user,
meta: pagination_dict(forms)
meta: pagination_dict(forms_list)
end
end
end
@ -201,19 +201,16 @@ class FormsController < ApplicationController
end
def actions_toolbar
selected_forms = @forms.where(id: JSON.parse(params[:items]).pluck('id'))
render json: {
actions:
Toolbars::FormsService.new(
current_user,
form_ids: JSON.parse(params[:items]).map { |i| i['id'] }
selected_forms,
current_user
).actions
}
end
def user_roles
render json: { data: user_roles_collection(Form.new).map(&:reverse) }
end
private
def set_breadcrumbs_items
@ -235,6 +232,12 @@ class FormsController < ApplicationController
end
end
def load_forms
# Team owners see all forms in the team
@forms = current_team.forms
@forms = @forms.readable_by_user(current_user) unless can_manage_team?(current_team)
end
def load_form
@form = current_team.forms.readable_by_user(current_user).find_by(id: params[:id])

View file

@ -15,7 +15,7 @@ class HiddenRepositoryCellRemindersController < ApplicationController
private
def load_repository
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id])
@repository = Repository.readable_by_user(current_user).find_by(id: params[:repository_id])
render_404 unless @repository
end

View file

@ -5,7 +5,7 @@ class LabelTemplatesController < ApplicationController
include TeamsHelper
before_action :check_feature_enabled, except: %i(index zpl_preview list)
before_action :load_label_templates, only: %i(index datatable list)
before_action :load_label_templates, only: %i(index list)
before_action :load_label_template, only: %i(show set_default update template_tags)
before_action :check_view_permissions, except: %i(create duplicate set_default delete update)
before_action :check_manage_permissions, only: %i(create duplicate set_default delete update)

View file

@ -6,7 +6,7 @@ class MyModuleRepositoriesController < ApplicationController
before_action :load_my_module, except: :assign_my_modules
before_action :load_repository, except: %i(repositories_dropdown_list repositories_list_html repositories_list create)
before_action :check_my_module_view_permissions, except: %i(update consume_modal update_consumption assign_my_modules)
before_action :check_repository_view_permissions, except: %i(repositories_dropdown_list repositories_list_html repositories_list create)
before_action :check_repository_view_permissions, except: %i(index_dt repositories_dropdown_list repositories_list_html repositories_list create)
before_action :check_repository_row_consumption_permissions, only: %i(consume_modal update_consumption)
before_action :check_assign_repository_records_permissions, only: %i(update create)
before_action :load_my_modules, only: :assign_my_modules
@ -19,6 +19,8 @@ class MyModuleRepositoriesController < ApplicationController
rows_view = 'repository_rows/simple_view_index'
preload_cells = false
else
return render_403 unless can_read_repository?(@repository)
rows_view = 'repository_rows/index'
preload_cells = true
end
@ -150,8 +152,8 @@ class MyModuleRepositoriesController < ApplicationController
end
def repositories_list
@assigned_repositories = @my_module.readable_live_and_snapshot_repositories_list(current_user)
render json: @assigned_repositories, each_serializer: AssignedRepositorySerializer, scope: {user: current_user, my_module: @my_module }
@assigned_repositories = @my_module.live_and_snapshot_repositories_list
render json: @assigned_repositories, each_serializer: AssignedRepositorySerializer, scope: { user: current_user, my_module: @my_module }
end
def full_view_table
@ -164,7 +166,7 @@ class MyModuleRepositoriesController < ApplicationController
end
def repositories_dropdown_list
@repositories = Repository.viewable_by_user(current_user).joins("
@repositories = Repository.readable_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

View file

@ -67,7 +67,7 @@ class MyModuleRepositorySnapshotsController < ApplicationController
end
def full_view_sidebar
@repository = Repository.viewable_by_user(current_user, current_team).find_by(id: params[:repository_id])
@repository = Repository.readable_by_user(current_user, current_team).find_by(id: params[:repository_id])
@repository_snapshots = @my_module.repository_snapshots
.where(parent_id: params[:repository_id])
.order(created_at: :desc)

View file

@ -71,7 +71,8 @@ class MyModuleShareableLinksController < ApplicationController
my_module: @my_module,
include_stock_consumption: @repository.has_stock_management? && params[:assigned].present?,
disable_reminders: true, # reminders are always disabled for shareable links
disable_stock_management: true # stock management is always disabled in MyModule context
disable_stock_management: true, # stock management is always disabled in MyModule context
shareable_link_view: true
}
@all_rows_count = datatable_service.all_count
@ -89,6 +90,7 @@ class MyModuleShareableLinksController < ApplicationController
page = (params[:start].to_i / per_page) + 1
datatable_service = RepositorySnapshotDatatableService.new(@repository_snapshot, params, nil, @my_module, preload_cells: false)
@datatable_params = { shareable_link_view: true }
@all_rows_count = datatable_service.all_count
@filtered_rows_count = datatable_service.filtered_count
@columns_mappings = datatable_service.mappings

View file

@ -47,7 +47,7 @@ class MyModulesController < ApplicationController
def new
@my_module = @experiment.my_modules.new
assigned_users = User.where(id: @experiment.user_assignments.select(:user_id))
assigned_users = @experiment.users
render json: {
html: render_to_string(
@ -423,10 +423,10 @@ class MyModulesController < ApplicationController
end
def inventory_assigning_my_module_filter
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
viewable_experiments = Experiment.readable_by_user(current_user, current_team)
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
experiment = Experiment.viewable_by_user(current_user, current_team)
experiment = Experiment.readable_by_user(current_user, current_team)
.joins(:my_modules)
.where(experiments: { id: viewable_experiments })
.where(my_modules: { id: assignable_my_modules })

View file

@ -60,22 +60,16 @@ class NavigationsController < ApplicationController
end
def settings_menu_links
links = [
{
name: I18n.t('users.settings.sidebar.teams'), url: teams_path
}, {
name: I18n.t('users.settings.sidebar.account_nav.addons'), url: addons_path
}
]
if can_create_acitivity_filters?
links.push({ name: I18n.t('users.settings.sidebar.webhooks'), url: users_settings_webhooks_path })
end
links = [{ name: I18n.t('users.settings.sidebar.teams'), url: teams_path }]
links << { name: I18n.t('users.settings.sidebar.groups'), url: users_settings_team_user_groups_path(current_team) } if can_manage_team?(current_team)
links << { name: I18n.t('users.settings.sidebar.account_nav.addons'), url: addons_path }
private_methods.select { |i| i.to_s[/^settings_menu_links_[a-z]*_extension$/] }.each do |method|
links = __send__(method, links)
end
links << { name: I18n.t('users.settings.sidebar.webhooks'), url: users_settings_webhooks_path } if can_create_acitivity_filters?
links
end

View file

@ -53,7 +53,7 @@ module Navigator
end
def fetch_projects(object = nil, archived = false)
if object&.is_a?(ProjectFolder)
if object.is_a?(ProjectFolder)
folder = object
project = nil
else
@ -70,37 +70,65 @@ module Navigator
(projects.archived IS TRUE AND experiments.id IS NOT NULL)
THEN 1 ELSE 0 END) > 0 AS has_children'
end
disabled_sql = 'SUM(CASE WHEN project_user_roles IS NULL THEN 0 ELSE 1 END) < 1 AS disabled'
disabled_sql = 'SUM(CASE
WHEN project_user_roles IS NULL AND
project_user_group_roles IS NULL AND
project_team_roles IS NULL
THEN 0 ELSE 1 END) < 1 AS disabled'
current_team.projects
.where(project_folder_id: folder)
.visible_to(current_user, current_team)
.with_children_viewable_by_user(current_user)
.joins("LEFT OUTER JOIN user_assignments project_user_assignments
ON project_user_assignments.assignable_type = 'Project'
AND project_user_assignments.assignable_id = projects.id
AND project_user_assignments.user_id = #{current_user.id}
LEFT OUTER JOIN user_roles project_user_roles
ON project_user_roles.id = project_user_assignments.user_role_id
AND project_user_roles.permissions @> ARRAY['#{ProjectPermissions::READ}']::varchar[]")
.where('projects.archived = :archived OR
(
(
experiments.archived = :archived OR
my_modules.archived = :archived
) AND
:archived IS TRUE
) OR
projects.id = :project_id',
archived: archived,
project_id: project&.id || -1)
.select(
'projects.id',
'projects.name',
'projects.archived',
disabled_sql,
has_children_sql
).group('projects.id')
projects =
if can_manage_team?(current_team)
# Team owners see all projects in the team
current_team.projects
else
current_team.projects.readable_by_user(current_user, current_team)
end
projects.where(project_folder_id: folder)
.with_children_viewable_by_user(current_user)
.joins("LEFT OUTER JOIN user_assignments project_user_assignments
ON project_user_assignments.assignable_type = 'Project'
AND project_user_assignments.assignable_id = projects.id
AND project_user_assignments.user_id = #{current_user.id}
LEFT OUTER JOIN user_roles project_user_roles
ON project_user_roles.id = project_user_assignments.user_role_id
AND project_user_roles.permissions @> ARRAY['#{ProjectPermissions::READ}']::varchar[]
LEFT OUTER JOIN user_group_assignments project_user_group_assignments
ON project_user_group_assignments.assignable_type = 'Project'
AND project_user_group_assignments.assignable_id = projects.id
AND project_user_group_assignments.user_group_id IN (
SELECT user_group_memberships.user_group_id FROM user_group_memberships
WHERE user_group_memberships.user_id = #{current_user.id}
)
LEFT OUTER JOIN user_roles project_user_group_roles
ON project_user_group_roles.id = project_user_group_assignments.user_role_id
AND project_user_group_roles.permissions @> ARRAY['#{ProjectPermissions::READ}']::varchar[]
LEFT OUTER JOIN team_assignments project_team_assignments
ON project_team_assignments.assignable_type = 'Project'
AND project_team_assignments.assignable_id = projects.id
AND project_team_assignments.team_id = #{current_team.id}
LEFT OUTER JOIN user_roles project_team_roles
ON project_team_roles.id = project_team_assignments.user_role_id
AND project_team_roles.permissions @> ARRAY['#{ProjectPermissions::READ}']::varchar[]
")
.where('projects.archived = :archived OR
(
(
experiments.archived = :archived OR
my_modules.archived = :archived
) AND
:archived IS TRUE
) OR
projects.id = :project_id',
archived: archived,
project_id: project&.id || -1)
.select(
'projects.id',
'projects.name',
'projects.archived',
disabled_sql,
has_children_sql
).group('projects.id')
end
def fetch_project_folders(object = nil, archived = false)
@ -109,13 +137,20 @@ module Navigator
else
object&.project_folder
end
has_children_sql = 'SUM(CASE WHEN viewable_projects.id IS NOT NULL OR project_folders_project_folders.id IS NOT NULL
has_children_sql = 'SUM(CASE WHEN visible_projects.id IS NOT NULL OR project_folders_project_folders.id IS NOT NULL
THEN 1 ELSE 0 END) > 0'
visible_projects =
if can_manage_team?(current_team)
# Team owners see all projects in the team
current_team.projects
else
current_team.projects.readable_by_user(current_user, current_team)
end
current_team.project_folders.where(parent_folder: folder)
.left_outer_joins(:projects, project_folders: {})
.joins(
"LEFT OUTER JOIN (#{Project.viewable_by_user(current_user, current_team).where(archived: archived).to_sql}) " \
"viewable_projects ON viewable_projects.project_folder_id = project_folders.id"
"LEFT OUTER JOIN (#{visible_projects.where(archived: archived).to_sql}) " \
"visible_projects ON visible_projects.project_folder_id = project_folders.id"
)
.select(
'project_folders.id',
@ -147,7 +182,7 @@ module Navigator
THEN 1 ELSE 0 END) > 0 AS has_children'
end
experiments = project.experiments
.viewable_by_user(current_user, current_team)
.readable_by_user(current_user, current_team)
.with_children_viewable_by_user(current_user)
.select(
'experiments.id',
@ -169,8 +204,7 @@ module Navigator
end
def fetch_my_modules(experiment, archived = false)
my_modules = experiment.my_modules
.viewable_by_user(current_user, current_team)
my_modules = experiment.my_modules.readable_by_user(current_user, current_team)
my_modules = my_modules.where(archived: archived) unless experiment.archived_branch?
my_modules

View file

@ -8,7 +8,6 @@ class ProjectsController < ApplicationController
include CardsViewHelper
include ExperimentsHelper
include Breadcrumbs
include UserRolesHelper
include FavoritesActions
attr_reader :current_folder
@ -16,11 +15,12 @@ class ProjectsController < ApplicationController
helper_method :current_folder
before_action :switch_team_with_param, only: :index
before_action :load_vars, only: %i(update create_tag assigned_users_list show)
before_action :load_projects, only: %i(index actions_toolbar)
before_action :load_project, only: %i(update create_tag assigned_users_list show)
before_action :load_current_folder, only: :index
before_action :check_read_permissions, except: %i(index create update archive_group restore_group
inventory_assigning_project_filter
actions_toolbar user_roles users_filter head_of_project_users_list
actions_toolbar users_filter head_of_project_users_list
favorite unfavorite)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: :update
@ -32,9 +32,9 @@ class ProjectsController < ApplicationController
def index
respond_to do |format|
format.json do
projects = Lists::ProjectsService.new(current_team, current_user, current_folder, params).call
render json: projects, each_serializer: Lists::ProjectAndFolderSerializer, user: current_user,
meta: pagination_dict(projects)
projects_list = Lists::ProjectsService.new(current_team, @projects, @current_folder, params, user: current_user).call
render json: projects_list, each_serializer: Lists::ProjectAndFolderSerializer, user: current_user,
meta: pagination_dict(projects_list)
end
format.html do
render 'projects/index'
@ -54,10 +54,10 @@ class ProjectsController < ApplicationController
end
def inventory_assigning_project_filter
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
viewable_experiments = Experiment.readable_by_user(current_user, current_team)
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
projects = Project.viewable_by_user(current_user, current_team)
projects = Project.readable_by_user(current_user, current_team)
.active
.joins(experiments: :my_modules)
.where(experiments: { id: viewable_experiments })
@ -86,7 +86,6 @@ class ProjectsController < ApplicationController
end
def update
default_public_user_role_name_before_update = @project.default_public_user_role&.name
old_status = @project.status
@project.assign_attributes(project_update_params)
return_error = false
@ -105,14 +104,6 @@ class ProjectsController < ApplicationController
end
message_edited = @project.name_changed? || @project.description_changed?
message_visibility = if !@project.visibility_changed?
nil
elsif @project.visible?
t('projects.activity.visibility_visible')
else
t('projects.activity.visibility_hidden')
end
message_archived = if !@project.archived_changed?
nil
elsif @project.archived?
@ -124,41 +115,14 @@ class ProjectsController < ApplicationController
start_date_changes = @project.changes[:start_date]
due_date_changes = @project.changes[:due_date]
default_public_user_role_name = nil
if !@project.visibility_changed? && @project.default_public_user_role_id_changed?
@project.visibility_will_change! # triggers assignment sync
default_public_user_role_name = UserRole.find(project_params[:default_public_user_role_id]).name
end
@project.last_modified_by = current_user
if !return_error && @project.save
# Add activities if needed
if message_visibility.present? && @project.visible?
log_activity(:project_grant_access_to_all_team_members,
@project,
{ visibility: message_visibility,
role: @project.default_public_user_role.name,
team: @project.team.id })
end
if message_visibility.present? && !@project.visible?
log_activity(:project_remove_access_from_all_team_members,
@project,
{ visibility: message_visibility,
role: default_public_user_role_name_before_update,
team: @project.team.id })
end
log_activity(:edit_project) if message_edited.present?
log_activity(:archive_project) if message_archived == 'archive'
log_activity(:restore_project) if message_archived == 'restore'
if default_public_user_role_name.present?
log_activity(:project_access_changed_all_team_members,
@project,
{ team: @project.team.id, role: default_public_user_role_name })
end
if supervised_by_id_changes.present?
log_activity(:remove_head_of_project, @project, { user_target: supervised_by_id_changes[0] }) if supervised_by_id_changes[0].present? # remove head of project
log_activity(:set_head_of_project, @project, { user_target: supervised_by_id_changes[1] }) if supervised_by_id_changes[1].present? # add head of project
@ -300,16 +264,17 @@ class ProjectsController < ApplicationController
render json: { data: users.map { |u| [u.id, u.name, { avatar_url: avatar_path(u, :icon_small) }] } }, status: :ok
end
def user_roles
render json: { data: user_roles_collection(Project.new).map(&:reverse) }
end
def actions_toolbar
project_ids = JSON.parse(params[:items]).select { |i| i['type'] == 'projects' }.pluck('id')
project_folder_ids = JSON.parse(params[:items]).select { |i| i['type'] == 'project_folders' }.pluck('id')
selected_projects = @projects.where(id: project_ids)
selected_project_folders = current_user.current_team.project_folders.where(id: project_folder_ids)
render json: {
actions:
Toolbars::ProjectsService.new(
current_user,
items: JSON.parse(params[:items])
selected_projects,
selected_project_folders,
current_user
).actions
}
end
@ -319,9 +284,8 @@ class ProjectsController < ApplicationController
def project_params
params.require(:project)
.permit(
:name, :visibility,
:name,
:archived, :project_folder_id,
:default_public_user_role_id,
:due_date,
:start_date,
:description
@ -330,14 +294,23 @@ class ProjectsController < ApplicationController
def project_update_params
params.require(:project)
.permit(:name, :visibility, :archived, :default_public_user_role_id, :due_date, :start_date, :description, :status, :supervised_by_id)
.permit(:name, :archived, :due_date, :start_date, :description, :status, :supervised_by_id)
end
def view_type_params
params.require(:project).require(:view_type)
end
def load_vars
def load_projects
@projects = if can_manage_team?(current_team)
# Team owners see all projects in the team
current_team.projects
else
current_team.projects.readable_by_user(current_user, current_team)
end
end
def load_project
@project = Project.find_by(id: params[:id] || params[:project_id])
render_404 unless @project

View file

@ -7,7 +7,7 @@ class ProtocolsController < ApplicationController
include ProtocolsIoHelper
include TeamsHelper
include ProtocolsExporterV2
include UserRolesHelper
include FormFieldValuesHelper
before_action :check_create_permissions, only: %i(
create
@ -79,11 +79,17 @@ class ProtocolsController < ApplicationController
def index
respond_to do |format|
format.json do
protocols = Lists::ProtocolsService.new(Protocol.viewable_by_user(current_user, @current_team), params).call
render json: protocols,
protocols = if can_manage_team?(current_team)
# Team owners see all protocol templates in the team
current_team.repository_protocols
else
current_team.repository_protocols.readable_by_user(current_user, current_team)
end
protocols_list = Lists::ProtocolsService.new(protocols, params).call
render json: protocols_list,
each_serializer: Lists::ProtocolSerializer,
user: current_user,
meta: pagination_dict(protocols)
meta: pagination_dict(protocols_list)
end
format.html do
render 'index'
@ -531,6 +537,7 @@ class ProtocolsController < ApplicationController
Protocol.transaction do
protocol = @importer.import_new_protocol(@protocol_json)
rescue StandardError => e
Rails.logger.error e.message
Rails.logger.error e.backtrace.join("\n")
transaction_error = true
raise ActiveRecord::Rollback
@ -876,10 +883,6 @@ class ProtocolsController < ApplicationController
render json: { job_id: @job.job_id }
end
def user_roles
render json: { data: user_roles_collection(Protocol.new).map(&:reverse) }
end
private
def set_importer
@ -1089,7 +1092,7 @@ class ProtocolsController < ApplicationController
end
def create_params
params.require(:protocol).permit(:name, :default_public_user_role_id, :visibility)
params.require(:protocol).permit(:name)
end
def check_protocolsio_import_permissions

View file

@ -26,7 +26,7 @@ class ReportsController < ApplicationController
def index
respond_to do |format|
format.json do
reports = Lists::ReportsService.new(Report.viewable_by_user(current_user, current_team), params).call
reports = Lists::ReportsService.new(Report.readable_by_user(current_user, current_team), params).call
render json: reports, each_serializer: Lists::ReportSerializer,
user: current_user, meta: pagination_dict(reports)
end
@ -370,10 +370,10 @@ class ReportsController < ApplicationController
end
def load_repositories_vars
live_repositories = Repository.viewable_by_user(current_user).sort_by { |r| r.name.downcase }
live_repositories = Repository.readable_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)
.where.not(original_repository: current_team.repositories)
.select('DISTINCT ON ("repositories"."parent_id") "repositories".*')
.sort_by { |r| r.name.downcase }
@repositories = live_repositories + snapshots_of_deleted
@ -398,7 +398,7 @@ class ReportsController < ApplicationController
def load_available_repositories
@available_repositories = []
repositories = Repository.active
.viewable_by_user(current_user)
.readable_by_user(current_user)
.name_like(search_params[:query])
.limit(Constants::SEARCH_LIMIT)
repositories.each do |repository|

View file

@ -10,14 +10,13 @@ class RepositoriesController < ApplicationController
include MyModulesHelper
before_action :switch_team_with_param, only: %i(index)
before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar
before_action :load_repository, except: %i(index create create_modal archive restore actions_toolbar
export_repositories list)
before_action :load_repositories, only: %i(index list)
before_action :load_repositories, only: %i(index actions_toolbar)
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 list)
before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet
import_records sidebar archive restore actions_toolbar
import_records archive restore actions_toolbar
export_repositories list)
before_action :check_manage_permissions, only: %i(rename_modal update)
before_action :check_delete_permissions, only: %i(destroy destroy_modal)
@ -45,7 +44,12 @@ class RepositoriesController < ApplicationController
end
def list
results = @repositories.select(:id, :name, 'LOWER(repositories.name)')
repositories = if params[:manageable] == 'true'
Repository.with_granted_permissions(current_user, RepositoryPermissions::ROWS_UPDATE, current_team)
else
Repository.readable_by_user(current_user, current_team)
end
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?
@ -74,15 +78,6 @@ class RepositoriesController < ApplicationController
}
end
def sidebar
render json: {
html: render_to_string(partial: 'repositories/sidebar', locals: {
repositories: @repositories,
archived: params[:archived] == 'true'
})
}
end
def show
respond_to do |format|
format.html do
@ -332,14 +327,14 @@ class RepositoriesController < ApplicationController
end
def import_records
render_403 unless can_create_repository_rows?(Repository.viewable_by_user(current_user)
render_403 unless can_create_repository_rows?(Repository.readable_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.viewable_by_user(current_user).find_by(id: import_params[:id]),
repository: Repository.readable_by_user(current_user).find_by(id: import_params[:id]),
mappings: import_params[:mappings],
session: session,
user: current_user,
@ -404,7 +399,7 @@ class RepositoriesController < ApplicationController
end
def export_repositories
repositories = Repository.viewable_by_user(current_user, current_team).where(id: params[:repository_ids])
repositories = Repository.readable_by_user(current_user, current_team).where(id: params[:repository_ids])
if repositories.present?
RepositoriesExportJob
.perform_later(params[:file_type], repositories.pluck(:id), user_id: current_user.id, team_id: current_team.id)
@ -472,12 +467,12 @@ class RepositoriesController < ApplicationController
end
def actions_toolbar
selected_repositories = @repositories.where(id: JSON.parse(params[:items]).pluck('id'))
render json: {
actions:
Toolbars::RepositoriesService.new(
current_user,
current_team,
repository_ids: JSON.parse(params[:items]).map { |i| i['id'] }
selected_repositories,
current_user
).actions
}
end
@ -486,16 +481,20 @@ class RepositoriesController < ApplicationController
def load_repository
repository_id = params[:id] || params[:repository_id]
@repository = Repository.viewable_by_user(current_user, current_user.teams).find_by(id: repository_id)
@repository = Repository.readable_by_user(current_user, current_user.teams).find_by(id: repository_id)
render_404 unless @repository
end
def load_repositories
@repositories = if params[:appendable] == 'true'
Repository.appendable_by_user(current_user)
else
Repository.viewable_by_user(current_user)
end
@repositories =
if params[:appendable] == 'true'
Repository.appendable_by_user(current_user)
elsif can_manage_team?(current_team)
# Team owners see all repositories in the team
current_team.repositories.or(Repository.shared_with_team(current_team))
else
Repository.readable_by_user(current_user, current_team)
end
end
def load_repositories_for_archiving
@ -530,10 +529,6 @@ class RepositoriesController < ApplicationController
}
end
def check_view_all_permissions
render_403 unless can_read_team?(current_team)
end
def check_view_permissions
current_team_switch(@repository.team) unless @repository.shared_with?(current_team)
render_403 unless can_read_repository?(@repository)

View file

@ -108,7 +108,7 @@ class RepositoryColumnsController < ApplicationController
AvailableRepositoryColumn = Struct.new(:id, :name)
def load_repository
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id])
@repository = Repository.readable_by_user(current_user).find_by(id: params[:repository_id])
render_404 unless @repository
end

View file

@ -56,7 +56,7 @@ class RepositoryRowConnectionsController < ApplicationController
end
def repositories
repositories = Repository.viewable_by_user(current_user)
repositories = Repository.readable_by_user(current_user)
.search_by_name_and_id(current_user, current_user.teams, params[:query])
.order(name: :asc)
.page(params[:page] || 1)
@ -70,7 +70,7 @@ class RepositoryRowConnectionsController < ApplicationController
end
def repository_rows
selected_repository = Repository.viewable_by_user(current_user).find(params[:selected_repository_id])
selected_repository = Repository.readable_by_user(current_user).find(params[:selected_repository_id])
repository_rows = selected_repository.repository_rows
.where.not(id: @repository_row.id)
@ -95,14 +95,14 @@ class RepositoryRowConnectionsController < ApplicationController
return render_422(t('.invalid_params')) unless @relation_type
@connection_repository = Repository.viewable_by_user(current_user)
@connection_repository = Repository.readable_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.viewable_by_user(current_user).find_by(id: params[:repository_id])
@repository = Repository.readable_by_user(current_user).find_by(id: params[:repository_id])
render_404 unless @repository
end
@ -145,19 +145,23 @@ class RepositoryRowConnectionsController < ApplicationController
repository_connections.map do |connection|
repository_row = @relation_type == 'parent' ? connection.parent : connection.child
{
name: repository_row.name_with_label,
code: repository_row.code,
path: repository_repository_row_path(repository_row.repository, repository_row),
repository_name: repository_row.repository.name_with_label,
repository_path: repository_path(repository_row.repository),
unlink_path: repository_repository_row_repository_row_connection_path(
repository_row.repository,
repository_row,
connection
)
}
if can_read_repository?(repository_row.repository)
{
name: repository_row.name_with_label,
code: repository_row.code,
path: repository_repository_row_path(repository_row.repository, repository_row),
repository_name: repository_row.repository.name_with_label,
repository_path: repository_path(repository_row.repository),
can_connect_rows: can_connect_repository_rows?(repository_row.repository),
unlink_path: repository_repository_row_repository_row_connection_path(
repository_row.repository,
repository_row,
connection
)
}
else
{ name: I18n.t('repositories.item_card.relationships.private_item_name') }
end
end
end

View file

@ -58,13 +58,11 @@ class RepositoryRowsController < ApplicationController
end
end
@assigned_modules = @repository_row.my_modules
.joins(experiment: :project)
.joins(:my_module_repository_rows)
.select('my_module_repository_rows.created_at, my_modules.*')
.order('my_module_repository_rows.created_at': :desc)
.distinct
@viewable_modules = @assigned_modules.viewable_by_user(current_user, current_user.teams)
@assigned_modules = @repository_row.my_modules.distinct
@viewable_modules = @assigned_modules.readable_by_user(current_user, current_user.teams)
.joins(:my_module_repository_rows)
.select('my_module_repository_rows.created_at, my_modules.*')
.order(my_module_repository_rows: { created_at: :desc })
@reminders_present = @repository_row.repository_cells.with_active_reminder(@current_user).any?
end
end
@ -83,7 +81,7 @@ class RepositoryRowsController < ApplicationController
end
if update_params[:my_module_id].present?
my_module = MyModule.viewable_by_user(current_user, current_team).find_by(id: update_params[:my_module_id])
my_module = MyModule.readable_by_user(current_user, current_team).find_by(id: update_params[:my_module_id])
return render_403 unless my_module.present? && can_read_my_module?(my_module)
@ -215,6 +213,8 @@ class RepositoryRowsController < ApplicationController
{ repository_row: @repository_row.id,
repository_column: update_params['repository_cells']&.keys&.first ||
I18n.t('repositories.table.row_name') })
record_annotation_notification(@repository_row, row_cell_update.cell) if row_cell_update.cell && row_cell_update.cell.value_type == 'RepositoryTextValue'
end
@reminders_present = @repository_row.repository_cells.with_active_reminder(@current_user).any?
@ -299,7 +299,7 @@ class RepositoryRowsController < ApplicationController
params[:query],
whole_phrase: true
)
viewable_modules = assigned_modules.viewable_by_user(current_user, current_user.teams)
viewable_modules = assigned_modules.readable_by_user(current_user, current_user.teams)
private_modules_number = assigned_modules.where.not(id: viewable_modules).count
render json: {
html: render_to_string(partial: 'shared/my_modules_list_partial', locals: {
@ -372,7 +372,7 @@ class RepositoryRowsController < ApplicationController
AvailableRepositoryRow = Struct.new(:id, :name, :has_file_attached)
def load_repository
@repository = Repository.viewable_by_user(current_user)
@repository = Repository.readable_by_user(current_user)
.eager_load(:repository_columns)
.find_by(id: params[:repository_id])
render_404 unless @repository
@ -384,7 +384,7 @@ class RepositoryRowsController < ApplicationController
FormRepositoryRowsFieldValue.find_by(id: params[:form_repository_rows_field_value_id])
end
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id]) ||
@repository = Repository.readable_by_user(current_user).find_by(id: params[:repository_id]) ||
RepositorySnapshot.find_by(id: params[:repository_id])
render_404 unless @form_repository_rows_field_value || @repository
@ -476,7 +476,7 @@ class RepositoryRowsController < ApplicationController
smart_annotation_notification(
old_text: old_text,
new_text: cell.value.data,
subject: cell.repository_column.repository,
subject: cell.repository_row,
title: t('notifications.repository_annotation_title',
user: current_user.full_name,
column: cell.repository_column.name,

View file

@ -70,7 +70,7 @@ class RepositoryTableFiltersController < ApplicationController
private
def load_repository
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id])
@repository = Repository.readable_by_user(current_user).find_by(id: params[:repository_id])
render_403 unless can_read_repository?(@repository)
end

View file

@ -171,8 +171,7 @@ class SearchController < ApplicationController
current_team,
params[:query],
search_object_classes,
limit: Constants::QUICK_SEARCH_LIMIT,
fetch_latest_versions: class_name == 'protocol')
limit: Constants::QUICK_SEARCH_LIMIT)
.order(updated_at: :desc)
end

View file

@ -84,7 +84,7 @@ class SmartAnnotationsController < ApplicationController
when MyModule
protocols_my_module_path(resource)
when RepositoryRow
repository_repository_row_path(resource.repository, resource)
repository_repository_row_path(resource.repository, resource, my_module_id: params[:my_module_id])
end
end

View file

@ -8,6 +8,7 @@ class StorageLocationsController < ApplicationController
before_action :switch_team_with_param, only: %i(index show)
before_action :check_storage_locations_enabled, except: :unassign_rows
before_action :load_storage_locations, only: %i(index actions_toolbar)
before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows export_container import_container)
before_action :set_inline_name_editing, only: %i(show)
before_action :check_read_permissions, except: %i(index create tree actions_toolbar import_container unassign_rows)
@ -26,7 +27,7 @@ class StorageLocationsController < ApplicationController
respond_to do |format|
format.html
format.json do
storage_locations = Lists::StorageLocationsService.new(current_user, current_team, params).call
storage_locations = Lists::StorageLocationsService.new(@storage_locations, params, user: current_user).call
render json: storage_locations,
each_serializer: Lists::StorageLocationSerializer,
user: current_user,
@ -131,7 +132,7 @@ class StorageLocationsController < ApplicationController
end
def tree
records = StorageLocation.viewable_by_user(current_user, current_team)
records = StorageLocation.readable_by_user(current_user, current_team)
.where(
parent: nil,
container: [false, params[:container] == 'true']
@ -188,11 +189,12 @@ class StorageLocationsController < ApplicationController
end
def actions_toolbar
selected_storage_locations = @storage_locations.where(id: JSON.parse(params[:items]).pluck('id'))
render json: {
actions:
Toolbars::StorageLocationsService.new(
current_user,
storage_location_ids: JSON.parse(params[:items]).pluck('id')
selected_storage_locations,
current_user
).actions
}
end
@ -212,6 +214,10 @@ class StorageLocationsController < ApplicationController
params.permit(:id, :destination_storage_location_id)
end
def load_storage_locations
@storage_locations = StorageLocation.readable_by_user(current_user, current_team).or(StorageLocation.shared_with_team(current_team))
end
def load_storage_location
@storage_location = StorageLocation.find(storage_location_params[:id])
@parent_location = @storage_location.parent

View file

@ -22,6 +22,14 @@ class TeamSharedObjectsController < ApplicationController
if @model.permission_level_changed?
@model.save!
@model.team_shared_objects.each(&:destroy!) unless global_permission_level == :not_shared
case global_permission_level
when :shared_read
@model.demote_all_sharing_assignments_to_viewer!
when :not_shared
@model.destroy_all_sharing_assignments!
end
case @model
when Repository
setup_repository_global_share_activity
@ -66,9 +74,9 @@ class TeamSharedObjectsController < ApplicationController
def load_vars
case params[:object_type]
when 'Repository'
@model = Repository.viewable_by_user(current_user).find_by(id: params[:object_id])
@model = Repository.readable_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])
@model = StorageLocation.readable_by_user(current_user).find_by(id: params[:object_id])
end
render_404 unless @model
@ -88,7 +96,7 @@ class TeamSharedObjectsController < ApplicationController
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 unless public_send(:"can_share_#{object_name}?", @model)
render_403 if !@model.shareable_write? && update_params[:write_permissions].present?
end

View file

@ -9,7 +9,7 @@ class TeamsController < ApplicationController
before_action :load_vars, only: %i(sidebar export_projects export_projects_modal
disable_tasks_sharing_modal shared_tasks_toggle)
before_action :load_current_folder, only: :sidebar
before_action :check_read_permissions, except: %i(view_type visible_teams visible_users)
before_action :check_read_permissions, except: %i(view_type visible_teams visible_users current_team_users)
before_action :check_export_projects_permissions, only: %i(export_projects_modal export_projects)
def visible_teams
@ -22,7 +22,12 @@ class TeamsController < ApplicationController
if params[:teams].present?
teams = teams.where(id: params[:teams])
end
users = User.where(id: teams.joins(:users).select('users.id')).order(:full_name)
users = User.where(id: UserAssignment.where(assignable: teams).select(:user_id)).order(:full_name)
render json: users, each_serializer: UserSerializer, user: current_user
end
def current_team_users
users = current_team.users.order(:full_name)
render json: users, each_serializer: UserSerializer, user: current_user
end
@ -154,7 +159,7 @@ class TeamsController < ApplicationController
if export_projects_params[:project_folder_ids]
folders = @team.project_folders.where(id: export_projects_params[:project_folder_ids])
folders.each do |folder|
@exp_projects += folder.inner_projects.visible_to(current_user, @team)
@exp_projects += folder.inner_projects.readable_by_user(current_user, @team)
end
end

View file

@ -35,7 +35,7 @@ class Users::PasswordsController < Devise::PasswordsController
flash_message = resource.active_for_authentication? ? :updated : :updated_not_active
set_flash_message!(:notice, flash_message)
resource.after_database_authentication if check_database_authentication?(resource)
sign_in(resource_name, resource)
sign_in(resource_name, resource, event: :authentication)
else
set_flash_message!(:notice, :updated_not_active)
end

View file

@ -15,6 +15,7 @@ module Users
create
show
users_datatable
members
)
before_action :load_team, only: %i(
@ -24,12 +25,13 @@ module Users
description_html
update
destroy
members
)
before_action :check_create_team_permission,
only: %i(new create)
before_action :set_breadcrumbs_items, only: %i(index show)
before_action :set_breadcrumbs_items, only: %i(index show members)
layout 'fluid'
@ -57,7 +59,13 @@ module Users
end
end
def show; end
def show
@active_tab = :details
end
def members
@active_tab = :members
end
def users_datatable
render json: ::TeamUsersDatatable.new(view_context, @team, @user)

View file

@ -0,0 +1,96 @@
# frozen_string_literal: true
module Users
module Settings
class UserGroupMembershipsController < ApplicationController
before_action :check_user_groups_enabled
before_action :load_team
before_action :load_user_group
before_action :check_manage_permissions, except: %i(index show)
def index
memberships = Lists::UserGroupMembershipsService.new(@user_group.user_group_memberships, params).call
render json: memberships, each_serializer: Lists::UserGroupMembershipSerializer, user: current_user, meta: pagination_dict(memberships)
end
def actions_toolbar
render json: {
actions:
Toolbars::UserGroupMembershipsService.new(
current_user,
@user_group,
user_group_membership_ids: JSON.parse(params[:items]).pluck('id')
).actions
}
end
def show; end
def create
ActiveRecord::Base.transaction do
new_users = @team.users.where(id: params[:user_ids])
new_users.each do |user|
@user_group.user_group_memberships.create!(user: user, created_by: current_user)
log_activity(:add_group_user_member, user)
end
render json: { message: :success }, status: :created
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
head :unprocessable_entity
raise ActiveRecord::Rollback
end
end
def destroy_multiple
members = @user_group.user_group_memberships.where(id: params[:membership_ids])
members.each do |member|
log_activity(:remove_group_user_member, member.user)
end
if members.destroy_all
render json: { message: :success }, status: :ok
else
head :unprocessable_entity
end
end
private
def check_user_groups_enabled
render '/users/settings/user_groups/promo' unless UserGroup.enabled?
end
def load_team
@team = Team.find(params[:team_id])
end
def load_user_group
@user_group = @team.user_groups.find(params[:user_group_id])
end
def load_user_group_membership
@user_group_membership = @user_group.user_group_memberships.find(params[:id])
end
def check_manage_permissions
render_403 unless can_manage_team?(@team)
end
def log_activity(type_of, user_target)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: @user_group.team,
team: @user_group.team,
message_items: {
user_group: @user_group.id,
team: @user_group.team.id,
user_target: user_target.id
})
end
end
end
end

View file

@ -0,0 +1,141 @@
# frozen_string_literal: true
module Users
module Settings
class UserGroupsController < ApplicationController
before_action :load_team
before_action :set_breadcrumbs_items, only: %i(index show)
before_action :check_user_groups_enabled, except: :users
before_action :load_user_group, except: %i(index unassigned_users actions_toolbar create)
before_action :check_read_permissions, only: %i(users)
before_action :check_manage_permissions, except: %i(users)
def index
respond_to do |format|
format.html do
@active_tab = :user_groups
end
format.json do
user_groups = Lists::UserGroupsService.new(@team.user_groups, params).call
render json: user_groups, each_serializer: Lists::UserGroupSerializer, user: current_user, meta: pagination_dict(user_groups)
end
end
end
def actions_toolbar
render json: {
actions:
Toolbars::UserGroupsService.new(
current_user,
@team,
user_group_ids: JSON.parse(params[:items]).pluck('id')
).actions
}
end
def unassigned_users
@unassigned_users = @team.users.search(false, params[:query])
if params[:user_group_id].present?
@user_group = @team.user_groups.find(params[:user_group_id])
@unassigned_users = @unassigned_users.where.not(id: @user_group.users.select(:id))
end
end
def show
@active_tab = :user_groups
end
def create
@user_group = @team.user_groups.new
@user_group.created_by = current_user
@user_group.last_modified_by = current_user
@user_group.assign_attributes(user_group_params)
if @user_group.save
log_activity(:create_user_group)
@user_group.users.each do |user|
log_activity(:add_group_user_member, { user_target: user.id })
end
render json: { message: t('user_groups.create.success') }, status: :created
else
render json: { error: @user_group.errors.full_messages.join(", ") }, status: :unprocessable_entity
end
end
def update
ActiveRecord::Base.transaction do
@user_group.last_modified_by = current_user
@user_group.assign_attributes(user_group_params)
@user_group.save!
log_activity(:update_user_group)
render json: {}, status: :ok
rescue ActiveRecord::RecordInvalid => e
render json: { errors: e.message }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
def destroy
log_activity(:delete_user_group)
if @user_group.destroy
render json: { message: t('user_groups.delete.success') }, status: :ok
else
render json: { errors: t('user_groups.delete.error') }, status: :unprocessable_entity
end
end
def users
render json: @user_group.users, each_serializer: UserSerializer, user: current_user
end
private
def check_user_groups_enabled
@active_tab = :user_groups
render '/users/settings/user_groups/promo' unless UserGroup.enabled?
end
def user_group_params
params.require(:user_group).permit(
:name,
user_group_memberships_attributes: %i(id user_id)
)
end
def load_team
@team = Team.find(params[:team_id])
end
def load_user_group
@user_group = @team.user_groups.find(params[:user_group_id] || params[:id])
end
def check_read_permissions
render_403 unless can_read_team?(@team)
end
def check_manage_permissions
render_403 unless can_manage_team?(@team)
end
def log_activity(type_of, message_items = {})
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
subject: @user_group.team,
team: @user_group.team,
message_items: {
user_group: @user_group.id,
team: @user_group.team.id
}.merge(message_items))
end
def set_breadcrumbs_items
@breadcrumbs_items = [
{ label: t('breadcrumbs.teams'), url: teams_path },
{ label: @team.name, url: team_path(@team) }
]
end
end
end
end

View file

@ -109,11 +109,13 @@ module Users
)
end
redirect_url = teams_path if params[:leave]
generate_notification(current_user,
@user_assignment.user,
@user_assignment.assignable,
false)
render json: { status: :ok, job_id: job_id, success_message: success_message }
render json: { status: :ok, job_id: job_id, success_message: success_message, redirect_url: redirect_url }
end
end

View file

@ -2,7 +2,7 @@ class TeamsDatatable < CustomDatatable
include InputSanitizeHelper
def_delegator :@view, :link_to
def_delegator :@view, :team_path
def_delegator :@view, :members_users_settings_team_path
def_delegator :@view, :leave_user_team_html_path
MEMEBERS_SORT_COL = 'members'.freeze
@ -28,7 +28,7 @@ class TeamsDatatable < CustomDatatable
{
'DT_RowId': record.id,
'0': if current_user_team_role(record)&.owner?
link_to(escape_input(record.name), team_path(record))
link_to(escape_input(record.name), members_users_settings_team_path(record))
else
escape_input(record.name)
end,

View file

@ -162,7 +162,7 @@ module CommentHelper
smart_annotation_notification(
old_text: old_text,
new_text: comment.message,
subject: step.protocol,
subject: step,
title: t('notifications.step_comment_annotation_title',
step: step.name,
user: current_user.full_name),

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module FormFieldValuesHelper
include Canaid::Helpers::PermissionsHelper
def form_repository_rows_field_value_formatter(field_values, user = current_user)
field_values&.value&.map do |value|
row_code = "#{RepositoryRow::ID_PREFIX}#{value['id']}"
repository = Repository.find_by(id: value['repository_id'])
if repository.nil? || can_read_repository?(user, repository)
"#{value['name']} (#{row_code})"
else
I18n.t('my_modules.assigned_items.repository.private_repository_row_name', repository_row_code: row_code)
end
end&.join(' | ')
end
end

View file

@ -53,6 +53,8 @@ module GlobalActivitiesHelper
path = ''
case obj
when UserGroup
path = users_settings_team_user_group_path(obj.team, obj)
when User
return "[@#{obj.full_name}~#{obj.id.base62_encode}]"
when Tag
@ -61,6 +63,8 @@ module GlobalActivitiesHelper
when Team
path = projects_path(team: obj.id)
when Repository
return I18n.t('repositories.private') unless can_read_repository?(obj)
path = repository_path(obj, team: obj.team.id)
when RepositoryRow
# Handle private repository rows
@ -70,6 +74,8 @@ module GlobalActivitiesHelper
path = repository_path(obj.repository, team: obj.repository.team.id)
when RepositoryColumn
return I18n.t('repositories.repository_column.private') unless can_read_repository?(obj.repository)
return current_value unless obj.repository
path = repository_path(obj.repository, team: obj.repository.team.id)

View file

@ -2,6 +2,7 @@
module ReportsHelper
include StringUtility
include FormFieldValuesHelper
include Canaid::Helpers::PermissionsHelper
def render_report_element(element, provided_locals = nil)
@ -10,6 +11,8 @@ module ReportsHelper
return unless can_read_experiment?(element.experiment)
when 'my_module'
return unless can_read_my_module?(element.my_module)
when 'my_module_repository'
return unless can_read_repository?(element.repository)
end
# Determine partial
@ -22,7 +25,7 @@ module ReportsHelper
# First, recursively render element's children
if element.children.active.present?
element.children.active.find_each do |child|
children_html.safe_concat render_report_element(child, provided_locals)
children_html.safe_concat render_report_element(child, provided_locals) || ''
end
end
locals[:report_element] = element
@ -139,6 +142,8 @@ module ReportsHelper
)
elsif form_field_value.is_a?(FormDatetimeFieldValue)
form_field_value&.formatted_localize
elsif form_field_value.is_a?(FormRepositoryRowsFieldValue)
form_repository_rows_field_value_formatter(form_field_value)
else
form_field_value&.formatted
end

View file

@ -101,11 +101,14 @@ module RepositoryDatatableHelper
# otherwise it will result in duplicated SQL queries
has_stock_management = repository.has_stock_management?
reminders_enabled = !options[:disable_reminders] && Repository.reminders_enabled?
shareable_link_view = options[:shareable_link_view] && my_module.shared?
# Always disabled in a simple view
stock_managable = false
stock_consumption_permitted = has_stock_management && stock_consumption_permitted?(repository, my_module)
repository_rows.map do |record|
next { code: record.code } unless shareable_link_view || can_read_repository?(record.repository)
row = {
DT_RowId: record.id,
DT_RowAttr: { 'data-state': row_style(record, my_module) },
@ -156,7 +159,7 @@ module RepositoryDatatableHelper
consumed_stock_formatted =
number_with_precision(
record.consumed_stock,
precision: (record.repository.repository_stock_column.metadata['decimals'].to_i || 0),
precision: record.repository.repository_stock_column.metadata['decimals'].to_i || 0,
strip_insignificant_zeros: true
)
row['consumedStock'][:value] = {

View file

@ -4,7 +4,7 @@ module TableHelper
def table_data
content = {}
data = JSON.parse(contents)['data']
return [] if data.blank?
return {} if data.blank?
cells = metadata.fetch('cells', [])

View file

@ -2,15 +2,23 @@
module UserRolesHelper
def user_roles_collection(object, with_inherit: false)
permission_group = "#{object.class.name}Permissions".constantize
permissions = permission_group.constants.map { |const| permission_group.const_get(const) }
if (object.respond_to?(:private_shared_with_read?) && object.private_shared_with_read?(current_team)) ||
(object.respond_to?(:shared_with_read?) && object.shared_with_read?(current_team))
viewer_role = UserRole.find_predefined_viewer_role
roles = [[viewer_role.name, viewer_role.id]]
else
permission_group = "#{object.class.permission_class}Permissions".constantize
permissions = permission_group.constants.map { |const| permission_group.const_get(const) }
roles = user_roles_subset_by_permissions(permissions).order(id: :asc).pluck(:name, :id)
end
roles = user_roles_subset_by_permissions(permissions).order(id: :asc).pluck(:name, :id)
if with_inherit
roles = [[t('access_permissions.reset'), 'reset',
t("access_permissions.partials.#{object.class.name.underscore}_member_field.reset_description")]] +
roles
end
roles
end

View file

@ -0,0 +1,10 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
import UserGroupShow from '../../vue/user_groups/show.vue';
import { mountWithTurbolinks } from './helpers/turbolinks.js';
const app = createApp();
app.component('UserGroupShow', UserGroupShow);
app.config.globalProperties.i18n = window.I18n;
app.use(PerfectScrollbar);
mountWithTurbolinks(app, '#userGroupShow');

View file

@ -0,0 +1,10 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
import UserGroupsTable from '../../vue/user_groups/index.vue';
import { mountWithTurbolinks } from './helpers/turbolinks.js';
const app = createApp();
app.component('UserGroupsTable', UserGroupsTable);
app.config.globalProperties.i18n = window.I18n;
app.use(PerfectScrollbar);
mountWithTurbolinks(app, '#userGroupsTable');

View file

@ -22,24 +22,6 @@
@change="changeProject"
/>
</div>
<div v-if="selectedProject == 0">
<div class="flex gap-2 text-xs items-center">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox" v-model="publicProject" value="visible"/>
<span class="sci-checkbox-label"></span>
</div>
<span v-html="i18n.t('projects.index.modal_new_project.visibility_html')"></span>
</div>
<div class="mt-4" :class="{'hidden': !publicProject}">
<label class="sci-label">{{ i18n.t("dashboard.create_task_modal.user_role") }}</label>
<SelectDropdown
:options="userRoles"
:value="defaultRole"
@change="changeRole"
:placeholder="i18n.t('dashboard.create_task_modal.user_role_placeholder')"
/>
</div>
</div>
<div class="mt-4">
<label class="sci-label">{{ i18n.t("dashboard.create_task_modal.experiment") }}</label>
<SelectDropdown
@ -94,7 +76,6 @@ export default {
}
},
created() {
this.fetchUserRoles();
$('#create-task-modal').on('hidden.bs.modal', () => {
this.$emit('close');
});
@ -113,10 +94,6 @@ export default {
type: String,
required: true
},
rolesUrl: {
type: String,
required: true
},
createUrl: {
type: String,
required: true
@ -130,10 +107,7 @@ export default {
experiments: [],
selectedExperiment: null,
newExperimentName: '',
userRoles: [],
taskName: '',
publicProject: false,
defaultRole: null,
creatingTask: false
};
},
@ -153,9 +127,7 @@ export default {
},
project: {
id: this.selectedProject,
name: this.newProjectName,
visibility: (this.publicProject ? 'visible' : 'hidden'),
default_public_user_role_id: this.defaultRole
name: this.newProjectName
}
})
.then((response) => {
@ -163,9 +135,6 @@ export default {
window.location.href = response.data.my_module_path;
});
},
changeRole(value) {
this.defaultRole = value;
},
changeProject(value, label) {
this.selectedProject = value;
this.newProjectName = label;
@ -204,12 +173,6 @@ export default {
this.selectedExperiment = null;
this.newExperimentName = '';
},
fetchUserRoles() {
axios.get(this.rolesUrl)
.then((response) => {
this.userRoles = response.data.data;
});
},
focusInput() {
this.$refs.taskName.focus();
}

View file

@ -171,6 +171,7 @@ export default {
cellRendererParams: {
statusesList: this.statusesList
},
notSelectable: true,
minWidth: 180
},
{

View file

@ -12,7 +12,11 @@
</div>
<div v-else class="flex items-center gap-2 py-0.5">
<div class="w-3 h-3 rounded-full"
:class="this.statusColor(params.data.status_cell.status)"></div>
:class="{
'bg-sn-grey-500': params.data.status_cell.status === 'not_started',
'bg-sn-science-blue': params.data.status_cell.status === 'in_progress',
'bg-sn-alert-green': params.data.status_cell.status === 'done'
}"></div>
<span class="truncate">
{{ this.i18n.t(`experiments.table.column.status.${params.data.status_cell.status}`) }}
</span>
@ -34,20 +38,17 @@ export default {
}
},
methods: {
statusColor(status) {
optionRenderer(option) {
let color = 'bg-sn-grey-500';
if (status === 'in_progress') {
if (option[0] === 'in_progress') {
color = 'bg-sn-science-blue';
} else if (status === 'done') {
} else if (option[0] === 'done') {
color = 'bg-sn-alert-green';
}
return color;
},
optionRenderer(option) {
return `
<div class="flex items-center gap-2">
<div class="${this.statusColor(option[0])} w-3 h-3 rounded-full"></div>
<div class="${color} w-3 h-3 rounded-full"></div>
<span>${option[1]}</span>
</div>`;
},

View file

@ -15,10 +15,11 @@
<template v-for="(row, index) in field.field_value.value" :key="row.id">
<div class="flex items-center gap-2">
<span v-if="index > 0">|</span>
<a
<a v-if="row.has_access"
class="cursor-pointer text-sn-blue record-info-link"
:href="itemCardUrl(row)"
>{{ row.name }} (ID{{ row.id }})</a>
>{{ row.name }}</a>
<span v-else class="cursor-pointer"> {{ row.name }}</span>
<i v-if="!disabled" @click="removeValue(row.id)" class="sn-icon sn-icon-unlink-italic-s cursor-pointer"></i>
</div>
</template>

View file

@ -33,6 +33,7 @@
<div class="modal-body flex flex-col gap-6" :class="{ '!pb-3': notification }">
<RepositoryRowSelector
:multiple="true"
:manageableRepositoriesOnly="true"
@change="selectedItemValues = $event"
@repositoryChange="changeSelectedInventory"
/>

View file

@ -1,8 +1,12 @@
<template>
<div class="bg-white px-4 my-4 task-section">
<div class="py-4 flex items-center gap-4">
<i ref="openHandler" @click="toggleContainer" class="sn-icon sn-icon-right cursor-pointer"></i>
<h2 class="my-0 flex items-center gap-1">
<i ref="openHandler"
@click="toggleContainer"
class="sn-icon sn-icon-right cursor-pointer"
data-e2e="e2e-IC-task-assignedItems-visibilityToggle">
</i>
<h2 class="my-0 flex items-center gap-1" data-e2e="e2e-TX-task-assignedItems-title">
{{ i18n.t('my_modules.assigned_items.title') }}
<span class="text-sn-grey-500 font-normal text-base">[{{ totalRows }}]</span>
</h2>

View file

@ -10,14 +10,22 @@
<ul ref="reminders" v-html="reminders" class="list-none pl-0"></ul>
</template>
</GeneralDropdown>
<a class="hover:no-underline record-info-link truncate block"
<a v-if="params.data.recordInfoUrl"
class="hover:no-underline record-info-link truncate block"
:title="params.data[0]"
:href="params.data.recordInfoUrl"
>
{{ params.data[0] }}
</a>
<span v-else
:title="i18n.t('my_modules.assigned_items.repository.private_repository_row_name', {repository_row_code: params.data.code })"
class="text-sn-grey truncate"
>
<i class="sn-icon sn-icon-locked-task"></i>
{{ i18n.t('my_modules.assigned_items.repository.private_repository_row_name', {repository_row_code: params.data.code }) }}
</span>
<span v-for="state in params.data.DT_RowAttr['data-state']" class="text-sn-grey bg-sn-light-grey text-xs px-1.5 py-1 ">
<span v-if="params.data.DT_RowAttr" v-for="state in params.data.DT_RowAttr['data-state']" class="text-sn-grey bg-sn-light-grey text-xs px-1.5 py-1 ">
{{ state }}
</span>
</div>

View file

@ -19,6 +19,7 @@
</h3>
</div>
<button
v-if="repository.attributes.urls.full_view"
class="btn btn-light icon-btn ml-auto full-screen"
:data-table-url="fullViewUrl"
:data-e2e="`e2e-BT-task-assignedItems-inventory${ repository.id }-expand`"

View file

@ -7,11 +7,10 @@
<img :src="user.avatar" class="w-7 h-7 rounded-full" />
</div>
<div v-if="hiddenUsers.length > 0" :title="hiddenUsersTitle"
class="flex shrink-0 items-center justify-center w-7 h-7 text-xs
rounded-full bg-sn-dark-grey text-sn-white">
class="flex shrink-0 items-center justify-center w-7 h-7 text-xs rounded-full bg-sn-dark-grey text-sn-white">
+{{ hiddenUsers.length }}
</div>
<div v-if="canManage" class="flex items-center shrink-0 justify-center w-7 h-7 rounded-full bg-sn-light-grey text-sn-dark-grey">
<div v-if="canManage" class="flex items-center shrink-0 justify-center w-7 h-7 rounded-full border-dashed bg-sn-white text-sn-sleepy-grey border-sn-sleepy-grey">
<i class="sn-icon sn-icon-new-task"></i>
</div>
</div>

View file

@ -3,15 +3,16 @@
<template v-if="params.data.tags.length > 0 || params.data.permissions.manage_tags">
<GeneralDropdown v-if="params.data.tags.length > 0">
<template v-slot:field>
<div
class="sci-tag text-white max-w-[150px]"
:style="{'background': params.data.tags[0].color}">
<div class="truncate">{{ params.data.tags[0].name }}</div>
</div>
<div v-if="params.data.tags.length > 1"
class="h-6 min-w-[24px] text-sn-dark-grey inline-flex items-center justify-center rounded-full text-[.625rem]
ml-1.5 bg-sn-light-grey border !border-sn-sleepy-grey cursor-pointer">
<span>+{{ params.data.tags.length - 1 }}</span>
<div class="flex items-center gap-1.5">
<div
class="sci-tag text-white max-w-[150px]"
:style="{'background': params.data.tags[0].color}">
<div class="truncate">{{ params.data.tags[0].name }}</div>
</div>
<div v-if="params.data.tags.length > 1"
class="flex shrink-0 items-center justify-center w-7 h-7 text-xs rounded-full bg-sn-dark-grey text-sn-white">
<span>+{{ params.data.tags.length - 1 }}</span>
</div>
</div>
</template>
<template v-slot:flyout>
@ -29,8 +30,7 @@
</template>
</GeneralDropdown>
<div v-if="params.data.permissions.manage_tags" @click.stop="openModal"
class="cursor-pointer text-sn-sleep-grey border !border-dashed h-6 w-6 flex items-center
justify-center !border-sn-sleep-grey rounded-full ">
class="flex items-center shrink-0 justify-center w-7 h-7 rounded-full border-dashed bg-sn-white text-sn-sleepy-grey border-sn-sleepy-grey">
<i class="sn-icon sn-icon-new-task"></i>
</div>
</template>

View file

@ -5,11 +5,12 @@
<img :src="logoUrl" alt="SciNote" class="h-full block">
</a>
</div>
<div v-if="currentTeam" class="w-64" :data-e2e="'e2e-DD-topMenu-teams'">
<div v-if="currentTeam" class="w-64">
<SelectDropdown
:value="currentTeam"
:options="teams"
@change="switchTeam"
:e2eValue="'e2e-DD-topMenu-teams'"
></SelectDropdown>
</div>
<QuickSearch
@ -144,11 +145,16 @@ export default {
},
computed: {
settingsMenuItems() {
return this.settingsMenu.map((item) => ({ text: item.name, url: item.url })).concat(
return this.settingsMenu.map((item) => ({
text: item.name,
url: item.url,
data_e2e: `e2e-DO-topMenu-settings-${item.name.replace(' ','').toLowerCase()}`
})).concat(
{
text: this.i18n.t('left_menu_bar.support_links.core_version'),
modalTarget: '#aboutModal',
url: ''
url: '',
data_e2e: `e2e-DO-topMenu-settings-scinoteVersion`
}
);
}

View file

@ -55,12 +55,12 @@
@update="updateDescription"
@close="descriptionModalObject = null"/>
<ExportLimitExceededModal v-if="exportLimitExceded" :description="exportDescription" @close="exportLimitExceded = false"/>
<ProjectFormModal v-if="editProject" :userRolesUrl="userRolesUrl"
<ProjectFormModal v-if="editProject"
:project="editProject" @close="editProject = null" @update="updateTable(); updateNavigator()" />
<EditFolderModal v-if="editFolder" :folder="editFolder"
@close="editFolder = null" @update="updateTable(); updateNavigator()" />
<ProjectFormModal v-if="newProject" :createUrl="createUrl"
:currentFolderId="currentFolderId" :userRolesUrl="userRolesUrl"
:currentFolderId="currentFolderId"
@close="newProject = false" @create="updateTable(); updateNavigator()" />
<NewFolderModal v-if="newFolder" :createFolderUrl="createFolderUrl"
:currentFolderId="currentFolderId" :viewMode="currentViewMode"

View file

@ -51,22 +51,6 @@
:placeholder="i18n.t('projects.index.add_description')"
></TinymceEditor>
</div>
<div class="flex gap-2 text-xs items-center">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox" v-model="visible" value="visible" data-e2e="e2e-CB-projects-newProjectModal-access"/>
<span class="sci-checkbox-label"></span>
</div>
<span v-html="i18n.t('projects.index.modal_new_project.visibility_html')"></span>
</div>
<div class="mt-6" :class="{'hidden': !visible}">
<label class="sci-label">{{ i18n.t("user_assignment.select_default_user_role") }}</label>
<SelectDropdown
:options="userRoles"
:value="defaultRole"
@change="changeRole"
:e2eValue="'e2e-DD-projects-newProjectModal-defaultRole'"
/>
</div>
</div>
<div class="modal-footer">
<button
@ -80,7 +64,7 @@
<button
class="btn btn-primary"
type="submit"
:disabled="submitting || (visible && !defaultRole) || !validName"
:disabled="submitting || !validName"
data-e2e="e2e-BT-projects-newProjectModal-create"
>
{{ submitButtonLabel }}
@ -94,7 +78,6 @@
<script>
import SelectDropdown from '../../shared/select_dropdown.vue';
import DateTimePicker from '../../shared/date_time_picker.vue';
import TinymceEditor from '../../shared/tinymce_editor.vue';
import axios from '../../../packs/custom_axios.js';
@ -104,25 +87,14 @@ export default {
name: 'ProjectFormModal',
props: {
project: Object,
userRolesUrl: String,
currentFolderId: String,
createUrl: String
},
mixins: [modalMixin],
components: {
SelectDropdown,
DateTimePicker,
TinymceEditor
},
watch: {
visible(newValue) {
if (newValue) {
[this.defaultRole] = this.userRoles.find((role) => role[1] === 'Viewer');
} else {
this.defaultRole = null;
}
}
},
computed: {
validName() {
return this.name.length >= GLOBAL_CONSTANTS.NAME_MIN_LENGTH;
@ -142,16 +114,10 @@ export default {
return this.i18n.t('projects.index.modal_edit_project.submit');
}
},
mounted() {
this.fetchUserRoles();
},
data() {
return {
name: this.project?.name || '',
visible: this.project ? !this.project.hidden : false,
defaultRole: this.project?.default_public_user_role_id,
error: null,
userRoles: [],
submitting: false,
startDate: null,
dueDate: null,
@ -175,9 +141,7 @@ export default {
name: this.name,
start_date: this.startDate,
due_date: this.dueDate,
description: this.description,
visibility: (this.visible ? 'visible' : 'hidden'),
default_public_user_role_id: this.defaultRole
description: this.description
};
if (this.createUrl) {
@ -209,23 +173,12 @@ export default {
});
this.submitting = false;
},
changeRole(role) {
this.defaultRole = role;
},
updateStartDate(startDate) {
this.startDate = this.stripTime(startDate);
},
updateDueDate(dueDate) {
this.dueDate = this.stripTime(dueDate);
},
fetchUserRoles() {
if (this.userRolesUrl) {
axios.get(this.userRolesUrl)
.then((response) => {
this.userRoles = response.data.data;
});
}
},
stripTime(date) {
if (date) {
return new Date(Date.UTC(

View file

@ -1,15 +1,27 @@
<template>
<div v-if="params.data.status" class="py-0.5">
<SelectDropdown
:options="params.statusesList"
:value="params.data.status"
@change="changeStatus"
size="xs"
:borderless="true"
:optionRenderer="optionRenderer"
:labelRenderer="optionRenderer"
:disabled="!params.data.urls.update"
/>
<div v-if="params.data.status">
<div v-if="params.data.urls.update" class="py-0.5">
<SelectDropdown
:options="params.statusesList"
:value="params.data.status"
@change="changeStatus"
size="xs"
:borderless="true"
:optionRenderer="optionRenderer"
:labelRenderer="optionRenderer"
/>
</div>
<div v-else class="flex items-center gap-2 py-0.5">
<div class="w-3 h-3 rounded-full"
:class="{
'bg-sn-grey-500': params.data.status === 'not_started',
'bg-sn-science-blue': params.data.status === 'in_progress',
'bg-sn-alert-green': params.data.status === 'done'
}"></div>
<span class="truncate">
{{ this.i18n.t(`projects.index.status.${params.data.status}`) }}
</span>
</div>
</div>
</template>

View file

@ -2,7 +2,7 @@
<div v-if="params.data.supervised_by">
<GeneralDropdown v-if="canManage" @open="openFlyout" @close="closeFlyout" position="right">
<template v-slot:field>
<div class="flex items-center gap-1 cursor-pointer h-9">
<div class="flex items-center gap-1 cursor-pointer h-9 px-1" :class="{ 'bg-sn-super-light-blue': opened }">
<div v-if="params.data.supervised_by.id" class="flex items-center gap-2" :title="params.data.supervised_by.name">
<img :src="params.data.supervised_by.avatar" class="w-7 h-7 rounded-full" />
<span>{{ params.data.supervised_by.name }}</span>
@ -67,7 +67,8 @@ export default {
data() {
return {
allUsers: [],
query: ''
query: '',
opened: false
};
},
watch: {
@ -83,6 +84,7 @@ export default {
methods: {
openFlyout() {
this.loadUsers();
this.opened = true;
this.$nextTick(() => {
if (this.$refs.searchInput) {
this.$refs.searchInput.focus();
@ -102,6 +104,7 @@ export default {
},
closeFlyout() {
this.query = '';
this.opened = false;
},
selectUser(user) {
if (user[0] === this.params.data.supervised_by.id) {

View file

@ -9,10 +9,18 @@
</div>
</template>
<template v-else>
<a class="task-section-caret" tabindex="0" role="button" data-toggle="collapse" href="#protocol-content" aria-expanded="true" aria-controls="protocol-content">
<a class="task-section-caret"
tabindex="0"
role="button"
data-toggle="collapse"
href="#protocol-content"
aria-expanded="true"
aria-controls="protocol-content"
data-e2e="e2e-IC-task-protocol-visibilityToggle"
>
<i class="sn-icon sn-icon-right"></i>
<div class="task-section-title truncate">
<h2>{{ i18n.t('Protocol') }}</h2>
<h2 data-e2e="e2e-TX-task-protocol-sectionTitle">{{ i18n.t('Protocol') }}</h2>
</div>
</a>
</template>
@ -29,16 +37,30 @@
:title="i18n.t('protocols.steps.new_step_title')"
@keyup.enter="addStep(steps.length)"
@click="addStep(steps.length)"
tabindex="0">
tabindex="0"
data-e2e="e2e-BT-task-protocol-newStep">
<span class="sn-icon sn-icon-new-task" aria-hidden="true"></span>
<span class="tw-hidden xl:inline">{{ i18n.t("protocols.steps.new_step") }}</span>
</a>
<template v-if="steps.length > 0">
<button :title="i18n.t('protocols.steps.collapse_label')" v-if="!stepCollapsed" class="btn btn-secondary icon-btn xl:!px-4" @click="collapseSteps" tabindex="0">
<button
:title="i18n.t('protocols.steps.collapse_label')"
v-if="!stepCollapsed"
class="btn btn-secondary icon-btn xl:!px-4"
@click="collapseSteps"
tabindex="0"
data-e2e="e2e-BT-task-protocol-collapseAll"
>
<i class="sn-icon sn-icon-collapse-all"></i>
<span class="tw-hidden xl:inline">{{ i18n.t("protocols.steps.collapse_label") }}</span>
</button>
<button v-else :title="i18n.t('protocols.steps.expand_label')" class="btn btn-secondary icon-btn xl:!px-4" @click="expandSteps" tabindex="0">
<button v-else
:title="i18n.t('protocols.steps.expand_label')"
class="btn btn-secondary icon-btn xl:!px-4"
@click="expandSteps"
tabindex="0"
data-e2e="e2e-BT-task-protocol-expandAll"
>
<i class="sn-icon sn-icon-expand-all"></i>
<span class="tw-hidden xl:inline">{{ i18n.t("protocols.steps.expand_label") }}</span>
</button>
@ -51,7 +73,13 @@
@protocol:add_protocol_steps="addSteps"
:canDeleteSteps="steps.length > 0 && urls.delete_steps_url !== null"
/>
<button class="btn btn-light icon-btn" data-toggle="modal" data-target="#print-protocol-modal" tabindex="0">
<button
class="btn btn-light icon-btn"
data-toggle="modal"
data-target="#print-protocol-modal"
tabindex="0"
data-e2e="e2e-BT-task-protocol-print"
>
<span class="sn-icon sn-icon-printer" aria-hidden="true"></span>
</button>
<a v-if="steps.length > 0 && urls.reorder_steps_url"
@ -60,13 +88,20 @@
@click="startStepReorder"
@keyup.enter="startStepReorder"
:class="{'disabled': steps.length == 1}"
tabindex="0" >
tabindex="0"
data-e2e="e2e-BT-task-protocol-reorderSteps"
>
<i class="sn-icon sn-icon-sort" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
<div id="protocol-content" class="protocol-content collapse in" aria-expanded="true">
<div
id="protocol-content"
class="protocol-content collapse in"
aria-expanded="true"
data-e2e="e2e-CO-task-protocol-content"
>
<div class="sci-divider" v-if="!inRepository"></div>
<div class="mb-4">
<div class="protocol-name mt-4" v-if="!inRepository">
@ -78,6 +113,7 @@
:allowBlank="!inRepository"
:attributeName="`${i18n.t('Protocol')} ${i18n.t('name')}`"
@update="updateName"
:dataE2e="'task-protocol-title'"
/>
<span v-else>
{{ protocol.attributes.name }}

View file

@ -1,18 +1,26 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-dialog" role="document" data-e2e="e2e-MD-protocol-addProtocolSteps">
<form @submit.prevent="submit">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
data-e2e="e2e-BT-protocol-addProtocolStepsModal-close"
>
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" >
<h4 class="modal-title truncate !block" data-e2e="e2e-TX-protocol-addProtocolStepsModal-title">
{{ i18n.t('protocols.steps.modals.add_protocol_steps.title') }}
</h4>
</div>
<div class="modal-body">
<p class="mb-6">{{ i18n.t('protocols.steps.modals.add_protocol_steps.description')}}</p>
<p class="mb-6" data-e2e="e2e-TX-protocol-addProtocolStepsModal-description">
{{ i18n.t('protocols.steps.modals.add_protocol_steps.description')}}
</p>
<div class="mb-6">
<label class="sci-label">{{ i18n.t('protocols.steps.modals.add_protocol_steps.protocol_label') }}</label>
<SelectDropdown
@ -21,6 +29,7 @@
:searchable="true"
:value="selectedProtocol"
@change="selectedProtocol = $event"
:e2eValue="'e2e-DD-protocol-addProtocolStepsModal-selectProtocol'"
></SelectDropdown>
</div>
<div class="relative">
@ -36,12 +45,25 @@
:searchable="true"
:value="selectedSteps"
@change="selectedSteps= $event"
:e2eValue="'e2e-DD-protocol-addProtocolStepsModal-selectSteps'"
></SelectDropdown>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" :disabled="submitting || !validObject" type="submit">
<button
type="button"
class="btn btn-secondary"
data-dismiss="modal"
data-e2e="e2e-BT-protocol-addProtocolStepsModal-cancel"
>
{{ i18n.t('general.cancel') }}
</button>
<button
class="btn btn-primary"
:disabled="submitting || !validObject"
type="submit"
data-e2e="e2e-BT-protocol-addProtocolStepsModal-addSteps"
>
{{ i18n.t('protocols.steps.modals.add_protocol_steps.confirm') }}
</button>
</div>

View file

@ -1,20 +1,44 @@
<template>
<div ref="modal" @keydown.esc="cancel" class="modal delete-steps-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-content" data-e2e="e2e-MD-task-protocol-deleteAllSteps">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<h4 class="modal-title">
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
data-e2e="e2e-BT-task-protocol-deleteAllStepsModal-close"
>
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title" data-e2e="e2e-TX-task-protocol-deleteAllStepsModal-title">
{{ i18n.t('protocols.steps.modals.delete_steps.title')}}
</h4>
</div>
<div class="modal-body">
<p>{{ i18n.t('protocols.steps.modals.delete_steps.description_1')}}</p>
<p class="warning">{{ i18n.t('protocols.steps.modals.delete_steps.description_2')}}</p>
<p data-e2e="e2e-TX-task-protocol-deleteAllStepsModal-description">
{{ i18n.t('protocols.steps.modals.delete_steps.description_1')}}
</p>
<p class="warning" data-e2e="e2e-TX-task-protocol-deleteAllStepsModal-warning">
{{ i18n.t('protocols.steps.modals.delete_steps.description_2')}}
</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="cancel">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-danger" @click="confirm">{{ i18n.t('protocols.steps.modals.delete_steps.confirm')}}</button>
<button
class="btn btn-secondary"
@click="cancel"
data-e2e="e2e-BT-task-protocol-deleteAllStepsModal-cancel"
>
{{ i18n.t('general.cancel') }}
</button>
<button
class="btn btn-danger"
@click="confirm"
data-e2e="e2e-BT-task-protocol-deleteAllStepsModal-delete"
>
{{ i18n.t('protocols.steps.modals.delete_steps.confirm')}}
</button>
</div>
</div>
</div>

View file

@ -9,6 +9,7 @@
aria-haspopup="true"
aria-expanded="true"
tabindex="0"
data-e2e="e2e-DD-task-protocol-actions"
>
<span>{{ i18n.t("my_modules.protocol.options_dropdown.title") }}</span>
<span class="sn-icon sn-icon-down"></span>
@ -22,6 +23,7 @@
ref="loadProtocol"
data-action="load-from-repository"
@click="loadProtocol"
data-e2e="e2e-DO-task-protocol-actions-loadFromRepository"
>
<span>{{ i18n.t("my_modules.protocol.options_dropdown.load_from_repo") }}</span>
</a>
@ -30,6 +32,7 @@
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
data-turbolinks="false"
@click.prevent="openAddStepsModal()"
data-e2e="e2e-DO-task-protocol-actions-addProtocolSteps"
>
<span>{{
i18n.t("my_modules.protocol.options_dropdown.add_protocol_steps")
@ -42,6 +45,7 @@
data-target="#newProtocolModal"
v-bind:data-protocol-name="protocol.attributes.name"
:class="{ disabled: !protocol.attributes.urls.save_to_repo_url }"
data-e2e="e2e-DO-task-protocol-actions-saveAsNewTemplate"
>
<span>{{
i18n.t("my_modules.protocol.options_dropdown.save_to_repo")
@ -53,6 +57,7 @@
data-turbolinks="false"
:href="protocol.attributes.urls.export_url"
:class="{ disabled: !protocol.attributes.urls.export_url }"
data-e2e="e2e-DO-task-protocol-actions-exportProtocol"
>
<span>{{
i18n.t("my_modules.protocol.options_dropdown.export")
@ -64,6 +69,7 @@
ref="updateProtocol"
data-action="update-self"
@click="updateProtocol"
data-e2e="e2e-DO-task-protocol-actions-updateProtocol"
>
<span>{{
i18n.t("my_modules.protocol.options_dropdown.update_protocol")
@ -75,6 +81,7 @@
ref="unlinkProtocol"
data-action="unlink"
@click="unlinkProtocol"
data-e2e="e2e-DO-task-protocol-actions-unlinkProtocol"
>
<span>{{
i18n.t("my_modules.protocol.options_dropdown.unlink")
@ -86,6 +93,7 @@
ref="revertProtocol"
data-action="revert"
@click="revertProtocol"
data-e2e="e2e-DO-task-protocol-actions-revertProtocol"
>
<span>{{
i18n.t("my_modules.protocol.options_dropdown.revert_protocol")
@ -96,6 +104,7 @@
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
data-turbolinks="false"
@click.prevent="openStepsDeletingModal()"
data-e2e="e2e-DO-task-protocol-actions-deleteAllSteps"
>
<span>{{
i18n.t("my_modules.protocol.options_dropdown.delete_steps")

View file

@ -65,6 +65,7 @@
:position="'right'"
:caret="true"
:dataE2e="`e2e-DD-protocol-step${step.id}-insertContent`"
:disableOverflow="true"
@create:custom_well_plate="openCustomWellPlateModal"
@create:table="(...args) => this.createElement('table', ...args)"
@create:checklist="createElement('checklist')"
@ -661,7 +662,7 @@
}
}
}).fail(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
HelperModule.flashAlertMsg(this.i18n.t('errors.general_saving_data'), 'danger');
})
}
},

View file

@ -22,26 +22,10 @@
:placeholder="i18n.t('protocols.new_protocol_modal.name_placeholder')" />
</div>
</div>
<div class="flex gap-2 text-xs items-center">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox" v-model="visible" value="visible" data-e2e="e2e-CB-newProtocolModal-grantAccess"/>
<span class="sci-checkbox-label"></span>
</div>
<span v-html="i18n.t('protocols.new_protocol_modal.access_label')" data-e2e="e2e-TX-newProtocolModal-grantAccess"></span>
</div>
<div class="mt-6" :class="{'hidden': !visible}">
<label class="sci-label">{{ i18n.t("protocols.new_protocol_modal.role_label") }}</label>
<SelectDropdown
:options="userRoles"
:value="defaultRole"
:data-e2e="`e2e-DD-newProtocolModal-defaultUserRole`"
@change="changeRole"
/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" data-e2e="e2e-BT-newProtocolModal-cancel">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" type="submit" :disabled="submitting || (visible && !defaultRole) || !validName" data-e2e="e2e-BT-newProtocolModal-create">
<button class="btn btn-primary" type="submit" :disabled="submitting || !validName" data-e2e="e2e-BT-newProtocolModal-create">
{{ i18n.t('protocols.new_protocol_modal.create_new') }}
</button>
</div>
@ -54,32 +38,15 @@
<script>
/* global GLOBAL_CONSTANTS */
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'NewProtocolModal',
props: {
createUrl: String,
userRolesUrl: String
createUrl: String
},
mixins: [modalMixin],
components: {
SelectDropdown
},
watch: {
visible(newValue) {
if (newValue) {
[this.defaultRole] = this.userRoles.find((role) => role[1] === 'Viewer');
} else {
this.defaultRole = null;
}
}
},
mounted() {
this.fetchUserRoles();
},
computed: {
validName() {
return this.name.length >= GLOBAL_CONSTANTS.NAME_MIN_LENGTH;
@ -88,9 +55,6 @@ export default {
data() {
return {
name: '',
visible: false,
defaultRole: null,
userRoles: [],
error: null,
submitting: false
};
@ -101,9 +65,7 @@ export default {
axios.post(this.createUrl, {
protocol: {
name: this.name,
visibility: (this.visible ? 'visible' : 'hidden'),
default_public_user_role_id: this.defaultRole
name: this.name
}
}).then(() => {
this.error = null;
@ -113,17 +75,6 @@ export default {
this.submitting = false;
this.error = error.response.data.error;
});
},
changeRole(role) {
this.defaultRole = role;
},
fetchUserRoles() {
if (this.userRolesUrl) {
axios.get(this.userRolesUrl)
.then((response) => {
this.userRoles = response.data.data;
});
}
}
}
};

View file

@ -25,7 +25,6 @@
/>
</div>
<NewProtocolModal v-if="newProtocol" :createUrl="createUrl"
:userRolesUrl="userRolesUrl"
@close="newProtocol = false" @create="updateTable" />
<AccessModal v-if="accessModalParams" :params="accessModalParams"
@close="accessModalParams = null" @refresh="this.reloadingTable = true" />

View file

@ -108,7 +108,7 @@ export default {
mounted() {
document.addEventListener('mouseover', this.loadColumnsInfo);
},
beforeDestroy() {
beforeUnmount() {
document.removeEventListener('mouseover', this.loadColumnsInfo);
},
computed: {

View file

@ -1,6 +1,9 @@
<template>
<a class="hover:no-underline flex items-center gap-1"
:title="params.data.name"
:class="{
'pointer-events-none text-sn-grey': !params.data.urls.show
}"
:href="params.data.urls.show"
>
<span class="truncate">

View file

@ -18,6 +18,7 @@
@share="share"
@create="newRepository = true"
@tableReloaded="reloadingTable = false"
@access="access"
/>
</div>
<ConfirmationModal
@ -75,6 +76,8 @@
confirm: 'e2e-BT-confirmSharingChangesModal-delete'
}"
></ConfirmationModal>
<AccessModal v-if="accessModalParams" :params="accessModalParams"
@close="accessModalParams = null" @refresh="reloadingTable = true" />
</template>
<script>
@ -89,6 +92,9 @@ import DuplicateRepositoryModal from './modals/duplicate.vue';
import ShareObjectModal from '../shared/share_modal.vue';
import DataTable from '../shared/datatable/table.vue';
import NameRenderer from './renderers/name.vue';
import AccessModal from '../shared/access_modal/modal.vue';
import UsersRenderer from '../projects/renderers/users.vue';
import escapeHtml from '../shared/escape_html.js';
export default {
name: 'RepositoriesTable',
@ -100,7 +106,9 @@ export default {
EditRepositoryModal,
DuplicateRepositoryModal,
NameRenderer,
ShareObjectModal
ShareObjectModal,
AccessModal,
UsersRenderer
},
props: {
dataSource: {
@ -125,11 +133,16 @@ export default {
archivedPageUrl: {
type: String,
required: true
},
userRolesUrl: {
type: String,
required: true
}
},
data() {
return {
reloadingTable: false,
accessModalParams: null,
exportRepository: null,
newRepository: false,
editRepository: null,
@ -186,8 +199,14 @@ export default {
field: 'created_at',
headerName: this.i18n.t('libraries.index.table.added_on'),
sortable: true
},
{
}, {
field: 'assigned_users',
headerName: this.i18n.t('repositories.index.table.access'),
sortable: true,
cellRenderer: 'UsersRenderer',
minWidth: 210,
notSelectable: true
}, {
field: 'created_by',
headerName: this.i18n.t('libraries.index.table.added_by'),
sortable: true
@ -252,6 +271,12 @@ export default {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
access(_event, rows) {
this.accessModalParams = {
object: rows[0],
roles_path: this.userRolesUrl
};
},
async deleteRepository(event, rows) {
const [repository] = rows;
this.deleteModal.e2eAttributes = {
@ -263,13 +288,14 @@ export default {
};
this.deleteModal.title = this.i18n.t('repositories.index.modal_delete.title_html', { name: repository.name });
this.deleteModal.description = `
<p data-e2e="e2e-TX-deleteInventoryModal-info">${this.i18n.t('repositories.index.modal_delete.message_html', { name: repository.name })}</p>
<p data-e2e="e2e-TX-deleteInventoryModal-info">${this.i18n.t('repositories.index.modal_delete.message_html', { name: escapeHtml(repository.name) })}</p>
<div class="alert alert-danger" role="alert" data-e2e="e2e-TX-deleteInventoryModal-warning">
<span class="fas fa-exclamation-triangle"></span>
${this.i18n.t('repositories.index.modal_delete.alert_heading')}
<ul>
<li>${this.i18n.t('repositories.index.modal_delete.alert_line_1')}</li>
<li>${this.i18n.t('repositories.index.modal_delete.alert_line_2')}</li>
<li>${this.i18n.t('repositories.index.modal_delete.alert_line_3')}</li>
</ul>
</div>
`;

View file

@ -23,7 +23,7 @@ export default {
archivedUrl: { type: String, required: true },
disabled: { type: String, default: 'false' }
},
beforeDestroy() {
beforeUnmount() {
delete window.initRepositoryStateMenu;
},
computed: {

View file

@ -184,14 +184,15 @@
</a>
</div>
<div v-if="parentsCount">
<details v-for="(parent) in parents" @toggle="updateOpenState(parent.code, $event.target.open)" :key="parent.code" class="flex flex-col font-normal gap-4 group cursor-default">
<details v-for="(parent) in parents" @toggle="updateOpenState(parent.code, $event.target.open)" :key="parent.code" class="flex flex-col font-normal group cursor-default">
<summary class="flex flex-row gap-3 mb-4 relative group">
<img :src="icons.delimiter_path" class="w-3 h-3 cursor-pointer flex-shrink-0 relative top-1"
:class="{ 'rotate-90': relationshipDetailsState[parent.code] }" />
<span>
<span>{{ i18n.t('repositories.item_card.relationships.item') }}</span>
<a :href="parent.path" class="record-info-link btn-text-link !text-sn-science-blue">{{ parent.name }}</a>
<button v-if="permissions.can_connect_rows" @click="openUnlinkModal(parent)"
<a v-if="parent.path" :href="parent.path" class="record-info-link btn-text-link !text-sn-science-blue">{{ parent.name }}</a>
<span v-else>{{ parent.name }}</span>
<button v-if="permissions.can_connect_rows && parent.can_connect_rows" @click="openUnlinkModal(parent)"
class=" ml-2 bg-transparent border-none opacity-0 group-hover:opacity-100 cursor-pointer">
<img :src="icons.unlink_path" />
</button>
@ -201,7 +202,7 @@
<span>
{{ i18n.t('repositories.item_card.relationships.id', { code: parent.code }) }}
</span>
<span>
<span v-if="parent.repository_name && parent.repository_path">
<span>{{ i18n.t('repositories.item_card.relationships.inventory') }}</span>
<a :href="parent.repository_path" class="btn-text-link !text-sn-science-blue">
{{ parent.repository_name }}
@ -233,15 +234,16 @@
</div>
<div v-if="childrenCount">
<details v-for="(child) in children" :key="child.code" @toggle="updateOpenState(child.code, $event.target.open)"
class="flex flex-col font-normal gap-4 group-last-of-type:[&>p:last-child]:mb-0">
class="flex flex-col font-normal group-last-of-type:[&>p:last-child]:mb-0">
<summary class="flex flex-row gap-3 mb-4 relative group"
:class="{ 'group-last-of-type:mb-0': !relationshipDetailsState[child.code] }">
<img :src="icons.delimiter_path" class="w-3 h-3 flex-shrink-0 cursor-pointer relative top-1"
:class="{ 'rotate-90': relationshipDetailsState[child.code] }"/>
<span class="group/child">
<span>{{ i18n.t('repositories.item_card.relationships.item') }}</span>
<a :href="child.path" class="record-info-link btn-text-link !text-sn-science-blue">{{ child.name }}</a>
<button v-if="permissions.can_connect_rows" @click="openUnlinkModal(child)"
<a v-if="child.path" :href="child.path" class="record-info-link btn-text-link !text-sn-science-blue">{{ child.name }}</a>
<span v-else>{{ child.name }}</span>
<button v-if="permissions.can_connect_rows && child.can_connect_rows" @click="openUnlinkModal(child)"
class="ml-2 bg-transparent border-none opacity-0 group-hover:opacity-100 cursor-pointer">
<img :src="icons.unlink_path" />
</button>
@ -249,7 +251,7 @@
</summary>
<p class="flex flex-col gap-3 ml-6 mb-4">
<span>{{ i18n.t('repositories.item_card.relationships.id', { code: child.code }) }}</span>
<span>
<span v-if="child.repository_name && child.repository_path">
<span>{{ i18n.t('repositories.item_card.relationships.inventory') }}</span>
<a :href="child.repository_path" class="btn-text-link !text-sn-science-blue">
{{ child.repository_name }}

View file

@ -60,7 +60,7 @@ export default {
}
});
},
beforeDestroy() {
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
this.removeScrollListener();
},

View file

@ -222,7 +222,7 @@ export default {
created() {
window.manageStockModalComponent = this;
},
beforeDestroy() {
beforeUnmount() {
delete window.manageStockModalComponent;
},
mounted() {

Some files were not shown because too many files have changed in this diff Show more