From d6c3468002efd17c6415b7c4f31be0385452f301 Mon Sep 17 00:00:00 2001 From: Andrej Date: Wed, 10 Jul 2024 07:29:18 +0200 Subject: [PATCH 001/249] Implement storage location backend [SCI-10465] --- ...age_location_repository_rows_controller.rb | 85 +++++++++++++++++++ .../storage_locations_controller.rb | 65 ++++++++++++++ app/models/repository_row.rb | 2 + app/models/storage_location.rb | 25 ++++++ app/models/storage_location_repository_row.rb | 30 +++++++ ...rage_location_repository_row_serializer.rb | 21 +++++ .../lists/storage_location_serializer.rb | 19 +++++ app/services/lists/base_service.rb | 2 +- ...torage_location_repository_rows_service.rb | 18 ++++ .../lists/storage_locations_service.rb | 18 ++++ config/locales/en.yml | 3 + config/routes.rb | 4 + .../20240705122903_add_storage_locations.rb | 38 +++++++++ 13 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 app/controllers/storage_location_repository_rows_controller.rb create mode 100644 app/controllers/storage_locations_controller.rb create mode 100644 app/models/storage_location.rb create mode 100644 app/models/storage_location_repository_row.rb create mode 100644 app/serializers/lists/storage_location_repository_row_serializer.rb create mode 100644 app/serializers/lists/storage_location_serializer.rb create mode 100644 app/services/lists/storage_location_repository_rows_service.rb create mode 100644 app/services/lists/storage_locations_service.rb create mode 100644 db/migrate/20240705122903_add_storage_locations.rb diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb new file mode 100644 index 0000000000..bab428d806 --- /dev/null +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +class StorageLocationRepositoryRowsController < ApplicationController + before_action :load_storage_location_repository_row, only: %i(update destroy) + before_action :load_storage_location + before_action :load_repository_row + before_action :check_read_permissions, only: :index + before_action :check_manage_permissions, except: :index + + def index + storage_location_repository_row = Lists::StorageLocationRepositoryRowsService.new( + current_team, storage_location_repository_row_params + ).call + render json: storage_location_repository_row, + each_serializer: Lists::StorageLocationRepositoryRowSerializer, + include: %i(repository_row) + end + + def update + @storage_location_repository_row.update(storage_location_repository_row_params) + + if @storage_location_repository_row.save + render json: {} + else + render json: @storage_location_repository_row.errors, status: :unprocessable_entity + end + end + + def create + @storage_location_repository_row = StorageLocationRepositoryRow.new( + repository_row: @repository_row, + storage_location: @storage_location, + metadata: storage_location_repository_row_params[:metadata], + created_by: current_user + ) + + if @storage_location_repository_row.save + render json: {} + else + render json: @storage_location_repository_row.errors, status: :unprocessable_entity + end + end + + def destroy + if @storage_location_repository_row.discard + render json: {} + else + render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity + end + end + + private + + def load_storage_location_repository_row + @storage_location_repository_row = StorageLocationRepositoryRow.find( + storage_location_repository_row_params[:storage_location_id] + ) + render_404 unless @storage_location_repository_row + end + + def load_storage_location + @storage_location = StorageLocation.where(team: current_team).find( + storage_location_repository_row_params[:storage_location_id] + ) + render_404 unless @storage_location + end + + def load_repository_row + @repository_row = RepositoryRow.find(storage_location_repository_row_params[:repository_row_id]) + render_404 unless @repository_row + end + + def storage_location_repository_row_params + params.permit(:id, :storage_location_id, :repository_row_id, + metadata: { position: [] }) + end + + def check_read_permissions + render_403 unless true + end + + def check_manage_permissions + render_403 unless true + end +end diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb new file mode 100644 index 0000000000..309167cc11 --- /dev/null +++ b/app/controllers/storage_locations_controller.rb @@ -0,0 +1,65 @@ +# 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 + + def index + storage_locations = Lists::StorageLocationsService.new(current_team, storage_location_params).call + render json: storage_locations, each_serializer: Lists::StorageLocationSerializer + end + + def update + @storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id] + @storage_location.update(storage_location_params) + + if @storage_location.save + render json: {} + else + render json: @storage_location.errors, status: :unprocessable_entity + end + end + + def create + @storage_location = StorageLocation.new( + 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] + + if @storage_location.save + render json: {} + else + render json: @storage_location.errors, status: :unprocessable_entity + end + end + + def destroy + if @storage_location.discard + render json: {} + else + render json: { errors: @storage_location.errors.full_messages }, status: :unprocessable_entity + end + end + + private + + def storage_location_params + params.permit(:id, :parent_id, :name, :container, :signed_blob_id, :description, + metadata: { dimensions: [], parent_coordinations: [], display_type: :string }) + end + + def load_storage_location + @storage_location = StorageLocation.where(team: current_team).find(storage_location_params[:id]) + render_404 unless @storage_location + end + + def check_read_permissions + render_403 unless true + end + + def check_manage_permissions + render_403 unless true + end +end diff --git a/app/models/repository_row.rb b/app/models/repository_row.rb index 923164d0f0..0b03595fc5 100644 --- a/app/models/repository_row.rb +++ b/app/models/repository_row.rb @@ -98,6 +98,8 @@ class RepositoryRow < ApplicationRecord class_name: 'RepositoryRow', source: :parent, dependent: :destroy + has_many :storage_location_repository_rows, inverse_of: :repository_row, dependent: :destroy + has_many :storage_locations, through: :storage_location_repository_rows auto_strip_attributes :name, nullify: false validates :name, diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb new file mode 100644 index 0000000000..8249b76678 --- /dev/null +++ b/app/models/storage_location.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class StorageLocation < ApplicationRecord + include Discard::Model + ID_PREFIX = 'SL' + include PrefixedIdModel + + default_scope -> { kept } + + has_one_attached :image + + belongs_to :team + belongs_to :parent, class_name: 'StorageLocation', optional: true + belongs_to :created_by, class_name: 'User' + + has_many :storage_location_repository_rows, inverse_of: :storage_location + has_many :repository_rows, through: :storage_location_repository_row + + validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } + + after_discard do + StorageLocation.where(parent_id: id).each(&:discard) + storage_location_repository_rows.each(&:discard) + end +end diff --git a/app/models/storage_location_repository_row.rb b/app/models/storage_location_repository_row.rb new file mode 100644 index 0000000000..f3bdb1d1f4 --- /dev/null +++ b/app/models/storage_location_repository_row.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class StorageLocationRepositoryRow < ApplicationRecord + include Discard::Model + + default_scope -> { kept } + + belongs_to :storage_location, inverse_of: :storage_location_repository_rows + belongs_to :repository_row, inverse_of: :storage_location_repository_rows + belongs_to :created_by, class_name: 'User' + + with_options if: -> { storage_location.container && storage_location.metadata['type'] == 'grid' } do + validate :position_must_be_present + validate :ensure_uniq_position + end + + def position_must_be_present + if metadata['position'].blank? + errors.add(:base, I18n.t('activerecord.errors.models.storage_location.missing_position')) + end + end + + def ensure_uniq_position + if StorageLocationRepositoryRow.where(storage_location: storage_location) + .where('metadata @> ?', { position: metadata['position'] }.to_json) + .where.not(id: id).exists? + errors.add(:base, I18n.t('activerecord.errors.models.storage_location.not_uniq_position')) + end + end +end diff --git a/app/serializers/lists/storage_location_repository_row_serializer.rb b/app/serializers/lists/storage_location_repository_row_serializer.rb new file mode 100644 index 0000000000..57a4bb7489 --- /dev/null +++ b/app/serializers/lists/storage_location_repository_row_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Lists + class StorageLocationRepositoryRowSerializer < ActiveModel::Serializer + attributes :created_by, :created_on, :position + + belongs_to :repository_row, serializer: RepositoryRowSerializer + + def created_by + object.created_by.full_name + end + + def created_on + I18n.l(object.created_at, format: :full) + end + + def position + object.metadata['position'] + end + end +end diff --git a/app/serializers/lists/storage_location_serializer.rb b/app/serializers/lists/storage_location_serializer.rb new file mode 100644 index 0000000000..10eb48f575 --- /dev/null +++ b/app/serializers/lists/storage_location_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Lists + class StorageLocationSerializer < ActiveModel::Serializer + attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on + + def owned_by + object.team.name + end + + def created_by + object.created_by.full_name + end + + def created_on + I18n.l(object.created_at, format: :full) + end + end +end diff --git a/app/services/lists/base_service.rb b/app/services/lists/base_service.rb index 2ac6ceb2af..c3b844f904 100644 --- a/app/services/lists/base_service.rb +++ b/app/services/lists/base_service.rb @@ -29,7 +29,7 @@ def order_params end def paginate_records - @records = @records.page(@params[:page]).per(@params[:per_page]) + @records = @records.page(@params[:page]).per(@params[:per_page]) if @params[:page].present? end def sort_direction(order_params) diff --git a/app/services/lists/storage_location_repository_rows_service.rb b/app/services/lists/storage_location_repository_rows_service.rb new file mode 100644 index 0000000000..6f671b8ab7 --- /dev/null +++ b/app/services/lists/storage_location_repository_rows_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Lists + class StorageLocationRepositoryRowsService < BaseService + def initialize(team, params) + @team = team + @storage_location_id = params[:storage_location_id] + @params = params + end + + def fetch_records + @records = StorageLocationRepositoryRow.includes(:repository_row).where(storage_location_id: @storage_location_id) + end + + def filter_records + end + end +end diff --git a/app/services/lists/storage_locations_service.rb b/app/services/lists/storage_locations_service.rb new file mode 100644 index 0000000000..e802be6f68 --- /dev/null +++ b/app/services/lists/storage_locations_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Lists + class StorageLocationsService < BaseService + def initialize(team, params) + @team = team + @parent_id = params[:parent_id] + @params = params + end + + def fetch_records + @records = StorageLocation.where(team: @team, parent_id: @parent_id) + end + + def filter_records + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index e48b383cc0..ec905ed98a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -259,6 +259,9 @@ en: attributes: text: Text is too long position: "Position has already been taken by another item in the checklist" + storage_location: + missing_position: 'Missing position metadata' + not_uniq_position: 'Position already taken' storage: limit_reached: "Storage limit has been reached." helpers: diff --git a/config/routes.rb b/config/routes.rb index 49db07eb47..4e081d8b3b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -807,6 +807,10 @@ resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy) + resources :storage_locations, only: %i(index create destroy update) do + resources :storage_location_repository_rows, only: %i(index create destroy update) + end + get 'search' => 'search#index' get 'search/new' => 'search#new', as: :new_search resource :search, only: [], controller: :search do diff --git a/db/migrate/20240705122903_add_storage_locations.rb b/db/migrate/20240705122903_add_storage_locations.rb new file mode 100644 index 0000000000..10b18a7332 --- /dev/null +++ b/db/migrate/20240705122903_add_storage_locations.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class AddStorageLocations < ActiveRecord::Migration[7.0] + include DatabaseHelper + + def up + create_table :storage_locations do |t| + t.string :name + t.string :description + t.references :parent, index: true, foreign_key: { to_table: :storage_locations } + t.references :team, index: true, foreign_key: { to_table: :teams } + t.references :created_by, foreign_key: { to_table: :users } + t.boolean :container, default: false, null: false, index: true + t.jsonb :metadata, null: false, default: {} + t.datetime :discarded_at, index: true + + t.timestamps + end + + create_table :storage_location_repository_rows do |t| + t.references :repository_row, index: true, foreign_key: { to_table: :repository_rows } + t.references :storage_location, index: true, foreign_key: { to_table: :storage_locations } + t.references :created_by, foreign_key: { to_table: :users } + t.jsonb :metadata, null: false, default: {} + t.datetime :discarded_at, index: true + + t.timestamps + end + + add_gin_index_without_tags :storage_locations, :name + add_gin_index_without_tags :storage_locations, :description + end + + def down + drop_table :storage_location_repository_rows + drop_table :storage_locations + end +end From b1e5199c59e013c8c622c59969069cfbf6f60651 Mon Sep 17 00:00:00 2001 From: Andrej Date: Wed, 10 Jul 2024 14:57:00 +0200 Subject: [PATCH 002/249] Add schema [SCI-10465] --- db/schema.rb | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 5ecee75c70..6dfb6998ef 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do +ActiveRecord::Schema[7.0].define(version: 2024_07_05_122903) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gist" enable_extension "pg_trgm" @@ -721,8 +721,8 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "type" - t.datetime "start_time_dup" - t.datetime "end_time_dup" + t.datetime "start_time_dup", precision: nil + t.datetime "end_time_dup", precision: nil t.index "((end_time)::date)", name: "index_repository_date_time_range_values_on_end_time_as_date", where: "((type)::text = 'RepositoryDateRangeValue'::text)" t.index "((end_time)::time without time zone)", name: "index_repository_date_time_range_values_on_end_time_as_time", where: "((type)::text = 'RepositoryTimeRangeValue'::text)" t.index "((start_time)::date)", name: "index_repository_date_time_range_values_on_start_time_as_date", where: "((type)::text = 'RepositoryDateRangeValue'::text)" @@ -1083,6 +1083,40 @@ t.index ["user_id"], name: "index_steps_on_user_id" end + create_table "storage_location_repository_rows", force: :cascade do |t| + t.bigint "repository_row_id" + t.bigint "storage_location_id" + t.bigint "created_by_id" + t.jsonb "metadata", default: {}, null: false + t.datetime "discarded_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["created_by_id"], name: "index_storage_location_repository_rows_on_created_by_id" + t.index ["discarded_at"], name: "index_storage_location_repository_rows_on_discarded_at" + t.index ["repository_row_id"], name: "index_storage_location_repository_rows_on_repository_row_id" + t.index ["storage_location_id"], name: "index_storage_location_repository_rows_on_storage_location_id" + end + + create_table "storage_locations", force: :cascade do |t| + t.string "name" + t.string "description" + t.bigint "parent_id" + t.bigint "team_id" + t.bigint "created_by_id" + t.boolean "container", default: false, null: false + t.jsonb "metadata", default: {}, null: false + t.datetime "discarded_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index "trim_html_tags((description)::text) gin_trgm_ops", name: "index_storage_locations_on_description", using: :gin + t.index "trim_html_tags((name)::text) gin_trgm_ops", name: "index_storage_locations_on_name", using: :gin + t.index ["container"], name: "index_storage_locations_on_container" + t.index ["created_by_id"], name: "index_storage_locations_on_created_by_id" + t.index ["discarded_at"], name: "index_storage_locations_on_discarded_at" + t.index ["parent_id"], name: "index_storage_locations_on_parent_id" + t.index ["team_id"], name: "index_storage_locations_on_team_id" + end + create_table "tables", force: :cascade do |t| t.binary "contents", null: false t.datetime "created_at", precision: nil, null: false @@ -1278,6 +1312,9 @@ t.integer "failed_attempts", default: 0, null: false t.datetime "locked_at", precision: nil t.string "unlock_token" + t.string "api_key" + t.datetime "api_key_expires_at", precision: nil + t.datetime "api_key_created_at", precision: nil t.index "trim_html_tags((full_name)::text) gin_trgm_ops", name: "index_users_on_full_name", using: :gin t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true @@ -1502,6 +1539,12 @@ add_foreign_key "steps", "protocols" add_foreign_key "steps", "users" add_foreign_key "steps", "users", column: "last_modified_by_id" + add_foreign_key "storage_location_repository_rows", "repository_rows" + add_foreign_key "storage_location_repository_rows", "storage_locations" + add_foreign_key "storage_location_repository_rows", "users", column: "created_by_id" + add_foreign_key "storage_locations", "storage_locations", column: "parent_id" + add_foreign_key "storage_locations", "teams" + add_foreign_key "storage_locations", "users", column: "created_by_id" add_foreign_key "tables", "users", column: "created_by_id" add_foreign_key "tables", "users", column: "last_modified_by_id" add_foreign_key "tags", "projects" From 88f6a12bdfe514c7959d2de44a11a200ec7a74a2 Mon Sep 17 00:00:00 2001 From: Andrej Date: Thu, 11 Jul 2024 07:54:44 +0200 Subject: [PATCH 003/249] Fix serializing local storage on update and create [SCI-10465] --- .../storage_location_repository_rows_controller.rb | 12 ++++++++---- app/controllers/storage_locations_controller.rb | 4 ++-- app/models/storage_location.rb | 2 +- .../storage_location_repository_rows_service.rb | 3 +-- app/services/lists/storage_locations_service.rb | 3 +-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb index bab428d806..416a965364 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -20,7 +20,9 @@ def update @storage_location_repository_row.update(storage_location_repository_row_params) if @storage_location_repository_row.save - render json: {} + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer, + include: :repository_row else render json: @storage_location_repository_row.errors, status: :unprocessable_entity end @@ -30,12 +32,14 @@ def create @storage_location_repository_row = StorageLocationRepositoryRow.new( repository_row: @repository_row, storage_location: @storage_location, - metadata: storage_location_repository_row_params[:metadata], + metadata: storage_location_repository_row_params[:metadata] || {}, created_by: current_user ) if @storage_location_repository_row.save - render json: {} + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer, + include: :repository_row else render json: @storage_location_repository_row.errors, status: :unprocessable_entity end @@ -53,7 +57,7 @@ def destroy def load_storage_location_repository_row @storage_location_repository_row = StorageLocationRepositoryRow.find( - storage_location_repository_row_params[:storage_location_id] + storage_location_repository_row_params[:id] ) render_404 unless @storage_location_repository_row end diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 309167cc11..bea2d3b60e 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -15,7 +15,7 @@ def update @storage_location.update(storage_location_params) if @storage_location.save - render json: {} + render json: @storage_location, serializer: Lists::StorageLocationSerializer else render json: @storage_location.errors, status: :unprocessable_entity end @@ -29,7 +29,7 @@ def create @storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id] if @storage_location.save - render json: {} + render json: @storage_location, serializer: Lists::StorageLocationSerializer else render json: @storage_location.errors, status: :unprocessable_entity end diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index 8249b76678..40e3d767dc 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -19,7 +19,7 @@ class StorageLocation < ApplicationRecord validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } after_discard do - StorageLocation.where(parent_id: id).each(&:discard) + StorageLocation.where(parent_id: id).find_each(&:discard) storage_location_repository_rows.each(&:discard) end end diff --git a/app/services/lists/storage_location_repository_rows_service.rb b/app/services/lists/storage_location_repository_rows_service.rb index 6f671b8ab7..bb3abe765b 100644 --- a/app/services/lists/storage_location_repository_rows_service.rb +++ b/app/services/lists/storage_location_repository_rows_service.rb @@ -12,7 +12,6 @@ def fetch_records @records = StorageLocationRepositoryRow.includes(:repository_row).where(storage_location_id: @storage_location_id) end - def filter_records - end + def filter_records; end end end diff --git a/app/services/lists/storage_locations_service.rb b/app/services/lists/storage_locations_service.rb index e802be6f68..9829a243e0 100644 --- a/app/services/lists/storage_locations_service.rb +++ b/app/services/lists/storage_locations_service.rb @@ -12,7 +12,6 @@ def fetch_records @records = StorageLocation.where(team: @team, parent_id: @parent_id) end - def filter_records - end + def filter_records; end end end From 1090fc3cf882d74d83475ab98a106c6c90b33f5b Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 16 Jul 2024 15:20:02 +0200 Subject: [PATCH 004/249] Add storage locations table [SCI-10857] --- .../storage_locations_controller.rb | 50 +++++- app/helpers/left_menu_bar_helper.rb | 16 +- .../packs/vue/storage_locations_table.js | 10 ++ .../vue/storage_locations/table.vue | 143 ++++++++++++++++++ app/permissions/team.rb | 4 + .../lists/storage_location_serializer.rb | 10 +- app/views/storage_locations/index.html.erb | 23 +++ config/locales/en.yml | 18 +++ config/routes.rb | 3 + config/webpack/webpack.config.js | 3 +- 10 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 app/javascript/packs/vue/storage_locations_table.js create mode 100644 app/javascript/vue/storage_locations/table.vue create mode 100644 app/views/storage_locations/index.html.erb diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index bea2d3b60e..a6af1aecc8 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -4,10 +4,17 @@ 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 def index - storage_locations = Lists::StorageLocationsService.new(current_team, storage_location_params).call - render json: storage_locations, each_serializer: Lists::StorageLocationSerializer + respond_to do |format| + format.html + format.json do + storage_locations = Lists::StorageLocationsService.new(current_team, params).call + render json: storage_locations, each_serializer: Lists::StorageLocationSerializer, + user: current_user, meta: pagination_dict(storage_locations) + end + end end def update @@ -43,6 +50,12 @@ def destroy end end + def actions_toolbar + render json: { + actions: [] # TODO: Add actions + } + end + private def storage_location_params @@ -62,4 +75,37 @@ def check_read_permissions def check_manage_permissions render_403 unless true end + + def set_breadcrumbs_items + @breadcrumbs_items = [] + + @breadcrumbs_items.push({ + label: t('breadcrumbs.inventories') + }) + + @breadcrumbs_items.push({ + label: t('breadcrumbs.locations'), + url: storage_locations_path + }) + + storage_locations = [] + if params[:parent_id] + location = StorageLocation.where(team: current_team).find_by(id: params[:parent_id]) + if location + storage_locations.unshift(breadcrumbs_item(location)) + while location.parent + location = location.parent + storage_locations.unshift(breadcrumbs_item(location)) + end + end + end + @breadcrumbs_items += storage_locations + end + + def breadcrumbs_item(location) + { + label: location.name, + url: storage_locations_path(parent_id: location.id) + } + end end diff --git a/app/helpers/left_menu_bar_helper.rb b/app/helpers/left_menu_bar_helper.rb index f8a77be758..b6a2d22d27 100644 --- a/app/helpers/left_menu_bar_helper.rb +++ b/app/helpers/left_menu_bar_helper.rb @@ -19,8 +19,16 @@ def left_menu_elements url: repositories_path, name: t('left_menu_bar.repositories'), icon: 'sn-icon-inventory', - active: repositories_are_selected?, - submenu: [] + active: repositories_are_selected? || storage_locations_are_selected?, + submenu: [{ + url: repositories_path, + name: t('left_menu_bar.items'), + active: repositories_are_selected? + }, { + url: storage_locations_path, + name: t('left_menu_bar.locations'), + active: storage_locations_are_selected? + }] }, { url: "#", name: t('left_menu_bar.templates'), @@ -63,6 +71,10 @@ def repositories_are_selected? controller_name == 'repositories' end + def storage_locations_are_selected? + controller_name == 'storage_locations' + end + def protocols_are_selected? controller_name == 'protocols' end diff --git a/app/javascript/packs/vue/storage_locations_table.js b/app/javascript/packs/vue/storage_locations_table.js new file mode 100644 index 0000000000..e83db78249 --- /dev/null +++ b/app/javascript/packs/vue/storage_locations_table.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import PerfectScrollbar from 'vue3-perfect-scrollbar'; +import StorageLocations from '../../vue/storage_locations/table.vue'; +import { mountWithTurbolinks } from './helpers/turbolinks.js'; + +const app = createApp(); +app.component('StorageLocations', StorageLocations); +app.config.globalProperties.i18n = window.I18n; +app.use(PerfectScrollbar); +mountWithTurbolinks(app, '#storageLocationsTable'); diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue new file mode 100644 index 0000000000..53991400ef --- /dev/null +++ b/app/javascript/vue/storage_locations/table.vue @@ -0,0 +1,143 @@ + + + diff --git a/app/permissions/team.rb b/app/permissions/team.rb index 0507a53c42..7b69fe2e7e 100644 --- a/app/permissions/team.rb +++ b/app/permissions/team.rb @@ -43,6 +43,10 @@ within_limits && team.permission_granted?(user, TeamPermissions::INVENTORIES_CREATE) end + can :create_storage_locations do |user, team| + true # TODO: Add permission check + end + can :create_reports do |user, team| team.permission_granted?(user, TeamPermissions::REPORTS_CREATE) end diff --git a/app/serializers/lists/storage_location_serializer.rb b/app/serializers/lists/storage_location_serializer.rb index 10eb48f575..ff69c971b0 100644 --- a/app/serializers/lists/storage_location_serializer.rb +++ b/app/serializers/lists/storage_location_serializer.rb @@ -2,7 +2,9 @@ module Lists class StorageLocationSerializer < ActiveModel::Serializer - attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on + include Rails.application.routes.url_helpers + + attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on, :urls def owned_by object.team.name @@ -15,5 +17,11 @@ def created_by def created_on I18n.l(object.created_at, format: :full) end + + def urls + { + show: storage_locations_path(parent_id: object.id), + } + end end end diff --git a/app/views/storage_locations/index.html.erb b/app/views/storage_locations/index.html.erb new file mode 100644 index 0000000000..44b7556424 --- /dev/null +++ b/app/views/storage_locations/index.html.erb @@ -0,0 +1,23 @@ +<% provide(:head_title, t("storage_locations.index.head_title")) %> +<% provide(:container_class, "no-second-nav-container") %> + +<% if current_team %> +
+
+
+

