Source: dynamic-data-mapping-form-builder/src/main/resources/META-INF/resources/js/components/RuleEditor/RuleEditor.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 '../Calculator/Calculator.es';

import 'clay-alert';

import 'clay-button';

import 'clay-modal';

import 'dynamic-data-mapping-form-renderer/js/components/PageRenderer/PageRenderer.es';
import {PagesVisitor} from 'dynamic-data-mapping-form-renderer';
import {makeFetch} from 'dynamic-data-mapping-form-renderer/js/util/fetch.es';
import Component from 'metal-component';
import dom from 'metal-dom';
import Soy from 'metal-soy';
import {Config} from 'metal-state';

import {maxPageIndex, pageOptions} from '../../util/pageSupport.es';
import {getFieldProperty} from '../LayoutProvider/util/fields.es';
import templates from './RuleEditor.soy';

const fieldOptionStructure = Config.shapeOf({
	dataType: Config.string(),
	name: Config.string(),
	options: Config.arrayOf(
		Config.shapeOf({
			label: Config.string(),
			name: Config.string(),
			value: Config.string(),
		})
	),
	type: Config.string(),
	value: Config.string(),
});

/**
 * RuleEditor.
 * @extends Component
 */

class RuleEditor extends Component {
	convertAutoFillDataToArray(action, type) {
		const data = action[type];
		const originalData = action[`${type}Data`];

		return Object.keys(data).map((key, index) => {
			const {label, name, required, type} = originalData[index];

			const fieldsTypes = this.getTypesByFieldType(type);

			const actionsFieldOptions = this.getFieldsByTypes(
				this.actionsFieldOptions,
				fieldsTypes
			);

			return {
				fieldOptions: actionsFieldOptions,
				label,
				name,
				required,
				type,
				value: data[key],
			};
		});
	}

	created() {
		if (this.rule) {
			this._prepareRuleEditor();
		}

		this._fetchFunctionsURL();
		this._setDataProviderTarget();
		this._setActionsInputsOutputs();
	}

	disposed() {
		this.setState({
			actions: [],
			conditions: [],
			logicalOperator: '',
		});
	}

	formatDataProviderInputParameter(actionParameters, parameters) {
		return parameters.reduce(
			(result, {name, value}) => ({
				...result,
				[name]:
					Object.keys(actionParameters).indexOf(name) !== -1
						? actionParameters[name]
						: value,
			}),
			{}
		);
	}

	formatDataProviderOutputParameter(actionParameters, parameters) {
		return parameters.reduce(
			(result, {id}) => ({
				...result,
				[id]:
					Object.keys(actionParameters).indexOf(id) !== -1
						? actionParameters[id]
						: undefined,
			}),
			{}
		);
	}

	getDataProviderOptions(id, index) {
		let promise;

		if (id) {
			promise = this._fetchDataProviderParameters(id, index)
				.then(({inputs, outputs}) => {
					let actions = this.actions;

					if (!this.isDisposed()) {
						actions = actions.map((action, currentIndex) => {
							return index == currentIndex
								? {
										...action,
										ddmDataProviderInstanceUUID: this.dataProvider.find(
											(data) => {
												return data.id == id;
											}
										).uuid,
										hasRequiredInputs: inputs.some(
											(input) => input.required
										),
										inputs: this.formatDataProviderInputParameter(
											action.inputs,
											inputs
										),
										inputsData: inputs,
										outputs: this.formatDataProviderOutputParameter(
											action.outputs,
											outputs
										),
										outputsData: outputs,
								  }
								: action;
						});
					}

					return actions[index];
				})
				.catch((error) => {
					throw new Error(error);
				});
		}
		else {
			promise = Promise.resolve(this.actions[index]);
		}

		return promise;
	}

	getFieldOptions(fieldName) {
		let options = [];
		const visitor = new PagesVisitor(this.pages);

		const field = visitor.findField((field) => {
			return field.fieldName === fieldName;
		});

		options = field ? field.options : [];

		return options;
	}

	getFieldsByTypes(fields, types) {
		return fields.filter((field) =>
			types.some((fieldType) => field.type == fieldType)
		);
	}

	getTypesByFieldType(fieldType) {
		let list = [];

		if (fieldType == 'list') {
			list = ['checkbox_multiple', 'radio', 'select'];
		}
		else if (fieldType == 'text') {
			list = [
				'checkbox_multiple',
				'date',
				'numeric',
				'radio',
				'select',
				'text',
			];
		}
		else if (fieldType == 'number') {
			list = ['numeric'];
		}

		return list;
	}

	handleRuleAdded({ruleName}) {
		this._handleRuleSaved('ruleAdded', ruleName);
	}

	handleRuleEdited({ruleName}) {
		this._handleRuleSaved('ruleEdited', ruleName);
	}

	isValueOperand({type}) {
		return type !== 'field' && type !== 'user';
	}

