diff --git a/.gitignore b/.gitignore index a16f03f0c..4e459907b 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,7 @@ public/marvin4js-license.cxl /app/assets/builds/* !/app/assets/builds/.keep + +# Ignore automatically generated js-routes files. +/app/javascript/routes.js +/app/javascript/routes.d.ts diff --git a/Gemfile b/Gemfile index 31fc95950..19a25a069 100644 --- a/Gemfile +++ b/Gemfile @@ -94,6 +94,7 @@ gem 'graphviz' gem 'cssbundling-rails' gem 'jsbundling-rails' +gem 'js-routes' gem 'tailwindcss-rails', '~> 2.4' @@ -107,6 +108,7 @@ group :development, :test do gem 'awesome_print' gem 'better_errors' gem 'binding_of_caller' + gem 'brakeman', require: false gem 'bullet' gem 'byebug' gem 'factory_bot_rails' diff --git a/Gemfile.lock b/Gemfile.lock index deeb09c01..a41d9bd63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,6 +208,8 @@ GEM debug_inspector (>= 0.0.1) bootsnap (1.16.0) msgpack (~> 1.2) + brakeman (6.1.2) + racc builder (3.2.4) bullet (7.0.7) activesupport (>= 3.0.0) @@ -386,6 +388,8 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + js-routes (2.2.8) + railties (>= 4) jsbundling-rails (1.1.1) railties (>= 6.0.0) json (2.6.3) @@ -797,6 +801,7 @@ DEPENDENCIES better_errors binding_of_caller bootsnap + brakeman bullet byebug canaid! @@ -826,6 +831,7 @@ DEPENDENCIES image_processing img2zpl! jbuilder + js-routes jsbundling-rails json-jwt json_matchers diff --git a/Rakefile b/Rakefile index 9f98587c3..c172d03df 100644 --- a/Rakefile +++ b/Rakefile @@ -5,3 +5,5 @@ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks Doorkeeper::Rake.load_tasks +# Update js-routes file before javascript build +task 'javascript:build' => 'js:routes:typescript' diff --git a/VERSION b/VERSION index 2dba38f44..39fc130ef 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.35.0.2 +1.36.0 diff --git a/app/assets/javascripts/repository_columns/index.js b/app/assets/javascripts/repository_columns/index.js index d17ad503c..a2e39aa83 100644 --- a/app/assets/javascripts/repository_columns/index.js +++ b/app/assets/javascripts/repository_columns/index.js @@ -185,7 +185,7 @@ var RepositoryColumns = (function() { disableSearch: true, labelHTML: true, optionLabel: function(option) { - return `
+ return `
${option.label} ${option.params.text_description || ''}
` @@ -284,6 +284,7 @@ var RepositoryColumns = (function() { let editableRow = ($(el).attr('data-editable-row') === 'true') ? 'has-permissions' : ''; let editUrl = $(el).attr('data-edit-column-url'); let destroyUrl = $(el).attr('data-destroy-column-url'); + const isDisabled = $(el).attr('data-disabled') === 'true'; let thederName; if ($(el).find('.modal-tooltiptext').length > 0) { @@ -315,7 +316,9 @@ var RepositoryColumns = (function() { -
${thederName}
+
+ ${thederName} ${isDisabled ? `` : ''} +
${ getColumnTypeText(el, colId) || `` } diff --git a/app/assets/javascripts/sitewide/marvinjs_editor.js b/app/assets/javascripts/sitewide/marvinjs_editor.js index a334bf9f5..2afdc0196 100644 --- a/app/assets/javascripts/sitewide/marvinjs_editor.js +++ b/app/assets/javascripts/sitewide/marvinjs_editor.js @@ -202,10 +202,14 @@ var MarvinJsEditorApi = (function() { } $(marvinJsModal).modal('hide'); - config.editor.focus(); + if (config.editor) config.editor.focus(); + config.button.dataset.inProgress = false; - if (MarvinJsEditor.saveCallback) MarvinJsEditor.saveCallback(); + if (MarvinJsEditor.saveCallback) { + MarvinJsEditor.saveCallback(); + delete MarvinJsEditor.saveCallback; + } }, error: function(response) { if (response.status === 403) { @@ -264,8 +268,8 @@ var MarvinJsEditorApi = (function() { MarvinJsEditor.save(config); } else if (config.mode === 'edit') { config.objectType = 'Asset'; + MarvinJsEditor.saveCallback = (() => window.location.reload()); MarvinJsEditor.update(config); - location.reload(); } else if (config.mode === 'new-tinymce') { config.objectType = 'TinyMceAsset'; MarvinJsEditor.save(config); diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 400c99bdf..9e83df130 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -2,6 +2,7 @@ @import "tailwind/buttons"; @import "tailwind/modals"; @import "tailwind/flyouts"; +@import "tailwind/radio"; @import "tailwind/loader.css"; @tailwind base; @@ -69,6 +70,6 @@ html { @keyframes shine-lines { 0% { background-position: -150px } - + 40%, 100% { background-position: 320px } } diff --git a/app/assets/stylesheets/shared/comments_sidebar.scss b/app/assets/stylesheets/shared/comments_sidebar.scss index a60b508fa..65c081105 100644 --- a/app/assets/stylesheets/shared/comments_sidebar.scss +++ b/app/assets/stylesheets/shared/comments_sidebar.scss @@ -110,13 +110,13 @@ } .send-comment { + bottom: 5px; color: $brand-primary; cursor: pointer; display: inline-block; position: absolute; right: 5px; text-align: center; - top: 5px; } } diff --git a/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss b/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss index b940efe9e..5b7fd31ca 100644 --- a/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss +++ b/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss @@ -1,5 +1,5 @@ // scss-lint:disable SelectorDepth QualifyingElement - +/* :root { --sci-radio-size: 16px; } @@ -85,3 +85,4 @@ input[type="radio"].sci-radio { } } } +*/ diff --git a/app/assets/stylesheets/tailwind/buttons.css b/app/assets/stylesheets/tailwind/buttons.css index 9f7001362..121cd8b03 100644 --- a/app/assets/stylesheets/tailwind/buttons.css +++ b/app/assets/stylesheets/tailwind/buttons.css @@ -114,7 +114,7 @@ } .btn.btn-light { - @apply bg-transparent text-sn-blue border-transparent; + @apply bg-transparent text-sn-blue border-transparent bg-sn-white; } .btn.btn-light.btn-black { diff --git a/app/assets/stylesheets/tailwind/radio.css b/app/assets/stylesheets/tailwind/radio.css new file mode 100644 index 000000000..197119397 --- /dev/null +++ b/app/assets/stylesheets/tailwind/radio.css @@ -0,0 +1,42 @@ +@layer components { + + .sci-radio-container { + @apply inline-block h-4 w-4 relative; + } + + input[type="radio"].sci-radio { + @apply cursor-pointer shrink-0 h-4 w-4 m-0 opacity-0 relative z-[2]; + } + + input[type="radio"].sci-radio + .sci-radio-label { + @apply inline-block shrink-0 h-4 w-4 absolute left-0; + } + + input[type="radio"].sci-radio + .sci-radio-label::before { + @apply border-[1px] border-solid border-sn-black rounded-full text-white text-center transition-all + h-4 w-4 left-0 absolute; + content: ""; + } + + input[type="radio"].sci-radio + .sci-radio-label::after{ + @apply bg-white rounded-full text-white text-center transition-all + absolute w-2.5 h-2.5 top-[3px] left-[3px] ; + content: ""; + } + + input[type="radio"].sci-radio:checked + .sci-radio-label::before { + @apply !border-sn-blue; + } + + input[type="radio"].sci-radio:checked + .sci-radio-label::after { + @apply !bg-sn-science-blue; + } + + input[type="radio"].sci-radio:disabled + .sci-radio-label::before { + @apply !border-sn-sleepy-grey; + } + + input[type="radio"].sci-radio:checked:disabled + .sci-radio-label::after { + @apply !bg-sn-sleepy-grey; + } +} diff --git a/app/controllers/access_permissions/projects_controller.rb b/app/controllers/access_permissions/projects_controller.rb index a50091fa7..3527cfefe 100644 --- a/app/controllers/access_permissions/projects_controller.rb +++ b/app/controllers/access_permissions/projects_controller.rb @@ -103,8 +103,6 @@ module AccessPermissions destroy: true ) - user_assignment.destroy! - log_activity(:unassign_user_from_project, { user_target: user_assignment.user.id, role: user_assignment.user_role.name }) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 29fa820c3..25aff0c0c 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -10,14 +10,14 @@ class RepositoriesController < ApplicationController include MyModulesHelper before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar - export_modal export_repositories) - before_action :load_repositories, only: :index + export_modal export_repositories list) + before_action :load_repositories, only: %i(index list) before_action :load_repositories_for_archiving, only: :archive before_action :load_repositories_for_restoring, only: :restore - before_action :check_view_all_permissions, only: %i(index sidebar) + before_action :check_view_all_permissions, only: %i(index sidebar list) before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet import_records sidebar archive restore actions_toolbar - export_modal export_repositories) + export_modal export_repositories list) before_action :check_manage_permissions, only: %i(rename_modal update) before_action :check_delete_permissions, only: %i(destroy destroy_modal) before_action :check_archive_permissions, only: %i(archive restore) @@ -44,6 +44,16 @@ class RepositoriesController < ApplicationController end end + def list + results = @repositories + results = results.name_like(params[:query]) if params[:query].present? + render json: { data: results.map { |r| [r.id, r.name] } } + end + + def rows_list + render json: { data: @repository.repository_rows.map { |r| [r.id, r.name] } } + end + def sidebar render json: { html: render_to_string(partial: 'repositories/sidebar', locals: { diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb index 416a96536..ae9ed52fb 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -1,19 +1,20 @@ # frozen_string_literal: true class StorageLocationRepositoryRowsController < ApplicationController - before_action :load_storage_location_repository_row, only: %i(update destroy) + before_action :check_storage_locations_enabled, except: :destroy + before_action :load_storage_location_repository_row, only: %i(update destroy move) before_action :load_storage_location - before_action :load_repository_row - before_action :check_read_permissions, only: :index - before_action :check_manage_permissions, except: :index + before_action :load_repository_row, only: %i(create update destroy move) + before_action :check_read_permissions, except: %i(create actions_toolbar) + before_action :check_manage_permissions, only: %i(create update destroy) def index storage_location_repository_row = Lists::StorageLocationRepositoryRowsService.new( - current_team, storage_location_repository_row_params + current_team, params ).call render json: storage_location_repository_row, each_serializer: Lists::StorageLocationRepositoryRowSerializer, - include: %i(repository_row) + meta: (pagination_dict(storage_location_repository_row) unless @storage_location.with_grid?) end def update @@ -21,8 +22,7 @@ class StorageLocationRepositoryRowsController < ApplicationController if @storage_location_repository_row.save render json: @storage_location_repository_row, - serializer: Lists::StorageLocationRepositoryRowSerializer, - include: :repository_row + serializer: Lists::StorageLocationRepositoryRowSerializer else render json: @storage_location_repository_row.errors, status: :unprocessable_entity end @@ -38,13 +38,30 @@ class StorageLocationRepositoryRowsController < ApplicationController if @storage_location_repository_row.save render json: @storage_location_repository_row, - serializer: Lists::StorageLocationRepositoryRowSerializer, - include: :repository_row + serializer: Lists::StorageLocationRepositoryRowSerializer else render json: @storage_location_repository_row.errors, status: :unprocessable_entity end end + def move + ActiveRecord::Base.transaction do + @storage_location_repository_row.discard + @storage_location_repository_row = StorageLocationRepositoryRow.create!( + repository_row: @repository_row, + storage_location: @storage_location, + metadata: storage_location_repository_row_params[:metadata] || {}, + created_by: current_user + ) + + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer + rescue ActiveRecord::RecordInvalid => e + render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + end + def destroy if @storage_location_repository_row.discard render json: {} @@ -53,8 +70,21 @@ class StorageLocationRepositoryRowsController < ApplicationController end end + def actions_toolbar + render json: { + actions: Toolbars::StorageLocationRepositoryRowsService.new( + current_user, + items_ids: JSON.parse(params[:items]).map { |i| i['id'] } + ).actions + } + end + private + def check_storage_locations_enabled + render_403 unless StorageLocation.storage_locations_enabled? + end + def load_storage_location_repository_row @storage_location_repository_row = StorageLocationRepositoryRow.find( storage_location_repository_row_params[:id] @@ -80,10 +110,12 @@ class StorageLocationRepositoryRowsController < ApplicationController end def check_read_permissions - render_403 unless true + render_403 unless can_read_storage_location_containers?(current_team) end def check_manage_permissions - render_403 unless true + unless can_manage_storage_location_containers?(current_team) && can_read_repository?(@repository_row.repository) + render_403 + end end end diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index a6af1aecc..49c0375a1 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true class StorageLocationsController < ApplicationController - before_action :load_storage_location, only: %i(update destroy) - before_action :check_read_permissions, only: :index - before_action :check_manage_permissions, except: :index - before_action :set_breadcrumbs_items, only: :index + before_action :check_storage_locations_enabled, except: :unassign_rows + before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows) + before_action :check_read_permissions, except: %i(index create tree actions_toolbar) + before_action :check_create_permissions, only: :create + before_action :check_manage_permissions, only: %i(update destroy duplicate move unassign_rows) + before_action :set_breadcrumbs_items, only: %i(index show) def index respond_to do |format| @@ -17,14 +19,17 @@ class StorageLocationsController < ApplicationController end end + def show; end + def update - @storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id] + @storage_location.image.purge if params[:file_name].blank? + @storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id] @storage_location.update(storage_location_params) if @storage_location.save render json: @storage_location, serializer: Lists::StorageLocationSerializer else - render json: @storage_location.errors, status: :unprocessable_entity + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity end end @@ -33,12 +38,12 @@ class StorageLocationsController < ApplicationController storage_location_params.merge({ team: current_team, created_by: current_user }) ) - @storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id] + @storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id] if @storage_location.save render json: @storage_location, serializer: Lists::StorageLocationSerializer else - render json: @storage_location.errors, status: :unprocessable_entity + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity end end @@ -46,34 +51,103 @@ class StorageLocationsController < ApplicationController if @storage_location.discard render json: {} else - render json: { errors: @storage_location.errors.full_messages }, status: :unprocessable_entity + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity end end + def duplicate + new_storage_location = @storage_location.duplicate! + if new_storage_location + render json: new_storage_location, serializer: Lists::StorageLocationSerializer + else + render json: { errors: :failed }, status: :unprocessable_entity + end + end + + def move + storage_location_destination = + if move_params[:destination_storage_location_id] == 'root_storage_location' + nil + else + current_team.storage_locations.find(move_params[:destination_storage_location_id]) + end + + @storage_location.update!(parent: storage_location_destination) + + render json: { message: I18n.t('storage_locations.index.move_modal.success_flash') } + rescue StandardError => e + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") + render json: { error: I18n.t('storage_locations.index.move_modal.error_flash') }, status: :bad_request + end + + def tree + records = current_team.storage_locations.where(parent: nil, container: [false, params[:container] == 'true']) + render json: storage_locations_recursive_builder(records) + end + + def available_positions + render json: { positions: @storage_location.available_positions } + end + + def unassign_rows + @storage_location.storage_location_repository_rows.where(id: params[:ids]).discard_all + + render json: { status: :ok } + end + def actions_toolbar render json: { - actions: [] # TODO: Add actions + actions: + Toolbars::StorageLocationsService.new( + current_user, + storage_location_ids: JSON.parse(params[:items]).map { |i| i['id'] } + ).actions } end private + def check_storage_locations_enabled + render_403 unless StorageLocation.storage_locations_enabled? + end + def storage_location_params - params.permit(:id, :parent_id, :name, :container, :signed_blob_id, :description, - metadata: { dimensions: [], parent_coordinations: [], display_type: :string }) + params.permit(:id, :parent_id, :name, :container, :description, + metadata: [:display_type, dimensions: [], parent_coordinations: []]) + end + + def move_params + params.permit(:id, :destination_storage_location_id) end def load_storage_location - @storage_location = StorageLocation.where(team: current_team).find(storage_location_params[:id]) + @storage_location = current_team.storage_locations.find_by(id: storage_location_params[:id]) render_404 unless @storage_location end def check_read_permissions - render_403 unless true + if @storage_location.container + render_403 unless can_read_storage_location_containers?(current_team) + else + render_403 unless can_read_storage_locations?(current_team) + end + end + + def check_create_permissions + if storage_location_params[:container] + render_403 unless can_create_storage_location_containers?(current_team) + else + render_403 unless can_create_storage_locations?(current_team) + end end def check_manage_permissions - render_403 unless true + if @storage_location.container + render_403 unless can_manage_storage_location_containers?(current_team) + else + render_403 unless can_manage_storage_locations?(current_team) + end end def set_breadcrumbs_items @@ -89,8 +163,8 @@ class StorageLocationsController < ApplicationController }) storage_locations = [] - if params[:parent_id] - location = StorageLocation.where(team: current_team).find_by(id: params[:parent_id]) + if params[:parent_id] || @storage_location + location = (current_team.storage_locations.find_by(id: params[:parent_id]) || @storage_location) if location storage_locations.unshift(breadcrumbs_item(location)) while location.parent @@ -108,4 +182,15 @@ class StorageLocationsController < ApplicationController url: storage_locations_path(parent_id: location.id) } end + + def storage_locations_recursive_builder(storage_locations) + storage_locations.map do |storage_location| + { + storage_location: storage_location, + children: storage_locations_recursive_builder( + storage_location.storage_locations.where(container: [false, params[:container] == 'true']) + ) + } + end + end end diff --git a/app/controllers/users/settings/user_settings_controller.rb b/app/controllers/users/settings/user_settings_controller.rb index 2b106bc17..04620a496 100644 --- a/app/controllers/users/settings/user_settings_controller.rb +++ b/app/controllers/users/settings/user_settings_controller.rb @@ -17,8 +17,8 @@ module Users next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s) case key.to_s - when 'task_step_states' - update_task_step_states(data) + when 'task_step_states', 'result_states' + update_object_states(data, key.to_s) else current_user.settings[key] = data end @@ -34,18 +34,18 @@ module Users private - def update_task_step_states(task_step_states_data) - current_states = current_user.settings.fetch('task_step_states', {}) + def update_object_states(object_states_data, object_state_key) + current_states = current_user.settings.fetch(object_state_key, {}) - task_step_states_data.each do |step_id, collapsed| + object_states_data.each do |object_id, collapsed| if collapsed - current_states[step_id] = true + current_states[object_id] = true else - current_states.delete(step_id) + current_states.delete(object_id) end end - current_user.settings['task_step_states'] = current_states + current_user.settings[object_state_key] = current_states end end end diff --git a/app/javascript/packs/vue/storage_locations_container.js b/app/javascript/packs/vue/storage_locations_container.js new file mode 100644 index 000000000..abf6912c3 --- /dev/null +++ b/app/javascript/packs/vue/storage_locations_container.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import PerfectScrollbar from 'vue3-perfect-scrollbar'; +import StorageLocationsContainer from '../../vue/storage_locations/container.vue'; +import { mountWithTurbolinks } from './helpers/turbolinks.js'; + +const app = createApp(); +app.component('StorageLocationsContainer', StorageLocationsContainer); +app.config.globalProperties.i18n = window.I18n; +app.use(PerfectScrollbar); +mountWithTurbolinks(app, '#StorageLocationsContainer'); diff --git a/app/javascript/vue/projects/modals/move.vue b/app/javascript/vue/projects/modals/move.vue index 08eaa66ab..0b92d73a9 100644 --- a/app/javascript/vue/projects/modals/move.vue +++ b/app/javascript/vue/projects/modals/move.vue @@ -98,20 +98,19 @@ export default { if (this.query === '') { return this.foldersTree; } - return this.foldersTree.map((folder) => ( - { - folder: folder.folder, - children: folder.children.filter((child) => ( - child.folder.name.toLowerCase().includes(this.query.toLowerCase()) - )), - } - )).filter((folder) => ( - folder.folder.name.toLowerCase().includes(this.query.toLowerCase()) - || folder.children.length > 0 - )); + return this.filteredFoldersTreeHelper(this.foldersTree); }, }, methods: { + filteredFoldersTreeHelper(foldersTree) { + return foldersTree.map(({ folder, children }) => { + if (folder.name.toLowerCase().includes(this.query.toLowerCase())) { + return { folder, children }; + } + const filteredChildren = this.filteredFoldersTreeHelper(children); + return filteredChildren.length ? { folder, children: filteredChildren } : null; + }).filter(Boolean); + }, selectFolder(folderId) { this.selectedFolderId = folderId; }, diff --git a/app/javascript/vue/protocol/container.vue b/app/javascript/vue/protocol/container.vue index fdd6b6e08..d03b02909 100644 --- a/app/javascript/vue/protocol/container.vue +++ b/app/javascript/vue/protocol/container.vue @@ -177,6 +177,7 @@
+ {{ i18n.t("protocols.steps.add_step") }}
+ {{ i18n.t("protocols.steps.add_step") }}
diff --git a/app/javascript/vue/protocol/step.vue b/app/javascript/vue/protocol/step.vue index 5b1ad50ff..ce3322308 100644 --- a/app/javascript/vue/protocol/step.vue +++ b/app/javascript/vue/protocol/step.vue @@ -145,7 +145,19 @@ @attachments:viewMode="changeAttachmentsViewMode" @attachment:viewMode="updateAttachmentViewMode"/>
+
+