Skip to content

Conversation

@Banibrata-Manna
Copy link
Contributor

Re Enabled the Bulk CSV Upload and added it's route in menu.

@gemini-code-assist
Copy link

Summary of Changes

Hello @Banibrata-Manna, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request re-enables the bulk CSV upload feature, providing users with a streamlined way to import inventory cycle counts. It introduces a dedicated user interface for uploading CSV files, mapping their columns to predefined application fields, and managing these mappings. The changes also include the necessary backend service integrations and state management updates to process the uploaded data and display the status of recent uploads.

Highlights

  • Bulk CSV Upload Re-enabled: The core functionality for bulk uploading inventory cycle counts via CSV files has been restored and integrated into the application.
  • Dedicated Bulk Upload Page: A new view (BulkUpload.vue) has been added to handle the entire bulk upload process, including file selection, mapping, and initiation of the upload.
  • CSV Field Mapping: New components (CreateMappingModal.vue, CycleCountUploadActionPopover.vue) and logic are introduced to allow users to define and save custom mappings between CSV columns and application fields, supporting both required and optional fields.
  • Menu and Routing Integration: A 'Bulk Upload' option has been added to the application menu, and a corresponding route (/bulkUpload) has been configured for easy access.
  • API and State Management Updates: New API services (bulkUploadInventoryCounts, fetchCycleCountImportSystemMessages) and Vuex store actions, getters, and mutations have been implemented to support the bulk upload process and track system messages.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request re-enables the bulk CSV upload feature, which is a great addition. However, the current implementation has several critical issues that must be addressed before merging. There is significant code duplication in a new component, which also seems to be incorrectly implemented for its purpose. I've also found a couple of bugs that will lead to runtime errors, related to an incorrect use of this in a Vue setup script and the possibility of committing undefined to the Vuex store. Additionally, there are several medium-severity issues concerning code quality, maintainability, and user experience that I've highlighted with suggestions for improvement.

