From 4753bef936d4754a5286040beb7fb06c1259cae7 Mon Sep 17 00:00:00 2001 From: Andrej Date: Wed, 2 Apr 2025 19:49:44 +0200 Subject: [PATCH 1/8] Add repository templates [SCI-11708] --- app/controllers/repositories_controller.rb | 30 ++- .../repository_templates_controller.rb | 32 +++ .../vue/repositories/modals/new.vue | 102 +++++++- app/models/repository.rb | 1 + app/models/repository_template.rb | 221 ++++++++++++++++++ app/models/team.rb | 9 + config/locales/en.yml | 71 ++++++ config/routes.rb | 6 + ...250331114826_create_repository_template.rb | 16 ++ ...92301_add_repository_templates_to_teams.rb | 19 ++ db/schema.rb | 16 +- 11 files changed, 511 insertions(+), 12 deletions(-) create mode 100644 app/controllers/repository_templates_controller.rb create mode 100644 app/models/repository_template.rb create mode 100644 db/migrate/20250331114826_create_repository_template.rb create mode 100644 db/migrate/20250402092301_add_repository_templates_to_teams.rb diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 92780b3d5..4baf20faa 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -151,17 +151,31 @@ class RepositoriesController < ApplicationController end def create - @repository = Repository.new( - team: current_team, - created_by: current_user - ) - @repository.assign_attributes(repository_params) + Repository.transaction do + @repository = Repository.new(team: current_team, created_by: current_user) + @repository.assign_attributes(repository_params) - if @repository.save + @repository.save! log_activity(:create_inventory) + + repository_template = current_team.repository_templates.find_by(id: repository_params[:repository_template_id]) + if repository_template.present? + repository_template.column_definitions&.each do |column_attributes| + service = RepositoryColumns::CreateColumnService + .call(user: current_user, repository: @repository, team: current_team, + column_type: column_attributes['column_type'], + params: column_attributes['params'].with_indifferent_access) + unless service.succeed? + render json: service.errors, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + end + end render json: { message: t('repositories.index.modal_create.success_flash_html', name: @repository.name) } - else + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error e.message render json: @repository.errors, status: :unprocessable_entity + raise ActiveRecord::Rollback end end @@ -548,7 +562,7 @@ class RepositoriesController < ApplicationController end def repository_params - params.require(:repository).permit(:name) + params.require(:repository).permit(:name, :repository_template_id) end def import_params diff --git a/app/controllers/repository_templates_controller.rb b/app/controllers/repository_templates_controller.rb new file mode 100644 index 000000000..6ea84186e --- /dev/null +++ b/app/controllers/repository_templates_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class RepositoryTemplatesController < ApplicationController + before_action :check_read_permissions + before_action :load_repository_template + + def index + repository_templates = current_team.repository_templates.order(:id) + render json: { + data: repository_templates.map { |repository_template| [repository_template.id, repository_template.name] } + } + end + + def list_repository_columns + render json: { + name: @repository_template.name, + columns: @repository_template.column_definitions&.map do |column| + [column.dig('params', 'name'), I18n.t("libraries.manange_modal_column.select.#{RepositoryColumn.data_types.key(column['column_type']).underscore}")] + end + } + end + + private + + def load_repository_template + @repository_template = current_team.repository_templates.find_by(id: params[:id]) + end + + def check_read_permissions + render_403 unless can_create_repositories?(current_team) + end +end diff --git a/app/javascript/vue/repositories/modals/new.vue b/app/javascript/vue/repositories/modals/new.vue index 88ab932d2..50ecaa65c 100644 --- a/app/javascript/vue/repositories/modals/new.vue +++ b/app/javascript/vue/repositories/modals/new.vue @@ -22,10 +22,37 @@ :placeholder="i18n.t('repositories.index.modal_create.name_placeholder')" /> +
+ + +
+
+ Loading +
+ +
+
@@ -40,9 +67,18 @@ import axios from '../../../packs/custom_axios.js'; import modalMixin from '../../shared/modal_mixin'; +import SelectDropdown from '../../shared/select_dropdown.vue'; + +import { + repository_templates_path, + list_repository_columns_repository_template_path +} from '../../../routes.js'; export default { name: 'NewRepositoryModal', + components: { + SelectDropdown + }, props: { createUrl: String }, @@ -51,21 +87,42 @@ export default { return { name: '', error: null, - submitting: false + submitting: false, + repositoryTemplates: [], + repositoryTemplate: null, + showColumnInfo: false, + hoveredRow: {}, + loadingHoveredRow: false }; }, + created() { + this.fetctRepositoryTemplates(); + }, + mounted() { + document.addEventListener('mouseover', this.loadColumnsInfo); + }, + beforeDestroy() { + document.removeEventListener('mouseover', this.loadColumnsInfo); + }, computed: { + repositoryTemplateUrl() { + return repository_templates_path(); + }, validName() { return this.name.length >= GLOBAL_CONSTANTS.NAME_MIN_LENGTH; } }, methods: { + listRepositoryTemplateColumnsUrl(repositoryTemplateId) { + return list_repository_columns_repository_template_path(repositoryTemplateId); + }, submit() { this.submitting = true; axios.post(this.createUrl, { repository: { - name: this.name + name: this.name, + repository_template_id: this.repositoryTemplate } }).then((response) => { this.error = null; @@ -76,6 +133,45 @@ export default { this.submitting = false; this.error = error.response.data.name; }); + }, + repositoryTemplateOptionRenderer(row) { + return ` +
+
+
${row[1]}
+
+ +
`; + }, + fetctRepositoryTemplates() { + axios.get(this.repositoryTemplateUrl) + .then((response) => { + this.repositoryTemplates = response.data.data; + [this.repositoryTemplate] = this.repositoryTemplates[0]; + }); + }, + loadColumnsInfo(e) { + if (!e.target.classList.contains('show-items-columns')) { + this.showColumnInfo = false; + this.hoveredRow = {}; + return; + } + + this.loadingHoveredRow = true; + + this.showColumnInfo = true; + + axios.get(this.listRepositoryTemplateColumnsUrl(e.target.dataset.itemId)) + .then((response) => { + this.loadingHoveredRow = false; + this.hoveredRow = { + name: response.data.name, + columns: response.data.columns + }; + }); + + e.stopPropagation(); + e.preventDefault(); } } }; diff --git a/app/models/repository.rb b/app/models/repository.rb index 8f9c3f271..14aaaba11 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -28,6 +28,7 @@ class Repository < RepositoryBase inverse_of: :original_repository has_many :repository_ledger_records, as: :reference, dependent: :nullify has_many :repository_table_filters, dependent: :destroy + belongs_to :repository_template, inverse_of: :repositories, optional: true before_save :sync_name_with_snapshots, if: :name_changed? before_destroy :refresh_report_references_on_destroy, prepend: true diff --git a/app/models/repository_template.rb b/app/models/repository_template.rb new file mode 100644 index 000000000..bebd3dcdb --- /dev/null +++ b/app/models/repository_template.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +class RepositoryTemplate < ApplicationRecord + belongs_to :team, inverse_of: :repository_templates + has_many :repositories, inverse_of: :repository_template, dependent: :destroy + + def self.default + RepositoryTemplate.new( + name: I18n.t('repository_templates.default_template_name'), + column_definitions: [], + predefined: true + ) + end + + def self.cell_lines + RepositoryTemplate.new( + name: I18n.t('repository_templates.cell_lines_template_name'), + column_definitions: [ + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.species') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.organ') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue], + params: { name: I18n.t('repository_templates.template_columns.morphology'), + metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') }, + repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.endothelial') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.epithelial') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.fibroblast') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.lymphoblast') }] } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue], + params: { name: I18n.t('repository_templates.template_columns.culture_type'), + metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') }, + repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.adherent') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.suspension') }] } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStockValue], + params: { name: I18n.t('repository_templates.template_columns.stock'), + metadata: { decimals: 2 }, + repository_stock_unit_items_attributes: RepositoryStockUnitItem::DEFAULT_UNITS.map { |unit| { data: unit } } + + [{ data: I18n.t('repository_templates.template_columns.stock_units.vials') }] } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryNumberValue], + params: { name: I18n.t('repository_templates.template_columns.passage_number') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.lot_number') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue], + params: { name: I18n.t('repository_templates.template_columns.freezing_date') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.operator') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.yield') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStatusValue], + params: { name: I18n.t('repository_templates.template_columns.status'), + repository_status_items_attributes: [{ status: I18n.t('repository_templates.template_columns.repository_status_value.frozen'), icon: '❄️' }, + { status: I18n.t('repository_templates.template_columns.repository_status_value.in_subculturing'), icon: '🧫' }, + { status: I18n.t('repository_templates.template_columns.repository_status_value.out_of_tock'), icon: '❌' }] } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue], + params: { name: I18n.t('repository_templates.template_columns.handling_procedure') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.notes') } + } + ], + predefined: true + ) + end + + def self.equipment + RepositoryTemplate.new( + name: I18n.t('repository_templates.equipment_template_name'), + column_definitions: [ + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue], + params: { name: I18n.t('repository_templates.template_columns.calibration_date'), + reminder_value: 1, reminder_unit: 2419200, reminder_message: I18n.t('repository_templates.template_columns.calibration_message') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStatusValue], + params: { name: I18n.t('repository_templates.template_columns.availability_status'), + repository_status_items_attributes: [{ status: I18n.t('repository_templates.template_columns.repository_status_value.available_for_use'), icon: '🟢' }, + { status: I18n.t('repository_templates.template_columns.repository_status_value.in_use'), icon: '🟥' }, + { status: I18n.t('repository_templates.template_columns.repository_status_value.out_of_service'), icon: '❌' }, + { status: I18n.t('repository_templates.template_columns.repository_status_value.under_maintenance'), icon: '🔧' }] } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue], + params: { name: I18n.t('repository_templates.template_columns.safety_handling_info') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue], + params: { name: I18n.t('repository_templates.template_columns.training_records') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.contact_person') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.contact_phone') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.internal_id') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.manufacturer') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.serial_number') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.notes') } + } + ], + predefined: true + ) + end + + def self.chemicals_and_reagents + RepositoryTemplate.new( + name: I18n.t('repository_templates.chemicals_and_reagents_template_name'), + column_definitions: [ + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.concentration') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStockValue], + params: { name: I18n.t('repository_templates.template_columns.stock'), + metadata: { decimals: 2 }, + repository_stock_unit_items_attributes: RepositoryStockUnitItem::DEFAULT_UNITS.map { |unit| { data: unit } } } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue], + params: { name: I18n.t('repository_templates.template_columns.date_opened') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue], + params: { name: I18n.t('repository_templates.template_columns.expiration_date'), + reminder_value: 1, reminder_unit: 2419200, reminder_message: I18n.t('repository_templates.template_columns.expiration_date_message') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue], + params: { name: I18n.t('repository_templates.template_columns.storage_conditions'), + metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') }, + repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.minus_twenty_celsious') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.two_to_eigth_celsious') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.minus_eigthty') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.ambient') }] } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue], + params: { name: I18n.t('repository_templates.template_columns.type'), + metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') }, + repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.buffer') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.liquid') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.reagent') }, + { data: I18n.t('repository_templates.template_columns.repository_list_value.solid') }] } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.purity') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.cas_number') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue], + params: { name: I18n.t('repository_templates.template_columns.safety_sheet') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue], + params: { name: I18n.t('repository_templates.template_columns.vendor') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.catalog_number') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.lot') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.price') } + }, + { + column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue], + params: { name: I18n.t('repository_templates.template_columns.molecular_weight') } + } + ], + predefined: true + ) + end +end diff --git a/app/models/team.rb b/app/models/team.rb index b71759413..ee8cb2ff4 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -14,6 +14,7 @@ class Team < ApplicationRecord before_save -> { shareable_links.destroy_all }, if: -> { !shareable_links_enabled? } after_create :generate_template_project after_create :create_default_label_templates + after_create :create_default_repository_templates scope :teams_select, -> { select(:id, :name).order(name: :asc) } scope :ordered, -> { order('LOWER(name)') } @@ -44,6 +45,7 @@ class Team < ApplicationRecord has_many :activities, inverse_of: :team, dependent: :destroy has_many :assets, inverse_of: :team, dependent: :destroy has_many :label_templates, dependent: :destroy + has_many :repository_templates, inverse_of: :team, dependent: :destroy has_many :team_shared_objects, inverse_of: :team, dependent: :destroy has_many :team_shared_repositories, -> { where(shared_object_type: 'RepositoryBase') }, @@ -210,4 +212,11 @@ class Team < ApplicationRecord ZebraLabelTemplate.default_203dpi.update(team: self, default: false) FluicsLabelTemplate.default.update(team: self, default: true) end + + def create_default_repository_templates + RepositoryTemplate.default.update(team: self) + RepositoryTemplate.cell_lines.update(team: self) + RepositoryTemplate.equipment.update(team: self) + RepositoryTemplate.chemicals_and_reagents.update(team: self) + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 5850af453..2a5df3ea2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1316,6 +1316,76 @@ en: unreachable: "Printer is offline" search: "Checking printer status" + repository_templates: + default_template_name: 'Default template' + cell_lines_template_name: 'Cell lines template' + equipment_template_name: 'Equipment template' + chemicals_and_reagents_template_name: "Chemicals & reagents template" + repository_list_value_delimiter: 'return' + template_columns: + species: 'Species' + organ: 'Organ' + morphology: 'Morphology' + culture_type: 'Culture Type' + stock: 'Stock' + passage_number: 'Passage Number' + lot_number: 'Lot Number' + freezing_date: 'Freezing Date' + operator: 'Operator' + yield: 'Yield' + status: 'Status' + handling_procedure: 'Handling Procedure' + notes: 'Notes' + calibration_date: 'Calibration Date' + calibration_message: 'Consider recalibration.' + availability_status: 'Availability Status' + safety_handling_info: 'Safety & Handling Info' + training_records: 'Training Records' + contact_person: 'Contact Person' + contact_phone: 'Contact Phone' + internal_id: 'Internal ID' + manufacturer: 'Manufacturer' + serial_number: 'Serial Number' + concentration: 'Concentration' + date_opened: 'Date Opened' + expiration_date: 'Expiration Date' + expiration_date_message: 'Consider replacing the item.' + storage_conditions: 'Storage Conditions' + type: 'Type' + purity: 'Purity' + cas_number: 'CAS Number' + safety_sheet: 'Safety Sheet' + vendor: 'Vendor' + catalog_number: 'Catalog Number' + lot: 'Lot' + price: 'Price' + molecular_weight: 'Molecular Weight' + repository_list_value: + endothelial: 'Endothelial' + epithelial: 'Epithelial' + fibroblast: 'Fibroblast' + lymphoblast: 'Lymphoblast' + adherent: 'Adherent' + suspension: 'Suspension' + minus_twenty_celsious: "-20°C" + two_to_eigth_celsious: "2°C to 8°C" + minus_eigthty: "-80°C" + ambient: 'Ambient' + buffer: 'Buffer' + liquid: 'Liquid' + reagent: 'Reagent' + solid: 'Solid' + stock_units: + vials: 'Vial(s)' + repository_status_value: + frozen: 'Frozen' + in_subculturing: 'In subculturing' + out_of_tock: 'Out of stock' + available_for_use: 'Available for use' + in_use: 'In use' + out_of_service: 'Out of service' + under_maintenance: 'Under maintenance' + my_modules: details: title: "Details" @@ -2171,6 +2241,7 @@ en: name_placeholder: "My inventory" submit: "Create" success_flash_html: "Inventory %{name} successfully created." + repository_template_label: "Select inventory template" modal_confirm_sharing: title: "Inventory sharing changes" description_1: "You will no longer share this inventory with some of the teams. All unshared inventory items assigned to tasks will be automatically removed and this action is irreversible. Any item relationship links (if they exist) will also be deleted." diff --git a/config/routes.rb b/config/routes.rb index 295e44c60..2447fc64a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -841,6 +841,12 @@ Rails.application.routes.draw do get :repositories end + resources :repository_templates, only: %i(index) do + member do + get :list_repository_columns + end + end + resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy) resources :storage_locations, only: %i(index create destroy update show) do diff --git a/db/migrate/20250331114826_create_repository_template.rb b/db/migrate/20250331114826_create_repository_template.rb new file mode 100644 index 000000000..88dad8657 --- /dev/null +++ b/db/migrate/20250331114826_create_repository_template.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateRepositoryTemplate < ActiveRecord::Migration[7.0] + def change + create_table :repository_templates do |t| + t.string :name + t.jsonb :column_definitions + t.references :team, index: true, foreign_key: { to_table: :teams } + t.boolean :predefined, null: false, default: false + + t.timestamps + end + + add_reference :repositories, :repository_template, null: true, foreign_key: true + end +end diff --git a/db/migrate/20250402092301_add_repository_templates_to_teams.rb b/db/migrate/20250402092301_add_repository_templates_to_teams.rb new file mode 100644 index 000000000..8e2f8bc59 --- /dev/null +++ b/db/migrate/20250402092301_add_repository_templates_to_teams.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddRepositoryTemplatesToTeams < ActiveRecord::Migration[7.0] + def up + Team.find_each do |team| + RepositoryTemplate.default.update(team: team) + RepositoryTemplate.cell_lines.update(team: team) + RepositoryTemplate.equipment.update(team: team) + RepositoryTemplate.chemicals_and_reagents.update(team: team) + end + end + + def down + # rubocop:disable Rails/SkipsModelValidations + Repository.update_all(repository_template_id: nil) + # rubocop:enable Rails/SkipsModelValidations + RepositoryTemplate.destroy_all + end +end diff --git a/db/schema.rb b/db/schema.rb index 3c6536756..b53f551f4 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: 2025_03_25_124848) do +ActiveRecord::Schema[7.0].define(version: 2025_04_02_092301) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gist" enable_extension "pg_trgm" @@ -730,10 +730,12 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do t.bigint "restored_by_id" t.string "external_id" t.integer "repository_rows_count", default: 0, null: false + t.bigint "repository_template_id" t.index ["archived"], name: "index_repositories_on_archived" t.index ["archived_by_id"], name: "index_repositories_on_archived_by_id" t.index ["discarded_at"], name: "index_repositories_on_discarded_at" t.index ["my_module_id"], name: "index_repositories_on_my_module_id" + t.index ["repository_template_id"], name: "index_repositories_on_repository_template_id" t.index ["restored_by_id"], name: "index_repositories_on_restored_by_id" t.index ["team_id", "external_id"], name: "unique_index_repositories_on_external_id", unique: true t.index ["team_id"], name: "index_repositories_on_team_id" @@ -1027,6 +1029,16 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do t.index ["user_id"], name: "index_repository_table_states_on_user_id" end + create_table "repository_templates", force: :cascade do |t| + t.string "name" + t.jsonb "column_definitions" + t.bigint "team_id" + t.boolean "predefined", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["team_id"], name: "index_repository_templates_on_team_id" + end + create_table "repository_text_values", force: :cascade do |t| t.string "data" t.datetime "created_at", precision: nil @@ -1586,6 +1598,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do add_foreign_key "reports", "projects" add_foreign_key "reports", "users" add_foreign_key "reports", "users", column: "last_modified_by_id" + add_foreign_key "repositories", "repository_templates" add_foreign_key "repositories", "users", column: "archived_by_id" add_foreign_key "repositories", "users", column: "created_by_id" add_foreign_key "repositories", "users", column: "restored_by_id" @@ -1631,6 +1644,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do add_foreign_key "repository_stock_values", "users", column: "created_by_id" add_foreign_key "repository_stock_values", "users", column: "last_modified_by_id" add_foreign_key "repository_table_filters", "users", column: "created_by_id" + add_foreign_key "repository_templates", "teams" add_foreign_key "repository_text_values", "users", column: "created_by_id" add_foreign_key "repository_text_values", "users", column: "last_modified_by_id" add_foreign_key "result_assets", "assets" From 17837c0f8ed9d8325da475d8a993dae6e7629b1a Mon Sep 17 00:00:00 2001 From: Andrej Date: Thu, 3 Apr 2025 09:21:11 +0200 Subject: [PATCH 2/8] Add test for repository template [SCI-11708] --- .../repository_templates_controller.rb | 3 +- app/models/repository.rb | 2 +- .../repositories_controller_spec.rb | 10 +++- .../repository_templates_controller_spec.rb | 49 +++++++++++++++++++ spec/factories/repository_template.rb | 7 +++ spec/models/repository_spec.rb | 1 + spec/models/repository_template_spec.rb | 29 +++++++++++ spec/models/team_spec.rb | 8 +++ 8 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 spec/controllers/repository_templates_controller_spec.rb create mode 100644 spec/factories/repository_template.rb create mode 100644 spec/models/repository_template_spec.rb diff --git a/app/controllers/repository_templates_controller.rb b/app/controllers/repository_templates_controller.rb index 6ea84186e..21a12609c 100644 --- a/app/controllers/repository_templates_controller.rb +++ b/app/controllers/repository_templates_controller.rb @@ -2,7 +2,7 @@ class RepositoryTemplatesController < ApplicationController before_action :check_read_permissions - before_action :load_repository_template + before_action :load_repository_template, only: :list_repository_columns def index repository_templates = current_team.repository_templates.order(:id) @@ -24,6 +24,7 @@ class RepositoryTemplatesController < ApplicationController def load_repository_template @repository_template = current_team.repository_templates.find_by(id: params[:id]) + render_404 unless @repository_template end def check_read_permissions diff --git a/app/models/repository.rb b/app/models/repository.rb index 14aaaba11..c49416810 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -22,13 +22,13 @@ class Repository < RepositoryBase class_name: 'User', inverse_of: :restored_repositories, optional: true + belongs_to :repository_template, inverse_of: :repositories, optional: true has_many :repository_snapshots, class_name: 'RepositorySnapshot', foreign_key: :parent_id, inverse_of: :original_repository has_many :repository_ledger_records, as: :reference, dependent: :nullify has_many :repository_table_filters, dependent: :destroy - belongs_to :repository_template, inverse_of: :repositories, optional: true before_save :sync_name_with_snapshots, if: :name_changed? before_destroy :refresh_report_references_on_destroy, prepend: true diff --git a/spec/controllers/repositories_controller_spec.rb b/spec/controllers/repositories_controller_spec.rb index 7a04b3a2f..3a5a1f8b4 100644 --- a/spec/controllers/repositories_controller_spec.rb +++ b/spec/controllers/repositories_controller_spec.rb @@ -8,6 +8,7 @@ describe RepositoriesController, type: :controller do let!(:user) { controller.current_user } let!(:team) { create :team, created_by: user } let(:action) { post :create, params: params, format: :json } + let(:repository_template) { create :repository_template, team: team } describe 'index' do let(:repository) { create :repository, team: team, created_by: user } @@ -29,7 +30,7 @@ describe RepositoriesController, type: :controller do end describe 'POST create' do - let(:params) { { repository: { name: 'My Repository' } } } + let(:params) { { repository: { name: 'My Repository', repository_template_id: repository_template.id } } } it 'calls create activity for creating inventory' do expect(Activities::CreateActivityService) @@ -43,6 +44,13 @@ describe RepositoriesController, type: :controller do expect { action } .to(change { Activity.count }) end + + it 'returns success response' do + expect { action }.to change(Repository, :count).by(1) + expect(response).to have_http_status(:success) + expect(response.media_type).to eq 'application/json' + expect(Repository.order(created_at: :desc).first.repository_template).to eq repository_template + end end describe 'DELETE destroy' do diff --git a/spec/controllers/repository_templates_controller_spec.rb b/spec/controllers/repository_templates_controller_spec.rb new file mode 100644 index 000000000..960199991 --- /dev/null +++ b/spec/controllers/repository_templates_controller_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe RepositoryTemplatesController, type: :controller do + login_user + + let!(:user) { controller.current_user } + let!(:team) { create :team, created_by: user } + + describe 'index' do + + let(:action) { get :index, format: :json } + + it 'correct JSON format' do + action + + expect(response).to have_http_status(:success) + expect(response.media_type).to eq 'application/json' + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(4) + + end + end + + describe 'list_repository_columns' do + let(:repository_template) { team.repository_templates.last } + let(:action) { get :list_repository_columns, params: { id: repository_template } ,format: :json } + let(:action_invalid) { get :list_repository_columns, params: { id: -1 } ,format: :json } + + it 'correct JSON format' do + action + + expect(response).to have_http_status(:success) + expect(response.media_type).to eq 'application/json' + + parsed_response = JSON.parse(response.body) + expect(parsed_response['name']).to eq(repository_template.name) + expect(parsed_response['columns'].count).to eq(repository_template.column_definitions.count) + end + + it 'invalid id' do + action_invalid + + expect(response).to have_http_status(:not_found) + + end + end +end diff --git a/spec/factories/repository_template.rb b/spec/factories/repository_template.rb new file mode 100644 index 000000000..bbdc82efb --- /dev/null +++ b/spec/factories/repository_template.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :repository_template do + association :team + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f98db1499..94a287b38 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -24,6 +24,7 @@ describe Repository, type: :model do describe 'Relations' do it { should belong_to :team } it { should belong_to(:created_by).class_name('User') } + it { should belong_to(:repository_template).optional } it { should have_many :repository_rows } it { should have_many :repository_table_states } it { should have_many :report_elements } diff --git a/spec/models/repository_template_spec.rb b/spec/models/repository_template_spec.rb new file mode 100644 index 000000000..165524ab0 --- /dev/null +++ b/spec/models/repository_template_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe RepositoryTemplate, type: :model do + let(:repository_template) { build :repository_template } + + it 'is valid' do + expect(repository_template).to be_valid + end + + it 'should be of class Repository Template' do + expect(subject.class).to eq RepositoryTemplate + end + + describe 'Database table' do + it { should have_db_column :name } + it { should have_db_column :team_id } + it { should have_db_column :column_definitions } + it { should have_db_column :predefined } + it { should have_db_column :created_at } + it { should have_db_column :updated_at } + end + + describe 'Relations' do + it { should belong_to :team } + it { should have_many(:repositories).dependent(:destroy) } + end +end diff --git a/spec/models/team_spec.rb b/spec/models/team_spec.rb index 00fcb66c6..b50e79edb 100644 --- a/spec/models/team_spec.rb +++ b/spec/models/team_spec.rb @@ -37,6 +37,7 @@ describe Team, type: :model do it { should have_many(:team_shared_objects).dependent(:destroy) } it { should have_many :shared_repositories } it { should have_many(:shareable_links).dependent(:destroy) } + it { should have_many(:repository_templates).dependent(:destroy) } end describe 'Validations' do @@ -87,5 +88,12 @@ describe Team, type: :model do expect(team.shareable_links.count).to eq(0) end end + + context 'repository templates after team create' do + it 'create repository templates after team create' do + expect_any_instance_of(Team).to receive(:create_default_repository_templates) + team.save! + end + end end end From f559e4f2080cd6aa3b9387757160bbe57ff736cd Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 4 Apr 2025 15:30:48 +0200 Subject: [PATCH 3/8] Remove perfect scroll bar from dashboard and activities [SCI-11734] --- app/assets/javascripts/dashboard/current_tasks.js | 1 - app/assets/javascripts/dashboard/recent_work.js | 2 -- app/assets/javascripts/sitewide/dropdown_selector.js | 12 +----------- app/assets/stylesheets/dashboard/recent_work.scss | 1 + app/assets/stylesheets/global_activities.scss | 2 ++ app/assets/stylesheets/shared/dropdown_selector.scss | 2 +- app/views/dashboards/_current_tasks.html.erb | 2 +- app/views/dashboards/_recent_work.html.erb | 2 +- app/views/global_activities/index.html.erb | 4 ++-- 9 files changed, 9 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/dashboard/current_tasks.js b/app/assets/javascripts/dashboard/current_tasks.js index c539aa9f3..b1a36e6e5 100644 --- a/app/assets/javascripts/dashboard/current_tasks.js +++ b/app/assets/javascripts/dashboard/current_tasks.js @@ -117,7 +117,6 @@ var DasboardCurrentTasksWidget = (function() { } } appendTasksList(result, '.current-tasks-list-wrapper'); - PerfectSb().update_all(); InfiniteScroll.init('.current-tasks-list-wrapper', { url: $currentTasksList.data('tasksListUrl'), diff --git a/app/assets/javascripts/dashboard/recent_work.js b/app/assets/javascripts/dashboard/recent_work.js index 2e57b1b01..b25c36826 100644 --- a/app/assets/javascripts/dashboard/recent_work.js +++ b/app/assets/javascripts/dashboard/recent_work.js @@ -32,8 +32,6 @@ var DasboardRecentWorkWidget = (function() { } else { container.append($('#recent-work-no-results-template').html()); } - - PerfectSb().update_all(); }); } diff --git a/app/assets/javascripts/sitewide/dropdown_selector.js b/app/assets/javascripts/sitewide/dropdown_selector.js index c178e3245..d6c2d7d32 100644 --- a/app/assets/javascripts/sitewide/dropdown_selector.js +++ b/app/assets/javascripts/sitewide/dropdown_selector.js @@ -1,4 +1,4 @@ -/* global PerfectScrollbar activePSB PerfectSb I18n */ +/* global I18n */ /* eslint-disable no-unused-vars, no-use-before-define */ /* @@ -301,7 +301,6 @@ var dropdownSelector = (function() { function generateDropdown(selector, config = {}) { var selectElement = $(selector); var optionContainer; - var perfectScroll; var dropdownContainer; var toggleElement; @@ -410,10 +409,6 @@ var dropdownSelector = (function() { } }); - // Initialize scroll bar inside options container - perfectScroll = new PerfectScrollbar(dropdownContainer.find('.dropdown-container')[0]); - activePSB.push(perfectScroll); - // Select options container optionContainer = dropdownContainer.find('.dropdown-container'); @@ -450,7 +445,6 @@ var dropdownSelector = (function() { if (dropdownContainer.hasClass('open')) { // Each time we open option container we must scroll it dropdownContainer.find('.dropdown-container').scrollTop(0); - PerfectSb().update_all(); // on Open we load new data loadData(selectElement, dropdownContainer); @@ -652,9 +646,6 @@ var dropdownSelector = (function() { $(`
${I18n.t('dropdown_selector.nothing_found')}
`).appendTo(container.find('.dropdown-container')); } - // Update scrollbar - PerfectSb().update_all(); - // Check position of option dropdown refreshDropdownSelection(selector, container); @@ -865,7 +856,6 @@ var dropdownSelector = (function() { }].concat(optionsAjax); } loadData(selector, container, optionsAjax); - PerfectSb().update_all(); }); // For local options we convert options element from select to correct array } else if (selector.data('select-by-group')) { diff --git a/app/assets/stylesheets/dashboard/recent_work.scss b/app/assets/stylesheets/dashboard/recent_work.scss index d4424e4f7..aa0ffe90c 100644 --- a/app/assets/stylesheets/dashboard/recent_work.scss +++ b/app/assets/stylesheets/dashboard/recent_work.scss @@ -8,6 +8,7 @@ .recent-work-container { height: 100%; + overflow-y: auto; padding: 0 8px; position: relative; diff --git a/app/assets/stylesheets/global_activities.scss b/app/assets/stylesheets/global_activities.scss index e3c918061..b299b6897 100644 --- a/app/assets/stylesheets/global_activities.scss +++ b/app/assets/stylesheets/global_activities.scss @@ -155,6 +155,7 @@ .activities-container { height: 100%; + overflow-y: auto; padding-top: 10px; position: absolute; width: 100%; @@ -284,6 +285,7 @@ .filters-container { height: 100%; margin-bottom: 60px; + overflow-y: auto; padding: 15px 20px; position: absolute; } diff --git a/app/assets/stylesheets/shared/dropdown_selector.scss b/app/assets/stylesheets/shared/dropdown_selector.scss index a272ab601..2fe5b69c9 100644 --- a/app/assets/stylesheets/shared/dropdown_selector.scss +++ b/app/assets/stylesheets/shared/dropdown_selector.scss @@ -135,7 +135,7 @@ bottom: calc(100% - 30px); box-shadow: $flyout-shadow; display: none; - overflow: hidden; + overflow-y: auto; position: fixed; transition: .2s; transition-property: top, bottom, box-shadow; diff --git a/app/views/dashboards/_current_tasks.html.erb b/app/views/dashboards/_current_tasks.html.erb index 85f3a36bd..984f53451 100644 --- a/app/views/dashboards/_current_tasks.html.erb +++ b/app/views/dashboards/_current_tasks.html.erb @@ -76,7 +76,7 @@
-
+
diff --git a/app/views/dashboards/_recent_work.html.erb b/app/views/dashboards/_recent_work.html.erb index 588e73025..029bf677a 100644 --- a/app/views/dashboards/_recent_work.html.erb +++ b/app/views/dashboards/_recent_work.html.erb @@ -12,7 +12,7 @@
-
+
diff --git a/app/views/global_activities/index.html.erb b/app/views/global_activities/index.html.erb index f9a264889..e79326c84 100644 --- a/app/views/global_activities/index.html.erb +++ b/app/views/global_activities/index.html.erb @@ -7,7 +7,7 @@
-
+

<%= t('activities.index.no_activities_message') %>

@@ -24,7 +24,7 @@
-
+
<%= render partial: "side_filters" %>
From e995c4978d7d4f2fa37004016e80d8a85ae07d98 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 4 Apr 2025 16:01:39 +0200 Subject: [PATCH 4/8] Task CSS fixes [SCI-11531] --- app/javascript/vue/shared/content/content_toolbar.vue | 3 ++- app/javascript/vue/shared/menu_dropdown.vue | 5 +++-- .../my_modules/step_attachments/_context_menu.html.erb | 2 +- .../my_modules/step_attachments/_thumbnail.html.erb | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/javascript/vue/shared/content/content_toolbar.vue b/app/javascript/vue/shared/content/content_toolbar.vue index a50b5c1cf..7d344ed70 100644 --- a/app/javascript/vue/shared/content/content_toolbar.vue +++ b/app/javascript/vue/shared/content/content_toolbar.vue @@ -7,12 +7,13 @@