	populateActionTargetValue(id, index) {
		const {actions, pageOptions} = this;

		const previousTarget = actions[index].target;

		actions[index].target = id;
		actions[index].label = id;

		if (actions[index].action == 'jump-to-page') {
			const selectedOption = pageOptions.find((option) => {
				return option.value == id;
			});

			actions[index].label = selectedOption.label;
		}

		if (id === undefined) {
			actions[index].target = '';
		}
		else if (id === '') {
			actions[index].inputs = {};
			actions[index].outputs = {};
			actions[index].hasRequiredInputs = false;
		}
		else if (
			previousTarget !== id &&
			actions[index].action == 'calculate'
		) {
			actions[index].calculatorFields = this._updateCalculatorFields(
				this.actions[index],
				id
			);
		}

		this.setState({
			actions,
		});
	}

	populateDataProviderOptions(id, index) {
		const {actions} = this;

		actions[index].target = id;
		actions[index].label = id;

		this.getDataProviderOptions(id, index)
			.then((actionData) => {
				if (!this.isDisposed()) {
					this.setState({
						actions: actions.map((action, currentIndex) => {
							return index == currentIndex ? actionData : action;
						}),
					});
				}
			})
			.catch((error) => {
				throw new Error(error);
			});
	}

	prepareStateForRender(state) {
		const {pages} = this;
		const actions = state.loadingDataProviderOptions
			? []
			: state.actions.map((action) => ({
					...action,
					inputs: action.inputs
						? this.convertAutoFillDataToArray(action, 'inputs')
						: [],
					outputs: action.outputs
						? this.convertAutoFillDataToArray(action, 'outputs')
						: [],
			  }));

		const conditions = state.conditions.map((condition) => {
			const fieldName = condition.operands[0].value;
			let firstOperandOptions = [];
			let operators = [];

			if (fieldName) {
				const {dataType} = this._getFieldTypeByFieldName(fieldName);

				operators = this._getOperatorsByFieldType(dataType);

				firstOperandOptions = this.getFieldOptions(fieldName);
			}

			return {
				...condition,
				binaryOperator: this._isBinary(condition.operator),
				firstOperandOptions,
				operands: condition.operands.map((operand, index) => {
					if (index === 1 && this.isValueOperand(operand)) {
						operand = {
							...operand,
							dataType: getFieldProperty(
								pages,
								condition.operands[0].value,
								'dataType'
							),
							type: getFieldProperty(
								pages,
								condition.operands[0].value,
								'type'
							),
						};
					}

					return operand;
				}),
				operators,
			};
		});

		let {actionTypes} = this;

		if (pages.length < 3) {
			actionTypes = this.actionTypes.filter((obj) => {
				return obj.value !== 'jump-to-page';
			});
		}

		return {
			...state,
			actionTypes,
			actions,
			conditions,
		};
	}

	syncPages(pages) {
		const {actions} = this;
		let {conditions} = this;

		const visitor = new PagesVisitor(pages);

		conditions.forEach((condition, index) => {
			let firstOperandFieldExists = false;
			const secondOperand = condition.operands[1];
			let secondOperandFieldExists = false;

			visitor.mapFields(({fieldName}) => {
				if (condition.operands[0].value === fieldName) {
					firstOperandFieldExists = true;
				}

				if (secondOperand && secondOperand.value === fieldName) {
					secondOperandFieldExists = true;
				}
			});

			if (condition.operands[0].value === 'user') {
				firstOperandFieldExists = true;
			}

			if (!firstOperandFieldExists) {
				conditions = this._clearAllConditionFieldValues(index);
			}

			if (
				!secondOperandFieldExists &&
				secondOperand &&
				secondOperand.type == 'field'
			) {
				conditions = this._clearSecondOperandValue(conditions, index);
			}
		});

		const maxPage = maxPageIndex(conditions, pages);

		this.setState({
			actions: this._syncActions(actions),
			actionsFieldOptions: this._actionsFieldOptionsValueFn(),
			calculatorResultOptions: this._calculatorResultOptionsValueFn(),
			conditions,
			conditionsFieldOptions: this._conditionsFieldOptionsValueFn([
				'fieldset',
				'paragraph',
			]),
			deletedFields: this._getDeletedFields(visitor),
			pageOptions: pageOptions(pages, maxPage),
			roles: this._rolesValueFn(),
		});
	}

	willUpdate() {
		const invalidRule =
			!this._validateConditionsFilling() ||
			!this._validateActionsFilling();

		this.setState({invalidRule});

		this.emit('ruleValidatorChanged', invalidRule);
	}

	_calculatorResultOptionsValueFn() {
		const {pages} = this;
		const fields = [];
		const visitor = new PagesVisitor(pages);

		visitor.mapFields((field) => {
			if (field.type == 'numeric') {
				fields.push({
					...field,
					options: field.options ? field.options : [],
					value: field.fieldName,
				});
			}
		});

		return fields;
	}

