@atlaskit/editor-plugin-extension
Version:
editor-plugin-extension plugin for @atlaskit/editor-core
374 lines (368 loc) • 12 kB
JavaScript
import { getFieldDeserializer, getFieldSerializer, isDateRange, isExpand, isFieldset, isTabField, isTabGroup } from '@atlaskit/editor-common/extensions';
import { getNameFromDuplicateField, isDuplicateField } from './utils';
const isOption = option => {
return option && typeof option === 'object' && 'label' in option && 'value' in option;
};
const isOptions = options => {
return Array.isArray(options) && options.every(isOption);
};
/** maps the typed-values from the Form values object */
function extract(value, field, options) {
if (isOptions(value)) {
return value.map(item => item.value);
} else if (isOption(value)) {
return value.value;
} else if (isDateRange(value)) {
return value;
} else if (value !== undefined && field.type === 'number') {
if (value === '') {
return;
}
return Number(value);
}
// Workaround for https://product-fabric.atlassian.net/browse/DST-2701
else if (options !== null && options !== void 0 && options.useDefaultValue && value === undefined && 'defaultValue' in field) {
return field.defaultValue;
}
return value;
}
export const findDuplicateFields = fields => findDuplicateFieldsInternal(flattenFields(fields));
const findDuplicateFieldsInternal = fields => {
const allowDuplicatesMap = {};
return fields.find(field => {
if (isExpand(field)) {
return findDuplicateFieldsInternal(field.fields);
} else if (isTabGroup(field)) {
return field.fields.find(tabField => findDuplicateFieldsInternal(tabField.fields));
} else if (allowDuplicatesMap[field.name] === undefined) {
allowDuplicatesMap[field.name] = !!field.allowDuplicates;
return;
} else if (!field.allowDuplicates || !allowDuplicatesMap[field.name]) {
return field;
}
return;
});
};
export const serialize = async (manifest, data, fields, options = {}) => {
const result = [];
const {
depth = 0,
parentType
} = options;
const flattenedFields = flattenFields(fields);
const fillResults = flattenedFields.map(async field => {
if (isTabGroup(field)) {
const tabGroupData = await serializeTabGroupField(manifest, field, data);
result.push(...tabGroupData);
} else if (isTabField(field)) {
const tabData = await serializeTabField(manifest, field, data);
result.push(...tabData);
} else if (isExpand(field)) {
const expandData = await serializeExpandField(manifest, field, data);
result.push(...expandData);
}
// WARNING: don't recursively serialize, limit to depth < 1
// serializable?
else if (isFieldset(field) && depth === 0) {
const fieldsetData = await serializeFieldset(manifest, field, data, depth);
if (fieldsetData) {
result.push(fieldsetData);
}
} else {
const value = extract(data[field.name], field, {
useDefaultValue: true
});
// ignore undefined values
if (value !== undefined) {
result.push({
[field.name]: value
});
}
}
});
await Promise.all(fillResults);
// Crunch fields down to parameters
const parameters = result.reduce((obj, current) => {
// eslint-disable-next-line guard-for-in
for (const key in current) {
obj[key] = current[key];
}
return obj;
}, {});
// Fix up duplicate values (currently only for fieldsets)
const hasDuplicateFields = parentType === 'fieldset' && !!flattenedFields.find(field => field.allowDuplicates);
if (hasDuplicateFields) {
return serializeMergeDuplicateFieldData(parameters, data, flattenedFields);
}
return parameters;
};
const serializeFieldset = async (manifest, field, data, depth) => {
let fieldSerializer;
try {
fieldSerializer = getFieldSerializer(manifest, field.options.transformer);
} catch (ex) {
// We only throw if there is data that may be lost
if (data[field.name] !== undefined) {
throw ex;
}
}
if (!fieldSerializer) {
return;
}
const {
fields: fieldsetFields
} = field;
const fieldParams = extract(data[field.name], field, {
useDefaultValue: true
}) || {};
const extracted = await serialize(manifest, fieldParams, fieldsetFields, {
depth: depth + 1,
parentType: 'fieldset'
});
return {
[field.name]: fieldSerializer(extracted)
};
};
const serializeExpandField = async (manifest, field, data) => {
const expandData = field.hasGroupedValues ? data[field.name] || {} : data;
const value = await serialize(manifest, expandData, field.fields, {
parentType: 'expand'
});
const results = [];
if (!field.hasGroupedValues) {
// eslint-disable-next-line guard-for-in
for (const fieldName in value) {
results.push({
[fieldName]: value[fieldName]
});
}
} else {
results.push({
[field.name]: value
});
}
return results;
};
const resolveTabValues = async (manifest, tabField, groupData) => {
const tabFieldParams = tabField.hasGroupedValues ? groupData[tabField.name] || {} : groupData;
return await serialize(manifest, tabFieldParams, tabField.fields, {
parentType: 'tab'
});
};
const serializeTabGroupField = async (manifest, field, data) => {
const {
fields: tabs
} = field;
const results = [];
const value = {};
for (let i = 0; i < tabs.length; i++) {
const tabField = tabs[i];
const tabFieldParameters = await resolveTabValues(manifest, tabField, field.hasGroupedValues ? data[field.name] || {} : data);
if (tabField.hasGroupedValues) {
// Keep namespaced by tab
value[tabField.name] = tabFieldParameters;
} else {
// Copy into tabGroup value
// eslint-disable-next-line guard-for-in
for (const fieldName in tabFieldParameters) {
value[fieldName] = tabFieldParameters[fieldName];
}
}
}
// Now for tabGroup...
if (field.hasGroupedValues) {
results.push({
[field.name]: value
});
} else {
// eslint-disable-next-line guard-for-in
for (const fieldName in value) {
results.push({
[fieldName]: value
});
}
}
return results;
};
const serializeTabField = async (manifest, field, data) => {
const results = [];
const tabField = field;
const tabFieldParameters = await resolveTabValues(manifest, tabField, data);
if (tabField.hasGroupedValues) {
// Keep namespaced by tab
results.push({
[tabField.name]: tabFieldParameters
});
} else {
// Copy into tabGroup value
// eslint-disable-next-line guard-for-in
for (const fieldName in tabFieldParameters) {
results.push({
[fieldName]: tabFieldParameters[fieldName]
});
}
}
return results;
};
const serializeMergeDuplicateFieldData = (parameters, formData, flattenedFields) => {
// Weed out all the non-duplicate field names
const allDuplicateFieldNames = Object.keys(formData).filter(key => isDuplicateField(key));
return flattenedFields.reduce((newParams, field) => {
const paramValue = parameters[field.name];
if (!field.allowDuplicates && paramValue !== undefined) {
newParams[field.name] = paramValue;
} else {
// extract the given duplicate values through the field
const duplicateValues = allDuplicateFieldNames.filter(name => getNameFromDuplicateField(name) === field.name).map(duplicateFieldName => extract(formData[duplicateFieldName], field, {
useDefaultValue: true
}));
// Merge and ensure that all values are worth serializing
const mergedValues = [paramValue,
// first value
...duplicateValues].filter(value => value !== undefined);
if (mergedValues.length > 0) {
// Replace so the duplicate field values are saved under the
// fieldName as an array
newParams[field.name] = mergedValues;
}
}
return newParams;
}, {});
};
function injectDefaultValues(data, fields) {
const copy = [...convertToParametersArray(data)];
for (const field of fields) {
const {
name
} = field;
const fieldIndex = copy.findIndex(item => Object.entries(item)[0][0] === name);
if (fieldIndex >= 0 && !isFieldset(field)) {
continue;
}
if (isFieldset(field)) {
const {
fields: fieldsetFields
} = field;
if (fieldIndex >= 0) {
const fieldValue = Object.entries(copy[fieldIndex])[0][1];
copy[fieldIndex] = {
[name]: injectDefaultValues(fieldValue, fieldsetFields)
};
} else {
copy.push({
[name]: injectDefaultValues({}, fieldsetFields)
});
}
}
if ('defaultValue' in field) {
copy.push({
[name]: field.defaultValue
});
}
}
return convertToParametersObject(copy);
}
/**
* Flattens the given FieldDefinition[] so it resembles the expected data
* structure in result Parameters.
*/
const flattenFields = fields => {
const flattenAccumulator = (accumulator, field) => {
if (isTabGroup(field)) {
if (field.hasGroupedValues) {
accumulator.push(field);
} else {
const flattenedTabs = field.fields.reduce((tabAccumulator, tab) => {
return tabAccumulator.concat(tab.hasGroupedValues ?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tab : tab.fields.reduce(flattenAccumulator, []));
}, []);
accumulator.push(...flattenedTabs);
}
} else if (isExpand(field)) {
if (field.hasGroupedValues) {
accumulator.push(field);
} else {
const flattenedExpand = field.fields.reduce(flattenAccumulator, []);
accumulator.push(...flattenedExpand);
}
} else {
accumulator.push(field);
}
return accumulator;
};
return fields.reduce(flattenAccumulator, []);
};
export const deserialize = async (manifest, data, fields, depth = 0) => {
const dataArray = convertToParametersArray(data);
let result = [];
const errors = [];
const flattenedFields = flattenFields(fields);
for (const item of dataArray) {
const [name, originalValue] = Object.entries(item)[0];
const field = flattenedFields.find(field => field.name === getNameFromDuplicateField(name));
if (field === undefined) {
continue;
}
let value = extract(originalValue, field);
if (value === undefined) {
continue;
}
// WARNING: don't recursively serialize, limit to depth < 1
// deserializable?
if (isFieldset(field) && depth === 0) {
const fieldDeserializer = getFieldDeserializer(manifest, field.options.transformer);
if (fieldDeserializer) {
try {
value = fieldDeserializer(value);
} catch (error) {
errors.push({
[name]: error instanceof Error ? error.message : String(error)
});
continue;
}
value = await deserialize(manifest, value, field.fields, depth + 1);
}
}
result.push({
[name]: value
});
}
result = convertToParametersObject(result);
if (errors.length > 0) {
result.errors = convertToParametersObject(errors);
}
return injectDefaultValues(result, flattenedFields);
};
const convertToParametersObject = (parameters = []) => {
if (!Array.isArray(parameters)) {
return parameters;
}
return parameters.reduce((obj, current) => {
// eslint-disable-next-line guard-for-in
for (const key in current) {
const keys = Object.keys(obj);
let resultKey = key;
let idx = 1;
while (keys.indexOf(resultKey) >= 0) {
resultKey = `${getNameFromDuplicateField(key)}:${idx}`;
idx++;
}
obj[resultKey] = current[key];
}
return obj;
}, {});
};
const convertToParametersArray = (parameters = {}) => {
if (Array.isArray(parameters)) {
return parameters;
}
const dataArray = [];
// eslint-disable-next-line guard-for-in
for (const name in parameters) {
dataArray.push({
[name]: parameters[name]
});
}
return dataArray;
};