Source: layout-content-page-editor-web/src/main/resources/META-INF/resources/js/reducers/segmentsExperiences.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 {
	addSegmentsExperience,
	getExperienceUsedPortletIds,
	removeExperience,
	updatePageEditorLayoutData
} from '../utils/FragmentsEditorFetchUtils.es';
import {getRowFragmentEntryLinkIds} from '../utils/FragmentsEditorGetUtils.es';
import {setIn, updateUsedWidgets} from '../utils/FragmentsEditorUpdateUtils.es';
import {
	containsFragmentEntryLinkId,
	getEmptyLayoutData,
	getLayoutDataFragmentEntryLinkIds
} from '../utils/LayoutDataList.es';
import {getFragmentEntryLinkContent} from './fragments.es';

const EDIT_SEGMENTS_EXPERIENCE_URL =
	'/segments.segmentsexperience/update-segments-experience';

const UPDATE_SEGMENTS_EXPERIENCE_PRIORITY_URL =
	'/segments.segmentsexperience/update-segments-experience-priority';

/**
 * Stores a the layout data of a new experience in layoutDataList
 * @param {object} state
 * @param {Array<{segmentsExperienceId: string}>} state.layoutDataList
 * @param {object} state.layoutData
 * @param {string} state.defaultSegmentsExperienceId
 * @param {string} segmentsExperienceId The segmentsExperience id that owns this LayoutData
 * @param {string} layoutData The new LayoutData to store
 * @returns {object}
 */
function _storeNewLayoutData(state, segmentsExperienceId, layoutData) {
	const nextState = state;
	nextState.layoutDataList.push({
		layoutData,
		segmentsExperienceId
	});
	return nextState;
}

/**
 *
 * @param {object} state
 * @param {object} state.layoutData
 * @param {Array<{segmentsExperienceId: string ,layoutData: object}>} state.layoutDataList
 * @param {string} segmentsExperienceId
 * @returns {Promise}
 */
function _switchLayoutDataList(state, segmentsExperienceId) {
	let nextState = state;

	return new Promise((resolve, reject) => {
		try {
			updatePageEditorLayoutData(
				state.layoutData,
				state.segmentsExperienceId || state.defaultSegmentsExperienceId
			)
				.then(() => {
					const prevLayout = nextState.layoutData;
					const prevSegmentsExperienceId =
						state.segmentsExperienceId ||
						nextState.defaultSegmentsExperienceId;

					let layoutData = {};

					if (segmentsExperienceId === prevSegmentsExperienceId) {
						layoutData = nextState.layoutData;
					} else {
						const layoutDataItem = nextState.layoutDataList.find(
							segmentedLayout => {
								return (
									segmentedLayout.segmentsExperienceId ===
									segmentsExperienceId
								);
							}
						);

						layoutData = layoutDataItem
							? layoutDataItem.layoutData
							: getEmptyLayoutData();
					}

					nextState = setIn(nextState, ['layoutData'], layoutData);

					const newlayoutDataList = nextState.layoutDataList.map(
						segmentedLayout => {
							return segmentedLayout.segmentsExperienceId ===
								prevSegmentsExperienceId
								? {...segmentedLayout, layoutData: prevLayout}
								: segmentedLayout;
						}
					);

					nextState = setIn(
						nextState,
						['layoutDataList'],
						newlayoutDataList
					);

					resolve(nextState);
				})
				.catch(error => {
					reject(error);
				});
		} catch (e) {
			reject(e);
		}
	});
}

/**
 *
 * @param {object} state
 * @param {Array<{segmentsExperienceId: string}>} state.layoutDataList
 * @param {string} state.defaultSegmentsExperienceId
 * @returns {object}
 */