	_clearAction(index) {
		const {actions} = this;

		return actions.map((action, currentIndex) => {
			return currentIndex === index
				? {
						action: '',
						calculatorFields: [],
						expression: '',
						inputs: {},
						label: '',
						outputs: {},
						target: '',
				  }
				: action;
		});
	}

	_clearAllConditionFieldValues(index) {
		const {secondOperandSelectedList} = this;
		let {conditions} = this;

		conditions = this._clearFirstOperandValue(conditions, index);
		conditions = this._clearOperatorValue(conditions, index);
		conditions = this._clearSecondOperandValue(conditions, index);

		this.setState({
			conditions,
			secondOperandSelectedList,
		});

		return conditions;
	}

	_clearFirstOperandValue(conditions, index) {
		if (conditions[index] && conditions[index].operands[0]) {
			conditions[index].operands[0].type = '';
			conditions[index].operands[0].value = '';
		}

		return conditions;
	}

	_clearOperatorValue(conditions, index) {
		if (conditions[index]) {
			conditions[index].operator = '';
		}

		return conditions;
	}

	_clearSecondOperandValue(conditions, index) {
		if (conditions[index] && conditions[index].operands[1]) {
			conditions[index].operands = [conditions[index].operands[0]];
		}

		return conditions;
	}

	_clearSelectedSecondOperand(secondOperandSelectedList) {
		return secondOperandSelectedList;
	}

	_fetchDataProviderParameters(id) {
		const {dataProviderInstanceParameterSettingsURL} = this;

		return makeFetch({
			method: 'GET',
			url: `${dataProviderInstanceParameterSettingsURL}?ddmDataProviderInstanceId=${id}`,
		}).catch((error) => {
			throw new Error(error);
		});
	}

	_fetchFunctionsURL() {
		const {functionsURL} = this;

		makeFetch({
			method: 'GET',
			url: functionsURL,
		})
			.then((responseData) => {
				if (!this.isDisposed()) {
					this.setState({
						calculatorFunctions: responseData,
					});
				}
			})
			.catch((error) => {
				throw new Error(error);
			});
	}

	_actionsFieldOptionsValueFn() {
		return this._getFieldOptions();
	}

	_conditionsFieldOptionsValueFn(omittedFieldsList) {
		return this._getFieldOptions(omittedFieldsList);
	}

	_getDeletedFields(visitor) {
		const existentFields = [];
		const {actionsFieldOptions} = this;
		let deletedFields = [];

		actionsFieldOptions.forEach((field) => {
			visitor.mapFields(({fieldName}) => {
				if (field.fieldName === fieldName) {
					existentFields.push(fieldName);
				}
			});
		});

		const oldFields = actionsFieldOptions.map((field) => field.fieldName);

		deletedFields = oldFields.filter((field) => {
			return existentFields.indexOf(field) > -1 ? false : field;
		});

		return deletedFields;
	}

	_getFieldOptions(omittedFieldsList = []) {
		const {pages} = this;
		const fields = [];
		const visitor = new PagesVisitor(pages);

		visitor.visitFields((field) => {
			if (omittedFieldsList.indexOf(field.type) < 0) {
				fields.push({
					...field,
					options: field.options ? field.options : [],
					value: field.fieldName,
				});
			}
		});

		return fields;
	}

	_getFieldLabel(fieldName) {
		const pages = this.pages;

		let fieldLabel;

		if (pages) {
			for (let page = 0; page < pages.length; page++) {
				const rows = pages[page].rows;

				for (let row = 0; row < rows.length; row++) {
					const cols = rows[row].columns;

					for (let col = 0; col < cols.length; col++) {
						const fields = cols[col].fields;

						for (let field = 0; field < fields.length; field++) {
							if (
								pages[page].rows[row].columns[col].fields[field]
									.fieldName === fieldName
							) {
								fieldLabel =
									pages[page].rows[row].columns[col].fields[
										field
									].label;
								break;
							}
						}
					}
				}
			}
		}

		return fieldLabel;
	}

	_getFieldTypeByFieldName(fieldName) {
		let dataType = '';
		let repeatable = false;
		let type = '';

		if (fieldName === 'user') {
			dataType = 'user';
		}
		else {
			const selectedField = this.actionsFieldOptions.find(
				(field) => field.value === fieldName
			);

			if (selectedField) {
				dataType = selectedField.dataType;
				repeatable = selectedField.repeatable;
				type = selectedField.type;
			}
		}

		return {
			dataType,
			repeatable,
			type,
		};
	}

	_getIndex(fieldInstance, fieldClass) {
		const firstOperand = dom.closest(fieldInstance.element, fieldClass);

		return firstOperand.getAttribute(`${fieldClass.substring(1)}-index`);
	}

	_getOperatorsByFieldType(fieldType) {
		if (fieldType === 'integer' || fieldType === 'double') {
			fieldType = 'number';
		}

		if (!Object.hasOwnProperty.call(this.functionsMetadata, fieldType)) {
			fieldType = 'text';
		}

		return this.functionsMetadata[fieldType].map((metadata) => {
			return {
				...metadata,
				value: metadata.name,
			};
		});
	}

