From 2da397cc7636a8d8d51aba196dbe0ab146291bce Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 29 Jul 2024 15:41:40 +0200 Subject: [PATCH] Add assign/unassign modal and move modal [SCI-10870] --- .gitignore | 4 + Gemfile | 1 + Gemfile.lock | 3 + Rakefile | 2 + app/controllers/repositories_controller.rb | 18 +++- ...age_location_repository_rows_controller.rb | 29 ++++-- .../storage_locations_controller.rb | 18 +++- .../vue/storage_locations/container.vue | 83 +++++++++++++++- app/javascript/vue/storage_locations/grid.vue | 9 +- .../vue/storage_locations/modals/assign.vue | 99 +++++++++++++++++++ .../modals/assign/container_selector.vue | 43 ++++++++ .../modals/assign/position_selector.vue | 77 +++++++++++++++ .../modals/assign/row_selector.vue | 69 +++++++++++++ .../vue/storage_locations/modals/move.vue | 35 +------ .../storage_locations/modals/move_tree.vue | 2 +- .../modals/move_tree_mixin.js | 50 ++++++++++ .../vue/storage_locations/table.vue | 5 +- app/models/storage_location.rb | 18 ++++ ...torage_location_repository_rows_service.rb | 6 +- app/views/storage_locations/index.html.erb | 1 - app/views/storage_locations/show.html.erb | 1 + config/environments/development.rb | 4 + config/initializers/js_routes.rb | 7 ++ config/locales/en.yml | 15 +++ config/routes.rb | 5 +- 25 files changed, 541 insertions(+), 63 deletions(-) create mode 100644 app/javascript/vue/storage_locations/modals/assign.vue create mode 100644 app/javascript/vue/storage_locations/modals/assign/container_selector.vue create mode 100644 app/javascript/vue/storage_locations/modals/assign/position_selector.vue create mode 100644 app/javascript/vue/storage_locations/modals/assign/row_selector.vue create mode 100644 app/javascript/vue/storage_locations/modals/move_tree_mixin.js create mode 100644 config/initializers/js_routes.rb diff --git a/.gitignore b/.gitignore index a16f03f0c..4e459907b 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,7 @@ public/marvin4js-license.cxl /app/assets/builds/* !/app/assets/builds/.keep + +# Ignore automatically generated js-routes files. +/app/javascript/routes.js +/app/javascript/routes.d.ts diff --git a/Gemfile b/Gemfile index 31fc95950..9d1063a5f 100644 --- a/Gemfile +++ b/Gemfile @@ -94,6 +94,7 @@ gem 'graphviz' gem 'cssbundling-rails' gem 'jsbundling-rails' +gem 'js-routes' gem 'tailwindcss-rails', '~> 2.4' diff --git a/Gemfile.lock b/Gemfile.lock index deeb09c01..481cfbbf6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -386,6 +386,8 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + js-routes (2.2.8) + railties (>= 4) jsbundling-rails (1.1.1) railties (>= 6.0.0) json (2.6.3) @@ -826,6 +828,7 @@ DEPENDENCIES image_processing img2zpl! jbuilder + js-routes jsbundling-rails json-jwt json_matchers diff --git a/Rakefile b/Rakefile index 9f98587c3..c172d03df 100644 --- a/Rakefile +++ b/Rakefile @@ -5,3 +5,5 @@ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks Doorkeeper::Rake.load_tasks +# Update js-routes file before javascript build +task 'javascript:build' => 'js:routes:typescript' diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 29fa820c3..25aff0c0c 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -10,14 +10,14 @@ class RepositoriesController < ApplicationController include MyModulesHelper before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar - export_modal export_repositories) - before_action :load_repositories, only: :index + export_modal export_repositories list) + before_action :load_repositories, only: %i(index list) before_action :load_repositories_for_archiving, only: :archive before_action :load_repositories_for_restoring, only: :restore - before_action :check_view_all_permissions, only: %i(index sidebar) + before_action :check_view_all_permissions, only: %i(index sidebar list) before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet import_records sidebar archive restore actions_toolbar - export_modal export_repositories) + export_modal export_repositories list) before_action :check_manage_permissions, only: %i(rename_modal update) before_action :check_delete_permissions, only: %i(destroy destroy_modal) before_action :check_archive_permissions, only: %i(archive restore) @@ -44,6 +44,16 @@ class RepositoriesController < ApplicationController end end + def list + results = @repositories + results = results.name_like(params[:query]) if params[:query].present? + render json: { data: results.map { |r| [r.id, r.name] } } + end + + def rows_list + render json: { data: @repository.repository_rows.map { |r| [r.id, r.name] } } + end + def sidebar render json: { html: render_to_string(partial: 'repositories/sidebar', locals: { diff --git a/app/controllers/storage_location_repository_rows_controller.rb b/app/controllers/storage_location_repository_rows_controller.rb index ab15e6c1d..052585c28 100644 --- a/app/controllers/storage_location_repository_rows_controller.rb +++ b/app/controllers/storage_location_repository_rows_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class StorageLocationRepositoryRowsController < ApplicationController - before_action :load_storage_location_repository_row, only: %i(update destroy) + before_action :load_storage_location_repository_row, only: %i(update destroy move) before_action :load_storage_location - before_action :load_repository_row, only: %i(create update destroy) + before_action :load_repository_row, only: %i(create update destroy move) before_action :check_read_permissions, except: %i(create actions_toolbar) before_action :check_manage_permissions, only: %i(create update destroy) @@ -13,7 +13,6 @@ class StorageLocationRepositoryRowsController < ApplicationController ).call render json: storage_location_repository_row, each_serializer: Lists::StorageLocationRepositoryRowSerializer, - include: %i(repository_row), meta: (pagination_dict(storage_location_repository_row) unless @storage_location.with_grid?) end @@ -22,8 +21,7 @@ class StorageLocationRepositoryRowsController < ApplicationController if @storage_location_repository_row.save render json: @storage_location_repository_row, - serializer: Lists::StorageLocationRepositoryRowSerializer, - include: :repository_row + serializer: Lists::StorageLocationRepositoryRowSerializer else render json: @storage_location_repository_row.errors, status: :unprocessable_entity end @@ -39,13 +37,30 @@ class StorageLocationRepositoryRowsController < ApplicationController if @storage_location_repository_row.save render json: @storage_location_repository_row, - serializer: Lists::StorageLocationRepositoryRowSerializer, - include: :repository_row + serializer: Lists::StorageLocationRepositoryRowSerializer else render json: @storage_location_repository_row.errors, status: :unprocessable_entity end end + def move + ActiveRecord::Base.transaction do + @storage_location_repository_row.discard + @storage_location_repository_row = StorageLocationRepositoryRow.create!( + repository_row: @repository_row, + storage_location: @storage_location, + metadata: storage_location_repository_row_params[:metadata] || {}, + created_by: current_user + ) + + render json: @storage_location_repository_row, + serializer: Lists::StorageLocationRepositoryRowSerializer + rescue ActiveRecord::RecordInvalid => e + render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + end + def destroy if @storage_location_repository_row.discard render json: {} diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 5532b7b0d..e5940e455 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StorageLocationsController < ApplicationController - before_action :load_storage_location, only: %i(update destroy duplicate move show) + before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows) before_action :check_read_permissions, except: %i(index create tree actions_toolbar) before_action :check_create_permissions, only: :create before_action :check_manage_permissions, only: %i(update destroy duplicate move) @@ -81,10 +81,20 @@ class StorageLocationsController < ApplicationController end def tree - records = current_team.storage_locations.where(parent: nil, container: false) + records = current_team.storage_locations.where(parent: nil, container: [false, params[:container] == 'true']) render json: storage_locations_recursive_builder(records) end + def available_positions + render json: { positions: @storage_location.available_positions } + end + + def unassign_rows + @storage_location.storage_location_repository_rows.where(id: params[:ids]).discard_all + + render json: { status: :ok } + end + def actions_toolbar render json: { actions: @@ -172,7 +182,9 @@ class StorageLocationsController < ApplicationController storage_locations.map do |storage_location| { storage_location: storage_location, - children: storage_locations_recursive_builder(storage_location.storage_locations.where(container: false)) + children: storage_locations_recursive_builder( + storage_location.storage_locations.where(container: [false, params[:container] == 'true']) + ) } end end diff --git a/app/javascript/vue/storage_locations/container.vue b/app/javascript/vue/storage_locations/container.vue index 53e5b970f..9f56d4236 100644 --- a/app/javascript/vue/storage_locations/container.vue +++ b/app/javascript/vue/storage_locations/container.vue @@ -3,13 +3,13 @@
-
- +
+ + + + @@ -32,12 +54,16 @@ import axios from '../../packs/custom_axios.js'; import DataTable from '../shared/datatable/table.vue'; import Grid from './grid.vue'; +import AssignModal from './modals/assign.vue'; +import ConfirmationModal from '../shared/confirmation_modal.vue'; export default { name: 'StorageLocationsContainer', components: { DataTable, - Grid + Grid, + AssignModal, + ConfirmationModal }, props: { dataSource: { @@ -52,6 +78,10 @@ export default { type: Boolean, default: false }, + containerId: { + type: Number, + default: null + }, gridSize: Array }, data() { @@ -62,7 +92,14 @@ export default { editStorageLocation: null, objectToMove: null, moveToUrl: null, - assignedItems: [] + assignedItems: [], + openAssignModal: false, + assignToPosition: null, + assignToContainer: null, + rowIdToMove: null, + cellIdToUnassign: null, + assignMode: 'assign', + storageLocationUnassignDescription: '' }; }, computed: { @@ -123,6 +160,44 @@ export default { handleTableReload(items) { this.reloadingTable = false; this.assignedItems = items; + }, + assignRow() { + this.openAssignModal = true; + this.rowIdToMove = null; + this.assignToContainer = this.containerId; + this.assignToPosition = null; + this.cellIdToUnassign = null; + this.assignMode = 'assign'; + }, + assignRowToPosition(position) { + this.openAssignModal = true; + this.rowIdToMove = null; + this.assignToContainer = this.containerId; + this.assignToPosition = position; + this.cellIdToUnassign = null; + this.assignMode = 'assign'; + }, + moveRow(_event, data) { + this.openAssignModal = true; + this.rowIdToMove = data[0].row_id; + this.assignToContainer = null; + this.assignToPosition = null; + this.cellIdToUnassign = data[0].id; + this.assignMode = 'move'; + }, + async unassignRows(event, rows) { + this.storageLocationUnassignDescription = this.i18n.t( + 'storage_locations.show.unassign_modal.description', + { items: rows.length } + ); + const ok = await this.$refs.unassignStorageLocationModal.show(); + if (ok) { + axios.post(event.path).then(() => { + this.reloadingTable = true; + }).catch((error) => { + HelperModule.flashAlertMsg(error.response.data.error, 'danger'); + }); + } } } }; diff --git a/app/javascript/vue/storage_locations/grid.vue b/app/javascript/vue/storage_locations/grid.vue index 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