Add assign/unassign modal and move modal [SCI-10870]

This commit is contained in:
Anton 2024-07-29 15:41:40 +02:00
parent 66d620b5da
commit 2da397cc76
25 changed files with 541 additions and 63 deletions

4
.gitignore vendored
View file

@ -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

View file

@ -94,6 +94,7 @@ gem 'graphviz'
gem 'cssbundling-rails'
gem 'jsbundling-rails'
gem 'js-routes'
gem 'tailwindcss-rails', '~> 2.4'

View file

@ -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

View file

@ -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'

View file

@ -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: {

View file

@ -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: {}

View file

@ -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

View file

@ -3,13 +3,13 @@
<div v-if="withGrid">
<div class="py-4">
<div class="h-11">
<button class="btn btn-primary">
<button class="btn btn-primary" @click="assignRow">
<i class="sn-icon sn-icon-new-task"></i>
{{ i18n.t('storage_locations.show.toolbar.assign') }}
</button>
</div>
</div>
<Grid :gridSize="gridSize" :assignedItems="assignedItems" />
<Grid :gridSize="gridSize" :assignedItems="assignedItems" @assign="assignRowToPosition"/>
</div>
<div class="h-full bg-white p-4">
<DataTable :columnDefs="columnDefs"
@ -20,9 +20,31 @@
:toolbarActions="toolbarActions"
:actionsUrl="actionsUrl"
:scrollMode="paginationMode"
@assign="assignRow"
@move="moveRow"
@unassign="unassignRows"
@tableReloaded="handleTableReload"
/>
</div>
<Teleport to="body">
<AssignModal
v-if="openAssignModal"
:assignMode="assignMode"
:selectedContainer="assignToContainer"
:selectedPosition="assignToPosition"
:selectedRow="rowIdToMove"
:cellId="cellIdToUnassign"
:withGrid="withGrid"
@close="openAssignModal = false; this.reloadingTable = true"
></AssignModal>
<ConfirmationModal
:title="i18n.t('storage_locations.show.unassign_modal.title')"
:description="storageLocationUnassignDescription"
confirmClass="btn btn-danger"
:confirmText="i18n.t('storage_locations.show.unassign_modal.button')"
ref="unassignStorageLocationModal"
></ConfirmationModal>
</Teleport>
</div>
</template>
@ -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');
});
}
}
}
};

View file

@ -25,9 +25,10 @@
>
<div
class="h-full w-full rounded-full items-center flex justify-center"
@click="assignRow(cell.row, cell.column)"
:class="{
'bg-sn-background-green': cellIsOccupied(cell.row, cell.column),
'bg-white': !cellIsOccupied(cell.row, cell.column)
'bg-white cursor-pointer': !cellIsOccupied(cell.row, cell.column)
}"
>
{{ 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;

View file

@ -0,0 +1,99 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<form @submit.prevent="submit">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_title`) }}
</h4>
</div>
<div class="modal-body">
<p class="mb-4">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_description`) }}
</p>
<RowSelector v-if="!selectedRow" @change="this.rowId = $event" class="mb-4"></RowSelector>
<ContainerSelector v-if="!selectedContainer" @change="this.containerId = $event"></ContainerSelector>
<PositionSelector
v-if="containerId && !selectedPosition && withGrid"
:key="containerId"
:selectedContainerId="containerId"
@change="this.position = $event"></PositionSelector>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" type="submit">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_action`) }}
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
/* global HelperModule */
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
import RowSelector from './assign/row_selector.vue';
import ContainerSelector from './assign/container_selector.vue';
import PositionSelector from './assign/position_selector.vue';
import {
storage_location_storage_location_repository_rows_path,
move_storage_location_storage_location_repository_row_path,
} from '../../../routes.js';
export default {
name: 'NewProjectModal',
props: {
selectedRow: Number,
selectedContainer: Number,
cellId: Number,
selectedPosition: Array,
withGrid: Boolean,
assignMode: String
},
mixins: [modalMixin],
computed: {
createUrl() {
return storage_location_storage_location_repository_rows_path({
storage_location_id: this.containerId
});
},
moveUrl() {
return move_storage_location_storage_location_repository_row_path(this.containerId, this.cellId);
},
actionUrl() {
return this.assignMode === 'assign' ? this.createUrl : this.moveUrl;
}
},
data() {
return {
rowId: this.selectedRow,
containerId: this.selectedContainer,
position: this.selectedPosition
};
},
components: {
RowSelector,
ContainerSelector,
PositionSelector
},
methods: {
submit() {
axios.post(this.actionUrl, {
repository_row_id: this.rowId,
metadata: { position: this.position?.map((pos) => parseInt(pos, 10)) }
}).then(() => {
this.$emit('close');
});
}
}
};
</script>

