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 000000000..bab428d80 --- /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 000000000..309167cc1 --- /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 923164d0f..0b03595fc 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 000000000..8249b7667 --- /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 000000000..f3bdb1d1f --- /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 000000000..57a4bb748 --- /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 000000000..10eb48f57 --- /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 2ac6ceb2a..c3b844f90 100644 --- a/app/services/lists/base_service.rb +++ b/app/services/lists/base_service.rb @@ -29,7 +29,7 @@ module Lists 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 000000000..6f671b8ab --- /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 000000000..e802be6f6 --- /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 e48b383cc..ec905ed98 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 49db07eb4..4e081d8b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -807,6 +807,10 @@ Rails.application.routes.draw do 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 000000000..10b18a733 --- /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 5ecee75c7..6dfb6998e 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 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do 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 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do 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 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do 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 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do 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 bab428d80..416a96536 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -20,7 +20,9 @@ class StorageLocationRepositoryRowsController < ApplicationController @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 @@ class StorageLocationRepositoryRowsController < ApplicationController @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 @@ class StorageLocationRepositoryRowsController < ApplicationController 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 309167cc1..bea2d3b60 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -15,7 +15,7 @@ class StorageLocationsController < ApplicationController @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 @@ class StorageLocationsController < ApplicationController @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 8249b7667..40e3d767d 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 6f671b8ab..bb3abe765 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 @@ module Lists @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 e802be6f6..9829a243e 100644 --- a/app/services/lists/storage_locations_service.rb +++ b/app/services/lists/storage_locations_service.rb @@ -12,7 +12,6 @@ module Lists @records = StorageLocation.where(team: @team, parent_id: @parent_id) end - def filter_records - end + def filter_records; end end end From aa0c5dd6facbcd5fb138f2ce1e02d40b270b122a Mon Sep 17 00:00:00 2001 From: Andrej Date: Wed, 29 May 2024 17:18:00 +0200 Subject: [PATCH 004/249] Update logic for saving update at and modified by on repository row [SCI-10746] --- app/controllers/api/v1/inventory_cells_controller.rb | 1 + app/models/asset.rb | 2 +- app/models/repository_asset_value.rb | 2 +- app/models/repository_cell.rb | 8 +++++++- app/models/repository_checklist_items_value.rb | 11 +++++++++++ app/models/repository_checklist_value.rb | 2 +- app/models/repository_date_time_range_value_base.rb | 2 +- app/models/repository_date_time_value_base.rb | 2 +- app/models/repository_list_value.rb | 2 +- app/models/repository_number_value.rb | 2 +- app/models/repository_status_value.rb | 2 +- app/models/repository_stock_value.rb | 2 +- app/models/repository_text_value.rb | 2 +- .../repository_rows/update_repository_row_service.rb | 1 + 14 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v1/inventory_cells_controller.rb b/app/controllers/api/v1/inventory_cells_controller.rb index 1e1820c49..4d88d5a89 100644 --- a/app/controllers/api/v1/inventory_cells_controller.rb +++ b/app/controllers/api/v1/inventory_cells_controller.rb @@ -47,6 +47,7 @@ module Api end def destroy + @inventory_item.update!(last_modified_by: current_user) @inventory_cell.destroy! render body: nil end diff --git a/app/models/asset.rb b/app/models/asset.rb index 5a5f36f88..ce981c919 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -35,7 +35,7 @@ class Asset < ApplicationRecord has_one :result_asset, inverse_of: :asset, dependent: :destroy has_one :result, through: :result_asset, touch: true has_one :repository_asset_value, inverse_of: :asset, dependent: :destroy - has_one :repository_cell, through: :repository_asset_value + has_one :repository_cell, through: :repository_asset_value, touch: true has_many :report_elements, inverse_of: :asset, dependent: :destroy has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy has_many :asset_sync_tokens, dependent: :destroy diff --git a/app/models/repository_asset_value.rb b/app/models/repository_asset_value.rb index 899149f92..5e9ba015f 100644 --- a/app/models/repository_asset_value.rb +++ b/app/models/repository_asset_value.rb @@ -12,7 +12,7 @@ class RepositoryAssetValue < ApplicationRecord belongs_to :asset, inverse_of: :repository_asset_value, dependent: :destroy - has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value + has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true accepts_nested_attributes_for :repository_cell validates :asset, :repository_cell, presence: true diff --git a/app/models/repository_cell.rb b/app/models/repository_cell.rb index bff0e34d8..443d64089 100644 --- a/app/models/repository_cell.rb +++ b/app/models/repository_cell.rb @@ -5,7 +5,7 @@ class RepositoryCell < ApplicationRecord attr_accessor :importing - belongs_to :repository_row + belongs_to :repository_row, touch: true belongs_to :repository_column belongs_to :value, polymorphic: true, inverse_of: :repository_cell, dependent: :destroy @@ -45,10 +45,16 @@ class RepositoryCell < ApplicationRecord uniqueness: { scope: :repository_column }, unless: :importing + after_touch :update_repository_row_last_modified_by + scope :with_active_reminder, lambda { |user| reminder_repository_cells_scope(self, user) } + def update_repository_row_last_modified_by + repository_row.update!(last_modified_by_id: value.last_modified_by_id) + end + def self.create_with_value!(row, column, data, user) cell = new(repository_row: row, repository_column: column) cell.transaction do diff --git a/app/models/repository_checklist_items_value.rb b/app/models/repository_checklist_items_value.rb index 4ce8b0b58..ee30d7237 100644 --- a/app/models/repository_checklist_items_value.rb +++ b/app/models/repository_checklist_items_value.rb @@ -5,4 +5,15 @@ class RepositoryChecklistItemsValue < ApplicationRecord belongs_to :repository_checklist_value, inverse_of: :repository_checklist_items_values validates :repository_checklist_item, :repository_checklist_value, presence: true + + after_create :touch_repository_checklist_value + before_destroy :touch_repository_checklist_value + + private + + # rubocop:disable Rails/SkipsModelValidations + def touch_repository_checklist_value + repository_checklist_value.touch + end + # rubocop:enable Rails/SkipsModelValidations end diff --git a/app/models/repository_checklist_value.rb b/app/models/repository_checklist_value.rb index 302b03943..cd06a33c7 100644 --- a/app/models/repository_checklist_value.rb +++ b/app/models/repository_checklist_value.rb @@ -5,7 +5,7 @@ class RepositoryChecklistValue < ApplicationRecord inverse_of: :created_repository_checklist_values belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', inverse_of: :modified_repository_checklist_values - has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value + has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true has_many :repository_checklist_items_values, dependent: :destroy has_many :repository_checklist_items, -> { order('data ASC') }, through: :repository_checklist_items_values, diff --git a/app/models/repository_date_time_range_value_base.rb b/app/models/repository_date_time_range_value_base.rb index 37ee40321..3f20ebbb7 100644 --- a/app/models/repository_date_time_range_value_base.rb +++ b/app/models/repository_date_time_range_value_base.rb @@ -7,7 +7,7 @@ class RepositoryDateTimeRangeValueBase < ApplicationRecord inverse_of: :created_repository_date_time_values belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', optional: true, inverse_of: :modified_repository_date_time_values - has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value + has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true accepts_nested_attributes_for :repository_cell validates :repository_cell, :start_time, :end_time, :type, presence: true diff --git a/app/models/repository_date_time_value_base.rb b/app/models/repository_date_time_value_base.rb index 35f5e33b1..f3c1e18ae 100644 --- a/app/models/repository_date_time_value_base.rb +++ b/app/models/repository_date_time_value_base.rb @@ -7,7 +7,7 @@ class RepositoryDateTimeValueBase < ApplicationRecord inverse_of: :created_repository_date_time_values belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', optional: true, inverse_of: :modified_repository_date_time_values - has_one :repository_cell, as: :value, dependent: :destroy + has_one :repository_cell, as: :value, dependent: :destroy, touch: true accepts_nested_attributes_for :repository_cell before_save :reset_notification_sent, if: -> { data_changed? } diff --git a/app/models/repository_list_value.rb b/app/models/repository_list_value.rb index 81c2c97d5..c038e92ff 100644 --- a/app/models/repository_list_value.rb +++ b/app/models/repository_list_value.rb @@ -8,7 +8,7 @@ class RepositoryListValue < ApplicationRecord belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User' - has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value + has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true accepts_nested_attributes_for :repository_cell validates :repository_cell, presence: true diff --git a/app/models/repository_number_value.rb b/app/models/repository_number_value.rb index 58a437589..4075a0510 100644 --- a/app/models/repository_number_value.rb +++ b/app/models/repository_number_value.rb @@ -5,7 +5,7 @@ class RepositoryNumberValue < ApplicationRecord inverse_of: :created_repository_number_values belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', inverse_of: :modified_repository_number_values - has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value + has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true accepts_nested_attributes_for :repository_cell validates :repository_cell, :data, presence: true diff --git a/app/models/repository_status_value.rb b/app/models/repository_status_value.rb index 386426531..2c633dec1 100644 --- a/app/models/repository_status_value.rb +++ b/app/models/repository_status_value.rb @@ -6,7 +6,7 @@ class RepositoryStatusValue < ApplicationRecord inverse_of: :created_repository_status_value belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true, inverse_of: :modified_repository_status_value - has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value + has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true accepts_nested_attributes_for :repository_cell validates :repository_cell, :repository_status_item, presence: true diff --git a/app/models/repository_stock_value.rb b/app/models/repository_stock_value.rb index 2f882ce3e..6a5869050 100644 --- a/app/models/repository_stock_value.rb +++ b/app/models/repository_stock_value.rb @@ -9,7 +9,7 @@ class RepositoryStockValue < ApplicationRecord belongs_to :repository_stock_unit_item, optional: true belongs_to :created_by, class_name: 'User', optional: true, inverse_of: :created_repository_stock_values belongs_to :last_modified_by, class_name: 'User', optional: true, inverse_of: :modified_repository_stock_values - has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value + has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value, touch: true has_one :repository_row, through: :repository_cell has_many :repository_ledger_records, dependent: :destroy accepts_nested_attributes_for :repository_cell diff --git a/app/models/repository_text_value.rb b/app/models/repository_text_value.rb index d6c8daa01..259cd03d6 100644 --- a/app/models/repository_text_value.rb +++ b/app/models/repository_text_value.rb @@ -7,7 +7,7 @@ class RepositoryTextValue < ApplicationRecord belongs_to :last_modified_by, foreign_key: :last_modified_by_id, class_name: 'User', inverse_of: :modified_repository_text_values - has_one :repository_cell, as: :value, dependent: :destroy + has_one :repository_cell, as: :value, dependent: :destroy, touch: true accepts_nested_attributes_for :repository_cell validates :repository_cell, presence: true diff --git a/app/services/repository_rows/update_repository_row_service.rb b/app/services/repository_rows/update_repository_row_service.rb index f42463880..0b962b827 100644 --- a/app/services/repository_rows/update_repository_row_service.rb +++ b/app/services/repository_rows/update_repository_row_service.rb @@ -26,6 +26,7 @@ module RepositoryRows @cell = @repository_row.repository_cells.find_by(repository_column_id: @column.id) if @cell.present? && value.blank? + @repository_row.last_modified_by = @user @cell.destroy! @cell = nil @record_updated = true From 096d1fca921a5e092f98ec3ffc395c80d48b4daf Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Thu, 11 Jul 2024 15:41:13 +0200 Subject: [PATCH 005/249] Implement soft locked repository [SCI-10880] --- app/helpers/repository_datatable_helper.rb | 18 ++++++++++++++++-- app/models/soft_locked_repository.rb | 11 +++++++++++ .../toolbars/repository_rows_service.rb | 8 ++++---- .../repositories/_toolbar_buttons.html.erb | 2 +- app/views/repository_rows/show.json.jbuilder | 4 ++-- config/initializers/extends.rb | 2 +- 6 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 app/models/soft_locked_repository.rb diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb index 34c9b336b..cc82b8f46 100644 --- a/app/helpers/repository_datatable_helper.rb +++ b/app/helpers/repository_datatable_helper.rb @@ -9,7 +9,8 @@ module RepositoryDatatableHelper reminders_enabled = Repository.reminders_enabled? repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows stock_managable = has_stock_management && !options[:disable_stock_management] && - can_manage_repository_stock?(repository) + can_manage_repository_stock?(repository) && + !repository.is_a?(SoftLockedRepository) stock_consumption_permitted = has_stock_management && options[:include_stock_consumption] && options[:my_module] && stock_consumption_permitted?(repository, options[:my_module]) @@ -36,7 +37,7 @@ module RepositoryDatatableHelper row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders end - unless options[:view_mode] + unless options[:view_mode] || repository.is_a?(SoftLockedRepository) row['recordUpdateUrl'] = Rails.application.routes.url_helpers.repository_repository_row_path(repository, record) row['recordEditable'] = record.editable? @@ -244,6 +245,19 @@ module RepositoryDatatableHelper } end + def soft_locked_repository_default_columns(record) + { + '1': assigned_row(record), + '2': record.code, + '3': escape_input(record.name), + '4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}", + '5': I18n.l(record.created_at, format: :full), + '6': escape_input(record.created_by.full_name), + '7': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''), + '8': escape_input(record.archived_by&.full_name) + } + end + def linked_repository_default_columns(record) { '1': assigned_row(record), diff --git a/app/models/soft_locked_repository.rb b/app/models/soft_locked_repository.rb new file mode 100644 index 000000000..93a51178d --- /dev/null +++ b/app/models/soft_locked_repository.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class SoftLockedRepository < Repository + # this is for repositories only editable via API + + enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS.except(:shared_write) + + def shareable_write? + false + end +end diff --git a/app/services/toolbars/repository_rows_service.rb b/app/services/toolbars/repository_rows_service.rb index ad0924727..0ef1992d0 100644 --- a/app/services/toolbars/repository_rows_service.rb +++ b/app/services/toolbars/repository_rows_service.rb @@ -42,7 +42,7 @@ module Toolbars private def restore_action - return unless can_manage_repository_rows?(@repository) + return unless can_manage_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) return unless @repository_rows.all?(&:archived?) @@ -57,7 +57,7 @@ module Toolbars end def edit_action - return unless can_manage_repository_rows?(@repository) + return unless can_manage_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) return unless @repository_rows.all?(&:active?) @@ -87,7 +87,7 @@ module Toolbars end def duplicate_action - return unless can_create_repository_rows?(@repository) + return unless can_create_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) return unless @repository_rows.all?(&:active?) @@ -151,7 +151,7 @@ module Toolbars end def archive_action - return unless can_manage_repository_rows?(@repository) + return unless can_manage_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) return unless @repository_rows.all?(&:active?) diff --git a/app/views/repositories/_toolbar_buttons.html.erb b/app/views/repositories/_toolbar_buttons.html.erb index 39408b711..871026706 100644 --- a/app/views/repositories/_toolbar_buttons.html.erb +++ b/app/views/repositories/_toolbar_buttons.html.erb @@ -11,7 +11,7 @@ <%= t('repositories.index.snapshot_provisioning_in_progress') %> <% end %> - <% if can_create_repository_rows?(@repository) %> + <% if can_create_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) %> @@ -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 000000000..51acf366f --- /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 a81190bb9..b41684841 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 b4589d7db..22aa7ef24 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -819,6 +819,10 @@ Rails.application.routes.draw do 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 bb7e85b19db4dd67cd978d637becc54d3a129b7f Mon Sep 17 00:00:00 2001 From: Klemen Benedicic Date: Thu, 25 Jul 2024 13:52:31 +0200 Subject: [PATCH 024/249] Add data-e2e to protocol versions modal [SCI-10920] --- .../vue/protocols/modals/versions.vue | 34 ++++++++++++------- app/views/my_modules/_header_actions.html.erb | 6 +++- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/javascript/vue/protocols/modals/versions.vue b/app/javascript/vue/protocols/modals/versions.vue index 083b6d0e5..781522703 100644 --- a/app/javascript/vue/protocols/modals/versions.vue +++ b/app/javascript/vue/protocols/modals/versions.vue @@ -1,12 +1,12 @@ @@ -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 ded0d878f..ccc4aaaae 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 000000000..9fe585663 --- /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 000000000..d42b661b3 --- /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 000000000..7b84a48cd --- /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 000000000..24411cd53 --- /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 052b8e268..6f59b9824 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 720642392..ac3cfab89 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 000000000..04ac32345 --- /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 01044eb5a..6cbc21ee4 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 b23cda749..e0b365612 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 692d7ae73..a2b1186af 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -105,4 +105,8 @@ Rails.application.configure do 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 000000000..588f5b292 --- /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 831f33f7c..e077ef09c 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 22aa7ef24..4606e0706 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -194,6 +194,8 @@ Rails.application.routes.draw do 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 @@ Rails.application.routes.draw do 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 028/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 2b106bc17..04620a496 100644 --- a/app/controllers/users/settings/user_settings_controller.rb +++ b/app/controllers/users/settings/user_settings_controller.rb @@ -17,8 +17,8 @@ module Users next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s) case key.to_s - when 'task_step_states' - update_task_step_states(data) + when 'task_step_states', 'result_states' + update_object_states(data, key.to_s) else current_user.settings[key] = data end @@ -34,18 +34,18 @@ module Users private - def update_task_step_states(task_step_states_data) - current_states = current_user.settings.fetch('task_step_states', {}) + def update_object_states(object_states_data, object_state_key) + current_states = current_user.settings.fetch(object_state_key, {}) - task_step_states_data.each do |step_id, collapsed| + object_states_data.each do |object_id, collapsed| if collapsed - current_states[step_id] = true + current_states[object_id] = true else - current_states.delete(step_id) + current_states.delete(object_id) end end - current_user.settings['task_step_states'] = current_states + current_user.settings[object_state_key] = current_states end end end diff --git a/app/javascript/vue/results/result.vue b/app/javascript/vue/results/result.vue index bb2137b43..cd8816346 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 23eefb5e8..3b4bc97dc 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 f8bf283fa..b1554c283 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 767937497..37ea43114 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 156cc8b7f..9988a28a6 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 a580cc3cf..684cc56ca 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 a00708ea5bea6ba832b48b590b34817180b6ac1b Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 30 Jul 2024 11:39:21 +0200 Subject: [PATCH 029/249] Remove edit ability for read only inventories [SCI-10930] --- app/controllers/assets_controller.rb | 4 +++- app/controllers/repositories_controller.rb | 2 +- app/services/toolbars/repositories_service.rb | 12 ++++++++---- app/services/toolbars/repository_rows_service.rb | 2 +- app/views/repositories/_repository_table.html.erb | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 6a583d933..383964890 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -21,11 +21,13 @@ class AssetsController < ApplicationController before_action :check_manage_permission, only: %i(edit destroy duplicate rename toggle_view_mode) def file_preview + editable = can_manage_asset?(@asset) && (@asset.repository_asset_value.blank? || + !@asset.repository_cell.repository_row.repository.is_a?(SoftLockedRepository)) render json: { html: render_to_string( partial: 'shared/file_preview/content', locals: { asset: @asset, - can_edit: can_manage_asset?(@asset), + can_edit: editable, gallery: params[:gallery], preview: params[:preview] }, diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index f0057d77a..09dddfdf4 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -480,7 +480,7 @@ class RepositoriesController < ApplicationController end def set_inline_name_editing - return unless can_manage_repository?(@repository) + return unless can_manage_repository?(@repository) && !@repository.is_a?(SoftLockedRepository) @inline_editable_title_config = { name: 'title', diff --git a/app/services/toolbars/repositories_service.rb b/app/services/toolbars/repositories_service.rb index 80e6e8fa0..bdc0048e1 100644 --- a/app/services/toolbars/repositories_service.rb +++ b/app/services/toolbars/repositories_service.rb @@ -31,7 +31,7 @@ module Toolbars private def rename_action - return unless @single && can_manage_repository?(@repository) + return unless @single && can_manage_repository?(@repository) && !@repository.is_a?(SoftLockedRepository) { name: :update, @@ -67,7 +67,9 @@ module Toolbars end def archive_action - return unless @repositories.all? { |repository| can_archive_repository?(repository) } + return unless @repositories.all? do |repository| + can_archive_repository?(repository) && !@repository.is_a?(SoftLockedRepository) + end { name: :archive, @@ -90,7 +92,9 @@ module Toolbars end def restore_action - return unless @repositories.all? { |repository| can_archive_repository?(repository) } + return unless @repositories.all? do |repository| + can_archive_repository?(repository) && !repository.is_a?(SoftLockedRepository) + end { name: :restore, @@ -102,7 +106,7 @@ module Toolbars end def delete_action - return unless @single && can_delete_repository?(@repository) + return unless @single && can_delete_repository?(@repository) && !@repository.is_a?(SoftLockedRepository) { name: :delete, diff --git a/app/services/toolbars/repository_rows_service.rb b/app/services/toolbars/repository_rows_service.rb index 0ef1992d0..cfbbdf84e 100644 --- a/app/services/toolbars/repository_rows_service.rb +++ b/app/services/toolbars/repository_rows_service.rb @@ -166,7 +166,7 @@ module Toolbars end def delete_action - return unless can_delete_repository_rows?(@repository) + return unless can_delete_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) return unless @repository_rows.all?(&:archived?) diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index 59ec696d5..f60d324a4 100644 --- a/app/views/repositories/_repository_table.html.erb +++ b/app/views/repositories/_repository_table.html.erb @@ -52,7 +52,7 @@ data-type="<%= column.data_type %>" data-edit-column-url="<%= edit_repository_repository_column_path(repository, column) %>" data-destroy-column-url="<%= repository_columns_destroy_html_path(repository, column) %>" - data-editable-row="<%= can_manage_repository_column?(column) %>" + data-editable-row="<%= can_manage_repository_column?(column) && !repository.is_a?(SoftLockedRepository) %>" <% column.metadata.each do |k, v| %> <%= "data-metadata-#{k}=#{v}" %> <% end %> From 0cac65063c0ff58bd092cba49dc1ad1db07d1ece Mon Sep 17 00:00:00 2001 From: Klemen Benedicic Date: Tue, 30 Jul 2024 13:24:43 +0200 Subject: [PATCH 030/249] Update step text title and content data-e2e [SCI-10920] --- app/javascript/vue/shared/content/text.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/vue/shared/content/text.vue b/app/javascript/vue/shared/content/text.vue index 2e89fba8e..b5e5de961 100644 --- a/app/javascript/vue/shared/content/text.vue +++ b/app/javascript/vue/shared/content/text.vue @@ -15,7 +15,7 @@ :allowBlank="true" :autofocus="editingName" :attributeName="`${i18n.t('Text')} ${i18n.t('name')}`" - :dataE2e="`${dataE2e}-stepText${element.id}`" + :dataE2e="`${dataE2e}-stepText${element.id}-title`" @editingEnabled="enableNameEdit" @editingDisabled="disableNameEdit" @update="updateName" @@ -36,7 +36,7 @@
Date: Tue, 30 Jul 2024 13:45:03 +0200 Subject: [PATCH 031/249] Force ActiveStorage variant recreation when generating reports [SCI-10931] --- app/models/concerns/tiny_mce_images.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/tiny_mce_images.rb b/app/models/concerns/tiny_mce_images.rb index 9ab9fa986..bd9cbc49e 100644 --- a/app/models/concerns/tiny_mce_images.rb +++ b/app/models/concerns/tiny_mce_images.rb @@ -29,18 +29,19 @@ module TinyMceImages )[0] next unless tm_asset_to_update - tm_asset = tm_asset.image.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT).processed + variant = tm_asset.image.variant(resize_to_limit: Constants::LARGE_PIC_FORMAT) + resized_asset = ActiveStorage::Variant.new(variant.blob, variant.variation).processed width_attr = tm_asset_to_update.attributes['width'] height_attr = tm_asset_to_update.attributes['height'] if width_attr && height_attr && (width_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[0] || height_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[1]) - width_attr.value = tm_asset.image.blob.metadata['width'].to_s - height_attr.value = tm_asset.image.blob.metadata['height'].to_s + width_attr.value = resized_asset.image.blob.metadata['width'].to_s + height_attr.value = resized_asset.image.blob.metadata['height'].to_s end - tm_asset_to_update.attributes['src'].value = convert_to_base64(tm_asset.image) + tm_asset_to_update.attributes['src'].value = convert_to_base64(resized_asset) description = html_description.css('body').inner_html.to_s end description From 1d63c6f81892815e0a50f66c28ad07f34adbfd9c Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 30 Jul 2024 14:36:00 +0200 Subject: [PATCH 032/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 2b91c58a9..5b6afc8c8 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 9f56d4236..2db65f9d7 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 ccc4aaaae..7ed5f43ca 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); }, - assignRow(row, column) { - if (this.cellIsOccupied(row, column)) { + 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); + }, + 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 628f0e99b..0ab620d51 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 @@ module Lists def position object.metadata['position'] end + + def hidden + !can_read_repository?(object.repository_row.repository) + end end end From 5714e684ef0a65aaec7506ebb5f4e1f85a60334f Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 30 Jul 2024 15:48:12 +0200 Subject: [PATCH 033/249] Change step/result toolbar interaction [SCI-10929] --- app/javascript/vue/protocol/step.vue | 2 +- app/javascript/vue/results/result.vue | 2 +- app/javascript/vue/shared/content/content_toolbar.vue | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/vue/protocol/step.vue b/app/javascript/vue/protocol/step.vue index 3569808f5..ce3322308 100644 --- a/app/javascript/vue/protocol/step.vue +++ b/app/javascript/vue/protocol/step.vue @@ -146,7 +146,7 @@ @attachment:viewMode="updateAttachmentViewMode"/> -
+
{{ i18n.t('protocols.steps.insert.button') }}: From 121a30ede60f1affaa96ebd72d9de7dfcf142655 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 30 Jul 2024 17:36:45 +0200 Subject: [PATCH 034/249] Remove add columns to the repository column management [SCI-10930] --- .../repository_columns/_manage_column_modal_index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/repository_columns/_manage_column_modal_index.html.erb b/app/views/repository_columns/_manage_column_modal_index.html.erb index dc58c813d..a4d8e4131 100644 --- a/app/views/repository_columns/_manage_column_modal_index.html.erb +++ b/app/views/repository_columns/_manage_column_modal_index.html.erb @@ -12,7 +12,7 @@
From cf1aef21a98160c3b87dc652a9ead14f5f312b3e Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Wed, 31 Jul 2024 14:49:05 +0200 Subject: [PATCH 038/249] Fix result filtering [SCI-10939] --- app/models/result.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/models/result.rb b/app/models/result.rb index caab15ba6..47ddab74c 100644 --- a/app/models/result.rb +++ b/app/models/result.rb @@ -42,12 +42,12 @@ class Result < ApplicationRecord options = {}) teams = options[:teams] || current_team || user.teams.select(:id) - new_query = distinct.left_joins(:result_comments, :result_texts, result_tables: :table) - .joins(:my_module) - .joins("INNER JOIN user_assignments my_module_user_assignments " \ - "ON my_module_user_assignments.assignable_type = 'MyModule' " \ - "AND my_module_user_assignments.assignable_id = my_modules.id") - .where(my_module_user_assignments: { user_id: user, team_id: teams }) + new_query = left_joins(:result_comments, :result_texts, result_tables: :table) + .joins(:my_module) + .joins("INNER JOIN user_assignments my_module_user_assignments " \ + "ON my_module_user_assignments.assignable_type = 'MyModule' " \ + "AND my_module_user_assignments.assignable_id = my_modules.id") + .where(my_module_user_assignments: { user_id: user, team_id: teams }) unless include_archived new_query = new_query.joins(my_module: { experiment: :project }) @@ -57,7 +57,9 @@ class Result < ApplicationRecord projects: { archived: false }) end - new_query.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, { with_subquery: true, raw_input: new_query }) + new_query.where_attributes_like_boolean( + SEARCHABLE_ATTRIBUTES, query, { with_subquery: true, raw_input: new_query } + ).distinct end def self.search_subquery(query, raw_input) From 8748711d5d9cf8822646606bde9e654737c6065e Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Wed, 17 Jul 2024 10:22:44 +0200 Subject: [PATCH 039/249] Fix repository row touch for checklist values [SCI-10890] --- app/models/repository_checklist_items_value.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/repository_checklist_items_value.rb b/app/models/repository_checklist_items_value.rb index ee30d7237..4e244c632 100644 --- a/app/models/repository_checklist_items_value.rb +++ b/app/models/repository_checklist_items_value.rb @@ -6,14 +6,18 @@ class RepositoryChecklistItemsValue < ApplicationRecord validates :repository_checklist_item, :repository_checklist_value, presence: true - after_create :touch_repository_checklist_value - before_destroy :touch_repository_checklist_value + after_commit :touch_repository_checklist_value private # rubocop:disable Rails/SkipsModelValidations def touch_repository_checklist_value - repository_checklist_value.touch + # check if value was deleted, if so, touch repositroy_row directly + if RepositoryChecklistValue.exists?(repository_checklist_value.id) + repository_checklist_value.touch + elsif RepositoryRow.exists?(repository_checklist_value.repository_cell.repository_row.id) + repository_checklist_value.repository_cell.repository_row.touch + end end # rubocop:enable Rails/SkipsModelValidations end From 52bd8c933bc3f1f7f2cbe6738618aa2427fb1200 Mon Sep 17 00:00:00 2001 From: Andrej Date: Thu, 1 Aug 2024 11:38:12 +0200 Subject: [PATCH 040/249] Fix handling of subject class for notifications [SCI-10936] --- app/notifications/base_notification.rb | 4 ++++ app/notifications/delivery_notification.rb | 2 +- app/notifications/general_notification.rb | 1 - app/services/lists/protocols_service.rb | 16 ++++++++++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/notifications/base_notification.rb b/app/notifications/base_notification.rb index d17d4a245..8eb97d074 100644 --- a/app/notifications/base_notification.rb +++ b/app/notifications/base_notification.rb @@ -34,6 +34,10 @@ class BaseNotification < Noticed::Base private + def subject_class + ApplicationRecord.descendants.find { |klass| klass.name == params[:subject_class] } + end + def database_notification? # always save all notifications, # but flag if they should display in app or not diff --git a/app/notifications/delivery_notification.rb b/app/notifications/delivery_notification.rb index fa85905bf..403606cef 100644 --- a/app/notifications/delivery_notification.rb +++ b/app/notifications/delivery_notification.rb @@ -16,7 +16,7 @@ class DeliveryNotification < BaseNotification def subject return unless params[:subject_id] && params[:subject_class] - params[:subject_class].constantize.find(params[:subject_id]) + subject_class.find(params[:subject_id]) rescue ActiveRecord::RecordNotFound NonExistantRecord.new(params[:subject_name]) end diff --git a/app/notifications/general_notification.rb b/app/notifications/general_notification.rb index f74c2a298..c683736d0 100644 --- a/app/notifications/general_notification.rb +++ b/app/notifications/general_notification.rb @@ -14,7 +14,6 @@ class GeneralNotification < BaseNotification end def subject - subject_class = params[:subject_class].constantize subject_class.find(params[:subject_id]) rescue NameError, ActiveRecord::RecordNotFound NonExistantRecord.new(params[:subject_name]) diff --git a/app/services/lists/protocols_service.rb b/app/services/lists/protocols_service.rb index 5d57e75ca..b74c68e55 100644 --- a/app/services/lists/protocols_service.rb +++ b/app/services/lists/protocols_service.rb @@ -25,14 +25,21 @@ module Lists @records = @records.preload(:parent, :latest_published_version, :draft, :protocol_keywords, user_assignments: %i(user user_role)) .joins("LEFT OUTER JOIN protocols protocol_versions " \ - "ON protocol_versions.protocol_type = #{Protocol.protocol_types[:in_repository_published_version]} " \ + "ON protocol_versions.protocol_type = + #{Protocol.connection.quote( + Protocol.protocol_types[:in_repository_published_version] + )} " \ "AND protocol_versions.parent_id = protocols.parent_id") .joins("LEFT OUTER JOIN protocols protocol_originals " \ - "ON protocol_originals.protocol_type = #{Protocol.protocol_types[:in_repository_published_original]} " \ + "ON protocol_originals.protocol_type = + #{Protocol.connection.quote( + Protocol.protocol_types[:in_repository_published_original] + )} " \ "AND protocol_originals.id = protocols.parent_id OR " \ "(protocols.id = protocol_originals.id AND protocols.parent_id IS NULL)") .joins("LEFT OUTER JOIN protocols linked_task_protocols " \ - "ON linked_task_protocols.protocol_type = #{Protocol.protocol_types[:linked]} " \ + "ON linked_task_protocols.protocol_type = + #{Protocol.connection.quote(Protocol.protocol_types[:linked])} " \ "AND (linked_task_protocols.parent_id = protocol_versions.id OR " \ "linked_task_protocols.parent_id = protocol_originals.id)") .joins('LEFT OUTER JOIN "protocol_protocol_keywords" ' \ @@ -49,7 +56,8 @@ module Lists '"protocols".*', 'COALESCE("protocols"."parent_id", "protocols"."id") AS adjusted_parent_id', 'STRING_AGG(DISTINCT("protocol_keywords"."name"), \', \') AS "protocol_keywords_str"', - "CASE WHEN protocols.protocol_type = #{Protocol.protocol_types[:in_repository_draft]} " \ + "CASE WHEN protocols.protocol_type = + #{Protocol.connection.quote(Protocol.protocol_types[:in_repository_draft])} " \ "THEN 0 ELSE COUNT(DISTINCT(\"protocol_versions\".\"id\")) + 1 " \ "END AS nr_of_versions", 'COUNT(DISTINCT("linked_task_protocols"."id")) AS nr_of_linked_tasks', From 124810e07e2b8264dd0b98880cca6eaac23554b6 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 1 Aug 2024 15:07:43 +0200 Subject: [PATCH 041/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 0be9a0740..776f9ab01 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 000000000..fa6dcc9c9 --- /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 0b03595fc..e20b3c147 100644 --- a/app/models/repository_row.rb +++ b/app/models/repository_row.rb @@ -175,6 +175,16 @@ class RepositoryRow < ApplicationRecord 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 3526eb620..61c1d8f6d 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 b88f823d1..b7d90ed6e 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 b3da5cf8c35e1386fcc09d95511957320c5f8a15 Mon Sep 17 00:00:00 2001 From: Andrej Date: Thu, 1 Aug 2024 15:44:47 +0200 Subject: [PATCH 042/249] Fix toolbar buttons on archived task canvas page [SCI-10945] --- app/assets/javascripts/my_modules/archived.js | 30 +++++++++++++++++-- app/controllers/my_modules_controller.rb | 2 +- app/services/toolbars/my_modules_service.rb | 10 +++---- app/views/experiments/module_archive.html.erb | 12 ++++++++ 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/my_modules/archived.js b/app/assets/javascripts/my_modules/archived.js index c4b1ce39b..6c7c0890d 100644 --- a/app/assets/javascripts/my_modules/archived.js +++ b/app/assets/javascripts/my_modules/archived.js @@ -7,14 +7,15 @@ let taskId = $(this).closest('.task-selector-container').data('task-id'); let index = $.inArray(taskId, selectedTasks); - window.actionToolbarComponent.fetchActions({ my_module_ids: selectedTasks }); - // If checkbox is checked and row ID is not in list of selected folder IDs if (this.checked && index === -1) { selectedTasks.push(taskId); } else if (!this.checked && index !== -1) { selectedTasks.splice(index, 1); } + + const items = selectedTasks.length ? JSON.stringify(selectedTasks.map((item) => ({ id: item }))) : []; + window.actionToolbarComponent.fetchActions({ items }); }); function restoreMyModules(url, ids) { @@ -63,7 +64,32 @@ }); } + function initAccessModal() { + $('#module-archive').on('click', '#openAccessModal', (e) => { + e.preventDefault(); + const container = document.getElementById('accessModalContainer'); + const target = e.currentTarget; + + $.get(target.dataset.url, (data) => { + const object = { + ...data.data.attributes, + id: data.data.id, + type: data.data.type + }; + const { rolesUrl } = container.dataset; + const params = { + object, + roles_path: rolesUrl + }; + const modal = $('#accessModalComponent').data('accessModal'); + modal.params = params; + modal.open(); + }); + }); + } + window.initActionToolbar(); initRestoreMyModules(); initMoveButton(); + initAccessModal(); }()); diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 7764d6c4e..807c5261d 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -410,7 +410,7 @@ class MyModulesController < ApplicationController actions: Toolbars::MyModulesService.new( current_user, - my_module_ids: JSON.parse(params[:items]).map { |i| i['id'] } + my_module_ids: params[:items].present? ? JSON.parse(params[:items]).map { |i| i['id'] } : params[:items] ).actions } end diff --git a/app/services/toolbars/my_modules_service.rb b/app/services/toolbars/my_modules_service.rb index 2efb4d24a..87ff407ed 100644 --- a/app/services/toolbars/my_modules_service.rb +++ b/app/services/toolbars/my_modules_service.rb @@ -41,6 +41,7 @@ module Toolbars name: 'restore', label: I18n.t('experiments.table.toolbar.restore'), icon: 'sn-icon sn-icon-restore', + button_id: 'restoreTask', path: restore_my_modules_experiment_path(experiment), type: :emit } @@ -68,16 +69,12 @@ module Toolbars return unless can_read_my_module?(my_module) - path = if can_manage_my_module_users?(my_module) - edit_access_permissions_my_module_path(my_module) - else - access_permissions_my_module_path(my_module) - end - { name: 'access', label: I18n.t('experiments.table.my_module_actions.access'), icon: 'sn-icon sn-icon-project-member-access', + path: my_module_path(my_module, format: :json), + button_id: 'openAccessModal', type: :emit } end @@ -89,6 +86,7 @@ module Toolbars name: 'move', label: I18n.t('experiments.table.toolbar.move'), icon: 'sn-icon sn-icon-move', + button_id: 'moveTask', type: :emit, path: move_modules_experiment_path(@my_modules.first.experiment, my_module_ids: @my_modules.pluck(:id)) } diff --git a/app/views/experiments/module_archive.html.erb b/app/views/experiments/module_archive.html.erb index 89840b75f..4acd02b19 100644 --- a/app/views/experiments/module_archive.html.erb +++ b/app/views/experiments/module_archive.html.erb @@ -31,6 +31,17 @@
+ +
+
+ + + +
<% unless @my_modules.present? %>
@@ -38,5 +49,6 @@
<% end %> <%= javascript_include_tag('vue_components_action_toolbar') %> +<%= javascript_include_tag 'vue_legacy_access_modal' %> <%= javascript_include_tag('projects/canvas') %> <%= javascript_include_tag('my_modules/archived') %> From 351aed95207c532a9643ff04ed45152eb93e9118 Mon Sep 17 00:00:00 2001 From: Andrej Date: Thu, 1 Aug 2024 16:42:33 +0200 Subject: [PATCH 043/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 08eaa66ab..0b92d73a9 100644 --- a/app/javascript/vue/projects/modals/move.vue +++ b/app/javascript/vue/projects/modals/move.vue @@ -98,20 +98,19 @@ export default { if (this.query === '') { return this.foldersTree; } - return this.foldersTree.map((folder) => ( - { - folder: folder.folder, - children: folder.children.filter((child) => ( - child.folder.name.toLowerCase().includes(this.query.toLowerCase()) - )), - } - )).filter((folder) => ( - folder.folder.name.toLowerCase().includes(this.query.toLowerCase()) - || folder.children.length > 0 - )); + return this.filteredFoldersTreeHelper(this.foldersTree); }, }, methods: { + filteredFoldersTreeHelper(foldersTree) { + return foldersTree.map(({ folder, children }) => { + if (folder.name.toLowerCase().includes(this.query.toLowerCase())) { + return { folder, children }; + } + const filteredChildren = this.filteredFoldersTreeHelper(children); + return filteredChildren.length ? { folder, children: filteredChildren } : null; + }).filter(Boolean); + }, selectFolder(folderId) { this.selectedFolderId = folderId; }, diff --git a/app/javascript/vue/storage_locations/modals/assign/container_selector.vue b/app/javascript/vue/storage_locations/modals/assign/container_selector.vue index d42b661b3..c5bd98e2b 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 6f59b9824..0066f69ef 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') }} - +