//Todo: Generating unique identifiers as we are currently storing in local storage. Need to remove it as we will be storing data on server.
function generateUniqueMappingPrefId() {
const id = Math.floor(Math.random() * 1000);
return !fieldMappings.value[id] ? id : this.generateUniqueMappingPrefId();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The recursive call this.generateUniqueMappingPrefId() will cause a runtime error because this is undefined within the <script setup> context. You should make a direct recursive call to the function instead.

  return !fieldMappings.value[id] ? id : generateUniqueMappingPrefId();

Comment on lines +1 to +132
<template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="closeModal">
<ion-icon :icon="close" />
</ion-button>
</ion-buttons>
<ion-title>{{ translate("CSV Mapping") }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-item>
<ion-input :label="translate('Mapping name')" :placeholder="translate('Field mapping name')" v-model="mappingName" />
</ion-item>

<ion-content>
<div>
<ion-list>
<ion-item-divider>
<ion-label>{{ translate("Required") }} </ion-label>
</ion-item-divider>
<ion-item :key="field" v-for="(fieldValues, field) in getFields(fields, true)">
<ion-select :label="translate(fieldValues.label)" interface="popover" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item-divider>
<ion-label>{{ translate("Optional") }} </ion-label>
</ion-item-divider>
<ion-item :key="field" v-for="(fieldValues, field) in getFields(fields, false)">
<ion-select :label="translate(fieldValues.label)" interface="popover" :placeholder = "translate('Select')" v-model="fieldMapping[field]">
<ion-select-option :key="index" v-for="(prop, index) in fileColumns">{{ prop }}</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button @click="saveMapping">
<ion-icon :icon="saveOutline" />
</ion-fab-button>
</ion-fab>
</ion-content>
</template>

<script setup>
import {
IonButtons,
IonButton,
IonContent,
IonFab,
IonFabButton,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonItemDivider,
IonLabel,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
IonList,
modalController
} from '@ionic/vue';
import { close, save, saveOutline } from "ionicons/icons";
import { computed, defineProps, onMounted, ref } from "vue";
import { useStore } from "vuex";
import { showToast } from '@/utils';
import { translate } from "@hotwax/dxp-components";
const store = useStore();
const props = defineProps(["content", "seletedFieldMapping", "mappingType"])
const fieldMappings = computed(() => store.getters["user/getFieldMappings"])
let mappingName = ref(null)
let fieldMapping = ref ({})
let fileColumns = ref([])
let identificationTypeId = ref('SKU')
const fields = process.env["VUE_APP_MAPPING_INVCOUNT"] ? JSON.parse(process.env["VUE_APP_MAPPING_INVCOUNT"]) : {}
onMounted(() => {
fieldMapping.value = { ...props.seletedFieldMapping }
fileColumns.value = Object.keys(props.content[0]);
})
function getFields(fields, required = true) {
return Object.keys(fields).reduce((result, key) => {
if (fields[key].required === required) {
result[key] = fields[key];
}
return result;
}, {});
}
function closeModal() {
modalController.dismiss({ dismissed: true });
}
async function saveMapping() {
if(!mappingName.value || !mappingName.value.trim()) {
showToast(translate("Enter mapping name"));
return
}
if (!areAllFieldsSelected()) {
showToast(translate("Map all required fields"));
return
}
const id = generateUniqueMappingPrefId();
await store.dispatch("user/createFieldMapping", { id, name: mappingName.value, value: fieldMapping.value, mappingType: props.mappingType })
closeModal();
}
function areAllFieldsSelected() {
const requiredFields = Object.keys(getFields(fields, true));
const selectedFields = Object.keys(fieldMapping.value).filter(key => fieldMapping.value[key] !== '')
return requiredFields.every(field => selectedFields.includes(field));
}
//Todo: Generating unique identifiers as we are currently storing in local storage. Need to remove it as we will be storing data on server.
function generateUniqueMappingPrefId() {
const id = Math.floor(Math.random() * 1000);
return !fieldMappings.value[id] ? id : this.generateUniqueMappingPrefId();
}
</script>
<style scoped>
ion-content {
--padding-bottom: 80px;
}
</style> No newline at end of file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This component is an exact copy of CreateMappingModal.vue, which introduces significant code duplication. Furthermore, it's being used as a popover for actions on uploaded files in BulkUpload.vue, but its implementation is for creating a field mapping. The props it receives in BulkUpload.vue (systemMessage, fileName) do not match the props it declares (content, seletedFieldMapping, mappingType), which will lead to incorrect behavior. This component needs to be implemented correctly for its intended purpose, or removed if CreateMappingModal.vue can be reused in a more appropriate way.

Comment on lines +62 to +81
async fetchCycleCountImportSystemMessages({commit} ,payload) {
let systemMessages;
try {
const twentyFourHoursEarlier = DateTime.now().minus({ hours: 24 });
const resp = await CountService.fetchCycleCountImportSystemMessages({
systemMessageTypeId: "ImportInventoryCounts",
initDate_from: twentyFourHoursEarlier.toMillis(),
orderByField: 'initDate desc, processedDate desc',
pageSize: 100
})
if (!hasError(resp)) {
systemMessages = resp.data
} else {
throw resp.data;
}
} catch (err: any) {
logger.error(err)
}
commit(types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED, systemMessages)
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In the fetchCycleCountImportSystemMessages action, if an API error occurs, systemMessages will remain undefined. Committing undefined to the store will cause a runtime error in BulkUpload.vue when systemMessages.length is accessed. To prevent this, initialize systemMessages to an empty array. Also, the payload parameter is unused and can be removed.

Suggested change
async fetchCycleCountImportSystemMessages({commit} ,payload) {
let systemMessages;
try {
const twentyFourHoursEarlier = DateTime.now().minus({ hours: 24 });
const resp = await CountService.fetchCycleCountImportSystemMessages({
systemMessageTypeId: "ImportInventoryCounts",
initDate_from: twentyFourHoursEarlier.toMillis(),
orderByField: 'initDate desc, processedDate desc',
pageSize: 100
})
if (!hasError(resp)) {
systemMessages = resp.data
} else {
throw resp.data;
}
} catch (err: any) {
logger.error(err)
}
commit(types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED, systemMessages)
},
async fetchCycleCountImportSystemMessages({commit}) {
let systemMessages = [];
try {
const twentyFourHoursEarlier = DateTime.now().minus({ hours: 24 });
const resp = await CountService.fetchCycleCountImportSystemMessages({
systemMessageTypeId: "ImportInventoryCounts",
initDate_from: twentyFourHoursEarlier.toMillis(),
orderByField: 'initDate desc, processedDate desc',
pageSize: 100
})
if (!hasError(resp)) {
systemMessages = resp.data
} else {
throw resp.data;
}
} catch (err: any) {
logger.error(err)
}
commit(types.COUNT_IMPORT_SYSTEM_MESSAGES_UPDATED, systemMessages)
},

const store = useStore();
const props = defineProps(["content", "seletedFieldMapping", "mappingType"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a typo in the prop name seletedFieldMapping. It should be selectedFieldMapping. This typo is also present where the prop is used (e.g., line 85).

const props = defineProps(["content", "selectedFieldMapping", "mappingType"])

let mappingName = ref(null)
let fieldMapping = ref ({})
let fileColumns = ref([])
let identificationTypeId = ref('SKU')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The variable identificationTypeId is defined but never used within the component. It should be removed to improve code clarity.

});
}

const bulkUploadInventoryCounts = async (payload: any): Promise <any> => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The payload and return types for bulkUploadInventoryCounts (and fetchCycleCountImportSystemMessages on line 39) are typed as any. Using any undermines the benefits of TypeScript. Please define specific interfaces for payloads and response data to improve type safety and code maintainability.

Comment on lines +110 to +132
import { addOutline, cloudUploadOutline, ellipsisVerticalOutline, trashBinOutline } from "ionicons/icons";
import { translate } from '@/i18n';
import { computed, ref } from "vue";
import { useStore } from 'vuex';
import logger from "@/logger";
import { hasError, jsonToCsv, parseCsv, showToast } from "@/utils";
import CreateMappingModal from "@/components/CreateMappingModal.vue";
import { CountService } from "@/services/CountService"
import CycleCountUploadActionPopover from "@/components/CycleCountUploadActionPopover.vue"
const store = useStore();
const fieldMappings = computed(() => store.getters["user/getFieldMappings"])
const systemMessages = computed(() => store.getters["count/getCycleCountImportSystemMessages"])
let file = ref(null)
let uploadedFile = ref({})
let fileName = ref(null)
let content = ref([])
let fieldMapping = ref({})
let fileColumns = ref([])
let selectedMappingId = ref(null)
const fileUploaded = ref(false);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There are unused assets in this component. The icon trashBinOutline is imported on line 110 but never used. Similarly, the ref fileUploaded is defined on line 132, but its value is never read. Please remove them to keep the code clean.

Comment on lines +181 to +193
function getFileProcessingStatus(systemMessage) {
let processingStatus = "pending"
if (systemMessage.statusId === 'SmsgConsumed') {
processingStatus = "processed"
} else if (systemMessage.statusId === 'SmsgConsuming') {
processingStatus = "processing"
} else if (systemMessage.statusId === 'SmsgCancelled') {
processingStatus = 'cancelled'
} else if (systemMessage.statusId === 'SmsgError') {
processingStatus = 'error'
}
return processingStatus;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getFileProcessingStatus function can be simplified and made more readable by using a map object instead of a series of if-else if statements.

function getFileProcessingStatus(systemMessage) {
  const statusMap = {
    'SmsgConsumed': 'processed',
    'SmsgConsuming': 'processing',
    'SmsgCancelled': 'cancelled',
    'SmsgError': 'error'
  };
  return statusMap[systemMessage.statusId] || 'pending';
}

Comment on lines +221 to +224
} catch {
content.value = []
showToast(translate("Please upload a valid csv to continue"));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block in the parse function swallows the error without logging it. This makes debugging difficult if an error occurs during file parsing. It's a good practice to log the caught error.

  } catch (err) {
    content.value = []
    logger.error("Failed to parse CSV file:", err)
    showToast(translate("Please upload a valid csv to continue"));
  }

const data = jsonToCsv(uploadedData)
const formData = new FormData();
formData.append("uploadedFile", data, fileName.value);
formData.append("fileName", fileName.value.replace(".csv", ""));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using fileName.value.replace(".csv", "") to get the filename without the extension is not robust. It will only replace the first occurrence. For a file named my.file.csv.csv, it would result in my.file.csv. A better approach is to remove only the last extension.

  formData.append("fileName", fileName.value.substring(0, fileName.value.lastIndexOf('.')) || fileName.value);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant