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.html b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.html index f07c3bccb..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 @@ -1,28 +1,50 @@ \ No newline at end of file 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..175a70dd5 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,585 +1,977 @@ -import { LightningElement, track, api } from 'lwc'; +/** + * 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, - 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 delimiterToGuess(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; - - - // Initialize the parser - renderedCallback() { - if(!this.parserInitialized){ - loadScript(this, PARSER) - .then(() => { - this.parserInitialized = true; - }) - .catch(error => console.error(error)); - } - } - - handleInputChange(event){ - // Set Defualt Values - this.header = true; - this.skipEmptyLines = true; - - if(event.detail.files.length > 0){ - 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) => { - console.log('results: ' + JSON.stringify(parsedResults)); - - // get the medta columns - this._columnHeaders = parsedResults.meta.fields; - console.log('columnHeaders: ' + JSON.stringify(this._columnHeaders)); - - // 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; - 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; - - // 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 - - // 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(); - - 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 - let standardField; - if (columnHeader.includes(' ')) { - standardField = columnHeader.replaceAll(' ', ''); - } else { - standardField = columnHeader; - } - if (fieldNames.includes(standardField)) { - console.log('standard field: ' + columnHeader); - 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)) { - 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) { - 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.'); - } - - // 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; - console.log('Duplicate headers found: ' + duplicateHeaders.join(', ') + '. Please remove the duplicate headers and try again.'); - 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 - console.log('parsedResults.length: ' + parsedResults.data.length); - 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) { - 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 - // 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; - } - } - - 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 - // 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)) { - 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') { - // 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); - - // Set the rows of data - console.log('newRows: ' + JSON.stringify(newRows)); - - // Seralize 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; - this._isLoading = false; - // 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; - return; - }); - - }, - error: (error) => { - console.error(error); - this._errorMessage = 'Parser Error: ' + error; - this._isError = true; - this._isLoading = false; - return; - } - }) - } - } - - // Handle auto navigation to the next screen/action - handleNext() { - // If there is an error, do not navigate - console.log('handleNext: ' + this._isError); - 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); - } - } - } - - 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; + + /** + * Determines if the upload was successful + * @returns {boolean} True if uploadFileStatus indicates success + */ + get isUploadSuccessful() { + return ( + this.uploadFileStatus && this.uploadFileStatus.startsWith("Success:") + ); + } + + @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) { + // 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 = "Reading file..."; + + // 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; + + /** + * 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; + this.uploadFileStatus = "Error: File too large"; + return; + } + + this.uploadFileStatus = "Parsing CSV..."; + 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; + this.uploadFileStatus = "Error: Invalid file"; + return; + } + + // 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 === "" + ); + + // 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; + 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 + 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; + }); + } + } + + this.uploadFileStatus = "Retrieving field information..."; + 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; + 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 + + // 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; + this.uploadFileStatus = "Error: Invalid fields"; + 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; + this.uploadFileStatus = "Error: Duplicate headers"; + 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; + this.uploadFileStatus = "Error: No data found"; + 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 + // 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); + + this.uploadFileStatus = "Validating payload size..."; + + /** + * 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; + this.uploadFileStatus = "Error: Data too large"; + return; + } + } catch (serializationError) { + console.error( + "Error estimating payload size:", + serializationError + ); + // Continue anyway - the actual error will be caught below + } + + this.uploadFileStatus = "Finalizing..."; + + // 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 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); + 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; + this.uploadFileStatus = "Error: Processing failed"; + return; + } + + // 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; + this.uploadFileStatus = "Error: Field retrieval failed"; + return; + }); + }, + error: (error) => { + console.error("PapaParse parsing error:", error); + // 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 += "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; + this.uploadFileStatus = "Error: Parsing failed"; + 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); + } +} 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..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 @@ - 55.0 - true - Convert CSV to Records - LWC version - This will make the parsing on the client side and not on the APEX side - - 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 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..4ac1fe65d --- /dev/null +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecords/lwcConvertCSVToRecords.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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/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..096504347 --- /dev/null +++ b/flow_screen_components/ConvertCSVToRecords_LWC/force-app/main/default/lwc/lwcConvertCSVToRecordsCPE/lwcConvertCSVToRecordsCPE.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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