mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-09 13:28:53 +08:00
Merge pull request #7614 from aignatov-bio/ai-sci-9851-add-import-preview
Add preview step and success step [SCI-9851]
This commit is contained in:
commit
161cb698d9
13 changed files with 272 additions and 16 deletions
|
@ -227,6 +227,11 @@ GEM
|
||||||
mail
|
mail
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
|
caxlsx (4.0.0)
|
||||||
|
htmlentities (~> 4.3, >= 4.3.4)
|
||||||
|
marcel (~> 1.0)
|
||||||
|
nokogiri (~> 1.10, >= 1.10.4)
|
||||||
|
rubyzip (>= 1.3.0, < 3)
|
||||||
cgi (0.4.1)
|
cgi (0.4.1)
|
||||||
childprocess (4.1.0)
|
childprocess (4.1.0)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
|
@ -353,6 +358,7 @@ GEM
|
||||||
nokogiri (~> 1.0)
|
nokogiri (~> 1.0)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
|
htmlentities (4.3.4)
|
||||||
http (5.1.1)
|
http (5.1.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
|
@ -731,13 +737,13 @@ GEM
|
||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.4.2)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
|
uri (0.13.0)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
activemodel (>= 3.0)
|
activemodel (>= 3.0)
|
||||||
mail (>= 2.2.5)
|
mail (>= 2.2.5)
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
public_suffix
|
public_suffix
|
||||||
uri (0.13.0)
|
|
||||||
version_gem (1.1.3)
|
version_gem (1.1.3)
|
||||||
view_component (3.9.0)
|
view_component (3.9.0)
|
||||||
activesupport (>= 5.2.0, < 8.0)
|
activesupport (>= 5.2.0, < 8.0)
|
||||||
|
@ -795,6 +801,7 @@ DEPENDENCIES
|
||||||
capybara
|
capybara
|
||||||
capybara-email
|
capybara-email
|
||||||
caracal!
|
caracal!
|
||||||
|
caxlsx
|
||||||
cssbundling-rails
|
cssbundling-rails
|
||||||
cucumber-rails
|
cucumber-rails
|
||||||
database_cleaner
|
database_cleaner
|
||||||
|
|
|
@ -6,20 +6,31 @@
|
||||||
:uploading="uploading"
|
:uploading="uploading"
|
||||||
@uploadFile="uploadFile"
|
@uploadFile="uploadFile"
|
||||||
@generatePreview="generatePreview"
|
@generatePreview="generatePreview"
|
||||||
|
@changeStep="changeStep"
|
||||||
|
@importRows="importRecords"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global HelperModule */
|
/* global HelperModule */
|
||||||
|
|
||||||
import axios from '../../../../packs/custom_axios';
|
import axios from '../../../../packs/custom_axios';
|
||||||
import InfoModal from '../../../shared/info_modal.vue';
|
import InfoModal from '../../../shared/info_modal.vue';
|
||||||
import UploadStep from './upload_step.vue';
|
import UploadStep from './upload_step.vue';
|
||||||
import MappingStep from './mapping_step.vue';
|
import MappingStep from './mapping_step.vue';
|
||||||
|
import PreviewStep from './preview_step.vue';
|
||||||
|
import SuccessStep from './success_step.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ImportRepositoryModal',
|
name: 'ImportRepositoryModal',
|
||||||
components: { InfoModal, UploadStep, MappingStep },
|
components: {
|
||||||
|
InfoModal,
|
||||||
|
UploadStep,
|
||||||
|
MappingStep,
|
||||||
|
PreviewStep,
|
||||||
|
SuccessStep
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
repositoryUrl: String,
|
repositoryUrl: String,
|
||||||
required: true
|
required: true
|
||||||
|
@ -70,8 +81,10 @@ export default {
|
||||||
this.params.onlyAddNewItems = onlyAddNewItems;
|
this.params.onlyAddNewItems = onlyAddNewItems;
|
||||||
this.importRecords(true);
|
this.importRecords(true);
|
||||||
},
|
},
|
||||||
|
changeStep(step) {
|
||||||
importRecords(preview = false) {
|
this.activeStep = step;
|
||||||
|
},
|
||||||
|
importRecords(preview) {
|
||||||
const jsonData = {
|
const jsonData = {
|
||||||
file_id: this.params.temp_file.id,
|
file_id: this.params.temp_file.id,
|
||||||
mappings: this.params.mapping,
|
mappings: this.params.mapping,
|
||||||
|
@ -80,7 +93,16 @@ export default {
|
||||||
should_overwrite_with_empty_cells: this.params.updateWithEmptyCells,
|
should_overwrite_with_empty_cells: this.params.updateWithEmptyCells,
|
||||||
can_edit_existing_items: !this.params.onlyAddNewItems
|
can_edit_existing_items: !this.params.onlyAddNewItems
|
||||||
};
|
};
|
||||||
axios.post(this.params.attributes.urls.import_records, jsonData);
|
axios.post(this.params.attributes.urls.import_records, jsonData)
|
||||||
|
.then((response) => {
|
||||||
|
if (preview) {
|
||||||
|
this.params.preview = response.data.changes;
|
||||||
|
this.params.import_date = response.data.import_date;
|
||||||
|
this.activeStep = 'PreviewStep';
|
||||||
|
} else {
|
||||||
|
this.activeStep = 'SuccessStep';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,7 +52,6 @@
|
||||||
:params="params"
|
:params="params"
|
||||||
:selected="this.selectedItemsIndexes.includes(index)"
|
:selected="this.selectedItemsIndexes.includes(index)"
|
||||||
@selection:changed="handleChange"
|
@selection:changed="handleChange"
|
||||||
:availableFields="this.availableFields"
|
|
||||||
:autoMapping="this.autoMapping"
|
:autoMapping="this.autoMapping"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -94,7 +93,7 @@ import modalMixin from '../../../shared/modal_mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MappingStep',
|
name: 'MappingStep',
|
||||||
emits: ['step:next'],
|
emits: ['close', 'generatePreview'],
|
||||||
mixins: [modalMixin],
|
mixins: [modalMixin],
|
||||||
components: {
|
components: {
|
||||||
SelectDropdown,
|
SelectDropdown,
|
||||||
|
@ -104,7 +103,7 @@ export default {
|
||||||
params: {
|
params: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
v-else
|
v-else
|
||||||
:options="dropdownOptions"
|
:options="dropdownOptions"
|
||||||
@change="changeSelected"
|
@change="changeSelected"
|
||||||
@isOpen="handleIsOpen"
|
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:size="'sm'"
|
:size="'sm'"
|
||||||
:placeholder="computeMatchNotFound ?
|
:placeholder="computeMatchNotFound ?
|
||||||
|
|
147
app/javascript/vue/repositories/modals/import/preview_step.vue
Normal file
147
app/javascript/vue/repositories/modals/import/preview_step.vue
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<template>
|
||||||
|
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content grow">
|
||||||
|
<div class="modal-header gap-4">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
|
||||||
|
<i class="sn-icon sn-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title truncate !block" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step3.title') }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-sn-dark-grey mb-6">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step3.subtitle', { inventory: params.attributes.name }) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center justify-between text-sn-dark-gray text-sm">
|
||||||
|
<div>
|
||||||
|
<div v-html="i18n.t('repositories.import_records.steps.step3.updated_items')"></div>
|
||||||
|
<hr class="my-1">
|
||||||
|
<h2 class="m-0 text-sn-alert-green">0</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-html="i18n.t('repositories.import_records.steps.step3.new_items')"></div>
|
||||||
|
<hr class="my-1">
|
||||||
|
<h2 class="m-0 text-sn-alert-green">0</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-html="i18n.t('repositories.import_records.steps.step3.unchanged_items')"></div>
|
||||||
|
<hr class="my-1">
|
||||||
|
<h2 class="m-0 ">0</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-html="i18n.t('repositories.import_records.steps.step3.duplicated_items')"></div>
|
||||||
|
<hr class="my-1">
|
||||||
|
<h2 class="m-0 ">0</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-html="i18n.t('repositories.import_records.steps.step3.invalid_items')"></div>
|
||||||
|
<hr class="my-1">
|
||||||
|
<h2 class="m-0 text-sn-alert-passion">0</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-html="i18n.t('repositories.import_records.steps.step3.invalid_items')"></div>
|
||||||
|
<hr class="my-1">
|
||||||
|
<h2 class="m-0 text-sn-alert-passion">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-6">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step2.importedFileText') }} {{ params.file_name }}
|
||||||
|
</div>
|
||||||
|
<div class="h-80">
|
||||||
|
<ag-grid-vue
|
||||||
|
class="ag-theme-alpine w-full flex-grow h-full z-10"
|
||||||
|
:columnDefs="columnDefs"
|
||||||
|
:defaultColDef="{
|
||||||
|
resizable: true,
|
||||||
|
sortable: false,
|
||||||
|
suppressMovable: true
|
||||||
|
}"
|
||||||
|
:rowData="tableData"
|
||||||
|
:suppressRowTransform="true"
|
||||||
|
:suppressRowClickSelection="true"
|
||||||
|
:enableCellTextSelection="true"
|
||||||
|
></ag-grid-vue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="$emit('changeStep', 'MappingStep')">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step3.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="$emit('importRows')">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step3.confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { AgGridVue } from 'ag-grid-vue3';
|
||||||
|
import modalMixin from '../../../shared/modal_mixin';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PreviewStep',
|
||||||
|
mixins: [modalMixin],
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
AgGridVue
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
columnDefs() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
headerName: this.i18n.t('repositories.import_records.steps.step3.code')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
headerName: this.i18n.t('repositories.import_records.steps.step3.name')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.params.attributes.repository_columns.forEach((col) => {
|
||||||
|
columns.push({
|
||||||
|
field: `col_${col[0]}`,
|
||||||
|
headerName: col[1]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
columns.push({
|
||||||
|
field: 'status',
|
||||||
|
headerName: this.i18n.t('repositories.import_records.steps.step3.status'),
|
||||||
|
pinned: 'right'
|
||||||
|
});
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
tableData() {
|
||||||
|
const data = this.params.preview.data.map((row) => {
|
||||||
|
const rowFormated = row.attributes;
|
||||||
|
row.relationships.repository_cells.data.forEach((c) => {
|
||||||
|
const cell = this.params.preview.included.find((c1) => c1.id === c.id);
|
||||||
|
if (cell) {
|
||||||
|
rowFormated[`col_${cell.attributes.repository_column_id}`] = cell.attributes.formatted_value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return rowFormated;
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-sm" role="document">
|
||||||
|
<div class="modal-content grow">
|
||||||
|
<div class="modal-header gap-4">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
|
||||||
|
<i class="sn-icon sn-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title truncate !block" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step4.title') }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-sn-dark-grey mb-6">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step4.subtitle', { inventory: params.attributes.name }) }}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<b>{{ i18n.t('repositories.import_records.steps.step4.import_date') }}</b>
|
||||||
|
{{ params.import_date }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>{{ i18n.t('repositories.import_records.steps.step4.imported_file') }}</b>
|
||||||
|
{{ params.file_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" >
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step4.download_report') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="close">
|
||||||
|
{{ i18n.t('repositories.import_records.steps.step4.close') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import modalMixin from '../../../shared/modal_mixin';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SuccessStep',
|
||||||
|
mixins: [modalMixin],
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -91,7 +91,7 @@ import modalMixin from '../../../shared/modal_mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UploadStep',
|
name: 'UploadStep',
|
||||||
emits: ['step:next', 'info:hide'],
|
emits: ['uploadFile'],
|
||||||
components: {
|
components: {
|
||||||
DragAndDropUpload
|
DragAndDropUpload
|
||||||
},
|
},
|
||||||
|
@ -123,6 +123,9 @@ export default {
|
||||||
},
|
},
|
||||||
uploadFile(file) {
|
uploadFile(file) {
|
||||||
this.$emit('uploadFile', file);
|
this.$emit('uploadFile', file);
|
||||||
|
},
|
||||||
|
handleError(error) {
|
||||||
|
this.error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class RepositoryCellSerializer < ActiveModel::Serializer
|
class RepositoryCellSerializer < ActiveModel::Serializer
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
attributes :id, :value, :changes
|
attributes :id, :value, :changes, :repository_column_id, :formatted_value
|
||||||
|
|
||||||
def changes
|
def changes
|
||||||
object.value.changes
|
object.value.changes
|
||||||
|
@ -12,4 +12,8 @@ class RepositoryCellSerializer < ActiveModel::Serializer
|
||||||
def value
|
def value
|
||||||
object.value
|
object.value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def formatted_value
|
||||||
|
object.value.formatted
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,5 +4,6 @@ class RepositoryRowSerializer < ActiveModel::Serializer
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
attributes :id, :name, :code
|
attributes :id, :name, :code
|
||||||
has_many :repository_cells
|
|
||||||
|
has_many :repository_cells, serializer: RepositoryCellSerializer
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class RepositorySerializer < ActiveModel::Serializer
|
class RepositorySerializer < ActiveModel::Serializer
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
attributes :urls, :id, :team_id, :repository_columns
|
attributes :urls, :id, :team_id, :repository_columns, :name
|
||||||
|
|
||||||
def repository_columns
|
def repository_columns
|
||||||
object.repository_columns.pluck(:id, :name, :data_type)
|
object.repository_columns.pluck(:id, :name, :data_type)
|
||||||
|
|
|
@ -10,7 +10,7 @@ module ImportRepository
|
||||||
|
|
||||||
def import!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
def import!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
status = run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
status = run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
@temp_file.destroy
|
#@temp_file.destroy
|
||||||
status
|
status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -166,9 +166,9 @@ module RepositoryImportParser
|
||||||
imported_rows,
|
imported_rows,
|
||||||
each_serializer: RepositoryRowSerializer,
|
each_serializer: RepositoryRowSerializer,
|
||||||
include: [:repository_cells]
|
include: [:repository_cells]
|
||||||
).as_json[:included]
|
).as_json
|
||||||
|
|
||||||
{ status: :ok, nr_of_added: @new_rows_added, total_nr: @total_new_rows, changes: changes }
|
{ status: :ok, nr_of_added: @new_rows_added, total_nr: @total_new_rows, changes: changes, import_date: I18n.l(Date.today, format: :full_date) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
def import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
|
|
|
@ -2264,6 +2264,27 @@ en:
|
||||||
scinoteColumns: 'SciNote columns'
|
scinoteColumns: 'SciNote columns'
|
||||||
status: 'Status'
|
status: 'Status'
|
||||||
exampleData: 'Example data'
|
exampleData: 'Example data'
|
||||||
|
step3:
|
||||||
|
title: 'Import preview'
|
||||||
|
subtitle: 'This is a preview of items you are importing/updating to the %{inventory}. The import can still be canceled.'
|
||||||
|
updated_items: 'Updated<br>items'
|
||||||
|
new_items: 'New<br>items'
|
||||||
|
unchanged_items: 'Unchanged<br>items'
|
||||||
|
duplicated_items: 'Duplicated<br>items'
|
||||||
|
invalid_items: 'Invalid<br>items'
|
||||||
|
invalid_cells: 'Invalid<br>cells'
|
||||||
|
code: 'Code'
|
||||||
|
name: 'Name'
|
||||||
|
status: 'Status'
|
||||||
|
cancel: 'Cancel import'
|
||||||
|
confirm: 'Confirm'
|
||||||
|
step4:
|
||||||
|
title: 'Success report'
|
||||||
|
subtitle: '%{inventory} was successfully updated.'
|
||||||
|
import_date: 'Import date:'
|
||||||
|
imported_file: 'Imported file:'
|
||||||
|
download_report: 'Download success report'
|
||||||
|
close: 'Close'
|
||||||
info_sidebar:
|
info_sidebar:
|
||||||
title: 'Guide for updating the inventory'
|
title: 'Guide for updating the inventory'
|
||||||
elements:
|
elements:
|
||||||
|
|
Loading…
Add table
Reference in a new issue