View file

@ -0,0 +1,43 @@
<template>
<div>
<div class="mb-4">
<div class="sci-input-container-v2 left-icon">
<input type="text"
v-model="query"
class="sci-input-field"
ref="input"
autofocus="true"
:placeholder=" i18n.t('storage_locations.index.move_modal.placeholder.find_storage_locations')" />
<i class="sn-icon sn-icon-search"></i>
</div>
</div>
<div class="max-h-80 overflow-y-auto">
<div class="p-2 flex items-center gap-2 cursor-pointer text-sn-blue hover:bg-sn-super-light-grey"
@click="selectStorageLocation(null)"
:class="{'!bg-sn-super-light-blue': selectedStorageLocationId == null}">
<i class="sn-icon sn-icon-projects"></i>
{{ i18n.t('storage_locations.index.move_modal.search_header') }}
</div>
<MoveTree :storageLocationTrees="filteredStorageLocationTree" :value="selectedStorageLocationId" @selectStorageLocation="selectStorageLocation" />
</div>
</div>
</template>
<script>
import MoveTreeMixin from '../move_tree_mixin';
export default {
name: 'ContainerSelector',
mixins: [MoveTreeMixin],
data() {
return {
container: true
};
},
watch: {
selectedStorageLocationId() {
this.$emit('change', this.selectedStorageLocationId);
}
}
};
</script>

View file

@ -0,0 +1,77 @@
<template>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="">
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.row`) }}</div>
<SelectDropdown
:options="availableRows"
:value="selectedRow"
@change="selectedRow = $event"
></SelectDropdown>
</div>
<div>
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.column`) }}</div>
<SelectDropdown
:disabled="!selectedRow"
:options="availableColumns"
:value="selectedColumn"
@change="selectedColumn= $event"
></SelectDropdown>
</div>
</div>
</template>
<script>
import SelectDropdown from '../../../shared/select_dropdown.vue';
import axios from '../../../../packs/custom_axios.js';
import {
available_positions_storage_location_path,
} from '../../../../routes.js';
export default {
name: 'PositionSelector',
components: {
SelectDropdown
},
props: {
selectedContainerId: Number
},
created() {
axios.get(this.positionsUrl)
.then((response) => {
this.availablePositions = response.data.positions;
this.$nextTick(() => {
[[this.selectedRow]] = this.availableRows;
this.$nextTick(() => {
[[this.selectedColumn]] = this.availableColumns;
});
});
});
},
watch: {
selectedRow() {
[[this.selectedColumn]] = this.availableColumns;
},
selectedColumn() {
this.$emit('change', [this.selectedRow, this.selectedColumn]);
}
},
computed: {
positionsUrl() {
return available_positions_storage_location_path(this.selectedContainerId);
},
availableRows() {
return Object.keys(this.availablePositions).map((row) => [row, row]);
},
availableColumns() {
return (this.availablePositions[this.selectedRow] || []).map((col) => [col, col]);
}
},
data() {
return {
availablePositions: {},
selectedRow: null,
selectedColumn: null
};
}
};
</script>