	_handleActionAdded() {
		const {actions} = this;
		const newAction = {
			action: '',
			calculatorFields: [],
			expression: '',
			inputs: {},
			label: '',
			outputs: {},
			target: '',
		};

		if (actions.length == 0) {
			actions.push(newAction);
		}

		actions.push(newAction);

		this.setState({
			actions,
			invalidRule: true,
		});
	}

	_handleActionSelection(event) {
		const {fieldInstance, value} = event;
		const index = parseInt(
			this._getIndex(fieldInstance, '.action-type'),
			10
		);

		const {actions, conditions} = this;

		let newActions = [...actions];

		if (value && value.length > 0 && value[0]) {
			const fieldName = value[0];

			if (actions.length > 0) {
				const previousAction = actions[index].action;

				if (fieldName !== previousAction) {
					newActions = newActions.map((action, currentIndex) => {
						return currentIndex === index
							? {
									...action,
									action: fieldName,
									calculatorFields: [],
									label: '',
									source: conditions[0].operands[0].source,
									target: '',
							  }
							: action;
					});
				}
			}
			else {
				newActions.push({action: fieldName});
			}
		}
		else {
			newActions = this._clearAction(index);
		}

		this.setState({
			actions: newActions,
		});
	}

	_handleConditionAdded() {
		const {conditions} = this;

		conditions.push({
			operands: [
				{
					type: '',
					value: '',
				},
			],
			operator: '',
		});

		this.setState({
			conditions,
		});
	}

	_handleDataProviderInputEdited(event) {
		const {fieldInstance, value} = event;
		const {actions} = this;
		const actionIndex = this._getIndex(fieldInstance, '.action');
		const inputIndex = this._getIndex(
			fieldInstance,
			'.container-input-field'
		);

		const editedInput = Object.keys(actions[actionIndex].inputs)[
			inputIndex
		];

		actions[actionIndex].inputs[editedInput] = value[0];

		this.setState({
			actions,
		});
	}

	_handleDataProviderOutputEdited(event) {
		const {fieldInstance, value} = event;
		const actionIndex = this._getIndex(fieldInstance, '.action');
		const outputIndex = this._getIndex(
			fieldInstance,
			'.container-output-field'
		);
		const {actions} = this;

		const editedOutput = Object.keys(actions[actionIndex].outputs)[
			outputIndex
		];

		actions[actionIndex].outputs[editedOutput] = value[0];

		this.setState({
			actions,
		});
	}

	_handleDeleteAction(event) {
		const {currentTarget} = event;
		const index = currentTarget.getAttribute('data-index');

		this.refs.confirmationModalAction.show();
		this.setState({
			activeActionIndex: parseInt(index, 10),
		});
	}

	_handleDeleteCondition(event) {
		const {currentTarget} = event;
		const index = currentTarget.getAttribute('data-index');

		this.refs.confirmationModalCondition.show();
		this.setState({
			activeConditionIndex: parseInt(index, 10),
		});
	}

	_handleEditExpression({expression, index}) {
		const {actions} = this;

		actions[index].expression = expression;
		this.setState({actions});
	}

	_handleFirstOperandFieldEdited(event) {
		const {fieldInstance, value} = event;
		const index = this._getIndex(fieldInstance, '.condition-if');
		const {actions, pages} = this;
		let {conditions} = this;

		if (value && value.length > 0 && value[0]) {
			const fieldName = value[0];
			const {dataType, repeatable} = this._getFieldTypeByFieldName(
				fieldName
			);

			const firstOperand = {
				label: this._getFieldLabel(fieldName),
				repeatable,
				type: dataType == 'user' ? 'user' : 'field',
				value: fieldName,
			};

			if (conditions.length === 0) {
				const operands = [firstOperand];

				conditions.push({operands});
			}
			else {
				if (fieldName !== conditions[index].operands[0].value) {
					conditions[index].operator = '';
					this._clearSecondOperandValue(conditions, index);
				}

				conditions[index].operands[0] = firstOperand;
			}
		}
		else {
			conditions = this._clearAllConditionFieldValues(index);
		}

		let maxPageIndex;

		const visitor = new PagesVisitor(pages);

		if (conditions[index].operands[0].value != '') {
			visitor.mapFields(
				(field, fieldIndex, columnIndex, rowIndex, pageIndex) => {
					if (
						field.fieldName === conditions[index].operands[0].value
					) {
						maxPageIndex = pageIndex;

						conditions[index].operands[0].source = pageIndex;
					}
				}
			);
		}

		if (actions && actions[0].action != '') {
			actions.map((action) => {
				if (action.action == 'jump-to-page') {
					action.source = conditions[index].operands[0].source;
				}

				return {
					action,
				};
			});
		}

		this.setState({
			actions,
			conditions,
			pageOptions: pageOptions(pages, maxPageIndex),
		});
	}