function _switchLayoutDataToDefault(state) {
	let nextState = state;

	const baseLayoutData = nextState.layoutDataList.find(layoutDataItem => {
		return (
			layoutDataItem.segmentsExperienceId ===
			nextState.defaultSegmentsExperienceId
		);
	});

	nextState = setIn(nextState, ['layoutData'], baseLayoutData.layoutData);

	return nextState;
}

/**
 *
 * @param {object} state
 * @param {Array<{segmentsExperienceId: string}>} state.layoutDataList
 * @param {string} segmentsExperienceId
 * @returns {object}
 */
function _removeLayoutDataItem(state, segmentsExperienceId) {
	let nextState = state;

	nextState = setIn(
		nextState,
		['layoutDataList'],
		nextState.layoutDataList.filter(layoutDataItem => {
			return layoutDataItem.segmentsExperienceId !== segmentsExperienceId;
		})
	);

	return nextState;
}

/**
 *
 * @param {object} state
 * @param {string} segmentsExperienceId
 * @returns {object}
 */
function _setUsedWidgets(state, segmentsExperienceId) {
	return getExperienceUsedPortletIds(segmentsExperienceId).then(
		portletIds => {
			const widgets = updateUsedWidgets(state.widgets, portletIds);

			state = setIn(state, ['widgets'], widgets);

			return state;
		}
	);
}

/**
 *
 * @param {object} state
 * @param {string} segmentsExperienceId
 * @returns {object}
 */
function _updateFragmentEntryLinks(state, segmentsExperienceId) {
	const fragmentEntryLinkIds = getLayoutDataFragmentEntryLinkIds(
		state.layoutData
	);

	const promises = fragmentEntryLinkIds.map(fragmentEntryLinkId => {
		let fragmentEntryLink = state.fragmentEntryLinks[fragmentEntryLinkId];

		return getFragmentEntryLinkContent(
			state.renderFragmentEntryURL,
			fragmentEntryLink,
			state.portletNamespace,
			segmentsExperienceId
		).then(response => {
			fragmentEntryLink = response;

			state = setIn(
				state,
				['fragmentEntryLinks', fragmentEntryLinkId],
				fragmentEntryLink
			);
		});
	});

	return Promise.all(promises).then(() => state);
}

/**
 * @param {object} state
 * @param {string} state.classNameId
 * @param {string} state.classPK
 * @param {string} state.defaultLanguageId
 * @param {string} state.defaultSegmentsExperienceId
 * @param {Array} state.layoutData
 * @param {Array<{segmentsExperienceId: string}>} state.layoutDataList
 * @param {object} action
 * @param {string} action.segmentsEntryId
 * @param {string} action.name
 * @param {string} action.type
 * @return {Promise}
 * @review
 */
function createSegmentsExperienceReducer(state, action) {
	return new Promise((resolve, reject) => {
		const {name, segmentsEntryId} = action;
		let nextState = state;

		addSegmentsExperience({
			name,
			segmentsEntryId
		})
			.then(objectResponse => {
				if (objectResponse.error) throw objectResponse.error;
				return objectResponse;
			})
			.then(function _success({
				fragmentEntryLinks,
				layoutData,
				segmentsExperience
			}) {
				const {
					active,
					name,
					priority,
					segmentsEntryId,
					segmentsExperienceId
				} = segmentsExperience;

				nextState = setIn(
					nextState,
					['availableSegmentsExperiences', segmentsExperienceId],
					{
						active,
						name,
						priority,
						segmentsEntryId,
						segmentsExperienceId
					}
				);

				nextState = _storeNewLayoutData(
					nextState,
					segmentsExperienceId,
					layoutData
				);

				nextState = _updateFragmentEntryLinksEditableValues(
					nextState,
					fragmentEntryLinks
				);

				_switchLayoutDataList(nextState, segmentsExperienceId)
					.then(newState =>
						setIn(
							newState,
							['segmentsExperienceId'],
							segmentsExperienceId
						)
					)
					.then(nextNewState =>
						_updateFragmentEntryLinks(
							nextNewState,
							segmentsExperienceId
						)
					)
					.then(nextNewState =>
						_setUsedWidgets(
							nextNewState,
							action.segmentsExperienceId
						)
					)
					.then(nextNewState => {
						resolve(nextNewState);
					})
					.catch(e => {
						reject(e);
					});
			})
			.catch(function _fail(error) {
				reject(error);
			});
	});
}