<%= t('storage_locations.index.head_title') %>

+
+
+
+
+ +
+
+
+ + <%= javascript_include_tag 'vue_storage_locations_table' %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index ec905ed98a..5409ac5fff 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -343,6 +343,8 @@ en: templates: "Templates" protocol: "Protocol" label: "Label" + items: "Items" + locations: "Locations" reports: "Reports" settings: "Settings" activities: "Activities" @@ -2663,6 +2665,21 @@ en: repository_ledger_records: errors: my_module_references_missing: 'Task references are not set' + storage_locations: + index: + head_title: "Locations" + new_location: "New location" + new_box: "New box" + table: + name: "Location name" + id: "ID" + sub_locations: "Sub-locations" + items: "Items" + free_spaces: "Free spaces" + shared: "Shared" + owned_by: "Owned by" + created_on: "Created on" + description: "Description" libraries: manange_modal_column_index: title: "Manage columns" @@ -4302,6 +4319,7 @@ en: labels: "Label" teams: "All Teams" addons: "Add-ons" + locations: "Locations" label_printer: "Label printer" fluics_printer: "Fluics printer" diff --git a/config/routes.rb b/config/routes.rb index 4e081d8b3b..ad233e0a63 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -808,6 +808,9 @@ resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy) resources :storage_locations, only: %i(index create destroy update) do + collection do + get :actions_toolbar + end resources :storage_location_repository_rows, only: %i(index create destroy update) end diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js index 623270ff44..b8800b1b8f 100644 --- a/config/webpack/webpack.config.js +++ b/config/webpack/webpack.config.js @@ -65,7 +65,8 @@ const entryList = { vue_legacy_tags_modal: './app/javascript/packs/vue/legacy/tags_modal.js', vue_legacy_access_modal: './app/javascript/packs/vue/legacy/access_modal.js', vue_legacy_repository_menu_dropdown: './app/javascript/packs/vue/legacy/repository_menu_dropdown.js', - vue_dashboard_new_task: './app/javascript/packs/vue/dashboard_new_task.js' + vue_dashboard_new_task: './app/javascript/packs/vue/dashboard_new_task.js', + vue_storage_locations_table: './app/javascript/packs/vue/storage_locations_table.js' }; // Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949 From 782256f6557315d499a16183e05303da095ce342 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 18 Jul 2024 11:46:30 +0200 Subject: [PATCH 005/249] Add toolbar for storage locations [SCI-10861] --- .../storage_locations_controller.rb | 6 +- app/permissions/team.rb | 4 + .../toolbars/storage_locations_service.rb | 86 +++++++++++++++++++ config/locales/en.yml | 5 ++ config/routes.rb | 4 + 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/services/toolbars/storage_locations_service.rb diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index a6af1aecc8..9d3521058d 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -52,7 +52,11 @@ def destroy 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 diff --git a/app/permissions/team.rb b/app/permissions/team.rb index 7b69fe2e7e..827fa6e4c1 100644 --- a/app/permissions/team.rb +++ b/app/permissions/team.rb @@ -47,6 +47,10 @@ true # TODO: Add permission check end + can :manage_storage_locations do |user, team| + true # TODO: Add permission check + end + can :create_reports do |user, team| team.permission_granted?(user, TeamPermissions::REPORTS_CREATE) end diff --git a/app/services/toolbars/storage_locations_service.rb b/app/services/toolbars/storage_locations_service.rb new file mode 100644 index 0000000000..fe33eeeff8 --- /dev/null +++ b/app/services/toolbars/storage_locations_service.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Toolbars + class StorageLocationsService + attr_reader :current_user + + include Canaid::Helpers::PermissionsHelper + include Rails.application.routes.url_helpers + + def initialize(current_user, storage_location_ids: []) + @current_user = current_user + @storage_locations = StorageLocation.where(id: storage_location_ids) + + @single = @storage_locations.length == 1 + end + + def actions + return [] if @storage_locations.none? + + [ + edit_action, + move_action, + duplicate_action, + delete_action + ].compact + end + + private + + def edit_action + return unless @single + + return unless can_manage_storage_locations?(current_user.current_team) + + { + name: 'edit', + label: I18n.t('storage_locations.index.toolbar.edit'), + icon: 'sn-icon sn-icon-edit', + path: storage_location_path(@storage_locations.first), + type: :emit + } + end + + def move_action + return unless @single + + return unless can_manage_storage_locations?(current_user.current_team) + + { + name: 'set_as_default', + label: I18n.t("storage_locations.index.toolbar.move"), + icon: 'sn-icon sn-icon-move', + path: move_storage_location_path(@storage_locations.first), + type: :emit + } + end + + def duplicate_action + return unless @single + + return unless can_manage_storage_locations?(current_user.current_team) + + { + name: 'duplicate', + label: I18n.t('storage_locations.index.toolbar.duplicate'), + icon: 'sn-icon sn-icon-duplicate', + path: copy_storage_location_path(@storage_locations.first), + type: :emit + } + end + + def delete_action + return unless @single + + return unless can_manage_storage_locations?(current_user.current_team) + + { + name: 'delete', + label: I18n.t('storage_locations.index.toolbar.delete'), + icon: 'sn-icon sn-icon-delete', + path: storage_location_path(@storage_locations.first), + type: :emit + } + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 5409ac5fff..d32c0f24e6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2670,6 +2670,11 @@ en: head_title: "Locations" new_location: "New location" new_box: "New box" + toolbar: + edit: 'Edit' + move: 'Move' + duplicate: 'Duplicate' + delete: 'Delete' table: name: "Location name" id: "ID" diff --git a/config/routes.rb b/config/routes.rb index ad233e0a63..41a447ed5a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -811,6 +811,10 @@ collection do get :actions_toolbar end + member do + post :move + post :copy + end resources :storage_location_repository_rows, only: %i(index create destroy update) end From 4f88a5581d7558edb9475c47cc164c459bdb251d Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Thu, 18 Jul 2024 13:05:59 +0200 Subject: [PATCH 006/249] Implement sub-location count column [SCI-10859] --- app/javascript/vue/storage_locations/table.vue | 3 ++- app/models/storage_location.rb | 3 ++- .../lists/storage_location_serializer.rb | 13 +++++++++++-- app/services/lists/storage_locations_service.rb | 7 ++++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 53991400ef..84fd2fe7b1 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -62,8 +62,9 @@ export default { sortable: true }, { - field: 'sub_locations', + field: 'sub_location_count', headerName: this.i18n.t('storage_locations.index.table.sub_locations'), + width: 250, sortable: true }, { diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index 40e3d767dc..b0be86ad54 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -13,7 +13,8 @@ class StorageLocation < ApplicationRecord belongs_to :parent, class_name: 'StorageLocation', optional: true belongs_to :created_by, class_name: 'User' - has_many :storage_location_repository_rows, inverse_of: :storage_location + has_many :storage_location_repository_rows, inverse_of: :storage_location, dependent: :destroy + has_many :storage_locations, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy has_many :repository_rows, through: :storage_location_repository_row validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } diff --git a/app/serializers/lists/storage_location_serializer.rb b/app/serializers/lists/storage_location_serializer.rb index ff69c971b0..ddef5e1f5e 100644 --- a/app/serializers/lists/storage_location_serializer.rb +++ b/app/serializers/lists/storage_location_serializer.rb @@ -4,7 +4,8 @@ module Lists class StorageLocationSerializer < ActiveModel::Serializer include Rails.application.routes.url_helpers - attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on, :urls + attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on, :urls, + :sub_location_count def owned_by object.team.name @@ -18,9 +19,17 @@ def created_on I18n.l(object.created_at, format: :full) end + def sub_location_count + if object.respond_to?(:sub_location_count) + object.sub_location_count + else + StorageLocation.where(parent_id: object.id).count + end + end + def urls { - show: storage_locations_path(parent_id: object.id), + show: storage_locations_path(parent_id: object.id) } end end diff --git a/app/services/lists/storage_locations_service.rb b/app/services/lists/storage_locations_service.rb index 9829a243e0..a632fbd462 100644 --- a/app/services/lists/storage_locations_service.rb +++ b/app/services/lists/storage_locations_service.rb @@ -9,7 +9,12 @@ def initialize(team, params) end def fetch_records - @records = StorageLocation.where(team: @team, parent_id: @parent_id) + @records = + StorageLocation.joins('LEFT JOIN storage_locations AS sub_locations ' \ + 'ON storage_locations.id = sub_locations.parent_id') + .select('storage_locations.*, COUNT(sub_locations.id) AS sub_location_count') + .where(team: @team, parent_id: @parent_id) + .group(:id) end def filter_records; end From d0c07d6a3dae692bfbb74bc9fd0f14b87a9767ab Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 19 Jul 2024 13:24:23 +0200 Subject: [PATCH 007/249] Add create/edit modal for storage locations [SCI-10860]/ --- .../storage_locations_controller.rb | 13 +- .../vue/shared/drag_and_drop_upload.vue | 2 +- .../vue/storage_locations/modals/new_edit.vue | 223 ++++++++++++++++++ .../vue/storage_locations/table.vue | 53 ++++- .../lists/storage_location_serializer.rb | 22 +- app/views/storage_locations/index.html.erb | 3 +- config/locales/en.yml | 27 +++ 7 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 app/javascript/vue/storage_locations/modals/new_edit.vue diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 9d3521058d..849b163485 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -18,13 +18,14 @@ def index 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 +34,12 @@ def create 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 @@ -63,8 +64,8 @@ def actions_toolbar private 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 load_storage_location diff --git a/app/javascript/vue/shared/drag_and_drop_upload.vue b/app/javascript/vue/shared/drag_and_drop_upload.vue index dd2c313bfe..2796acb484 100644 --- a/app/javascript/vue/shared/drag_and_drop_upload.vue +++ b/app/javascript/vue/shared/drag_and_drop_upload.vue @@ -19,7 +19,7 @@ {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }} {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }} -
+
{{ supportingText }}
diff --git a/app/javascript/vue/storage_locations/modals/new_edit.vue b/app/javascript/vue/storage_locations/modals/new_edit.vue new file mode 100644 index 0000000000..73a376d570 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/new_edit.vue @@ -0,0 +1,223 @@ + + + diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 53991400ef..6a176db8f9 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -6,16 +6,21 @@ :reloadingTable="reloadingTable" :toolbarActions="toolbarActions" :actionsUrl="actionsUrl" - @archive="archive" - @restore="restore" - @delete="deleteRepository" - @update="update" - @duplicate="duplicate" - @export="exportRepositories" - @share="share" - @create="newRepository = true" + @create_location="openCreateLocationModal" + @create_box="openCreateBoxModal" + @edit="edit" @tableReloaded="reloadingTable = false" /> + + + @@ -23,11 +28,13 @@ /* global */ import DataTable from '../shared/datatable/table.vue'; +import EditModal from './modals/new_edit.vue'; export default { name: 'RepositoriesTable', components: { - DataTable + DataTable, + EditModal }, props: { dataSource: { @@ -40,11 +47,17 @@ export default { }, createUrl: { type: String + }, + directUploadUrl: { + type: String } }, data() { return { - reloadingTable: false + reloadingTable: false, + openEditModal: false, + editModalMode: null, + editStorageLocation: null }; }, computed: { @@ -126,14 +139,34 @@ export default { } }, methods: { + openCreateLocationModal() { + this.openEditModal = true; + this.editModalMode = 'location'; + this.editStorageLocation = null; + }, + openCreateBoxModal() { + this.openEditModal = true; + this.editModalMode = 'box'; + this.editStorageLocation = null; + }, + edit(action, params) { + this.openEditModal = true; + this.editModalMode = params[0].container ? 'box' : 'location'; + [this.editStorageLocation] = params; + }, // Renderers nameRenderer(params) { const { name, urls } = params.data; + let boxIcon = ''; + if (params.data.container) { + boxIcon = ''; + } return ` + ${boxIcon} ${name} `; } diff --git a/app/serializers/lists/storage_location_serializer.rb b/app/serializers/lists/storage_location_serializer.rb index ff69c971b0..e29d31a02e 100644 --- a/app/serializers/lists/storage_location_serializer.rb +++ b/app/serializers/lists/storage_location_serializer.rb @@ -4,12 +4,24 @@ module Lists class StorageLocationSerializer < ActiveModel::Serializer include Rails.application.routes.url_helpers - attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on, :urls + attributes :id, :code, :name, :container, :description, :owned_by, :created_by, + :created_on, :urls, :metadata, :file_name def owned_by object.team.name end + def metadata + { + display_type: object.metadata['display_type'], + dimensions: object.metadata['dimensions'] || [] + } + end + + def file_name + object.image.filename if object.image.attached? + end + def created_by object.created_by.full_name end @@ -19,8 +31,14 @@ def created_on end def urls + show_url = if @object.container + storage_location_path(@object) + else + storage_locations_path(parent_id: object.id) + end { - show: storage_locations_path(parent_id: object.id), + show: show_url, + update: storage_location_path(@object) } end end diff --git a/app/views/storage_locations/index.html.erb b/app/views/storage_locations/index.html.erb index 44b7556424..564b05a727 100644 --- a/app/views/storage_locations/index.html.erb +++ b/app/views/storage_locations/index.html.erb @@ -13,7 +13,8 @@ diff --git a/config/locales/en.yml b/config/locales/en.yml index 957a355410..7605be074b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2687,6 +2687,33 @@ en: owned_by: "Owned by" created_on: "Created on" description: "Description" + edit_modal: + title_create_location: "Create new location" + title_create_box: "Create new box" + title_edit_location: "Edit location" + title_edit_box: "Edit box" + description_create_location: "Fill in the fields and create a new location." + description_create_box: "Fill in the fields to create a new box. Defining the box dimensions allows you to control the number of available spaces for placing inventory items." + name_label_location: "Location name" + image_label_location: "Image of location" + name_label_box: "Box name" + image_label_box: "Image of box" + drag_and_drop_supporting_text: ".png or .jpg file" + description_label: "Description" + name_placeholder: "Big freezer" + description_placeholder: "Keep everyone on the same page. You can also use smart annotations." + dimensions_label: "Dimensions (rows x columns)" + no_grid: "No grid" + grid: "Grid" + no_grid_tooltip: "You can assign unlimited items to the “No-grid” box but they do not have assigned position." + success_message: + create_location: "Location %{name} was successfully created." + create_box: "Box %{name} was successfully created." + edit_location: "Location %{name} was successfully updated." + edit_box: "Box %{name} was successfully updated." + errors: + max_length: "is too long (maximum is %{max_length} characters)" + libraries: manange_modal_column_index: title: "Manage columns" From 2f53772f61ccc9bba546b8278137e3996e8ef28a Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 23 Jul 2024 08:50:17 +0200 Subject: [PATCH 008/249] Add move modal for storage locations [SCI-10863] --- .../storage_locations_controller.rb | 49 +++++++- .../vue/storage_locations/modals/move.vue | 114 ++++++++++++++++++ .../storage_locations/modals/move_tree.vue | 45 +++++++ .../vue/storage_locations/table.vue | 23 +++- app/models/storage_location.rb | 32 +++++ app/models/team.rb | 1 + .../toolbars/storage_locations_service.rb | 2 +- app/views/storage_locations/index.html.erb | 1 + config/locales/en.yml | 11 ++ config/routes.rb | 1 + 10 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 app/javascript/vue/storage_locations/modals/move.vue create mode 100644 app/javascript/vue/storage_locations/modals/move_tree.vue diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 849b163485..fb30f964a1 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StorageLocationsController < ApplicationController - before_action :load_storage_location, only: %i(update destroy) + before_action :load_storage_location, only: %i(update destroy move) before_action :check_read_permissions, only: :index before_action :check_manage_permissions, except: :index before_action :set_breadcrumbs_items, only: :index @@ -51,6 +51,30 @@ def destroy 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 = StorageLocation.inner_storage_locations(current_team) + .order(:name) + .select(:id, :name, :parent_id, :container) + render json: storage_locations_recursive_builder(nil, records) + end + def actions_toolbar render json: { actions: @@ -68,8 +92,12 @@ def storage_location_params 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 @@ -95,7 +123,7 @@ def set_breadcrumbs_items storage_locations = [] if params[:parent_id] - location = StorageLocation.where(team: current_team).find_by(id: params[:parent_id]) + location = current_team.storage_locations.find_by(id: params[:parent_id]) if location storage_locations.unshift(breadcrumbs_item(location)) while location.parent @@ -113,4 +141,19 @@ def breadcrumbs_item(location) url: storage_locations_path(parent_id: location.id) } end + + def storage_locations_recursive_builder(storage_location, records) + children = records.select do |i| + defined?(i.parent_id) && i.parent_id == storage_location&.id + end + + children.filter_map do |i| + next if i.container + + { + storage_location: i, + children: storage_locations_recursive_builder(i, records) + } + end + end end diff --git a/app/javascript/vue/storage_locations/modals/move.vue b/app/javascript/vue/storage_locations/modals/move.vue new file mode 100644 index 0000000000..052b8e268f --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/move.vue @@ -0,0 +1,114 @@ + + + diff --git a/app/javascript/vue/storage_locations/modals/move_tree.vue b/app/javascript/vue/storage_locations/modals/move_tree.vue new file mode 100644 index 0000000000..720642392c --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/move_tree.vue @@ -0,0 +1,45 @@ + + + diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 6a176db8f9..d0efc50ad1 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -10,6 +10,7 @@ @create_box="openCreateBoxModal" @edit="edit" @tableReloaded="reloadingTable = false" + @move="move" /> + @@ -29,12 +33,14 @@ import DataTable from '../shared/datatable/table.vue'; import EditModal from './modals/new_edit.vue'; +import MoveModal from './modals/move.vue'; export default { name: 'RepositoriesTable', components: { DataTable, - EditModal + EditModal, + MoveModal }, props: { dataSource: { @@ -50,6 +56,9 @@ export default { }, directUploadUrl: { type: String + }, + storageLocationTreeUrl: { + type: String } }, data() { @@ -57,7 +66,9 @@ export default { reloadingTable: false, openEditModal: false, editModalMode: null, - editStorageLocation: null + editStorageLocation: null, + objectToMove: null, + moveToUrl: null }; }, computed: { @@ -169,6 +180,14 @@ export default { ${boxIcon} ${name} `; + }, + updateTable() { + this.reloadingTable = true; + this.objectToMove = null; + }, + move(event, rows) { + [this.objectToMove] = rows; + this.moveToUrl = event.path; } } }; diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index 40e3d767dc..d3b5051a62 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -17,9 +17,41 @@ class StorageLocation < ApplicationRecord has_many :repository_rows, through: :storage_location_repository_row validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } + validate :parent_validation, if: -> { parent.present? } after_discard do StorageLocation.where(parent_id: id).find_each(&:discard) storage_location_repository_rows.each(&:discard) end + + def self.inner_storage_locations(team, storage_location = nil) + entry_point_condition = storage_location ? 'parent_id = ?' : 'parent_id IS NULL' + + inner_storage_locations_sql = + "WITH RECURSIVE inner_storage_locations(id, selected_storage_locations_ids) AS ( + SELECT id, ARRAY[id] + FROM storage_locations + WHERE team_id = ? AND #{entry_point_condition} + UNION ALL + SELECT storage_locations.id, selected_storage_locations_ids || storage_locations.id + FROM inner_storage_locations + JOIN storage_locations ON storage_locations.parent_id = inner_storage_locations.id + WHERE NOT storage_locations.id = ANY(selected_storage_locations_ids) + ) + SELECT id FROM inner_storage_locations ORDER BY selected_storage_locations_ids".gsub(/\n|\t/, ' ').squeeze(' ') + + if storage_location.present? + where("storage_locations.id IN (#{inner_storage_locations_sql})", team.id, storage_location.id) + else + where("storage_locations.id IN (#{inner_storage_locations_sql})", team.id) + end + end + + def parent_validation + if parent.id == id + errors.add(:parent, I18n.t('activerecord.errors.models.storage_location.attributes.parent_storage_location')) + elsif StorageLocation.inner_storage_locations(team, self).exists?(id: parent_id) + errors.add(:parent, I18n.t('activerecord.errors.models.project_folder.attributes.parent_storage_location_child')) + end + end end diff --git a/app/models/team.rb b/app/models/team.rb index 4b3df88ae6..311fc27bbe 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -72,6 +72,7 @@ class Team < ApplicationRecord source_type: 'RepositoryBase', dependent: :destroy has_many :shareable_links, inverse_of: :team, dependent: :destroy + has_many :storage_locations, dependent: :destroy attr_accessor :without_templates diff --git a/app/services/toolbars/storage_locations_service.rb b/app/services/toolbars/storage_locations_service.rb index fe33eeeff8..4fdea27435 100644 --- a/app/services/toolbars/storage_locations_service.rb +++ b/app/services/toolbars/storage_locations_service.rb @@ -47,7 +47,7 @@ def move_action return unless can_manage_storage_locations?(current_user.current_team) { - name: 'set_as_default', + name: 'move', label: I18n.t("storage_locations.index.toolbar.move"), icon: 'sn-icon sn-icon-move', path: move_storage_location_path(@storage_locations.first), diff --git a/app/views/storage_locations/index.html.erb b/app/views/storage_locations/index.html.erb index 564b05a727..4229ceb357 100644 --- a/app/views/storage_locations/index.html.erb +++ b/app/views/storage_locations/index.html.erb @@ -15,6 +15,7 @@ data-source="<%= storage_locations_path(format: :json, parent_id: params[:parent_id]) %>" direct-upload-url="<%= rails_direct_uploads_url %>" create-url="<%= storage_locations_path(parent_id: params[:parent_id]) if can_create_storage_locations?(current_team) %>" + storage-location-tree-url="<%= tree_storage_locations_path %>" /> diff --git a/config/locales/en.yml b/config/locales/en.yml index 7605be074b..dd9cc89022 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -264,6 +264,9 @@ en: storage_location: missing_position: 'Missing position metadata' not_uniq_position: 'Position already taken' + attributes: + parent_storage_location: "Storage location cannot be parent to itself" + parent_storage_location_child: "Storage location cannot be moved to it's child" storage: limit_reached: "Storage limit has been reached." helpers: @@ -2713,6 +2716,14 @@ en: edit_box: "Box %{name} was successfully updated." errors: max_length: "is too long (maximum is %{max_length} characters)" + move_modal: + title: 'Move %{name}' + description: 'Select where you want to move %{name}.' + search_header: 'Locations' + success_flash: "You have successfully moved the selected location/box to another location." + error_flash: "An error occurred. The selected location/box has not been moved." + placeholder: + find_storage_locations: 'Find location' libraries: manange_modal_column_index: diff --git a/config/routes.rb b/config/routes.rb index 41a447ed5a..8dd0e7692a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -810,6 +810,7 @@ resources :storage_locations, only: %i(index create destroy update) do collection do get :actions_toolbar + get :tree end member do post :move From d725e278c17939c2fa5d8313c550c5b67648ca07 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 23 Jul 2024 09:44:57 +0200 Subject: [PATCH 009/249] Fix in memory loading of storage location tree [SCI-10863] --- app/controllers/storage_locations_controller.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index fb30f964a1..33f049c304 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -142,17 +142,11 @@ def breadcrumbs_item(location) } end - def storage_locations_recursive_builder(storage_location, records) - children = records.select do |i| - defined?(i.parent_id) && i.parent_id == storage_location&.id - end - - children.filter_map do |i| - next if i.container - + def storage_locations_recursive_builder(parent_storage_location, records) + records.where(parent: parent_storage_location, container: false).map do |storage_location| { - storage_location: i, - children: storage_locations_recursive_builder(i, records) + storage_location: storage_location, + children: storage_locations_recursive_builder(storage_location, records) } end end From 1c3cc71174b4a8f7029b0423ad2ea628505e8d54 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 22 Jul 2024 12:12:47 +0200 Subject: [PATCH 010/249] Add duplicate action to locations [SCI-10862] --- .../storage_locations_controller.rb | 11 ++++- .../vue/storage_locations/table.vue | 14 ++++++- app/models/concerns/cloneable.rb | 18 +++++--- app/models/storage_location.rb | 41 +++++++++++++++++++ .../toolbars/storage_locations_service.rb | 2 +- config/locales/en.yml | 2 + config/routes.rb | 2 +- 7 files changed, 80 insertions(+), 10 deletions(-) diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 849b163485..8440ed8660 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StorageLocationsController < ApplicationController - before_action :load_storage_location, only: %i(update destroy) + before_action :load_storage_location, only: %i(update destroy duplicate) before_action :check_read_permissions, only: :index before_action :check_manage_permissions, except: :index before_action :set_breadcrumbs_items, only: :index @@ -51,6 +51,15 @@ def destroy 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 actions_toolbar render json: { actions: diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 6a176db8f9..13b6310ab7 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -9,6 +9,7 @@ @create_location="openCreateLocationModal" @create_box="openCreateBoxModal" @edit="edit" + @duplicate="duplicate" @tableReloaded="reloadingTable = false" /> @@ -25,8 +26,9 @@ diff --git a/app/javascript/vue/storage_locations/grid.vue b/app/javascript/vue/storage_locations/grid.vue new file mode 100644 index 0000000000..ded0d878f0 --- /dev/null +++ b/app/javascript/vue/storage_locations/grid.vue @@ -0,0 +1,102 @@ + + + diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index b276156452..f8d10648c3 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -39,6 +39,14 @@ def duplicate! end end + def with_grid? + metadata['display_type'] == 'grid' + end + + def grid_size + metadata['dimensions'] if with_grid? + end + private def recursive_duplicate(old_parent_id = nil, new_parent_id = nil) @@ -62,8 +70,9 @@ def copy_image(old_storage_location, new_storage_location) metadata: old_blob.metadata ) new_storage_location.image.attach(to_blob) + end end - + def self.inner_storage_locations(team, storage_location = nil) entry_point_condition = storage_location ? 'parent_id = ?' : 'parent_id IS NULL' diff --git a/app/serializers/lists/storage_location_repository_row_serializer.rb b/app/serializers/lists/storage_location_repository_row_serializer.rb index 57a4bb7489..628f0e99b4 100644 --- a/app/serializers/lists/storage_location_repository_row_serializer.rb +++ b/app/serializers/lists/storage_location_repository_row_serializer.rb @@ -2,9 +2,15 @@ module Lists class StorageLocationRepositoryRowSerializer < ActiveModel::Serializer - attributes :created_by, :created_on, :position + attributes :created_by, :created_on, :position, :row_id, :row_name - belongs_to :repository_row, serializer: RepositoryRowSerializer + def row_id + object.repository_row.id + end + + def row_name + object.repository_row.name + end def created_by object.created_by.full_name diff --git a/app/views/storage_locations/index.html.erb b/app/views/storage_locations/index.html.erb index 4229ceb357..3cb7dcb0b2 100644 --- a/app/views/storage_locations/index.html.erb +++ b/app/views/storage_locations/index.html.erb @@ -2,13 +2,13 @@ <% provide(:container_class, "no-second-nav-container") %> <% if current_team %> -
+

<%= t('storage_locations.index.head_title') %>

-
+
+<% provide(:container_class, "no-second-nav-container") %> + +<% if current_team %> +
+
+
+

<%= @storage_location.name %>

+
+
+
+
+ +
+
+
+ + <%= javascript_include_tag 'vue_storage_locations_box' %> +<% end %> diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index 04d84ac30c..a580cc3cfc 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -636,6 +636,8 @@ class Extends preferences/index addons/index search/index + storage_locations/index + storage_locations/show ) DEFAULT_USER_NOTIFICATION_SETTINGS = { diff --git a/config/locales/en.yml b/config/locales/en.yml index 18549245f8..1e576d17f9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2671,6 +2671,13 @@ en: errors: my_module_references_missing: 'Task references are not set' storage_locations: + show: + table: + position: "Position" + reminders: "Reminders" + row_id: "Item ID" + row_name: "Name" + stock: "Stock" index: head_title: "Locations" new_location: "New location" diff --git a/config/routes.rb b/config/routes.rb index d3f668c56c..b4589d7db6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -807,7 +807,7 @@ resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy) - resources :storage_locations, only: %i(index create destroy update) do + resources :storage_locations, only: %i(index create destroy update show) do collection do get :actions_toolbar get :tree @@ -816,7 +816,11 @@ post :move post :duplicate end - resources :storage_location_repository_rows, only: %i(index create destroy update) + resources :storage_location_repository_rows, only: %i(index create destroy update) do + collection do + get :actions_toolbar + end + end end get 'search' => 'search#index' diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js index b8800b1b8f..bb9603fe28 100644 --- a/config/webpack/webpack.config.js +++ b/config/webpack/webpack.config.js @@ -66,7 +66,8 @@ const entryList = { vue_legacy_access_modal: './app/javascript/packs/vue/legacy/access_modal.js', vue_legacy_repository_menu_dropdown: './app/javascript/packs/vue/legacy/repository_menu_dropdown.js', vue_dashboard_new_task: './app/javascript/packs/vue/dashboard_new_task.js', - vue_storage_locations_table: './app/javascript/packs/vue/storage_locations_table.js' + vue_storage_locations_table: './app/javascript/packs/vue/storage_locations_table.js', + vue_storage_locations_box: './app/javascript/packs/vue/storage_locations_box.js' }; // Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949 From 9d541e28b2dc2ae356acb5b9e6df7bece0b968f4 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 23 Jul 2024 14:50:39 +0200 Subject: [PATCH 013/249] Add delete location action [SCI-10864] --- .../storage_locations_controller.rb | 2 +- .../vue/storage_locations/table.vue | 39 ++++++++++++++++++- .../toolbars/storage_locations_service.rb | 10 ++++- config/locales/en.yml | 7 ++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 4336f8008a..7b0fa55dfc 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -49,7 +49,7 @@ def destroy 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 diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 9f9096a588..945046335e 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -12,6 +12,7 @@ @duplicate="duplicate" @tableReloaded="reloadingTable = false" @move="move" + @delete="deleteStorageLocation" /> +
@@ -36,13 +44,15 @@ import axios from '../../packs/custom_axios.js'; import DataTable from '../shared/datatable/table.vue'; import EditModal from './modals/new_edit.vue'; import MoveModal from './modals/move.vue'; +import ConfirmationModal from '../shared/confirmation_modal.vue'; export default { name: 'RepositoriesTable', components: { DataTable, EditModal, - MoveModal + MoveModal, + ConfirmationModal }, props: { dataSource: { @@ -70,7 +80,9 @@ export default { editModalMode: null, editStorageLocation: null, objectToMove: null, - moveToUrl: null + moveToUrl: null, + storageLocationDeleteTitle: '', + storageLocationDeleteDescription: '' }; }, computed: { @@ -200,6 +212,29 @@ export default { move(event, rows) { [this.objectToMove] = rows; this.moveToUrl = event.path; + }, + async deleteStorageLocation(event, rows) { + const storageLocationType = rows[0].container ? this.i18n.t('storage_locations.box') : this.i18n.t('storage_locations.location'); + const description = ` +

${this.i18n.t('storage_locations.index.delete_modal.description_1_html', + { name: rows[0].name, type: storageLocationType, num_of_items: event.number_of_items })}

+

${this.i18n.t('storage_locations.index.delete_modal.description_2_html')}

`; + + this.storageLocationDeleteDescription = description; + this.storageLocationDeleteTitle = this.i18n.t('storage_locations.index.delete_modal.title', { type: storageLocationType }); + const ok = await this.$refs.deleteStorageLocationModal.show(); + if (ok) { + axios.delete(event.path).then((_) => { + this.reloadingTable = true; + HelperModule.flashAlertMsg(this.i18n.t('storage_locations.index.delete_modal.success_message', + { + type: storageLocationType[0].toUpperCase() + storageLocationType.slice(1), + name: rows[0].name + }), 'success'); + }).catch((error) => { + HelperModule.flashAlertMsg(error.response.data.error, 'danger'); + }); + } } } }; diff --git a/app/services/toolbars/storage_locations_service.rb b/app/services/toolbars/storage_locations_service.rb index a6b275b016..900b3004b5 100644 --- a/app/services/toolbars/storage_locations_service.rb +++ b/app/services/toolbars/storage_locations_service.rb @@ -74,11 +74,19 @@ def delete_action return unless can_manage_storage_locations?(current_user.current_team) + storage_location = @storage_locations.first + + number_of_items = storage_location.storage_location_repository_rows.count + + StorageLocation.inner_storage_locations(current_user.current_team, storage_location) + .where(container: true) + .joins(:storage_location_repository_rows) + .count { name: 'delete', label: I18n.t('storage_locations.index.toolbar.delete'), icon: 'sn-icon sn-icon-delete', - path: storage_location_path(@storage_locations.first), + number_of_items: number_of_items, + path: storage_location_path(storage_location), type: :emit } end diff --git a/config/locales/en.yml b/config/locales/en.yml index 1e576d17f9..a81190bb9d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2671,6 +2671,8 @@ en: errors: my_module_references_missing: 'Task references are not set' storage_locations: + box: 'box' + location: 'location' show: table: position: "Position" @@ -2733,6 +2735,11 @@ en: error_flash: "An error occurred. The selected location/box has not been moved." placeholder: find_storage_locations: 'Find location' + delete_modal: + title: 'Delete a %{type}' + description_1_html: "You're about to delete %{name}. This action will delete the %{type}. %{num_of_items} items inside will lose their assigned positions." + description_2_html: 'Are you sure you want to delete it?' + success_message: "%{type} %{name} successfully deleted." libraries: manange_modal_column_index: From 50928f69482b77b53e4a966a449eb33af6b899e0 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 25 Jul 2024 12:03:00 +0200 Subject: [PATCH 014/249] Add toolbar to container table [SCI-10868] --- ...age_location_repository_rows_controller.rb | 5 +- app/javascript/vue/storage_locations/box.vue | 16 +++++- ...torage_location_repository_rows_service.rb | 55 +++++++++++++++++++ config/locales/en.yml | 4 ++ config/routes.rb | 4 ++ 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 app/services/toolbars/storage_location_repository_rows_service.rb diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb index d59ebb08f4..947a5c5747 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -56,7 +56,10 @@ def destroy def actions_toolbar render json: { - actions: [] + actions: Toolbars::StorageLocationRepositoryRowsService.new( + current_user, + items_ids: JSON.parse(params[:items]).map { |i| i['id'] } + ).actions } end diff --git a/app/javascript/vue/storage_locations/box.vue b/app/javascript/vue/storage_locations/box.vue index cc08114784..7b71406731 100644 --- a/app/javascript/vue/storage_locations/box.vue +++ b/app/javascript/vue/storage_locations/box.vue @@ -3,7 +3,10 @@
- +
@@ -99,6 +102,17 @@ export default { }, toolbarActions() { const left = []; + + if (!this.withGrid) { + left.push({ + name: 'assign', + icon: 'sn-icon sn-icon-new-task', + label: this.i18n.t('storage_locations.show.toolbar.assign'), + type: 'emit', + buttonStyle: 'btn btn-primary' + }); + } + return { left, right: [] diff --git a/app/services/toolbars/storage_location_repository_rows_service.rb b/app/services/toolbars/storage_location_repository_rows_service.rb new file mode 100644 index 0000000000..51acf366f7 --- /dev/null +++ b/app/services/toolbars/storage_location_repository_rows_service.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Toolbars + class StorageLocationRepositoryRowsService + attr_reader :current_user + + include Canaid::Helpers::PermissionsHelper + include Rails.application.routes.url_helpers + + def initialize(current_user, items_ids: []) + @current_user = current_user + @assigned_rows = StorageLocationRepositoryRow.where(id: items_ids) + @storage_location = @assigned_rows.first&.storage_location + + @single = @assigned_rows.length == 1 + end + + def actions + return [] if @assigned_rows.none? + + [ + unassign_action, + move_action + ].compact + end + + private + + def unassign_action + { + name: 'edit', + label: I18n.t('storage_locations.show.toolbar.unassign'), + icon: 'sn-icon sn-icon-close', + path: unassign_storage_location_storage_location_repository_rows_path( + @storage_location, ids: @assigned_rows.pluck(:id) + ), + type: :emit + } + end + + def move_action + return unless @single + + { + name: 'move', + label: I18n.t('storage_locations.show.toolbar.move'), + icon: 'sn-icon sn-icon-move', + path: move_storage_location_storage_location_repository_row_path( + @storage_location, @assigned_rows.first + ), + type: :emit + } + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index a81190bb9d..b416848418 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2680,6 +2680,10 @@ en: row_id: "Item ID" row_name: "Name" stock: "Stock" + toolbar: + assign: 'Assign item' + unassign: 'Unassign' + move: 'Move' index: head_title: "Locations" new_location: "New location" diff --git a/config/routes.rb b/config/routes.rb index b4589d7db6..22aa7ef24d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -819,6 +819,10 @@ resources :storage_location_repository_rows, only: %i(index create destroy update) do collection do get :actions_toolbar + post :unassign + end + member do + post :move end end end From 776fdeabc5d6542980f2f17d6856058aa0944ef4 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 25 Jul 2024 14:06:26 +0200 Subject: [PATCH 015/249] Update radio buttons [SCI-10913] --- .../stylesheets/application.tailwind.css | 3 +- .../shared_styles/elements/radio_buttons.scss | 3 +- app/assets/stylesheets/tailwind/radio.css | 42 +++++++++++++++++++ app/views/design_elements/_radio.html.erb | 19 +++++++++ app/views/design_elements/index.html.erb | 2 + 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/tailwind/radio.css create mode 100644 app/views/design_elements/_radio.html.erb diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 400c99bdf9..9e83df130c 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -2,6 +2,7 @@ @import "tailwind/buttons"; @import "tailwind/modals"; @import "tailwind/flyouts"; +@import "tailwind/radio"; @import "tailwind/loader.css"; @tailwind base; @@ -69,6 +70,6 @@ html { @keyframes shine-lines { 0% { background-position: -150px } - + 40%, 100% { background-position: 320px } } diff --git a/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss b/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss index b940efe9e0..5b7fd31ca1 100644 --- a/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss +++ b/app/assets/stylesheets/shared_styles/elements/radio_buttons.scss @@ -1,5 +1,5 @@ // scss-lint:disable SelectorDepth QualifyingElement - +/* :root { --sci-radio-size: 16px; } @@ -85,3 +85,4 @@ input[type="radio"].sci-radio { } } } +*/ diff --git a/app/assets/stylesheets/tailwind/radio.css b/app/assets/stylesheets/tailwind/radio.css new file mode 100644 index 0000000000..1971193979 --- /dev/null +++ b/app/assets/stylesheets/tailwind/radio.css @@ -0,0 +1,42 @@ +@layer components { + + .sci-radio-container { + @apply inline-block h-4 w-4 relative; + } + + input[type="radio"].sci-radio { + @apply cursor-pointer shrink-0 h-4 w-4 m-0 opacity-0 relative z-[2]; + } + + input[type="radio"].sci-radio + .sci-radio-label { + @apply inline-block shrink-0 h-4 w-4 absolute left-0; + } + + input[type="radio"].sci-radio + .sci-radio-label::before { + @apply border-[1px] border-solid border-sn-black rounded-full text-white text-center transition-all + h-4 w-4 left-0 absolute; + content: ""; + } + + input[type="radio"].sci-radio + .sci-radio-label::after{ + @apply bg-white rounded-full text-white text-center transition-all + absolute w-2.5 h-2.5 top-[3px] left-[3px] ; + content: ""; + } + + input[type="radio"].sci-radio:checked + .sci-radio-label::before { + @apply !border-sn-blue; + } + + input[type="radio"].sci-radio:checked + .sci-radio-label::after { + @apply !bg-sn-science-blue; + } + + input[type="radio"].sci-radio:disabled + .sci-radio-label::before { + @apply !border-sn-sleepy-grey; + } + + input[type="radio"].sci-radio:checked:disabled + .sci-radio-label::after { + @apply !bg-sn-sleepy-grey; + } +} diff --git a/app/views/design_elements/_radio.html.erb b/app/views/design_elements/_radio.html.erb new file mode 100644 index 0000000000..2d7929388e --- /dev/null +++ b/app/views/design_elements/_radio.html.erb @@ -0,0 +1,19 @@ +

Radio

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/app/views/design_elements/index.html.erb b/app/views/design_elements/index.html.erb index b8c3d4dc1f..523cfe07e5 100644 --- a/app/views/design_elements/index.html.erb +++ b/app/views/design_elements/index.html.erb @@ -10,6 +10,8 @@ end %> +<%= render partial: 'radio' %> + <%= render partial: 'select' %> <%= render partial: 'modals' %> From e4fb5d71dfaf74fe3dcbc96e738037dd92afa222 Mon Sep 17 00:00:00 2001 From: Andrej Date: Mon, 29 Jul 2024 09:44:15 +0200 Subject: [PATCH 016/249] Add permissions for storage location and rename box to container [SCI-10882] --- ...age_location_repository_rows_controller.rb | 12 +++-- .../storage_locations_controller.rb | 25 ++++++++-- ..._box.js => storage_locations_container.js} | 6 +-- .../{box.vue => container.vue} | 4 +- .../vue/storage_locations/modals/new_edit.vue | 4 +- .../vue/storage_locations/table.vue | 20 ++++---- app/permissions/team.rb | 20 +++++++- .../lists/storage_locations_service.rb | 2 +- app/views/storage_locations/show.html.erb | 8 ++-- .../extends/permission_extends.rb | 28 ++++++++--- config/locales/en.yml | 18 +++---- config/webpack/webpack.config.js | 2 +- ...135302_add_storage_location_permissions.rb | 47 +++++++++++++++++++ 13 files changed, 146 insertions(+), 50 deletions(-) rename app/javascript/packs/vue/{storage_locations_box.js => storage_locations_container.js} (56%) rename app/javascript/vue/storage_locations/{box.vue => container.vue} (97%) create mode 100644 db/migrate/20240725135302_add_storage_location_permissions.rb diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb index 947a5c5747..ab15e6c1db 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -3,9 +3,9 @@ class StorageLocationRepositoryRowsController < ApplicationController before_action :load_storage_location_repository_row, only: %i(update destroy) before_action :load_storage_location - before_action :load_repository_row, only: %i(update destroy) - before_action :check_read_permissions, only: :index - before_action :check_manage_permissions, except: :index + before_action :load_repository_row, only: %i(create update destroy) + 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( @@ -90,10 +90,12 @@ def storage_location_repository_row_params 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 7b0fa55dfc..5532b7b0d5 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -2,8 +2,9 @@ class StorageLocationsController < ApplicationController before_action :load_storage_location, only: %i(update destroy duplicate move show) - before_action :check_read_permissions, only: :index - before_action :check_manage_permissions, except: :index + 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) before_action :set_breadcrumbs_items, only: %i(index show) def index @@ -111,11 +112,27 @@ def load_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 diff --git a/app/javascript/packs/vue/storage_locations_box.js b/app/javascript/packs/vue/storage_locations_container.js similarity index 56% rename from app/javascript/packs/vue/storage_locations_box.js rename to app/javascript/packs/vue/storage_locations_container.js index 1a44307440..abf6912c3f 100644 --- a/app/javascript/packs/vue/storage_locations_box.js +++ b/app/javascript/packs/vue/storage_locations_container.js @@ -1,10 +1,10 @@ import { createApp } from 'vue/dist/vue.esm-bundler.js'; import PerfectScrollbar from 'vue3-perfect-scrollbar'; -import StorageLocationsBox from '../../vue/storage_locations/box.vue'; +import StorageLocationsContainer from '../../vue/storage_locations/container.vue'; import { mountWithTurbolinks } from './helpers/turbolinks.js'; const app = createApp(); -app.component('StorageLocationsBox', StorageLocationsBox); +app.component('StorageLocationsContainer', StorageLocationsContainer); app.config.globalProperties.i18n = window.I18n; app.use(PerfectScrollbar); -mountWithTurbolinks(app, '#storageLocationsBox'); +mountWithTurbolinks(app, '#StorageLocationsContainer'); diff --git a/app/javascript/vue/storage_locations/box.vue b/app/javascript/vue/storage_locations/container.vue similarity index 97% rename from app/javascript/vue/storage_locations/box.vue rename to app/javascript/vue/storage_locations/container.vue index 7b71406731..53e5b970f8 100644 --- a/app/javascript/vue/storage_locations/box.vue +++ b/app/javascript/vue/storage_locations/container.vue @@ -13,7 +13,7 @@
{{ this.errors.name }}
-
+
@@ -163,7 +163,7 @@ export default { SmartAnnotation.init($(this.$refs.description), false); $(this.$refs.modal).on('hidden.bs.modal', this.handleAtWhoModalClose); - this.object.container = this.editModalMode === 'box'; + this.object.container = this.editModalMode === 'container'; }, methods: { submit() { diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 945046335e..01044eb5a1 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -7,7 +7,7 @@ :toolbarActions="toolbarActions" :actionsUrl="actionsUrl" @create_location="openCreateLocationModal" - @create_box="openCreateBoxModal" + @create_container="openCreateContainerModal" @edit="edit" @duplicate="duplicate" @tableReloaded="reloadingTable = false" @@ -149,9 +149,9 @@ export default { buttonStyle: 'btn btn-primary' }); left.push({ - name: 'create_box', + name: 'create_container', icon: 'sn-icon sn-icon-item', - label: this.i18n.t('storage_locations.index.new_box'), + label: this.i18n.t('storage_locations.index.new_container'), type: 'emit', path: this.createUrl, buttonStyle: 'btn btn-secondary' @@ -169,14 +169,14 @@ export default { this.editModalMode = 'location'; this.editStorageLocation = null; }, - openCreateBoxModal() { + openCreateContainerModal() { this.openEditModal = true; - this.editModalMode = 'box'; + this.editModalMode = 'container'; this.editStorageLocation = null; }, edit(action, params) { this.openEditModal = true; - this.editModalMode = params[0].container ? 'box' : 'location'; + this.editModalMode = params[0].container ? 'container' : 'location'; [this.editStorageLocation] = params; }, duplicate(action) { @@ -195,13 +195,13 @@ export default { name, urls } = params.data; - let boxIcon = ''; + let containerIcon = ''; if (params.data.container) { - boxIcon = ''; + containerIcon = ''; } return ` - ${boxIcon} + ${containerIcon} ${name} `; }, @@ -214,7 +214,7 @@ export default { this.moveToUrl = event.path; }, async deleteStorageLocation(event, rows) { - const storageLocationType = rows[0].container ? this.i18n.t('storage_locations.box') : this.i18n.t('storage_locations.location'); + const storageLocationType = rows[0].container ? this.i18n.t('storage_locations.container') : this.i18n.t('storage_locations.location'); const description = `

${this.i18n.t('storage_locations.index.delete_modal.description_1_html', { name: rows[0].name, type: storageLocationType, num_of_items: event.number_of_items })}

diff --git a/app/permissions/team.rb b/app/permissions/team.rb index 827fa6e4c1..bcc30367e8 100644 --- a/app/permissions/team.rb +++ b/app/permissions/team.rb @@ -43,12 +43,28 @@ within_limits && team.permission_granted?(user, TeamPermissions::INVENTORIES_CREATE) end + can :read_storage_locations do |user, team| + team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_READ) + end + can :create_storage_locations do |user, team| - true # TODO: Add permission check + team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_CREATE) end can :manage_storage_locations do |user, team| - true # TODO: Add permission check + team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_MANAGE) + end + + can :read_storage_location_containers do |user, team| + team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ) + end + + can :create_storage_location_containers do |user, team| + team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE) + end + + can :manage_storage_location_containers do |user, team| + team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE) end can :create_reports do |user, team| diff --git a/app/services/lists/storage_locations_service.rb b/app/services/lists/storage_locations_service.rb index 9829a243e0..57e4a773ef 100644 --- a/app/services/lists/storage_locations_service.rb +++ b/app/services/lists/storage_locations_service.rb @@ -9,7 +9,7 @@ def initialize(team, params) end def fetch_records - @records = StorageLocation.where(team: @team, parent_id: @parent_id) + @records = @team.storage_locations.where(parent_id: @parent_id) end def filter_records; end diff --git a/app/views/storage_locations/show.html.erb b/app/views/storage_locations/show.html.erb index cab9d1f5a9..b23cda749b 100644 --- a/app/views/storage_locations/show.html.erb +++ b/app/views/storage_locations/show.html.erb @@ -8,9 +8,9 @@

<%= @storage_location.name %>

-
-
- +
+
- <%= javascript_include_tag 'vue_storage_locations_box' %> + <%= javascript_include_tag 'vue_storage_locations_container' %> <% end %> diff --git a/config/initializers/extends/permission_extends.rb b/config/initializers/extends/permission_extends.rb index 12b63cbca0..b7b2843a14 100644 --- a/config/initializers/extends/permission_extends.rb +++ b/config/initializers/extends/permission_extends.rb @@ -13,7 +13,13 @@ module TeamPermissions REPORTS_CREATE LABEL_TEMPLATES_READ LABEL_TEMPLATES_MANAGE - ).each { |permission| const_set(permission, "team_#{permission.underscore}") } + STORAGE_LOCATIONS_CREATE + STORAGE_LOCATIONS_MANAGE + STORAGE_LOCATIONS_READ + STORAGE_LOCATION_CONTAINERS_CREATE + STORAGE_LOCATION_CONTAINERS_MANAGE + STORAGE_LOCATION_CONTAINERS_READ + ).each { |permission| const_set(permission, "team_#{permission.parameterize}") } end module ProtocolPermissions @@ -24,7 +30,7 @@ module ProtocolPermissions MANAGE USERS_MANAGE MANAGE_DRAFT - ).each { |permission| const_set(permission, "protocol_#{permission.underscore}") } + ).each { |permission| const_set(permission, "protocol_#{permission.parameterize}") } end module ReportPermissions @@ -33,7 +39,7 @@ module ReportPermissions READ MANAGE USERS_MANAGE - ).each { |permission| const_set(permission, "report_#{permission.underscore}") } + ).each { |permission| const_set(permission, "report_#{permission.parameterize}") } end module ProjectPermissions @@ -51,7 +57,7 @@ module ProjectPermissions COMMENTS_MANAGE_OWN TAGS_MANAGE EXPERIMENTS_CREATE - ).each { |permission| const_set(permission, "project_#{permission.underscore}") } + ).each { |permission| const_set(permission, "project_#{permission.parameterize}") } end module ExperimentPermissions @@ -65,7 +71,7 @@ module ExperimentPermissions USERS_MANAGE READ_CANVAS ACTIVITIES_READ - ).each { |permission| const_set(permission, "experiment_#{permission.underscore}") } + ).each { |permission| const_set(permission, "experiment_#{permission.parameterize}") } end module MyModulePermissions @@ -107,7 +113,7 @@ module MyModulePermissions USERS_MANAGE DESIGNATED_USERS_MANAGE STOCK_CONSUMPTION_UPDATE - ).each { |permission| const_set(permission, "task_#{permission.underscore}") } + ).each { |permission| const_set(permission, "task_#{permission.parameterize}") } end module RepositoryPermissions @@ -127,7 +133,7 @@ module RepositoryPermissions COLUMNS_DELETE USERS_MANAGE FILTERS_MANAGE - ).each { |permission| const_set(permission, "inventory_#{permission.underscore}") } + ).each { |permission| const_set(permission, "inventory_#{permission.parameterize}") } end module PredefinedRoles @@ -147,6 +153,12 @@ module PredefinedRoles TeamPermissions::REPORTS_CREATE, TeamPermissions::LABEL_TEMPLATES_READ, TeamPermissions::LABEL_TEMPLATES_MANAGE, + TeamPermissions::STORAGE_LOCATIONS_READ, + TeamPermissions::STORAGE_LOCATIONS_CREATE, + TeamPermissions::STORAGE_LOCATIONS_MANAGE, + TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ, + TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE, + TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE, ProtocolPermissions::READ, ProtocolPermissions::READ_ARCHIVED, ProtocolPermissions::MANAGE_DRAFT, @@ -241,6 +253,8 @@ module PredefinedRoles VIEWER_PERMISSIONS = [ TeamPermissions::LABEL_TEMPLATES_READ, + TeamPermissions::STORAGE_LOCATIONS_READ, + TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ, ProtocolPermissions::READ, ProtocolPermissions::READ_ARCHIVED, ReportPermissions::READ, diff --git a/config/locales/en.yml b/config/locales/en.yml index b416848418..831f33f7c5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2671,7 +2671,7 @@ en: errors: my_module_references_missing: 'Task references are not set' storage_locations: - box: 'box' + container: 'box' location: 'location' show: table: @@ -2687,7 +2687,7 @@ en: index: head_title: "Locations" new_location: "New location" - new_box: "New box" + new_container: "New box" duplicate: success_message: "Location was successfully duplicated." toolbar: @@ -2707,15 +2707,15 @@ en: description: "Description" edit_modal: title_create_location: "Create new location" - title_create_box: "Create new box" + title_create_container: "Create new box" title_edit_location: "Edit location" - title_edit_box: "Edit box" + title_edit_container: "Edit box" description_create_location: "Fill in the fields and create a new location." - description_create_box: "Fill in the fields to create a new box. Defining the box dimensions allows you to control the number of available spaces for placing inventory items." + description_create_container: "Fill in the fields to create a new box. Defining the box dimensions allows you to control the number of available spaces for placing inventory items." name_label_location: "Location name" image_label_location: "Image of location" - name_label_box: "Box name" - image_label_box: "Image of box" + name_label_container: "Box name" + image_label_container: "Image of box" drag_and_drop_supporting_text: ".png or .jpg file" description_label: "Description" name_placeholder: "Big freezer" @@ -2726,9 +2726,9 @@ en: no_grid_tooltip: "You can assign unlimited items to the “No-grid” box but they do not have assigned position." success_message: create_location: "Location %{name} was successfully created." - create_box: "Box %{name} was successfully created." + create_container: "Box %{name} was successfully created." edit_location: "Location %{name} was successfully updated." - edit_box: "Box %{name} was successfully updated." + edit_container: "Box %{name} was successfully updated." errors: max_length: "is too long (maximum is %{max_length} characters)" move_modal: diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js index bb9603fe28..6cdd886b80 100644 --- a/config/webpack/webpack.config.js +++ b/config/webpack/webpack.config.js @@ -67,7 +67,7 @@ const entryList = { vue_legacy_repository_menu_dropdown: './app/javascript/packs/vue/legacy/repository_menu_dropdown.js', vue_dashboard_new_task: './app/javascript/packs/vue/dashboard_new_task.js', vue_storage_locations_table: './app/javascript/packs/vue/storage_locations_table.js', - vue_storage_locations_box: './app/javascript/packs/vue/storage_locations_box.js' + vue_storage_locations_container: './app/javascript/packs/vue/storage_locations_container.js' }; // Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949 diff --git a/db/migrate/20240725135302_add_storage_location_permissions.rb b/db/migrate/20240725135302_add_storage_location_permissions.rb new file mode 100644 index 0000000000..5ba79a8dd2 --- /dev/null +++ b/db/migrate/20240725135302_add_storage_location_permissions.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class AddStorageLocationPermissions < ActiveRecord::Migration[7.0] + STORAGE_LOCATIONS_MANAGE_PERMISSION = [ + TeamPermissions::STORAGE_LOCATIONS_CREATE, + TeamPermissions::STORAGE_LOCATIONS_MANAGE, + TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE, + TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE + ].freeze + + STORAGE_LOCATIONS_READ_PERMISSION = [ + TeamPermissions::STORAGE_LOCATIONS_READ, + TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ + ].freeze + + def up + @owner_role = UserRole.find_predefined_owner_role + @normal_user_role = UserRole.find_predefined_normal_user_role + @viewer_user_role = UserRole.find_predefined_viewer_role + + @owner_role.permissions = @owner_role.permissions | STORAGE_LOCATIONS_MANAGE_PERMISSION | + STORAGE_LOCATIONS_READ_PERMISSION + @normal_user_role.permissions = @normal_user_role.permissions | STORAGE_LOCATIONS_MANAGE_PERMISSION | + STORAGE_LOCATIONS_READ_PERMISSION + @viewer_user_role.permissions = @viewer_user_role.permissions | STORAGE_LOCATIONS_READ_PERMISSION + + @owner_role.save(validate: false) + @normal_user_role.save(validate: false) + @viewer_user_role.save(validate: false) + end + + def down + @owner_role = UserRole.find_predefined_owner_role + @normal_user_role = UserRole.find_predefined_normal_user_role + @viewer_user_role = UserRole.find_predefined_viewer_role + + @owner_role.permissions = @owner_role.permissions - STORAGE_LOCATIONS_MANAGE_PERMISSION - + STORAGE_LOCATIONS_READ_PERMISSION + @normal_user_role.permissions = @normal_user_role.permissions - STORAGE_LOCATIONS_MANAGE_PERMISSION - + STORAGE_LOCATIONS_READ_PERMISSION + @viewer_user_role.permissions = @viewer_user_role.permissions - STORAGE_LOCATIONS_READ_PERMISSION + + @owner_role.save(validate: false) + @normal_user_role.save(validate: false) + @viewer_user_role.save(validate: false) + end +end From 2da397cc7636a8d8d51aba196dbe0ab146291bce Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 29 Jul 2024 15:41:40 +0200 Subject: [PATCH 017/249] Add assign/unassign modal and move modal [SCI-10870] --- .gitignore | 4 + Gemfile | 1 + Gemfile.lock | 3 + Rakefile | 2 + app/controllers/repositories_controller.rb | 18 +++- ...age_location_repository_rows_controller.rb | 29 ++++-- .../storage_locations_controller.rb | 18 +++- .../vue/storage_locations/container.vue | 83 +++++++++++++++- app/javascript/vue/storage_locations/grid.vue | 9 +- .../vue/storage_locations/modals/assign.vue | 99 +++++++++++++++++++ .../modals/assign/container_selector.vue | 43 ++++++++ .../modals/assign/position_selector.vue | 77 +++++++++++++++ .../modals/assign/row_selector.vue | 69 +++++++++++++ .../vue/storage_locations/modals/move.vue | 35 +------ .../storage_locations/modals/move_tree.vue | 2 +- .../modals/move_tree_mixin.js | 50 ++++++++++ .../vue/storage_locations/table.vue | 5 +- app/models/storage_location.rb | 18 ++++ ...torage_location_repository_rows_service.rb | 6 +- app/views/storage_locations/index.html.erb | 1 - app/views/storage_locations/show.html.erb | 1 + config/environments/development.rb | 4 + config/initializers/js_routes.rb | 7 ++ config/locales/en.yml | 15 +++ config/routes.rb | 5 +- 25 files changed, 541 insertions(+), 63 deletions(-) create mode 100644 app/javascript/vue/storage_locations/modals/assign.vue create mode 100644 app/javascript/vue/storage_locations/modals/assign/container_selector.vue create mode 100644 app/javascript/vue/storage_locations/modals/assign/position_selector.vue create mode 100644 app/javascript/vue/storage_locations/modals/assign/row_selector.vue create mode 100644 app/javascript/vue/storage_locations/modals/move_tree_mixin.js create mode 100644 config/initializers/js_routes.rb diff --git a/.gitignore b/.gitignore index a16f03f0c5..4e459907b1 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,7 @@ public/marvin4js-license.cxl /app/assets/builds/* !/app/assets/builds/.keep + +# Ignore automatically generated js-routes files. +/app/javascript/routes.js +/app/javascript/routes.d.ts diff --git a/Gemfile b/Gemfile index 31fc95950f..9d1063a5fc 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' diff --git a/Gemfile.lock b/Gemfile.lock index deeb09c01f..481cfbbf60 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -386,6 +386,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) @@ -826,6 +828,7 @@ DEPENDENCIES image_processing img2zpl! jbuilder + js-routes jsbundling-rails json-jwt json_matchers diff --git a/Rakefile b/Rakefile index 9f98587c32..c172d03df4 100644 --- a/Rakefile +++ b/Rakefile @@ -5,3 +5,5 @@ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks Doorkeeper::Rake.load_tasks +# Update js-routes file before javascript build +task 'javascript:build' => 'js:routes:typescript' diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 29fa820c3a..25aff0c0cc 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 @@ def index 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 ab15e6c1db..052585c288 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class StorageLocationRepositoryRowsController < ApplicationController - before_action :load_storage_location_repository_row, only: %i(update destroy) + before_action :load_storage_location_repository_row, only: %i(update destroy move) before_action :load_storage_location - before_action :load_repository_row, only: %i(create update destroy) + 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) @@ -13,7 +13,6 @@ def index ).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 @@ -22,8 +21,7 @@ def update 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 @@ -39,13 +37,30 @@ def create 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: {} diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 5532b7b0d5..e5940e455b 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StorageLocationsController < ApplicationController - before_action :load_storage_location, only: %i(update destroy duplicate move show) + 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) @@ -81,10 +81,20 @@ def move end def tree - records = current_team.storage_locations.where(parent: nil, container: false) + 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: @@ -172,7 +182,9 @@ 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)) + children: storage_locations_recursive_builder( + storage_location.storage_locations.where(container: [false, params[:container] == 'true']) + ) } end end diff --git a/app/javascript/vue/storage_locations/container.vue b/app/javascript/vue/storage_locations/container.vue index 53e5b970f8..9f56d4236b 100644 --- a/app/javascript/vue/storage_locations/container.vue +++ b/app/javascript/vue/storage_locations/container.vue @@ -3,13 +3,13 @@
-
- +
+ + + +
@@ -32,12 +54,16 @@ import axios from '../../packs/custom_axios.js'; import DataTable from '../shared/datatable/table.vue'; import Grid from './grid.vue'; +import AssignModal from './modals/assign.vue'; +import ConfirmationModal from '../shared/confirmation_modal.vue'; export default { name: 'StorageLocationsContainer', components: { DataTable, - Grid + Grid, + AssignModal, + ConfirmationModal }, props: { dataSource: { @@ -52,6 +78,10 @@ export default { type: Boolean, default: false }, + containerId: { + type: Number, + default: null + }, gridSize: Array }, data() { @@ -62,7 +92,14 @@ export default { editStorageLocation: null, objectToMove: null, moveToUrl: null, - assignedItems: [] + assignedItems: [], + openAssignModal: false, + assignToPosition: null, + assignToContainer: null, + rowIdToMove: null, + cellIdToUnassign: null, + assignMode: 'assign', + storageLocationUnassignDescription: '' }; }, computed: { @@ -123,6 +160,44 @@ export default { handleTableReload(items) { this.reloadingTable = false; this.assignedItems = items; + }, + assignRow() { + this.openAssignModal = true; + this.rowIdToMove = null; + this.assignToContainer = this.containerId; + this.assignToPosition = null; + this.cellIdToUnassign = null; + this.assignMode = 'assign'; + }, + assignRowToPosition(position) { + this.openAssignModal = true; + this.rowIdToMove = null; + this.assignToContainer = this.containerId; + this.assignToPosition = position; + this.cellIdToUnassign = null; + this.assignMode = 'assign'; + }, + moveRow(_event, data) { + this.openAssignModal = true; + this.rowIdToMove = data[0].row_id; + this.assignToContainer = null; + this.assignToPosition = null; + this.cellIdToUnassign = data[0].id; + this.assignMode = 'move'; + }, + async unassignRows(event, rows) { + this.storageLocationUnassignDescription = this.i18n.t( + 'storage_locations.show.unassign_modal.description', + { items: rows.length } + ); + const ok = await this.$refs.unassignStorageLocationModal.show(); + if (ok) { + axios.post(event.path).then(() => { + this.reloadingTable = true; + }).catch((error) => { + HelperModule.flashAlertMsg(error.response.data.error, 'danger'); + }); + } } } }; diff --git a/app/javascript/vue/storage_locations/grid.vue b/app/javascript/vue/storage_locations/grid.vue index ded0d878f0..ccc4aaaae8 100644 --- a/app/javascript/vue/storage_locations/grid.vue +++ b/app/javascript/vue/storage_locations/grid.vue @@ -25,9 +25,10 @@ >
{{ rowsList[cell.row] }}{{ columnsList[cell.column] }} @@ -81,6 +82,12 @@ export default { cellIsOccupied(row, column) { return this.assignedItems.some((item) => item.position[0] === row + 1 && item.position[1] === column + 1); }, + assignRow(row, column) { + if (this.cellIsOccupied(row, column)) { + return; + } + this.$emit('assign', [row + 1, column + 1]); + }, handleScroll() { this.$refs.columnsContainer.scrollLeft = this.$refs.cellsContainer.scrollLeft; this.$refs.rowContainer.scrollTop = this.$refs.cellsContainer.scrollTop; diff --git a/app/javascript/vue/storage_locations/modals/assign.vue b/app/javascript/vue/storage_locations/modals/assign.vue new file mode 100644 index 0000000000..9fe5856632 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/assign.vue @@ -0,0 +1,99 @@ + + + diff --git a/app/javascript/vue/storage_locations/modals/assign/container_selector.vue b/app/javascript/vue/storage_locations/modals/assign/container_selector.vue new file mode 100644 index 0000000000..d42b661b30 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/assign/container_selector.vue @@ -0,0 +1,43 @@ + + + diff --git a/app/javascript/vue/storage_locations/modals/assign/position_selector.vue b/app/javascript/vue/storage_locations/modals/assign/position_selector.vue new file mode 100644 index 0000000000..7b84a48cd3 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/assign/position_selector.vue @@ -0,0 +1,77 @@ + + + diff --git a/app/javascript/vue/storage_locations/modals/assign/row_selector.vue b/app/javascript/vue/storage_locations/modals/assign/row_selector.vue new file mode 100644 index 0000000000..24411cd535 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/assign/row_selector.vue @@ -0,0 +1,69 @@ + + + diff --git a/app/javascript/vue/storage_locations/modals/move.vue b/app/javascript/vue/storage_locations/modals/move.vue index 052b8e268f..6f59b98245 100644 --- a/app/javascript/vue/storage_locations/modals/move.vue +++ b/app/javascript/vue/storage_locations/modals/move.vue @@ -51,16 +51,15 @@ import axios from '../../../packs/custom_axios.js'; import modalMixin from '../../shared/modal_mixin'; -import MoveTree from './move_tree.vue'; +import MoveTreeMixin from './move_tree_mixin'; export default { name: 'NewProjectModal', props: { selectedObject: Array, - storageLocationTreeUrl: String, moveToUrl: String }, - mixins: [modalMixin], + mixins: [modalMixin, MoveTreeMixin], data() { return { selectedStorageLocationId: null, @@ -68,37 +67,7 @@ export default { query: '' }; }, - components: { - MoveTree - }, - mounted() { - axios.get(this.storageLocationTreeUrl).then((response) => { - this.storageLocationTree = response.data; - }); - }, - computed: { - filteredStorageLocationTree() { - if (this.query === '') { - return this.storageLocationTree; - } - - return this.storageLocationTree.map((storageLocation) => ( - { - storage_location: storageLocation.storage_location, - children: storageLocation.children.filter((child) => ( - child.storage_location.name.toLowerCase().includes(this.query.toLowerCase()) - )) - } - )).filter((storageLocation) => ( - storageLocation.storage_location.name.toLowerCase().includes(this.query.toLowerCase()) - || storageLocation.children.length > 0 - )); - } - }, methods: { - selectStorageLocation(storageLocationId) { - this.selectedStorageLocationId = storageLocationId; - }, submit() { axios.post(this.moveToUrl, { destination_storage_location_id: this.selectedStorageLocationId || 'root_storage_location' diff --git a/app/javascript/vue/storage_locations/modals/move_tree.vue b/app/javascript/vue/storage_locations/modals/move_tree.vue index 720642392c..ac3cfab893 100644 --- a/app/javascript/vue/storage_locations/modals/move_tree.vue +++ b/app/javascript/vue/storage_locations/modals/move_tree.vue @@ -12,7 +12,7 @@ class="cursor-pointer flex items-center pl-1 flex-1 gap-2 text-sn-blue hover:bg-sn-super-light-grey" :class="{'!bg-sn-super-light-blue': storageLocationTree.storage_location.id == value}"> - +
{{ storageLocationTree.storage_location.name }}
diff --git a/app/javascript/vue/storage_locations/modals/move_tree_mixin.js b/app/javascript/vue/storage_locations/modals/move_tree_mixin.js new file mode 100644 index 0000000000..04ac323456 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/move_tree_mixin.js @@ -0,0 +1,50 @@ +import axios from '../../../packs/custom_axios.js'; +import MoveTree from './move_tree.vue'; +import { + tree_storage_locations_path +} from '../../../routes.js'; + +export default { + mounted() { + axios.get(this.storageLocationTreeUrl).then((response) => { + this.storageLocationTree = response.data; + }); + }, + data() { + return { + selectedStorageLocationId: null, + storageLocationTree: [], + query: '' + }; + }, + computed: { + storageLocationTreeUrl() { + return tree_storage_locations_path({ format: 'json', container: this.container }); + }, + filteredStorageLocationTree() { + if (this.query === '') { + return this.storageLocationTree; + } + + return this.storageLocationTree.map((storageLocation) => ( + { + storage_location: storageLocation.storage_location, + children: storageLocation.children.filter((child) => ( + child.storage_location.name.toLowerCase().includes(this.query.toLowerCase()) + )) + } + )).filter((storageLocation) => ( + storageLocation.storage_location.name.toLowerCase().includes(this.query.toLowerCase()) + || storageLocation.children.length > 0 + )); + } + }, + components: { + MoveTree + }, + methods: { + selectStorageLocation(storageLocationId) { + this.selectedStorageLocationId = storageLocationId; + } + } +}; diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 01044eb5a1..6cbc21ee4e 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -24,7 +24,7 @@ :editStorageLocation="editStorageLocation" />
diff --git a/app/views/storage_locations/show.html.erb b/app/views/storage_locations/show.html.erb index b23cda749b..e0b365612f 100644 --- a/app/views/storage_locations/show.html.erb +++ b/app/views/storage_locations/show.html.erb @@ -15,6 +15,7 @@ data-source="<%= storage_location_storage_location_repository_rows_path(@storage_location) %>" :with-grid="<%= @storage_location.with_grid? %>" :grid-size="<%= @storage_location.grid_size.to_json %>" + :container-id="<%= @storage_location.id %>" />
diff --git a/config/environments/development.rb b/config/environments/development.rb index 692d7ae732..a2b1186afa 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -105,4 +105,8 @@ config.x.new_team_on_signup = false end config.hosts << "dev.scinote.test" + + # Automatically update js-routes file + # when routes.rb is changed + config.middleware.use(JsRoutes::Middleware) end diff --git a/config/initializers/js_routes.rb b/config/initializers/js_routes.rb new file mode 100644 index 0000000000..588f5b292d --- /dev/null +++ b/config/initializers/js_routes.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +JsRoutes.setup do |c| + # Setup your JS module system: + # ESM, CJS, AMD, UMD or nil + # c.module_type = "ESM" +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 831f33f7c5..e077ef09c4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2684,6 +2684,21 @@ en: assign: 'Assign item' unassign: 'Unassign' move: 'Move' + unassign_modal: + title: 'Unassign location' + description: 'Are you sure you want to remove %{items} item(s) from their current storage location?' + button: 'Unassign' + assign_modal: + assign_title: 'Assign position' + move_title: 'Move item' + assign_description: 'Select an item to assign it to a location.' + move_description: 'Select a new location for your item.' + assign_action: 'Assign' + move_action: 'Move' + row: 'Row' + column: 'Column' + inventory: 'Inventory' + item: 'Item' index: head_title: "Locations" new_location: "New location" diff --git a/config/routes.rb b/config/routes.rb index 22aa7ef24d..4606e0706d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -194,6 +194,8 @@ get 'create_modal', to: 'repositories#create_modal', defaults: { format: 'json' } get 'actions_toolbar' + get :list + get :rows_list end member do get :export_empty_repository @@ -815,11 +817,12 @@ member do post :move post :duplicate + post :unassign_rows + get :available_positions end resources :storage_location_repository_rows, only: %i(index create destroy update) do collection do get :actions_toolbar - post :unassign end member do post :move From 3b2748ae71911a04e5a17440ffc8f13464c81d7a Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 30 Jul 2024 09:31:21 +0200 Subject: [PATCH 018/249] Add state saving for result [SCI-10794] --- .../settings/user_settings_controller.rb | 16 +++++++------- app/javascript/vue/results/result.vue | 21 +++++++++++++++++++ app/javascript/vue/results/results.vue | 8 +++++-- app/jobs/cleanup_user_settings_job.rb | 4 +++- app/models/result.rb | 3 +++ app/serializers/result_serializer.rb | 7 ++++++- config/initializers/extends.rb | 1 + 7 files changed, 48 insertions(+), 12 deletions(-) diff --git a/app/controllers/users/settings/user_settings_controller.rb b/app/controllers/users/settings/user_settings_controller.rb index 2b106bc178..04620a4964 100644 --- a/app/controllers/users/settings/user_settings_controller.rb +++ b/app/controllers/users/settings/user_settings_controller.rb @@ -17,8 +17,8 @@ def update next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s) case key.to_s - when 'task_step_states' - update_task_step_states(data) + when 'task_step_states', 'result_states' + update_object_states(data, key.to_s) else current_user.settings[key] = data end @@ -34,18 +34,18 @@ def update private - def update_task_step_states(task_step_states_data) - current_states = current_user.settings.fetch('task_step_states', {}) + def update_object_states(object_states_data, object_state_key) + current_states = current_user.settings.fetch(object_state_key, {}) - task_step_states_data.each do |step_id, collapsed| + object_states_data.each do |object_id, collapsed| if collapsed - current_states[step_id] = true + current_states[object_id] = true else - current_states.delete(step_id) + current_states.delete(object_id) end end - current_user.settings['task_step_states'] = current_states + current_user.settings[object_state_key] = current_states end end end diff --git a/app/javascript/vue/results/result.vue b/app/javascript/vue/results/result.vue index bb2137b43e..cd88163461 100644 --- a/app/javascript/vue/results/result.vue +++ b/app/javascript/vue/results/result.vue @@ -158,6 +158,9 @@ export default { resultToReload: { type: Number, required: false }, activeDragResult: { required: false + }, + userSettingsUrl: { + required: false } }, data() { @@ -215,6 +218,17 @@ export default { deep: true } }, + mounted() { + this.$nextTick(() => { + const resultId = `#resultBody${this.result.id}`; + this.isCollapsed = this.result.attributes.collapsed; + if (this.isCollapsed) { + $(resultId).collapse('hide'); + } else { + $(resultId).collapse('show'); + } + }); + }, computed: { reorderableElements() { return this.orderedElements.map((e) => ({ id: e.id, attributes: e.attributes.orderable })); @@ -321,6 +335,13 @@ export default { toggleCollapsed() { this.isCollapsed = !this.isCollapsed; this.result.attributes.collapsed = this.isCollapsed; + + const settings = { + key: 'result_states', + data: { [this.result.id]: this.isCollapsed } + }; + + axios.put(this.userSettingsUrl, { settings: [settings] }); }, dragEnter(e) { if (!this.urls.upload_attachment_url) return; diff --git a/app/javascript/vue/results/results.vue b/app/javascript/vue/results/results.vue index 23eefb5e8f..3b4bc97dce 100644 --- a/app/javascript/vue/results/results.vue +++ b/app/javascript/vue/results/results.vue @@ -22,6 +22,7 @@ :result="result" :resultToReload="resultToReload" :activeDragResult="activeDragResult" + :userSettingsUrl="userSettingsUrl" @result:elements:loaded="resultToReload = null" @result:move_element="reloadResult" @result:attachments:loaded="resultToReload = null" @@ -64,7 +65,8 @@ export default { canCreate: { type: String, required: true }, archived: { type: String, required: true }, active_url: { type: String, required: true }, - archived_url: { type: String, required: true } + archived_url: { type: String, required: true }, + userSettingsUrl: { type: String, required: false } }, data() { return { @@ -74,10 +76,12 @@ export default { resultToReload: null, nextPageUrl: null, loadingPage: false, - activeDragResult: null + activeDragResult: null, + userSettingsUrl: null }; }, mounted() { + this.userSettingsUrl = document.querySelector('meta[name="user-settings-url"]').getAttribute('content'); window.addEventListener('scroll', this.loadResults, false); window.addEventListener('scroll', this.initStackableHeaders, false); this.nextPageUrl = this.url; diff --git a/app/jobs/cleanup_user_settings_job.rb b/app/jobs/cleanup_user_settings_job.rb index f8bf283fad..b1554c283f 100644 --- a/app/jobs/cleanup_user_settings_job.rb +++ b/app/jobs/cleanup_user_settings_job.rb @@ -4,7 +4,9 @@ class CleanupUserSettingsJob < ApplicationJob queue_as :default def perform(record_type, record_id) - raise ArgumentError, 'Invalid record_type' unless %w(task_step_states results_order).include?(record_type) + unless %w(task_step_states results_order result_states).include?(record_type) + raise ArgumentError, 'Invalid record_type' + end sanitized_record_id = record_id.to_i.to_s raise ArgumentError, 'Invalid record_id' unless sanitized_record_id == record_id.to_s diff --git a/app/models/result.rb b/app/models/result.rb index 767937497e..37ea431140 100644 --- a/app/models/result.rb +++ b/app/models/result.rb @@ -37,6 +37,9 @@ class Result < ApplicationRecord accepts_nested_attributes_for :tables before_save :ensure_default_name + after_discard do + CleanupUserSettingsJob.perform_later('result_states', id) + end def self.search(user, include_archived, diff --git a/app/serializers/result_serializer.rb b/app/serializers/result_serializer.rb index 156cc8b7f8..9988a28a60 100644 --- a/app/serializers/result_serializer.rb +++ b/app/serializers/result_serializer.rb @@ -10,7 +10,12 @@ class ResultSerializer < ActiveModel::Serializer attributes :name, :id, :urls, :updated_at, :created_at_formatted, :updated_at_formatted, :user, :my_module_id, :attachments_manageble, :marvinjs_enabled, :marvinjs_context, :type, :wopi_enabled, :wopi_context, :created_at, :created_by, :archived, :assets_order, - :open_vector_editor_context, :comments_count, :assets_view_mode, :storage_limit + :open_vector_editor_context, :comments_count, :assets_view_mode, :storage_limit, :collapsed + + def collapsed + result_states = current_user.settings.fetch('result_states', {}) + result_states[object.id.to_s] == true + end def marvinjs_enabled MarvinJsService.enabled? diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index a580cc3cfc..684cc56caa 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -675,6 +675,7 @@ class Extends repository_export_file_type navigator_collapsed navigator_width + result_states ).freeze end From 452547564e0c610642ba8c37a691e7e8f4ef3490 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 30 Jul 2024 10:41:06 +0200 Subject: [PATCH 019/249] Enable protocol template renaming [SCI-10851] --- app/controllers/protocols_controller.rb | 1 - app/models/protocol.rb | 8 -------- config/locales/en.yml | 1 - 3 files changed, 10 deletions(-) diff --git a/app/controllers/protocols_controller.rb b/app/controllers/protocols_controller.rb index b0ade7fb48..63a782d595 100644 --- a/app/controllers/protocols_controller.rb +++ b/app/controllers/protocols_controller.rb @@ -909,7 +909,6 @@ def move_protocols(action) end def set_inline_name_editing - return unless @protocol.initial_draft? return unless can_manage_protocol_draft_in_repository?(@protocol) @inline_editable_title_config = { diff --git a/app/models/protocol.rb b/app/models/protocol.rb index a1a69b3d80..f26da6f078 100644 --- a/app/models/protocol.rb +++ b/app/models/protocol.rb @@ -70,12 +70,10 @@ class Protocol < ApplicationRecord with_options if: :in_repository_published_version? do validates :parent, presence: true validate :parent_type_constraint - validate :versions_same_name_constraint end with_options if: :in_repository_draft? do # Only one draft can exist for each protocol validate :ensure_single_draft - validate :versions_same_name_constraint end with_options if: -> { in_repository? && !parent && !archived_changed?(from: false) } do |protocol| # Active protocol must have unique name inside its team @@ -797,12 +795,6 @@ def parent_type_constraint end end - def versions_same_name_constraint - if parent.present? && !parent.name.eql?(name) - errors.add(:base, I18n.t('activerecord.errors.models.protocol.wrong_version_name')) - end - end - def version_number_constraint if Protocol.where(protocol_type: Protocol::REPOSITORY_TYPES) .where.not(id: id) diff --git a/config/locales/en.yml b/config/locales/en.yml index a6e68a6b62..9eb819df6a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -166,7 +166,6 @@ en: protocol: unchangable: "Published protocols can not be changed!" wrong_parent_type: "Protocol can only be linked to published protocol!" - wrong_version_name: "Protocol versions should have same name!" wrong_version_number: "Protocol version number should be sequential!" wrong_parent_draft_number: "Procol can have only 1 draft" attributes: From 1d63c6f81892815e0a50f66c28ad07f34adbfd9c Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 30 Jul 2024 14:36:00 +0200 Subject: [PATCH 020/249] Add storage grid interactions [SCI-10922] --- app/javascript/vue/shared/datatable/table.vue | 5 ++ .../vue/storage_locations/container.vue | 19 +++++++- app/javascript/vue/storage_locations/grid.vue | 48 +++++++++++++++---- ...rage_location_repository_row_serializer.rb | 14 ++++-- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/app/javascript/vue/shared/datatable/table.vue b/app/javascript/vue/shared/datatable/table.vue index 2b91c58a99..5b6afc8c89 100644 --- a/app/javascript/vue/shared/datatable/table.vue +++ b/app/javascript/vue/shared/datatable/table.vue @@ -580,8 +580,11 @@ export default { this.gridApi.forEachNode((node) => { if (this.selectedRows.find((row) => row.id === node.data.id)) { node.setSelected(true); + } else { + node.setSelected(false); } }); + this.$emit('selectionChanged', this.selectedRows); } }, setSelectedRows(e) { @@ -594,6 +597,7 @@ export default { } else { this.selectedRows = this.selectedRows.filter((row) => row.id !== e.data.id); } + this.$emit('selectionChanged', this.selectedRows); }, emitAction(action) { this.$emit(action.name, action, this.selectedRows); @@ -605,6 +609,7 @@ export default { clickCell(e) { if (e.column.colId !== 'rowMenu' && e.column.userProvidedColDef.notSelectable !== true) { e.node.setSelected(true); + this.$emit('selectionChanged', this.selectedRows); } }, applyFilters(filters) { diff --git a/app/javascript/vue/storage_locations/container.vue b/app/javascript/vue/storage_locations/container.vue index 9f56d4236b..2db65f9d70 100644 --- a/app/javascript/vue/storage_locations/container.vue +++ b/app/javascript/vue/storage_locations/container.vue @@ -9,7 +9,13 @@
- +
@@ -93,6 +100,7 @@ export default { objectToMove: null, moveToUrl: null, assignedItems: [], + selectedItems: [], openAssignModal: false, assignToPosition: null, assignToContainer: null, @@ -106,6 +114,7 @@ export default { paginationMode() { return this.withGrid ? 'none' : 'pages'; }, + columnDefs() { const columns = [{ field: 'position', @@ -161,6 +170,14 @@ export default { this.reloadingTable = false; this.assignedItems = items; }, + selectRow(row) { + if (this.$refs.table.selectedRows.includes(row)) { + this.$refs.table.selectedRows = this.$refs.table.selectedRows.filter((r) => r !== row); + } else { + this.$refs.table.selectedRows.push(row); + } + this.$refs.table.restoreSelection(); + }, assignRow() { this.openAssignModal = true; this.rowIdToMove = null; diff --git a/app/javascript/vue/storage_locations/grid.vue b/app/javascript/vue/storage_locations/grid.vue index ccc4aaaae8..7ed5f43caf 100644 --- a/app/javascript/vue/storage_locations/grid.vue +++ b/app/javascript/vue/storage_locations/grid.vue @@ -25,13 +25,21 @@ >
- {{ rowsList[cell.row] }}{{ columnsList[cell.column] }} + +
@@ -51,6 +59,10 @@ export default { assignedItems: { type: Array, default: () => [] + }, + selectedItems: { + type: Array, + default: () => [] } }, mounted() { @@ -79,14 +91,32 @@ export default { } }, methods: { - cellIsOccupied(row, column) { - return this.assignedItems.some((item) => item.position[0] === row + 1 && item.position[1] === column + 1); + cellObject(cell) { + return this.assignedItems.find((item) => item.position[0] === cell.row + 1 && item.position[1] === cell.column + 1); + }, + cellIsOccupied(cell) { + return this.cellObject(cell) && !this.cellObject(cell)?.hidden; + }, + cellIsHidden(cell) { + return this.cellObject(cell)?.hidden; + }, + cellIsSelected(cell) { + return this.selectedItems.some((item) => item.position[0] === cell.row + 1 && item.position[1] === cell.column + 1); }, - assignRow(row, column) { - if (this.cellIsOccupied(row, column)) { + cellIsAvailable(cell) { + return !this.cellIsOccupied(cell) && !this.cellIsHidden(cell); + }, + assignRow(cell) { + if (this.cellIsOccupied(cell)) { + this.$emit('select', this.cellObject(cell)); return; } - this.$emit('assign', [row + 1, column + 1]); + + if (this.cellIsHidden(cell)) { + return; + } + + this.$emit('assign', [cell.row + 1, cell.column + 1]); }, handleScroll() { this.$refs.columnsContainer.scrollLeft = this.$refs.cellsContainer.scrollLeft; diff --git a/app/serializers/lists/storage_location_repository_row_serializer.rb b/app/serializers/lists/storage_location_repository_row_serializer.rb index 628f0e99b4..0ab620d51e 100644 --- a/app/serializers/lists/storage_location_repository_row_serializer.rb +++ b/app/serializers/lists/storage_location_repository_row_serializer.rb @@ -2,18 +2,20 @@ module Lists class StorageLocationRepositoryRowSerializer < ActiveModel::Serializer - attributes :created_by, :created_on, :position, :row_id, :row_name + include Canaid::Helpers::PermissionsHelper + + attributes :created_by, :created_on, :position, :row_id, :row_name, :hidden def row_id - object.repository_row.id + object.repository_row.id unless hidden end def row_name - object.repository_row.name + object.repository_row.name unless hidden end def created_by - object.created_by.full_name + object.created_by.full_name unless hidden end def created_on @@ -23,5 +25,9 @@ def created_on def position object.metadata['position'] end + + def hidden + !can_read_repository?(object.repository_row.repository) + end end end From b774c04f5b30cd14be6010c3916c4a3774d0a7d1 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 31 Jul 2024 11:31:53 +0200 Subject: [PATCH 021/249] Add stock to storage locations [SCI-10921] --- app/javascript/vue/storage_locations/container.vue | 2 +- .../storage_location_repository_row_serializer.rb | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/javascript/vue/storage_locations/container.vue b/app/javascript/vue/storage_locations/container.vue index 2db65f9d70..4578b4fcab 100644 --- a/app/javascript/vue/storage_locations/container.vue +++ b/app/javascript/vue/storage_locations/container.vue @@ -117,7 +117,7 @@ export default { columnDefs() { const columns = [{ - field: 'position', + field: 'position_formatted', headerName: this.i18n.t('storage_locations.show.table.position'), sortable: true, notSelectable: true, diff --git a/app/serializers/lists/storage_location_repository_row_serializer.rb b/app/serializers/lists/storage_location_repository_row_serializer.rb index 0ab620d51e..9c2aeec5c1 100644 --- a/app/serializers/lists/storage_location_repository_row_serializer.rb +++ b/app/serializers/lists/storage_location_repository_row_serializer.rb @@ -4,7 +4,7 @@ module Lists class StorageLocationRepositoryRowSerializer < ActiveModel::Serializer include Canaid::Helpers::PermissionsHelper - attributes :created_by, :created_on, :position, :row_id, :row_name, :hidden + attributes :created_by, :created_on, :position, :row_id, :row_name, :hidden, :position_formatted, :stock def row_id object.repository_row.id unless hidden @@ -19,13 +19,23 @@ def created_by end def created_on - I18n.l(object.created_at, format: :full) + I18n.l(object.created_at, format: :full) unless hidden end def position object.metadata['position'] end + def position_formatted + "#{('A'..'Z').to_a[position[0] - 1]}#{position[1]}" if position + end + + def stock + if object.repository_row.repository.has_stock_management? && !hidden + object.repository_row.repository_cells.find_by(value_type: 'RepositoryStockValue')&.value&.formatted + end + end + def hidden !can_read_repository?(object.repository_row.repository) end From 124810e07e2b8264dd0b98880cca6eaac23554b6 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 1 Aug 2024 15:07:43 +0200 Subject: [PATCH 022/249] Add locations to item card [SCI-10923] --- .../RepositoryItemSidebar.vue | 17 ++++++ .../vue/repository_item_sidebar/locations.vue | 55 +++++++++++++++++++ app/models/repository_row.rb | 10 ++++ app/views/repository_rows/show.json.jbuilder | 2 + config/locales/en.yml | 5 ++ 5 files changed, 89 insertions(+) create mode 100644 app/javascript/vue/repository_item_sidebar/locations.vue diff --git a/app/javascript/vue/repository_item_sidebar/RepositoryItemSidebar.vue b/app/javascript/vue/repository_item_sidebar/RepositoryItemSidebar.vue index 0be9a0740b..776f9ab01f 100644 --- a/app/javascript/vue/repository_item_sidebar/RepositoryItemSidebar.vue +++ b/app/javascript/vue/repository_item_sidebar/RepositoryItemSidebar.vue @@ -312,6 +312,11 @@ +
+ +
+ +
@@ -367,6 +372,7 @@ import ScrollSpy from './repository_values/ScrollSpy.vue'; import CustomColumns from './customColumns.vue'; import RepositoryItemSidebarTitle from './Title.vue'; import UnlinkModal from './unlink_modal.vue'; +import Locations from './locations.vue'; import axios from '../../packs/custom_axios.js'; const items = [ @@ -405,6 +411,14 @@ const items = [ { id: 'highlight-item-5', textId: 'text-item-5', + labelAlias: 'locations_label', + label: 'locations-label', + sectionId: 'locations-section', + showInSnapshot: false + }, + { + id: 'highlight-item-6', + textId: 'text-item-6', labelAlias: 'QR_label', label: 'QR-label', sectionId: 'qr-section', @@ -416,6 +430,7 @@ export default { name: 'RepositoryItemSidebar', components: { CustomColumns, + Locations, 'repository-item-sidebar-title': RepositoryItemSidebarTitle, 'inline-edit': InlineEdit, 'scroll-spy': ScrollSpy, @@ -433,6 +448,7 @@ export default { repository: null, defaultColumns: null, customColumns: null, + repositoryRow: null, parentsCount: 0, childrenCount: 0, parents: null, @@ -591,6 +607,7 @@ export default { { params: { my_module_id: this.myModuleId } } ).then((response) => { const result = response.data; + this.repositoryRow = result; this.repositoryRowId = result.id; this.repository = result.repository; this.optionsPath = result.options_path; diff --git a/app/javascript/vue/repository_item_sidebar/locations.vue b/app/javascript/vue/repository_item_sidebar/locations.vue new file mode 100644 index 0000000000..fa6dcc9c99 --- /dev/null +++ b/app/javascript/vue/repository_item_sidebar/locations.vue @@ -0,0 +1,55 @@ + + + diff --git a/app/models/repository_row.rb b/app/models/repository_row.rb index 0b03595fc5..e20b3c1471 100644 --- a/app/models/repository_row.rb +++ b/app/models/repository_row.rb @@ -175,6 +175,16 @@ def row_archived? self[:archived] end + def grouped_storage_locations + storage_location_repository_rows.joins(:storage_location).group(:storage_location_id).select( + "storage_location_id as id, + MAX(storage_locations.name) as name, + jsonb_agg(jsonb_build_object( + 'id', storage_location_repository_rows.id, 'metadata', + storage_location_repository_rows.metadata) + ) as positions").as_json + end + def archived row_archived? || repository&.archived? end diff --git a/app/views/repository_rows/show.json.jbuilder b/app/views/repository_rows/show.json.jbuilder index 3526eb6203..61c1d8f6da 100644 --- a/app/views/repository_rows/show.json.jbuilder +++ b/app/views/repository_rows/show.json.jbuilder @@ -37,6 +37,8 @@ json.actions do end end +json.locations @repository_row.grouped_storage_locations + json.default_columns do json.name @repository_row.name json.code @repository_row.code diff --git a/config/locales/en.yml b/config/locales/en.yml index b88f823d16..b7d90ed6ef 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2616,7 +2616,12 @@ en: custom_columns_label: 'Custom columns' relationships_label: 'Relationships' assigned_label: 'Assigned' + locations_label: 'Locations' QR_label: 'QR' + locations: + title: 'Locations (%{count})' + container: 'Box' + assign: 'Assign new location' repository_stock_values: manage_modal: title: "Stock %{item}" From 351aed95207c532a9643ff04ed45152eb93e9118 Mon Sep 17 00:00:00 2001 From: Andrej Date: Thu, 1 Aug 2024 16:42:33 +0200 Subject: [PATCH 023/249] Fix searching through tree for location and folders [SCI-10816] --- app/javascript/vue/projects/modals/move.vue | 21 ++++++------ .../modals/assign/container_selector.vue | 2 +- .../vue/storage_locations/modals/move.vue | 4 +-- .../storage_locations/modals/move_tree.vue | 6 ++-- .../modals/move_tree_mixin.js | 34 +++++++++---------- 5 files changed, 33 insertions(+), 34 deletions(-) diff --git a/app/javascript/vue/projects/modals/move.vue b/app/javascript/vue/projects/modals/move.vue index 08eaa66ab1..0b92d73a91 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/storage_locations/modals/assign/container_selector.vue b/app/javascript/vue/storage_locations/modals/assign/container_selector.vue index d42b661b30..c5bd98e2b3 100644 --- a/app/javascript/vue/storage_locations/modals/assign/container_selector.vue +++ b/app/javascript/vue/storage_locations/modals/assign/container_selector.vue @@ -18,7 +18,7 @@ {{ i18n.t('storage_locations.index.move_modal.search_header') }} - + diff --git a/app/javascript/vue/storage_locations/modals/move.vue b/app/javascript/vue/storage_locations/modals/move.vue index 6f59b98245..0066f69efc 100644 --- a/app/javascript/vue/storage_locations/modals/move.vue +++ b/app/javascript/vue/storage_locations/modals/move.vue @@ -31,7 +31,7 @@ {{ i18n.t('storage_locations.index.move_modal.search_header') }} - + {{ this.errors.name }} -
+
- +
{{ i18n.t('storage_locations.index.edit_modal.no_grid') }} @@ -40,16 +40,16 @@
- +
{{ i18n.t('storage_locations.index.edit_modal.grid') }}
- +
- +
@@ -134,6 +134,15 @@ export default { mode() { return this.editStorageLocation ? 'edit' : 'create'; }, + canChangeGrid() { + return !this.object.code || this.object.is_empty; + }, + warningBoxNotEmpty() { + if (this.canChangeGrid) { + return ''; + } + return this.i18n.t('storage_locations.index.edit_modal.warning_box_not_empty'); + }, validObject() { this.errors = {}; diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index aa802c553d..d7680763a5 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -53,6 +53,10 @@ def root_storage_location storage_location end + def empty? + storage_location_repository_rows.count.zero? + end + def duplicate! ActiveRecord::Base.transaction do new_storage_location = dup diff --git a/app/serializers/lists/storage_location_serializer.rb b/app/serializers/lists/storage_location_serializer.rb index 36af6982e7..e6e5040a14 100644 --- a/app/serializers/lists/storage_location_serializer.rb +++ b/app/serializers/lists/storage_location_serializer.rb @@ -6,12 +6,16 @@ class StorageLocationSerializer < ActiveModel::Serializer include ShareableSerializer attributes :id, :code, :name, :container, :description, :owned_by, :created_by, - :created_on, :urls, :metadata, :file_name, :sub_location_count + :created_on, :urls, :metadata, :file_name, :sub_location_count, :is_empty def owned_by object.team.name end + def is_empty + object.empty? + end + def metadata { display_type: object.metadata['display_type'], diff --git a/app/services/model_exporters/experiment_exporter.rb b/app/services/model_exporters/experiment_exporter.rb index 67167e78c8..c82a0192f6 100644 --- a/app/services/model_exporters/experiment_exporter.rb +++ b/app/services/model_exporters/experiment_exporter.rb @@ -93,7 +93,7 @@ def result_orderable_element(element) element_json = element.as_json case element.orderable_type when 'ResultText' - element_json['step_text'] = element.orderable.as_json + element_json['result_text'] = element.orderable.as_json when 'ResultTable' element_json['table'] = table(element.orderable.table) end diff --git a/app/services/model_exporters/team_exporter.rb b/app/services/model_exporters/team_exporter.rb index 31a75fd489..0f992a497c 100644 --- a/app/services/model_exporters/team_exporter.rb +++ b/app/services/model_exporters/team_exporter.rb @@ -47,10 +47,6 @@ def team(team) user_assignments: team.user_assignments.map do |ua| user_assignment(ua) end, - notifications: Notification - .includes(:user_notifications) - .where('user_notifications.user_id': team.users) - .map { |n| notification(n) }, repositories: team.repositories.map { |r| repository(r) }, tiny_mce_assets: team.tiny_mce_assets.map { |tma| tiny_mce_asset_data(tma) }, protocols: team.protocols.where(my_module: nil).map do |pr| @@ -68,15 +64,6 @@ def team(team) } end - def notification(notification) - notification_json = notification.as_json - notification_json['type_of'] = Extends::NOTIFICATIONS_TYPES - .key(notification - .read_attribute('type_of')) - .to_s - notification_json - end - def label_templates(templates) templates.where.not(type: 'FluicsLabelTemplate').map do |template| template_json = template.as_json @@ -97,7 +84,6 @@ def user(user) copy_files([user], :avatar, File.join(@dir_to_export, 'avatars')) { user: user_json, - user_notifications: user.user_notifications, user_identities: user.user_identities, repository_table_states: user.repository_table_states.where(repository: @team.repositories) diff --git a/app/services/team_importer.rb b/app/services/team_importer.rb index 42a1758810..e600b451ef 100644 --- a/app/services/team_importer.rb +++ b/app/services/team_importer.rb @@ -71,7 +71,6 @@ def import_from_dir(import_dir) # Find new id of the first admin in the team @admin_id = @user_mappings[team_json['default_admin_id']] - create_notifications(team_json['notifications']) create_protocol_keywords(team_json['protocol_keywords'], team) create_protocols(team_json['protocols'], nil, team) create_project_folders(team_json['project_folders'], team) @@ -83,17 +82,6 @@ def import_from_dir(import_dir) # Second run, we needed it because of some models should be created team_json['users'].each do |user_json| - user_json['user_notifications'].each do |user_notification_json| - user_notification = UserNotification.new(user_notification_json) - user_notification.id = nil - user_notification.user_id = find_user(user_notification.user_id) - user_notification.notification_id = - @notification_mappings[user_notification.notification_id] - next if user_notification.notification_id.blank? - - user_notification.save! - end - user_json['repository_table_states'].each do |rep_tbl_state_json| rep_tbl_state = RepositoryTableState.new(rep_tbl_state_json) rep_tbl_state.id = nil @@ -423,21 +411,6 @@ def create_users(users_json, team) end end - def create_notifications(notifications_json) - puts 'Creating notifications...' - notifications_json.each do |notification_json| - notification = Notification.new(notification_json) - next if notification.type_of.blank? - - orig_notification_id = notification.id - notification.id = nil - notification.generator_user_id = find_user(notification.generator_user_id) - notification.save! - @notification_mappings[orig_notification_id] = notification.id - @notification_counter += 1 - end - end - def create_repositories(repositories_json, team, snapshots = false) puts 'Creating repositories...' repositories_json.each do |repository_json| @@ -756,7 +729,7 @@ def create_my_modules(my_modules_json, experiment, user_id = nil) def create_protocols(protocols_json, my_module = nil, team = nil, user_id = nil) - sorted_protocols = protocols_json.sort_by { |p| p['id'] } + sorted_protocols = protocols_json.sort_by { |p| p['protocol']['id'] } puts 'Creating protocols...' sorted_protocols.each do |protocol_json| diff --git a/config/locales/en.yml b/config/locales/en.yml index 84b9f22a6f..8d32abbc85 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2745,6 +2745,7 @@ en: name_label_container: "Box name" image_label_container: "Image of box" drag_and_drop_supporting_text: ".png or .jpg file" + warning_box_not_empty: "Box dimensions can be updated only when the box is empty." description_label: "Description" name_placeholder: "Big freezer" description_placeholder: "Keep everyone on the same page. You can also use smart annotations." From c4c14553ff5558d833020425d944f087648ea8c9 Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Wed, 11 Sep 2024 14:36:14 +0200 Subject: [PATCH 072/249] Implement storage location activities [SCI-10925] --- ...age_location_repository_rows_controller.rb | 75 +++++++---- .../storage_locations_controller.rb | 126 ++++++++++++------ .../team_shared_objects_controller.rb | 2 +- app/helpers/global_activities_helper.rb | 2 + app/models/activity.rb | 3 + app/models/storage_location_repository_row.rb | 13 +- .../activities/create_activity_service.rb | 6 +- .../references/_storage_location.html.erb | 15 +++ config/initializers/extends.rb | 29 +++- config/locales/global_activities/en.yml | 37 +++++ 10 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 app/views/global_activities/references/_storage_location.html.erb diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb index 2907cd8969..c9ee5647b6 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -17,30 +17,36 @@ def index meta: (pagination_dict(storage_location_repository_row) unless @storage_location.with_grid?) end - def update - @storage_location_repository_row.update(storage_location_repository_row_params) + def create + ActiveRecord::Base.transaction do + @storage_location_repository_row = StorageLocationRepositoryRow.new( + repository_row: @repository_row, + storage_location: @storage_location, + metadata: storage_location_repository_row_params[:metadata] || {}, + created_by: current_user + ) - if @storage_location_repository_row.save - render json: @storage_location_repository_row, - serializer: Lists::StorageLocationRepositoryRowSerializer - else - render json: @storage_location_repository_row.errors, status: :unprocessable_entity + if @storage_location_repository_row.save + log_activity(:storage_location_repository_row_created) + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer + else + render json: @storage_location_repository_row.errors, status: :unprocessable_entity + end end end - def create - @storage_location_repository_row = StorageLocationRepositoryRow.new( - repository_row: @repository_row, - storage_location: @storage_location, - metadata: storage_location_repository_row_params[:metadata] || {}, - created_by: current_user - ) - - if @storage_location_repository_row.save - render json: @storage_location_repository_row, - serializer: Lists::StorageLocationRepositoryRowSerializer - else - render json: @storage_location_repository_row.errors, status: :unprocessable_entity + def update + ActiveRecord::Base.transaction do + @storage_location_repository_row.update(storage_location_repository_row_params) + + if @storage_location_repository_row.save + log_activity(:storage_location_repository_row_moved) + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer + else + render json: @storage_location_repository_row.errors, status: :unprocessable_entity + end end end @@ -53,7 +59,7 @@ def move metadata: storage_location_repository_row_params[:metadata] || {}, created_by: current_user ) - + log_activity(:storage_location_repository_row_moved) render json: @storage_location_repository_row, serializer: Lists::StorageLocationRepositoryRowSerializer rescue ActiveRecord::RecordInvalid => e @@ -63,10 +69,13 @@ def move end def destroy - if @storage_location_repository_row.discard - render json: {} - else - render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity + ActiveRecord::Base.transaction do + if @storage_location_repository_row.discard + log_activity(:storage_location_repository_row_deleted) + render json: {} + else + render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity + end end end @@ -74,7 +83,7 @@ def actions_toolbar render json: { actions: Toolbars::StorageLocationRepositoryRowsService.new( current_user, - items_ids: JSON.parse(params[:items]).map { |i| i['id'] } + items_ids: JSON.parse(params[:items]).pluck('id') ).actions } end @@ -116,4 +125,18 @@ def check_read_permissions def check_manage_permissions render_403 unless can_manage_storage_location?(@storage_location) end + + def log_activity(type_of, message_items = {}) + Activities::CreateActivityService + .call(activity_type: type_of, + owner: current_user, + team: @storage_location.team, + subject: @storage_location_repository_row.repository_row, + message_items: { + storage_location: @storage_location_repository_row.storage_location_id, + repository_row: @storage_location_repository_row.repository_row_id, + position: @storage_location_repository_row.human_readable_position, + user: current_user.id + }.merge(message_items)) + end end diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index efab4b0c31..1cc42bc501 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -21,60 +21,78 @@ def index def show; end - def update - @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: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity - end - end - def create - @storage_location = StorageLocation.new( - storage_location_params.merge({ created_by: current_user }) - ) + ActiveRecord::Base.transaction do + @storage_location = StorageLocation.new( + storage_location_params.merge({ created_by: current_user }) + ) - @storage_location.team = @storage_location.root_storage_location.team || current_team + @storage_location.team = @storage_location.root_storage_location.team || current_team - @storage_location.image.attach(params[:signed_blob_id]) if 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: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + if @storage_location.save + log_activity('storage_location_created') + render json: @storage_location, serializer: Lists::StorageLocationSerializer + else + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + end + end + end + + def update + ActiveRecord::Base.transaction do + @storage_location.image.purge if params[:file_name].blank? + @storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id] + @storage_location.update(storage_location_params) + + if @storage_location.save + log_activity('storage_location_edited') + render json: @storage_location, serializer: Lists::StorageLocationSerializer + else + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + end end end def destroy - if @storage_location.discard - render json: {} - else - render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + ActiveRecord::Base.transaction do + if @storage_location.discard + log_activity('storage_location_deleted') + render json: {} + else + render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity + end end end def duplicate - 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 + ActiveRecord::Base.transaction do + new_storage_location = @storage_location.duplicate! + if new_storage_location + @storage_location = new_storage_location + log_activity('storage_location_created') + render json: @storage_location, serializer: Lists::StorageLocationSerializer + else + render json: { errors: :failed }, status: :unprocessable_entity + end end end def move - 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 + ActiveRecord::Base.transaction do + original_storage_location = @storage_location.parent + destination_storage_location = + if move_params[:destination_storage_location_id] == 'root_storage_location' + nil + else + current_team.storage_locations.find(move_params[:destination_storage_location_id]) + end - @storage_location.update!(parent: storage_location_destination) + @storage_location.update!(parent: destination_storage_location) + + log_activity('storage_location_moved', { storage_location_original: original_storage_location.id, storage_location_destination: destination_storage_location.id }) + end render json: { message: I18n.t('storage_locations.index.move_modal.success_flash') } rescue StandardError => e @@ -93,7 +111,11 @@ def available_positions end def unassign_rows - @storage_location.storage_location_repository_rows.where(id: params[:ids]).discard_all + ActiveRecord::Base.transaction do + @storage_location_repository_rows = @storage_location.storage_location_repository_rows.where(id: params[:ids]) + @storage_location_repository_rows.each(&:discard) + log_unassign_activities + end render json: { status: :ok } end @@ -206,4 +228,32 @@ def storage_locations_recursive_builder(storage_locations) } end end + + def log_activity(type_of, message_items = {}) + Activities::CreateActivityService + .call(activity_type: "#{'container_' if @storage_location.container}#{type_of}", + owner: current_user, + team: @storage_location.team, + subject: @storage_location, + message_items: { + storage_location: @storage_location.id, + user: current_user.id + }.merge(message_items)) + end + + def log_unassign_activities + @storage_location_repository_rows.each do |storage_location_repository_row| + Activities::CreateActivityService + .call(activity_type: :storage_location_repository_row_deleted, + owner: current_user, + team: @storage_location.team, + subject: storage_location_repository_row.repository_row, + message_items: { + storage_location: storage_location_repository_row.storage_location_id, + repository_row: storage_location_repository_row.repository_row_id, + position: storage_location_repository_row.human_readable_position, + user: current_user.id + }) + end + end end diff --git a/app/controllers/team_shared_objects_controller.rb b/app/controllers/team_shared_objects_controller.rb index 5ef3e50faa..97eb725ab4 100644 --- a/app/controllers/team_shared_objects_controller.rb +++ b/app/controllers/team_shared_objects_controller.rb @@ -71,7 +71,7 @@ def share_all_params } end - def log_activity(type_of, team_shared_object) + def log_activity(type_of) # log activity logic end end diff --git a/app/helpers/global_activities_helper.rb b/app/helpers/global_activities_helper.rb index 608d3d6de6..a2d1a9d79a 100644 --- a/app/helpers/global_activities_helper.rb +++ b/app/helpers/global_activities_helper.rb @@ -108,6 +108,8 @@ def generate_link(message_item, activity) else project_folder_path(obj, team: obj.team.id) end + when StorageLocation + path = storage_location_path(obj) else return current_value end diff --git a/app/models/activity.rb b/app/models/activity.rb index 35aaabd235..13ad8b80a4 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -187,6 +187,9 @@ def generate_breadcrumb(subject) when Asset breadcrumbs[:asset] = subject.blob.filename.to_s generate_breadcrumb(subject.result || subject.step || subject.repository_cell.repository_row.repository) + when StorageLocation + breadcrumbs[:storage_location] = subject.name + generate_breadcrumb(subject.team) end end diff --git a/app/models/storage_location_repository_row.rb b/app/models/storage_location_repository_row.rb index f3bdb1d1f4..126dfad0ba 100644 --- a/app/models/storage_location_repository_row.rb +++ b/app/models/storage_location_repository_row.rb @@ -14,10 +14,17 @@ class StorageLocationRepositoryRow < ApplicationRecord validate :ensure_uniq_position end + def human_readable_position + return unless metadata['position'] + + column_letter = ('A'..'Z').to_a[metadata['position'][0] - 1] + row_number = metadata['position'][1] + + "#{column_letter}#{row_number}" + end + def position_must_be_present - if metadata['position'].blank? - errors.add(:base, I18n.t('activerecord.errors.models.storage_location.missing_position')) - end + errors.add(:base, I18n.t('activerecord.errors.models.storage_location.missing_position')) if metadata['position'].blank? end def ensure_uniq_position diff --git a/app/services/activities/create_activity_service.rb b/app/services/activities/create_activity_service.rb index b695337567..ce3671e803 100644 --- a/app/services/activities/create_activity_service.rb +++ b/app/services/activities/create_activity_service.rb @@ -61,7 +61,11 @@ def enrich_message_items end if id - obj = const.find id + obj = if const.respond_to?(:with_discarded) + const.with_discarded.find id + else + const.find id + end @activity.message_items[k] = { type: const.to_s, value: obj.public_send(getter_method).to_s, id: id } @activity.message_items[k][:value_for] = getter_method @activity.message_items[k][:value_type] = value_type unless value_type.nil? diff --git a/app/views/global_activities/references/_storage_location.html.erb b/app/views/global_activities/references/_storage_location.html.erb new file mode 100644 index 0000000000..ed8a8787ee --- /dev/null +++ b/app/views/global_activities/references/_storage_location.html.erb @@ -0,0 +1,15 @@ +<%= render partial: "global_activities/references/team", + locals: { team: team, subject: team, breadcrumbs: breadcrumbs, values: values, type_of: type_of } %> +
+ + <% if subject %> + <%= route_to_other_team(storage_location_path(subject.id, team: subject.team.id), + team, + subject.name&.truncate(Constants::NAME_TRUNCATION_LENGTH), + title: subject.name) %> + <% else %> + + <%= breadcrumbs['storage_location']&.truncate(Constants::NAME_TRUNCATION_LENGTH) %> + + <% end %> +
diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index 54f73fee56..995f9fce4e 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -188,7 +188,7 @@ class Extends ACTIVITY_SUBJECT_TYPES = %w( Team RepositoryBase Project Experiment MyModule Result Protocol Report RepositoryRow - ProjectFolder Asset Step LabelTemplate + ProjectFolder Asset Step LabelTemplate StorageLocation StorageLocationRepositoryRow ).freeze SEARCHABLE_ACTIVITY_SUBJECT_TYPES = %w( @@ -205,7 +205,8 @@ class Extends my_module: %i(results protocols), result: [:assets], protocol: [:steps], - step: [:assets] + step: [:assets], + storage_location: [:storage_location_repository_rows] } ACTIVITY_MESSAGE_ITEMS_TYPES = @@ -495,7 +496,24 @@ class Extends task_step_asset_renamed: 305, result_asset_renamed: 306, protocol_step_asset_renamed: 307, - inventory_items_added_or_updated_with_import: 308 + inventory_items_added_or_updated_with_import: 308, + storage_location_created: 309, + storage_location_deleted: 310, + storage_location_edited: 311, + storage_location_moved: 312, + storage_location_shared: 313, + storage_location_unshared: 314, + storage_location_sharing_updated: 315, + container_storage_location_created: 316, + container_storage_location_deleted: 317, + container_storage_location_edited: 318, + container_storage_location_moved: 319, + container_storage_location_shared: 320, + container_storage_location_unshared: 321, + container_storage_location_sharing_updated: 322, + storage_location_repository_row_created: 323, + storage_location_repository_row_deleted: 324, + storage_location_repository_row_moved: 325 } ACTIVITY_GROUPS = { @@ -515,7 +533,10 @@ class Extends 190, 191, *204..215, 220, 223, 227, 228, 229, *230..235, *237..240, *253..256, *279..283, 300, 304, 307], team: [92, 94, 93, 97, 104, 244, 245], - label_templates: [*216..219] + label_templates: [*216..219], + storage_locations: [*309..315], + container_storage_location: [*316..322], + storage_location_repository_rows: [*323..325] } TOP_LEVEL_ASSIGNABLES = %w(Project Team Protocol Repository).freeze diff --git a/config/locales/global_activities/en.yml b/config/locales/global_activities/en.yml index f6b52f7728..b0a85b35f5 100644 --- a/config/locales/global_activities/en.yml +++ b/config/locales/global_activities/en.yml @@ -322,6 +322,23 @@ en: protocol_step_asset_renamed_html: "%{user} renamed file %{old_name} to %{new_name} on protocol’s step %{step} in Protocol repository." result_asset_renamed_html: "%{user} renamed file %{old_name} to %{new_name} on result %{result} on task %{my_module}." item_added_with_import_html: "%{user} edited %{num_of_items} inventory item(s) in %{repository}." + storage_location_created_html: "%{user} created location %{storage_location}." + storage_location_deleted_html: "%{user} deleted location %{storage_location}." + storage_location_edited_html: "%{user} edited location %{storage_location}." + storage_location_moved_html: "%{user} moved location %{storage_location} from %{storage_location_original} to %{storage_location_destination}." + storage_location_shared_html: "%{user} shared location %{storage_location} with team %{team} with %{permission_level} permission." + storage_location_unshared_html: "%{user} unshared location %{storage_location} with team %{team}." + storage_location_sharing_updated_html: "%{user} changed permission of shared location %{storage_location} with team %{team} to %{permission_level}." + container_storage_location_created_html: "%{user} created box %{storage_location}." + container_storage_location_deleted_html: "%{user} deleted box %{storage_location}." + container_storage_location_edited_html: "%{user} edited box %{storage_location}." + container_storage_location_moved_html: "%{user} moved box %{storage_location} from %{storage_location_original} to %{storage_location_destination}." + container_storage_location_shared_html: "%{user} shared box %{storage_location} with team %{team} with %{permission_level} permission." + container_storage_location_unshared_html: "%{user} unshared box %{storage_location} with team %{team}." + container_storage_location_sharing_updated_html: "%{user} changed permission of shared box %{storage_location} with team %{team} to %{permission_level}." + storage_location_repository_row_created_html: "%{user} assigned %{repository_row} to box %{storage_location} %{position}." + storage_location_repository_row_deleted_html: "%{user} unassigned %{repository_row} from box %{storage_location} %{position}." + storage_location_repository_row_moved_html: "%{user} moved item %{repository_row} from box %{storage_location_original} %{positions} to box %{storage_location_destination} %{positions}." activity_name: create_project: "Project created" rename_project: "Project renamed" @@ -601,6 +618,23 @@ en: task_step_file_duplicated: "File attachment on Task step duplicated" result_file_duplicated: "File attachment on Task result duplicated" protocol_step_file_duplicated: "File attachment on Protocol step duplicated" + storage_location_created: "Location created" + storage_location_deleted: "Location deleted" + storage_location_edited: "Location edited" + storage_location_moved: "Location moved" + storage_location_shared: "Location shared" + storage_location_unshared: "Location unshared" + storage_location_sharing_updated: "Location sharing permission updated" + container_storage_location_created: "Box created" + container_storage_location_deleted: "Box deleted" + container_storage_location_edited: "Box edited" + container_storage_location_moved: "Box moved" + container_storage_location_shared: "Box shared" + container_storage_location_unshared: "Box unshared" + container_storage_location_sharing_updated: "Box sharing permission updated" + storage_location_repository_row_created: "Inventory item location assigned" + storage_location_repository_row_deleted: "Inventory item location unassigned" + storage_location_repository_row_moved: "Inventory item location moved" activity_group: projects: "Projects" task_results: "Task results" @@ -614,6 +648,8 @@ en: team: "Team" exports: "Exports" label_templates: "Label templates" + storage_locations: "Locations" + container_storage_locations: "Boxes" subject_name: repository: "Inventory" project: "Project" @@ -623,3 +659,4 @@ en: protocol: "Protocol" step: "Step" report: "Report" + storage_location: "Location" From 326a0c1ec4076f4245cd13c58c343a6daed8aab7 Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Wed, 11 Sep 2024 16:11:11 +0200 Subject: [PATCH 073/249] (Re)implement team sharing activities [SCI-10925] --- .../team_shared_objects_controller.rb | 104 ++++++++++++++++-- ...key_constraint_from_team_shared_objects.rb | 7 ++ 2 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20240911133615_remove_foreign_key_constraint_from_team_shared_objects.rb diff --git a/app/controllers/team_shared_objects_controller.rb b/app/controllers/team_shared_objects_controller.rb index 97eb725ab4..4914b76146 100644 --- a/app/controllers/team_shared_objects_controller.rb +++ b/app/controllers/team_shared_objects_controller.rb @@ -6,25 +6,54 @@ class TeamSharedObjectsController < ApplicationController def update ActiveRecord::Base.transaction do + @activities_to_log = [] + # Global share - if params[:select_all_teams] - @model.update!(permission_level: params[:select_all_write_permission] ? :shared_write : :shared_read) - @model.team_shared_objects.each(&:destroy!) - next + if @model.globally_shareable? + permission_level = + if params[:select_all_teams] + params[:select_all_write_permission] ? :shared_write : :shared_read + else + :not_shared + end + + @model.permission_level = permission_level + + if @model.permission_level_changed? + @model.save! + @model.team_shared_objects.each(&:destroy!) unless permission_level == :not_shared + case @model + when Repository + setup_repository_global_share_activity + end + + log_activities and next + end end # Share to specific teams params[:team_share_params].each do |t| @model.update!(permission_level: :not_shared) if @model.globally_shareable? - @model.team_shared_objects.find_or_initialize_by(team_id: t['id']).update!( + + team_shared_object = @model.team_shared_objects.find_or_initialize_by(team_id: t['id']) + + new_record = team_shared_object.new_record? + team_shared_object.update!( permission_level: t['private_shared_with_write'] ? :shared_write : :shared_read ) + + setup_team_share_activity(team_shared_object, new_record) if team_shared_object.saved_changes? end # Unshare @model.team_shared_objects.where.not( team_id: params[:team_share_params].filter { |t| t['private_shared_with'] }.pluck('id') - ).each(&:destroy!) + ).each do |team_shared_object| + team_shared_object.destroy! + setup_team_share_activity(team_shared_object, false) + end + + log_activities end end @@ -71,7 +100,66 @@ def share_all_params } end - def log_activity(type_of) - # log activity logic + def setup_team_share_activity(team_shared_object, new_record) + type = + case @model + when Repository + if team_shared_object.destroyed? + :unshare_inventory + elsif new_record + :share_inventory + else + :update_share_inventory + end + when StorageLocation + if team_shared_object.destroyed? + "#{'container_' if @model.container?}storage_location_unshared" + elsif new_record + "#{'container_' if @model.container?}storage_location_shared" + else + "#{'container_' if @model.container?}storage_location_sharing_updated" + end + end + + @activities_to_log << { + type: type, + message_items: { + @model.model_name.param_key.to_sym => team_shared_object.shared_object.id, + team: team_shared_object.team.id, + permission_level: Extends::SHARED_INVENTORIES_PL_MAPPINGS[team_shared_object.permission_level.to_sym] + } + } + end + + def setup_repository_global_share_activity + message_items = { + repository: @model.id, + team: @model.team.id, + permission_level: Extends::SHARED_INVENTORIES_PL_MAPPINGS[@model.permission_level.to_sym] + } + + activity_params = + if @model.saved_changes['permission_level'][0] == 'not_shared' + { type: :share_inventory_with_all, message_items: message_items } + elsif @model.saved_changes['permission_level'][1] == 'not_shared' + { type: :unshare_inventory_with_all, message_items: message_items } + else + { type: :update_share_with_all_permission_level, message_items: message_items } + end + + @activities_to_log << activity_params + end + + def log_activities + @activities_to_log.each do |activity_params| + Activities::CreateActivityService + .call(activity_type: activity_params[:type], + owner: current_user, + team: @model.team, + subject: @model, + message_items: { + user: current_user.id + }.merge(activity_params[:message_items])) + end end end diff --git a/db/migrate/20240911133615_remove_foreign_key_constraint_from_team_shared_objects.rb b/db/migrate/20240911133615_remove_foreign_key_constraint_from_team_shared_objects.rb new file mode 100644 index 0000000000..410557230a --- /dev/null +++ b/db/migrate/20240911133615_remove_foreign_key_constraint_from_team_shared_objects.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoveForeignKeyConstraintFromTeamSharedObjects < ActiveRecord::Migration[7.0] + def change + remove_foreign_key :team_shared_objects, :repositories, column: :shared_object_id + end +end From c230999f5eb4634a65ed9db5819aa21d2384554f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:53:21 +0200 Subject: [PATCH 074/249] Bump path-to-regexp from 1.8.0 to 1.9.0 (#7845) Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 1.8.0 to 1.9.0. - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v1.8.0...v1.9.0) --- updated-dependencies: - dependency-name: path-to-regexp dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c71fb47ab6..6fdba990ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5919,9 +5919,9 @@ path-parse@^1.0.7: integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + version "1.9.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" + integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== dependencies: isarray "0.0.1" From fe078fbe8d1c95e8f501b006ab3ae414f82bdd0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:56:24 +0200 Subject: [PATCH 075/249] Bump omniauth-saml from 2.1.0 to 2.1.1 (#7844) Bumps [omniauth-saml](https://github.com/omniauth/omniauth-saml) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/omniauth/omniauth-saml/releases) - [Changelog](https://github.com/omniauth/omniauth-saml/blob/master/CHANGELOG.md) - [Commits](https://github.com/omniauth/omniauth-saml/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: omniauth-saml dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a18763584e..3648ce9776 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -495,9 +495,9 @@ GEM omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.1.0) - omniauth (~> 2.0) - ruby-saml (~> 1.12) + omniauth-saml (2.1.1) + omniauth (~> 2.1) + ruby-saml (>= 1.17) omniauth_openid_connect (0.7.1) omniauth (>= 1.9, < 3) openid_connect (~> 2.2) @@ -561,8 +561,9 @@ GEM faraday-follow_redirects json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (3.0.6) - rack + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) rails (7.0.8.4) From 8e79237e2f18892ff78416d89f3d4bd198fb1c23 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 12 Sep 2024 14:36:55 +0200 Subject: [PATCH 076/249] Fix interactions for box locations [SCI-11033] --- .../vue/storage_locations/container.vue | 2 +- .../vue/storage_locations/modals/new_edit.vue | 2 +- .../vue/storage_locations/table.vue | 12 +++--- .../vue/user_preferences/container.vue | 18 ++++----- .../lists/storage_location_serializer.rb | 2 + .../lists/storage_locations_service.rb | 39 +++++++++++++++++++ .../storage_locations/import_service.rb | 2 +- config/initializers/extends.rb | 2 + config/locales/en.yml | 4 +- 9 files changed, 63 insertions(+), 20 deletions(-) diff --git a/app/javascript/vue/storage_locations/container.vue b/app/javascript/vue/storage_locations/container.vue index e5b3a4174e..c74e075d81 100644 --- a/app/javascript/vue/storage_locations/container.vue +++ b/app/javascript/vue/storage_locations/container.vue @@ -127,7 +127,7 @@ export default { field: 'position_formatted', headerName: this.i18n.t('storage_locations.show.table.position'), sortable: true, - notSelectable: true + cellClass: 'text-sn-blue cursor-pointer' }, { field: 'reminders', diff --git a/app/javascript/vue/storage_locations/modals/new_edit.vue b/app/javascript/vue/storage_locations/modals/new_edit.vue index 22843b34cd..a4a01826e7 100644 --- a/app/javascript/vue/storage_locations/modals/new_edit.vue +++ b/app/javascript/vue/storage_locations/modals/new_edit.vue @@ -21,7 +21,7 @@
{{ this.errors.name }} diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 66f52d9df8..6fd836dc0b 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -116,12 +116,7 @@ export default { sortable: true }, { - field: 'items', - headerName: this.i18n.t('storage_locations.index.table.items'), - sortable: true - }, - { - field: 'shared', + field: 'shared_label', headerName: this.i18n.t('storage_locations.index.table.shared'), sortable: true }, @@ -135,6 +130,11 @@ export default { headerName: this.i18n.t('storage_locations.index.table.created_on'), sortable: true }, + { + field: 'created_by', + headerName: this.i18n.t('storage_locations.index.table.created_by'), + sortable: true + }, { field: 'description', headerName: this.i18n.t('storage_locations.index.table.description'), diff --git a/app/javascript/vue/user_preferences/container.vue b/app/javascript/vue/user_preferences/container.vue index f960b49b97..de10f81bdd 100644 --- a/app/javascript/vue/user_preferences/container.vue +++ b/app/javascript/vue/user_preferences/container.vue @@ -41,16 +41,14 @@ From 3fc6f20bd42ae5c00d2a0752a33a2c0e516012a4 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 20 Sep 2024 15:16:16 +0200 Subject: [PATCH 109/249] Fix import and validation for containers without box [SCI-11065] --- ...age_location_repository_rows_controller.rb | 4 +-- .../vue/storage_locations/modals/assign.vue | 2 ++ app/models/storage_location_repository_row.rb | 12 +++++++++ .../storage_locations/import_service.rb | 25 +++++++++++++------ config/locales/en.yml | 2 ++ 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb index c9ee5647b6..a1d3fc196a 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -31,7 +31,7 @@ def create render json: @storage_location_repository_row, serializer: Lists::StorageLocationRepositoryRowSerializer else - render json: @storage_location_repository_row.errors, status: :unprocessable_entity + render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity end end end @@ -45,7 +45,7 @@ def update render json: @storage_location_repository_row, serializer: Lists::StorageLocationRepositoryRowSerializer else - render json: @storage_location_repository_row.errors, status: :unprocessable_entity + render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity end end end diff --git a/app/javascript/vue/storage_locations/modals/assign.vue b/app/javascript/vue/storage_locations/modals/assign.vue index 499c7f1d9b..b3bfa0a6d2 100644 --- a/app/javascript/vue/storage_locations/modals/assign.vue +++ b/app/javascript/vue/storage_locations/modals/assign.vue @@ -91,6 +91,8 @@ export default { metadata: { position: this.position?.map((pos) => parseInt(pos, 10)) } }).then(() => { this.$emit('close'); + }).catch((error) => { + HelperModule.flashAlertMsg(error.response.data.errors.join(', '), 'danger'); }); } } diff --git a/app/models/storage_location_repository_row.rb b/app/models/storage_location_repository_row.rb index 608b7d0e90..73e57a85b5 100644 --- a/app/models/storage_location_repository_row.rb +++ b/app/models/storage_location_repository_row.rb @@ -14,6 +14,10 @@ class StorageLocationRepositoryRow < ApplicationRecord validate :ensure_uniq_position end + with_options if: -> { storage_location.container && storage_location.metadata['display_type'] != 'grid' } do + validate :unique_repository_row + end + def human_readable_position return unless metadata['position'] @@ -34,4 +38,12 @@ def ensure_uniq_position errors.add(:base, I18n.t('activerecord.errors.models.storage_location.not_uniq_position')) end end + + def unique_repository_row + if storage_location.storage_location_repository_rows + .where(repository_row_id: repository_row_id) + .where.not(id: id).exists? + errors.add(:base, I18n.t('activerecord.errors.models.storage_location.not_uniq_repository_row')) + end + end end diff --git a/app/services/storage_locations/import_service.rb b/app/services/storage_locations/import_service.rb index 01a6a29fe5..02dd282016 100644 --- a/app/services/storage_locations/import_service.rb +++ b/app/services/storage_locations/import_service.rb @@ -26,8 +26,13 @@ def import_items return { status: :error, message: I18n.t('storage_locations.show.import_modal.errors.invalid_position') } end + # Check if duplicate repository rows are present in the file + if !@storage_location.with_grid? && @rows.pluck(:repository_row_id).uniq.length != @rows.length + return { status: :error, message: I18n.t('storage_locations.show.import_modal.errors.duplicate_items') } + end + ActiveRecord::Base.transaction do - unassign_repository_rows! + unassign_repository_rows! if @storage_location.with_grid? @rows.each do |row| if @storage_location.with_grid? && !position_valid?(row[:position]) @@ -59,18 +64,22 @@ def parse_rows! row = SpreadsheetParser.parse_row(r, @sheet) { position: convert_position_letter_to_number(row[0]), - repository_row_id: row[1].gsub('IT', '').to_i + repository_row_id: row[1].to_s.gsub('IT', '').to_i } end end def import_row!(row) - storage_location_repository_row = - @storage_location.storage_location_repository_rows - .find_or_initialize_by( - repository_row_id: row[:repository_row_id], - metadata: { position: row[:position] } - ) + storage_location_repository_row = if @storage_location.with_grid? + @storage_location.storage_location_repository_rows + .find_or_initialize_by( + repository_row_id: row[:repository_row_id], + metadata: { position: row[:position] } + ) + else + @storage_location.storage_location_repository_rows + .find_or_initialize_by(repository_row_id: row[:repository_row_id]) + end if storage_location_repository_row.new_record? @assigned_count += 1 diff --git a/config/locales/en.yml b/config/locales/en.yml index 4b046bb51d..cc93a7783f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -264,6 +264,7 @@ en: storage_location: missing_position: 'Missing position metadata' not_uniq_position: 'Position already taken' + not_uniq_repository_row: 'Inventory item already exists' attributes: parent_storage_location: "Storage location cannot be parent to itself" parent_storage_location_child: "Storage location cannot be moved to it's child" @@ -2709,6 +2710,7 @@ en: errors: invalid_structure: "The imported file content doesn't meet criteria." invalid_position: "Positions in the file must match with the box." + duplicate_items: "Item ID has duplicates in the imported file" invalid_item: "Item ID %{row_id} doesn't exist." index: head_title: "Locations" From c5d6c8e0391c3cae7aca6f8810d5f0c570a5769a Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 20 Sep 2024 18:41:57 +0200 Subject: [PATCH 110/249] Storage locations fixes for sharing and moving locations [SCI-11075] --- app/controllers/storage_locations_controller.rb | 11 ++++++++--- app/helpers/global_activities_helper.rb | 8 +++++++- app/services/lists/storage_locations_service.rb | 1 + config/locales/en.yml | 1 + 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index aa3622e38d..b239c95031 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -11,8 +11,10 @@ class StorageLocationsController < ApplicationController def index respond_to do |format| format.html do - @parent_location = StorageLocation.viewable_by_user(current_user) - .find_by(id: storage_location_params[:parent_id]) if storage_location_params[:parent_id] + if storage_location_params[:parent_id] + @parent_location = StorageLocation.viewable_by_user(current_user) + .find_by(id: storage_location_params[:parent_id]) + end end format.json do storage_locations = Lists::StorageLocationsService.new(current_user, current_team, params).call @@ -94,7 +96,10 @@ def move @storage_location.update!(parent: destination_storage_location) - log_activity('storage_location_moved', { storage_location_original: original_storage_location.id, storage_location_destination: destination_storage_location.id }) + log_activity('storage_location_moved', { + storage_location_original: original_storage_location&.id, # nil if moved from root + storage_location_destination: destination_storage_location&.id # nil if moved to root + }) end render json: { message: I18n.t('storage_locations.index.move_modal.success_flash') } diff --git a/app/helpers/global_activities_helper.rb b/app/helpers/global_activities_helper.rb index a2d1a9d79a..e50bfd936f 100644 --- a/app/helpers/global_activities_helper.rb +++ b/app/helpers/global_activities_helper.rb @@ -109,7 +109,11 @@ def generate_link(message_item, activity) project_folder_path(obj, team: obj.team.id) end when StorageLocation - path = storage_location_path(obj) + path = if obj.new_record? + storage_locations_path(team: activity.team.id) + else + storage_location_path(obj) + end else return current_value end @@ -125,6 +129,8 @@ def generate_name(message_item) return I18n.t('projects.index.breadcrumbs_root') if obj.is_a?(ProjectFolder) && obj.new_record? + return I18n.t('storage_locations.index.breadcrumbs_root') if obj.is_a?(StorageLocation) && obj.new_record? + return message_item['value'] unless obj value = obj.public_send(message_item['value_for'] || 'name') diff --git a/app/services/lists/storage_locations_service.rb b/app/services/lists/storage_locations_service.rb index 649f61472d..7d497b2450 100644 --- a/app/services/lists/storage_locations_service.rb +++ b/app/services/lists/storage_locations_service.rb @@ -89,6 +89,7 @@ def shared_sql_select SELECT 1 FROM team_shared_objects WHERE team_shared_objects.shared_object_id = storage_locations.id AND team_shared_objects.shared_object_type = 'StorageLocation' + AND storage_locations.team_id = :team_id ) THEN 1 WHEN EXISTS ( SELECT 1 FROM team_shared_objects diff --git a/config/locales/en.yml b/config/locales/en.yml index 4b046bb51d..6f0276709a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2712,6 +2712,7 @@ en: invalid_item: "Item ID %{row_id} doesn't exist." index: head_title: "Locations" + breadcrumbs_root: "Locations" new_location: "New location" new_container: "New box" duplicate: From 52d2cd6d0ced77350b5de7c844f16c996bb725c0 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 23 Sep 2024 12:47:46 +0200 Subject: [PATCH 111/249] Storage locations box fixes [SCI-11077][SCI-11078][SCI-11080] --- app/controllers/repositories_controller.rb | 2 + .../vue/storage_locations/container.vue | 65 +++++++++++-------- .../modals/assign/row_selector.vue | 2 +- ...rage_location_repository_row_serializer.rb | 6 +- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 2f562a5caa..4ec0abb037 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -46,6 +46,8 @@ def index def list results = @repositories results = results.name_like(params[:query]) if params[:query].present? + results = results.joins(:repository_rows).distinct if params[:non_empty].present? + render json: { data: results.map { |r| [r.id, r.name] } } end diff --git a/app/javascript/vue/storage_locations/container.vue b/app/javascript/vue/storage_locations/container.vue index 0cfa9cbbc4..b4e88c92eb 100644 --- a/app/javascript/vue/storage_locations/container.vue +++ b/app/javascript/vue/storage_locations/container.vue @@ -125,34 +125,43 @@ export default { }, columnDefs() { - const columns = [{ - field: 'position_formatted', - headerName: this.i18n.t('storage_locations.show.table.position'), - sortable: true, - cellClass: 'text-sn-blue cursor-pointer' - }, - { - field: 'reminders', - headerName: this.i18n.t('storage_locations.show.table.reminders'), - sortable: false, - cellRenderer: RemindersRender - }, - { - field: 'row_id', - headerName: this.i18n.t('storage_locations.show.table.row_id'), - sortable: true - }, - { - field: 'row_name', - headerName: this.i18n.t('storage_locations.show.table.row_name'), - sortable: true, - cellRenderer: ItemNameRenderer - }, - { - field: 'stock', - headerName: this.i18n.t('storage_locations.show.table.stock'), - sortable: false - }]; + let columns = []; + + if (this.withGrid) { + columns.push({ + field: 'position_formatted', + headerName: this.i18n.t('storage_locations.show.table.position'), + sortable: true, + cellClass: 'text-sn-blue cursor-pointer' + }); + } + + columns = columns.concat( + [ + { + field: 'reminders', + headerName: this.i18n.t('storage_locations.show.table.reminders'), + sortable: false, + cellRenderer: RemindersRender + }, + { + field: 'row_code', + headerName: this.i18n.t('storage_locations.show.table.row_id'), + sortable: true + }, + { + field: 'row_name', + headerName: this.i18n.t('storage_locations.show.table.row_name'), + sortable: true, + cellRenderer: ItemNameRenderer + }, + { + field: 'stock', + headerName: this.i18n.t('storage_locations.show.table.stock'), + sortable: false + } + ] + ); return columns; }, diff --git a/app/javascript/vue/storage_locations/modals/assign/row_selector.vue b/app/javascript/vue/storage_locations/modals/assign/row_selector.vue index 24411cd535..ac26f07ea9 100644 --- a/app/javascript/vue/storage_locations/modals/assign/row_selector.vue +++ b/app/javascript/vue/storage_locations/modals/assign/row_selector.vue @@ -48,7 +48,7 @@ export default { }, computed: { repositoriesUrl() { - return list_team_repositories_path(this.teamId); + return list_team_repositories_path(this.teamId, { non_empty: true }); }, rowsUrl() { if (!this.selectedRepository) { diff --git a/app/serializers/lists/storage_location_repository_row_serializer.rb b/app/serializers/lists/storage_location_repository_row_serializer.rb index 00509d4576..a4df12a4f9 100644 --- a/app/serializers/lists/storage_location_repository_row_serializer.rb +++ b/app/serializers/lists/storage_location_repository_row_serializer.rb @@ -6,9 +6,13 @@ class StorageLocationRepositoryRowSerializer < ActiveModel::Serializer include Rails.application.routes.url_helpers attributes :created_by, :created_on, :position, :row_id, :row_name, :hidden, :position_formatted, :stock, - :have_reminders, :reminders_url, :row_url + :have_reminders, :reminders_url, :row_url, :row_code def row_id + object.repository_row.id + end + + def row_code object.repository_row.code end From 5ac9f16f58c2af98e8ee2be22122b381e26294f9 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 23 Sep 2024 15:17:50 +0200 Subject: [PATCH 112/249] Add unassign action to item card [SCI-11086][SCI-11079] --- .../vue/repository_item_sidebar/locations.vue | 40 ++++++++++++------- config/locales/en.yml | 1 + 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/app/javascript/vue/repository_item_sidebar/locations.vue b/app/javascript/vue/repository_item_sidebar/locations.vue index 4f61847c1b..589ae55665 100644 --- a/app/javascript/vue/repository_item_sidebar/locations.vue +++ b/app/javascript/vue/repository_item_sidebar/locations.vue @@ -10,13 +10,13 @@ diff --git a/app/javascript/vue/storage_locations/renderers/description.vue b/app/javascript/vue/storage_locations/renderers/description.vue index ea2d9d576e..49c9b7996e 100644 --- a/app/javascript/vue/storage_locations/renderers/description.vue +++ b/app/javascript/vue/storage_locations/renderers/description.vue @@ -1,5 +1,15 @@ diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 5647fbaee4..3c346f1986 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -13,6 +13,7 @@ @duplicate="duplicate" @tableReloaded="reloadingTable = false" @move="move" + @showDescription="showDescription" @delete="deleteStorageLocation" @share="share" /> @@ -25,6 +26,10 @@ :directUploadUrl="directUploadUrl" :editStorageLocation="editStorageLocation" /> + @@ -52,6 +57,7 @@ import DataTable from '../shared/datatable/table.vue'; import EditModal from './modals/new_edit.vue'; import MoveModal from './modals/move.vue'; import ConfirmationModal from '../shared/confirmation_modal.vue'; +import DescriptionModal from './modals/description.vue'; import ShareObjectModal from '../shared/share_modal.vue'; import DescriptionRenderer from './renderers/description.vue'; import NameRenderer from './renderers/storage_name_renderer.vue'; @@ -65,7 +71,8 @@ export default { ConfirmationModal, ShareObjectModal, DescriptionRenderer, - NameRenderer + NameRenderer, + DescriptionModal }, props: { dataSource: { @@ -96,7 +103,8 @@ export default { moveToUrl: null, shareStorageLocation: null, storageLocationDeleteTitle: '', - storageLocationDeleteDescription: '' + storageLocationDeleteDescription: '', + descriptionModalObject: null }; }, computed: { @@ -143,8 +151,10 @@ export default { field: 'sa_description', headerName: this.i18n.t('storage_locations.index.table.description'), sortable: false, - notSelectable: true, - cellRenderer: 'DescriptionRenderer' + cellStyle: { 'white-space': 'normal' }, + cellRenderer: DescriptionRenderer, + autoHeight: true, + minWidth: 110 }]; return columns; @@ -254,6 +264,9 @@ export default { }); } }, + showDescription(_e, storageLocation) { + [this.descriptionModalObject] = storageLocation; + }, share(_event, rows) { const [storageLocation] = rows; this.shareStorageLocation = storageLocation; From 85cd2bcafbdd7ce5d0113fd56796f99b3255a374 Mon Sep 17 00:00:00 2001 From: Andrej Date: Fri, 20 Sep 2024 15:33:16 +0200 Subject: [PATCH 119/249] Show hashed api key on the frontend [SCI-11059] --- .../users/registrations_controller.rb | 5 +++-- app/models/user.rb | 1 + .../edit_partials/_api_key.html.erb | 16 +++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index fb4d4d496c..17c936933c 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -208,11 +208,12 @@ def two_factor_qr_code end def regenerate_api_key - current_user.regenerate_api_key! + token = current_user.regenerate_api_key! redirect_to(edit_user_registration_path(anchor: 'api-key'), flash: { - success: t('users.registrations.edit.api_key.generated') + success: t('users.registrations.edit.api_key.generated'), + token: token }) end diff --git a/app/models/user.rb b/app/models/user.rb index 271797beee..41c3b9bd50 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -519,6 +519,7 @@ def regenerate_api_key! api_key_created_at: Time.current, api_key_expires_at: Constants::API_KEY_EXPIRES_IN.from_now ) + api_key end def revoke_api_key! diff --git a/app/views/users/registrations/edit_partials/_api_key.html.erb b/app/views/users/registrations/edit_partials/_api_key.html.erb index b219a4b554..75faaaa05a 100644 --- a/app/views/users/registrations/edit_partials/_api_key.html.erb +++ b/app/views/users/registrations/edit_partials/_api_key.html.erb @@ -5,15 +5,13 @@

<% if current_user.api_key %>
-
- <%= password_field_tag :api_key, - current_user.api_key, - name: 'api_key', - class: 'form-control sci-input-field !text-sn-black !font-mono !cursor-text', - disabled: 'disabled' - %> - -
+

+ <% if flash[:token] %> + <%= flash[:token] %> + <% else %> + <%= '*' * (current_user.api_key.length - 4) + current_user.api_key.last(4) %> + <% end %> +

<% if current_user.api_key_expires_at < Time.current %>

<%= t("users.registrations.edit.api_key.expired") %> From c27646aa3d640e03cbf41952ebf0f0ff78b9fff1 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 25 Sep 2024 12:06:20 +0200 Subject: [PATCH 120/249] Add smart annotation notifications for storage locations [SCI-11097] --- .../storage_locations_controller.rb | 26 +++++++++++++++++++ .../concerns/breadcrumbs_helper.rb | 7 +++++ config/locales/en.yml | 2 ++ 3 files changed, 35 insertions(+) diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index b239c95031..ef8001ce97 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class StorageLocationsController < ApplicationController + include ActionView::Helpers::TextHelper + include ApplicationHelper + include Rails.application.routes.url_helpers + before_action :check_storage_locations_enabled, except: :unassign_rows before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows export_container import_container) before_action :check_read_permissions, except: %i(index create tree actions_toolbar) @@ -38,6 +42,7 @@ def create if @storage_location.save log_activity('storage_location_created') + storage_location_annotation_notification render json: @storage_location, serializer: Lists::StorageLocationSerializer else render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity @@ -49,10 +54,12 @@ def update ActiveRecord::Base.transaction do @storage_location.image.purge if params[:file_name].blank? @storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id] + old_description = @storage_location.description @storage_location.update(storage_location_params) if @storage_location.save log_activity('storage_location_edited') + storage_location_annotation_notification(old_description) if old_description != @storage_location.description render json: @storage_location, serializer: Lists::StorageLocationSerializer else render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity @@ -259,6 +266,25 @@ def log_activity(type_of, message_items = {}) }.merge(message_items)) end + def storage_location_annotation_notification(old_text = nil) + url = if @storage_location.container + storage_location_path(@storage_location.id) + else + storage_locations_path(parent_id: @storage_location.id) + end + + smart_annotation_notification( + old_text: old_text, + new_text: @storage_location.description, + subject: @storage_location, + title: t('notifications.storage_location_annotation_title', + storage_location: @storage_location.name, + user: current_user.full_name), + message: t('notifications.storage_location_annotation_message_html', + storage_location: link_to(@storage_location.name, url)) + ) + end + def log_unassign_activities @storage_location_repository_rows.each do |storage_location_repository_row| Activities::CreateActivityService diff --git a/app/serializers/concerns/breadcrumbs_helper.rb b/app/serializers/concerns/breadcrumbs_helper.rb index b3b39c1b94..96112bc2f2 100644 --- a/app/serializers/concerns/breadcrumbs_helper.rb +++ b/app/serializers/concerns/breadcrumbs_helper.rb @@ -59,6 +59,13 @@ def generate_breadcrumbs(subject, breadcrumbs) when LabelTemplate parent = subject.team url = label_template_path(subject) + when StorageLocation + parent = subject.team + url = if subject.container + storage_location_path(subject) + else + storage_locations_path(parent_id: subject.id) + end when Team parent = nil url = projects_path(team: subject.id) diff --git a/config/locales/en.yml b/config/locales/en.yml index e0c8a18822..d22f07b1ff 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3931,6 +3931,8 @@ en: deliver_error: 'Exportable content' experiment_annotation_title: "%{user} mentioned you in experiment %{experiment}." experiment_annotation_message_html: "Project: %{project} | Experiment: %{experiment}" + storage_location_annotation_title: "%{user} mentioned you in storage location %{storage_location}." + storage_location_annotation_message_html: "Storage location: %{storage_location}" project_comment_annotation_title: "%{user} mentioned you in a comment on project %{project}." project_annotation_message_html: "Project: %{project}" my_module_description_annotation_title: "%{user} mentioned you in notes on task %{my_module}." From f3eb94f9c2ae18351c0e443abae3c097d177da53 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 25 Sep 2024 13:37:34 +0200 Subject: [PATCH 121/249] Add placeholder for disabled storage locations to item card [SCI-11084] --- app/controllers/repository_rows_controller.rb | 1 + app/helpers/storage_locations_helper.rb | 7 +++++++ app/javascript/vue/repository_item_sidebar/locations.vue | 5 ++++- app/views/repository_rows/show.json.jbuilder | 1 + config/locales/en.yml | 1 + 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 app/helpers/storage_locations_helper.rb diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index 9a6d2da672..6a82de2a9f 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -4,6 +4,7 @@ class RepositoryRowsController < ApplicationController include ApplicationHelper include MyModulesHelper include RepositoryDatatableHelper + include StorageLocationsHelper before_action :load_repository, except: %i(show print rows_to_print print_zpl validate_label_template_columns) before_action :load_repository_or_snapshot, only: %i(show print rows_to_print print_zpl diff --git a/app/helpers/storage_locations_helper.rb b/app/helpers/storage_locations_helper.rb new file mode 100644 index 0000000000..1e0b6d651b --- /dev/null +++ b/app/helpers/storage_locations_helper.rb @@ -0,0 +1,7 @@ +module StorageLocationsHelper + def storage_locations_placeholder + "

+ #{I18n.t('storage_locations.storage_locations_disabled')} +
" + end +end diff --git a/app/javascript/vue/repository_item_sidebar/locations.vue b/app/javascript/vue/repository_item_sidebar/locations.vue index 589ae55665..d496fd4adb 100644 --- a/app/javascript/vue/repository_item_sidebar/locations.vue +++ b/app/javascript/vue/repository_item_sidebar/locations.vue @@ -7,7 +7,7 @@ {{ i18n.t('repositories.locations.assign') }}
-