	_handleLogicalOperationChange(event) {
		const {target} = event;
		const {value} = target.dataset;

		if (value !== this.logicalOperator) {
			this.setState({
				logicalOperator: value,
			});
		}
	}

	_handleModalButtonClicked(event) {
		event.stopPropagation();

		if (!event.target.classList.contains('close-modal')) {
			const activeActionIndex = this.activeActionIndex;
			const activeConditionIndex = this.activeConditionIndex;

			const {actions, conditions, pages} = this;

			if (activeConditionIndex > -1) {
				conditions.splice(activeConditionIndex, 1);
			}

			if (activeActionIndex > -1) {
				actions.splice(activeActionIndex, 1);
			}

			if (this.refs.confirmationModalAction.visible) {
				this.refs.confirmationModalAction.emit('hide');
			}

			if (this.refs.confirmationModalCondition.visible) {
				this.refs.confirmationModalCondition.emit('hide');
			}

			const maxPage = maxPageIndex(conditions, pages);

			this.setState({
				actions,
				activeActionIndex: -1,
				activeConditionIndex: -1,
				conditions,
				pageOptions: pageOptions(pages, maxPage),
			});
		}
	}

	_handleOperatorEdited(event) {
		const {fieldInstance, value} = event;
		let {conditions} = this;
		let operatorValue = '';

		if (value && value.length > 0 && value[0]) {
			operatorValue = value[0];
		}

		const index = this._getIndex(fieldInstance, '.condition-operator');

		if (!operatorValue || !this._isBinary(operatorValue)) {
			conditions = this._clearSecondOperandValue(conditions, index);

			conditions[index].operator = operatorValue;
		}
		else {
			conditions[index].operator = operatorValue;
		}

		this.setState({
			conditions,
		});
	}

	_handleRuleSaved(eventName, ruleName) {
		const actions = this._removeActionInternalProperties();
		const conditions = this._removeConditionInternalProperties();
		const {logicalOperator, ruleEditedIndex} = this;

		const rule = {
			actions,
			conditions,
			['logical-operator']: logicalOperator,
			ruleEditedIndex,
		};

		if (ruleName) {
			rule.name = ruleName;
		}

		this.emit(eventName, rule);
	}

	_handleRuleCancelled() {
		this.emit('ruleCancelled', {});
	}

	_handleSecondOperandFieldEdited(event) {
		const {conditions} = this;
		const {fieldInstance, value} = event;
		let fieldValue = '';

		if (value && typeof value == 'object' && value[0]) {
			fieldValue = value[0];
		}
		else if (value && typeof value == 'string') {
			fieldValue = value;
		}

		let index;

		if (dom.closest(fieldInstance.element, '.condition-type-value')) {
			index = this._getIndex(fieldInstance, '.condition-type-value');
		}

		let secondOperand = conditions[index].operands[1];

		if (!secondOperand) {
			secondOperand = {
				dataType: fieldInstance.dataType,
				type: fieldInstance.type,
			};
		}

		let userType = '';

		if (conditions[index].operands[0].type === 'user') {
			userType = conditions[index].operands[0].type;
		}

		conditions[index].operands[1] = {
			...secondOperand,
			dataType: fieldInstance.dataType,
			label: fieldValue,
			type: userType ? userType : fieldInstance.type,
			value: fieldValue,
		};

		this.setState({
			conditions,
		});
	}

	_handleSecondOperandTypeEdited(event) {
		let {conditions} = this;
		const {fieldInstance, value} = event;
		const index = this._getIndex(fieldInstance, '.condition-type');
		const {operands} = conditions[index];
		const secondOperand = operands[1];

		let secondOperandType = 'field';
		let valueType = 'field';
		if (value[0] == 'value') {
			valueType = 'string';
			secondOperandType = this._getFieldTypeByFieldName(operands[0].value)
				.dataType;
		}

		if (
			secondOperand &&
			secondOperand.type === secondOperandType &&
			value[0] !== ''
		) {
			return;
		}

		if (value[0] == '') {
			conditions = this._clearSecondOperandValue(conditions, index);
		}
		else if (secondOperand && secondOperand.dataType != valueType) {
			conditions[index].operands[1].type = '';
			conditions[index].operands[1].value = '';
		}

		if (secondOperand) {
			secondOperand.type = secondOperandType;
		}
		else if (value[0] !== '') {
			conditions[index].operands.push({
				type: secondOperandType,
				value: '',
			});
		}

		this.setState({
			conditions,
		});
	}

	_handleSecondOperandValueEdited(event) {
		const {conditions} = this;
		const {fieldInstance, value} = event;
		const index = this._getIndex(fieldInstance, '.condition-type-value');
		const secondOperandValue = Array.isArray(value) ? value[0] : value;

		this.setState({
			conditions: conditions.map((condition, conditionIndex) => {
				const operands = [...condition.operands];

				if (index == conditionIndex) {
					operands[1] = {
						...operands[1],
						value: secondOperandValue,
					};
				}

				return {
					...condition,
					operands,
				};
			}),
		});
	}

