/**
* 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 'frontend-js-web/liferay/compat/modal/Modal.es';
import Component from 'metal-component';
import Soy, {Config} from 'metal-soy';
import {
CREATE_SEGMENTS_EXPERIENCE,
DELETE_SEGMENTS_EXPERIENCE,
EDIT_SEGMENTS_EXPERIENCE,
SELECT_SEGMENTS_EXPERIENCE,
UPDATE_SEGMENTS_EXPERIENCE_PRIORITY
} from '../../actions/actions.es';
import getConnectedComponent from '../../store/ConnectedComponent.es';
import {setIn} from '../../utils/FragmentsEditorUpdateUtils.es';
import templates from './SegmentsExperienceSelector.soy';
import './segmentsExperiences/modal.es';
const DISMISS_ALERT_ANIMATION_WAIT = 500;
const MODAL_EXPERIENCE_STATE_KEY = 'modalExperienceState';
/**
* Stores a given modalState
*
* @param {object} modalState
* @param {'creation' | 'edition'} modalState.type
* @param {string} modalState.experienceName
* @param {string} modalState.segmentsExperienceId
* @param {string} modalState.classPK
* @returns {void}
*/
function storeExperiencesState(modalState) {
window.sessionStorage.setItem(
MODAL_EXPERIENCE_STATE_KEY,
JSON.stringify(modalState)
);
}
/**
* @typedef experienceState
* @property {object} modalStates
* @property {'creation'|'edition'} modalStates.type
* @property {string} modalStates.experienceName
* @property {string} modalStates.classPK
* @property {string} modalStates.segmentsExperienceId
* @property {string} selectedSegmentsExpereinceId
*/
/**
* Looks for a modalState stored, wipes it and returns it;
*
* @returns {experienceState|null}
*/
function restoreExperiencesState() {
const state = window.sessionStorage.getItem(MODAL_EXPERIENCE_STATE_KEY);
if (state !== null) {
window.sessionStorage.removeItem(MODAL_EXPERIENCE_STATE_KEY);
return JSON.parse(state);
}
return state;
}
/**
* @typedef modalState
* @property {Object} [edition]
* @property {string} edition.name
* @property {string} edition.segmentsEntryId
* @property {string} edition.segmentsExperienceId
* @property {Object} [creation]
* @property {string} creation.name
* @property {string} creation.segmentsEntryId
* @property {string} creation.segmentsExperienceId
*/
/**
* @typedef experiencesState
* @property {modalState} modalState
* @property {string} selectedSegmentsExperienceId
*/
/**
* This function provides an state to restore a desired internal experiences state
*
* The state is provided conditionally
* if there is a modalState stored
* if the current page matches the one provided by that modalState
* and if the current url provides a segment id
*
* @param {string} classPK
* @param {string} incomingSegmentId
* @returns {modalState|null}
*/
function getExperiencesState(classPK, incomingSegmentId) {
if (!classPK) return null;
const prevState = restoreExperiencesState();
if (
incomingSegmentId &&
prevState &&
prevState.modalStates.classPK === classPK
) {
const {modalStates, selectedSegmentsExperienceId} = prevState;
return {
modalStates: {
[prevState.modalStates.type]: {
name: modalStates.experienceName,
segmentsEntryId: incomingSegmentId,
segmentsExperienceId: modalStates.segmentsExperienceId
}
},
selectedSegmentsExperienceId
};
}
return null;
}
/**
* Tells if a priority an `obj2`
* has higher, equal or lower priority
* than `obj1`.
* Return values can be 1, 0, or -1
* @param {object} obj1
* @param {object} obj2
* @returns {number}
* @review
*/
function comparePriority(obj1, obj2) {
let result = 0;
if (obj1.priority > obj2.priority) {
result = -1;
}
if (obj1.priority < obj2.priority) {
result = 1;
}
return result;
}
/**
* SegmentsExperienceSelector
*/
class SegmentsExperienceSelector extends Component {
/**
* Transforms `availableSegmentsEntries` and `availableSegmentsExperiences` objects into arrays
* Adds `activeSegmentsExperienceName` to the component state
* @inheritDoc
* @review
*/
prepareStateForRender(state) {
const availableSegmentsExperiencesArray = Object.values(
state.availableSegmentsExperiences || []
)
.sort(comparePriority)
.map(experience => {
const segmentEntry =
state.availableSegmentsEntries[experience.segmentsEntryId];
const name = segmentEntry && segmentEntry.name;
const updatedExperience = setIn(
experience,
['segmentsEntryName'],
name
);
return updatedExperience;
});
const selectedSegmentsExperienceId =
state.segmentsExperienceId || state.defaultSegmentsExperienceId;
const activeExperience = availableSegmentsExperiencesArray.find(
experience =>
experience.segmentsExperienceId === selectedSegmentsExperienceId
);
const availableSegmentsEntries = Object.values(
state.availableSegmentsEntries || []
).filter(
segment => segment.segmentsEntryId !== state.defaultSegmentsEntryId
);
const innerState = {
...state,
activeSegmentsExperienceName:
activeExperience && activeExperience.name,
availableSegmentsEntries,
availableSegmentsExperiences: availableSegmentsExperiencesArray,
classPK: state.classPK,
segmentsExperienceId: selectedSegmentsExperienceId
};
return innerState;
}
/**
* The classPK only changes when the component is connected for the first time
* with the store. This updates `this.modalStates` with previous persisted states
* if neccesary
*
* @param {string} next
* @review
*/
syncClassPK(next) {
if (next) {
const experiencesState = getExperiencesState(
next,
this.selectedSegmentsEntryId
);
this.modalStates = experiencesState && experiencesState.modalStates;
if (
experiencesState &&
experiencesState.selectedSegmentsExperienceId
) {
this._selectSegmentsExperience(
experiencesState.selectedSegmentsExperienceId
);
}
}
}
/**
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_closeDropdown() {
this._experiencesErrorHandler({
creation: false
});
this.openDropdown = false;
}
/**
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_closeCreateModal() {
this._experiencesModalStateHandler({
creation: false
});
}
/**
* Dispatches action to create an experience
* @memberof SegmentsExperienceSelector
* @param {!string} name
* @param {!string} segmentsEntryId
* @private
* @review
*/
_createSegmentsExperience(name, segmentsEntryId) {
this.store
.dispatch({
name,
segmentsEntryId,
type: CREATE_SEGMENTS_EXPERIENCE
})
.done(() => {
this._closeCreateModal();
Liferay.Util.openToast({
title: Liferay.Language.get(
'the-experience-was-created-successfully'
),
type: 'success'
});
})
.failed(() => {
this._experiencesErrorHandler({
creation: true
});
});
}
/**
* Dispatches action to delete an experience
* @memberof SegmentsExperienceSelector
* @param {!string} segmentsExperienceId
* @private
* @review
*/
_deleteSegmentsExperience(segmentsExperienceId) {
this.store
.dispatch({
segmentsExperienceId,
type: DELETE_SEGMENTS_EXPERIENCE
})
.done(() => {
Liferay.Util.openToast({
title: Liferay.Language.get(
'the-experience-was-deleted-successfully'
),
type: 'success'
});
})
.failed(() => {
this._openDropdown();
this._experiencesErrorHandler({
deletion: true
});
});
}
/**
* Clears the creation error with a `DISMISS_ALERT_ANIMATION_WAIT` miliseconds wait,
* so the dismissable alert can complete its animation
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_dismissCreationError() {
setTimeout(() => {
this._experiencesErrorHandler({
creation: false
});
}, DISMISS_ALERT_ANIMATION_WAIT);
}
/**
* Clears the edition with a `DISMISS_ALERT_ANIMATION_WAIT` miliseconds wait,
* so the dismissable alert can complete its animation
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_dismissEditionError() {
setTimeout(() => {
this._experiencesErrorHandler({
edition: false
});
}, DISMISS_ALERT_ANIMATION_WAIT);
}
/**
* Moves the focus to the create experience button
* Clears the error with a 500 miliseconds wait,
* so the dismissable alert can complete its animation
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_dismissDeletionError() {
this.refs.newExperienceBtn.focus();
setTimeout(() => {
this._experiencesErrorHandler({
deletion: false
});
}, 500);
}
/**
* Updates the error status for the experiences
*
* @param {object} objError
* @param {boolean} [objError.deletion] - True if experience deletion error has ocurred
* @param {boolean} [objError.creation] - True if experience creation error has ocurred
* @param {boolean} [objError.edition] - True if experience edition error has ocurred
* @memberof SegmentsExperienceSelector
*/
_experiencesErrorHandler(objError = {}) {
requestAnimationFrame(() => {
this._segmentsExperienceErrors = Object.assign(
this._segmentsExperienceErrors || {},
objError
);
});
}
/**
* @param {!string} segmentsExperienceId
* @param {!string} name
* @memberof SegmentsExperienceSelector
*/
_editSegmentsExperience({name, segmentsEntryId, segmentsExperienceId}) {
this.store
.dispatch({
name,
segmentsEntryId,
segmentsExperienceId,
type: EDIT_SEGMENTS_EXPERIENCE
})
.done(() => {
this._experiencesModalStateHandler({
edition: false
});
Liferay.Util.openToast({
title: Liferay.Language.get(
'the-experience-was-updated-successfully'
),
type: 'success'
});
})
.failed(() => {
this._experiencesErrorHandler({
edition: true
});
});
}
/**
* Callback that is executed on delete button click
* @memberof SegmentsExperienceSelector
* @param {!Event} event
* @review
* @private
*/
_handleDeleteButtonClick(event) {
this._experiencesErrorHandler({
deletion: false
});
const confirmed = confirm(
Liferay.Language.get('do-you-want-to-delete-this-experience')
);
if (confirmed) {
const segmentsExperienceId = event.currentTarget.getAttribute(
'data-segmentsExperienceId'
);
this._deleteSegmentsExperience(segmentsExperienceId);
}
}
/**
* Callback that is executed on dropdown blur
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_handleDropdownBlur() {
cancelAnimationFrame(this.willToggleDropdownId);
this.willToggleDropdownId = requestAnimationFrame(() => {
this._closeDropdown();
});
}
/**
* Callback that is executed on dropdown button click
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_handleDropdownButtonClick() {
this._toggleDropdown();
}
/**
* Callback that is executed on dropdown focus
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_handleDropdownFocus() {
cancelAnimationFrame(this.willToggleDropdownId);
}
/**
* Callback executed on edit button click
* @memberof SegmentsExperienceSelector
* @param {Event} event
* @private
* @review
*/
_handleEditButtonClick(event) {
const name = event.currentTarget.getAttribute('data-name');
const segmentsEntryId = event.currentTarget.getAttribute(
'data-segmentsEntryId'
);
const segmentsExperienceId = event.currentTarget.getAttribute(
'data-segmentsExperienceId'
);
this._openEditModal({
name,
segmentsEntryId,
segmentsExperienceId
});
}
/**
* @memberof SegmentsExperienceSelector
*/
_handleNewSegmentButtonClick(type, experienceName) {
const classPK = this.classPK;
const segmentsExperienceId =
(this.modalStates[type] &&
this.modalStates[type].segmentsExperienceId) ||
undefined;
storeExperiencesState({
modalStates: {
classPK,
experienceName,
segmentsExperienceId,
type
},
selectedSegmentsExperienceId: this.segmentsExperienceId
});
Liferay.Util.navigate(this.editSegmentsEntryURL);
}
/**
* Callback that is executed on experience click
* @memberof SegmentsExperienceSelector
* @param {Event} event
* @private
* @review
*/
_handleSegmentsExperienceClick(event) {
const segmentsExperienceId =
event.delegateTarget.dataset.segmentsExperienceId;
this._selectSegmentsExperience(segmentsExperienceId);
}
/**
* @memberof SegmentsExperienceSelector
* @review
* @param {!string} name
* @param {!string} segmentsEntryId
*/
_handleEditFormSubmit(name, segmentsEntryId) {
this._editSegmentsExperience({
name,
segmentsEntryId,
segmentsExperienceId: this.modalStates.edition.segmentsExperienceId
});
}
/**
* Triggers update priority store action
* @param {Event} event
* @memberof SegmentsExperienceSelector
* @review
*/
_handleMoveExperienceUpButtonClick(event) {
const priority = event.currentTarget.getAttribute('data-priority');
const segmentsExperienceId = event.currentTarget.getAttribute(
'data-segmentsExperienceId'
);
const buttonPriorityUp = this.refs[
`buttonPriorityUp${segmentsExperienceId}`
];
const selectExperienceBtnRef = this.refs[
`selectExperienceButton${segmentsExperienceId}`
];
this._updatePriority({
focusFallbackElement: selectExperienceBtnRef,
payload: {
direction: 'up',
priority,
segmentsExperienceId
},
priorityButton: buttonPriorityUp.element
});
}
/**
* Triggers update priority store action
* @param {Event} event
* @memberof SegmentsExperienceSelector
* @review
*/
_handleMoveExperienceDownButtonClick(event) {
const priority = event.currentTarget.getAttribute('data-priority');
const segmentsExperienceId = event.currentTarget.getAttribute(
'data-segmentsExperienceId'
);
const buttonPriorityDown = this.refs[
`buttonPriorityDown${segmentsExperienceId}`
];
const selectExperienceBtnRef = this.refs[
`selectExperienceButton${segmentsExperienceId}`
];
this._updatePriority({
focusFallbackElement: selectExperienceBtnRef,
payload: {
direction: 'down',
priority,
segmentsExperienceId
},
priorityButton: buttonPriorityDown.element
});
}
/**
* Dispatchs priority update actions
* and handles focus change when necessary
*
* @param {HTMLButtonElement} priorityButton
* @param {HTMLElement} focusFallbackElement
* @param {object} payload
* @param {!('down'|'up')} payload.direction
* @param {!number} payload.priority
* @param {!string} payload.segmentsExperienceId
* @memberof SegmentsExperienceSelector
*/
_updatePriority({focusFallbackElement, payload, priorityButton}) {
const onBlur = () => {
focusFallbackElement.focus();
priorityButton.removeEventListener('blur', onBlur);
};
priorityButton.addEventListener('blur', onBlur);
const removeBlurListener = () => {
priorityButton.removeEventListener('blur', onBlur);
};
this.store
.dispatch({...payload, type: UPDATE_SEGMENTS_EXPERIENCE_PRIORITY})
.done(removeBlurListener)
.failed(removeBlurListener);
}
/**
* Opens dropdown
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_openDropdown() {
this.openDropdown = true;
}
/**
* Opens experience creation modal
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_openCreateModal() {
this._experiencesModalStateHandler({
creation: true
});
}
/**
* Opens edit experience modal
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_openEditModal({
name = '',
segmentsEntryId = null,
segmentsExperienceId = null
}) {
this._experiencesModalStateHandler({
edition: {
name,
segmentsEntryId,
segmentsExperienceId
}
});
}
/**
* @param {object} [newState = {}]
* @param {boolean} [newState.creation] - The status of the experience creation modal
* @param {boolean} [newState.edition] - The status of the experience edition modal
* @memberof SegmentsExperienceSelector
*/
_experiencesModalStateHandler(newState = {}) {
this.modalStates = newState;
}
/**
* Dispatches action to select an experience
* @memberof SegmentsExperienceSelector
* @param {!string} segmentsExperienceId
* @private
* @review
*/
_selectSegmentsExperience(segmentsExperienceId) {
this.store.dispatch({
segmentsExperienceId,
type: SELECT_SEGMENTS_EXPERIENCE
});
}
/**
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_closeEditModal() {
this._experiencesErrorHandler({
edition: false
});
this._experiencesModalStateHandler({
edition: false
});
}
/**
* Toggles the modal
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_toggleEditModal() {
const modalEditAction =
this.modalStates && this.modalStates.edition
? this._closeEditModal
: this._openEditModal;
modalEditAction.call(this);
}
/**
* Toggles the modal
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_toggleCreateModal() {
const modalAction = this.modalStates.creation
? this._closeCreateModal
: this._openCreateModal;
modalAction.call(this);
}
/**
* Toggles the dropdown
* @memberof SegmentsExperienceSelector
* @private
* @review
*/
_toggleDropdown() {
const dropdownAction = this.openDropdown
? this._closeDropdown
: this._openDropdown;
dropdownAction.call(this);
}
}
SegmentsExperienceSelector.STATE = {
/**
* Url to redirect the user when clicking new experience
*/
editSegmentsEntryURL: Config.string(),
/**
* Contains the state of Experience edition and creation
*/
modalStates: Config.object(),
/**
* Boolean to control the state of the experiences modal
*/
openDropdown: Config.bool()
.internal()
.value(false),
/**
* Segments Id of a just created Segment to recover Experiences modal state
*/
selectedSegmentsEntryId: Config.string()
};
const ConnectedSegmentsExperienceSelector = getConnectedComponent(
SegmentsExperienceSelector,
[
'classPK',
'availableSegmentsExperiences',
'segmentsExperienceId',
'defaultSegmentsEntryId'
]
);
Soy.register(ConnectedSegmentsExperienceSelector, templates);
export {ConnectedSegmentsExperienceSelector};
export default ConnectedSegmentsExperienceSelector;