Merge branch 'develop' into features/design-update

This commit is contained in:
Martin Artnik 2025-08-21 10:46:50 +02:00
commit b82e8da538
137 changed files with 1398 additions and 865 deletions

View file

@ -58,7 +58,7 @@ gem 'jbuilder' # JSON structures via a Builder-style DSL
gem 'logging', '~> 2.0.0'
gem 'mime-types', '~> 3.4'
gem 'nested_form_fields'
gem 'nokogiri', '~> 1.18.8' # HTML/XML parser
gem 'nokogiri', '~> 1.18.9' # HTML/XML parser
gem 'noticed'
gem 'oj'
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
@ -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,14 +85,14 @@ 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 (2.2.0)
activerecord (>= 4.2)
@ -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)
@ -486,9 +474,9 @@ GEM
net-protocol
newrelic_rpm (9.14.0)
nio4r (2.7.4)
nokogiri (1.18.8-arm64-darwin)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
noticed (1.6.3)
http (>= 4.0.0)
@ -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)
@ -750,7 +814,7 @@ GEM
railties (>= 6.0.0)
tailwindcss-rails (2.4.0-x86_64-linux)
railties (>= 6.0.0)
thor (1.3.2)
thor (1.4.0)
tilt (2.4.0)
timecop (0.9.6)
timeout (0.4.3)
@ -836,7 +900,6 @@ DEPENDENCIES
cssbundling-rails
cucumber-rails
database_cleaner
datadog
deface (~> 1.9)
delayed_job_active_record
devise (~> 4.9.4)
@ -866,7 +929,7 @@ DEPENDENCIES
mime-types (~> 3.4)
nested_form_fields
newrelic_rpm
nokogiri (~> 1.18.8)
nokogiri (~> 1.18.9)
noticed
oj
omniauth (~> 2.1)
@ -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.1
1.44.0

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

@ -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

@ -75,6 +75,8 @@ module AccessPermissions
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
@ -106,7 +108,7 @@ module AccessPermissions
end
def show_user_group_assignments
render json: @model.user_group_assignments.includes(:user_role, :user_group).order('user_groups.name ASC'),
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
@ -122,7 +124,7 @@ module AccessPermissions
private
def model_parameter
@model.class.permission_class.name.parameterize.to_sym
@model.class.permission_class.model_name.param_key
end
def manage_permission_constant

View file

@ -7,7 +7,7 @@ module AccessPermissions
def update
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"],
"#{assignment_type}_id": permitted_params[:"#{assignment_type}_id"] || current_team.id,
team: current_team
)
@ -24,16 +24,18 @@ module AccessPermissions
)
end
UserAssignments::PropagateAssignmentJob.perform_later(@assignment, destroy: permitted_params[:user_role_id] == 'reset')
UserAssignments::PropagateAssignmentJob.perform_later(@assignment)
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

View file

@ -8,7 +8,7 @@ module AccessPermissions
def update
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"],
"#{assignment_type}_id": permitted_params[:"#{assignment_type}_id"] || current_team.id,
team: current_team
)
@ -26,11 +26,15 @@ module AccessPermissions
end
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 json: { user_role_id: @assignment.user_role_id }, status: :ok
end
private

View file

@ -15,7 +15,7 @@ module AccessPermissions
end
def check_read_permissions
render_403 unless can_read_repository?(@model) || can_manage_team?(@model.team)
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

@ -32,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

@ -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

