@shopgate/engage
Version:
Shopgate's ENGAGE library.
252 lines (242 loc) • 8.43 kB
JavaScript
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;
};