Source: reducers/collectionsReducers.js

/**
 * @module react-cmf/lib/reducers/collectionsReducers
 */
import { Map, List, fromJS } from 'immutable';
import invariant from 'invariant';
import CONSTANTS from '../constant';

export const defaultState = new Map();

/**
 * Get element id. If it doesn't have "id" property, we consider it as immutable.
 */
export function getId(element) {
	const id = element.id;
	if (id === undefined) {
		return element.get('id');
	}
	return id;
}

/*
 * backward compatibility, as mutateCollection action creator still use 'id' field
 * to represent path to collection
 */
export function getActionWithCollectionIdAsArray(action) {
	if (action.collectionId || action.id) {
		const collectionId = action.collectionId || action.id;
		return {
			...action,
			collectionId: Array.isArray(collectionId) ? collectionId : collectionId.split('.'),
		};
	}
	return action;
}

/**
 * addElementToCollection
 *
 * @param state current redux state
 * @param action redux action
 * @returns {object} the new state
 */
function addCollectionElement(state, action) {
	if (action.operations.add) {
		return action.operations.add.reduce((s, e) => {
			const element = s.getIn(action.collectionId);
			if (List.isList(element)) {
				return s.setIn(action.collectionId, element.push(e));
			}
			if (Map.isMap(element)) {
				return s.setIn(action.collectionId, element.merge(e));
			}
			return state;
		}, state);
	}
	return state;
}

function deleteListElements(state, action) {
	function shouldBeRemoved(element) {
		return action.operations.delete.indexOf(getId(element)) >= 0;
	}

	const collection = state.getIn(action.collectionId);
	if (collection.some(shouldBeRemoved)) {
		return state.setIn(action.collectionId, collection.filterNot(shouldBeRemoved));
	}
	return state;
}

function deleteMapElements(state, action) {
	const collection = state.getIn(action.collectionId);

	if (action.operations.delete.some(id => collection.has(id))) {
		const changedCollection = action.operations.delete.reduce(
			(collectionAccu, element) => collectionAccu.delete(element),
			collection,
		);
		return state.setIn(action.collectionId, changedCollection);
	}

	return state;
}

/**
 * deleteElementFromCollection
 *
 * @param state current redux state
 * @param action redux action
 * @returns {object} the new state
 */
function deleteCollectionElement(state, action) {
	if (action.operations.delete) {
		const collection = state.getIn(action.collectionId);
		if (Map.isMap(collection)) {
			return deleteMapElements(state, action);
		} else if (List.isList(collection)) {
			return deleteListElements(state, action);
		}
		throw new Error('CMF collection deletion is only compatible with ImmutableJs List and Map');
	}
	return state;
}

function updateListElements(state, action) {
	const updates = action.operations.update;

	const changedCollection = state
		.getIn(action.collectionId)
		.map(element => updates[getId(element)] || element);
	return state.setIn(action.collectionId, changedCollection);
}

function updateMapElements(state, action) {
	const updates = action.operations.update;
	const changedCollection = Object.keys(updates).reduce(
		(collectionAccu, id) => collectionAccu.set(id, updates[id]),
		state.getIn(action.collectionId),
	);
	return state.setIn(action.collectionId, changedCollection);
}

/**
 * updateCollectionElement
 *
 * @param state current redux state
 * @param action redux action
 * @returns {object} the new state
 */
function updateCollectionElement(state, action) {
	if (action.operations.update) {
		const collection = state.getIn(action.collectionId);
		if (Map.isMap(collection)) {
			return updateMapElements(state, action);
		} else if (List.isList(collection)) {
			return updateListElements(state, action);
		}
		throw new Error('CMF collection update is only compatible with ImmutableJs List and Map');
	}
	return state;
}

/**
 * mutateCollection
 *
 * @param {object} state the current redux state
 * @param {object} action redux action
 * @returns {object} the new state
 */
function mutateCollection(state, action) {
	if (!action.operations || !state.hasIn(action.collectionId) || state.isEmpty()) {
		return state;
	}
	let newState = addCollectionElement(state, action);
	newState = deleteCollectionElement(newState, action);
	return updateCollectionElement(newState, action);
}

/**
 * @param  {object} state  the state
 * @param  {object} action redux action
 * @return {object}        the new state
 */
function collectionsReducers(state = defaultState, action = { type: '' }) {
	const newAction = getActionWithCollectionIdAsArray(action);
	switch (newAction.type) {
		case CONSTANTS.COLLECTION_ADD_OR_REPLACE:
			return state.setIn(newAction.collectionId, fromJS(newAction.data));
		case CONSTANTS.COLLECTION_REMOVE:
			if (!state.getIn(newAction.collectionId)) {
				invariant(
					process.env.NODE_ENV === 'production',
					`Can't remove collection ${newAction.collectionId} since it doesn't exist.`,
				);
				return state;
			}
			return state.deleteIn(newAction.collectionId);
		case CONSTANTS.COLLECTION_MUTATE:
			return mutateCollection(state, newAction);
		default:
			return state;
	}
}

export default collectionsReducers;