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 @@
+
+
+
+
+
+
+ {{ i18n.t('storage_locations.index.move_modal.search_header') }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
{{ i18n.t(`storage_locations.show.assign_modal.row`) }}
+
+
+
+
{{ i18n.t(`storage_locations.show.assign_modal.column`) }}
+
+
+
+
+
+
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 @@
+
+
+
+
{{ i18n.t(`storage_locations.show.assign_modal.inventory`) }}
+
+
+
+
{{ i18n.t(`storage_locations.show.assign_modal.item`) }}
+
+
+
+
+
+
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