mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-06 11:57:16 +08:00
Merge pull request #8225 from aignatov-bio/ai-sci-11535-allow-multiple-selecting-in-storage-locations
Enable multiple selection in storage locations grid [SCI-11535]
This commit is contained in:
commit
f74e65d311
5 changed files with 187 additions and 54 deletions
|
@ -19,22 +19,46 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@storage_location_repository_row = StorageLocationRepositoryRow.new(
|
||||
repository_row: @repository_row,
|
||||
storage_location: @storage_location,
|
||||
metadata: storage_location_repository_row_params[:metadata] || {},
|
||||
created_by: current_user
|
||||
)
|
||||
storage_location_repository_rows = []
|
||||
|
||||
@storage_location_repository_row.with_lock do
|
||||
if @storage_location_repository_row.save
|
||||
log_activity(:storage_location_repository_row_created)
|
||||
render json: @storage_location_repository_row,
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||
else
|
||||
render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity
|
||||
if @storage_location.with_grid?
|
||||
params[:positions].each do |position|
|
||||
if position.dig(2, :occupied)
|
||||
occupied_storage_location_repository_row = @storage_location.storage_location_repository_rows.find_by(id: position.dig(2, :id))
|
||||
raise ActiveRecord::RecordInvalid, occupied_row unless discard_storage_location_repository_rows(occupied_storage_location_repository_row)
|
||||
end
|
||||
storage_location_repository_row = StorageLocationRepositoryRow.new(
|
||||
repository_row: @repository_row,
|
||||
storage_location: @storage_location,
|
||||
metadata: { position: position[0..1] },
|
||||
created_by: current_user
|
||||
)
|
||||
storage_location_repository_row.with_lock do
|
||||
storage_location_repository_row.save!
|
||||
storage_location_repository_rows << storage_location_repository_row
|
||||
end
|
||||
end
|
||||
else
|
||||
storage_location_repository_row = StorageLocationRepositoryRow.new(
|
||||
repository_row: @repository_row,
|
||||
storage_location: @storage_location,
|
||||
created_by: current_user
|
||||
)
|
||||
storage_location_repository_row.with_lock do
|
||||
storage_location_repository_row.save!
|
||||
storage_location_repository_rows << storage_location_repository_row
|
||||
end
|
||||
end
|
||||
|
||||
log_activity(:storage_location_repository_row_created, {
|
||||
repository_row: @repository_row.id,
|
||||
position: storage_location_repository_rows.map(&:human_readable_position).join(', ')
|
||||
})
|
||||
|
||||
render json: storage_location_repository_rows, each_serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -43,7 +67,10 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
@storage_location_repository_row.update(storage_location_repository_row_params)
|
||||
|
||||
if @storage_location_repository_row.save
|
||||
log_activity(:storage_location_repository_row_moved)
|
||||
log_activity(:storage_location_repository_row_moved, {
|
||||
repository_row: @storage_location_repository_row.repository_row_id,
|
||||
position: @storage_location_repository_row.human_readable_position
|
||||
})
|
||||
render json: @storage_location_repository_row,
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||
else
|
||||
|
@ -58,17 +85,26 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
@original_position = @storage_location_repository_row.human_readable_position
|
||||
|
||||
@storage_location_repository_row.discard
|
||||
|
||||
metadata = if @storage_location.with_grid?
|
||||
{ position: params[:positions][0][0..1] } # For now, we only support moving one row at a time
|
||||
else
|
||||
{}
|
||||
end
|
||||
|
||||
@storage_location_repository_row = StorageLocationRepositoryRow.create!(
|
||||
repository_row: @repository_row,
|
||||
storage_location: @storage_location,
|
||||
metadata: storage_location_repository_row_params[:metadata] || {},
|
||||
metadata: metadata,
|
||||
created_by: current_user
|
||||
)
|
||||
log_activity(
|
||||
:storage_location_repository_row_moved,
|
||||
{
|
||||
storage_location_original: @original_storage_location.id,
|
||||
position_original: @original_position
|
||||
position_original: @original_position,
|
||||
repository_row: @storage_location_repository_row.repository_row_id,
|
||||
position: @storage_location_repository_row.human_readable_position
|
||||
}
|
||||
)
|
||||
render json: @storage_location_repository_row,
|
||||
|
@ -81,8 +117,7 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
if @storage_location_repository_row.discard
|
||||
log_activity(:storage_location_repository_row_deleted)
|
||||
if discard_storage_location_repository_rows(@storage_location_repository_row)
|
||||
render json: {}
|
||||
else
|
||||
render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity
|
||||
|
@ -101,6 +136,18 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def discard_storage_location_repository_rows(storage_location_repository_row)
|
||||
if storage_location_repository_row.discard
|
||||
log_activity(:storage_location_repository_row_deleted, {
|
||||
repository_row: storage_location_repository_row.repository_row_id,
|
||||
position: storage_location_repository_row.human_readable_position
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def check_storage_locations_enabled
|
||||
render_403 unless StorageLocation.storage_locations_enabled?
|
||||
end
|
||||
|
@ -142,11 +189,9 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
team: @storage_location.team,
|
||||
subject: @storage_location_repository_row.storage_location,
|
||||
subject: @storage_location,
|
||||
message_items: {
|
||||
storage_location: @storage_location_repository_row.storage_location_id,
|
||||
repository_row: @storage_location_repository_row.repository_row_id,
|
||||
position: @storage_location_repository_row.human_readable_position,
|
||||
storage_location: @storage_location.id,
|
||||
user: current_user.id
|
||||
}.merge(message_items))
|
||||
end
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
:gridSize="gridSize"
|
||||
:assignedItems="assignedItems"
|
||||
:selectedItems="selectedItems"
|
||||
@assign="assignRowToPosition"
|
||||
:selectedEmptyCells="selectedEmptyCells"
|
||||
@selectEmptyCell="selectEmptyCell"
|
||||
@select="selectRow"
|
||||
/>
|
||||
</div>
|
||||
|
@ -32,11 +33,12 @@
|
|||
v-if="openAssignModal"
|
||||
:assignMode="assignMode"
|
||||
:selectedContainer="assignToContainer"
|
||||
:selectedPosition="assignToPosition"
|
||||
:selectedPositions="assignToPositions"
|
||||
:selectedRow="rowIdToMove"
|
||||
:selectedRowName="rowNameToMove"
|
||||
:cellId="cellIdToUnassign"
|
||||
@close="openAssignModal = false; resetTableSearch(); this.reloadingTable = true"
|
||||
@assign="assignCallback"
|
||||
@close="openAssignModal = false"
|
||||
></AssignModal>
|
||||
<ImportModal
|
||||
v-if="openImportModal"
|
||||
|
@ -112,8 +114,8 @@ export default {
|
|||
moveToUrl: null,
|
||||
assignedItems: [],
|
||||
selectedItems: [],
|
||||
selectedEmptyCells: [],
|
||||
openAssignModal: false,
|
||||
assignToPosition: null,
|
||||
assignToContainer: null,
|
||||
rowIdToMove: null,
|
||||
rowNameToMove: null,
|
||||
|
@ -126,6 +128,14 @@ export default {
|
|||
paginationMode() {
|
||||
return this.withGrid ? 'none' : 'pages';
|
||||
},
|
||||
assignToPositions() {
|
||||
if (this.assignMode === 'assign' && this.withGrid) {
|
||||
return this.selectedEmptyCells.map((cell) => [cell.row + 1, cell.column + 1, { occupied: false }]).concat(
|
||||
this.selectedItems.map((item) => [item.position[0], item.position[1], { occupied: true, id: item.id }])
|
||||
);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
tableId() {
|
||||
return this.withGrid ? 'StorageLocationsContainerGrid' : 'StorageLocationsContainer';
|
||||
},
|
||||
|
@ -218,32 +228,37 @@ export default {
|
|||
}
|
||||
this.$refs.table.restoreSelection();
|
||||
},
|
||||
selectEmptyCell(cell) {
|
||||
if (this.selectedEmptyCells.find((c) => c.row === cell.row && c.column === cell.column)) {
|
||||
this.selectedEmptyCells = this.selectedEmptyCells.filter((c) => c.row !== cell.row || c.column !== cell.column);
|
||||
} else {
|
||||
this.selectedEmptyCells.push(cell);
|
||||
}
|
||||
},
|
||||
assignRow() {
|
||||
this.openAssignModal = true;
|
||||
this.rowIdToMove = null;
|
||||
this.rowNameToMove = null;
|
||||
this.assignToContainer = this.containerId;
|
||||
this.assignToPosition = null;
|
||||
this.assignToPositions = [];
|
||||
this.cellIdToUnassign = null;
|
||||
this.assignMode = 'assign';
|
||||
},
|
||||
assignRowToPosition(position) {
|
||||
this.openAssignModal = true;
|
||||
this.rowIdToMove = null;
|
||||
this.rowNameToMove = null;
|
||||
this.assignToContainer = this.containerId;
|
||||
this.assignToPosition = position;
|
||||
this.cellIdToUnassign = null;
|
||||
this.assignMode = 'assign';
|
||||
assignCallback() {
|
||||
this.openAssignModal = false;
|
||||
this.resetTableSearch();
|
||||
this.reloadingTable = true;
|
||||
this.selectedEmptyCells = [];
|
||||
},
|
||||
moveRow(_event, data) {
|
||||
this.assignMode = 'move';
|
||||
this.openAssignModal = true;
|
||||
this.rowIdToMove = data[0].row_id;
|
||||
this.rowNameToMove = data[0].row_name || this.i18n.t('storage_locations.show.hidden');
|
||||
this.assignToContainer = null;
|
||||
this.assignToPosition = null;
|
||||
this.cellIdToUnassign = data[0].id;
|
||||
this.assignMode = 'move';
|
||||
|
||||
},
|
||||
async unassignRows(event, rows) {
|
||||
this.storageLocationUnassignDescription = this.i18n.t(
|
||||
|
@ -255,7 +270,6 @@ export default {
|
|||
axios.post(event.path).then(() => {
|
||||
this.resetTableSearch();
|
||||
this.reloadingTable = true;
|
||||
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
<div class="z-10 bg-sn-super-light-grey"></div>
|
||||
<div ref="columnsContainer" class="overflow-x-hidden">
|
||||
<div :style="{'width': `${columnsList.length * 54}px`}">
|
||||
<div v-for="column in columnsList" :key="column" class="uppercase float-left flex items-center justify-center w-[54px] ">
|
||||
<div v-for="column in columnsList" :key="column" @click="selectColumn(column)" class=" cursor-pointer uppercase float-left flex items-center justify-center w-[54px] ">
|
||||
<span>{{ column }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="rowContainer" class="overflow-y-hidden max-h-[70vh]">
|
||||
<div v-for="row in rowsList" :key="row" class="uppercase flex items-center justify-center h-[54px]">
|
||||
<div v-for="row in rowsList" :key="row" @click="selectRow(row)" class="cursor-pointer uppercase flex items-center justify-center h-[54px]">
|
||||
<span>{{ row }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
|||
>
|
||||
<div
|
||||
class="h-full w-full rounded-full items-center flex justify-center"
|
||||
@click="assignRow(cell)"
|
||||
@click="selectPosition(cell)"
|
||||
:class="{
|
||||
'bg-sn-background-green': cellIsOccupied(cell),
|
||||
'bg-sn-grey-100': cellIsHidden(cell),
|
||||
|
@ -64,6 +64,10 @@ export default {
|
|||
selectedItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedEmptyCells: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -108,12 +112,13 @@ export default {
|
|||
return this.cellObject(cell)?.hidden;
|
||||
},
|
||||
cellIsSelected(cell) {
|
||||
return this.selectedItems.some((item) => item.position[0] === cell.row + 1 && item.position[1] === cell.column + 1);
|
||||
return this.selectedItems.some((item) => item.position[0] === cell.row + 1 && item.position[1] === cell.column + 1)
|
||||
|| this.selectedEmptyCells.some((selectedCell) => selectedCell.row === cell.row && selectedCell.column === cell.column);
|
||||
},
|
||||
cellIsAvailable(cell) {
|
||||
return !this.cellIsOccupied(cell) && !this.cellIsHidden(cell);
|
||||
},
|
||||
assignRow(cell) {
|
||||
selectPosition(cell) {
|
||||
if (this.cellIsOccupied(cell)) {
|
||||
this.$emit('select', this.cellObject(cell));
|
||||
return;
|
||||
|
@ -123,7 +128,23 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.$emit('assign', [cell.row + 1, cell.column + 1]);
|
||||
this.$emit('selectEmptyCell', cell);
|
||||
},
|
||||
selectRow(row) {
|
||||
this.columnsList.forEach((column) => {
|
||||
const cell = { row: this.rowsList.indexOf(row), column: column - 1 };
|
||||
if (!this.cellIsSelected(cell) && !this.cellIsOccupied(cell)) {
|
||||
this.$emit('selectEmptyCell', cell);
|
||||
}
|
||||
});
|
||||
},
|
||||
selectColumn(column) {
|
||||
this.rowsList.forEach((row) => {
|
||||
const cell = { row: this.rowsList.indexOf(row), column: column - 1 };
|
||||
if (!this.cellIsSelected(cell) && !this.cellIsOccupied(cell)) {
|
||||
this.$emit('selectEmptyCell', cell);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleScroll() {
|
||||
this.$refs.columnsContainer.scrollLeft = this.$refs.cellsContainer.scrollLeft;
|
||||
|
|
|
@ -2,12 +2,36 @@
|
|||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="modal-content">
|
||||
<div v-if="overrideWarning" 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 v-if="selectedPosition" class="modal-title truncate !block">
|
||||
<h4 class="modal-title truncate !block">
|
||||
{{ i18n.t(`storage_locations.show.assign_modal.override.title`) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-html="i18n.t(`storage_locations.show.assign_modal.override.p_1_html`, {count: this.selectedPositions.filter((p) => p[2].occupied).length})"></p>
|
||||
<p>
|
||||
{{ i18n.t(`storage_locations.show.assign_modal.override.p_2`) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="removeOverride = true; submit()">
|
||||
{{ i18n.t(`storage_locations.show.assign_modal.override.skip`) }}
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="confirmOverride = true; submit()">
|
||||
{{ i18n.t(`storage_locations.show.assign_modal.override.cta`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else 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 v-if="selectedPositions.length > 0" class="modal-title truncate !block">
|
||||
{{ i18n.t(`storage_locations.show.assign_modal.selected_position_title`, { position: formattedPosition }) }}
|
||||
</h4>
|
||||
<h4 v-else-if="assignMode === 'assign' && selectedRow && selectedRowName" class="modal-title truncate !block">
|
||||
|
@ -33,10 +57,10 @@
|
|||
<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 && containerId > 0 && !selectedPosition"
|
||||
v-if="containerId && containerId > 0 && !(selectedPositions.length > 0)"
|
||||
:key="containerId"
|
||||
:selectedContainerId="containerId"
|
||||
@change="this.position = $event"></PositionSelector>
|
||||
@change="this.positions = [$event]"></PositionSelector>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
|
@ -71,11 +95,17 @@ export default {
|
|||
selectedRowName: String,
|
||||
selectedContainer: Number,
|
||||
cellId: Number,
|
||||
selectedPosition: Array,
|
||||
selectedPositions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
assignMode: String
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
computed: {
|
||||
showOverrideWarning() {
|
||||
return this.selectedPositions.find((p) => p[2].occupied);
|
||||
},
|
||||
validObject() {
|
||||
return this.rowId && this.containerId && this.containerId > 0;
|
||||
},
|
||||
|
@ -85,8 +115,12 @@ export default {
|
|||
});
|
||||
},
|
||||
formattedPosition() {
|
||||
if (this.selectedPosition) {
|
||||
return String.fromCharCode(96 + parseInt(this.selectedPosition[0], 10)).toUpperCase() + this.selectedPosition[1];
|
||||
if (this.selectedPositions.length > 0) {
|
||||
const pos = [];
|
||||
this.selectedPositions.forEach((p) => {
|
||||
pos.push(String.fromCharCode(96 + parseInt(p[0], 10)).toUpperCase() + p[1]);
|
||||
});
|
||||
return pos.join(', ');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
@ -101,8 +135,11 @@ export default {
|
|||
return {
|
||||
rowId: this.selectedRow,
|
||||
containerId: this.selectedContainer,
|
||||
position: this.selectedPosition,
|
||||
saving: false
|
||||
positions: this.selectedPositions,
|
||||
saving: false,
|
||||
overrideWarning: false,
|
||||
confirmOverride: false,
|
||||
removeOverride: false
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
@ -112,16 +149,26 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
submit() {
|
||||
if (this.showOverrideWarning && (!this.confirmOverride && !this.removeOverride)) {
|
||||
this.overrideWarning = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
|
||||
if (this.removeOverride) {
|
||||
this.positions = this.positions.filter((p) => !p[2].occupied);
|
||||
}
|
||||
|
||||
axios.post(this.actionUrl, {
|
||||
repository_row_id: this.rowId,
|
||||
metadata: { position: this.position?.map((pos) => parseInt(pos, 10)) }
|
||||
positions: this.positions.map((p) => [parseInt(p[0], 10), parseInt(p[1], 10), p[2]])
|
||||
}).then(() => {
|
||||
this.$emit('assign');
|
||||
this.$emit('close');
|
||||
this.saving = false;
|
||||
}).catch((error) => {
|
||||
|
|
|
@ -2818,7 +2818,13 @@ en:
|
|||
description_single: 'Are you sure you want to remove item from this location?'
|
||||
button: 'Unassign'
|
||||
assign_modal:
|
||||
selected_position_title: 'Assign to position %{position}'
|
||||
override:
|
||||
title: 'Override location'
|
||||
p_1_html: "You are about to override <b>%{count}</b> occupied locations. This action can't be undone."
|
||||
p_2: 'Are you sure you want to confirm this override?'
|
||||
skip: 'Skip occupied locations'
|
||||
cta: 'Override'
|
||||
selected_position_title: 'Assign to positions %{position}'
|
||||
selected_row_title: 'Assign new location'
|
||||
assign_title: 'Assign position'
|
||||
move_title: 'Move %{name}'
|
||||
|
|
Loading…
Add table
Reference in a new issue