@ -9,13 +9,13 @@ module Dashboard
date = params[:date].to_date
start_date = date.beginning_of_month - 8.days
end_date = date.end_of_month + 15.days
due_dates = current_user.my_modules.readable_by_user(current_user).active.uncomplete
.joins(experiment: :project)
.where(experiments: { archived: false })
.where(projects: { archived: false })
.where(my_modules: { due_date: start_date..end_date })
.joins(:protocols).where(protocols: { team_id: current_team.id })
.pluck(:due_date)
due_dates = MyModule.readable_by_user(current_user).active.uncomplete
.joins(experiment: :project)
.where(experiments: { archived: false })
.where(projects: { archived: false })
.where(my_modules: { due_date: start_date..end_date })
.joins(:protocols).where(protocols: { team_id: current_team.id })
.pluck(:due_date)
render json: { events: due_dates.map { |i| { date: i.to_date } } }
end
@ -23,12 +23,12 @@ module Dashboard
date = params[:date].to_date
start_date = date.beginning_of_day
end_date = date.end_of_day
my_modules = current_user.my_modules.readable_by_user(current_user).active.uncomplete
.joins(experiment: :project)
.where(experiments: { archived: false })
.where(projects: { archived: false })
.where(my_modules: { due_date: start_date..end_date })
.where(projects: { team_id: current_team.id })
my_modules = MyModule.readable_by_user(current_user).active.uncomplete
.joins(experiment: :project)
.where(experiments: { archived: false })
.where(projects: { archived: false })
.where(my_modules: { due_date: start_date..end_date })
.where(projects: { team_id: current_team.id })
render json: {
html: render_to_string(partial: 'shared/my_modules_list_partial',
locals: { my_modules: my_modules },

View file

@ -34,7 +34,7 @@ module Dashboard
.search_by_name(current_user, current_team, params[:query]).select(:id, :name)
unless params[:mode] == 'team'
projects = projects.where(id: current_user.my_modules.joins(:experiment)
projects = projects.where(id: MyModule.readable_by_user(current_user).joins(:experiment)
.group(:project_id).select(:project_id).pluck(:project_id))
end
render json: projects.map { |i| { value: i.id, label: escape_input(i.name) } }, status: :ok
@ -51,7 +51,7 @@ module Dashboard
.search_by_name(current_user, current_team, params[:query]).select(:id, :name)
unless params[:mode] == 'team'
experiments = experiments.where(id: current_user.my_modules
experiments = experiments.where(id: MyModule.readable_by_user(current_user)
.group(:experiment_id).select(:experiment_id).pluck(:experiment_id))
end
render json: experiments.map { |i| { value: i.id, label: escape_input(i.name) } }, status: :ok

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

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

@ -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

@ -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(

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

@ -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
@ -320,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
@ -331,7 +294,7 @@ 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

View file

@ -1092,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

@ -12,7 +12,7 @@ class RepositoriesController < ApplicationController
before_action :switch_team_with_param, only: %i(index)
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 actions_toolbar)
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_permissions, except: %i(index create_modal create update destroy parse_sheet
@ -44,7 +44,14 @@ class RepositoriesController < ApplicationController
end
def list
results = @repositories.select(:id, :name, 'LOWER(repositories.name)')
repositories = if params[:appendable] == 'true'
Repository.appendable_by_user(current_user)
elsif 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?
@ -482,9 +489,7 @@ class RepositoriesController < ApplicationController
def load_repositories
@repositories =
if params[:appendable] == 'true'
Repository.appendable_by_user(current_user)
elsif can_manage_team?(current_team)
if can_manage_team?(current_team)
# Team owners see all repositories in the team
current_team.repositories.or(Repository.shared_with_team(current_team))
else

View file

@ -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

@ -213,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?

View file

@ -25,13 +25,9 @@ class TeamSharedObjectsController < ApplicationController
case global_permission_level
when :shared_read
UserAssignment.where(assignable: @model).where.not(team: @model.team).update!(user_role: UserRole.find_predefined_viewer_role)
TeamAssignment.where(assignable: @model).where.not(team: @model.team).update!(user_role: UserRole.find_predefined_viewer_role)
UserGroupAssignment.where(assignable: @model).where.not(team: @model.team).update!(user_role: UserRole.find_predefined_viewer_role)
@model.demote_all_sharing_assignments_to_viewer!
when :not_shared
UserAssignment.where(assignable: @model).where.not(team: @model.team).destroy_all
TeamAssignment.where(assignable: @model).where.not(team: @model.team).destroy_all
UserGroupAssignment.where(assignable: @model).where.not(team: @model.team).destroy_all
@model.destroy_all_sharing_assignments!
end
case @model

View file

@ -5,9 +5,19 @@ class Users::PasswordsController < Devise::PasswordsController
# end
# POST /resource/password
# def create
# super
# end
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?
if resource.errors.added?(:email, :blank)
flash.now[:alert] = I18n.t('devise.errors.email.empty')
self.resource = resource_class.new
render :new
else
set_flash_message!(:notice, :send_instructions)
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
end
end
# GET /resource/password/edit?reset_password_token=abcdef
# def edit
@ -25,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

@ -5,10 +5,10 @@ module Users
class UserGroupsController < ApplicationController
before_action :load_team
before_action :set_breadcrumbs_items, only: %i(index show)
before_action :check_user_groups_enabled
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(index show unassigned_users actions_toolbar users)
before_action :check_manage_permissions, except: %i(index show unassigned_users actions_toolbar users)
before_action :check_read_permissions, only: %i(users)
before_action :check_manage_permissions, except: %i(users)
def index
respond_to do |format|
@ -58,7 +58,7 @@ module Users
end
render json: { message: t('user_groups.create.success') }, status: :created
else
render json: { errors: t('user_groups.create.error') }, status: :unprocessable_entity
render json: { error: @user_group.errors.full_messages.join(", ") }, status: :unprocessable_entity
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

@ -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

@ -7,7 +7,7 @@ module UserRolesHelper
viewer_role = UserRole.find_predefined_viewer_role
roles = [[viewer_role.name, viewer_role.id]]
else
permission_group = "#{object.class.name}Permissions".constantize
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)

View file

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

View file

@ -39,9 +39,16 @@ export default {
},
methods: {
optionRenderer(option) {
let color = 'bg-sn-grey-500';
if (option[0] === 'in_progress') {
color = 'bg-sn-science-blue';
} else if (option[0] === 'done') {
color = 'bg-sn-alert-green';
}
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

@ -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

@ -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

@ -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

@ -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

@ -94,6 +94,7 @@ 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',
@ -287,7 +288,7 @@ 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')}

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,7 +184,7 @@
</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] }" />
@ -192,7 +192,7 @@
<span>{{ i18n.t('repositories.item_card.relationships.item') }}</span>
<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" @click="openUnlinkModal(parent)"
<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>
@ -234,7 +234,7 @@
</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"
@ -243,7 +243,7 @@
<span>{{ i18n.t('repositories.item_card.relationships.item') }}</span>
<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" @click="openUnlinkModal(child)"
<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>

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() {

View file

@ -1,21 +1,41 @@
<template>
<div ref="modal" @keydown.esc="cancel" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-content" data-e2e="e2e-MD-task-result-deleteResult">
<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-result-deleteResultModal-close"
>
<i class="sn-icon sn-icon-close">
</i></button>
<h4 class="modal-title" data-e2e="e2e-TX-task-result-deleteResultModal-title">
{{ i18n.t("my_modules.results.delete_modal.title") }}
</h4>
</div>
<div class="modal-body">
<div class="modal-body" data-e2e="e2e-TX-task-result-deleteResultModal-description">
<p>{{ i18n.t("my_modules.results.delete_modal.description_1") }}</p>
<p><b>{{ i18n.t("my_modules.results.delete_modal.description_2") }}</b></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("my_modules.results.delete_modal.confirm") }}</button>
<button
class="btn btn-secondary"
@click="cancel"
data-e2e="e2e-BT-task-result-deleteResultModal-cancel"
>
{{ i18n.t('general.cancel') }}
</button>
<button
class="btn btn-danger"
@click="confirm"
data-e2e="e2e-BT-task-result-deleteResultModal-delete"
>
{{ i18n.t("my_modules.results.delete_modal.confirm") }}
</button>
</div>
</div>
</div>

View file

@ -6,6 +6,7 @@
@dragover.prevent
:data-id="result.id"
:class="{ 'bg-sn-super-light-blue': dragingFile, 'bg-white': !dragingFile, 'locked': locked, 'pointer-events-none': addingContent }"
:data-e2e="`e2e-CO-task-result${result.id}`"
>
<div class="text-xl items-center flex flex-col text-sn-blue h-full justify-center left-0 absolute top-0 w-full"
v-if="dragingFile"
@ -20,6 +21,7 @@
:href="'#resultBody' + result.id"
data-toggle="collapse"
data-remote="true"
:data-e2e="`e2e-BT-task-result${result.id}-visibilityToggle`"
@click="toggleCollapsed">
<span class="sn-icon sn-icon-right "></span>
</a>
@ -35,6 +37,7 @@
:placeholder="i18n.t('my_modules.results.placeholder')"
:defaultValue="i18n.t('my_modules.results.default_name')"
:timestamp="i18n.t('protocols.steps.timestamp', {date: result.attributes.created_at, user: result.attributes.created_by })"
:data-e2e="`task-result${result.id}`"
@editingEnabled="editingName = true"
@editingDisabled="editingName = false"
:editOnload="result.newResult == true"
@ -48,6 +51,7 @@
:btnText="i18n.t('my_modules.results.insert.button')"
:position="'right'"
:caret="true"
:data-e2e="`e2e-DD-task-result${result.id}-insertContent`"
:disableOverflow="true"
@create:custom_well_plate="openCustomWellPlateModal"
@create:table="(...args) => this.createElement('table', ...args)"
@ -66,14 +70,26 @@
:data-object-type="result.attributes.type"
tabindex="0"
></span> <!-- Hidden element to support legacy code -->
<tempplate v-if="result.attributes.steps.length == 0">
<button v-if="urls.update_url" ref="linkButton" :title="i18n.t('my_modules.results.link_steps')" class="btn btn-light icon-btn" @click="this.openLinkStepsModal = true">
<template v-if="result.attributes.steps.length == 0">
<button
v-if="urls.update_url"
ref="linkButton"
:title="i18n.t('my_modules.results.link_steps')"
class="btn btn-light icon-btn"
@click="this.openLinkStepsModal = true"
:data-e2e="`e2e-BT-task-result${result.id}-linkStep`"
>
{{ i18n.t('my_modules.results.link_to_step') }}
</button>
</tempplate>
</template>
<GeneralDropdown v-else ref="linkedStepsDropdown" position="right">
<template v-slot:field>
<button ref="linkButton" class="btn btn-light icon-btn" :title="i18n.t('my_modules.results.linked_steps')">
<button
ref="linkButton"
class="btn btn-light icon-btn"
:title="i18n.t('my_modules.results.linked_steps')"
:data-e2e="`e2e-DD-task-result${result.id}-linkStep-showLinked`"
>
<i class="sn-icon sn-icon-steps"></i>
<span class="absolute top-1 -right-1 h-4 min-w-4 bg-sn-science-blue text-white flex items-center justify-center rounded-full text-[10px]">
{{ result.attributes.steps.length }}
@ -87,14 +103,18 @@
:title="step.name"
:href="protocolUrl(step.id)"
class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer block hover:no-underline text-sn-blue truncate"
:data-e2e="`e2e-BT-task-result${result.id}-linkStep-step${step.id}`"
>
{{ step.name }}
</a>
</div>
<template v-if="urls.update_url">
<hr class="my-0">
<div class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer text-sn-blue"
@click="this.openLinkStepsModal = true; $refs.linkedStepsDropdown.closeMenu()">
<div
class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer text-sn-blue"
@click="this.openLinkStepsModal = true; $refs.linkedStepsDropdown.closeMenu()"
:data-e2e="`e2e-BT-task-result${result.id}-linkStep-manage`"
>
{{ i18n.t('protocols.steps.manage_links') }}
</div>
</template>
@ -105,7 +125,9 @@
class="open-comments-sidebar btn icon-btn btn-light"
data-turbolinks="false"
data-object-type="Result"
:data-object-id="result.id">
:data-object-id="result.id"
:data-e2e="`e2e-BT-task-result${result.id}-comments`"
>
<i class="sn-icon sn-icon-comments"></i>
<span class="comments-counter" :class="{ 'hidden': !result.attributes.comments_count }"
:id="`comment-count-${result.id}`">
@ -119,6 +141,7 @@
:btnClasses="'btn btn-light icon-btn'"
:position="'right'"
:btnIcon="'sn-icon sn-icon-more-hori'"
:data-e2e="`e2e-DD-task-result${result.id}-optionsMenu`"
@reorder="openReorderModal"
@duplicate="duplicateResult"
@archive="archiveResult"
@ -244,14 +267,46 @@ export default {
customWellPlate: false,
openLinkStepsModal: false,
wellPlateOptions: [
{ text: I18n.t('protocols.steps.insert.well_plate_options.custom'), emit: 'create:custom_well_plate'},
{ text: I18n.t('protocols.steps.insert.well_plate_options.32_x_48'), emit: 'create:table', params: [32, 48] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.16_x_24'), emit: 'create:table', params: [16, 24] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.8_x_12'), emit: 'create:table', params: [8, 12] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.6_x_8'), emit: 'create:table', params: [6, 8] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.4_x_6'), emit: 'create:table', params: [4, 6] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.3_x_4'), emit: 'create:table', params: [3, 4] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.2_x_3'), emit: 'create:table', params: [2, 3] }
{
text: I18n.t('protocols.steps.insert.well_plate_options.custom'),
emit: 'create:custom_well_plate',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-custom`
},
{
text: I18n.t('protocols.steps.insert.well_plate_options.32_x_48'), emit: 'create:table',
params: [32, 48],
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-32`
},
{
text: I18n.t('protocols.steps.insert.well_plate_options.16_x_24'), emit: 'create:table',
params: [16, 24],
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-16`
},
{
text: I18n.t('protocols.steps.insert.well_plate_options.8_x_12'), emit: 'create:table',
params: [8, 12],
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-8`
},
{
text: I18n.t('protocols.steps.insert.well_plate_options.6_x_8'), emit: 'create:table',
params: [6, 8],
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-6`
},
{
text: I18n.t('protocols.steps.insert.well_plate_options.4_x_6'), emit: 'create:table',
params: [4, 6],
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-4`
},
{
text: I18n.t('protocols.steps.insert.well_plate_options.3_x_4'), emit: 'create:table',
params: [3, 4],
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-3`
},
{
text: I18n.t('protocols.steps.insert.well_plate_options.2_x_3'), emit: 'create:table',
params: [2, 3],
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate-2`
}
],
editingName: false,
confirmingDelete: false,
@ -329,25 +384,29 @@ export default {
if (this.urls.upload_attachment_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.insert.add_file'),
emit: 'create:file'
emit: 'create:file',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-file-addFromPc`
}]);
}
if (this.result.attributes.wopi_enabled) {
menu = menu.concat([{
text: this.i18n.t('assets.create_wopi_file.button_text'),
emit: 'create:wopi_file'
emit: 'create:wopi_file',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-file-wopi`
}]);
}
if (this.result.attributes.open_vector_editor_context.new_sequence_asset_url) {
menu = menu.concat([{
text: this.i18n.t('open_vector_editor.new_sequence_file'),
emit: 'create:ove_file'
emit: 'create:ove_file',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-file-sequence`
}]);
}
if (this.result.attributes.marvinjs_enabled) {
menu = menu.concat([{
text: this.i18n.t('marvinjs.new_button'),
emit: 'create:marvinjs_file'
emit: 'create:marvinjs_file',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-file-chemical`
}]);
}
return menu;
@ -358,21 +417,25 @@ export default {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.insert.text'),
icon: 'sn-icon sn-icon-result-text',
emit: 'create:text'
emit: 'create:text',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-text`
}, {
text: this.i18n.t('my_modules.results.insert.attachment'),
submenu: this.filesMenu,
icon: 'sn-icon sn-icon-file',
position: 'left'
position: 'left',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-file`
}, {
text: this.i18n.t('my_modules.results.insert.table'),
icon: 'sn-icon sn-icon-tables',
emit: 'create:table'
emit: 'create:table',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-table`
}, {
text: this.i18n.t('my_modules.results.insert.well_plate'),
icon: 'sn-icon sn-icon-tables',
submenu: this.wellPlateOptions,
position: 'left'
position: 'left',
data_e2e: `e2e-DO-task-result${this.result.id}-insertMenu-wellPlate`
}]);
}
@ -383,31 +446,36 @@ export default {
if (this.urls.reorder_elements_url && this.elements.length > 1) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.rearrange'),
emit: 'reorder'
emit: 'reorder',
data_e2e: `e2e-DO-task-result${this.result.id}-optionsMenu-reorder`
}]);
}
if (this.urls.duplicate_url && !this.result.attributes.archived) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.duplicate'),
emit: 'duplicate'
emit: 'duplicate',
data_e2e: `e2e-DO-task-result${this.result.id}-optionsMenu-duplicate`
}]);
}
if (this.urls.archive_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.archive'),
emit: 'archive'
emit: 'archive',
data_e2e: `e2e-DO-task-result${this.result.id}-optionsMenu-archive`
}]);
}
if (this.urls.restore_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.restore'),
emit: 'restore'
emit: 'restore',
data_e2e: `e2e-DO-task-result${this.result.id}-optionsMenu-restore`
}]);
}
if (this.urls.delete_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.delete'),
emit: 'delete'
emit: 'delete',
data_e2e: `e2e-DO-task-result${this.result.id}-optionsMenu-delete`
}]);
}
return menu;

View file

@ -8,7 +8,14 @@
</div>
<div class="result-toolbar__left flex items-center">
<button v-if="canCreate" :title="i18n.t('my_modules.results.add_title')" class="btn btn-secondary" :class="{'mr-3': headerSticked}" @click="$emit('newResult')">
<button
v-if="canCreate"
:title="i18n.t('my_modules.results.add_title')"
class="btn btn-secondary"
:class="{'mr-3': headerSticked}"
@click="$emit('newResult')"
data-e2e="e2e-BT-task-results-createNew"
>
<i class="sn-icon sn-icon-new-task"></i>
{{ i18n.t('my_modules.results.add_label') }}
</button>

View file

@ -37,7 +37,7 @@
<perfect-scrollbar class="max-h-80 relative">
<div v-for="userGroup in filteredUserGroups" :key="userGroup.id" class="py-2 flex items-center w-full">
<div>
<img src="/images/icon/group.png" class="rounded-full w-8 h-8">
<img src="/images/icon/group.svg" class="rounded-full w-8 h-8">
</div>
<div
class="truncate ml-2"

View file

@ -50,11 +50,11 @@
<div>
<img
class="rounded-full w-8 h-8"
src="/images/icon/group.png"
src="/images/icon/group.svg"
>
</div>
<div class="truncate">
<div class="flex flex-row gap-2">
<div class="flex flex-row gap-2 items-center">
<div class="truncate"
:title="userGroupAssignment.attributes.user_group.name"
>{{ userGroupAssignment.attributes.user_group.name }}</div>
@ -114,7 +114,7 @@
>
</div>
<div class="truncate">
<div class="flex flex-row gap-2">
<div class="flex flex-row gap-2 items-center">
<div class="truncate"
:title="userAssignment.attributes.user.name"
:data-e2e="`e2e-TX-${dataE2e}-${userAssignment.attributes.user.name.replace(/\W/g, '')}-name`"
@ -329,7 +329,7 @@ export default {
if (!roleId) {
this.$emit('changeVisibility', false, null);
} else {
this.$emit('changeVisibility', true, roleId);
this.$emit('changeVisibility', true, response.data.user_role_id);
}
if (response.data.message) {
HelperModule.flashAlertMsg(response.data.message, 'success');

View file

@ -94,7 +94,7 @@ export default {
this.setContainerSize();
window.addEventListener('resize', this.setContainerSize);
},
beforeDestroy() {
beforeUnmount() {
window.removeEventListener('resize', this.setContainerSize);
},
methods: {

View file

@ -541,9 +541,10 @@ export default {
this.handleScroll();
})
.catch(() => {
.catch((e) => {
this.dataLoading = false;
this.$emit('tableReloaded', [], { filtered: this.searchValue.length > 0 });
console.error(e);
window.HelperModule.flashAlertMsg(this.i18n.t('general.error'), 'danger');
});
},
@ -558,6 +559,9 @@ export default {
this.rowData = newRows;
if (this.gridApi) {
const viewport = document.querySelector('.ag-body-viewport');
if (!viewport) return;
const { scrollTop } = viewport;
this.gridApi.setRowData(this.rowData);
this.$nextTick(() => {

View file

@ -6,6 +6,7 @@
<template v-if="isOpen">
<teleport to="body">
<div @click="closeOnClick && closeMenu()" ref="flyout"
:id="randomId"
class="sn-dropdown fixed z-[3000] bg-sn-white inline-block
rounded p-2.5 sn-shadow-menu-sm"
:class="{
@ -42,7 +43,8 @@ export default {
},
data() {
return {
isOpen: false
isOpen: false,
randomId: `dropdown-${Math.random().toString(36).substring(2, 15)}`,
};
},
directives: {
@ -70,7 +72,7 @@ export default {
}
},
closeMenu(e) {
if (e && e.target.closest('.sn-dropdown, .sn-select-dropdown, .sn-menu-dropdown, .dp__instance_calendar')) return;
if (e && e.target.closest(`.sn-dropdown#${this.randomId}, .sn-select-dropdown, .sn-menu-dropdown, .dp__instance_calendar`)) return;
this.isOpen = false;
}
}