/**
 * Updates the fragmentEntryLinks editableValues in State
 *
 * @param {object} state
 * @param {string} state.defaultSegmentsExperienceId
 * @param {object} state.fragmentEntryLinks
 * @param {string} fragmentEntryLinks
 * @returns {object}
 */
function _updateFragmentEntryLinksEditableValues(
	state,
	fragmentEntryLinks = {}
) {
	const updatedFragmentEntryLinks = state.fragmentEntryLinks;

	Object.entries(fragmentEntryLinks).forEach(([id, editableValues]) => {
		updatedFragmentEntryLinks[id].editableValues = editableValues;
	});

	return {
		...state,
		fragmentEntryLinks: updatedFragmentEntryLinks
	};
}

/**
 * @param {object} state
 * @param {Array} state.availableSegmentsExperiences
 * @param {string} state.defaultSegmentsExperienceId
 * @param {{structure: Array}} state.layoutData
 * @param {array} state.layoutDataList
 * @param {string} state.segmentsExperienceId
 * @param {object} action
 * @param {string} action.segmentsExperienceId
 * @param {string} action.type
 * @returns {Promise}
 */
function deleteSegmentsExperienceReducer(state, action) {
	return new Promise((resolve, reject) => {
		try {
			let nextState = state;
			const {segmentsExperienceId} = action;

			const fragmentEntryLinkIds = nextState.layoutData.structure
				.reduce(
					(allFragmentEntryLinkIds, row) => [
						...allFragmentEntryLinkIds,
						...getRowFragmentEntryLinkIds(row)
					],
					[]
				)
				.filter(
					fragmentEntryLinkId =>
						!containsFragmentEntryLinkId(
							nextState.layoutDataList,
							fragmentEntryLinkId,
							segmentsExperienceId
						)
				);

			removeExperience(segmentsExperienceId, fragmentEntryLinkIds)
				.then(() => {
					const priority =
						nextState.availableSegmentsExperiences[
							segmentsExperienceId
						].priority;

					const availableSegmentsExperiences = {
						...nextState.availableSegmentsExperiences
					};

					delete availableSegmentsExperiences[segmentsExperienceId];

					const experienceIdToSelect =
						segmentsExperienceId === nextState.segmentsExperienceId
							? nextState.defaultSegmentsExperienceId
							: nextState.segmentsExperienceId;

					Object.values(availableSegmentsExperiences).forEach(
						experience => {
							const segmentExperiencePriority =
								experience.priority;

							if (segmentExperiencePriority > priority) {
								experience.priority =
									segmentExperiencePriority - 1;
							}
						}
					);

					nextState = _removeLayoutDataItem(
						nextState,
						segmentsExperienceId
					);

					nextState = _switchLayoutDataToDefault(nextState);

					nextState = setIn(
						nextState,
						['availableSegmentsExperiences'],
						availableSegmentsExperiences
					);

					nextState = setIn(
						nextState,
						['segmentsExperienceId'],
						experienceIdToSelect
					);

					return _setUsedWidgets(nextState, experienceIdToSelect);
				})
				.then(nextNewState => resolve(nextNewState))
				.catch(error => {
					reject(error);
				});
		} catch (e) {
			reject(e);
		}
	});
}

/**
 *
 *
 * @export
 * @param {object} state
 * @param {object} state.layoutData
 * @param {object} state.layoutDataList
 * @param {string} state.segmentsExperienceId
 * @param {object} action
 * @param {string} action.segmentsExperienceId
 * @param {string} action.type
 * @returns {Promise}
 */