	_handleTargetSelection(event) {
		const {fieldInstance, value} = event;
		const {actions} = this;
		const id = value[0];
		const index = this._getIndex(fieldInstance, '.target-action');

		const previousTarget = actions[index].target;

		if (previousTarget !== id && actions[index].action == 'auto-fill') {
			this.populateDataProviderOptions(id, index);
		}
		else {
			this.populateActionTargetValue(id, index);
		}
	}

	_isBinary(value) {
		return (
			value === 'equals-to' ||
			value === 'not-equals-to' ||
			value === 'contains' ||
			value === 'not-contains' ||
			value === 'belongs-to' ||
			value === 'greater-than' ||
			value === 'greater-than-equals' ||
			value === 'less-than' ||
			value === 'less-than-equals'
		);
	}

	_isFieldAction(fieldName) {
		return (
			fieldName == 'enable' ||
			fieldName == 'show' ||
			fieldName == 'require'
		);
	}

	_prepareAutofillOutputs(action) {
		if (Array.isArray(action.outputs)) {
			action.outputs.forEach((output) => {
				delete output.actionsFieldOptions;
				delete output.name;
				delete output.type;
			});
		}

		return action.outputs;
	}

	_prepareRuleEditor() {
		const {rule} = this;

		const newRule = rule;

		let newActions = rule.actions.map((action) => {
			const newAction = {...action};

			if (action.action == 'jump-to-page') {
				newAction.target = (parseInt(action.target, 10) + 1).toString();
			}

			return newAction;
		});

		newActions = this._syncActions(newActions);

		newRule.actions = newActions;

		this.setState({
			actions: newActions,
			conditions: rule.conditions,
			logicalOperator: rule['logical-operator'].toLowerCase(),
			rule: newRule,
		});
	}

	_removeActionInternalProperties() {
		const {actions} = this;

		return actions.map((action) => {
			const {
				action: actionType,
				ddmDataProviderInstanceUUID,
				expression,
				inputs,
				label,
				outputs,
				source,
				target,
			} = action;

			let newAction = {
				action: actionType,
				label,
				target,
			};

			if (actionType == 'auto-fill') {
				newAction = {
					...newAction,
					ddmDataProviderInstanceUUID,
					inputs,
					outputs,
				};
			}
			else if (actionType == 'calculate') {
				newAction = {
					...newAction,
					expression,
				};
			}
			else if (actionType == 'jump-to-page') {
				newAction = {
					...newAction,
					source: `${source}`,
					target: `${parseInt(target, 10) - 1}`,
				};
			}

			return newAction;
		});
	}

	_removeConditionInternalProperties() {
		const {conditions} = this;

		conditions.forEach((condition) => {
			if (condition.operands[0].type == 'user') {
				condition.operands[0].label = condition.operands[0].value;
				condition.operands[1].type = 'list';
			}

			if (condition.operands[1]) {
				condition.operands[1].label = condition.operands[1].value;
			}
		});

		return conditions;
	}

	_rolesValueFn() {
		const {roles} = this;

		return roles.map((role) => {
			return {
				...role,
				value: role.label,
			};
		});
	}

	_setActions(actions) {
		if (actions.length == 0) {
			actions.push({
				action: '',
				calculatorFields: [],
				expression: '',
				hasRequiredInputs: false,
				inputs: {},
				label: '',
				outputs: {},
				target: '',
			});
		}

		return actions;
	}

	_setActionsInputsOutputs() {
		const {rule} = this;

		if (rule) {
			this.setState({
				loadingDataProviderOptions: true,
			});

			Promise.all(
				rule.actions.map((action, index) => {
					let newAction = {...action};

					if (action.ddmDataProviderInstanceUUID) {
						const {id} = this.dataProvider.find((dataProvider) => {
							let dataProviderId;

							if (
								dataProvider.uuid ===
								action.ddmDataProviderInstanceUUID
							) {
								dataProviderId = dataProvider.id;
							}

							return dataProviderId;
						});

						newAction = this.getDataProviderOptions(id, index);
					}

					newAction.calculatorFields = this._updateCalculatorFields(
						newAction,
						newAction.target
					);

					return newAction;
				})
			)
				.then((actions) => {
					this.setState({
						actions,
						loadingDataProviderOptions: false,
					});
				})
				.catch((error) => {
					throw new Error(error);
				});
		}
	}

	_setConditions(conditions) {
		if (conditions.length === 0) {
			conditions.push({
				operands: [
					{
						type: '',
						value: '',
					},
				],
				operator: '',
			});
		}

		return conditions;
	}

	_setDataProviderTarget() {
		const {dataProvider, rule} = this;

		if (!rule) {
			return;
		}

		this.setState({
			actions: rule.actions.map((action) => {
				if (action.action == 'auto-fill') {
					const {id} = dataProvider.find(
						({uuid}) => uuid === action.ddmDataProviderInstanceUUID
					);

					action.target = id;
				}

				return action;
			}),
		});
	}