View file

@ -0,0 +1,69 @@
<template>
<div>
<div class="mb-4">
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.inventory`) }}</div>
<SelectDropdown
:optionsUrl="repositoriesUrl"
placeholder="Select inventory"
:searchable="true"
@change="selectedRepository = $event"
></SelectDropdown>
</div>
<div>
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.item`) }}</div>
<SelectDropdown
:disabled="!selectedRepository"
:optionsUrl="rowsUrl"
:urlParams="{ repository_id: selectedRepository }"
placeholder="Select item"
:searchable="true"
@change="selectedRow= $event"
></SelectDropdown>
</div>
</div>
</template>
<script>
import SelectDropdown from '../../../shared/select_dropdown.vue';
import {
list_team_repositories_path,
rows_list_team_repositories_path
} from '../../../../routes.js';
export default {
name: 'RowSelector',
components: {
SelectDropdown
},
created() {
this.teamId = document.body.dataset.currentTeamId;
},
watch: {
selectedRepository() {
this.selectedRow = null;
},
selectedRow() {
this.$emit('change', this.selectedRow);
}
},
computed: {
repositoriesUrl() {
return list_team_repositories_path(this.teamId);
},
rowsUrl() {
if (!this.selectedRepository) {
return null;
}
return rows_list_team_repositories_path(this.teamId);
}
},
data() {
return {
selectedRepository: null,
selectedRow: null,
teamId: null
};
}
};
</script>

View file

@ -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'

View file

@ -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}">
<i class="sn-icon sn-icon-folder"></i>
<i v-if="storageLocationTree.storage_location.container" class="sn-icon sn-icon-item"></i>
<div class="flex-1 truncate p-2 pl-0" :title="storageLocationTree.storage_location.name">
{{ storageLocationTree.storage_location.name }}
</div>

View file

@ -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;
}
}
};

View file

@ -24,7 +24,7 @@
:editStorageLocation="editStorageLocation"
/>
<MoveModal v-if="objectToMove" :moveToUrl="moveToUrl"
:selectedObject="objectToMove" :storageLocationTreeUrl="storageLocationTreeUrl"
:selectedObject="objectToMove"
@close="objectToMove = null" @move="updateTable()" />
<ConfirmationModal
:title="storageLocationDeleteTitle"
@ -68,9 +68,6 @@ export default {
},
directUploadUrl: {
type: String
},
storageLocationTreeUrl: {
type: String
}
},
data() {

View file

@ -47,6 +47,24 @@ class StorageLocation < ApplicationRecord
metadata['dimensions'] if with_grid?
end
def available_positions
return unless with_grid?
occupied_positions = storage_location_repository_rows.pluck(:metadata).map { |metadata| metadata['position'] }
rows = {}
grid_size[0].times do |row|
rows_cells = []
grid_size[1].times.filter_map do |col|
rows_cells.push(col + 1) if occupied_positions.exclude?([row + 1, col + 1])
end
rows[row + 1] = rows_cells unless rows_cells.empty?
end
rows
end
private
def recursive_duplicate(old_parent_id = nil, new_parent_id = nil)

View file

@ -28,12 +28,10 @@ module Toolbars
def unassign_action
{
name: 'edit',
name: 'unassign',
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)
),
path: unassign_rows_storage_location_path(@storage_location, ids: @assigned_rows.pluck(:id)),
type: :emit
}
end

View file

@ -15,7 +15,6 @@
data-source="<%= storage_locations_path(format: :json, parent_id: params[:parent_id]) %>"
direct-upload-url="<%= rails_direct_uploads_url %>"
create-url="<%= storage_locations_path(parent_id: params[:parent_id]) if can_create_storage_locations?(current_team) %>"
storage-location-tree-url="<%= tree_storage_locations_path %>"
/>
</div>
</div>

View file

@ -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 %>"
/>
</div>
</div>

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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