View file

@ -79,6 +79,10 @@ export default {
excludeRows: {
type: Array,
default: () => []
},
manageableRepositoriesOnly: {
type: Boolean,
default: false
}
},
created() {
@ -97,7 +101,7 @@ export default {
mounted() {
document.addEventListener('mouseover', this.loadColumnsInfo);
},
beforeDestroy() {
beforeUnmount() {
document.removeEventListener('mouseover', this.loadColumnsInfo);
},
watch: {
@ -116,7 +120,7 @@ export default {
},
computed: {
repositoriesUrl() {
return list_team_repositories_path(this.teamId, { non_empty: true, active: true });
return list_team_repositories_path(this.teamId, { non_empty: true, active: true, manageable: this.manageableRepositoriesOnly });
},
rowsUrl() {
if (!this.selectedRepository) {

View file

@ -60,8 +60,9 @@
<template v-if="isOpen">
<teleport to="body">
<div ref="flyout"
class="sn-select-dropdown bg-white inline-block sn-shadow-menu-sm rounded w-full
fixed z-[3000]">
class="sn-select-dropdown bg-white inline-block sn-shadow-menu-sm rounded w-full fixed z-[3000]"
:data-e2e="`${e2eValue}-dropdownOptions`"
>
<div v-if="multiple && withCheckboxes" class="p-2.5 pb-0">
<div @click="selectAll" :class="sizeClass"
class="border border-x-0 !border-transparent border-solid !border-b-sn-light-grey
@ -72,7 +73,7 @@
{{ i18n.t('general.select_all') }}
</div>
</div>
<perfect-scrollbar ref="scrollContainer" class="p-2.5 flex flex-col max-h-80 relative" :class="{ 'pt-0': withCheckboxes }">
<div ref="scrollContainer" class="p-2.5 flex flex-col max-h-80 relative overflow-y-auto" :class="{ 'pt-0': withCheckboxes }">
<template v-for="(option, i) in filteredOptions" :key="option[0]">
<div
@click.stop="setValue(option[0])"
@ -98,7 +99,7 @@
<div v-if="filteredOptions.length === 0" class="text-sn-grey text-center py-2.5">
{{ noOptionsPlaceholder || this.i18n.t('general.select_dropdown.no_options_placeholder') }}
</div>
</perfect-scrollbar>
</div>
</div>
</teleport>
</template>
@ -148,7 +149,8 @@ export default {
fixedWidth: true,
focusedOption: null,
skipQueryCallback: false,
nextPage: 1
nextPage: 1,
totalOptionsCount: this.options.length
};
},
mixins: [FixedFlyoutMixin],
@ -225,11 +227,11 @@ export default {
if (this.newValue.length === 0) {
return false;
}
if (this.newValue.length === 1 && this.rawOptions.length > 1) {
if (this.newValue.length === 1 && this.totalOptionsCount > 1) {
this.selectAllState = 'indeterminate';
return this.renderLabel(this.rawOptions.find((option) => option[0] === this.newValue[0]));
}
if (this.newValue.length === this.rawOptions.length) {
if (this.newValue.length === this.totalOptionsCount) {
this.selectAllState = 'checked';
return this.allOptionsPlaceholder || this.i18n.t('general.select_dropdown.all_options_placeholder');
}
@ -263,7 +265,6 @@ export default {
if (!this.newValue && this.multiple) {
this.newValue = [];
}
this.fetchOptions();
},
watch: {
value(newValue) {
@ -275,7 +276,7 @@ export default {
this.$nextTick(() => {
this.setPosition();
this.$refs.search?.focus();
this.$refs.scrollContainer.$el.addEventListener('scroll', this.loadNextPage);
this.$refs.scrollContainer.addEventListener('scroll', this.loadNextPage);
});
}
},
@ -295,7 +296,7 @@ export default {
this.fetchOptions();
},
loadNextPage() {
const container = this.$refs.scrollContainer.$el;
const container = this.$refs.scrollContainer;
if (this.nextPage && container.scrollTop + container.clientHeight >= container.scrollHeight) {
this.fetchOptions();
}
@ -316,7 +317,15 @@ export default {
return this.newValue === value;
},
open() {
if (!this.disabled) this.isOpen = true;
if (this.disabled || this.isOpen) return;
this.isOpen = true;
if (this.optionsUrl) {
this.fetchedOptions = [];
this.nextPage = 1;
this.fetchOptions();
}
},
clear() {
this.newValue = this.multiple ? [] : null;
@ -390,6 +399,11 @@ export default {
} else {
this.fetchedOptions = response.data.data;
}
if (this.fetchedOptions.length > this.totalOptionsCount) {
this.totalOptionsCount = this.fetchedOptions.length;
}
this.$nextTick(() => {
this.setPosition();
});

View file

@ -66,6 +66,7 @@ import ShareObjectModal from '../shared/share_modal.vue';
import DescriptionRenderer from './renderers/description.vue';
import NameRenderer from './renderers/storage_name_renderer.vue';
import FindRowModal from './modals/find_row.vue';
import escapeHtml from '../shared/escape_html.js';
export default {
name: 'RepositoriesTable',
@ -264,7 +265,7 @@ export default {
const storageLocationType = rows[0].container ? this.i18n.t('storage_locations.container') : this.i18n.t('storage_locations.location');
const description = `
<p>${this.i18n.t('storage_locations.index.delete_modal.description_1_html',
{ name: rows[0].name, type: storageLocationType, num_of_items: event.number_of_items })}</p>
{ name: escapeHtml(rows[0].name), type: storageLocationType, num_of_items: event.number_of_items })}</p>
<p>${this.i18n.t('storage_locations.index.delete_modal.description_2_html')}</p>`;
this.storageLocationDeleteDescription = description;

View file

@ -21,6 +21,7 @@
:option-renderer="usersRenderer"
:label-renderer="usersRenderer"
:multiple="true"
:searchable="true"
:placeholder="i18n.t('user_groups.show.add_members_modal.select_members_placeholder')"
/>
</div>

View file

@ -32,6 +32,7 @@
:withCheckboxes="true"
:option-renderer="usersRenderer"
:label-renderer="usersRenderer"
:searchable="true"
:multiple="true"
:placeholder="i18n.t('user_groups.index.create_modal.select_members_placeholder')"
/>

View file

@ -49,7 +49,7 @@
</GeneralDropdown>
</div>
<div v-else>
<div class="flex items-center gap-1 cursor-pointer h-9">
<div class="flex items-center gap-1 h-9">
<div v-for="(user, i) in visibleUsers" :key="i" :title="user.full_name">
<img :src="user.avatar" class="w-7 h-7 rounded-full" />
</div>

View file

@ -88,7 +88,8 @@ module UserAssignments
new_assignment = parent_assignment.class.find_or_initialize_by(
"#{type}_id": parent_assignment.public_send(type).id,
assignable: resource
assignable: resource,
team_id: parent_assignment.team_id
)
return if new_assignment.manually_assigned?

View file

@ -28,12 +28,7 @@ module UserAssignments
next unless project.experiments.any?
# make sure all related experiments and my modules are assigned
UserAssignments::PropagateAssignmentJob.perform_later(
project,
user.id,
project.default_public_user_role || UserRole.find_predefined_viewer_role,
@assigned_by&.id
)
UserAssignments::PropagateAssignmentJob.perform_later(user_assignment)
end
end
end

View file

@ -2,6 +2,8 @@
module UserAssignments
class PropagateAssignmentJob < ApplicationJob
include Canaid::Helpers::PermissionsHelper
queue_as :high_priority
def perform(assignment, destroy: false, remove_from_team: false)
@ -13,6 +15,7 @@ module UserAssignments
ActiveRecord::Base.transaction do
@assignment.destroy! if destroy && !@assignment.destroyed?
cleanup!(@assignment)
sync_resource_user_associations(@resource)
end
end
@ -39,7 +42,7 @@ module UserAssignments
child_associations.find_each do |child_association|
if @destroy
destroy_or_update_assignment(child_association)
destroy_assignment(child_association)
else
create_or_update_assignment(child_association)
end
@ -47,7 +50,7 @@ module UserAssignments
sync_resource_user_associations(child_association)
end
destroy_or_update_assignment(resource) if resource.is_a?(Project) && @destroy
destroy_assignment(resource) if resource.is_a?(Project) && @destroy
end
def create_or_update_assignment(resource)
@ -66,31 +69,24 @@ module UserAssignments
)
end
def destroy_or_update_assignment(resource)
# also destroy user designations if it's a MyModule
resource.user_my_modules.where(user: @user).destroy_all if resource.is_a?(MyModule)
def destroy_assignment(resource)
assignment = resource.public_send(:"#{@type}_assignments").find_by(
"#{@type}_id" => @assignment.public_send(@type).id
)
return unless assignment
project = resource.is_a?(Project) ? resource : resource.project
assignment.destroy!
if project.default_public_user_role_id && !@remove_from_team
# if project is public, the assignment
# will reset to the default public role
cleanup!(assignment)
end
assignment.update!(
user_role_id: project.default_public_user_role_id,
assigned: :automatically,
assigned_by: @assignment.assigned_by
)
else
resource.favorites.where(user: @user).destroy_all if resource.respond_to?(:favorites)
assignment.destroy!
end
def cleanup!(assignment)
# clean up designations and favorites if user is no longer assigned
assigned_users = assignment.assignable.users
assignment.assignable.favorites.where.not(user: assigned_users).destroy_all if assignment.assignable.respond_to?(:favorites)
assignment.assignable.user_my_modules.where.not(user: assigned_users).destroy_all if assignment.assignable.is_a?(MyModule)
end
end
end

View file

@ -24,7 +24,7 @@ module Assignable
class_name: 'TeamAssignment',
inverse_of: :assignable
after_create :create_users_assignments
after_create :create_user_assignments!, unless: -> { skip_user_assignments }
def users
direct_user_ids = user_assignments.select(:user_id)
@ -41,6 +41,30 @@ module Assignable
User.where(id: direct_user_ids).or(User.where(id: group_user_ids)).or(User.where(id: team_user_ids))
end
def users_with_permission(permission, teams = Team.all)
permitted_individual_assignments = user_assignments.joins(:user_role).where(team: teams).where(
'user_roles.permissions @> ARRAY[?]::varchar[]', [permission]
)
disallowed_assignments = user_assignments.joins(:user_role).where(team: teams).where(
'NOT(user_roles.permissions @> ARRAY[?]::varchar[])', [permission]
)
permitted_user_group_assignments = user_group_assignments.joins(:user_role, user_group: { user_group_memberships: :user }).where(team: teams).where(
'user_roles.permissions @> ARRAY[?]::varchar[]', [permission]
)
permitted_team_assignments = team_assignments.joins(:user_role, team: { user_assignments: :user }).where(team: teams).where(
'user_roles.permissions @> ARRAY[?]::varchar[]', [permission]
)
User.where(id: permitted_individual_assignments.select(:user_id)).or(
User.where(id: permitted_user_group_assignments.select('user_group_memberships.user_id')).or(
User.where(id: permitted_team_assignments.select('user_assignments.user_id'))
)
).where.not(id: disallowed_assignments.select(:user_id))
end
def default_public_user_role_id(current_team = nil)
if team_assignments.loaded?
team_assignments.find { |ta| ta.team_id == (current_team || team).id }&.user_role_id
@ -53,12 +77,11 @@ module Assignable
false
end
def role_for_user(user, team)
user_assignments.find_by(user: user, team: team)&.user_role ||
user_group_assignments.joins(user_group: :user_group_memberships)
.where(team: team, user_groups: { user_group_memberships: { user_id: user.id } })
.last&.user_role ||
team_assignments.find_by(team: team)&.user_role
def reset_all_users_assignments!(assigned_by)
user_assignments.destroy_all
user_group_assignments.destroy_all
team_assignments.destroy_all
create_user_assignments!(assigned_by)
end
def manually_assigned_users
@ -86,7 +109,7 @@ module Assignable
team_assignment = team_assignments.find_by(team: team)
if team_assignment
User.where.not(id: user_assignments.select(:user_id)).where(id: team_assignment.team.users.select(:id)).find_each do |user|
User.where.not(id: user_assignments.select(:user_id).where(team: team)).where(id: team_assignment.team.users.select(:id)).find_each do |user|
users << {
user: user,
role: team_assignment.user_role,
@ -114,23 +137,36 @@ module Assignable
# Will be called when an assignment is changed (save/destroy) for the assignable model.
end
def create_users_assignments
return if skip_user_assignments
def after_team_assignment_changed(team_assignment = nil)
# Optional, redefine in the assignable model.
# Will be called when an assignment is changed (save/destroy) for the assignable model.
end
role = if top_level_assignable?
UserRole.find_predefined_owner_role
else
permission_parent.role_for_user(created_by, team)
end
def create_user_assignments!(user = created_by)
# First create initial assignments for the object's creator
if top_level_assignable?
user_assignments.create!(user: user, assigned: :manually, user_role: UserRole.find_predefined_owner_role)
else
parent_assignment = permission_parent.user_assignments.find_by(user: user, team: team)
if parent_assignment.present?
user_assignments.create!(user: user, user_role: parent_assignment.user_role)
else
parent_group_assignments = permission_parent.user_group_assignments
.joins(user_group: :user_group_memberships)
.where(team: team, user_groups: { user_group_memberships: { user_id: user.id } })
if parent_group_assignments.present?
parent_group_assignments.each do |parent_group_assignment|
user_group_assignments.create!(user_group: parent_group_assignment.user_group, user_role: parent_group_assignment.user_role)
end
else
parent_team_assignment = permission_parent.team_assignments.find_by(team: team)
team_assignments.create!(team: team, user_role: parent_team_assignment.user_role) if parent_team_assignment.present?
end
end
end
UserAssignment.create!(
user: created_by,
assignable: self,
assigned: top_level_assignable? ? :manually : :automatically,
user_role: role
)
UserAssignments::GenerateUserAssignmentsJob.perform_later(self, created_by.id)
# Generate assignments for the rest of users in the background
UserAssignments::GenerateUserAssignmentsJob.perform_later(self, user.id)
end
end
end

View file

@ -50,7 +50,7 @@ module PermissionCheckableModel
end
user_roles = UserRole.left_outer_joins(:team_assignments, user_group_assignments: { user_group: :users })
user_roles.where(user_group_assignments: { assignable: self, user_groups: { users: user } })
user_roles.where(user_group_assignments: { assignable: self, user_groups: { users: user }, team: permission_team })
.or(user_roles.where(team_assignments: { assignable: self, team: permission_team }))
.exists?(['user_roles.permissions @> ARRAY[?]::varchar[]', [permission]])
end

View file

@ -50,8 +50,8 @@ module Shareable
end
def can_manage_shared?(user)
globally_shared? ||
(shared_with?(user.current_team) && user.current_team.permission_granted?(user, TeamPermissions::MANAGE))
(globally_shared? || shared_with?(user.current_team)) &&
user.current_team.permission_granted?(user, TeamPermissions::MANAGE)
end
def shareable_write?
@ -99,4 +99,26 @@ module Shareable
shared_read? || team_shared_objects.exists?(team: team, permission_level: :shared_read)
end
def demote_all_sharing_assignments_to_viewer!(for_team: nil)
# take into account special roles with no read permission, and do not upgrade them to viewer
read_permission = "#{self.class.permission_class}Permissions".constantize::READ
teams = for_team ? Team.where(id: for_team.id).where.not(id: team.id) : Team.where.not(id: team.id)
[user_assignments, user_group_assignments, team_assignments].each do |assignments|
assignments.joins(:user_role)
.where(team_id: teams.select(:id))
.where(['user_roles.permissions @> ARRAY[?]::varchar[]', [read_permission]])
.update!(user_role: UserRole.find_predefined_viewer_role)
end
end
def destroy_all_sharing_assignments!(for_team: nil)
teams = for_team ? Team.where(id: for_team.id).where.not(id: team.id) : Team.where.not(id: team.id)
user_assignments.where(team_id: teams.select(:id)).destroy_all
user_group_assignments.where(team_id: teams.select(:id)).destroy_all
team_assignments.where(team_id: teams.select(:id)).destroy_all
end
end

View file

@ -348,8 +348,7 @@ class Experiment < ApplicationRecord
next unless my_module.save
# regenerate user assignments
my_module.user_assignments.destroy_all
UserAssignments::GenerateUserAssignmentsJob.perform_later(my_module, current_user.id)
my_module.reset_all_users_assignments!(current_user)
Activities::CreateActivityService.call(activity_type: :move_task,
owner: current_user,
@ -405,8 +404,7 @@ class Experiment < ApplicationRecord
m.save!
# regenerate user assignments
m.user_assignments.destroy_all
UserAssignments::GenerateUserAssignmentsJob.new(m, current_user.id).perform_now
m.reset_all_users_assignments!(current_user)
# Add activity
Activities::CreateActivityService.call(

View file

@ -23,7 +23,7 @@ class LabelTemplate < ApplicationRecord
scope :predefined, -> { where(predefined: true) }
def self.readable_by_user(user, teams)
where(team: teams.with_granted_permissions(user, TeamPermissions::LABEL_TEMPLATES_READ, teams))
where(team: Team.with_granted_permissions(user, TeamPermissions::LABEL_TEMPLATES_READ, teams))
end
def self.enabled?

View file

@ -35,7 +35,7 @@ class ProjectFolder < ApplicationRecord
scope :top_level, -> { where(parent_folder: nil) }
def self.readable_by_user(user, teams)
joins(team: :users)
joins(team: :user_assignments)
.where(teams: { user_assignments: { user: user } })
.where(team: teams)
end

View file

@ -17,7 +17,7 @@ class Protocol < ApplicationRecord
include PermissionCheckableModel
include TinyMceImages
skip_callback :create, :after, :create_users_assignments, if: -> { in_module? }
before_create -> { self.skip_user_assignments = true }, if: -> { in_module? }
enum visibility: { hidden: 0, visible: 1 }
enum protocol_type: {
@ -601,12 +601,17 @@ class Protocol < ApplicationRecord
end
parent_protocol.user_assignments.each do |parent_user_assignment|
parent_protocol.sync_child_protocol_user_assignment(parent_user_assignment, draft.id)
parent_protocol.sync_child_protocol_assignment(parent_user_assignment, draft.id)
end
parent_protocol.user_group_assignments.each do |parent_user_group_assignment|
parent_protocol.sync_child_protocol_user_assignment(parent_user_group_assignment, draft.id)
parent_protocol.sync_child_protocol_assignment(parent_user_group_assignment, draft.id)
end
parent_protocol.team_assignments.each do |parent_team_assignment|
parent_protocol.sync_child_protocol_assignment(parent_team_assignment, draft.id)
end
draft
end
@ -678,29 +683,30 @@ class Protocol < ApplicationRecord
published_versions.or(Protocol.where(id: draft&.id))
end
def sync_child_protocol_user_assignment(user_assignment, child_protocol_id = nil)
def sync_child_protocol_assignment(assignment, child_protocol_id = nil)
# Copy user assignments to child protocol(s)
Protocol.transaction(requires_new: true) do
# Reload to ensure a potential new draft is also included in child versions
reload
assignment_type = user_assignment.respond_to?(:user_group) ? 'user_group' : 'user'
assignment_key = "#{assignment_type}_id".to_sym
assignment_key = assignment.model_name.param_key
assignable_id_key = assignment_key.gsub('assignment', 'id')
(
# all or single child version protocol
child_protocol_id ? child_version_protocols.where(id: child_protocol_id) : child_version_protocols
).find_each do |child_protocol|
child_assignment = child_protocol.public_send("#{assignment_type}_assignments").find_or_initialize_by(
assignment_key => user_assignment.public_send(assignment_key)
child_assignment = child_protocol.public_send(assignment_key.pluralize).find_or_initialize_by(
assignable_id_key => assignment.public_send(assignable_id_key)
)
if user_assignment.destroyed?
if assignment.destroyed?
child_assignment.destroy! if child_assignment.persisted?
next
end
child_assignment.update!(
user_assignment.attributes.slice(
assignment.attributes.slice(
'user_role_id',
'assigned',
'assigned_by_id',
@ -714,15 +720,21 @@ class Protocol < ApplicationRecord
private
def after_user_assignment_changed(user_assignment)
return unless in_repository_published_original?
return if skip_user_assignments || !in_repository_published_original?
sync_child_protocol_user_assignment(user_assignment)
sync_child_protocol_assignment(user_assignment)
end
def after_user_group_assignment_changed(user_group_assignment)
return unless in_repository_published_original?
return if skip_user_assignments || !in_repository_published_original?
sync_child_protocol_user_assignment(user_group_assignment)
sync_child_protocol_assignment(user_group_assignment)
end
def after_team_assignment_changed(user_group_assignment)
return if skip_user_assignments || !in_repository_published_original?
sync_child_protocol_assignment(user_group_assignment)
end
def deep_clone(clone, current_user, include_file_versions: false)

View file

@ -32,6 +32,8 @@ class Repository < RepositoryBase
before_save :sync_name_with_snapshots, if: :name_changed?
before_destroy :refresh_report_references_on_destroy, prepend: true
after_save :unassign_unshared_items, if: :saved_change_to_permission_level
after_save :unlink_unshared_items, if: -> { saved_change_to_permission_level? && !globally_shared? }
validates :name,
presence: true,
@ -53,6 +55,10 @@ class Repository < RepositoryBase
.where(team: teams)
}
def self.permission_class
Repository
end
def top_level_assignable
true
end
@ -172,6 +178,26 @@ class Repository < RepositoryBase
.destroy_all
end
def unlink_unshared_items
repository_rows_ids = repository_rows.select(:id)
rows_to_unlink = RepositoryRow.joins("LEFT JOIN repository_row_connections \
ON repository_rows.id = repository_row_connections.parent_id \
OR repository_rows.id = repository_row_connections.child_id")
.where("repository_row_connections.parent_id IN (?) \
OR repository_row_connections.child_id IN (?)",
repository_rows_ids,
repository_rows_ids)
.joins(:repository)
.where.not(repository: self)
.where.not(repositories: { team: team })
.distinct
RepositoryRowConnection.where(parent: repository_rows_ids, child: rows_to_unlink)
.destroy_all
RepositoryRowConnection.where(child: repository_rows_ids, parent: rows_to_unlink)
.destroy_all
end
def archived_branch?
archived?
end

View file

@ -27,10 +27,6 @@ class RepositoryBase < ApplicationRecord
# Not discarded
default_scope -> { kept }
def self.permission_class
Repository
end
def self.stock_management_enabled?
ApplicationSettings.instance.values['stock_management_enabled']
end

View file

@ -54,8 +54,7 @@ class Result < ApplicationRecord
new_query = joins(:my_module)
.where(
my_modules: {
id: MyModule.with_granted_permissions(user, MyModulePermissions::READ)
.where(user_assignments: { team: teams }).select(:id)
id: MyModule.with_granted_permissions(user, MyModulePermissions::READ, teams).select(:id)
}
)

View file

@ -61,6 +61,7 @@ class Team < ApplicationRecord
has_many :shareable_links, inverse_of: :team, dependent: :destroy
has_many :storage_locations, dependent: :destroy
has_many :forms, dependent: :destroy
has_many :team_assignments, dependent: :destroy
attr_accessor :without_templates

View file

@ -5,7 +5,7 @@ class TeamAssignment < ApplicationRecord
belongs_to :team
belongs_to :user_role
belongs_to :assigned_by, class_name: 'User', optional: true
has_many :users, through: :team
delegate :users, to: :team
enum :assigned, { automatically: 0, manually: 1 }, suffix: true
@ -14,4 +14,11 @@ class TeamAssignment < ApplicationRecord
scope :as_viewers, -> { where(user_role: UserRole.find_predefined_viewer_role) }
validates :team, uniqueness: { scope: :assignable }
after_destroy :call_team_assignment_changed_hook
after_save :call_team_assignment_changed_hook
def call_team_assignment_changed_hook
assignable.__send__(:after_team_assignment_changed, self)
end
end

View file

@ -19,22 +19,57 @@ class TeamSharedObject < ApplicationRecord
# ifs needed for StorageLocations, which currently do not have assignments
after_update :update_assignments, if: -> { shared_object.respond_to?(:user_assignments) }
before_destroy :unassign_unshared_items, if: -> { shared_object.is_a?(Repository) }
before_destroy :unlink_unshared_items, if: -> { shared_object.is_a?(Repository) }
after_destroy :destroy_assignments, if: -> { shared_object.respond_to?(:user_assignments) }
private
def unassign_unshared_items
return if shared_object.shared_read? || shared_object.shared_write?
MyModuleRepositoryRow.joins(my_module: { experiment: { project: :team } })
.joins(repository_row: :repository)
.where(my_module: { experiment: { projects: { team: team } } })
.where(repository_rows: { repository: shared_object })
.destroy_all
end
def unlink_unshared_items
# We keep all the other teams shared with and the repository's own team
teams_ids = shared_object.teams_shared_with.where.not(id: team).pluck(:id)
teams_ids << shared_object.team_id
repository_rows_ids = shared_object.repository_rows.select(:id)
rows_to_unlink = RepositoryRow.joins("LEFT JOIN repository_row_connections \
ON repository_rows.id = repository_row_connections.parent_id \
OR repository_rows.id = repository_row_connections.child_id")
.where("repository_row_connections.parent_id IN (?) \
OR repository_row_connections.child_id IN (?)",
repository_rows_ids,
repository_rows_ids)
.joins(:repository)
.where.not(repositories: { team: teams_ids })
.select(:id)
RepositoryRowConnection.where("(repository_row_connections.parent_id IN (?) \
AND repository_row_connections.child_id IN (?)) \
OR (repository_row_connections.parent_id IN (?) \
AND repository_row_connections.child_id IN (?))",
repository_rows_ids,
rows_to_unlink,
rows_to_unlink,
repository_rows_ids)
.destroy_all
end
def update_assignments
return unless saved_change_to_permission_level? && permission_level == 'shared_read'
shared_object.user_assignments.where(team: team).update!(user_role: UserRole.find_predefined_viewer_role)
shared_object.user_group_assignments.where(team: team).update!(user_role: UserRole.find_predefined_viewer_role)
shared_object.team_assignments.where(team: team).update!(user_role: UserRole.find_predefined_viewer_role)
shared_object.demote_all_sharing_assignments_to_viewer!(for_team: team)
end
def destroy_assignments
shared_object.user_assignments.where(team: team).destroy_all
shared_object.user_group_assignments.where(team: team).destroy_all
shared_object.team_assignments.where(team: team).destroy_all
shared_object.destroy_all_sharing_assignments!(for_team: team)
end
def team_cannot_be_the_same

View file

@ -62,9 +62,7 @@ class User < ApplicationRecord
has_many :user_group_memberships, dependent: :destroy
has_many :user_groups, through: :user_group_memberships
has_many :teams, through: :user_assignments, source: :assignable, source_type: 'Team'
has_many :projects, through: :user_assignments, source: :assignable, source_type: 'Project'
has_many :user_my_modules, inverse_of: :user
has_many :my_modules, through: :user_assignments, source: :assignable, source_type: 'MyModule'
has_many :comments, inverse_of: :user
has_many :activities, inverse_of: :owner, foreign_key: 'owner_id'
has_many :results, inverse_of: :user
@ -465,6 +463,7 @@ class User < ApplicationRecord
# Returns a hash with user statistics
def statistics
statistics = {}
projects = Project.readable_by_user(self, teams)
statistics[:number_of_teams] = teams.count
statistics[:number_of_projects] = projects.count
number_of_experiments = 0

View file

@ -14,6 +14,7 @@ class UserGroup < ApplicationRecord
belongs_to :last_modified_by, class_name: 'User', optional: true
has_many :user_group_memberships, dependent: :destroy
has_many :users, through: :user_group_memberships, dependent: :destroy
has_many :user_group_assignments, dependent: :destroy
accepts_nested_attributes_for :user_group_memberships

View file

@ -7,7 +7,8 @@ class UserGroupAssignment < ApplicationRecord
belongs_to :team
belongs_to :user_group
belongs_to :user_role
belongs_to :assigned_by, class_name: 'User'
belongs_to :assigned_by, class_name: 'User', optional: true
has_many :users, through: :user_group
enum :assigned, { automatically: 0, manually: 1 }, suffix: true

View file

@ -14,10 +14,7 @@ module Recipients
end
return User.none unless record
User.where(id: record.user_assignments
.joins(:user_role)
.where('? = ANY(user_roles.permissions)', permission)
.select(:user_id))
record.users_with_permission(permission)
end
end
end

View file

@ -6,7 +6,7 @@ class Recipients::RepositoryItemRecipients
end
def recipients
repository_row = RepositoryRow.find(@repository_row_id)
repository_row.repository.team.users
repository = RepositoryRow.find(@repository_row_id).repository
repository.users_with_permission(RepositoryPermissions::READ, repository.team)
end
end

View file

@ -113,8 +113,7 @@ Canaid::Permissions.register_for(Repository) do
# repository: create field
can :create_repository_columns do |user, repository|
!repository.shared_with?(user.current_team) &&
repository.permission_granted?(user, RepositoryPermissions::COLUMNS_CREATE)
repository.permission_granted?(user, RepositoryPermissions::COLUMNS_CREATE)
end
can :manage_repository_columns do |user, repository|

View file

@ -12,6 +12,10 @@ module Api
has_many :project_comments, key: :comments, serializer: CommentSerializer
include TimestampableModel
def visibility
object.team_assignments.any? ? 'visible' : 'hidden'
end
end
end
end

View file

@ -11,7 +11,7 @@ module AssignmentsHelper
}
end
user_groups = object.user_group_assignments.map do |ua|
user_groups = object.user_group_assignments.where(team: current_user.current_team).map do |ua|
{
avatar: ActionController::Base.helpers.asset_path('icon/group.svg'),
full_name: ua.user_group_name_with_role

View file

@ -68,7 +68,8 @@ module Lists
provisioning_status: provisioning_status_my_module_url(object),
favorite: favorite_my_module_url(object),
unfavorite: unfavorite_my_module_url(object),
user_roles: user_roles_access_permissions_my_module_path(object)
user_roles: user_roles_access_permissions_my_module_path(object),
user_group_members: users_users_settings_team_user_groups_path(team_id: object.team.id)
}
if can_manage_project_users?(object.experiment.project)

View file

@ -58,7 +58,8 @@ module Lists
show_access: access_permissions_repository_path(object),
share: team_shared_objects_path(current_user.current_team, object_id: object.id, object_type: 'Repository'),
user_roles: user_roles_access_permissions_repository_path(object),
user_group_members: users_users_settings_team_user_groups_path(team_id: object.team.id)
user_group_members: users_users_settings_team_user_groups_path(team_id: current_user.current_team_id),
show_user_group_assignments_access: show_user_group_assignments_access_permissions_repository_path(object)
}
urls[:show] = repository_path(object) if can_read?
@ -68,7 +69,6 @@ module Lists
urls[:new_access] = new_access_permissions_repository_path(id: object.id)
urls[:create_access] = access_permissions_repositories_path(id: object.id)
urls[:unassigned_user_groups] = unassigned_user_groups_access_permissions_repository_path(id: object.id)
urls[:show_user_group_assignments_access] = show_user_group_assignments_access_permissions_repository_path(object)
end
urls

View file

@ -22,13 +22,13 @@ class ActivitiesService
activities =
if filters[:from_date].present? && filters[:to_date].present?
activities.where('created_at <= :from AND created_at >= :to',
activities.where('activities.created_at <= :from AND activities.created_at >= :to',
from: Time.zone.parse(filters[:from_date]).end_of_day.utc,
to: Time.zone.parse(filters[:to_date]).beginning_of_day.utc)
elsif filters[:from_date].present? && filters[:to_date].blank?
activities.where('created_at <= :from', from: Time.zone.parse(filters[:from_date]).end_of_day.utc)
activities.where('activities.created_at <= :from', from: Time.zone.parse(filters[:from_date]).end_of_day.utc)
elsif filters[:from_date].blank? && filters[:to_date].present?
activities.where(created_at: Time.zone.parse(filters[:to_date]).beginning_of_day.utc..)
activities.where('activities.created_at' => Time.zone.parse(filters[:to_date]).beginning_of_day.utc..)
else
activities
end
@ -36,23 +36,33 @@ class ActivitiesService
visible_projects = Project.readable_by_user(user, teams)
visible_my_modules = MyModule.readable_by_user(user, teams)
visible_forms = Form.readable_by_user(user, teams)
# Temporary solution until handling of deleted subjects is fully implemented
visible_repository_teams = user.teams.with_granted_permissions(user, RepositoryPermissions::READ, teams)
visible_repositories = Repository.readable_by_user(user, teams)
deleted_repository_activities =
activities.where(subject_type: %w(RepositoryBase RepositoryRow))
.joins("LEFT OUTER JOIN repositories ON (activities.subject_id = repositories.id AND activities.subject_type = 'RepositoryBase')")
.joins("LEFT OUTER JOIN repository_rows ON (activities.subject_id = repository_rows.id AND activities.subject_type = 'RepositoryRow')")
.where("(activities.subject_type = 'RepositoryBase' AND repositories.id IS NULL) OR
(activities.subject_type = 'RepositoryRow' AND repository_rows.id IS NULL)")
activities = Activity.from(activities, 'activities')
activities = activities.where(project: nil, team_id: teams).where.not(subject_type: %w(RepositoryBase RepositoryRow Protocol Form))
.or(activities.where(subject_type: %w(RepositoryBase RepositoryRow), team_id: visible_repository_teams.select(:id)))
.or(activities.where(id: deleted_repository_activities.select(:id)))
.or(activities.where(subject_type: 'Protocol', subject_id: Protocol.readable_by_user(user, teams).select(:id)))
.or(activities.where(project_id: visible_projects.select(:id)).where.not(subject_type: %w(Experiment MyModule Result Protocol)))
.or(activities.where(subject_type: 'Experiment', subject_id: Experiment.readable_by_user(user, teams).select(:id)))
.or(activities.where("subject_id IN (?) AND subject_type = 'MyModule' OR " \
"subject_id IN (?) AND subject_type = 'Result' OR " \
"subject_id IN (?) AND subject_type = 'Protocol' OR " \
"subject_id IN (?) AND subject_type = 'Form'",
"subject_id IN (?) AND subject_type = 'Form' OR " \
"subject_id IN (?) AND subject_type = 'RepositoryBase' OR " \
"subject_id IN (?) AND subject_type = 'RepositoryRow'",
visible_my_modules.select(:id),
Result.with_discarded.where(my_module: visible_my_modules).select(:id),
Protocol.where(my_module: visible_my_modules).select(:id),
visible_forms.select(:id)))
visible_forms.select(:id),
visible_repositories.select(:id),
RepositoryRow.where(repository_id: visible_repositories).select(:id)))
activities.order(created_at: :desc)
.page(filters[:page])

View file

@ -25,14 +25,14 @@ module Experiments
ActiveRecord::Base.transaction do
@exp.project = @project
sync_user_assignments(@exp)
@exp.reset_all_users_assignments!(@user)
@exp.my_modules.each do |my_module|
unless can_move_my_module?(@user, my_module)
@errors[:main] = I18n.t('move_to_project_service.my_modules_permission_error')
raise
end
sync_user_assignments(my_module)
my_module.reset_all_users_assignments!(@user)
clean_up_user_my_modules(my_module)
move_tags!(my_module)
end
@ -107,20 +107,6 @@ module Experiments
end
end
def sync_user_assignments(object)
# remove user assignments where the user are not present on the project
object.user_assignments.destroy_all
UserAssignment.create!(
user: @user,
assignable: object,
assigned: :automatically,
user_role: @project.user_assignments.find_by(user: @user).user_role
)
UserAssignments::GenerateUserAssignmentsJob.perform_later(object, @user.id)
end
def clean_up_user_my_modules(my_module)
my_module.user_my_modules.where.not(user_id: @project.users.select(:id)).destroy_all
end

View file

@ -3,7 +3,13 @@
module Lists
class FormsService < BaseService
def fetch_records
@records = @raw_data.left_outer_joins(:user_assignments)
user_assignments = @raw_data.joins(:user_assignments).select(:assignable_id, :user_id)
user_group_assignments = @raw_data.joins(user_group_assignments: { user_group: :user_group_memberships })
.select('user_group_assignments.assignable_id, user_group_memberships.user_id')
team_assignments = @raw_data.joins(team_assignments: { team: :user_assignments }).select('team_assignments.assignable_id, user_assignments.user_id')
@records = @raw_data.joins("LEFT JOIN (#{user_assignments.to_sql} UNION #{team_assignments.to_sql} UNION #{user_group_assignments.to_sql})
all_assigned_users ON all_assigned_users.assignable_id = forms.id")
.left_outer_joins(:form_responses)
.joins(
'LEFT OUTER JOIN users AS publishers ' \
@ -12,7 +18,7 @@ module Lists
'forms.* AS forms',
'publishers.full_name AS published_by_user',
'COUNT(DISTINCT form_responses.id) AS used_in_protocols_count',
'COUNT(DISTINCT user_assignments.id) AS user_assignment_count'
'COUNT(DISTINCT all_assigned_users.user_id) AS user_assignment_count'
).group('forms.id', 'publishers.full_name')
view_mode = @params[:view_mode] || 'active'

View file

@ -22,6 +22,11 @@ module Lists
@records = Protocol.where('protocols.id IN (?) OR protocols.id IN (?) OR protocols.id IN (?)',
original_without_versions, published_versions, new_drafts)
user_assignments = @records.joins(:user_assignments).select(:assignable_id, :user_id)
user_group_assignments = @records.joins(user_group_assignments: { user_group: :user_group_memberships })
.select('user_group_assignments.assignable_id, user_group_memberships.user_id')
team_assignments = @records.joins(team_assignments: { team: :user_assignments }).select('team_assignments.assignable_id, user_assignments.user_id')
@records = @records.preload(:parent, :latest_published_version, :draft,
:protocol_keywords, user_assignments: %i(user user_role))
.joins("LEFT OUTER JOIN protocols protocol_versions " \
@ -48,9 +53,8 @@ module Lists
'ON "protocol_protocol_keywords"."protocol_keyword_id" = "protocol_keywords"."id"')
.joins('LEFT OUTER JOIN "users" "archived_users" ON "archived_users"."id" = "protocols"."archived_by_id"')
.joins('LEFT OUTER JOIN "users" ON "users"."id" = "protocols"."published_by_id"')
.joins('LEFT OUTER JOIN "user_assignments" "all_user_assignments" ' \
'ON "all_user_assignments"."assignable_type" = \'Protocol\' ' \
'AND "all_user_assignments"."assignable_id" = "protocols"."id"')
.joins("LEFT JOIN (#{user_assignments.to_sql} UNION #{team_assignments.to_sql} UNION #{user_group_assignments.to_sql})
all_assigned_users ON all_assigned_users.assignable_id = protocols.id")
.group('"protocols"."id"')
.select(
'"protocols".*',
@ -61,7 +65,7 @@ module Lists
"THEN 0 ELSE COUNT(DISTINCT(\"protocol_versions\".\"id\")) + 1 " \
"END AS nr_of_versions",
'COUNT(DISTINCT("linked_task_protocols"."id")) AS nr_of_linked_tasks',
'COUNT(DISTINCT("all_user_assignments"."id")) AS "nr_of_assigned_users"',
'COUNT(DISTINCT all_assigned_users.user_id) AS "nr_of_assigned_users"',
'MAX("users"."full_name") AS "full_username_str"', # "Hack" to get single username
'MAX("archived_users"."full_name") AS "archived_full_username_str"'
)
@ -107,9 +111,7 @@ module Lists
@records = @records.where(protocols: { published_by_id: @filters[:published_by].values })
end
if @filters[:members].present?
@records = @records.where(all_user_assignments: { user_id: @filters[:members].values })
end
@records = @records.where(all_assigned_users: { user_id: @filters[:members].values }) if @filters[:members].present?
if @filters[:has_draft].present?
@records =

View file

@ -5,15 +5,23 @@ module Lists
private
def fetch_records
user_assignments = @raw_data.joins(:user_assignments).select(:assignable_id, :user_id)
user_group_assignments = @raw_data.joins(user_group_assignments: { user_group: :user_group_memberships })
.select('user_group_assignments.assignable_id, user_group_memberships.user_id')
team_assignments = @raw_data.joins(team_assignments: { team: :user_assignments }).select('team_assignments.assignable_id, user_assignments.user_id')
@records = @raw_data.joins('LEFT OUTER JOIN users AS creators ' \
'ON repositories.created_by_id = creators.id')
.joins('LEFT OUTER JOIN users AS archivers ' \
'ON repositories.archived_by_id = archivers.id')
.joins(:team)
.joins("LEFT OUTER JOIN (#{user_assignments.to_sql} UNION #{team_assignments.to_sql} UNION #{user_group_assignments.to_sql})
all_assigned_users ON all_assigned_users.assignable_id = repositories.id")
.joins('INNER JOIN teams AS teams_repositories ON teams_repositories.id = repositories.team_id')
.select('repositories.*')
.select('MAX(teams.name) AS team_name')
.select('MAX(teams_repositories.name) AS team_name')
.select('MAX(creators.full_name) AS created_by_user')
.select('MAX(archivers.full_name) AS archived_by_user')
.select('COUNT(DISTINCT all_assigned_users.user_id) AS "assigned_users_count"')
.select(shared_sql_select)
.preload(:team_assignments, :user_group_assignments, user_assignments: %i(user user_role))
.group('repositories.id')
@ -30,7 +38,7 @@ module Lists
@records = @records.where_attributes_like(
[
'repositories.name',
'teams.name',
'teams_repositories.name',
'creators.full_name',
'archivers.full_name'
],
@ -48,7 +56,8 @@ module Lists
archived_by: 'archived_by_user',
nr_of_rows: 'repository_rows_count',
code: 'repositories.id',
shared_label: 'shared'
shared_label: 'shared',
assigned_users: 'assigned_users_count'
}
end

View file

@ -5,14 +5,11 @@ module Lists
private
def fetch_records
@records = @raw_data.joins(
'LEFT OUTER JOIN users AS creators ' \
'ON user_groups.created_by_id = creators.id'
).left_joins(:user_group_memberships).includes(:users)
.select('user_groups.* as user_groups')
.select('creators.full_name AS created_by_user')
@records = @raw_data.left_joins(:created_by).left_joins(:user_group_memberships).includes(:users)
.select('user_groups.*')
.select('array_agg(users.full_name) AS created_by_user')
.select('COUNT(user_groups.id) AS members_count')
.group('user_groups.id, creators.full_name')
.group('user_groups.id')
end
def filter_records

View file

@ -47,6 +47,9 @@ module ModelExporters
user_assignments: @experiment.user_assignments.map do |ua|
user_assignment(ua)
end,
team_assignments: @experiment.team_assignments.map do |ta|
team_assignment(ta)
end,
my_modules: my_modules.map { |m| my_module(m) },
my_module_groups: my_module_groups
}, @assets_to_copy
@ -61,12 +64,24 @@ module ModelExporters
}
end
def team_assignment(team_assignment)
{
team_id: team_assignment.team_id,
assigned_by_id: team_assignment.assigned_by_id,
role_name: team_assignment.user_role.name,
assigned: team_assignment.assigned
}
end
def my_module(my_module)
{
my_module: my_module,
user_assignments: my_module.user_assignments.map do |ua|
user_assignment(ua)
end,
team_assignments: my_module.team_assignments.map do |ta|
team_assignment(ta)
end,
my_module_status_name: my_module.my_module_status&.name,
outputs: my_module.outputs,
my_module_tags: my_module.my_module_tags,

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