Source: core/reducers/fieldEditableReducer.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import {
	FieldSetUtil,
	FieldSupport,
	FieldUtil,
	RulesSupport,
	RulesUtil,
	SettingsContext,
} from 'dynamic-data-mapping-form-builder';
import sectionAdded from 'dynamic-data-mapping-form-builder/js/components/LayoutProvider/handlers/sectionAddedHandler.es';

import * as FormSupport from '../../util/FormSupport.es';
import {PagesVisitor} from '../../util/visitors.es';
import {EVENT_TYPES} from '../actions/eventTypes.es';

export const addField = ({
	defaultLanguageId,
	editingLanguageId,
	fieldNameGenerator,
	generateFieldNameUsingFieldLabel,
	indexes,
	newField,
	pages,
	parentFieldName,
}) => {
	const {columnIndex, pageIndex, rowIndex} = indexes;

	if (parentFieldName) {
		const visitor = new PagesVisitor(pages);

		return visitor.mapFields(
			(field) => {
				if (field.fieldName === parentFieldName) {
					const nestedFields = field.nestedFields
						? [...field.nestedFields, newField]
						: [newField];

					field = SettingsContext.updateField(
						{
							defaultLanguageId,
							editingLanguageId,
							fieldNameGenerator,
							generateFieldNameUsingFieldLabel,
						},
						field,
						'nestedFields',
						nestedFields
					);

					const pages = FormSupport.addFieldToColumn(
						[{rows: field.rows}],
						0,
						rowIndex,
						columnIndex,
						newField.fieldName
					);

					return SettingsContext.updateField(
						{
							defaultLanguageId,
							editingLanguageId,
							fieldNameGenerator,
							generateFieldNameUsingFieldLabel,
						},
						field,
						'rows',
						pages[0].rows
					);
				}

				return field;
			},
			true,
			true
		);
	}

	return FormSupport.addFieldToColumn(
		pages,
		pageIndex,
		rowIndex,
		columnIndex,
		newField
	);
};

export const deleteField = ({
	clean = false,
	defaultLanguageId,
	editingLanguageId,
	fieldName,
	fieldNameGenerator,
	fieldPage,
	generateFieldNameUsingFieldLabel,
	pages,
}) =>
	pages.map((page, pageIndex) => {
		if (fieldPage === pageIndex) {
			const pagesWithFieldRemoved = FieldSupport.removeField(
				{
					defaultLanguageId,
					editingLanguageId,
					fieldNameGenerator,
					generateFieldNameUsingFieldLabel,
				},
				pages,
				fieldName,
				clean
			);

			return {
				...page,
				rows: clean
					? FormSupport.removeEmptyRows(
							pagesWithFieldRemoved,
							pageIndex
					  )
					: pagesWithFieldRemoved[pageIndex].rows,
			};
		}

		return page;
	});

const updateFieldProperty = ({
	defaultLanguageId,
	editingLanguageId,
	fieldNameGenerator,
	focusedField,
	generateFieldNameUsingFieldLabel,
	pages,
	propertyName,
	propertyValue,
}) => {
	if (
		propertyName === 'fieldReference' &&
		propertyValue !== '' &&
		propertyValue !== focusedField.fieldName
	) {
		focusedField = SettingsContext.updateFieldReference(
			focusedField,
			FieldUtil.findInvalidFieldReference(
				focusedField,
				pages,
				propertyValue
			),
			false
		);
	}

	return SettingsContext.updateField(
		{
			defaultLanguageId,
			editingLanguageId,
			fieldNameGenerator,
			generateFieldNameUsingFieldLabel,
		},
		focusedField,
		propertyName,
		propertyValue
	);
};

/**
 * NOTE: This is a literal copy of the old LayoutProvider logic. Small changes
 * were made only to adapt to the reducer.
 */
