4 * @author Ryan Kaldari, 2010
5 * @author Neil Kandalgaonkar, 2010-11
6 * @author Moriel Schottlender, 2015
11 ( function ( mw, $ ) {
13 * This is a way of getting simple feedback from users. It's useful
14 * for testing new features -- users can give you feedback without
15 * the difficulty of opening a whole new talk page. For this reason,
16 * it also tends to collect a wider range of both positive and negative
17 * comments. However you do need to tend to the feedback page. It will
18 * get long relatively quickly, and you often get multiple messages
19 * reporting the same issue.
21 * It takes the form of thing on your page which, when clicked, opens a small
22 * dialog box. Submitting that dialog box appends its contents to a
23 * wiki page that you specify, as a new section.
25 * This feature works with any content model that defines a
26 * `mw.messagePoster.MessagePoster`.
28 * Minimal usage example:
30 * var feedback = new mw.Feedback();
31 * $( '#myButton' ).click( function () { feedback.launch(); } );
33 * You can also launch the feedback form with a prefilled subject and body.
34 * See the docs for the #launch() method.
38 * @param {Object} [config] Configuration object
39 * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
41 * @cfg {string} [apiUrl] api.php URL if the feedback page is on another wiki
42 * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
43 * title of the dialog box
44 * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/create/"] URL where
46 * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL
47 * where bugs can be listed
48 * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form.
49 * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory.
50 * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox.
51 * defaults to the message 'feedback-terms'.
53 mw.Feedback = function MwFeedback( config ) {
54 config = config || {};
56 this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
58 // Feedback page title
59 this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
61 this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle, config.apiUrl );
64 this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/';
65 this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
68 this.useragentCheckboxShow = !!config.showUseragentCheckbox;
69 this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory;
70 this.useragentCheckboxMessage = config.useragentCheckboxMessage ||
71 $( '<p>' ).append( mw.msg( 'feedback-terms' ) );
74 this.thankYouDialog = new OO.ui.MessageDialog();
78 OO.initClass( mw.Feedback );
80 /* Static Properties */
81 mw.Feedback.static.windowManager = null;
82 mw.Feedback.static.dialog = null;
87 * Respond to dialog submit event. If the information was
88 * submitted, either successfully or with an error, open
89 * a MessageDialog to thank the user.
91 * @param {string} [status] A status of the end of operation
92 * of the main feedback dialog. Empty if the dialog was
93 * dismissed with no action or the user followed the button
94 * to the external task reporting site.
96 mw.Feedback.prototype.onDialogSubmit = function ( status ) {
97 var dialogConfig = {};
101 title: mw.msg( 'feedback-thanks-title' ),
102 message: $( '<span>' ).msg(
104 this.feedbackPageTitle.getNameText(),
107 href: this.feedbackPageTitle.getUrl()
113 label: mw.msg( 'feedback-close' ),
124 title: mw.msg( 'feedback-error-title' ),
125 message: mw.msg( 'feedback-' + status ),
129 label: mw.msg( 'feedback-close' ),
137 // Show the message dialog
138 if ( !$.isEmptyObject( dialogConfig ) ) {
139 this.constructor.static.windowManager.openWindow(
147 * Modify the display form, and then open it, focusing interface on the subject.
149 * @param {Object} [contents] Prefilled contents for the feedback form.
150 * @param {string} [contents.subject] The subject of the feedback, as plaintext
151 * @param {string} [contents.message] The content of the feedback, as wikitext
153 mw.Feedback.prototype.launch = function ( contents ) {
155 if ( !this.constructor.static.dialog ) {
156 this.constructor.static.dialog = new mw.Feedback.Dialog();
157 this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
159 if ( !this.constructor.static.windowManager ) {
160 this.constructor.static.windowManager = new OO.ui.WindowManager();
161 this.constructor.static.windowManager.addWindows( [
162 this.constructor.static.dialog,
166 .append( this.constructor.static.windowManager.$element );
169 this.constructor.static.windowManager.openWindow(
170 this.constructor.static.dialog,
172 title: mw.msg( this.dialogTitleMessageKey ),
174 messagePosterPromise: this.messagePosterPromise,
175 title: this.feedbackPageTitle,
176 dialogTitleMessageKey: this.dialogTitleMessageKey,
177 bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
178 bugsTaskListLink: this.bugsTaskListLink,
180 show: this.useragentCheckboxShow,
181 mandatory: this.useragentCheckboxMandatory,
182 message: this.useragentCheckboxMessage
194 * @extends OO.ui.ProcessDialog
197 * @param {Object} config Configuration object
199 mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
200 // Parent constructor
201 mw.Feedback.Dialog.parent.call( this, config );
204 this.feedbackPageTitle = null;
206 this.$element.addClass( 'mwFeedback-Dialog' );
209 OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
211 /* Static properties */
212 mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
213 mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
214 mw.Feedback.Dialog.static.size = 'medium';
215 mw.Feedback.Dialog.static.actions = [
218 label: mw.msg( 'feedback-submit' ),
219 flags: [ 'primary', 'constructive' ]
223 label: mw.msg( 'feedback-external-bug-report-button' ),
224 flags: 'constructive'
228 label: mw.msg( 'feedback-cancel' ),
236 mw.Feedback.Dialog.prototype.initialize = function () {
237 var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
238 feedbackFieldsetLayout, termsOfUseLabel;
241 mw.Feedback.Dialog.parent.prototype.initialize.call( this );
243 this.feedbackPanel = new OO.ui.PanelLayout( {
249 this.$spinner = $( '<div>' )
250 .addClass( 'feedback-spinner' );
253 this.feedbackMessageLabel = new OO.ui.LabelWidget( {
254 classes: [ 'mw-feedbackDialog-welcome-message' ]
256 this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
257 indicator: 'required',
260 this.feedbackMessageInput = new OO.ui.TextInputWidget( {
264 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
265 label: mw.msg( 'feedback-subject' )
267 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
268 label: mw.msg( 'feedback-message' )
270 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
271 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
272 classes: [ 'mw-feedbackDialog-feedback-form' ]
275 // Useragent terms of use
276 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
277 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
278 classes: [ 'mw-feedbackDialog-feedback-terms' ],
282 termsOfUseLabel = new OO.ui.LabelWidget( {
283 classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
284 label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
287 this.feedbackPanel.$element.append(
288 this.feedbackMessageLabel.$element,
289 feedbackFieldsetLayout.$element,
290 this.useragentFieldLayout.$element,
291 termsOfUseLabel.$element
295 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
296 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
297 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
298 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
300 this.$body.append( this.feedbackPanel.$element );
304 * Validate the feedback form
306 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
309 !this.useragentMandatory ||
310 this.useragentCheckbox.isSelected()
312 this.feedbackSubjectInput.getValue()
315 this.actions.setAbilities( { submit: isValid } );
321 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
322 return this.feedbackPanel.$element.outerHeight( true );
328 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
329 return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
331 var plainMsg, parsedMsg,
332 settings = data.settings;
333 data.contents = data.contents || {};
335 // Prefill subject/message
336 this.feedbackSubjectInput.setValue( data.contents.subject );
337 this.feedbackMessageInput.setValue( data.contents.message );
340 this.messagePosterPromise = settings.messagePosterPromise;
341 this.setBugReportLink( settings.bugsTaskSubmissionLink );
342 this.feedbackPageTitle = settings.title;
343 this.feedbackPageName = settings.title.getNameText();
344 this.feedbackPageUrl = settings.title.getUrl();
346 // Useragent checkbox
347 if ( settings.useragentCheckbox.show ) {
348 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
351 this.useragentMandatory = settings.useragentCheckbox.mandatory;
352 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
354 // HACK: Setting a link in the messages doesn't work. There is already a report
355 // about this, and the bug report offers a somewhat hacky work around that
356 // includes setting a separate message to be parsed.
357 // We want to make sure the user can configure both the title of the page and
358 // a separate url, so this must be allowed to parse correctly.
359 // See https://phabricator.wikimedia.org/T49395#490610
361 'feedback-dialog-temporary-message':
362 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
364 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
365 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
366 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
367 this.feedbackMessageLabel.setLabel(
370 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
373 this.validateFeedbackForm();
380 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
381 return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
383 this.feedbackSubjectInput.focus();
390 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
391 if ( action === 'cancel' ) {
392 return new OO.ui.Process( function () {
393 this.close( { action: action } );
395 } else if ( action === 'external' ) {
396 return new OO.ui.Process( function () {
397 // Open in a new window
398 window.open( this.getBugReportLink(), '_blank' );
402 } else if ( action === 'submit' ) {
403 return new OO.ui.Process( function () {
405 userAgentMessage = ':' +
407 mw.msg( 'feedback-useragent' ) +
409 mw.html.escape( navigator.userAgent ) +
411 subject = this.feedbackSubjectInput.getValue(),
412 message = this.feedbackMessageInput.getValue();
414 // Add user agent if checkbox is selected
415 if ( this.useragentCheckbox.isSelected() ) {
416 message = userAgentMessage + message;
420 return this.messagePosterPromise.then( function ( poster ) {
421 return fb.postMessage( poster, subject, message );
423 fb.status = 'error4';
424 mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
425 } ).always( function () {
430 // Fallback to parent handler
431 return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
439 * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
440 * @param {string} subject Subject of message
441 * @param {string} message Body of message
442 * @return {jQuery.Promise} Promise representing success of message posting action
444 mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
450 ).then( function () {
451 fb.status = 'submitted';
452 }, function ( mainCode, secondaryCode, details ) {
453 if ( mainCode === 'api-fail' ) {
454 if ( secondaryCode === 'http' ) {
455 fb.status = 'error3';
456 // ajax request failed
457 mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
459 fb.status = 'error2';
460 mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
463 fb.status = 'error1';
471 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
472 return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
473 .first( function () {
474 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
477 this.feedbackPageTitle = null;
478 this.feedbackSubjectInput.setValue( '' );
479 this.feedbackMessageInput.setValue( '' );
480 this.useragentCheckbox.setSelected( false );
485 * Set the bug report link
487 * @param {string} link Link to the external bug report form
489 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
490 this.bugReportLink = link;
494 * Get the bug report link
496 * @return {string} Link to the external bug report form
498 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
499 return this.bugReportLink;
502 }( mediaWiki, jQuery ) );