Merge remote-tracking branch 'upstream/features/inventory-import-improvements' into SCI-10550-ik

This commit is contained in:
Ivan Kljun 2024-04-10 17:02:00 +02:00
commit 24553a0db6
17 changed files with 530 additions and 95 deletions

View file

@ -53,13 +53,21 @@ class RepositoriesController < ApplicationController
end
def show
current_team_switch(@repository.team) unless @repository.shared_with?(current_team)
@display_edit_button = can_create_repository_rows?(@repository)
@display_delete_button = can_delete_repository_rows?(@repository)
@display_duplicate_button = can_create_repository_rows?(@repository)
@snapshot_provisioning = @repository.repository_snapshots.provisioning.any?
respond_to do |format|
format.html do
current_team_switch(@repository.team) unless @repository.shared_with?(current_team)
@display_edit_button = can_create_repository_rows?(@repository)
@display_delete_button = can_delete_repository_rows?(@repository)
@display_duplicate_button = can_create_repository_rows?(@repository)
@snapshot_provisioning = @repository.repository_snapshots.provisioning.any?
@busy_printer = LabelPrinter.where.not(current_print_job_ids: []).first
@busy_printer = LabelPrinter.where.not(current_print_job_ids: []).first
end
format.json do
# render serialized repository json
render json: @repository, serializer: RepositorySerializer
end
end
end
def table_toolbar
@ -281,13 +289,9 @@ class RepositoriesController < ApplicationController
session: session
)
if parsed_file.too_large?
repository_response(t('general.file.size_exceeded',
file_size: Rails.configuration.x.file_max_size_mb))
return render json: { error: t('general.file.size_exceeded', file_size: Rails.configuration.x.file_max_size_mb) }, status: :unprocessable_entity
elsif parsed_file.has_too_many_rows?
repository_response(
t('repositories.import_records.error_message.items_limit',
items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT)
)
return render json: { error: t('repositories.import_records.error_message.items_limit', items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT) }, status: :unprocessable_entity
else
sheet = SpreadsheetParser.open_spreadsheet(import_params[:file])
duplicate_ids = SpreadsheetParser.duplicate_ids(sheet)
@ -298,22 +302,22 @@ class RepositoriesController < ApplicationController
@import_data = parsed_file.data
if @import_data.header.blank? || @import_data.columns.blank?
return repository_response(t('repositories.parse_sheet.errors.empty_file'))
return render json: { error: t('repositories.parse_sheet.errors.empty_file') }, status: :unprocessable_entity
end
if (@temp_file = parsed_file.generate_temp_file)
render json: {
html: render_to_string(partial: 'repositories/parse_records_modal', formats: :html)
import_data: @import_data,
temp_file: @temp_file
}
else
repository_response(t('repositories.parse_sheet.errors.temp_file_failure'))
return render json: { error: t('repositories.parse_sheet.errors.temp_file_failure') }, status: :unprocessable_entity
end
end
rescue ArgumentError, CSV::MalformedCSVError
repository_response(t('repositories.parse_sheet.errors.invalid_file',
encoding: ''.encoding))
return render json: { error: t('repositories.parse_sheet.errors.invalid_file', encoding: ''.encoding) }, status: :unprocessable_entity
rescue TypeError
repository_response(t('repositories.parse_sheet.errors.invalid_extension'))
return render json: { error: t('repositories.parse_sheet.errors.invalid_extension') }, status: :unprocessable_entity
end
end
@ -326,7 +330,7 @@ class RepositoriesController < ApplicationController
should_overwrite_with_empty_cells = params[:overwrite_with_empty_cells]
# Check if there exist mapping for repository record (it's mandatory)
if import_params[:mappings].value?('-1')
if import_params[:mappings].present? && import_params[:mappings].value?('-1')
import_records = repostiory_import_actions
status = import_records.import!(can_edit_existing_items, should_overwrite_with_empty_cells)
@ -365,7 +369,8 @@ class RepositoriesController < ApplicationController
row_ids: params[:row_ids],
header_ids: params[:header_ids]
},
file_type: params[:file_type]
file_type: params[:empty_export] == '1' ? 'csv' : params[:file_type],
empty_export: params[:empty_export] == '1'
)
update_user_export_file_type if current_user.settings[:repository_export_file_type] != params[:file_type]
log_activity(:export_inventory_items)

