Add create/edit modal for storage locations [SCI-10860]/

This commit is contained in:
Anton 2024-07-19 13:24:23 +02:00
parent 60ce8aa425
commit d0c07d6a3d
7 changed files with 323 additions and 20 deletions

View file

@ -18,13 +18,14 @@ class StorageLocationsController < ApplicationController
end
def update
@storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id]
@storage_location.image.purge if params[:file_name].blank?
@storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id]
@storage_location.update(storage_location_params)
if @storage_location.save
render json: @storage_location, serializer: Lists::StorageLocationSerializer
else
render json: @storage_location.errors, status: :unprocessable_entity
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
end
end
@ -33,12 +34,12 @@ class StorageLocationsController < ApplicationController
storage_location_params.merge({ team: current_team, created_by: current_user })
)
@storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id]
@storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id]
if @storage_location.save
render json: @storage_location, serializer: Lists::StorageLocationSerializer
else
render json: @storage_location.errors, status: :unprocessable_entity
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
end
end
@ -63,8 +64,8 @@ class StorageLocationsController < ApplicationController
private
def storage_location_params
params.permit(:id, :parent_id, :name, :container, :signed_blob_id, :description,
metadata: { dimensions: [], parent_coordinations: [], display_type: :string })
params.permit(:id, :parent_id, :name, :container, :description,
metadata: [:display_type, dimensions: [], parent_coordinations: []])
end
def load_storage_location

View file

@ -19,7 +19,7 @@
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
</div>
<div class="text-sn-grey">
<div class="text-sn-grey text-center">
{{ supportingText }}
</div>
</div>

View file

