UNPKG

@shopgate/engage

Version:
252 lines (242 loc) • 8.43 kB
import "core-js/modules/es.array.reduce.js"; import "core-js/modules/es.string.replace.js"; import sortBy from 'lodash/sortBy'; import setWith from 'lodash/setWith'; import { i18n } from '@shopgate/engage/core'; /** * @param {Object} attribute Customer attribute. * @returns {string} */ const mapCustomerAttributeType = attribute => { if (attribute.values && attribute.values.length) { if (attribute.type === 'collectionOfValues') { return 'multiselect'; } return 'select'; } switch (attribute.type) { case 'number': return 'number'; case 'boolean': return 'checkbox'; case 'date': return 'date'; case 'callingNumber': return 'phone_picker'; default: return 'text'; } }; /** * Generates customer attributes form field config * @param {Object} options Options for the helper * @param {Array} options.customerAttributes Customer attributes. * @param {Array} options.supportedCountries A list of supported countries. * @param {Array} options.countrySortOrder Sort order for supported countries. * @param {Object} options.userLocation User location for better phone picker defaults. * @param {boolean} options.allowPleaseChoose Allows please choose option for required attributes. * @returns {Object} */ export const generateCustomerAttributesFields = ({ customerAttributes, supportedCountries, countrySortOrder, userLocation, allowPleaseChoose = true }) => ({ ...Object.assign.apply(Object, [{}].concat(sortBy(customerAttributes, ['sequenceId']).map(attribute => ({ [`attribute_${attribute.code}`]: { type: mapCustomerAttributeType(attribute), label: `${attribute.name} ${attribute.isRequired ? '*' : ''}`, ...(attribute.values ? { options: { // For non required property allow the user to unset it. ...(!attribute.isRequired ? { '': '' } : {}), ...(attribute.isRequired && attribute.type !== 'collectionOfValues' && allowPleaseChoose ? { '': i18n.text('common.please_choose') } : {}), // Create regular options. ...Object.assign.apply(Object, [{}].concat(sortBy(attribute.values, ['sequenceId']).map(option => ({ [option.code]: option.name })))) } } : {}), ...(attribute.type === 'callingNumber' ? { config: { supportedCountries, countrySortOrder, userLocation } } : null) } })))) }); /** * Generates form constraints for attributes. * @param {Object} customerAttributes Customer attributes. * @returns {Object} */ export const generateFormConstraints = customerAttributes => ({ ...Object.assign.apply(Object, [{}].concat(customerAttributes.map(attribute => { const constraint = {}; if (attribute.isRequired || attribute.type === 'date') { constraint[`attribute_${attribute.code}`] = {}; } if (attribute.isRequired) { constraint[`attribute_${attribute.code}`].presence = { message: 'validation.required', allowEmpty: false }; } if (attribute.type === 'date') { constraint[`attribute_${attribute.code}`].datetime = { dateOnly: true, message: 'validation.date' }; } return constraint; }))) }); /** * Maps the type of an attribute value. * @param {Object|string|string[]|number} value Attribute value. * @param {*} attribute The attribute configuration. * @returns {Object|string|number} */ const mapAttributeType = (value, attribute) => { // Multi select if (attribute.type === 'collectionOfValues') { return (value || []).map(v => ({ code: v })); } // Single select. if (attribute.values?.length) { return { code: value.toString() }; } // Number type. if (attribute.type === 'number') { return value.length ? parseFloat(value.replace(',', '.') || 0) : null; } // Text types (date is just a formatted text) if (attribute.type === 'text' || attribute.type === 'date') { return value !== null ? value.toString() : ''; } return value; }; /** * Extracts attributes from form data as expected by the API. * @param {Object} customerAttributes Customer attributes. * @param {Object} formData Form data. * @returns {Object} */ export const extractAttributes = (customerAttributes, formData) => customerAttributes.map(attribute => ({ code: attribute.code, value: mapAttributeType(formData[`attribute_${attribute.code}`], attribute) })) // Removes wrong numbers (sometimes generated by the form builder when emptying the field) .filter(attribute => !Number.isNaN(attribute.value)) // Removes all attributes that are empty / no longer set. .filter(attribute => // Any number. typeof attribute.value === 'number' || // Non-empty strings. typeof attribute.value === 'string' && attribute.value.length || // Booleans are always allowed. attribute.value === true || attribute.value === false || // Object containing the code. !Array.isArray(attribute.value?.code) && attribute.value?.code?.length || // Array containing at least one code. attribute.value?.[0]?.code?.length); /** * Extracts the default values for the form * @param {Object} customerAttributes Customer attributes. * @returns {Object} */ export const extractDefaultValues = customerAttributes => Object.assign.apply(Object, [{}].concat(customerAttributes.map(attribute => { let { value } = attribute; if (value) { if (Array.isArray(value) && value[0] && typeof value[0] === 'object') { // Multi select L:95 value = value.reduce((acc, val) => [].concat(acc, [val.code]), []); } else if (typeof value === 'object') { // Single select L:100 value = value.code; } else if (value !== true && value !== false) { value = value.toString(); } } return { [`attribute_${attribute.code}`]: value }; }))); /** * Converts pipeline validation errors * @param {Array} errors The errors * @param {Array} attributes Attributes that where sent with the register request. Used to retrieve * extra form field for attribute validation errors. * @returns {Object|null} */ export const convertPipelineValidationErrors = (errors, attributes = []) => { if (!Array.isArray(errors) || errors.length === 0) { return null; } const converted = errors.reduce((result, error) => { const { path = [], code, subentityPath = [], displayMessage } = error; let { message } = error; if (path.length === 0 && subentityPath.length === 0) { result.general.push(error); return result; } let validationPath; if (path.length > 0) { message = i18n.text('validation.checkField'); validationPath = path.slice(2).join('.'); } else if (subentityPath.length > 0 && subentityPath.includes('attributes')) { /** * Validation errors for customer attributes needs special handling. They are sent * as an array to the server, which only includes the fields where an actual value was set. * The subentityPath of attribute validation errors will only contain an array index which * needs to be mapped to tha actual field id. * * So here in the first step, we search for the subentity path entry that comes after the * "attributes" entry and convert it back to an integer which can be used to determine * and entry within the attribute data that was used for the request. */ const attributeIndex = parseInt(subentityPath[subentityPath.indexOf('attributes') + 1], 10); message = i18n.text('validation.checkField'); // Retrieve the attribute code to mock a validation path that can be used to find the correct // form field. validationPath = `attributes.attribute_${attributes[attributeIndex].code}`; } else if (subentityPath.length > 0) { const field = subentityPath[subentityPath.length - 1]; if (code === 409 && field === 'emailAddress') { message = i18n.text('validation.emailConflict'); } else { message = i18n.text('validation.checkField'); } validationPath = subentityPath.join('.'); } if (validationPath) { setWith(result.validation, validationPath, displayMessage || message, Object); } return result; }, { validation: {}, general: [] }); return converted; };