View file

@ -54,9 +54,7 @@ const app = createApp({
// Info modal
infoParams: {
title: 'Guide for updating the inventory',
modalTitle: 'Update inventory',
helpText: 'Help',
steps: [
elements: [
{
id: 'el1',
icon: 'sn-icon-export',

View file

@ -57,18 +57,17 @@ export default {
methods: {
submit() {
const payload = {
repository_ids: this.rows.map(row => row.id),
repository_ids: this.rows.map((row) => row.id),
file_type: this.selectedOption
};
axios.post(this.exportAction.path, payload).then(response => {
axios.post(this.exportAction.path, payload).then((response) => {
this.$emit('export');
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch(error => {
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
}
}
};
</script>

View file

@ -1,26 +1,91 @@
<template>
<infoModal ref="modal" :startHidden="true" :infoParams="{}" :title="'UPDATE'" :helpText="'HELP ME'">
UPDAE
</infoModal>
<InfoModal ref="modal"
:startHidden="true"
:infoParams="infoParams"
:title="steps[activeStep].title"
:helpText="steps[activeStep].helpText">
<component
:is="steps[activeStep].component"
@step:next="proceedToNext"
@step:back="activeStep -= 1"
:stepData="stepData"
/>
</InfoModal>
</template>
<script>
/* global HelperModule */
import axios from '../../../packs/custom_axios.js';
import { shallowRef } from 'vue';
import InfoModal from '../../shared/info_modal.vue';
import FirstStep from './import/first_step.vue';
export default {
name: 'ImportRepositoryModal',
components: {InfoModal},
components: { InfoModal, FirstStep },
props: {
repositoryUrl: String, required: true
},
data() {
return {
infoParams: {}
activeStep: 0,
steps: [
{
id: I18n.t('repositories.import_records.steps.step0.id'),
icon: I18n.t('repositories.import_records.steps.step0.icon'),
label: I18n.t('repositories.import_records.steps.step0.label'),
title: I18n.t('repositories.import_records.steps.step0.title'),
helpText: I18n.t('repositories.import_records.steps.step0.helpText'),
component: shallowRef(FirstStep)
}
],
infoParams: {
title: I18n.t('repositories.import_records.info_sidebar.title'),
elements: [
{
id: I18n.t('repositories.import_records.info_sidebar.elements.element0.id'),
icon: I18n.t('repositories.import_records.info_sidebar.elements.element0.icon'),
label: I18n.t('repositories.import_records.info_sidebar.elements.element0.label'),
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element0.subtext')
},
{
id: I18n.t('repositories.import_records.info_sidebar.elements.element1.id'),
icon: I18n.t('repositories.import_records.info_sidebar.elements.element1.icon'),
label: I18n.t('repositories.import_records.info_sidebar.elements.element1.label'),
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element1.subtext')
},
{
id: I18n.t('repositories.import_records.info_sidebar.elements.element2.id'),
icon: I18n.t('repositories.import_records.info_sidebar.elements.element2.icon'),
label: I18n.t('repositories.import_records.info_sidebar.elements.element2.label'),
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element2.subtext')
},
{
id: I18n.t('repositories.import_records.info_sidebar.elements.element3.id'),
icon: I18n.t('repositories.import_records.info_sidebar.elements.element3.icon'),
label: I18n.t('repositories.import_records.info_sidebar.elements.element3.label'),
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element3.subtext')
},
{
id: I18n.t('repositories.import_records.info_sidebar.elements.element4.id'),
icon: I18n.t('repositories.import_records.info_sidebar.elements.element4.icon'),
label: I18n.t('repositories.import_records.info_sidebar.elements.element4.label'),
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element4.subtext'),
linkTo: I18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')
}
]
},
stepData: null
};
},
watch: {
activeStep(newVal, oldVal) {
console.log(`${oldVal} -> ${newVal}`);
},
stepData(newVal, oldVal) {
console.log(`${oldVal} -> ${newVal}`);
}
},
created() {
window.importRepositoryModalComponent = this;
},
@ -29,6 +94,11 @@ export default {
methods: {
open() {
this.$refs.modal.open();
},
proceedToNext(data) {
console.log('incoming data', data);
this.stepData = data;
this.activeStep += 1;
}
}
};

View file

@ -0,0 +1,175 @@
<template>
<div ref="firstStep" class="flex flex-col gap-6 h-full">
<!-- body -->
<div class="flex flex-col gap-6 h-full w-full">
<!-- export -->
<div id="export-section" class="flex flex-col gap-3">
<h3 class="my-0 text-sn-dark-grey">
{{ i18n.t('repositories.import_records.steps.step0.importTitle') }}
</h3>
<div id="export-buttons" class="flex flex-row gap-4">
<button class="btn btn-secondary btn-sm" @click="exportFullInventory">
<i class="sn-icon sn-icon-export"></i>
{{ i18n.t('repositories.import_records.steps.step0.exportFullInvBtnText') }}
</button>
<button class="btn btn-secondary btn-sm">
<i class="sn-icon sn-icon-export"></i>
{{ i18n.t('repositories.import_records.steps.step0.exportEmptyInvBtnText') }}
</button>
</div>
</div>
<!-- import -->
<div id="import-section" class="flex flex-col gap-3 h-full w-full">
<h3 class="my-0 text-sn-dark-grey">
{{ i18n.t('repositories.import_records.steps.step0.importBtnText') }}
</h3>
<DragAndDropUpload
@file:dropped="uploadFile"
@file:error="handleError"
@file:error:clear="this.error = null"
:supportingText="`${i18n.t('repositories.import_records.steps.step0.dragAndDropSupportingText')}`"
:supportedFormats="['xlsx', 'csv', 'xls', 'txt', 'tsv']"
/>
</div>
</div>
<!-- divider -->
<div class="sci-divider"></div>
<!-- footer -->
<div class="flex justify-end">
<div v-if="error" class="flex flex-row gap-2 my-auto mr-auto text-sn-delete-red">
<i class="sn-icon sn-icon-alert-warning"></i>
<div class="my-auto">{{ error }}</div>
</div>
<div v-if="exportInventoryMessage" class="flex flex-row gap-2 my-auto mr-auto text-sn-alert-green">
<i class="sn-icon sn-icon-check"></i>
<div class="my-auto">{{ exportInventoryMessage }}</div>
</div>
<button class="btn btn-secondary" data-dismiss="modal" aria-label="Close">
{{ i18n.t('repositories.import_records.steps.step0.cancelBtnText') }}
</button>
</div>
</div>
</template>
<script>
import axios from '../../../../packs/custom_axios';
import DragAndDropUpload from '../../../shared/drag_and_drop_upload.vue';
export default {
name: 'FirstStep',
emits: ['step:next'],
components: {
DragAndDropUpload
},
data() {
return {
showingInfo: false,
error: null,
teamId: null,
parseSheetUrl: null,
exportInventoryMessage: null
};
},
async created() {
// Fetch repository data and set it to state
const repositoryData = await this.fetchSerializedRepositoryData();
this.teamId = String(repositoryData.data.attributes.team_id);
this.parseSheetUrl = repositoryData.data.attributes.urls.parse_sheet;
},
watch: {
// clearing export message
exportInventoryMessage(newVal, oldVal) {
if (newVal && newVal !== oldVal) {
setTimeout(() => {
this.exportInventoryMessage = null;
}, 3000);
}
}
},
methods: {
async fetchSerializedRepositoryData() {
const url = window.location.pathname;
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error(error);
}
return '';
},
async exportFullInventory() {
const exportFullInventoryUrl = `/teams/${this.teamId}/export_repositories`;
const formData = new FormData();
const repositoryIds = [this.teamId];
const fileType = 'csv';
// required payload
formData.append('repository_ids', repositoryIds);
formData.append('file_type', fileType);
try {
const response = await axios.post(exportFullInventoryUrl, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
if (!response.status === 200) {
throw new Error('Network response was not ok');
}
if (response.status === 200) {
this.exportInventoryMessage = response.data.message;
}
return response;
} catch (error) {
console.error(error);
}
return '';
},
async uploadFile(file) {
this.uploading = true;
// First, parse the sheet
const parsedSheetResponse = await this.parseSheet(file);
// If parsed successfully, go to next step and pass the necessary data
if (parsedSheetResponse) {
const {
header: columnNames,
available_fields: availableFields,
columns: exampleData
} = parsedSheetResponse.data.import_data;
this.$emit('step:next', { columnNames, availableFields, exampleData });
}
},
async parseSheet(file) {
const formData = new FormData();
// required payload
formData.append('file', file);
formData.append('team_id', this.teamId);
try {
const response = await axios.post(this.parseSheetUrl, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
if (!response.status === 200) {
throw new Error('Network response was not ok');
}
return response;
} catch (error) {
console.error(error);
}
return '';
},
handleError(error) {
this.error = error;
}
}
};
</script>

View file

@ -0,0 +1,125 @@
<template>
<div
ref="dragAndDropUpload"
@drop.prevent="dropFile"
@dragenter.prevent="dragEnter($event)"
@dragleave.prevent="dragLeave($event)"
@dragover.prevent
class="flex h-full w-full p-6 rounded border border-sn-light-grey bg-sn-super-light-blue"
>
<div id="centered-content" class="flex flex-col gap-4 items-center h-fit w-fit m-auto">
<!-- icon -->
<i class="sn-icon sn-icon-import text-sn-dark-grey"></i>
<!-- text section -->
<div class="flex flex-col gap-1">
<div class="text-sn-dark-grey">
<span class="text-sn-science-blue hover:cursor-pointer" @click="handleImportClick">
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
</div>
<div class="text-sn-grey">
{{ supportingText }}
</div>
</div>
</div>
<!-- hidden input for importing via 'Import' click -->
<input type="file" ref="fileInput" style="display: none" @change="handleFileSelect">
</div>
</template>
<script>
export default {
name: 'DragAndDropUpload',
props: {
supportingText: {
type: String,
required: true
},
supportedFormats: {
type: Array,
required: true,
default: () => []
}
},
emits: ['file:dropped', 'file:error'],
data() {
return {
draggingFile: false,
uploading: false
};
},
methods: {
validateFile(file) {
// check if it's a single file
if (file.length > 1) {
const error = I18n.t('repositories.import_records.dragAndDropUpload.notSingleFileError');
this.$emit('file:error', error);
return false;
}
// check if it's a correct file type
const fileExtension = file.name.split('.')[1];
if (!this.supportedFormats.includes(fileExtension)) {
const error = I18n.t('repositories.import_records.dragAndDropUpload.wrongFileTypeError');
this.$emit('file:error', error);
return false;
}
// check if file is not empty
if (!file.size > 0) {
const error = I18n.t('repositories.import_records.dragAndDropUpload.emptyFileError');
this.$emit('file:error', error);
return false;
}
// check if it's conforming to size limit
if (file.size > GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB * 1024 * 1024) {
const error = `${I18n.t('repositories.import_records.dragAndDropUpload.fileTooLargeError')} ${GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB}`;
this.$emit('file:error', error);
return false;
}
return true;
},
dragEnter(e) {
// Detect if dragged element is a file
// https://stackoverflow.com/a/8494918
const dt = e.dataTransfer;
if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))) {
this.draggingFile = true;
}
},
dragLeave() {
this.draggingFile = false;
},
dropFile(e) {
if (e.dataTransfer && e.dataTransfer.files.length) {
this.draggingFile = false;
this.uploading = true;
const droppedFile = e.dataTransfer.files[0];
const fileIsValid = this.validateFile(droppedFile);
// successful drop
if (fileIsValid) {
this.$emit('file:dropped', droppedFile);
this.$emit('file:error:clear');
}
}
},
handleImportClick() {
this.$refs.fileInput.click();
},
handleFileSelect(event) {
const file = event.target.files[0];
const fileIsValid = this.validateFile(file);
if (fileIsValid) {
this.$emit('file:dropped', file);
}
}
}
};
</script>

View file

@ -1,13 +1,13 @@
<template>
<div class="!w-[300px] rounded bg-sn-super-light-grey gap-4 p-6 flex flex-col h-full">
<div id="info-component-header">
<h3 class="modal-title text-sn-dark-grey">{{ params.title }}</h3>
<h3 class="modal-title text-sn-dark-grey">{{ infoParams.title }}</h3>
</div>
<div class="grid grid-flow-row h-fit" v-for="(element, _index) in params.elements" :key="element.id">
<div class="grid grid-flow-row h-fit" v-for="(element, _index) in infoParams.elements" :key="element.id">
<a v-if="element.linkTo" :href="element.linkTo" target="_blank" class="flex flex-row gap-3 w-fit text-sn-blue hover:no-underline hover:text-sn-blue-hover">
<button class="btn btn-secondary btn-sm icon-btn hover:!border-sn-light-grey">
<i :class="element.icon" class="h-fit"></i>
<i :class="`sn-icon ${element.icon}`" class="h-fit size-9"></i>
</button>
<div class="flex flex-col gap-2 w-fit">
<div class="text-sn-blue font-bold hover:text-sn-blue-hover my-auto">{{ element.label }}</div>
@ -30,7 +30,7 @@
export default {
name: 'InfoComponent',
props: {
params: {
infoParams: {
type: Object,
required: true
}

View file

@ -5,13 +5,13 @@
<div id="body-container" class="flex flex-row w-full h-full">
<!-- info -->
<div id="info-part">
<info-component
<InfoComponent
v-if="showingInfo"
:params="this.infoParams"
:infoParams="infoParams"
/>
</div>
<!-- content -->
<div id="content-part" class="flex flex-col w-full p-6 gap-3">
<div id="content-part" class="flex flex-col w-full p-6 gap-6">
<!-- header -->
<div id="info-modal-header" class="flex flex-row h-fit w-full justify-between">
<div id="title-with-help" class="flex flex-row gap-3">
@ -26,7 +26,7 @@
</button>
</div>
<!-- main content -->
<div id="info-modal-main-content">
<div id="info-modal-main-content" class="h-full">
<slot></slot>
</div>
</div>
@ -44,10 +44,12 @@ export default {
name: 'InfoModal',
props: {
title: {
type: String, required: true
type: String,
required: true
},
helpText: {
type: String, required: true
type: String,
required: true
},
infoParams: {
type: Object,
@ -59,9 +61,7 @@ export default {
}
},
mixins: [modalMixin],
components: {
'info-component': InfoComponent
},
components: { InfoComponent },
data() {
return {
showingInfo: true

View file

@ -21,5 +21,5 @@ export default {
this.$emit('open');
$(this.$refs.modal).modal('show');
}
},
}
}
};

View file

@ -35,9 +35,15 @@ class RepositoryZipExportJob < ZipExportJob
params[:header_ids].map(&:to_i),
@user,
repository,
in_module: params[:my_module_id].present?)
in_module: params[:my_module_id].present?,
empty_export: @empty_export)
exported_data = service.export!
File.binwrite("#{dir}/export.#{@file_type}", exported_data)
if @empty_export
File.binwrite("#{dir}/Export_Inventory_Empty_#{Time.now.utc.strftime('%F %H-%M-%S_UTC')}.#{@file_type}", exported_data)
else
File.binwrite("#{dir}/export.#{@file_type}", exported_data)
end
end
def failed_notification_title

View file

@ -3,9 +3,10 @@
class ZipExportJob < ApplicationJob
include FailedDeliveryNotifiableJob
def perform(user_id:, params: {}, file_type: :csv)
def perform(user_id:, params: {}, file_type: :csv, empty_export: false)
@user = User.find(user_id)
@file_type = file_type.to_sym
@empty_export = empty_export
I18n.backend.date_format = @user.settings[:date_format] || Constants::DEFAULT_DATE_FORMAT
zip_input_dir = FileUtils.mkdir_p(Rails.root.join("tmp/temp_zip_#{Time.now.to_i}").to_s).first
zip_dir = FileUtils.mkdir_p(Rails.root.join('tmp/zip-ready').to_s).first

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class RepositoryChecklistValue < ApplicationRecord
attribute :current_repository_checklist_items
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User',
inverse_of: :created_repository_checklist_values
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User',
@ -78,11 +80,18 @@ class RepositoryChecklistValue < ApplicationRecord
self.last_modified_by = user
self.repository_checklist_items = repository_cell.repository_column
.repository_checklist_items
.where(id: item_ids)
preview ? validate : save!
if preview
self.current_repository_checklist_items = repository_checklist_items
clear_current_repository_checklist_items_change
self.current_repository_checklist_items =
repository_cell.repository_column.repository_checklist_items.where(id: item_ids)
validate
else
self.repository_checklist_items = repository_cell.repository_column
.repository_checklist_items
.where(id: item_ids)
save!
end
end
def snapshot!(cell_snapshot)

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RepositorySerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :urls, :id, :team_id
def urls
{
parse_sheet: parse_sheet_repository_path(object)
}
end
end

View file

@ -3,7 +3,7 @@
require 'csv'
module RepositoryCsvExport
def self.to_csv(rows, column_ids, user, repository, handle_file_name_func, in_module)
def self.to_csv(rows, column_ids, user, repository, handle_file_name_func, in_module, empty_export)
# Parse column names
csv_header = []
add_consumption = in_module && !repository.is_a?(RepositorySnapshot) && repository.has_stock_management?
@ -34,43 +34,45 @@ module RepositoryCsvExport
CSV.generate do |csv|
csv << csv_header
rows.each do |row|
csv_row = []
column_ids.each do |c_id|
case c_id
when -1, -2
next
when -3
csv_row << (repository.is_a?(RepositorySnapshot) ? row.parent_id : row.code)
when -4
csv_row << row.name
when -5
csv_row << row.created_by.full_name
when -6
csv_row << I18n.l(row.created_at, format: :full)
when -7
csv_row << (row.archived? && row.archived_by.present? ? row.archived_by.full_name : '')
when -8
csv_row << (row.archived? && row.archived_on.present? ? I18n.l(row.archived_on, format: :full) : '')
when -9
csv_row << row.parent_repository_rows.map(&:code).join(' | ')
csv_row << row.child_repository_rows.map(&:code).join(' | ')
else
cell = row.repository_cells.find_by(repository_column_id: c_id)
unless empty_export
rows.each do |row|
csv_row = []
column_ids.each do |c_id|
case c_id
when -1, -2
next
when -3
csv_row << (repository.is_a?(RepositorySnapshot) ? row.parent_id : row.code)
when -4
csv_row << row.name
when -5
csv_row << row.created_by.full_name
when -6
csv_row << I18n.l(row.created_at, format: :full)
when -7
csv_row << (row.archived? && row.archived_by.present? ? row.archived_by.full_name : '')
when -8
csv_row << (row.archived? && row.archived_on.present? ? I18n.l(row.archived_on, format: :full) : '')
when -9
csv_row << row.parent_repository_rows.map(&:code).join(' | ')
csv_row << row.child_repository_rows.map(&:code).join(' | ')
else
cell = row.repository_cells.find_by(repository_column_id: c_id)
csv_row << if cell
if cell.value_type == 'RepositoryAssetValue' && handle_file_name_func
handle_file_name_func.call(cell.value.asset)
else
SmartAnnotations::TagToText.new(
user, repository.team, cell.value.export_formatted
).text
csv_row << if cell
if cell.value_type == 'RepositoryAssetValue' && handle_file_name_func
handle_file_name_func.call(cell.value.asset)
else
SmartAnnotations::TagToText.new(
user, repository.team, cell.value.export_formatted
).text
end
end
end
end
end
csv_row << row.row_consumption(row.stock_consumption) if add_consumption
csv << csv_row
end
csv_row << row.row_consumption(row.stock_consumption) if add_consumption
csv << csv_row
end
end.encode('UTF-8', invalid: :replace, undef: :replace)
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class RepositoryExportService
def initialize(file_type, rows, columns, user, repository, handle_name_func = nil, in_module: false)
def initialize(file_type, rows, columns, user, repository, handle_name_func = nil, in_module: false, empty_export: false)
@file_type = file_type
@user = user
@rows = rows
@ -9,12 +9,13 @@ class RepositoryExportService
@repository = repository
@handle_name_func = handle_name_func
@in_module = in_module
@empty_export = empty_export
end
def export!
case @file_type
when :csv
file_data = RepositoryCsvExport.to_csv(@rows, @columns, @user, @repository, @handle_name_func, @in_module)
file_data = RepositoryCsvExport.to_csv(@rows, @columns, @user, @repository, @handle_name_func, @in_module, @empty_export)
when :xlsx
file_data = RepositoryXlsxExport.to_xlsx(@rows, @columns, @user, @repository, @handle_name_func, @in_module)
end

View file

@ -12,13 +12,16 @@
/>
</div>
<div ref="infoModal">
<div ref="infoModalWrapper">
<button @click="showInfo = true" class="btn btn-primary">Show Info Modal</button>
<info-modal
v-if="showInfo"
@close="showInfo = false"
:infoParams="infoParams"
>
ref="modal"
:start-hidden="false"
:info-params="infoParams"
title="Some title"
help-text="Help">
<div>I am a component that gets consumed by the slot</div>
</info-modal>
</div>

View file

@ -2135,29 +2135,57 @@ en:
list_error: "%{key}: %{val}"
import_records:
update_inventory: 'Update inventory'
steps:
step0:
id: 'step0'
icon: 'sn-icon-open'
label: 'Step 1'
title: 'Update inventory'
helpText: 'Help'
exportTitle: 'Export'
exportFullInvBtnText: 'Export full inventory'
exportEmptyInvBtnText: 'Export empty inventory'
importTitle: 'Import'
importBtnText: 'Import'
cancelBtnText: 'Cancel'
dragAndDropSupportingText: '.XLSX, .XLS or .CSV file'
info_sidebar:
title: 'Guide for updating the inventory'
elements:
element0:
id: 'el0'
icon: 'sn-icon-export'
label: 'Export inventory'
subtext: "Before making edits, we advise you to export the latest inventory information. If you're only adding new items, consider exporting empty inventory."
element1:
id: 'el1'
icon: 'sn-icon-edit'
label: 'Edit your data'
subtext: 'Make sure to include header names in first row, followed by item data.'
element2:
id: 'el2'
icon: 'sn-icon-import'
label: 'Import new or update items'
subtext: 'Upload your data using .xlsx, .csv or .txt files.'
element3:
id: 'el3'
icon: 'sn-icon-tables'
label: 'Merge your data'
subtext: 'Complete the process by merging the columns you want to update.'
element4:
id: 'el4'
icon: 'sn-icon-open'
label: 'Learn more'
linkTo: 'https://knowledgebase.scinote.net/en/knowledge/how-to-add-items-to-an-inventory'
dragAndDropUpload:
notSingleFileError: 'Single file import only. Please import one file at a time.'
wrongFileTypeError: 'The file has invalid extension (.csv, .xlsx, .txt or .tsv.)'
emptyFileError: 'You have uploaded empty file. There is not much to import.'
fileTooLargeError: 'File too large. Max file size limit is'
importText:
firstPart: 'Import'
secondPart: 'or drag and drop'
import: 'Import'
no_header_name: 'No column name'
success_flash: "%{number_of_rows} of %{total_nr} new item(s) successfully imported."