@ -0,0 +1,223 @@
<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.index.edit_modal.title_${mode}_${editModalMode}`) }}
</h4>
</div>
<div class="modal-body">
<p v-if="mode == 'create'" class="mb-6">{{ i18n.t(`storage_locations.index.edit_modal.description_create_${editModalMode}`) }}</p>
<div class="mb-6">
<label class="sci-label">
{{ i18n.t(`storage_locations.index.edit_modal.name_label_${editModalMode}`) }}
</label>
<div class="sci-input-container-v2">
<input
type="text"
v-model="object.name"
:placeholder="i18n.t(`storage_locations.index.edit_modal.name_placeholder`)"
>
</div>
<span v-if="this.errors.name" class="text-sn-coral text-xs">{{ this.errors.name }}</span>
</div>
<div v-if="editModalMode == 'box'" class="mb-6">
<label class="sci-label">
{{ i18n.t(`storage_locations.index.edit_modal.dimensions_label`) }}
</label>
<div class="flex items-center gap-2 mb-4">
<div class="sci-radio-container">
<input type="radio" class="sci-radio" :disabled="object.code" v-model="object.metadata.display_type" name="display_type" value="no_grid" >
<span class="sci-radio-label"></span>
</div>
<span>{{ i18n.t('storage_locations.index.edit_modal.no_grid') }}</span>
<i class="sn-icon sn-icon-info text-sn-grey" :title="i18n.t('storage_locations.index.edit_modal.no_grid_tooltip')"></i>
</div>
<div class="flex items-center gap-2 mb-4">
<div class="sci-radio-container">
<input type="radio" class="sci-radio" :disabled="object.code" v-model="object.metadata.display_type" name="display_type" value="grid" >
<span class="sci-radio-label"></span>
</div>
<span>{{ i18n.t('storage_locations.index.edit_modal.grid') }}</span>
<div class="sci-input-container-v2 !w-28">
<input type="number" :disabled="object.code" v-model="object.metadata.dimensions[0]" min="1" max="24">
</div>
<i class="sn-icon sn-icon-close-small"></i>
<div class="sci-input-container-v2 !w-28">
<input type="number" :disabled="object.code" v-model="object.metadata.dimensions[1]" min="1" max="24">
</div>
</div>
</div>
<div class="mb-6">
<label class="sci-label">
{{ i18n.t(`storage_locations.index.edit_modal.image_label_${editModalMode}`) }}
</label>
<DragAndDropUpload
v-if="!attachedImage && !object.file_name"
class="h-60"
@file:dropped="addFile"
@file:error="handleError"
@file:error:clear="this.imageError = null"
:supportingText="`${i18n.t('storage_locations.index.edit_modal.drag_and_drop_supporting_text')}`"
:supportedFormats="['jpg', 'png', 'jpeg']"
/>
<div v-else class="border border-sn-light-grey rounded flex items-center p-2 gap-2">
<i class="sn-icon sn-icon-result-image text-sn-grey"></i>
<span class="text-sn-blue">{{ object.file_name || attachedImage?.name }}</span>
<i class="sn-icon sn-icon-close text-sn-blue ml-auto cursor-pointer" @click="removeImage"></i>
</div>
</div>
<div class="mb-6">
<label class="sci-label">
{{ i18n.t(`storage_locations.index.edit_modal.description_label`) }}
</label>
<div class="sci-input-container-v2 h-32">
<textarea
ref="description"
v-model="object.description"
:placeholder="i18n.t(`storage_locations.index.edit_modal.description_placeholder`)"
></textarea>
</div>
<span v-if="this.errors.description" class="text-sn-coral text-xs">{{ this.errors.description }}</span>
</div>
</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" :disabled="!validObject" type="submit">
{{ mode == 'create' ? i18n.t('general.create') : i18n.t('general.save') }}
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
/* global HelperModule SmartAnnotation ActiveStorage GLOBAL_CONSTANTS */
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
import DragAndDropUpload from '../../shared/drag_and_drop_upload.vue';
export default {
name: 'EditLocationModal',
props: {
createUrl: String,
editModalMode: String,
directUploadUrl: String,
editStorageLocation: Object
},
components: {
DragAndDropUpload
},
mixins: [modalMixin],
data() {
return {
object: {
metadata: {
dimensions: [9, 9],
display_type: 'grid'
}
},
attachedImage: null,
imageError: false,
errors: {}
};
},
computed: {
mode() {
return this.editStorageLocation ? 'edit' : 'create';
},
validObject() {
this.errors = {};
if (!this.object.name) {
return false;
}
if (this.object.name.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH) {
this.errors.name = this.i18n.t('storage_locations.index.edit_modal.errors.max_length', { max_length: GLOBAL_CONSTANTS.NAME_MAX_LENGTH });
return false;
}
if (this.object.description && this.object.description.length > GLOBAL_CONSTANTS.TEXT_MAX_LENGTH) {
this.errors.description = this.i18n.t('storage_locations.index.edit_modal.errors.max_length', { max_length: GLOBAL_CONSTANTS.NAME_MAX_LENGTH });
return false;
}
return true;
}
},
created() {
if (this.editStorageLocation) {
this.object = this.editStorageLocation;
}
},
mounted() {
SmartAnnotation.init($(this.$refs.description), false);
$(this.$refs.modal).on('hidden.bs.modal', this.handleAtWhoModalClose);
this.object.container = this.editModalMode === 'box';
},
methods: {
submit() {
if (this.attachedImage) {
this.uploadImage();
} else {
this.saveLocation();
}
},
saveLocation() {
if (this.object.code) {
axios.put(this.object.urls.update, this.object)
.then(() => {
this.$emit('tableReloaded');
HelperModule.flashAlertMsg(this.i18n.t(`storage_locations.index.edit_modal.success_message.edit_${this.editModalMode}`, { name: this.object.name }), 'success');
this.close();
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
} else {
axios.post(this.createUrl, this.object)
.then(() => {
this.$emit('tableReloaded');
HelperModule.flashAlertMsg(this.i18n.t(`storage_locations.index.edit_modal.success_message.create_${this.editModalMode}`, { name: this.object.name }), 'success');
this.close();
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
}
},
handleError() {
},
addFile(file) {
this.attachedImage = file;
},
removeImage() {
this.attachedImage = null;
this.object.file_name = null;
},
uploadImage() {
const upload = new ActiveStorage.DirectUpload(this.attachedImage, this.directUploadUrl);
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
this.object.signed_blob_id = blob.signed_id;
this.saveLocation();
}
});
},
handleAtWhoModalClose() {
$('.atwho-view.old').css('display', 'none');
}
}
};
</script>

View file

@ -6,16 +6,21 @@
:reloadingTable="reloadingTable"
:toolbarActions="toolbarActions"
:actionsUrl="actionsUrl"
@archive="archive"
@restore="restore"
@delete="deleteRepository"
@update="update"
@duplicate="duplicate"
@export="exportRepositories"
@share="share"
@create="newRepository = true"
@create_location="openCreateLocationModal"
@create_box="openCreateBoxModal"
@edit="edit"
@tableReloaded="reloadingTable = false"
/>
<Teleport to="body">
<EditModal v-if="openEditModal"
@close="openEditModal = false"
@tableReloaded="reloadingTable = true"
:createUrl="createUrl"
:editModalMode="editModalMode"
:directUploadUrl="directUploadUrl"
:editStorageLocation="editStorageLocation"
/>
</Teleport>
</div>
</template>
@ -23,11 +28,13 @@
/* global */
import DataTable from '../shared/datatable/table.vue';
import EditModal from './modals/new_edit.vue';
export default {
name: 'RepositoriesTable',
components: {
DataTable
DataTable,
EditModal
},
props: {
dataSource: {
@ -40,11 +47,17 @@ export default {
},
createUrl: {
type: String
},
directUploadUrl: {
type: String
}
},
data() {
return {
reloadingTable: false
reloadingTable: false,
openEditModal: false,
editModalMode: null,
editStorageLocation: null
};
},
computed: {
@ -126,14 +139,34 @@ export default {
}
},
methods: {
openCreateLocationModal() {
this.openEditModal = true;
this.editModalMode = 'location';
this.editStorageLocation = null;
},
openCreateBoxModal() {
this.openEditModal = true;
this.editModalMode = 'box';
this.editStorageLocation = null;
},
edit(action, params) {
this.openEditModal = true;
this.editModalMode = params[0].container ? 'box' : 'location';
[this.editStorageLocation] = params;
},
// Renderers
nameRenderer(params) {
const {
name,
urls
} = params.data;
let boxIcon = '';
if (params.data.container) {
boxIcon = '<i class="sn-icon sn-icon-item"></i>';
}
return `<a class="hover:no-underline flex items-center gap-1"
title="${name}" href="${urls.show}">
${boxIcon}
<span class="truncate">${name}</span>
</a>`;
}

View file

@ -4,12 +4,24 @@ module Lists
class StorageLocationSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on, :urls
attributes :id, :code, :name, :container, :description, :owned_by, :created_by,
:created_on, :urls, :metadata, :file_name
def owned_by
object.team.name
end
def metadata
{
display_type: object.metadata['display_type'],
dimensions: object.metadata['dimensions'] || []
}
end
def file_name
object.image.filename if object.image.attached?
end
def created_by
object.created_by.full_name
end
@ -19,8 +31,14 @@ module Lists
end
def urls
show_url = if @object.container
storage_location_path(@object)
else
storage_locations_path(parent_id: object.id)
end
{
show: storage_locations_path(parent_id: object.id),
show: show_url,
update: storage_location_path(@object)
}
end
end

View file

@ -13,7 +13,8 @@
<storage-locations
actions-url="<%= actions_toolbar_storage_locations_path(current_team) %>"
data-source="<%= storage_locations_path(format: :json, parent_id: params[:parent_id]) %>"
create-url="<%= storage_locations_path if can_create_storage_locations?(current_team) %>"
direct-upload-url="<%= rails_direct_uploads_url %>"
create-url="<%= storage_locations_path(parent_id: params[:parent_id]) if can_create_storage_locations?(current_team) %>"
/>
</div>
</div>

View file

@ -2687,6 +2687,33 @@ en:
owned_by: "Owned by"
created_on: "Created on"
description: "Description"
edit_modal:
title_create_location: "Create new location"
title_create_box: "Create new box"
title_edit_location: "Edit location"
title_edit_box: "Edit box"
description_create_location: "Fill in the fields and create a new location."
description_create_box: "Fill in the fields to create a new box. Defining the box dimensions allows you to control the number of available spaces for placing inventory items."
name_label_location: "Location name"
image_label_location: "Image of location"
name_label_box: "Box name"
image_label_box: "Image of box"
drag_and_drop_supporting_text: ".png or .jpg file"
description_label: "Description"
name_placeholder: "Big freezer"
description_placeholder: "Keep everyone on the same page. You can also use smart annotations."
dimensions_label: "Dimensions (rows x columns)"
no_grid: "No grid"
grid: "Grid"
no_grid_tooltip: "You can assign unlimited items to the “No-grid” box but they do not have assigned position."
success_message:
create_location: "Location %{name} was successfully created."
create_box: "Box %{name} was successfully created."
edit_location: "Location %{name} was successfully updated."
edit_box: "Box %{name} was successfully updated."
errors:
max_length: "is too long (maximum is %{max_length} characters)"
libraries:
manange_modal_column_index:
title: "Manage columns"