	_syncActions(actions) {
		const {pages} = this;

		const visitor = new PagesVisitor(pages);

		actions.forEach((action) => {
			let targetFieldExists = false;

			visitor.mapFields(({fieldName}) => {
				if (action.target === fieldName) {
					targetFieldExists = true;
				}
			});

			action.calculatorFields = this._updateCalculatorFields(
				action,
				action.target
			);

			if (
				action.action !== 'auto-fill' &&
				action.action !== 'jump-to-page' &&
				!targetFieldExists
			) {
				action.target = '';
			}
			else if (action.action == 'auto-fill') {
				action = {
					...action,
					calculatorFields: [],
				};
			}
		});

		return actions;
	}

	_updateCalculatorFields(action, id) {
		const {calculatorResultOptions} = this;

		return calculatorResultOptions.reduce((prev, option) => {
			return option.fieldName === id
				? prev
				: [
						...prev,
						{
							...option,
							title: option.fieldName,
							type: 'item',
						},
				  ];
		}, []);
	}

	_validateActionsAutoFill(autoFillActions, type) {
		return autoFillActions.every((action) => {
			const parameterKeys = Object.keys(action[type]);

			let validation = parameterKeys.every((key) => action[type][key]);

			if (type === 'inputs' && !parameterKeys.length) {
				validation = Object.keys(action.outputs).length;
			}

			return validation;
		});
	}

	_validateActionsCalculateFilling(calculateActions) {
		let allFieldsFilled = true;

		calculateActions.forEach(({expression}) => {
			if (expression && expression.length == 0) {
				allFieldsFilled = false;
			}
		});

		return allFieldsFilled;
	}

	_validateActionsFilling() {
		const {actions} = this;

		let allFieldsFilled = true;

		const autofillActions = actions.filter((action) => {
			return action.action == 'auto-fill';
		});

		const calculateActions = actions.filter((action) => {
			return action.action == 'calculate';
		});

		if (actions) {
			actions.forEach((currentAction) => {
				const {action, target} = currentAction;

				if (action == '') {
					allFieldsFilled = false;
				}
				else if (target == '') {
					allFieldsFilled = false;
				}
			});

			if (allFieldsFilled) {
				if (
					autofillActions &&
					autofillActions.length > 0 &&
					calculateActions &&
					calculateActions.length > 0
				) {
					allFieldsFilled =
						this._validateInputOutputs(autofillActions) &&
						this._validateActionsCalculateFilling(calculateActions);
				}
				else if (autofillActions && autofillActions.length > 0) {
					allFieldsFilled =
						this._validateActionsAutoFill(
							autofillActions,
							'inputs'
						) &&
						this._validateActionsAutoFill(
							autofillActions,
							'outputs'
						);
				}
				else if (calculateActions && calculateActions.length > 0) {
					allFieldsFilled = this._validateActionsCalculateFilling(
						calculateActions
					);
				}
			}
		}

		return allFieldsFilled;
	}

	_validateConditionsFilling() {
		const {conditions} = this;

		for (let i = 0; i < conditions.length; i++) {
			const condition = conditions[i];
			const {operands, operator} = condition;

			if (operands[0].value == '' || !operator) {
				return false;
			}
			else if (
				operator &&
				this._isBinary(operator) &&
				!(operands[1] && !!operands[1].value && operands[1].value != '')
			) {
				return false;
			}
		}

		return true;
	}

	_validateInputOutputs(autofillActions) {
		return (
			this._validateActionsAutoFill(autofillActions, 'inputs') &&
			this._validateActionsAutoFill(autofillActions, 'outputs')
		);
	}
}