function selectSegmentsExperienceReducer(state, action) {
	return new Promise((resolve, reject) => {
		const nextState = state;

		_switchLayoutDataList(nextState, action.segmentsExperienceId)
			.then(newState => {
				const nextNewState = setIn(
					newState,
					['segmentsExperienceId'],
					action.segmentsExperienceId
				);

				return nextNewState;
			})
			.then(nextNewState =>
				_updateFragmentEntryLinks(
					nextNewState,
					action.segmentsExperienceId
				)
			)
			.then(nextNewState =>
				_setUsedWidgets(nextNewState, action.segmentsExperienceId)
			)
			.then(nextNewState => resolve(nextNewState))
			.catch(e => {
				reject(e);
			});
	});
}

/**
 * @param {object} state
 * @param {object} action
 * @param {string} action.segmentsEntryId
 * @param {string} action.name
 * @param {string} action.segmentsExperienceId
 * @param {string} action.type
 * @return {Promise}
 * @review
 */
function editSegmentsExperienceReducer(state, action) {
	return new Promise((resolve, reject) => {
		const {name, segmentsEntryId, segmentsExperienceId} = action;
		let nextState = state;

		const nameMap = JSON.stringify({
			[state.defaultLanguageId]: name
		});

		Liferay.Service(
			EDIT_SEGMENTS_EXPERIENCE_URL,
			{
				active: true,
				nameMap,
				segmentsEntryId,
				segmentsExperienceId
			},
			obj => {
				const {
					active,
					nameCurrentValue,
					priority,
					segmentsEntryId,
					segmentsExperienceId
				} = obj;

				nextState = setIn(
					nextState,
					['availableSegmentsExperiences', segmentsExperienceId],
					{
						active,
						name: nameCurrentValue,
						priority,
						segmentsEntryId,
						segmentsExperienceId
					}
				);

				resolve(nextState);
			},
			error => {
				reject(error);
			}
		);
	});
}

/**
 *
 *
 * @param {object} state
 * @param {Array} state.availableSegmentsExperiences
 * @param {object} action
 * @param {('up' | 'down')} action.direction
 * @param {string} action.segmentsExperienceId
 * @param {number|string} action.priority
 * @param {string} action.type
 * @return {Promise}
 */
function updateSegmentsExperiencePriorityReducer(state, action) {
	return new Promise((resolve, reject) => {
		const {direction, priority: oldPriority, segmentsExperienceId} = action;
		let nextState = state;

		const priority =
			typeof oldPriority === 'number'
				? oldPriority
				: parseInt(oldPriority, 10);

		const newPriority = direction === 'up' ? priority + 1 : priority - 1;

		Liferay.Service(UPDATE_SEGMENTS_EXPERIENCE_PRIORITY_URL, {
			newPriority,
			segmentsExperienceId
		})
			.then(() => {
				const availableSegmentsExperiencesArray = Object.values(
					nextState.availableSegmentsExperiences
				);

				const subTargetExperience = availableSegmentsExperiencesArray.find(
					experience => {
						return experience.priority === newPriority;
					}
				);

				const targetExperience = availableSegmentsExperiencesArray.find(
					experience => {
						return experience.priority === priority;
					}
				);

				nextState = setIn(
					nextState,
					[
						'availableSegmentsExperiences',
						targetExperience.segmentsExperienceId,
						'priority'
					],
					newPriority
				);

				nextState = setIn(
					nextState,
					[
						'availableSegmentsExperiences',
						subTargetExperience.segmentsExperienceId,
						'priority'
					],
					priority
				);

				resolve(nextState);
			})
			.catch(error => {
				reject(error);
			});
	});
}

export {
	createSegmentsExperienceReducer,
	deleteSegmentsExperienceReducer,
	editSegmentsExperienceReducer,
	updateSegmentsExperiencePriorityReducer,
	selectSegmentsExperienceReducer
};