From ddee05e60f4a0795841e742766f9e62eb153ec02 Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:20:06 -0600 Subject: [PATCH 01/13] Update ConvertCSVToRecords LWC component - Fix critical bugs: * Fix setter name mismatch: delimiterToGuess -> delimitersToGuess * Fix columnHeaders property usage (use @track instead of private) * Fix ignoreMissingFields property reference * Remove misleading console.log statements * Fix typos: Defualt -> Default, medta -> meta - Add JSDoc documentation to all methods - Remove excessive console.log statements (25+ debug logs removed) - Update API versions from 55.0 to 65.0 - Add SVG icon for component (lwcConvertCSVToRecords.svg) - Update masterLabel and description - Add safety checks for PapaParse library loading - Improve error handling and user feedback --- .../LwcConvertCSVToRecordsHelper.cls-meta.xml | 2 +- ...onvertCSVToRecordsHelper_Test.cls-meta.xml | 2 +- .../lwcConvertCSVToRecords.js | 133 +++-- .../lwcConvertCSVToRecords.js-meta.xml | 8 +- .../lwcConvertCSVToRecords.svg | 31 ++ .../lwcConvertCSVToRecordsCPE.js | 507 ++++++++++++------ .../lwcConvertCSVToRecordsCPE.js-meta.xml | 2 +- .../ConvertCSVToRecords_LWC/sfdx-project.json | 2 +- 8 files changed, 466 insertions(+), 221 deletions(-) create mode 100644 flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper.cls-meta.xml b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper.cls-meta.xml index 4b0bc9f38..82775b98b 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper.cls-meta.xml +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 65.0 Active diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper_Test.cls-meta.xml b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper_Test.cls-meta.xml index 4b0bc9f38..82775b98b 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper_Test.cls-meta.xml +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/classes/LwcConvertCSVToRecordsHelper_Test.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 65.0 Active diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js index f63fb8dec..24c856acb 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js @@ -1,3 +1,28 @@ +/** + * Lightning Web Component for Flow Screens: lwcConvertCSVToRecords + * + * A high-volume CSV to Records conversion component that processes CSV files + * on the client side using PapaParse library. This component allows users to + * upload CSV files and automatically converts them to Salesforce sObject records. + * + * Features: + * - Client-side CSV parsing (no server-side processing limits) + * - Automatic field mapping (standard and custom fields) + * - Support for large file uploads + * - Auto-navigation to next screen after parsing + * - Comprehensive error handling + * - Configurable PapaParse options + * + * Created By: Andy Haas + * + * Version History: + * V1.0.1 1/14/23 Fixed setting the selected object in CPE + * V1.0 12/29/22 Initial version hosted on FlowComponents + * + * @see https://unofficialsf.com/from-andy-haas-a-high-volume-convert-csv-to-records-screen-component/ + * @see https://www.papaparse.com/docs + */ + import { LightningElement, track, api } from 'lwc'; import { FlowNavigationFinishEvent, @@ -112,7 +137,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { @api get delimitersToGuess() { return this._delimitersToGuess; } - set delimiterToGuess(value) { + set delimitersToGuess(value) { this._delimitersToGuess = value; } _delimitersToGuess = []; @@ -218,23 +243,53 @@ export default class lwcConvertCSVToRecords extends LightningElement { _ignoreMissingFields = false; - // Initialize the parser + /** + * Lifecycle hook: Initializes the PapaParse library when component is rendered + * @returns {void} + */ renderedCallback() { if(!this.parserInitialized){ loadScript(this, PARSER) .then(() => { - this.parserInitialized = true; + // Verify PapaParse is available + if (typeof Papa !== 'undefined') { + this.parserInitialized = true; + } else { + this._errorMessage = 'PapaParse library failed to load. Please refresh the page.'; + this._isError = true; + console.error('PapaParse library not available after loading'); + } }) - .catch(error => console.error(error)); + .catch(error => { + this._errorMessage = 'Failed to load PapaParse library: ' + error.message; + this._isError = true; + console.error('PapaParse loading error:', error); + }); } } + /** + * Handles file input change event and processes the uploaded CSV file + * Parses the CSV using PapaParse, maps columns to Salesforce fields, + * and converts data to sObject format + * + * @param {Event} event - File input change event containing the uploaded file + * @returns {void} + */ handleInputChange(event){ - // Set Defualt Values + // Set Default Values this.header = true; this.skipEmptyLines = true; if(event.detail.files.length > 0){ + // Ensure PapaParse is loaded before proceeding + if (!this.parserInitialized || typeof Papa === 'undefined') { + this._errorMessage = 'PapaParse library is not loaded. Please refresh the page and try again.'; + this._isError = true; + this._isLoading = false; + return; + } + this._isLoading = true; const file = event.detail.files[0]; this.loading = true; @@ -253,11 +308,8 @@ export default class lwcConvertCSVToRecords extends LightningElement { transform: this._transform, delimitersToGuess: this._delimitersToGuess, complete: (parsedResults) => { - console.log('results: ' + JSON.stringify(parsedResults)); - - // get the medta columns - this._columnHeaders = parsedResults.meta.fields; - console.log('columnHeaders: ' + JSON.stringify(this._columnHeaders)); + // get the meta columns + this.columnHeaders = parsedResults.meta.fields; // See if there are any empty columns let emptyColumns = parsedResults.meta.fields.filter(field => field === ''); @@ -268,30 +320,21 @@ export default class lwcConvertCSVToRecords extends LightningElement { this._isError = true; this._errorMessage = 'There are empty columns in the CSV file. Please remove the empty columns and try again.'; this._isLoading = false; - console.log('There are empty columns in the CSV file. Please remove the empty columns and try again.'); return; } else if (emptyColumns.length > 0 && this._ignoreMissingColumns) { // If there are empty columns, but the user wants to ignore them, remove the empty columns parsedResults.meta.fields = parsedResults.meta.fields.filter(field => field !== ''); // Set the columnHeaders variable to the new columnHeaders array - this._columnHeaders = parsedResults.meta.fields; + this.columnHeaders = parsedResults.meta.fields; // Remove the empty columns from the data parsedResults.data = parsedResults.data.map(row => { return row.filter(field => field !== ''); }); - - console.log('User wants to ignore empty columns. Removing empty columns from CSV file.'); } - - - - console.log('objectName: ' + this.objectName); - getObjectFields({objectName: this.objectName}) .then(fieldList => { - console.log('fieldList: ' + JSON.stringify(fieldList)); // fieldList is an array of objects // Each object has a Name and a Type property @@ -308,14 +351,12 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Compare the column headers to the fields for the selected object // If the column header is not a match add __c to the end and recheck the fields // If the column header is still not a match, remove the column header from the list - for (let i = 0; i < this._columnHeaders.length; i++) { - let columnHeader = this._columnHeaders[i]; + for (let i = 0; i < this.columnHeaders.length; i++) { + let columnHeader = this.columnHeaders[i]; // Trim the column header columnHeader = columnHeader.trim(); - console.log('columnHeader: ' + columnHeader); - // For standard fields we need to remove the space inbetween the words // For example: Account Name becomes AccountName // Create standardField variable to store the new value @@ -326,7 +367,6 @@ export default class lwcConvertCSVToRecords extends LightningElement { standardField = columnHeader; } if (fieldNames.includes(standardField)) { - console.log('standard field: ' + columnHeader); newColumnHeaders.push({"newField":columnHeader, "oldField":columnHeader}); } else { @@ -368,29 +408,22 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Validate the field name if (fieldNames.includes(customField)) { - console.log('custom field: ' + customField); newColumnHeaders.push({"newField":customField, "oldField":columnHeader}); } else { - console.log('removed field: ' + columnHeader); fieldsToRemove.push(columnHeader); } } } - console.log('newColumnHeaders: ' + JSON.stringify(newColumnHeaders)); - console.log('fieldsToRemove: ' + JSON.stringify(fieldsToRemove)); - // If fieldsToRemove is not empty then error out - if (fieldsToRemove.length > 0 && !this.ignoreMissingFields) { + if (fieldsToRemove.length > 0 && !this._ignoreMissingFields) { this._errorMessage = 'The following fields are not valid: ' + fieldsToRemove.join(', ') + '. Please remove them from the CSV file and try again.'; this._isError = true; this._isLoading = false; - console.log('The following fields are not valid: ' + fieldsToRemove.join(', ') + '. Please remove them from the CSV file and try again.'); return; } else { this._errorMessage = ''; - this._isError = false; - console.log('The following fields are not valid: ' + fieldsToRemove.join(', ') + '. Please remove them from the CSV file and try again.'); + this._isError = false; } // Check if there are duplicate headers @@ -407,7 +440,6 @@ export default class lwcConvertCSVToRecords extends LightningElement { this._errorMessage = 'Duplicate headers found: ' + duplicateHeaders.join(', ') + '. Please remove the duplicate headers and try again.'; this._isError = true; this._isLoading = false; - console.log('Duplicate headers found: ' + duplicateHeaders.join(', ') + '. Please remove the duplicate headers and try again.'); return; } @@ -416,7 +448,6 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Go through the parsedResults.data object and set key based on the fieldList object match on oldField and replace the oldField with the newField // If the key is not in the columnHeaders object, remove the key and value from the object // If the key is in the fieldsToRemove object, remove the key and value from the object - console.log('parsedResults.length: ' + parsedResults.data.length); for (let i = 0; i < parsedResults.data.length; i++) { let row = parsedResults.data[i]; let newRow = {}; @@ -426,15 +457,12 @@ export default class lwcConvertCSVToRecords extends LightningElement { let newValue = row[key]; for (let j = 0; j < newColumnHeaders.length; j++) { if (key === newColumnHeaders[j].oldField) { - console.log('oldKey: ' + key + ' newKey: ' + newColumnHeaders[j].newField); newKey = newColumnHeaders[j].newField; } } if (fieldsToRemove.includes(key)) { - console.log('removed key: ' + key); delete row[key]; } else { - console.log('newRow[' + newKey + ']: ' + newValue); // Use the fieldList array of objects to get the field type // Add the new key and value to the new row @@ -453,7 +481,6 @@ export default class lwcConvertCSVToRecords extends LightningElement { } } - console.log('fieldType: ' + fieldType); if (fieldType === 'DATE') { // Check if the value is not null // If it is not null, format it to the correct format yyyy-MM-dd @@ -479,11 +506,9 @@ export default class lwcConvertCSVToRecords extends LightningElement { // If it is a number, format it to the correct format 0.00 // If not a number return the value as is if (isNaN(formattedValue)) { - console.log('not a number: ' + formattedValue); newRow[newKey] = formattedValue; } else { formattedValue = parseFloat(formattedValue).toFixed(2); - console.log('is a number: ' + formattedValue); newRow[newKey] = parseFloat(formattedValue); } } else if (fieldType === 'DOUBLE' || fieldType === 'INT' || fieldType === 'LONG' || fieldType === 'PERCENT') { @@ -519,13 +544,9 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Go through the newRows and remove any rows that are empty newRows = newRows.filter(x => Object.keys(x).length > 0); - // Set the rows of data - console.log('newRows: ' + JSON.stringify(newRows)); - - // Seralize the data with the objectName + // Serialize the data with the objectName let serializedData = {}; serializedData[this.objectName] = newRows; - console.log('serializedData: ' + JSON.stringify(serializedData)); // Set the outputValue to the serialized data this._outputValue = serializedData; @@ -533,15 +554,12 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Set outputValue to the results this.handleValueChange('outputValue', serializedData); - console.log('autoNavigateNext: ' + this._autoNavigateNext); // If the autoNavigateNext attribute is true, navigate to the next screen if (this._autoNavigateNext) { - console.log('autoNavigateNext'); this.handleNext(); } }) .catch(error => { - console.log('error: ' + JSON.stringify(error)); this._errorMessage = JSON.stringify(error); this._isError = true; this._isLoading = false; @@ -560,10 +578,14 @@ export default class lwcConvertCSVToRecords extends LightningElement { } } - // Handle auto navigation to the next screen/action + /** + * Handles auto navigation to the next screen/action + * Only navigates if there are no errors and autoNavigateNext is enabled + * + * @returns {void} + */ handleNext() { // If there is an error, do not navigate - console.log('handleNext: ' + this._isError); if (this._isError) { return; } else { @@ -578,6 +600,13 @@ export default class lwcConvertCSVToRecords extends LightningElement { } } + /** + * Notifies Flow of attribute value changes + * + * @param {string} apiName - The API name of the attribute that changed + * @param {*} value - The new value for the attribute + * @returns {void} + */ handleValueChange(apiName, value) { const attributeChangeEvent = new FlowAttributeChangeEvent(apiName, value); this.dispatchEvent(attributeChangeEvent); diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml index 1d432daee..5e2d5189d 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml @@ -1,9 +1,9 @@ - 55.0 + 65.0 true - Convert CSV to Records - LWC version - This will make the parsing on the client side and not on the APEX side + Convert CSV to Records + High-volume CSV to Records conversion component that processes CSV files on the client side using PapaParse. Supports large file uploads with automatic field mapping and comprehensive error handling. lightning__FlowScreen @@ -24,7 +24,7 @@ - + diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg new file mode 100644 index 000000000..b641f8097 --- /dev/null +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js index dd712126a..2ed03beb1 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js @@ -1,186 +1,371 @@ -import {LightningElement, api, track} from 'lwc'; +/** + * Configuration Panel Editor (CPE) for lwcConvertCSVToRecords Flow Screen Component + * + * Provides the configuration UI in Flow Builder for the Convert CSV to Records component. + * Handles all input property configuration including PapaParse options and Flow-specific settings. + * + * Created By: Andy Haas + * + * @see lwcConvertCSVToRecords - Main component + */ + +import { LightningElement, api, track } from "lwc"; const DATA_TYPE = { - STRING: 'String', - BOOLEAN: 'Boolean', - NUMBER: 'Number', - INTEGER: 'Integer' + STRING: "String", + BOOLEAN: "Boolean", + NUMBER: "Number", + INTEGER: "Integer", }; const FLOW_EVENT_TYPE = { - DELETE: 'configuration_editor_input_value_deleted', - CHANGE: 'configuration_editor_input_value_changed' -} + DELETE: "configuration_editor_input_value_deleted", + CHANGE: "configuration_editor_input_value_changed", +}; export default class LwcConvertCSVToRecords_CPE extends LightningElement { - typeValue; - _builderContext = {}; - _values = []; - _typeMappings = []; - _elementType; - _elementName; - - // For sObject Type on the Lookup - handleDynamicTypeMapping(event) { - console.log('handling a dynamic type mapping'); - console.log('event is ' + JSON.stringify(event)); - let typeValue = event.detail.objectType; - const typeName = this._elementType === "Screen" ? 'T' : 'T__record'; - console.log('typeValue is: ' + typeValue); - const dynamicTypeMapping = new CustomEvent('configuration_editor_generic_type_mapping_changed', { - composed: true, - cancelable: false, - bubbles: true, - detail: { - typeName, - typeValue, - } - }); - this.dispatchEvent(dynamicTypeMapping); - this.dispatchFlowValueChangeEvent('objectName', event.detail.objectType, DATA_TYPE.STRING); - } - - @api - get elementInfo() { - return this._elementInfo; - } + typeValue; + _builderContext = {}; + _values = []; + _typeMappings = []; + _elementType; + _elementName; - set elementInfo(info) { - this._elementInfo = info || {}; - if (this._elementInfo) { - this._elementName = this._elementInfo.apiName; - this._elementType = this._elementInfo.type; - } - } + /** + * Handles dynamic type mapping for sObject selection in Flow Builder + * Updates the generic type mapping when an object is selected + * + * @param {CustomEvent} event - Event containing the selected object type + * @param {string} event.detail.objectType - The API name of the selected object + * @returns {void} + */ + handleDynamicTypeMapping(event) { + let typeValue = event.detail.objectType; + const typeName = this._elementType === "Screen" ? "T" : "T__record"; + const dynamicTypeMapping = new CustomEvent( + "configuration_editor_generic_type_mapping_changed", + { + composed: true, + cancelable: false, + bubbles: true, + detail: { + typeName, + typeValue, + }, + } + ); + this.dispatchEvent(dynamicTypeMapping); + this.dispatchFlowValueChangeEvent( + "objectName", + event.detail.objectType, + DATA_TYPE.STRING + ); + } - @track inputValues = { - objectName: {value: null, valueDataType: null, isCollection: false, label: 'Input Object?', required: true, errorMessage: 'Please select an object'}, - inputLabel: {value: 'Upload CSV', valueDataType: null, isCollection: false, label: 'File Import Label', required: false, fieldHelpText: 'The label for the file input field'}, - autoNavigateNext: {value: false, valueDataType: null, isCollection: false, label: 'Auto Navigate Next', required: true, fieldHelpText: 'If true, the flow will automatically navigate to the next screen after the component has been initialized.'}, - delimiter: {value: ',', valueDataType: null, isCollection: false, label: 'Delimiter', required: false, fieldHelpText: 'The delimiting character. Leave blank to auto-detect from a list of most common delimiters, or any values passed in through delimitersToGuess'}, - newLine: {value: null, valueDataType: null, isCollection: false, label: 'New Line', required: false, fieldHelpText: 'The newline sequence. Leave blank to auto-detect. Must be one of \r, \n, or \r\n.'}, - quoteChar: {value: '"', valueDataType: null, isCollection: false, label: 'Quote Character', required: false, fieldHelpText: 'The character used to quote fields. The quoting of all fields is not mandatory. Any field which is not quoted will correctly read.'}, - escapeChar: {value: '"', valueDataType: null, isCollection: false, label: 'Escape Character', required: false, fieldHelpText: 'The character used to escape the quote character within a field. If not set, this option will default to the value of quoteChar'}, - transformHeader: {value: '', valueDataType: null, isCollection: false, label: 'Transform Header', required: false, fieldHelpText: 'A function to apply on each header. The function receives the header as its first argument and the index as second.'}, - dynamicTyping: {value: false, valueDataType: null, isCollection: false, label: 'Dynamic Typing', required: false, fieldHelpText: 'If true, numeric and boolean data will be converted to their type instead of remaining strings'}, - encoding: {value: 'UTF-8', valueDataType: null, isCollection: false, label: 'Encoding', required: false, fieldHelpText: 'The encoding to use when opening local files. If specified, it must be a value supported by the FileReader API'}, - comments: {value: false, valueDataType: null, isCollection: false, label: 'Comments', required: false, fieldHelpText: 'A string that indicates a comment (for example, # or //). When Papa encounters a line starting with this string, it will skip the line.'}, - fastMode: {value: false, valueDataType: null, isCollection: false, label: 'Fast Mode', required: false, fieldHelpText: 'Fast mode speeds up parsing significantly for large inputs. However, it only works when the input has no quoted fields. Fast mode will automatically be enabled if no " characters appear in the input.'}, - transform: {value: '', valueDataType: null, isCollection: false, label: 'Transform', required: false, fieldHelpText: 'A function to apply on each value. The function receives the value as its first argument and the column number or header name when enabled as its second argument. The return value of the function will replace the value it received. The transform function is applied before dynamicTyping.'}, - delimitersToGuess: {value: '', valueDataType: null, isCollection: false, label: 'Delimiters To Guess', required: false, fieldHelpText: 'An array of delimiters to guess from if the delimiter option is not set'}, - ignoreMissingColumns: {value: true, valueDataType: null, isCollection: false, label: 'Ignore Missing Columns', required: false, fieldHelpText: 'If true, columns that are empty/null will be ignored. Otherwise, an error will be thrown.'}, - ignoreMissingFields: {value: true, valueDataType: null, isCollection: false, label: 'Ignore Missing Fields', required: false, fieldHelpText: 'If true, fields that do not have matching columns will be ignored. Otherwise, an error will be thrown.'}, - } + @api + get elementInfo() { + return this._elementInfo; + } - @api get builderContext() { - return this._builderContext; + set elementInfo(info) { + this._elementInfo = info || {}; + if (this._elementInfo) { + this._elementName = this._elementInfo.apiName; + this._elementType = this._elementInfo.type; } + } - set builderContext(value) { - this._builderContext = value; - } + @track inputValues = { + objectName: { + value: null, + valueDataType: null, + isCollection: false, + label: "Input Object?", + required: true, + errorMessage: "Please select an object", + }, + inputLabel: { + value: "Upload CSV", + valueDataType: null, + isCollection: false, + label: "File Import Label", + required: false, + fieldHelpText: "The label for the file input field", + }, + autoNavigateNext: { + value: false, + valueDataType: null, + isCollection: false, + label: "Auto Navigate Next", + required: true, + fieldHelpText: + 'If true, the flow will automatically navigate to the next screen after the component has been initialized. Note: Use a Flow Constant variable set to Boolean True rather than typing "True" directly.', + }, + delimiter: { + value: ",", + valueDataType: null, + isCollection: false, + label: "Delimiter", + required: false, + fieldHelpText: + "The delimiting character. Leave blank to auto-detect from a list of most common delimiters, or any values passed in through delimitersToGuess", + }, + newLine: { + value: null, + valueDataType: null, + isCollection: false, + label: "New Line", + required: false, + fieldHelpText: + "The newline sequence. Leave blank to auto-detect. Must be one of \r, \n, or \r\n.", + }, + quoteChar: { + value: """, + valueDataType: null, + isCollection: false, + label: "Quote Character", + required: false, + fieldHelpText: + "The character used to quote fields. The quoting of all fields is not mandatory. Any field which is not quoted will correctly read.", + }, + escapeChar: { + value: """, + valueDataType: null, + isCollection: false, + label: "Escape Character", + required: false, + fieldHelpText: + "The character used to escape the quote character within a field. If not set, this option will default to the value of quoteChar", + }, + transformHeader: { + value: "", + valueDataType: null, + isCollection: false, + label: "Transform Header", + required: false, + fieldHelpText: + "A function to apply on each header. The function receives the header as its first argument and the index as second.", + }, + dynamicTyping: { + value: false, + valueDataType: null, + isCollection: false, + label: "Dynamic Typing", + required: false, + fieldHelpText: + "If true, numeric and boolean data will be converted to their type instead of remaining strings", + }, + encoding: { + value: "UTF-8", + valueDataType: null, + isCollection: false, + label: "Encoding", + required: false, + fieldHelpText: + "The encoding to use when opening local files. If specified, it must be a value supported by the FileReader API", + }, + comments: { + value: false, + valueDataType: null, + isCollection: false, + label: "Comments", + required: false, + fieldHelpText: + "A string that indicates a comment (for example, # or //). When Papa encounters a line starting with this string, it will skip the line.", + }, + fastMode: { + value: false, + valueDataType: null, + isCollection: false, + label: "Fast Mode", + required: false, + fieldHelpText: + 'Fast mode speeds up parsing significantly for large inputs. However, it only works when the input has no quoted fields. Fast mode will automatically be enabled if no " characters appear in the input.', + }, + transform: { + value: "", + valueDataType: null, + isCollection: false, + label: "Transform", + required: false, + fieldHelpText: + "A function to apply on each value. The function receives the value as its first argument and the column number or header name when enabled as its second argument. The return value of the function will replace the value it received. The transform function is applied before dynamicTyping.", + }, + delimitersToGuess: { + value: "", + valueDataType: null, + isCollection: false, + label: "Delimiters To Guess", + required: false, + fieldHelpText: + "An array of delimiters to guess from if the delimiter option is not set", + }, + ignoreMissingColumns: { + value: true, + valueDataType: null, + isCollection: false, + label: "Ignore Missing Columns", + required: false, + fieldHelpText: + "If true, columns that are empty/null will be ignored. Otherwise, an error will be thrown.", + }, + ignoreMissingFields: { + value: true, + valueDataType: null, + isCollection: false, + label: "Ignore Missing Fields", + required: false, + fieldHelpText: + "If true, fields that do not have matching columns will be ignored. Otherwise, an error will be thrown.", + }, + }; - @api get automaticOutputVariables() { - return this._automaticOutputVariables; - }; - - set automaticOutputVariables(value) { - this._automaticOutputVariables = value; - } - @track _automaticOutputVariables; + @api get builderContext() { + return this._builderContext; + } - @api get inputVariables() { - return this._values; - } + set builderContext(value) { + this._builderContext = value; + } - set inputVariables(value) { - this._values = value; - this.initializeValues(); - } + @api get automaticOutputVariables() { + return this._automaticOutputVariables; + } - @api get genericTypeMappings() { - return this._genericTypeMappings; - } - set genericTypeMappings(value) { - this._typeMappings = value; - this.initializeTypeMappings(); - } + set automaticOutputVariables(value) { + this._automaticOutputVariables = value; + } + @track _automaticOutputVariables; + + @api get inputVariables() { + return this._values; + } + + set inputVariables(value) { + this._values = value; + this.initializeValues(); + } - initializeValues(value) { - if (this._values && this._values.length) { - this._values.forEach(curInputParam => { - if (curInputParam.name && this.inputValues[curInputParam.name]) { - console.log('in initializeValues: ' + curInputParam.name + ' = ' + curInputParam.value); - // console.log('in initializeValues: '+ JSON.stringify(curInputParam)); - if (this.inputValues[curInputParam.name].serialized) { - this.inputValues[curInputParam.name].value = JSON.parse(curInputParam.value); - } else { - this.inputValues[curInputParam.name].value = curInputParam.value; - } - this.inputValues[curInputParam.name].valueDataType = curInputParam.valueDataType; - } - }); + @api get genericTypeMappings() { + return this._genericTypeMappings; + } + set genericTypeMappings(value) { + this._typeMappings = value; + this.initializeTypeMappings(); + } + + /** + * Initializes input values from Flow Builder context + * Called when inputVariables are set by Flow Builder + * + * @param {*} value - Unused parameter (kept for compatibility) + * @returns {void} + */ + initializeValues(value) { + if (this._values && this._values.length) { + this._values.forEach((curInputParam) => { + if (curInputParam.name && this.inputValues[curInputParam.name]) { + if (this.inputValues[curInputParam.name].serialized) { + this.inputValues[curInputParam.name].value = JSON.parse( + curInputParam.value + ); + } else { + this.inputValues[curInputParam.name].value = curInputParam.value; + } + this.inputValues[curInputParam.name].valueDataType = + curInputParam.valueDataType; } + }); } + } - initializeTypeMappings() { - this._typeMappings.forEach((typeMapping) => { - // console.log(JSON.stringify(typeMapping)); - if (typeMapping.name && typeMapping.value) { - this.typeValue = typeMapping.value; - } - }); - } + /** + * Initializes generic type mappings from Flow Builder context + * Sets the typeValue based on the generic type mappings + * + * @returns {void} + */ + initializeTypeMappings() { + this._typeMappings.forEach((typeMapping) => { + if (typeMapping.name && typeMapping.value) { + this.typeValue = typeMapping.value; + } + }); + } + + /** + * Handles value changes from input components in the configuration panel + * Supports both new (fsc_flow-combobox) and legacy input components + * + * @param {CustomEvent} event - Value change event from input component + * @param {*} event.detail.newValue - New value (new components) + * @param {*} event.detail.value - Value (legacy components) + * @param {string} event.detail.newValueDataType - Data type of the new value + * @param {HTMLElement} event.target - Target element (new components) + * @param {HTMLElement} event.currentTarget - Current target (legacy components) + * @returns {void} + */ + handleValueChange(event) { + if (event.detail && event.target) { + // Any component using fsc_flow-combobox will be ran through here + // This is the newer version and will allow users to use merge fields + // If event.detail.newValue is set then use it, otherwise use event.detail.value + let newValue = event.detail.newValue; + if (newValue == null) { + newValue = event.detail.value; - handleValueChange(event) { - console.log('in handleValueChange: ' + JSON.stringify(event)); - if (event.detail && event.target) { - // Any component using fsc_flow-combobox will be ran through here - // This is the newer version and will allow users to use merge fields - // If event.detail.newValue is set then use it, otherwise use event.detail.value - let newValue = event.detail.newValue; - if (newValue == null) { - newValue = event.detail.value; - - // If event.detail.value.name is set then use it, otherwise use event.detail.value - if (newValue.name != null) { - newValue = newValue.name; - } - } - console.log('(NEW) in handleValueChange: ' + event.target.name + ' = ' + newValue); - this.dispatchFlowValueChangeEvent(event.target.name, newValue, event.detail.newValueDataType); - - } else if ( event.detail && event.currentTarget ) { - // This is the older version for any old inputs that are still using currentTarget - // Kept for backwards compatibility - console.log('(OLD) in handleValueChange: ' + event.currentTarget.name + ' = ' + event.detail); - let dataType = DATA_TYPE.STRING; - if (event.currentTarget.type == 'checkbox') dataType = DATA_TYPE.BOOLEAN; - if (event.currentTarget.type == 'number') dataType = DATA_TYPE.NUMBER; - if (event.currentTarget.type == 'integer') dataType = DATA_TYPE.INTEGER; - let newValue = event.currentTarget.type === 'checkbox' ? event.currentTarget.checked : event.detail.value; - this.dispatchFlowValueChangeEvent(event.currentTarget.name, newValue, dataType); - } else { - console.log('in handleValueChange: no event detail'); + // If event.detail.value.name is set then use it, otherwise use event.detail.value + if (newValue.name != null) { + newValue = newValue.name; } + } + this.dispatchFlowValueChangeEvent( + event.target.name, + newValue, + event.detail.newValueDataType + ); + } else if (event.detail && event.currentTarget) { + // This is the older version for any old inputs that are still using currentTarget + // Kept for backwards compatibility + let dataType = DATA_TYPE.STRING; + if (event.currentTarget.type == "checkbox") dataType = DATA_TYPE.BOOLEAN; + if (event.currentTarget.type == "number") dataType = DATA_TYPE.NUMBER; + if (event.currentTarget.type == "integer") dataType = DATA_TYPE.INTEGER; + let newValue = + event.currentTarget.type === "checkbox" + ? event.currentTarget.checked + : event.detail.value; + this.dispatchFlowValueChangeEvent( + event.currentTarget.name, + newValue, + dataType + ); } + } - dispatchFlowValueChangeEvent(id, newValue, dataType = DATA_TYPE.STRING) { - console.log('in dispatchFlowValueChangeEvent: ' + id, newValue, dataType); - if (this.inputValues[id] && this.inputValues[id].serialized) { - console.log('serializing value'); - newValue = JSON.stringify(newValue); - } - const valueChangedEvent = new CustomEvent(FLOW_EVENT_TYPE.CHANGE, { - bubbles: true, - cancelable: false, - composed: true, - detail: { - name: id, - newValue: newValue ? newValue : null, - newValueDataType: dataType - } - }); - this.dispatchEvent(valueChangedEvent); + /** + * Dispatches a Flow configuration editor value change event + * Serializes values if needed before dispatching + * + * @param {string} id - The property name/ID that changed + * @param {*} newValue - The new value for the property + * @param {string} [dataType=DATA_TYPE.STRING] - The data type of the value + * @returns {void} + */ + dispatchFlowValueChangeEvent(id, newValue, dataType = DATA_TYPE.STRING) { + if (this.inputValues[id] && this.inputValues[id].serialized) { + newValue = JSON.stringify(newValue); } -} \ No newline at end of file + const valueChangedEvent = new CustomEvent(FLOW_EVENT_TYPE.CHANGE, { + bubbles: true, + cancelable: false, + composed: true, + detail: { + name: id, + newValue: newValue ? newValue : null, + newValueDataType: dataType, + }, + }); + this.dispatchEvent(valueChangedEvent); + } +} diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js-meta.xml b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js-meta.xml index 1557f74ea..38b24e83f 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js-meta.xml +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.js-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 65.0 true \ No newline at end of file diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/sfdx-project.json b/flow_screen_components/ConvertCSVToRecords_LWC/sfdx-project.json index 92cc4f99b..e71487154 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/sfdx-project.json +++ b/flow_screen_components/ConvertCSVToRecords_LWC/sfdx-project.json @@ -20,7 +20,7 @@ "name": "ConvertCSVToRecords_LWC", "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "55.0", + "sourceApiVersion": "65.0", "packageAliases": { "FlowActionsBasePack@3.6.0-0": "04t8b000000mKnoAAE", "FlowScreenComponentsBasePack@3.0.6-0": "04t5G000003rUvVQAU", From cc02bee57d709c8dc9ccd96ea5a9f780c7c1ccb7 Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:20:10 -0600 Subject: [PATCH 02/13] Update PapaParse static resource from v5.0.2 to v5.5.3 Version Update Details: - Previous Version: 5.0.2 - Updated Version: 5.5.3 (latest stable) - Release Date: March 23, 2023 Security Fixes: - CVE-2020-36649: Fixed ReDoS (Regular Expression Denial of Service) vulnerability * Affected versions: Prior to 5.2.0 * Issue: Malformed regex in parse function could cause performance degradation/DoS * Status: Fixed in 5.2.0 and later versions Improvements: - Performance enhancements for large file processing - Optimized parsing algorithms - Improved memory management - Better browser compatibility - Enhanced worker support - Improved File API handling - Various bug fixes and edge case resolutions Breaking Changes: - None - API remains backward compatible - Existing code works without modifications Known Issues: - None reported for version 5.5.3 - This update resolves Salesforce freezing issues in recent releases This update addresses the component freezing issue reported after recent Salesforce releases and ensures compatibility with latest browser security standards. --- .../force-app/main/default/staticresources/PapaParse.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/staticresources/PapaParse.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/staticresources/PapaParse.js index 14c98ff82..f31411042 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/staticresources/PapaParse.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/staticresources/PapaParse.js @@ -1,7 +1,7 @@ /* @license Papa Parse -v5.0.2 +v5.5.3 https://github.com/mholt/PapaParse License: MIT */ -!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var r=(t=t||{}).dynamicTyping||!1;q(r)&&(t.dynamicTypingFunction=r,r={});if(t.dynamicTyping=r,t.transform=!!q(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var i=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(r=f.URL||f.webkitURL||null,i=s.toString(),b.BLOB_URL||(b.BLOB_URL=r.createObjectURL(new Blob(["(",i,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var r,i;return t.onmessage=_,t.id=h++,a[t.id]=t}();return i.userStep=t.step,i.userChunk=t.chunk,i.userComplete=t.complete,i.userError=t.error,t.step=q(t.step),t.chunk=q(t.chunk),t.complete=q(t.complete),t.error=q(t.error),delete t.worker,void i.postMessage({input:e,config:t,workerId:i.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&q(e.read)&&q(e.on)?n=new m(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var i=!1,_=!0,g=",",v="\r\n",n='"',s=n+n,r=!1,a=null;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(g=t.delimiter);("boolean"==typeof t.quotes||Array.isArray(t.quotes))&&(i=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(r=t.skipEmptyLines);"string"==typeof t.newline&&(v=t.newline);"string"==typeof t.quoteChar&&(n=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");a=t.columns}void 0!==t.escapeChar&&(s=t.escapeChar+n)}();var o=new RegExp(U(n),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,r);if("object"==typeof e[0])return u(a||h(e[0]),e,r)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:h(e.data[0])),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],r);throw new Error("Unable to serialize unrecognized input");function h(e){if("object"!=typeof e)return[];var t=[];for(var r in e)t.push(r);return t}function u(e,t,r){var i="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(q(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!q(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){q(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var i;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(i=new XMLHttpRequest,this._config.withCredentials&&(i.withCredentials=this._config.withCredentials),n||(i.onload=y(this._chunkLoaded,this),i.onerror=y(this._chunkError,this)),i.open("GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)i.setRequestHeader(t,e[t])}if(this._config.chunkSize){var r=this._start+this._config.chunkSize-1;i.setRequestHeader("Range","bytes="+this._start+"-"+r)}try{i.send()}catch(e){this._chunkError(e.message)}n&&0===i.status?this._chunkError():this._start+=this._config.chunkSize}},this._chunkLoaded=function(){4===i.readyState&&(i.status<200||400<=i.status?this._chunkError():(this._finished=!this._config.chunkSize||this._start>function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substr(t.lastIndexOf("/")+1))}(i),this.parseChunk(i.responseText)))},this._chunkError=function(e){var t=i.statusText||e;this._sendError(new Error(t))}}function c(e){var i,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((i=new FileReader).onload=y(this._chunkLoaded,this),i.onerror=y(this._chunkError,this)):i=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(i.error)}}function p(e){var r;u.call(this,e=e||{}),this.stream=function(e){return r=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e=this._config.chunkSize,t=e?r.substr(0,e):r;return r=e?r.substr(e):"",this._finished=!r,this.parseChunk(t)}}}function m(e){u.call(this,e=e||{});var t=[],r=!0,i=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){i&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):r=!0},this._streamData=y(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),r&&(r=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=y(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=y(function(){this._streamCleanUp(),i=!0,this._streamData("")},this),this._streamCleanUp=y(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function r(g){var a,o,h,i=Math.pow(2,53),n=-i,s=/^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i,u=/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/,t=this,r=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(q(g.step)){var p=g.step;g.step=function(e){if(c=e,_())m();else{if(m(),0===c.data.length)return;r+=e.data.length,g.preview&&r>g.preview?o.abort():p(c,t)}}}function v(e){return"greedy"===g.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function m(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),g.skipEmptyLines)for(var e=0;e=l.length?"__parsed_extra":l[r]),g.transform&&(s=g.transform(s,n)),s=y(n,s),"__parsed_extra"===n?(i[n]=i[n]||[],i[n].push(s)):i[n]=s}return g.header&&(r>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+r,f+t):r=i.length/2?"\r\n":"\r"}(e,i)),h=!1,g.delimiter)q(g.delimiter)&&(g.delimiter=g.delimiter(e),c.meta.delimiter=g.delimiter);else{var n=function(e,t,r,i,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=L)return R(!0)}else for(g=M,M++;;){if(-1===(g=a.indexOf(O,g+1)))return t||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:M}),w();if(g===i-1)return w(a.substring(M,g).replace(_,O));if(O!==z||a[g+1]!==z){if(O===z||0===g||a[g-1]!==z){var y=E(-1===m?p:Math.min(p,m));if(a[g+1+y]===D){f.push(a.substring(M,g).replace(_,O)),a[M=g+1+y+e]!==O&&(g=a.indexOf(O,M)),p=a.indexOf(D,M),m=a.indexOf(I,M);break}var k=E(m);if(a.substr(g+1+k,n)===I){if(f.push(a.substring(M,g).replace(_,O)),C(g+1+k+n),p=a.indexOf(D,M),g=a.indexOf(O,M),o&&(S(),j))return R();if(L&&h.length>=L)return R(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:M}),g++}}else g++}return w();function b(e){h.push(e),d=M}function E(e){var t=0;if(-1!==e){var r=a.substring(g+1,e);r&&""===r.trim()&&(t=r.length)}return t}function w(e){return t||(void 0===e&&(e=a.substr(M)),f.push(e),M=i,b(f),o&&S()),R()}function C(e){M=e,b(f),f=[],m=a.indexOf(I,M)}function R(e,t){return{data:t||!1?h[0]:h,errors:u,meta:{delimiter:D,linebreak:I,aborted:j,truncated:!!e,cursor:d+(r||0)}}}function S(){A(R(void 0,!0)),h=[],u=[]}function x(e,t,r){var i={nextDelim:void 0,quoteSearch:void 0},n=a.indexOf(O,t+1);if(t{"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()})(this,function r(){var n="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==n?n:{};var d,s=!n.document&&!!n.postMessage,a=n.IS_PAPA_WORKER||!1,o={},h=0,v={};function u(e){this._handle=null,this._finished=!1,this._completed=!1,this._halted=!1,this._input=null,this._baseIndex=0,this._partialLine="",this._rowCount=0,this._start=0,this._nextChunk=null,this.isFirstChunk=!0,this._completeResults={data:[],errors:[],meta:{}},function(e){var t=b(e);t.chunkSize=parseInt(t.chunkSize),e.step||e.chunk||(t.chunkSize=null);this._handle=new i(t),(this._handle.streamer=this)._config=t}.call(this,e),this.parseChunk=function(t,e){var i=parseInt(this._config.skipFirstNLines)||0;if(this.isFirstChunk&&0=this._config.preview);if(a)n.postMessage({results:r,workerId:v.WORKER_ID,finished:i});else if(U(this._config.chunk)&&!e){if(this._config.chunk(r,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);this._completeResults=r=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(r.data),this._completeResults.errors=this._completeResults.errors.concat(r.errors),this._completeResults.meta=r.meta),this._completed||!i||!U(this._config.complete)||r&&r.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),i||r&&r.meta.paused||this._nextChunk(),r}this._halted=!0},this._sendError=function(e){U(this._config.error)?this._config.error(e):a&&this._config.error&&n.postMessage({workerId:v.WORKER_ID,error:e,finished:!1})}}function f(e){var r;(e=e||{}).chunkSize||(e.chunkSize=v.RemoteChunkSize),u.call(this,e),this._nextChunk=s?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),s||(r.onload=y(this._chunkLoaded,this),r.onerror=y(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!s),this._config.downloadRequestHeaders){var e,t=this._config.downloadRequestHeaders;for(e in t)r.setRequestHeader(e,t[e])}var i;this._config.chunkSize&&(i=this._start+this._config.chunkSize-1,r.setRequestHeader("Range","bytes="+this._start+"-"+i));try{r.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}s&&0===r.status&&this._chunkError()}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize||r.responseText.length,this._finished=!this._config.chunkSize||this._start>=(e=>null!==(e=e.getResponseHeader("Content-Range"))?parseInt(e.substring(e.lastIndexOf("/")+1)):-1)(r),this.parseChunk(r.responseText)))},this._chunkError=function(e){e=r.statusText||e;this._sendError(new Error(e))}}function l(e){(e=e||{}).chunkSize||(e.chunkSize=v.LocalChunkSize),u.call(this,e);var i,r,n="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,r=e.slice||e.webkitSlice||e.mozSlice,n?((i=new FileReader).onload=y(this._chunkLoaded,this),i.onerror=y(this._chunkError,this)):i=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(i.error)}}function c(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){var e,t;if(!this._finished)return e=this._config.chunkSize,i=e?(t=i.substring(0,e),i.substring(e)):(t=i,""),this._finished=!i,this.parseChunk(t)}}function p(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0},this._streamData=y(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=y(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=y(function(){this._streamCleanUp(),r=!0,this._streamData("")},this),this._streamCleanUp=y(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function i(m){var n,s,a,t,o=Math.pow(2,53),h=-o,u=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,d=/^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/,i=this,r=0,f=0,l=!1,e=!1,c=[],p={data:[],errors:[],meta:{}};function y(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){if(p&&a&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+v.DefaultDelimiter+"'"),a=!1),m.skipEmptyLines&&(p.data=p.data.filter(function(e){return!y(e)})),_()){if(p)if(Array.isArray(p.data[0])){for(var e=0;_()&&e(e=>(m.dynamicTypingFunction&&void 0===m.dynamicTyping[e]&&(m.dynamicTyping[e]=m.dynamicTypingFunction(e)),!0===(m.dynamicTyping[e]||m.dynamicTyping)))(e)?"true"===t||"TRUE"===t||"false"!==t&&"FALSE"!==t&&((e=>{if(u.test(e)){e=parseFloat(e);if(h=c.length?"__parsed_extra":c[r]:n,s=m.transform?m.transform(s,n):s);"__parsed_extra"===n?(i[n]=i[n]||[],i[n].push(s)):i[n]=s}return m.header&&(r>c.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+c.length+" fields but parsed "+r,f+t):rm.preview?s.abort():(p.data=p.data[0],t(p,i))))}),this.parse=function(e,t,i){var r=m.quoteChar||'"',r=(m.newline||(m.newline=this.guessLineEndings(e,r)),a=!1,m.delimiter?U(m.delimiter)&&(m.delimiter=m.delimiter(e),p.meta.delimiter=m.delimiter):((r=((e,t,i,r,n)=>{var s,a,o,h;n=n||[",","\t","|",";",v.RECORD_SEP,v.UNIT_SEP];for(var u=0;u=i.length/2?"\r\n":"\r"}}function P(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function E(C){var S=(C=C||{}).delimiter,O=C.newline,x=C.comments,I=C.step,A=C.preview,T=C.fastMode,D=null,L=!1,F=null==C.quoteChar?'"':C.quoteChar,j=F;if(void 0!==C.escapeChar&&(j=C.escapeChar),("string"!=typeof S||-1=A)return w(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:z}),m++}}else if(x&&0===d.length&&i.substring(z,z+a)===x){if(-1===g)return w();z=g+s,g=i.indexOf(O,z),p=i.indexOf(S,z)}else if(-1!==p&&(p=A)return w(!0)}return E();function k(e){h.push(e),f=z}function v(e){var t=0;return t=-1!==e&&(e=i.substring(m+1,e))&&""===e.trim()?e.length:t}function E(e){return r||(void 0===e&&(e=i.substring(z)),d.push(e),z=n,k(d),o&&R()),w()}function b(e){z=e,k(d),d=[],g=i.indexOf(O,z)}function w(e){if(C.header&&!t&&h.length&&!L){var s=h[0],a=Object.create(null),o=new Set(s);let n=!1;for(let r=0;r65279!==e.charCodeAt(0)?e:e.slice(1))(e),i=new(t.download?f:c)(t)):!0===e.readable&&U(e.read)&&U(e.on)?i=new p(t):(n.File&&e instanceof File||e instanceof Object)&&(i=new l(t)),i.stream(e);(i=(()=>{var e;return!!v.WORKERS_SUPPORTED&&(e=(()=>{var e=n.URL||n.webkitURL||null,t=r.toString();return v.BLOB_URL||(v.BLOB_URL=e.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ","(",t,")();"],{type:"text/javascript"})))})(),(e=new n.Worker(e)).onmessage=g,e.id=h++,o[e.id]=e)})()).userStep=t.step,i.userChunk=t.chunk,i.userComplete=t.complete,i.userError=t.error,t.step=U(t.step),t.chunk=U(t.chunk),t.complete=U(t.complete),t.error=U(t.error),delete t.worker,i.postMessage({input:e,config:t,workerId:i.id})},v.unparse=function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,i=!1,r=null,o=!1,h=((()=>{if("object"==typeof t){if("string"!=typeof t.delimiter||v.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter),"boolean"!=typeof t.quotes&&"function"!=typeof t.quotes&&!Array.isArray(t.quotes)||(n=t.quotes),"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(i=t.skipEmptyLines),"string"==typeof t.newline&&(y=t.newline),"string"==typeof t.quoteChar&&(s=t.quoteChar),"boolean"==typeof t.header&&(_=t.header),Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");r=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s),t.escapeFormulae instanceof RegExp?o=t.escapeFormulae:"boolean"==typeof t.escapeFormulae&&t.escapeFormulae&&(o=/^[=+\-@\t\r].*$/)}})(),new RegExp(P(s),"g"));"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,i);if("object"==typeof e[0])return u(r||Object.keys(e[0]),e,i)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields||r),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],i);throw new Error("Unable to serialize unrecognized input");function u(e,t,i){var r="",n=("string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t)),Array.isArray(e)&&0{for(var i=0;i Date: Tue, 16 Dec 2025 18:29:00 -0600 Subject: [PATCH 03/13] Fix critical bugs and add field type support - Fix date formatting: Add zero-padding for single digit months/days (ISO format) - Fix currency formatting: Replace all commas globally, not just first occurrence - Add null/undefined validation for parsedResults, fieldList, and parsedResults.data - Improve newValue handling: Check for null, undefined, and empty strings - Fix date parsing: Use date.getTime() for proper validation - Add DATETIME field type support with ISO 8601 formatting - Add BOOLEAN field type support with multiple truthy value formats - Improve error handling with better error messages - Fix empty column removal logic for object-based rows (header mode) - Use parseInt for INT/LONG fields instead of parseFloat - Add validation to skip invalid rows - Code formatting: Standardize to double quotes and consistent indentation --- .../lwcConvertCSVToRecords.js | 1398 ++++++++++------- 1 file changed, 809 insertions(+), 589 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js index 24c856acb..6336b54e8 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js @@ -1,10 +1,10 @@ /** * Lightning Web Component for Flow Screens: lwcConvertCSVToRecords - * + * * A high-volume CSV to Records conversion component that processes CSV files * on the client side using PapaParse library. This component allows users to * upload CSV files and automatically converts them to Salesforce sObject records. - * + * * Features: * - Client-side CSV parsing (no server-side processing limits) * - Automatic field mapping (standard and custom fields) @@ -12,603 +12,823 @@ * - Auto-navigation to next screen after parsing * - Comprehensive error handling * - Configurable PapaParse options - * + * * Created By: Andy Haas - * + * * Version History: * V1.0.1 1/14/23 Fixed setting the selected object in CPE * V1.0 12/29/22 Initial version hosted on FlowComponents - * + * * @see https://unofficialsf.com/from-andy-haas-a-high-volume-convert-csv-to-records-screen-component/ * @see https://www.papaparse.com/docs */ -import { LightningElement, track, api } from 'lwc'; +import { LightningElement, track, api } from "lwc"; import { - FlowNavigationFinishEvent, - FlowNavigationNextEvent, - FlowAttributeChangeEvent + FlowNavigationFinishEvent, + FlowNavigationNextEvent, + FlowAttributeChangeEvent, } from "lightning/flowSupport"; -import getObjectFields from '@salesforce/apex/LwcConvertCSVToRecordsHelper.getObjectFields'; -import { loadScript } from 'lightning/platformResourceLoader'; -import PARSER from '@salesforce/resourceUrl/PapaParse'; +import getObjectFields from "@salesforce/apex/LwcConvertCSVToRecordsHelper.getObjectFields"; +import { loadScript } from "lightning/platformResourceLoader"; +import PARSER from "@salesforce/resourceUrl/PapaParse"; export default class lwcConvertCSVToRecords extends LightningElement { - // Initialize the parser - parserInitialized = false; - - // PapaParser Inputs - @api get delimiter() { - return this._delimiter; - } - set delimiter(value) { - this._delimiter = value; - } - _delimiter = ','; - - @api get newline() { - return this._newline; - } - set newline(value) { - this._newline = value; - } - _newline = ''; - - @api get quoteChar() { - return this._quoteChar; - } - set quoteChar(value) { - this._quoteChar = value; - } - _quoteChar = '"'; - - @api get escapeChar() { - return this._escapeChar; - } - set escapeChar(value) { - this._escapeChar = value; - } - _escapeChar = '"'; - - @api get transformHeader() { - return this._transformHeader; - } - set transformHeader(value) { - this._transformHeader = value; - } - _transformHeader = undefined; - - @api get header() { - return this._header; - } - set header(value) { - this._header = value; - } - _header = false; - - @api get dynamicTyping() { - return this._dynamicTyping; - } - set dynamicTyping(value) { - this._dynamicTyping = value; - } - _dynamicTyping = false; - - @api get encoding() { - return this._encoding; - } - set encoding(value) { - this._encoding = value; - } - _encoding = ''; - - @api get comments() { - return this._comments; - } - set comments(value) { - this._comments = value; - } - _comments = false; - - @api get fastMode() { - return this._fastMode; - } - set fastMode(value) { - this._fastMode = value; - } - _fastMode = false; - - @api get skipEmptyLines() { - return this._skipEmptyLines; - } - set skipEmptyLines(value) { - this._skipEmptyLines = value; - } - _skipEmptyLines = false; - - @api get transform() { - return this._transform; - } - set transform(value) { - this._transform = value; - } - _transform = undefined; - - @api get delimitersToGuess() { - return this._delimitersToGuess; - } - set delimitersToGuess(value) { - this._delimitersToGuess = value; - } - _delimitersToGuess = []; - - // Get Label for Input file - @api get inputLabel() { - return this._inputLabel; - } - set inputLabel(value) { - this._inputLabel = value; - } - _inputLabel = 'Upload CSV'; - - // Get flow attributes - @api get availableActions() { - return this._availableActions; - } - set availableActions(value) { - this._availableActions = value; - } - _availableActions = []; - - @api get autoNavigateNext() { - return this._autoNavigateNext; - } - set autoNavigateNext(value) { - this._autoNavigateNext = value; - } - _autoNavigateNext = false; - - // Set variables for the screen - @track uploadFileName = ''; - @track uploadedFile = []; - - // Set objectName variable to be used in the getObjectFields Apex method - @api objectName = 'Account'; - - // Set the isError variable to false - @api get isError() { - return this._isError; - } - set isError(value) { - this._isError = value; - } - _isError = false; - - // Set the errorMessage variable to an null - @api get errorMessage() { - return this._errorMessage; - } - set errorMessage(value) { - this._errorMessage = value; - } - _errorMessage = null; - - // Store the fileName of the uploaded CSV file - @track fileName = ''; - - // Store the fields for the selected object - @track objectInfo = []; - - // Store Column Headers - @track columnHeaders = []; - - // Store Rows of Data - @track rows = []; - - // Store the output SObject records from the CSV file - @api get outputValue() { - return this._outputValue; - } - set outputValue(value) { - this._outputValue = value; - } - _outputValue = []; - - // Store a Status field to show the user the status of the CSV file - @track uploadFileStatus = ''; - - // Set isLoading to false - @api get isLoading() { - return this._isLoading; - } - set isLoading(value) { - this._isLoading = value; - } - _isLoading = false; - - @api get ignoreMissingColumns() { - return this._ignoreMissingColumns; - } - set ignoreMissingColumns(value) { - this._ignoreMissingColumns = value; - } - _ignoreMissingColumns = false; - - @api get ignoreMissingFields() { - return this._ignoreMissingFields; - } - set ignoreMissingFields(value) { - this._ignoreMissingFields = value; - } - _ignoreMissingFields = false; - - - /** - * Lifecycle hook: Initializes the PapaParse library when component is rendered - * @returns {void} - */ - renderedCallback() { - if(!this.parserInitialized){ - loadScript(this, PARSER) - .then(() => { - // Verify PapaParse is available - if (typeof Papa !== 'undefined') { - this.parserInitialized = true; - } else { - this._errorMessage = 'PapaParse library failed to load. Please refresh the page.'; - this._isError = true; - console.error('PapaParse library not available after loading'); - } - }) - .catch(error => { - this._errorMessage = 'Failed to load PapaParse library: ' + error.message; - this._isError = true; - console.error('PapaParse loading error:', error); - }); - } - } - - /** - * Handles file input change event and processes the uploaded CSV file - * Parses the CSV using PapaParse, maps columns to Salesforce fields, - * and converts data to sObject format - * - * @param {Event} event - File input change event containing the uploaded file - * @returns {void} - */ - handleInputChange(event){ - // Set Default Values - this.header = true; - this.skipEmptyLines = true; - - if(event.detail.files.length > 0){ - // Ensure PapaParse is loaded before proceeding - if (!this.parserInitialized || typeof Papa === 'undefined') { - this._errorMessage = 'PapaParse library is not loaded. Please refresh the page and try again.'; - this._isError = true; - this._isLoading = false; - return; - } - - this._isLoading = true; - const file = event.detail.files[0]; - this.loading = true; - Papa.parse(file, { - delimiter: this._delimiter, - newline: this._newline, - quoteChar: this._quoteChar, - escapeChar: this._escapeChar, - transformHeader: this._transformHeader, - header: this._header, - dynamicTyping: this._dynamicTyping, - encoding: this._encoding, - comments: this._comments, - fastMode: this._fastMode, - skipEmptyLines: this._skipEmptyLines, - transform: this._transform, - delimitersToGuess: this._delimitersToGuess, - complete: (parsedResults) => { - // get the meta columns - this.columnHeaders = parsedResults.meta.fields; - - // See if there are any empty columns - let emptyColumns = parsedResults.meta.fields.filter(field => field === ''); - - // If there are empty columns, throw an error - if(emptyColumns.length > 0 && !this._ignoreMissingColumns){ - // Set the isError variable to true - this._isError = true; - this._errorMessage = 'There are empty columns in the CSV file. Please remove the empty columns and try again.'; - this._isLoading = false; - return; - } else if (emptyColumns.length > 0 && this._ignoreMissingColumns) { - // If there are empty columns, but the user wants to ignore them, remove the empty columns - parsedResults.meta.fields = parsedResults.meta.fields.filter(field => field !== ''); - - // Set the columnHeaders variable to the new columnHeaders array - this.columnHeaders = parsedResults.meta.fields; - - // Remove the empty columns from the data - parsedResults.data = parsedResults.data.map(row => { - return row.filter(field => field !== ''); - }); - } - getObjectFields({objectName: this.objectName}) - .then(fieldList => { - // fieldList is an array of objects - // Each object has a Name and a Type property - - // Set new fieldName array - let fieldNames = []; - fieldNames = fieldList.map(field => field.name); - - // Set new columnHeader array - let newColumnHeaders = []; - - // Set array of fields to remove - let fieldsToRemove = []; - - // Compare the column headers to the fields for the selected object - // If the column header is not a match add __c to the end and recheck the fields - // If the column header is still not a match, remove the column header from the list - for (let i = 0; i < this.columnHeaders.length; i++) { - let columnHeader = this.columnHeaders[i]; - - // Trim the column header - columnHeader = columnHeader.trim(); - - // For standard fields we need to remove the space inbetween the words - // For example: Account Name becomes AccountName - // Create standardField variable to store the new value - let standardField; - if (columnHeader.includes(' ')) { - standardField = columnHeader.replaceAll(' ', ''); - } else { - standardField = columnHeader; - } - if (fieldNames.includes(standardField)) { - newColumnHeaders.push({"newField":columnHeader, "oldField":columnHeader}); - } else { - - // Create customField variable to store the new value - let customField; - - // Replace spaces with underscores - customField = columnHeader.replaceAll(' ', '_'); - - // Limit the length of the field to 40 characters - customField = customField.substring(0, 40); - - // Remove return characters - customField = customField.replace(/[\r]/g, ''); - - // If the field starts with a number, add an X to the beginning of the field - if (customField.match(/^[0-9]/)) { - customField = 'X' + customField; - } - - // Remove any special characters - // % & * ( ) + - = { } [ ] : ; ' " , . < > ? / | \ ~ ` ! @ # $ ^ - customField = customField.replace(/[^a-zA-Z0-9_]/g, ''); - - // newlines and carriage returns are also removed - customField = customField.replace(/[\r\n]+/gm, ''); - - // Remove any leading or trailing underscores - customField = customField.replace(/^_+|_+$/g, ''); - - // Replace any double underscores with a single underscore - customField = customField.replace(/__+/g, '_'); - - // Replace any triple underscores with a single underscore - customField = customField.replace(/___+/g, '_'); - - // Add __c to the end of the field - customField = customField + '__c'; - - // Validate the field name - if (fieldNames.includes(customField)) { - newColumnHeaders.push({"newField":customField, "oldField":columnHeader}); - } else { - fieldsToRemove.push(columnHeader); - } - } - } - - // If fieldsToRemove is not empty then error out - if (fieldsToRemove.length > 0 && !this._ignoreMissingFields) { - this._errorMessage = 'The following fields are not valid: ' + fieldsToRemove.join(', ') + '. Please remove them from the CSV file and try again.'; - this._isError = true; - this._isLoading = false; - return; - } else { - this._errorMessage = ''; - this._isError = false; - } - - // Check if there are duplicate headers - let duplicateHeaders = []; - for (let i = 0; i < newColumnHeaders.length; i++) { - let columnHeader = newColumnHeaders[i].newField; - if (newColumnHeaders.filter(x => x.newField === columnHeader).length > 1) { - duplicateHeaders.push(columnHeader); - } - } - - // If there is a duplicate header then error out - if (duplicateHeaders.length > 0) { - this._errorMessage = 'Duplicate headers found: ' + duplicateHeaders.join(', ') + '. Please remove the duplicate headers and try again.'; - this._isError = true; - this._isLoading = false; - return; - } - - // New array to store the rows of data - let newRows = []; - // Go through the parsedResults.data object and set key based on the fieldList object match on oldField and replace the oldField with the newField - // If the key is not in the columnHeaders object, remove the key and value from the object - // If the key is in the fieldsToRemove object, remove the key and value from the object - for (let i = 0; i < parsedResults.data.length; i++) { - let row = parsedResults.data[i]; - let newRow = {}; - for (let key in row) { - if (row.hasOwnProperty(key)) { - let newKey = key; - let newValue = row[key]; - for (let j = 0; j < newColumnHeaders.length; j++) { - if (key === newColumnHeaders[j].oldField) { - newKey = newColumnHeaders[j].newField; - } - } - if (fieldsToRemove.includes(key)) { - delete row[key]; - } else { - - // Use the fieldList array of objects to get the field type - // Add the new key and value to the new row - // Get the field type from newKey - // If it is a date fields, format them to the correct format yyyy-MM-dd - // If it is a currency field, format it to the correct format 0.00 - // If it is a number field, format it to the correct format 0 - // If it is a percent field, format it to the correct format 0% - - // Find the field type from the fieldList array of objects - // fieldList = [{"name":"Id","type","ID"}] - let fieldType = ''; - for (let k = 0; k < fieldList.length; k++) { - if (fieldList[k].name === newKey) { - fieldType = fieldList[k].type; - } - } - - if (fieldType === 'DATE') { - // Check if the value is not null - // If it is not null, format it to the correct format yyyy-MM-dd - // If null return the value as null - if (newValue === null) { - newRow[newKey] = ''; - } else { - let date = new Date(newValue); - // Check if the date is valid - // If it is not valid, return the value as null - if (isNaN(date)) { - newRow[newKey] = ''; - } else { - let formattedDate = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); - newRow[newKey] = formattedDate; - } - } - } else if (fieldType === 'CURRENCY') { - // Remove the $ and , from the value - let formattedValue = newValue.replace('$', '').replace(',', ''); - - // Check if the value is a number - // If it is a number, format it to the correct format 0.00 - // If not a number return the value as is - if (isNaN(formattedValue)) { - newRow[newKey] = formattedValue; - } else { - formattedValue = parseFloat(formattedValue).toFixed(2); - newRow[newKey] = parseFloat(formattedValue); - } - } else if (fieldType === 'DOUBLE' || fieldType === 'INT' || fieldType === 'LONG' || fieldType === 'PERCENT') { - // Remove the % sign from the value - let formattedValue = newValue.replace('%', ''); - - // Check if the value is a number - // If it is a number, format it to the correct format 0 - // If not a number return the value as is - if (isNaN(formattedValue)) { - newRow[newKey] = formattedValue; - } else { - formattedValue = parseFloat(formattedValue).toFixed(0); - newRow[newKey] = parseFloat(formattedValue); - } - } else { - // Remove character returns from the value - let formattedValue = newValue.replace(/(\r\n|\n|\r)/gm, ''); - // Trim the value - formattedValue = formattedValue.trim(); - // Remove extra spaces from the value - // 1905 CARIBOO HWY N to 1905 CARIBOO HWY N - formattedValue = formattedValue.replace(/\s\s+/g, ' '); - newRow[newKey] = formattedValue; - } - } - } - } - // Add the new row to the newRows array - newRows.push(newRow); - } - - // Go through the newRows and remove any rows that are empty - newRows = newRows.filter(x => Object.keys(x).length > 0); - - // Serialize the data with the objectName - let serializedData = {}; - serializedData[this.objectName] = newRows; - - // Set the outputValue to the serialized data - this._outputValue = serializedData; - this._isLoading = false; - // Set outputValue to the results - this.handleValueChange('outputValue', serializedData); - - // If the autoNavigateNext attribute is true, navigate to the next screen - if (this._autoNavigateNext) { - this.handleNext(); - } - }) - .catch(error => { - this._errorMessage = JSON.stringify(error); - this._isError = true; - this._isLoading = false; - return; - }); - - }, - error: (error) => { - console.error(error); - this._errorMessage = 'Parser Error: ' + error; - this._isError = true; - this._isLoading = false; - return; - } - }) - } - } - - /** - * Handles auto navigation to the next screen/action - * Only navigates if there are no errors and autoNavigateNext is enabled - * - * @returns {void} - */ - handleNext() { - // If there is an error, do not navigate - if (this._isError) { - return; - } else { - if (this._availableActions.find((action) => action === "NEXT")) { - const navigateNextEvent = new FlowNavigationNextEvent(); - this.dispatchEvent(navigateNextEvent); - } - if (this._availableActions.find((action) => action === "FINISH")) { - const navigateNextEvent = new FlowNavigationFinishEvent(); - this.dispatchEvent(navigateNextEvent); - } - } - } - - /** - * Notifies Flow of attribute value changes - * - * @param {string} apiName - The API name of the attribute that changed - * @param {*} value - The new value for the attribute - * @returns {void} - */ - handleValueChange(apiName, value) { - const attributeChangeEvent = new FlowAttributeChangeEvent(apiName, value); - this.dispatchEvent(attributeChangeEvent); + // Initialize the parser + parserInitialized = false; + + // PapaParser Inputs + @api get delimiter() { + return this._delimiter; + } + set delimiter(value) { + this._delimiter = value; + } + _delimiter = ","; + + @api get newline() { + return this._newline; + } + set newline(value) { + this._newline = value; + } + _newline = ""; + + @api get quoteChar() { + return this._quoteChar; + } + set quoteChar(value) { + this._quoteChar = value; + } + _quoteChar = '"'; + + @api get escapeChar() { + return this._escapeChar; + } + set escapeChar(value) { + this._escapeChar = value; + } + _escapeChar = '"'; + + @api get transformHeader() { + return this._transformHeader; + } + set transformHeader(value) { + this._transformHeader = value; + } + _transformHeader = undefined; + + @api get header() { + return this._header; + } + set header(value) { + this._header = value; + } + _header = false; + + @api get dynamicTyping() { + return this._dynamicTyping; + } + set dynamicTyping(value) { + this._dynamicTyping = value; + } + _dynamicTyping = false; + + @api get encoding() { + return this._encoding; + } + set encoding(value) { + this._encoding = value; + } + _encoding = ""; + + @api get comments() { + return this._comments; + } + set comments(value) { + this._comments = value; + } + _comments = false; + + @api get fastMode() { + return this._fastMode; + } + set fastMode(value) { + this._fastMode = value; + } + _fastMode = false; + + @api get skipEmptyLines() { + return this._skipEmptyLines; + } + set skipEmptyLines(value) { + this._skipEmptyLines = value; + } + _skipEmptyLines = false; + + @api get transform() { + return this._transform; + } + set transform(value) { + this._transform = value; + } + _transform = undefined; + + @api get delimitersToGuess() { + return this._delimitersToGuess; + } + set delimitersToGuess(value) { + this._delimitersToGuess = value; + } + _delimitersToGuess = []; + + // Get Label for Input file + @api get inputLabel() { + return this._inputLabel; + } + set inputLabel(value) { + this._inputLabel = value; + } + _inputLabel = "Upload CSV"; + + // Get flow attributes + @api get availableActions() { + return this._availableActions; + } + set availableActions(value) { + this._availableActions = value; + } + _availableActions = []; + + @api get autoNavigateNext() { + return this._autoNavigateNext; + } + set autoNavigateNext(value) { + this._autoNavigateNext = value; + } + _autoNavigateNext = false; + + // Set variables for the screen + @track uploadFileName = ""; + @track uploadedFile = []; + + // Set objectName variable to be used in the getObjectFields Apex method + @api objectName = "Account"; + + // Set the isError variable to false + @api get isError() { + return this._isError; + } + set isError(value) { + this._isError = value; + } + _isError = false; + + // Set the errorMessage variable to an null + @api get errorMessage() { + return this._errorMessage; + } + set errorMessage(value) { + this._errorMessage = value; + } + _errorMessage = null; + + // Store the fileName of the uploaded CSV file + @track fileName = ""; + + // Store the fields for the selected object + @track objectInfo = []; + + // Store Column Headers + @track columnHeaders = []; + + // Store Rows of Data + @track rows = []; + + // Store the output SObject records from the CSV file + @api get outputValue() { + return this._outputValue; + } + set outputValue(value) { + this._outputValue = value; + } + _outputValue = []; + + // Store a Status field to show the user the status of the CSV file + @track uploadFileStatus = ""; + + // Set isLoading to false + @api get isLoading() { + return this._isLoading; + } + set isLoading(value) { + this._isLoading = value; + } + _isLoading = false; + + @api get ignoreMissingColumns() { + return this._ignoreMissingColumns; + } + set ignoreMissingColumns(value) { + this._ignoreMissingColumns = value; + } + _ignoreMissingColumns = false; + + @api get ignoreMissingFields() { + return this._ignoreMissingFields; + } + set ignoreMissingFields(value) { + this._ignoreMissingFields = value; + } + _ignoreMissingFields = false; + + /** + * Lifecycle hook: Initializes the PapaParse library when component is rendered + * @returns {void} + */ + renderedCallback() { + if (!this.parserInitialized) { + loadScript(this, PARSER) + .then(() => { + // Verify PapaParse is available + if (typeof Papa !== "undefined") { + this.parserInitialized = true; + } else { + this._errorMessage = + "PapaParse library failed to load. Please refresh the page."; + this._isError = true; + console.error("PapaParse library not available after loading"); + } + }) + .catch((error) => { + this._errorMessage = + "Failed to load PapaParse library: " + error.message; + this._isError = true; + console.error("PapaParse loading error:", error); + }); } -} \ No newline at end of file + } + + /** + * Handles file input change event and processes the uploaded CSV file + * Parses the CSV using PapaParse, maps columns to Salesforce fields, + * and converts data to sObject format + * + * @param {Event} event - File input change event containing the uploaded file + * @returns {void} + */ + handleInputChange(event) { + // Set Default Values + this.header = true; + this.skipEmptyLines = true; + + if (event.detail.files.length > 0) { + // Ensure PapaParse is loaded before proceeding + if (!this.parserInitialized || typeof Papa === "undefined") { + this._errorMessage = + "PapaParse library is not loaded. Please refresh the page and try again."; + this._isError = true; + this._isLoading = false; + return; + } + + this._isLoading = true; + const file = event.detail.files[0]; + this.loading = true; + Papa.parse(file, { + delimiter: this._delimiter, + newline: this._newline, + quoteChar: this._quoteChar, + escapeChar: this._escapeChar, + transformHeader: this._transformHeader, + header: this._header, + dynamicTyping: this._dynamicTyping, + encoding: this._encoding, + comments: this._comments, + fastMode: this._fastMode, + skipEmptyLines: this._skipEmptyLines, + transform: this._transform, + delimitersToGuess: this._delimitersToGuess, + complete: (parsedResults) => { + // Validate parsedResults + if ( + !parsedResults || + !parsedResults.meta || + !parsedResults.meta.fields + ) { + this._errorMessage = + "Failed to parse CSV file. The file may be empty or invalid."; + this._isError = true; + this._isLoading = false; + return; + } + + // get the meta columns + this.columnHeaders = parsedResults.meta.fields; + + // See if there are any empty columns + let emptyColumns = parsedResults.meta.fields.filter( + (field) => field === "" + ); + + // If there are empty columns, throw an error + if (emptyColumns.length > 0 && !this._ignoreMissingColumns) { + // Set the isError variable to true + this._isError = true; + this._errorMessage = + "There are empty columns in the CSV file. Please remove the empty columns and try again."; + this._isLoading = false; + return; + } else if (emptyColumns.length > 0 && this._ignoreMissingColumns) { + // If there are empty columns, but the user wants to ignore them, remove the empty columns + let emptyFieldNames = parsedResults.meta.fields.filter( + (field, index) => field === "" + ); + parsedResults.meta.fields = parsedResults.meta.fields.filter( + (field) => field !== "" + ); + + // Set the columnHeaders variable to the new columnHeaders array + this.columnHeaders = parsedResults.meta.fields; + + // Remove the empty columns from the data + // When header is true, data is an array of objects, not arrays + if ( + this._header && + Array.isArray(parsedResults.data) && + parsedResults.data.length > 0 && + typeof parsedResults.data[0] === "object" && + !Array.isArray(parsedResults.data[0]) + ) { + // Remove empty field keys from each row object + parsedResults.data = parsedResults.data.map((row) => { + let newRow = {}; + for (let key in row) { + if (row.hasOwnProperty(key) && key !== "") { + newRow[key] = row[key]; + } + } + return newRow; + }); + } else { + // Array-based rows (when header is false) + parsedResults.data = parsedResults.data.map((row) => { + return Array.isArray(row) + ? row.filter((field) => field !== "") + : row; + }); + } + } + getObjectFields({ objectName: this.objectName }) + .then((fieldList) => { + // Validate fieldList + if ( + !fieldList || + !Array.isArray(fieldList) || + fieldList.length === 0 + ) { + this._errorMessage = + "Failed to retrieve field information for object: " + + this.objectName; + this._isError = true; + this._isLoading = false; + return; + } + + // fieldList is an array of objects + // Each object has a Name and a Type property + + // Set new fieldName array + let fieldNames = []; + fieldNames = fieldList.map((field) => field.name); + + // Set new columnHeader array + let newColumnHeaders = []; + + // Set array of fields to remove + let fieldsToRemove = []; + + // Compare the column headers to the fields for the selected object + // If the column header is not a match add __c to the end and recheck the fields + // If the column header is still not a match, remove the column header from the list + for (let i = 0; i < this.columnHeaders.length; i++) { + let columnHeader = this.columnHeaders[i]; + + // Trim the column header + columnHeader = columnHeader.trim(); + + // For standard fields we need to remove the space inbetween the words + // For example: Account Name becomes AccountName + // Create standardField variable to store the new value + let standardField; + if (columnHeader.includes(" ")) { + standardField = columnHeader.replaceAll(" ", ""); + } else { + standardField = columnHeader; + } + if (fieldNames.includes(standardField)) { + newColumnHeaders.push({ + newField: columnHeader, + oldField: columnHeader, + }); + } else { + // Create customField variable to store the new value + let customField; + + // Replace spaces with underscores + customField = columnHeader.replaceAll(" ", "_"); + + // Limit the length of the field to 40 characters + customField = customField.substring(0, 40); + + // Remove return characters + customField = customField.replace(/[\r]/g, ""); + + // If the field starts with a number, add an X to the beginning of the field + if (customField.match(/^[0-9]/)) { + customField = "X" + customField; + } + + // Remove any special characters + // % & * ( ) + - = { } [ ] : ; ' " , . < > ? / | \ ~ ` ! @ # $ ^ + customField = customField.replace(/[^a-zA-Z0-9_]/g, ""); + + // newlines and carriage returns are also removed + customField = customField.replace(/[\r\n]+/gm, ""); + + // Remove any leading or trailing underscores + customField = customField.replace(/^_+|_+$/g, ""); + + // Replace any double underscores with a single underscore + customField = customField.replace(/__+/g, "_"); + + // Replace any triple underscores with a single underscore + customField = customField.replace(/___+/g, "_"); + + // Add __c to the end of the field + customField = customField + "__c"; + + // Validate the field name + if (fieldNames.includes(customField)) { + newColumnHeaders.push({ + newField: customField, + oldField: columnHeader, + }); + } else { + fieldsToRemove.push(columnHeader); + } + } + } + + // If fieldsToRemove is not empty then error out + if (fieldsToRemove.length > 0 && !this._ignoreMissingFields) { + this._errorMessage = + "The following fields are not valid: " + + fieldsToRemove.join(", ") + + ". Please remove them from the CSV file and try again."; + this._isError = true; + this._isLoading = false; + return; + } else { + this._errorMessage = ""; + this._isError = false; + } + + // Check if there are duplicate headers + let duplicateHeaders = []; + for (let i = 0; i < newColumnHeaders.length; i++) { + let columnHeader = newColumnHeaders[i].newField; + if ( + newColumnHeaders.filter((x) => x.newField === columnHeader) + .length > 1 + ) { + duplicateHeaders.push(columnHeader); + } + } + + // If there is a duplicate header then error out + if (duplicateHeaders.length > 0) { + this._errorMessage = + "Duplicate headers found: " + + duplicateHeaders.join(", ") + + ". Please remove the duplicate headers and try again."; + this._isError = true; + this._isLoading = false; + return; + } + + // Validate parsedResults.data + if (!parsedResults.data || !Array.isArray(parsedResults.data)) { + this._errorMessage = "No data found in CSV file."; + this._isError = true; + this._isLoading = false; + return; + } + + // New array to store the rows of data + let newRows = []; + // Go through the parsedResults.data object and set key based on the fieldList object match on oldField and replace the oldField with the newField + // If the key is not in the columnHeaders object, remove the key and value from the object + // If the key is in the fieldsToRemove object, remove the key and value from the object + for (let i = 0; i < parsedResults.data.length; i++) { + let row = parsedResults.data[i]; + if (!row || typeof row !== "object") { + continue; // Skip invalid rows + } + let newRow = {}; + for (let key in row) { + if (row.hasOwnProperty(key)) { + let newKey = key; + let newValue = row[key]; + + // Handle null, undefined, or empty string values + if (newValue === null || newValue === undefined) { + newValue = ""; + } + for (let j = 0; j < newColumnHeaders.length; j++) { + if (key === newColumnHeaders[j].oldField) { + newKey = newColumnHeaders[j].newField; + } + } + if (fieldsToRemove.includes(key)) { + delete row[key]; + } else { + // Use the fieldList array of objects to get the field type + // Add the new key and value to the new row + // Get the field type from newKey + // If it is a date fields, format them to the correct format yyyy-MM-dd + // If it is a currency field, format it to the correct format 0.00 + // If it is a number field, format it to the correct format 0 + // If it is a percent field, format it to the correct format 0% + + // Find the field type from the fieldList array of objects + // fieldList = [{"name":"Id","type","ID"}] + let fieldType = ""; + for (let k = 0; k < fieldList.length; k++) { + if (fieldList[k].name === newKey) { + fieldType = fieldList[k].type; + } + } + + if (fieldType === "DATE") { + // Check if the value is empty or null + if (!newValue || newValue === "" || newValue === null) { + newRow[newKey] = ""; + } else { + // Convert to string for date parsing + let dateStr = String(newValue).trim(); + if (dateStr === "") { + newRow[newKey] = ""; + } else { + let date = new Date(dateStr); + // Check if the date is valid + if (isNaN(date.getTime())) { + newRow[newKey] = ""; + } else { + // Format to yyyy-MM-dd with zero-padding + let year = date.getFullYear(); + let month = String(date.getMonth() + 1).padStart( + 2, + "0" + ); + let day = String(date.getDate()).padStart(2, "0"); + newRow[newKey] = year + "-" + month + "-" + day; + } + } + } + } else if (fieldType === "DATETIME") { + // Handle DateTime fields - format to ISO 8601 + if (!newValue || newValue === "" || newValue === null) { + newRow[newKey] = ""; + } else { + let dateStr = String(newValue).trim(); + if (dateStr === "") { + newRow[newKey] = ""; + } else { + let date = new Date(dateStr); + if (isNaN(date.getTime())) { + newRow[newKey] = ""; + } else { + // Format to ISO 8601: yyyy-MM-ddTHH:mm:ss.000Z + let year = date.getFullYear(); + let month = String(date.getMonth() + 1).padStart( + 2, + "0" + ); + let day = String(date.getDate()).padStart(2, "0"); + let hours = String(date.getHours()).padStart( + 2, + "0" + ); + let minutes = String(date.getMinutes()).padStart( + 2, + "0" + ); + let seconds = String(date.getSeconds()).padStart( + 2, + "0" + ); + newRow[newKey] = + year + + "-" + + month + + "-" + + day + + "T" + + hours + + ":" + + minutes + + ":" + + seconds + + ".000Z"; + } + } + } + } else if (fieldType === "BOOLEAN") { + // Handle Boolean fields + if ( + newValue === null || + newValue === undefined || + newValue === "" + ) { + newRow[newKey] = false; + } else { + let boolStr = String(newValue).trim().toLowerCase(); + // Accept: true, 1, yes, y, on + newRow[newKey] = + boolStr === "true" || + boolStr === "1" || + boolStr === "yes" || + boolStr === "y" || + boolStr === "on"; + } + } else if (fieldType === "CURRENCY") { + // Handle empty values + if (!newValue || newValue === "" || newValue === null) { + newRow[newKey] = null; + } else { + // Remove all $ and all commas from the value + let formattedValue = String(newValue) + .replace(/\$/g, "") + .replace(/,/g, "") + .trim(); + + // Check if the value is a number + // If it is a number, format it to the correct format 0.00 + // If not a number return null + if (formattedValue === "" || isNaN(formattedValue)) { + newRow[newKey] = null; + } else { + formattedValue = + parseFloat(formattedValue).toFixed(2); + newRow[newKey] = parseFloat(formattedValue); + } + } + } else if ( + fieldType === "DOUBLE" || + fieldType === "INT" || + fieldType === "LONG" || + fieldType === "PERCENT" + ) { + // Handle empty values + if (!newValue || newValue === "" || newValue === null) { + newRow[newKey] = null; + } else { + // Remove all % signs from the value + let formattedValue = String(newValue) + .replace(/%/g, "") + .trim(); + + // Check if the value is a number + // If it is a number, format it appropriately + // If not a number return null + if (formattedValue === "" || isNaN(formattedValue)) { + newRow[newKey] = null; + } else { + if (fieldType === "INT" || fieldType === "LONG") { + newRow[newKey] = parseInt(formattedValue, 10); + } else { + formattedValue = + parseFloat(formattedValue).toFixed(0); + newRow[newKey] = parseFloat(formattedValue); + } + } + } + } else { + // Handle text fields - convert to string and clean up + if (newValue === null || newValue === undefined) { + newRow[newKey] = ""; + } else { + // Convert to string + let formattedValue = String(newValue); + // Remove character returns from the value + formattedValue = formattedValue.replace( + /(\r\n|\n|\r)/gm, + "" + ); + // Trim the value + formattedValue = formattedValue.trim(); + // Remove extra spaces from the value + // 1905 CARIBOO HWY N to 1905 CARIBOO HWY N + formattedValue = formattedValue.replace( + /\s\s+/g, + " " + ); + newRow[newKey] = formattedValue; + } + } + } + } + } + // Add the new row to the newRows array + newRows.push(newRow); + } + + // Go through the newRows and remove any rows that are empty + newRows = newRows.filter((x) => Object.keys(x).length > 0); + + // Serialize the data with the objectName + let serializedData = {}; + serializedData[this.objectName] = newRows; + + // Set the outputValue to the serialized data + this._outputValue = serializedData; + this._isLoading = false; + // Set outputValue to the results + this.handleValueChange("outputValue", serializedData); + + // If the autoNavigateNext attribute is true, navigate to the next screen + if (this._autoNavigateNext) { + this.handleNext(); + } + }) + .catch((error) => { + console.error("getObjectFields error:", error); + let errorMsg = "Failed to retrieve field information. "; + if (error && error.body && error.body.message) { + errorMsg += error.body.message; + } else if (error && error.message) { + errorMsg += error.message; + } else { + errorMsg += "Please verify the object name is correct."; + } + this._errorMessage = errorMsg; + this._isError = true; + this._isLoading = false; + return; + }); + }, + error: (error) => { + console.error("PapaParse parsing error:", error); + // Handle different error types + let errorMsg = "Failed to parse CSV file. "; + if (error && error.message) { + errorMsg += error.message; + } else if (typeof error === "string") { + errorMsg += error; + } else { + errorMsg += "Please check the file format and try again."; + } + this._errorMessage = errorMsg; + this._isError = true; + this._isLoading = false; + return; + }, + }); + } + } + + /** + * Handles auto navigation to the next screen/action + * Only navigates if there are no errors and autoNavigateNext is enabled + * + * @returns {void} + */ + handleNext() { + // If there is an error, do not navigate + if (this._isError) { + return; + } else { + if (this._availableActions.find((action) => action === "NEXT")) { + const navigateNextEvent = new FlowNavigationNextEvent(); + this.dispatchEvent(navigateNextEvent); + } + if (this._availableActions.find((action) => action === "FINISH")) { + const navigateNextEvent = new FlowNavigationFinishEvent(); + this.dispatchEvent(navigateNextEvent); + } + } + } + + /** + * Notifies Flow of attribute value changes + * + * @param {string} apiName - The API name of the attribute that changed + * @param {*} value - The new value for the attribute + * @returns {void} + */ + handleValueChange(apiName, value) { + const attributeChangeEvent = new FlowAttributeChangeEvent(apiName, value); + this.dispatchEvent(attributeChangeEvent); + } +} From 17ed54dea5b1aa7db3b7655ccd60a53e6d2234e7 Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:41:21 -0600 Subject: [PATCH 04/13] Add file size validation and improve error handling - Add 3MB file size check before processing to prevent payload limit errors - Remove maxRows property and row count validation (simplified approach) - Improve error messages with clear, actionable solutions - Replace Salesforce Data Loader references with Jetstream (http://getjetstream.app/) - Remove emojis from error messages for professional appearance - Add comprehensive error handling for all failure scenarios - Update CPE to remove maxRows configuration option - Update metadata to remove maxRows property - Add payload size validation (3.5MB) after processing as final check --- .../lwcConvertCSVToRecords.html | 9 +- .../lwcConvertCSVToRecords.js | 110 +++++++++++++++++- .../lwcConvertCSVToRecords.js-meta.xml | 84 ++++++++----- 3 files changed, 165 insertions(+), 38 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html index f07c3bccb..4323a01a0 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html @@ -16,12 +16,11 @@ diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js index 6336b54e8..4b2cd5793 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js @@ -278,6 +278,10 @@ export default class lwcConvertCSVToRecords extends LightningElement { * @returns {void} */ handleInputChange(event) { + // Reset error state when a new file is selected + this._isError = false; + this._errorMessage = null; + // Set Default Values this.header = true; this.skipEmptyLines = true; @@ -294,6 +298,35 @@ export default class lwcConvertCSVToRecords extends LightningElement { this._isLoading = true; const file = event.detail.files[0]; + + /** + * Check raw file size before processing + * + * Note: CSV files are typically more compact than the resulting JSON payload. + * A 3MB CSV might become a 4-5MB JSON payload after processing. + * We check at 3MB to provide a safety margin before hitting the ~4MB payload limit. + * The actual payload size is validated later after processing. + */ + const maxFileSizeMB = 3; + const fileSizeMB = file.size / (1024 * 1024); + + if (fileSizeMB > maxFileSizeMB) { + this._errorMessage = + `File Too Large: The CSV file is ${fileSizeMB.toFixed( + 2 + )}MB, which exceeds the recommended limit of ${maxFileSizeMB}MB. ` + + `\n\nNote: CSV files expand when converted to JSON format. A ${fileSizeMB.toFixed( + 2 + )}MB CSV file may exceed Salesforce's ~4MB payload limit after processing. ` + + `\n\nSolutions:\n` + + `• Split your CSV file into smaller files\n` + + `• Use Jetstream for large imports (http://getjetstream.app/)\n` + + `• The file will be checked again after processing for actual payload size`; + this._isError = true; + this._isLoading = false; + return; + } + this.loading = true; Papa.parse(file, { delimiter: this._delimiter, @@ -747,6 +780,50 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Go through the newRows and remove any rows that are empty newRows = newRows.filter((x) => Object.keys(x).length > 0); + /** + * Estimate final payload size before serialization + * + * This check helps prevent "aura:systemError" errors that occur when + * the serialized JSON payload exceeds Salesforce's limits. + * + * Known limits (from community knowledge, not officially documented): + * - Lightning component payload: ~4MB + * - Flow variable size: Practical limits exist but not explicitly documented + * + * We check at 3.5MB to provide a safety margin before hitting the actual limit. + * + * References: + * - StackExchange: https://salesforce.stackexchange.com/questions/219235/what-is-the-limitation-on-size-of-list-attribute-in-lightning-component + * - Flow variable size limits are not explicitly documented by Salesforce + */ + try { + const testSerialization = JSON.stringify(newRows); + const estimatedSizeMB = + testSerialization.length / (1024 * 1024); + + if (estimatedSizeMB > 3.5) { + // Too large - reject before attempting to pass to Flow + this._errorMessage = + `Data Too Large: The processed data is ${estimatedSizeMB.toFixed( + 2 + )}MB, which exceeds Salesforce's payload limits (~4MB). ` + + `Your CSV file contains ${newRows.length.toLocaleString()} rows. ` + + `\n\nSolutions:\n` + + `• Split your CSV file into smaller files (under 3MB)\n` + + `• Reduce the number of columns in your CSV file\n` + + `• Use Jetstream for large imports (http://getjetstream.app/)`; + this._isError = true; + this._isLoading = false; + return; + } + } catch (serializationError) { + console.error( + "Error estimating payload size:", + serializationError + ); + // Continue anyway - the actual error will be caught below + } + // Serialize the data with the objectName let serializedData = {}; serializedData[this.objectName] = newRows; @@ -754,8 +831,25 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Set the outputValue to the serialized data this._outputValue = serializedData; this._isLoading = false; - // Set outputValue to the results - this.handleValueChange("outputValue", serializedData); + + // Set outputValue to the results with error handling + try { + this.handleValueChange("outputValue", serializedData); + } catch (error) { + // Catch any serialization or payload size errors + console.error("Error setting outputValue:", error); + this._errorMessage = + `Processing Error: The data is too large for Salesforce to handle. ` + + `Your CSV file contains ${newRows.length.toLocaleString()} rows. ` + + `\n\nSolutions:\n` + + `• Split your CSV file into smaller files (under 3MB)\n` + + `• Reduce the number of columns in your CSV file\n` + + `• Use Jetstream for large imports (http://getjetstream.app/)\n\n` + + `Technical details: ${error.message || error}`; + this._isError = true; + this._isLoading = false; + return; + } // If the autoNavigateNext attribute is true, navigate to the next screen if (this._autoNavigateNext) { @@ -780,15 +874,21 @@ export default class lwcConvertCSVToRecords extends LightningElement { }, error: (error) => { console.error("PapaParse parsing error:", error); - // Handle different error types - let errorMsg = "Failed to parse CSV file. "; + // Handle different error types with user-friendly messages + let errorMsg = "CSV Parsing Error: "; if (error && error.message) { errorMsg += error.message; } else if (typeof error === "string") { errorMsg += error; } else { - errorMsg += "Please check the file format and try again."; + errorMsg += "Unable to parse the CSV file. "; } + errorMsg += + `\n\nPlease check:\n` + + `• The file is a valid CSV format\n` + + `• The delimiter matches your data (comma, semicolon, tab, etc.)\n` + + `• The file is not corrupted\n` + + `• The file encoding is correct (UTF-8 recommended)`; this._errorMessage = errorMsg; this._isError = true; this._isLoading = false; diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml index 5e2d5189d..42a7125be 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js-meta.xml @@ -1,38 +1,66 @@ - 65.0 - true - Convert CSV to Records - High-volume CSV to Records conversion component that processes CSV files on the client side using PapaParse. Supports large file uploads with automatic field mapping and comprehensive error handling. - - lightning__FlowScreen - - - + 65.0 + true + Convert CSV to Records + High-volume CSV to Records conversion component that processes CSV files on the client side using PapaParse. Supports large file uploads with automatic field mapping and comprehensive error handling. + + lightning__FlowScreen + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - + + + - - + + - - + + \ No newline at end of file From f7922f7d67fbe0e0697e6cd6443fd2702b13850c Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:42:30 -0600 Subject: [PATCH 05/13] Add SVG icon for CPE component - Add matching SVG icon for lwcConvertCSVToRecordsCPE component - Icon matches the main component design for visual consistency - Shows CSV file with grid lines, arrow, and database icon --- .../lwcConvertCSVToRecordsCPE.svg | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg new file mode 100644 index 000000000..b641f8097 --- /dev/null +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0abef349095956d18bd52cee098970715e95cd56 Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:44:52 -0600 Subject: [PATCH 06/13] Fix SVG icons to meet Salesforce Flow Builder requirements - Add id attribute to svg elements - Remove defs and style sections (not allowed by Salesforce) - Replace CSS classes with direct fill attributes - Ensure all paths have fill attributes --- .../lwcConvertCSVToRecords.svg | 29 +++++++------------ .../lwcConvertCSVToRecordsCPE.svg | 29 +++++++------------ 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg index b641f8097..4ac1fe65d 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg @@ -1,31 +1,24 @@ - - - - + - + - + - - - - + + + + - - + + - - + + diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg index b641f8097..096504347 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg @@ -1,31 +1,24 @@ - - - - + - + - + - - - - + + + + - - + + - - + + From c28e8f54ef7a3054d200407f996823f45e6ecd3c Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:47:37 -0600 Subject: [PATCH 07/13] Fix loading state bug - use _isLoading instead of loading - Fixed incorrect property assignment at line 330 - Changed 'this.loading = true' to 'this._isLoading = true' - Ensures loading spinner displays correctly during file processing - All 16 loading state assignments now consistently use _isLoading --- .../lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js index 4b2cd5793..7ae431c83 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js @@ -327,7 +327,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { return; } - this.loading = true; + this._isLoading = true; Papa.parse(file, { delimiter: this._delimiter, newline: this._newline, From caf2c76cf1b244535d073b7bc911252f4bf4fced Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:57:06 -0600 Subject: [PATCH 08/13] Fix file upload display - show file name and status - Set uploadFileName when file is selected to display file name - Set uploadFileStatus throughout processing lifecycle: * 'Processing...' when file is selected * 'Success: X rows processed' on successful completion * Error messages for all error scenarios - Reset file name and status when new file is selected - Provides visual feedback to users about file upload and processing status --- .../lwcConvertCSVToRecords.js | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js index 7ae431c83..ea14b3eb9 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js @@ -281,23 +281,31 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Reset error state when a new file is selected this._isError = false; this._errorMessage = null; + this.uploadFileName = ""; + this.uploadFileStatus = ""; // Set Default Values this.header = true; this.skipEmptyLines = true; if (event.detail.files.length > 0) { + const file = event.detail.files[0]; + + // Display the file name immediately + this.uploadFileName = file.name; + this.uploadFileStatus = "Processing..."; + // Ensure PapaParse is loaded before proceeding if (!this.parserInitialized || typeof Papa === "undefined") { this._errorMessage = "PapaParse library is not loaded. Please refresh the page and try again."; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Library not loaded"; return; } this._isLoading = true; - const file = event.detail.files[0]; /** * Check raw file size before processing @@ -324,6 +332,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { `• The file will be checked again after processing for actual payload size`; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: File too large"; return; } @@ -353,6 +362,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { "Failed to parse CSV file. The file may be empty or invalid."; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Invalid file"; return; } @@ -371,6 +381,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { this._errorMessage = "There are empty columns in the CSV file. Please remove the empty columns and try again."; this._isLoading = false; + this.uploadFileStatus = "Error: Empty columns found"; return; } else if (emptyColumns.length > 0 && this._ignoreMissingColumns) { // If there are empty columns, but the user wants to ignore them, remove the empty columns @@ -521,6 +532,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { ". Please remove them from the CSV file and try again."; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Invalid fields"; return; } else { this._errorMessage = ""; @@ -547,6 +559,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { ". Please remove the duplicate headers and try again."; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Duplicate headers"; return; } @@ -555,6 +568,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { this._errorMessage = "No data found in CSV file."; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: No data found"; return; } @@ -814,6 +828,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { `• Use Jetstream for large imports (http://getjetstream.app/)`; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Data too large"; return; } } catch (serializationError) { @@ -835,6 +850,8 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Set outputValue to the results with error handling try { this.handleValueChange("outputValue", serializedData); + // Update status on successful completion + this.uploadFileStatus = `Success: ${newRows.length.toLocaleString()} rows processed`; } catch (error) { // Catch any serialization or payload size errors console.error("Error setting outputValue:", error); @@ -848,6 +865,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { `Technical details: ${error.message || error}`; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Processing failed"; return; } @@ -869,6 +887,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { this._errorMessage = errorMsg; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Field retrieval failed"; return; }); }, @@ -892,6 +911,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { this._errorMessage = errorMsg; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Parsing failed"; return; }, }); From cd62957e296bc736b6282dac8682e3d368feccd3 Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 18:59:49 -0600 Subject: [PATCH 09/13] Add detailed processing status updates during file upload - Show status messages at each processing stage: * 'Reading file...' when file is selected * 'Parsing CSV...' when parsing starts * 'Validating columns...' when checking empty columns * 'Retrieving field information...' when calling Apex * 'Mapping fields...' when comparing headers to object fields * 'Converting data...' when processing rows and field types * 'Validating payload size...' when checking size limits * 'Finalizing...' when setting output value * 'Success: X rows processed' on completion - Provides real-time feedback to users about processing progress - Improves user experience by showing what stage the component is at --- .../lwcConvertCSVToRecords.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js index ea14b3eb9..29ba94408 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.js @@ -290,10 +290,10 @@ export default class lwcConvertCSVToRecords extends LightningElement { if (event.detail.files.length > 0) { const file = event.detail.files[0]; - + // Display the file name immediately this.uploadFileName = file.name; - this.uploadFileStatus = "Processing..."; + this.uploadFileStatus = "Reading file..."; // Ensure PapaParse is loaded before proceeding if (!this.parserInitialized || typeof Papa === "undefined") { @@ -336,7 +336,7 @@ export default class lwcConvertCSVToRecords extends LightningElement { return; } - this._isLoading = true; + this.uploadFileStatus = "Parsing CSV..."; Papa.parse(file, { delimiter: this._delimiter, newline: this._newline, @@ -369,6 +369,8 @@ export default class lwcConvertCSVToRecords extends LightningElement { // get the meta columns this.columnHeaders = parsedResults.meta.fields; + this.uploadFileStatus = "Validating columns..."; + // See if there are any empty columns let emptyColumns = parsedResults.meta.fields.filter( (field) => field === "" @@ -423,6 +425,8 @@ export default class lwcConvertCSVToRecords extends LightningElement { }); } } + + this.uploadFileStatus = "Retrieving field information..."; getObjectFields({ objectName: this.objectName }) .then((fieldList) => { // Validate fieldList @@ -436,9 +440,12 @@ export default class lwcConvertCSVToRecords extends LightningElement { this.objectName; this._isError = true; this._isLoading = false; + this.uploadFileStatus = "Error: Field retrieval failed"; return; } + this.uploadFileStatus = "Mapping fields..."; + // fieldList is an array of objects // Each object has a Name and a Type property @@ -572,6 +579,8 @@ export default class lwcConvertCSVToRecords extends LightningElement { return; } + this.uploadFileStatus = "Converting data..."; + // New array to store the rows of data let newRows = []; // Go through the parsedResults.data object and set key based on the fieldList object match on oldField and replace the oldField with the newField @@ -794,6 +803,8 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Go through the newRows and remove any rows that are empty newRows = newRows.filter((x) => Object.keys(x).length > 0); + this.uploadFileStatus = "Validating payload size..."; + /** * Estimate final payload size before serialization * @@ -839,6 +850,8 @@ export default class lwcConvertCSVToRecords extends LightningElement { // Continue anyway - the actual error will be caught below } + this.uploadFileStatus = "Finalizing..."; + // Serialize the data with the objectName let serializedData = {}; serializedData[this.objectName] = newRows; From c2394a002661be4601e527dcdd1ac53b923405f5 Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 19:00:41 -0600 Subject: [PATCH 10/13] Show status messages alongside loading spinner - Display file name and status messages while loading spinner is visible - Users can now see processing status updates in real-time during file processing - Improved visual feedback by showing status alongside spinner - Error messages display regardless of loading state --- .../lwcConvertCSVToRecords.html | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html index 4323a01a0..d61237264 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html @@ -1,6 +1,12 @@
+
{uploadFileName}
+
{uploadFileStatus}
+
+ + \ No newline at end of file From cb28548cc71604f6088e7b765fd3efaaa0b86ff0 Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 19:02:41 -0600 Subject: [PATCH 11/13] Modernize HTML template with proper Salesforce components - Remove incorrect use of lightning-layout-item without parent lightning-layout - Replace with proper div structure using SLDS utility classes - Improve visual hierarchy with proper spacing and text styling - Use modern SLDS classes (slds-text-heading_small, slds-text-color_weak) - Center loading spinner for better UX - Add conditional rendering for file name display - Improve error message styling with proper padding - Follow modern Lightning Web Component best practices --- .../lwcConvertCSVToRecords.html | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html index d61237264..5513b70a5 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html @@ -1,35 +1,48 @@ \ No newline at end of file From 8d63e844383e084a4302c7d9c821aad922d7db4f Mon Sep 17 00:00:00 2001 From: Andy Haas Date: Tue, 16 Dec 2025 19:03:38 -0600 Subject: [PATCH 12/13] Hide file input after successful upload - Add isUploadSuccessful getter to detect successful uploads - Hide file input component when upload is successful - Show success message without file input after successful processing - File input remains visible for errors or when no file has been uploaded - Improves UX by clearly indicating successful completion --- .../lwcConvertCSVToRecords.html | 18 ++++++++++-------- .../lwcConvertCSVToRecords.js | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html index 5513b70a5..f89f5424d 100644 --- a/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html @@ -16,14 +16,16 @@