RuleEditor.STATE = {
	actionTypes: Config.arrayOf(
		Config.shapeOf({
			label: Config.string(),
			value: Config.string(),
		})
	)
		.internal()
		.value([
			{
				label: Liferay.Language.get('show'),
				value: 'show',
			},
			{
				label: Liferay.Language.get('enable'),
				value: 'enable',
			},
			{
				label: Liferay.Language.get('require'),
				value: 'require',
			},
			{
				label: Liferay.Language.get('autofill'),
				value: 'auto-fill',
			},
			{
				label: Liferay.Language.get('calculate'),
				value: 'calculate',
			},
			{
				label: Liferay.Language.get('jump-to-page'),
				value: 'jump-to-page',
			},
		]),

	actions: Config.arrayOf(
		Config.shapeOf({
			action: Config.string(),
			calculatorFields: Config.arrayOf(fieldOptionStructure).value([]),
			expression: Config.string(),
			hasRequiredInputs: Config.bool(),
			inputs: Config.object(),
			label: Config.string(),
			outputs: Config.object(),
			target: Config.string(),
		})
	)
		.internal()
		.setter('_setActions')
		.value([]),

	actionsFieldOptions: Config.arrayOf(fieldOptionStructure)
		.internal()
		.valueFn('_actionsFieldOptionsValueFn'),

	/**
	 * Used for tracking which action we are currently focused on
	 * when trying to delete an action.
	 * @default 0
	 * @instance
	 * @memberof RuleEditor
	 * @type {Number}
	 */
	activeActionIndex: Config.number().value(-1),

	/**
	 * Used for tracking which condition we are currently focused on
	 * when trying to delete a condition.
	 * @default 0
	 * @instance
	 * @memberof RuleEditor
	 * @type {Number}
	 */

	activeConditionIndex: Config.number().value(-1),

	calculatorFunctions: Config.arrayOf(
		Config.shapeOf({
			label: Config.string(),
			tooltip: Config.string(),
			value: Config.string(),
		})
	)
		.internal()
		.value([]),

	calculatorResultOptions: Config.arrayOf(fieldOptionStructure)
		.internal()
		.valueFn('_calculatorResultOptionsValueFn'),

	/**
	 * @default 0
	 * @instance
	 * @memberof RuleEditor
	 * @type {?array}
	 */

	conditions: Config.arrayOf(
		Config.shapeOf({
			operands: Config.arrayOf(
				Config.shapeOf({
					dataType: Config.string(),
					label: Config.string(),
					repeatable: Config.bool(),
					type: Config.string(),
					value: Config.string(),
				})
			),
			operator: Config.string(),
		})
	)
		.internal()
		.setter('_setConditions')
		.value([]),

	conditionsFieldOptions: Config.arrayOf(fieldOptionStructure)
		.internal()
		.valueFn('_conditionsFieldOptionsValueFn'),

	dataProvider: Config.arrayOf(
		Config.shapeOf({
			id: Config.string(),
			name: Config.string(),
			uuid: Config.string(),
		})
	).internal(),

	dataProviderInstanceParameterSettingsURL: Config.string().required(),

	dataProviderInstancesURL: Config.string().required(),

	deletedFields: Config.arrayOf(Config.string()).value([]),

	fixedOptions: Config.arrayOf(fieldOptionStructure).value([
		{
			dataType: 'user',
			label: Liferay.Language.get('user'),
			name: 'user',
			value: 'user',
		},
	]),

	functionsMetadata: Config.shapeOf({
		number: Config.arrayOf(
			Config.shapeOf({
				label: Config.string(),
				name: Config.string(),
				parameterTypes: Config.array(),
				returnType: Config.string(),
			})
		),
		text: Config.arrayOf(
			Config.shapeOf({
				label: Config.string(),
				name: Config.string(),
				parameterTypes: Config.array(),
				returnType: Config.string(),
			})
		),
		user: Config.arrayOf(
			Config.shapeOf({
				label: Config.string(),
				name: Config.string(),
				parameterTypes: Config.array(),
				returnType: Config.string(),
			})
		),
	}),

	functionsURL: Config.string(),

	invalidRule: Config.bool().value(true),

	loadingDataProviderOptions: Config.bool(),

	logicalOperator: Config.string().internal().value('or'),

	pageOptions: Config.arrayOf(
		Config.shapeOf({
			dataType: Config.string(),
			name: Config.string(),
			options: Config.arrayOf(
				Config.shapeOf({
					label: Config.string(),
					name: Config.string(),
					value: Config.string(),
				})
			),
			type: Config.string(),
			value: Config.string(),
		})
	)
		.internal()
		.value([]),

	pages: Config.array().required(),

	readOnly: Config.bool().value(false),

	roles: Config.arrayOf(
		Config.shapeOf({
			id: Config.string(),
			name: Config.string(),
		})
	).valueFn('_rolesValueFn'),

	/**
	 * @default 0
	 * @instance
	 * @memberof RuleEditor
	 * @type {?array}
	 */

	rule: Config.shapeOf({
		actions: Config.arrayOf(
			Config.shapeOf({
				action: Config.string(),
				calculatorFields: Config.arrayOf(fieldOptionStructure).value(
					[]
				),
				ddmDataProviderInstanceUUID: Config.string(),
				expression: Config.string(),
				inputs: Config.object(),
				label: Config.string(),
				outputs: Config.object(),
				target: Config.string(),
			})
		),
		conditions: Config.arrayOf(
			Config.shapeOf({
				operands: Config.arrayOf(
					Config.shapeOf({
						label: Config.string(),
						repeatable: Config.bool(),
						type: Config.string(),
						value: Config.string(),
					})
				),
				operator: Config.string(),
			})
		),
		['logical-operator']: Config.string(),
	}),

	ruleEditedIndex: Config.number(),

	secondOperandList: Config.arrayOf(
		Config.shapeOf({
			name: Config.string(),
			value: Config.string(),
		})
	).value([
		{
			value: Liferay.Language.get('value'),
		},
		{
			name: 'field',
			value: Liferay.Language.get('other-field'),
		},
	]),

	/**
	 * @default undefined
	 * @instance
	 * @memberof RuleEditor
	 * @type {!string}
	 */

	spritemap: Config.string().required(),
};

Soy.register(RuleEditor, templates);

export default RuleEditor;