mirror of https://github.com/jitsi/jitsi-meet
feat(feedback): convert to react and redux (#1833)
* feat(feedback): convert to react and redux - For styles, remove "aui-dialog2" nesting so existing styles can be reused. - Remove Feedback.js and replace with calls to redux for state storing and accessing. - Add dispatching to FeedbackButton instead of relying on jquery clicking handling so the button can be hooked into redux. * address feedback * remove calling to not show feedback for recorder and filmstrippull/1872/head jitsi-meet_2338
parent
85a168d51b
commit
ff442853a2
@ -1,95 +0,0 @@ |
||||
/* global $, APP, JitsiMeetJS */ |
||||
import FeedbackWindow from "./FeedbackWindow"; |
||||
|
||||
/** |
||||
* Defines all methods in connection to the Feedback window. |
||||
* |
||||
* @type {{openFeedbackWindow: Function}} |
||||
*/ |
||||
const Feedback = { |
||||
|
||||
/** |
||||
* Initialise the Feedback functionality. |
||||
* @param emitter the EventEmitter to associate with the Feedback. |
||||
*/ |
||||
init: function (emitter) { |
||||
// CallStats is the way we send feedback, so we don't have to initialise
|
||||
// if callstats isn't enabled.
|
||||
if (!APP.conference.isCallstatsEnabled()) |
||||
return; |
||||
|
||||
// If enabled property is still undefined, i.e. it hasn't been set from
|
||||
// some other module already, we set it to true by default.
|
||||
if (typeof this.enabled == "undefined") |
||||
this.enabled = true; |
||||
|
||||
this.window = new FeedbackWindow(); |
||||
this.emitter = emitter; |
||||
|
||||
$("#feedbackButton").click(Feedback.openFeedbackWindow); |
||||
}, |
||||
/** |
||||
* Enables/ disabled the feedback feature. |
||||
*/ |
||||
enableFeedback: function (enable) { |
||||
this.enabled = enable; |
||||
}, |
||||
|
||||
/** |
||||
* Indicates if the feedback functionality is enabled. |
||||
* |
||||
* @return true if the feedback functionality is enabled, false otherwise. |
||||
*/ |
||||
isEnabled: function() { |
||||
return this.enabled && APP.conference.isCallstatsEnabled(); |
||||
}, |
||||
|
||||
/** |
||||
* Returns true if the feedback window is currently visible and false |
||||
* otherwise. |
||||
* @return {boolean} true if the feedback window is visible, false |
||||
* otherwise |
||||
*/ |
||||
isVisible: function() { |
||||
return $(".feedback").is(":visible"); |
||||
}, |
||||
|
||||
/** |
||||
* Indicates if the feedback is submitted. |
||||
* |
||||
* @return {boolean} {true} to indicate if the feedback is submitted, |
||||
* {false} - otherwise |
||||
*/ |
||||
isSubmitted: function() { |
||||
return Feedback.window.submitted; |
||||
}, |
||||
|
||||
/** |
||||
* Opens the feedback window. |
||||
*/ |
||||
openFeedbackWindow: function (callback) { |
||||
Feedback.window.show(callback); |
||||
|
||||
JitsiMeetJS.analytics.sendEvent('feedback.open'); |
||||
}, |
||||
|
||||
/** |
||||
* Returns the feedback score. |
||||
* |
||||
* @returns {*} |
||||
*/ |
||||
getFeedbackScore: function() { |
||||
return Feedback.window.feedbackScore; |
||||
}, |
||||
|
||||
/** |
||||
* Returns the feedback free text. |
||||
* |
||||
* @returns {null|*|message} |
||||
*/ |
||||
getFeedbackText: function() { |
||||
return Feedback.window.feedbackText; |
||||
} |
||||
}; |
||||
|
||||
export default Feedback; |
@ -1,184 +0,0 @@ |
||||
/* global $, APP, interfaceConfig */ |
||||
|
||||
const labels = { |
||||
1: 'Very Bad', |
||||
2: 'Bad', |
||||
3: 'Average', |
||||
4: 'Good', |
||||
5: 'Very Good' |
||||
}; |
||||
|
||||
/** |
||||
* Toggles the appropriate css class for the given number of stars, to |
||||
* indicate that those stars have been clicked/selected. |
||||
* |
||||
* @param starCount the number of stars, for which to toggle the css class |
||||
*/ |
||||
function toggleStars(starCount) { |
||||
let labelEl = $('#starLabel'); |
||||
let label = starCount >= 0 ? |
||||
labels[starCount + 1] : |
||||
''; |
||||
|
||||
$('#stars > a').each(function(index, el) { |
||||
if (index <= starCount) { |
||||
el.classList.add("starHover"); |
||||
} else |
||||
el.classList.remove("starHover"); |
||||
}); |
||||
labelEl.text(label); |
||||
} |
||||
|
||||
/** |
||||
* Constructs the html for the rated feedback window. |
||||
* |
||||
* @returns {string} the contructed html string |
||||
*/ |
||||
function createRateFeedbackHTML() { |
||||
|
||||
let starClassName = (interfaceConfig.ENABLE_FEEDBACK_ANIMATION) |
||||
? "icon-star-full shake-rotate" |
||||
: "icon-star-full"; |
||||
|
||||
return ` |
||||
<form id="feedbackForm" |
||||
action="javascript:false;" onsubmit="return false;"> |
||||
<div class="rating"> |
||||
<div class="star-label"> |
||||
<p id="starLabel"> </p> |
||||
</div> |
||||
<div id="stars" class="feedback-stars"> |
||||
<a class="star-btn"> |
||||
<i class=${ starClassName }></i> |
||||
</a> |
||||
<a class="star-btn"> |
||||
<i class=${ starClassName }></i> |
||||
</a> |
||||
<a class="star-btn"> |
||||
<i class=${ starClassName }></i> |
||||
</a> |
||||
<a class="star-btn"> |
||||
<i class=${ starClassName }></i> |
||||
</a> |
||||
<a class="star-btn"> |
||||
<i class=${ starClassName }></i> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<div class="details"> |
||||
<textarea id="feedbackTextArea" class="input-control"
|
||||
data-i18n="[placeholder]dialog.feedbackHelp"></textarea> |
||||
</div> |
||||
</form>`; |
||||
} |
||||
|
||||
/** |
||||
* Feedback is loaded callback |
||||
* Calls when Modal window is in DOM |
||||
* |
||||
* @param Feedback |
||||
*/ |
||||
let onLoadFunction = function (Feedback) { |
||||
$('#stars > a').each((index, el) => { |
||||
el.onmouseover = function(){ |
||||
toggleStars(index); |
||||
}; |
||||
el.onmouseleave = function(){ |
||||
toggleStars(Feedback.feedbackScore - 1); |
||||
}; |
||||
el.onclick = function(){ |
||||
Feedback.feedbackScore = index + 1; |
||||
Feedback.setFeedbackMessage(); |
||||
}; |
||||
}); |
||||
|
||||
// Init stars to correspond to previously entered feedback.
|
||||
if (Feedback.feedbackScore > 0) { |
||||
toggleStars(Feedback.feedbackScore - 1); |
||||
} |
||||
|
||||
if (Feedback.feedbackMessage && Feedback.feedbackMessage.length > 0) |
||||
$('#feedbackTextArea').text(Feedback.feedbackMessage); |
||||
|
||||
$('#feedbackTextArea').focus(); |
||||
}; |
||||
|
||||
/** |
||||
* On Feedback Submitted callback |
||||
* |
||||
* @param Feedback |
||||
*/ |
||||
function onFeedbackSubmitted(Feedback) { |
||||
let form = $('#feedbackForm'); |
||||
let message = form.find('textarea').val(); |
||||
|
||||
APP.conference.sendFeedback( |
||||
Feedback.feedbackScore, |
||||
message); |
||||
|
||||
// TODO: make sendFeedback return true or false.
|
||||
Feedback.submitted = true; |
||||
|
||||
//Remove history is submitted
|
||||
Feedback.feedbackScore = -1; |
||||
Feedback.feedbackMessage = ''; |
||||
Feedback.onHide(); |
||||
} |
||||
|
||||
/** |
||||
* On Feedback Closed callback |
||||
* |
||||
* @param Feedback |
||||
*/ |
||||
function onFeedbackClosed(Feedback) { |
||||
Feedback.onHide(); |
||||
} |
||||
|
||||
/** |
||||
* @class Dialog |
||||
* |
||||
*/ |
||||
export default class Dialog { |
||||
|
||||
constructor() { |
||||
this.feedbackScore = -1; |
||||
this.feedbackMessage = ''; |
||||
this.submitted = false; |
||||
this.onCloseCallback = function() {}; |
||||
|
||||
this.setDefaultOptions(); |
||||
} |
||||
|
||||
setDefaultOptions() { |
||||
var self = this; |
||||
|
||||
this.options = { |
||||
titleKey: 'dialog.rateExperience', |
||||
msgString: createRateFeedbackHTML(), |
||||
loadedFunction: function() {onLoadFunction(self);}, |
||||
submitFunction: function() {onFeedbackSubmitted(self);}, |
||||
closeFunction: function() {onFeedbackClosed(self);}, |
||||
wrapperClass: 'feedback', |
||||
size: 'medium' |
||||
}; |
||||
} |
||||
|
||||
setFeedbackMessage() { |
||||
this.feedbackMessage = $('#feedbackTextArea').val(); |
||||
} |
||||
|
||||
show(cb) { |
||||
const options = this.options; |
||||
if (typeof cb === 'function') { |
||||
this.onCloseCallback = cb; |
||||
} |
||||
|
||||
this.window = APP.UI.messageHandler.openTwoButtonDialog(options); |
||||
} |
||||
|
||||
onHide() { |
||||
this.onCloseCallback({ |
||||
feedbackSubmitted: this.submitted |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
/** |
||||
* The type of the action which signals feedback was closed without submitting. |
||||
* |
||||
* { |
||||
* type: CANCEL_FEEDBACK, |
||||
* message: string, |
||||
* score: number |
||||
* } |
||||
*/ |
||||
export const CANCEL_FEEDBACK = Symbol('CANCEL_FEEDBACK'); |
||||
|
||||
/** |
||||
* The type of the action which signals feedback was submitted for recording. |
||||
* |
||||
* { |
||||
* type: SUBMIT_FEEDBACK, |
||||
* message: string, |
||||
* score: number |
||||
* } |
||||
*/ |
||||
export const SUBMIT_FEEDBACK = Symbol('SUBMIT_FEEDBACK'); |
@ -0,0 +1,122 @@ |
||||
import { FEEDBACK_REQUEST_IN_PROGRESS } from '../../../modules/UI/UIErrors'; |
||||
|
||||
import { openDialog } from '../../features/base/dialog'; |
||||
|
||||
import { |
||||
CANCEL_FEEDBACK, |
||||
SUBMIT_FEEDBACK |
||||
} from './actionTypes'; |
||||
import { FeedbackDialog } from './components'; |
||||
|
||||
declare var config: Object; |
||||
declare var interfaceConfig: Object; |
||||
|
||||
/** |
||||
* Caches the passed in feedback in the redux store. |
||||
* |
||||
* @param {number} score - The quality score given to the conference. |
||||
* @param {string} message - A description entered by the participant that |
||||
* explains the rating. |
||||
* @returns {{ |
||||
* type: CANCEL_FEEDBACK, |
||||
* message: string, |
||||
* score: number |
||||
* }} |
||||
*/ |
||||
export function cancelFeedback(score, message) { |
||||
return { |
||||
type: CANCEL_FEEDBACK, |
||||
message, |
||||
score |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Potentially open the {@code FeedbackDialog}. It will not be opened if it is |
||||
* already open or feedback has already been submitted. |
||||
* |
||||
* @param {JistiConference} conference - The conference for which the feedback |
||||
* would be about. The conference is passed in because feedback can occur after |
||||
* a conference has been left, so references to it may no longer exist in redux. |
||||
* @returns {Promise} Resolved with value - false if the dialog is enabled and |
||||
* resolved with true if the dialog is disabled or the feedback was already |
||||
* submitted. Rejected if another dialog is already displayed. |
||||
*/ |
||||
export function maybeOpenFeedbackDialog(conference) { |
||||
return (dispatch, getState) => { |
||||
const state = getState(); |
||||
|
||||
if (interfaceConfig.filmStripOnly || config.iAmRecorder) { |
||||
// Intentionally fall through the if chain to prevent further action
|
||||
// from being taken with regards to showing feedback.
|
||||
} else if (state['features/base/dialog'].component === FeedbackDialog) { |
||||
// Feedback is currently being displayed.
|
||||
|
||||
return Promise.reject(FEEDBACK_REQUEST_IN_PROGRESS); |
||||
} else if (state['features/feedback'].submitted) { |
||||
// Feedback has been submitted already.
|
||||
|
||||
return Promise.resolve({ |
||||
thankYouDialogVisible: true, |
||||
feedbackSubmitted: true |
||||
}); |
||||
} else if (conference.isCallstatsEnabled()) { |
||||
return new Promise(resolve => { |
||||
dispatch(openFeedbackDialog(conference, () => { |
||||
const { submitted } = getState()['features/feedback']; |
||||
|
||||
resolve({ |
||||
feedbackSubmitted: submitted, |
||||
thankYouDialogVisible: false |
||||
}); |
||||
})); |
||||
}); |
||||
} |
||||
|
||||
// If the feedback functionality isn't enabled we show a thank
|
||||
// you dialog. Signaling it (true), so the caller
|
||||
// of requestFeedback can act on it
|
||||
return Promise.resolve({ |
||||
thankYouDialogVisible: true, |
||||
feedbackSubmitted: false |
||||
}); |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Opens {@code FeedbackDialog}. |
||||
* |
||||
* @param {JitsiConference} conference - The JitsiConference that is being |
||||
* rated. The conference is passed in because feedback can occur after a |
||||
* conference has been left, so references to it may no longer exist in redux. |
||||
* @param {Function} [onClose] - An optional callback to invoke when the dialog |
||||
* is closed. |
||||
* @returns {Object} |
||||
*/ |
||||
export function openFeedbackDialog(conference, onClose) { |
||||
return openDialog(FeedbackDialog, { |
||||
conference, |
||||
onClose |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Send the passed in feedback. |
||||
* |
||||
* @param {number} score - An integer between 1 and 5 indicating the user |
||||
* feedback. The negative integer -1 is used to denote no score was selected. |
||||
* @param {string} message - Detailed feedback from the user to explain the |
||||
* rating. |
||||
* @param {JitsiConference} conference - The JitsiConference for which the |
||||
* feedback is being left. |
||||
* @returns {{ |
||||
* type: SUBMIT_FEEDBACK |
||||
* }} |
||||
*/ |
||||
export function submitFeedback(score, message, conference) { |
||||
conference.sendFeedback(score, message); |
||||
|
||||
return { |
||||
type: SUBMIT_FEEDBACK |
||||
}; |
||||
} |
@ -0,0 +1,343 @@ |
||||
import StarIcon from '@atlaskit/icon/glyph/star'; |
||||
import StarFilledIcon from '@atlaskit/icon/glyph/star-filled'; |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { Dialog } from '../../base/dialog'; |
||||
import { translate } from '../../base/i18n'; |
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet'; |
||||
|
||||
import { cancelFeedback, submitFeedback } from '../actions'; |
||||
|
||||
declare var interfaceConfig: Object; |
||||
|
||||
const scoreAnimationClass = interfaceConfig.ENABLE_FEEDBACK_ANIMATION |
||||
? 'shake-rotate' : ''; |
||||
|
||||
/** |
||||
* The scores to display for selecting. The score is the index in the array and |
||||
* the value of the index is a translation key used for display in the dialog. |
||||
* |
||||
* @types {string[]} |
||||
*/ |
||||
const SCORES = [ |
||||
'feedback.veryBad', |
||||
'feedback.bad', |
||||
'feedback.average', |
||||
'feedback.good', |
||||
'feedback.veryGood' |
||||
]; |
||||
|
||||
/** |
||||
* A React {@code Component} for displaying a dialog to rate the current |
||||
* conference quality, write a message describing the experience, and submit |
||||
* the feedback. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class FeedbackDialog extends Component { |
||||
/** |
||||
* {@code FeedbackDialog} component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The cached feedback message, if any, that was set when closing a |
||||
* previous instance of {@code FeedbackDialog}. |
||||
*/ |
||||
_message: React.PropTypes.string, |
||||
|
||||
/** |
||||
* The cached feedback score, if any, that was set when closing a |
||||
* previous instance of {@code FeedbackDialog}. |
||||
*/ |
||||
_score: React.PropTypes.number, |
||||
|
||||
/** |
||||
* The JitsiConference that is being rated. The conference is passed in |
||||
* because feedback can occur after a conference has been left, so |
||||
* references to it may no longer exist in redux. |
||||
* |
||||
* @type {JitsiConference} |
||||
*/ |
||||
conference: React.PropTypes.object, |
||||
|
||||
/** |
||||
* Invoked to signal feedback submission or canceling. |
||||
*/ |
||||
dispatch: React.PropTypes.func, |
||||
|
||||
/** |
||||
* Callback invoked when {@code FeedbackDialog} is unmounted. |
||||
*/ |
||||
onClose: React.PropTypes.func, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code FeedbackDialog} instance. |
||||
* |
||||
* @param {Object} props - The read-only React {@code Component} props with |
||||
* which the new instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
const { _message, _score } = this.props; |
||||
|
||||
this.state = { |
||||
/** |
||||
* The currently entered feedback message. |
||||
* |
||||
* @type {string} |
||||
*/ |
||||
message: _message, |
||||
|
||||
/** |
||||
* The score selection index which is currently being hovered. The |
||||
* value -1 is used as a sentinel value to match store behavior of |
||||
* using -1 for no score having been selected. |
||||
* |
||||
* @type {number} |
||||
*/ |
||||
mousedOverScore: -1, |
||||
|
||||
/** |
||||
* The currently selected score selection index. The score will not |
||||
* be 0 indexed so subtract one to map with SCORES. |
||||
* |
||||
* @type {number} |
||||
*/ |
||||
score: _score > -1 ? _score - 1 : _score |
||||
}; |
||||
|
||||
/** |
||||
* An array of objects with click handlers for each of the scores listed |
||||
* in SCORES. This pattern is used for binding event handlers only once |
||||
* for each score selection icon. |
||||
* |
||||
* @type {Object[]} |
||||
*/ |
||||
this._scoreClickConfigurations = SCORES.map((textKey, index) => { |
||||
return { |
||||
_onClick: () => this._onScoreSelect(index), |
||||
_onMouseOver: () => this._onScoreMouseOver(index) |
||||
}; |
||||
}); |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onCancel = this._onCancel.bind(this); |
||||
this._onMessageChange = this._onMessageChange.bind(this); |
||||
this._onScoreContainerMouseLeave |
||||
= this._onScoreContainerMouseLeave.bind(this); |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Emits an analytics event to notify feedback has been opened. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
JitsiMeetJS.analytics.sendEvent('feedback.open'); |
||||
} |
||||
|
||||
/** |
||||
* Invokes the onClose callback, if defined, to notify of the close event. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
if (this.props.onClose) { |
||||
this.props.onClose(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { message, mousedOverScore, score } = this.state; |
||||
const scoreToDisplayAsSelected |
||||
= mousedOverScore > -1 ? mousedOverScore : score; |
||||
|
||||
const scoreIcons = this._scoreClickConfigurations.map( |
||||
(config, index) => { |
||||
const isFilled = index <= scoreToDisplayAsSelected; |
||||
const activeClass = isFilled ? 'active' : ''; |
||||
const className |
||||
= `star-btn ${scoreAnimationClass} ${activeClass}`; |
||||
|
||||
return ( |
||||
<a |
||||
className = { className } |
||||
key = { index } |
||||
onClick = { config._onClick } |
||||
onMouseOver = { config._onMouseOver }> |
||||
{ isFilled |
||||
? <StarFilledIcon |
||||
label = 'star-filled' |
||||
size = 'xlarge' /> |
||||
: <StarIcon |
||||
label = 'star' |
||||
size = 'xlarge' /> } |
||||
</a> |
||||
); |
||||
}); |
||||
|
||||
const { t } = this.props; |
||||
|
||||
return ( |
||||
<Dialog |
||||
okTitleKey = 'dialog.Submit' |
||||
onCancel = { this._onCancel } |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = 'feedback.rateExperience'> |
||||
<div className = 'feedback-dialog'> |
||||
<div className = 'rating'> |
||||
<div className = 'star-label'> |
||||
<p id = 'starLabel'> |
||||
{ t(SCORES[scoreToDisplayAsSelected]) } |
||||
</p> |
||||
</div> |
||||
<div |
||||
className = 'stars' |
||||
onMouseLeave = { this._onScoreContainerMouseLeave }> |
||||
{ scoreIcons } |
||||
</div> |
||||
</div> |
||||
<div className = 'details'> |
||||
<textarea |
||||
autoFocus = { true } |
||||
className = 'input-control' |
||||
id = 'feedbackTextArea' |
||||
onChange = { this._onMessageChange } |
||||
placeholder = { t('dialog.feedbackHelp') } |
||||
value = { message } /> |
||||
</div> |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Dispatches an action notifying feedback was not submitted. The submitted |
||||
* score will have one added as the rest of the app does not expect 0 |
||||
* indexing. |
||||
* |
||||
* @private |
||||
* @returns {boolean} Returns true to close the dialog. |
||||
*/ |
||||
_onCancel() { |
||||
const { message, score } = this.state; |
||||
const scoreToSubmit = score > -1 ? score + 1 : score; |
||||
|
||||
this.props.dispatch(cancelFeedback(scoreToSubmit, message)); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Updates the known entered feedback message. |
||||
* |
||||
* @param {Object} event - The DOM event from updating the textfield for the |
||||
* feedback message. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onMessageChange(event) { |
||||
this.setState({ message: event.target.value }); |
||||
} |
||||
|
||||
/** |
||||
* Updates the currently selected score. |
||||
* |
||||
* @param {number} score - The index of the selected score in SCORES. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onScoreSelect(score) { |
||||
this.setState({ score }); |
||||
} |
||||
|
||||
/** |
||||
* Sets the currently hovered score to null to indicate no hover is |
||||
* occurring. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onScoreContainerMouseLeave() { |
||||
this.setState({ mousedOverScore: -1 }); |
||||
} |
||||
|
||||
/** |
||||
* Updates the known state of the score icon currently behind hovered over. |
||||
* |
||||
* @param {number} mousedOverScore - The index of the SCORES value currently |
||||
* being moused over. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onScoreMouseOver(mousedOverScore) { |
||||
this.setState({ mousedOverScore }); |
||||
} |
||||
|
||||
/** |
||||
* Dispatches the entered feedback for submission. The submitted score will |
||||
* have one added as the rest of the app does not expect 0 indexing. |
||||
* |
||||
* @private |
||||
* @returns {boolean} Returns true to close the dialog. |
||||
*/ |
||||
_onSubmit() { |
||||
const { conference, dispatch } = this.props; |
||||
const { message, score } = this.state; |
||||
|
||||
const scoreToSubmit = score > -1 ? score + 1 : score; |
||||
|
||||
dispatch(submitFeedback(scoreToSubmit, message, conference)); |
||||
|
||||
return true; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated {@code FeedbackDialog}'s |
||||
* props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { message, score } = state['features/feedback']; |
||||
|
||||
return { |
||||
/** |
||||
* The cached feedback message, if any, that was set when closing a |
||||
* previous instance of {@code FeedbackDialog}. |
||||
* |
||||
* @type {string} |
||||
*/ |
||||
_message: message, |
||||
|
||||
/** |
||||
* The currently selected score selection index. |
||||
* |
||||
* @type {number} |
||||
*/ |
||||
_score: score |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(FeedbackDialog)); |
@ -1 +1,2 @@ |
||||
export * from './FeedbackButton'; |
||||
export { default as FeedbackButton } from './FeedbackButton'; |
||||
export { default as FeedbackDialog } from './FeedbackDialog'; |
||||
|
@ -1 +1,5 @@ |
||||
export * from './actions'; |
||||
export * from './actionTypes'; |
||||
export * from './components'; |
||||
|
||||
import './reducer'; |
||||
|
@ -0,0 +1,45 @@ |
||||
import { |
||||
ReducerRegistry |
||||
} from '../base/redux'; |
||||
|
||||
import { |
||||
CANCEL_FEEDBACK, |
||||
SUBMIT_FEEDBACK |
||||
} from './actionTypes'; |
||||
|
||||
const DEFAULT_STATE = { |
||||
message: '', |
||||
|
||||
// The sentinel value -1 is used to denote no rating has been set and to
|
||||
// preserve pre-redux behavior.
|
||||
score: -1, |
||||
submitted: false |
||||
}; |
||||
|
||||
/** |
||||
* Reduces the Redux actions of the feature features/feedback. |
||||
*/ |
||||
ReducerRegistry.register( |
||||
'features/feedback', |
||||
(state = DEFAULT_STATE, action) => { |
||||
switch (action.type) { |
||||
case CANCEL_FEEDBACK: { |
||||
return { |
||||
...state, |
||||
message: action.message, |
||||
score: action.score |
||||
}; |
||||
} |
||||
|
||||
case SUBMIT_FEEDBACK: { |
||||
return { |
||||
...state, |
||||
message: '', |
||||
score: -1, |
||||
submitted: true |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return state; |
||||
}); |
Loading…
Reference in new issue