export default (state, action, config) => {
	switch (action.type) {
		case EVENT_TYPES.FIELD.ADD: {
			const {
				data: {parentFieldName},
				indexes,
			} = action.payload;

			const {
				availableLanguageIds,
				defaultLanguageId,
				editingLanguageId,
				pages,
			} = state;
			const {
				generateFieldNameUsingFieldLabel,
				getFieldNameGenerator,
			} = config;

			const fieldNameGenerator = getFieldNameGenerator(
				pages,
				generateFieldNameUsingFieldLabel
			);

			const field =
				action.payload.newField ||
				FieldSupport.createField(
					{defaultLanguageId, editingLanguageId, fieldNameGenerator},
					action.payload
				);

			const settingsVisitor = new PagesVisitor(
				field.settingsContext.pages
			);

			const newField = {
				...field,
				settingsContext: {
					...field.settingsContext,
					availableLanguageIds,
					defaultLanguageId,
					pages: settingsVisitor.mapFields((field) =>
						FieldSupport.localizeField(
							field,
							defaultLanguageId,
							editingLanguageId
						)
					),
				},
			};

			const newPages = addField({
				defaultLanguageId,
				editingLanguageId,
				fieldNameGenerator,
				generateFieldNameUsingFieldLabel,
				indexes,
				newField,
				pages,
				parentFieldName,
			});

			return {
				activePage: indexes.pageIndex,
				focusedField: {
					...newField,
				},
				pages: newPages,
				previousFocusedField: newField,
			};
		}
		case EVENT_TYPES.FIELD.BLUR: {
			const {propertyName, propertyValue} = action.payload;

			let focusedField = state.focusedField;

			if (
				Object.keys(focusedField).length &&
				propertyName === 'fieldReference' &&
				(propertyValue === '' ||
					FieldUtil.findInvalidFieldReference(
						focusedField,
						state.pages,
						propertyValue
					))
			) {
				const {defaultLanguageId, editingLanguageId} = state;

				focusedField = SettingsContext.updateField(
					{
						defaultLanguageId,
						editingLanguageId,
					},
					SettingsContext.updateFieldReference(
						focusedField,
						false,
						true
					),
					propertyName,
					focusedField.fieldName
				);
			}

			return {
				fieldHovered: {},
				focusedField,
			};
		}
		case EVENT_TYPES.FIELD.CLICK: {
			const {activePage, field} = action.payload;
			const {defaultLanguageId, editingLanguageId} = state;

			const visitor = new PagesVisitor(field.settingsContext.pages);

			const focusedField = {
				...field,
				settingsContext: {
					...field.settingsContext,
					currentPage: activePage,
					pages: visitor.mapFields((currentfield) => {
						const {fieldName} = currentfield;

						if (fieldName === 'validation') {
							currentfield = {
								...currentfield,
								validation: {
									...currentfield.validation,
									fieldName: field.fieldName,
								},
							};
						}

						return FieldSupport.localizeField(
							currentfield,
							defaultLanguageId,
							editingLanguageId
						);
					}),
				},
			};

			return {
				activePage,
				focusedField,
				previousFocusedField: focusedField,
			};
		}
		case EVENT_TYPES.FIELD.CHANGE: {
			const {fieldName, propertyName, propertyValue} = action.payload;
			const {
				defaultLanguageId,
				editingLanguageId,
				focusedField,
				pages,
				rules,
			} = state;
			const {
				generateFieldNameUsingFieldLabel,
				getFieldNameGenerator,
			} = config;

			const fieldNameGenerator = getFieldNameGenerator(
				pages,
				generateFieldNameUsingFieldLabel
			);

			if (propertyName === 'name' && propertyValue === '') {
				return state;
			}

			const newFocusedField = updateFieldProperty({
				defaultLanguageId,
				editingLanguageId,
				fieldNameGenerator,
				focusedField: fieldName
					? FieldSupport.getField(pages, fieldName)
					: focusedField,
				generateFieldNameUsingFieldLabel,
				pages,
				propertyName,
				propertyValue,
			});

			const visitor = new PagesVisitor(pages);

			return {
				focusedField: newFocusedField,
				pages: visitor.mapFields(
					(field) => {
						if (field.fieldName === newFocusedField.fieldName) {
							return newFocusedField;
						}

						return field;
					},
					true,
					true
				),
				rules: RulesUtil.updateRulesReferences(
					rules || [],
					focusedField,
					newFocusedField
				),
			};
		}
		case EVENT_TYPES.FIELD.DELETE: {
			const {
				activePage,
				editRule = true,
				fieldName,
				removeEmptyRows = true,
			} = action.payload;
			const {defaultLanguageId, editingLanguageId, pages, rules} = state;
			const {
				generateFieldNameUsingFieldLabel,
				getFieldNameGenerator,
			} = config;

			const fieldNameGenerator = getFieldNameGenerator(
				pages,
				generateFieldNameUsingFieldLabel
			);

			const newPages = deleteField({
				clean: removeEmptyRows,
				defaultLanguageId,
				editingLanguageId,
				fieldName,
				fieldNameGenerator,
				fieldPage: activePage ?? state.activePage,
				generateFieldNameUsingFieldLabel,
				pages,
			});

			return {
				focusedField: {},
				pages: newPages,
				rules: editRule
					? RulesSupport.formatRules(newPages, rules)
					: rules,
			};
		}
		case EVENT_TYPES.FIELD.DUPLICATE: {
			const {fieldName, parentFieldName} = action.payload;
			const {availableLanguageIds, defaultLanguageId, pages} = state;
			const {
				generateFieldNameUsingFieldLabel,
				getFieldNameGenerator,
			} = config;

			const fieldNameGenerator = getFieldNameGenerator(
				pages,
				generateFieldNameUsingFieldLabel
			);

			const originalField = JSON.parse(
				JSON.stringify(
					FormSupport.findFieldByFieldName(pages, fieldName)
				)
			);

			const newField = FieldUtil.createDuplicatedField(originalField, {
				availableLanguageIds,
				defaultLanguageId,
				fieldNameGenerator,
				generateFieldNameUsingFieldLabel,
			});

			let newPages = null;

			if (parentFieldName) {
				const visitor = new PagesVisitor(pages);

				newPages = visitor.mapFields(
					(field) => {
						if (field.fieldName === parentFieldName) {
							const nestedFields = field.nestedFields
								? [...field.nestedFields, newField]
								: [newField];

							field = SettingsContext.updateField(
								{
									availableLanguageIds,
									defaultLanguageId,
									fieldNameGenerator,
									generateFieldNameUsingFieldLabel,
								},
								field,
								'nestedFields',
								nestedFields
							);

							let pages = [{rows: field.rows}];

							const {
								pageIndex,
								rowIndex,
							} = FormSupport.getFieldIndexes(
								pages,
								originalField.fieldName
							);

							const newRow = FormSupport.implAddRow(12, [
								newField.fieldName,
							]);

							pages = FormSupport.addRow(
								pages,
								rowIndex + 1,
								pageIndex,
								newRow
							);

							return SettingsContext.updateField(
								{
									availableLanguageIds,
									defaultLanguageId,
									fieldNameGenerator,
									generateFieldNameUsingFieldLabel,
								},
								field,
								'rows',
								pages[0].rows
							);
						}

						return field;
					},
					true,
					true
				);
			}
			else {
				const {pageIndex, rowIndex} = FormSupport.getFieldIndexes(
					pages,
					originalField.fieldName
				);

				const newRow = FormSupport.implAddRow(12, [newField]);

				newPages = FormSupport.addRow(
					pages,
					rowIndex + 1,
					pageIndex,
					newRow
				);
			}

			return {
				focusedField: {
					...newField,
				},
				pages: newPages,
			};
		}
		case EVENT_TYPES.FIELD_SET.ADD: {
			const {
				fieldSet,
				indexes,
				parentFieldName,
				properties,
				rows,
				useFieldName,
			} = action.payload;
			const {
				availableLanguageIds,
				defaultLanguageId,
				editingLanguageId,
				pages,
			} = state;
			const {
				generateFieldNameUsingFieldLabel,
				getFieldNameGenerator,
			} = config;

			const fieldNameGenerator = getFieldNameGenerator(
				pages,
				generateFieldNameUsingFieldLabel
			);

			const visitor = new PagesVisitor(fieldSet.pages);
			const nestedFields = [];

			visitor.mapFields((nestedField) => {
				nestedFields.push(
					SettingsContext.updateField(
						{
							availableLanguageIds,
							defaultLanguageId,
							fieldNameGenerator,
							generateFieldNameUsingFieldLabel,
						},
						nestedField,
						'label',
						nestedField.label
					)
				);
			});

			let fieldSetField = FieldSetUtil.createFieldSet(
				{
					availableLanguageIds,
					defaultLanguageId,
					editingLanguageId,
					fieldNameGenerator,
					generateFieldNameUsingFieldLabel,
				},
				{skipFieldNameGeneration: false, useFieldName},
				nestedFields
			);

			if (properties) {
				Object.keys(properties).forEach((key) => {
					fieldSetField = SettingsContext.updateField(
						{
							availableLanguageIds,
							defaultLanguageId,
							fieldNameGenerator,
							generateFieldNameUsingFieldLabel,
						},
						fieldSetField,
						key,
						properties[key]
					);
				});
			}

			if (fieldSet.id) {
				fieldSetField = SettingsContext.updateField(
					{
						availableLanguageIds,
						defaultLanguageId,
						fieldNameGenerator,
						generateFieldNameUsingFieldLabel,
					},
					fieldSetField,
					'ddmStructureId',
					fieldSet.id
				);
			}

			if (rows && rows.length) {
				fieldSetField = SettingsContext.updateField(
					{
						availableLanguageIds,
						defaultLanguageId,
						fieldNameGenerator,
						generateFieldNameUsingFieldLabel,
					},
					fieldSetField,
					'rows',
					rows
				);
			}

			const newField = SettingsContext.updateField(
				{
					availableLanguageIds,
					defaultLanguageId,
					fieldNameGenerator,
					generateFieldNameUsingFieldLabel,
				},
				fieldSetField,
				'label',
				fieldSet.localizedTitle
			);

			return {
				activePage: indexes.pageIndex,
				focusedField: {
					...newField,
				},
				pages: addField({
					defaultLanguageId,
					editingLanguageId,
					fieldNameGenerator,
					generateFieldNameUsingFieldLabel,
					indexes,
					newField,
					pages,
					parentFieldName,
				}),
				previousFocusedField: newField,
			};
		}
		case EVENT_TYPES.FIELD.EVALUATE: {
			const {settingsContextPages} = action.payload;
			const {
				defaultLanguageId,
				editingLanguageId,
				focusedField,
				pages,
				rules,
			} = state;
			const {
				generateFieldNameUsingFieldLabel,
				getFieldNameGenerator,
			} = config;

			const fieldName = FieldSupport.getField(
				settingsContextPages,
				'name'
			);
			const focusedFieldName = FieldSupport.getField(
				focusedField.settingsContext.pages,
				'name'
			);

			if (fieldName.instanceId !== focusedFieldName.instanceId) {
				return state;
			}

			const fieldNameGenerator = getFieldNameGenerator(
				pages,
				generateFieldNameUsingFieldLabel
			);

			let newFocusedField = {
				...focusedField,
				settingsContext: {
					...focusedField.settingsContext,
					pages: settingsContextPages,
				},
			};

			const settingsContextVisitor = new PagesVisitor(
				settingsContextPages
			);

			settingsContextVisitor.mapFields(({fieldName, value}) => {
				newFocusedField = updateFieldProperty({
					defaultLanguageId,
					editingLanguageId,
					fieldNameGenerator,
					focusedField: newFocusedField,
					generateFieldNameUsingFieldLabel,
					pages,
					propertyName: fieldName,
					propertyValue: value,
				});
			});

			const visitor = new PagesVisitor(pages);

			const newPages = visitor.mapFields(
				(field) => {
					if (field.fieldName !== fieldName.value) {
						return field;
					}

					return newFocusedField;
				},
				true,
				true
			);

			return {
				focusedField: newFocusedField,
				pages: newPages,
				rules: RulesUtil.updateRulesReferences(
					rules || [],
					focusedField,
					newFocusedField
				),
			};
		}
		case EVENT_TYPES.FIELD.HOVER:
			return {
				fieldHovered: action.payload,
			};
		case EVENT_TYPES.SECTION.ADD: {
			const {
				activePage,
				availableLanguageIds,
				defaultLanguageId,
				editingLanguageId,
				pages,
				rules,
			} = state;
			const {
				fieldTypes,
				generateFieldNameUsingFieldLabel,
				getFieldNameGenerator,
			} = config;

			const fieldNameGenerator = getFieldNameGenerator(
				pages,
				generateFieldNameUsingFieldLabel
			);

			return sectionAdded(
				{
					availableLanguageIds,
					defaultLanguageId,
					editingLanguageId,
					fieldNameGenerator,
					fieldTypes,
					generateFieldNameUsingFieldLabel,
				},
				{
					activePage,
					pages,
					rules,
				},
				action.payload
			);
		}
		default:
			return state;
	}
};