mw.feedback: Use standard OOjs UI dialog error handling
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.feedback.js
1 /*!
2 * mediawiki.feedback
3 *
4 * @author Ryan Kaldari, 2010
5 * @author Neil Kandalgaonkar, 2010-11
6 * @author Moriel Schottlender, 2015
7 * @since 1.19
8 */
9 /*jshint esversion:5 */
10 /*global OO*/
11 ( function ( mw, $ ) {
12 /**
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.
20 *
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.
24 *
25 * This feature works with any content model that defines a
26 * `mw.messagePoster.MessagePoster`.
27 *
28 * Minimal usage example:
29 *
30 * var feedback = new mw.Feedback();
31 * $( '#myButton' ).click( function () { feedback.launch(); } );
32 *
33 * You can also launch the feedback form with a prefilled subject and body.
34 * See the docs for the #launch() method.
35 *
36 * @class
37 * @constructor
38 * @param {Object} [config] Configuration object
39 * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
40 * feedback.
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/edit/form/1/"] URL where
45 * bugs can be posted
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'.
52 */
53 mw.Feedback = function MwFeedback( config ) {
54 config = config || {};
55
56 this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
57
58 // Feedback page title
59 this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
60
61 this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle, config.apiUrl );
62
63 // Links
64 this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/edit/form/1/';
65 this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
66
67 // Terms of use
68 this.useragentCheckboxShow = !!config.showUseragentCheckbox;
69 this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory;
70 this.useragentCheckboxMessage = config.useragentCheckboxMessage ||
71 $( '<p>' ).append( mw.msg( 'feedback-terms' ) );
72
73 // Message dialog
74 this.thankYouDialog = new OO.ui.MessageDialog();
75 };
76
77 /* Initialize */
78 OO.initClass( mw.Feedback );
79
80 /* Static Properties */
81 mw.Feedback.static.windowManager = null;
82 mw.Feedback.static.dialog = null;
83
84 /* Methods */
85
86 /**
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.
90 *
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.
95 */
96 mw.Feedback.prototype.onDialogSubmit = function ( status ) {
97 var dialogConfig = {};
98 switch ( status ) {
99 case 'submitted':
100 dialogConfig = {
101 title: mw.msg( 'feedback-thanks-title' ),
102 message: $( '<span>' ).msg(
103 'feedback-thanks',
104 this.feedbackPageTitle.getNameText(),
105 $( '<a>' ).attr( {
106 target: '_blank',
107 href: this.feedbackPageTitle.getUrl()
108 } )
109 ),
110 actions: [
111 {
112 action: 'accept',
113 label: mw.msg( 'feedback-close' ),
114 flags: 'primary'
115 }
116 ]
117 };
118 break;
119 }
120
121 // Show the message dialog
122 if ( !$.isEmptyObject( dialogConfig ) ) {
123 this.constructor.static.windowManager.openWindow(
124 this.thankYouDialog,
125 dialogConfig
126 );
127 }
128 };
129
130 /**
131 * Modify the display form, and then open it, focusing interface on the subject.
132 *
133 * @param {Object} [contents] Prefilled contents for the feedback form.
134 * @param {string} [contents.subject] The subject of the feedback, as plaintext
135 * @param {string} [contents.message] The content of the feedback, as wikitext
136 */
137 mw.Feedback.prototype.launch = function ( contents ) {
138 // Dialog
139 if ( !this.constructor.static.dialog ) {
140 this.constructor.static.dialog = new mw.Feedback.Dialog();
141 this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
142 }
143 if ( !this.constructor.static.windowManager ) {
144 this.constructor.static.windowManager = new OO.ui.WindowManager();
145 this.constructor.static.windowManager.addWindows( [
146 this.constructor.static.dialog,
147 this.thankYouDialog
148 ] );
149 $( 'body' )
150 .append( this.constructor.static.windowManager.$element );
151 }
152 // Open the dialog
153 this.constructor.static.windowManager.openWindow(
154 this.constructor.static.dialog,
155 {
156 title: mw.msg( this.dialogTitleMessageKey ),
157 settings: {
158 messagePosterPromise: this.messagePosterPromise,
159 title: this.feedbackPageTitle,
160 dialogTitleMessageKey: this.dialogTitleMessageKey,
161 bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
162 bugsTaskListLink: this.bugsTaskListLink,
163 useragentCheckbox: {
164 show: this.useragentCheckboxShow,
165 mandatory: this.useragentCheckboxMandatory,
166 message: this.useragentCheckboxMessage
167 }
168 },
169 contents: contents
170 }
171 );
172 };
173
174 /**
175 * mw.Feedback Dialog
176 *
177 * @class
178 * @extends OO.ui.ProcessDialog
179 *
180 * @constructor
181 * @param {Object} config Configuration object
182 */
183 mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
184 // Parent constructor
185 mw.Feedback.Dialog.parent.call( this, config );
186
187 this.status = '';
188 this.feedbackPageTitle = null;
189 // Initialize
190 this.$element.addClass( 'mwFeedback-Dialog' );
191 };
192
193 OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
194
195 /* Static properties */
196 mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
197 mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
198 mw.Feedback.Dialog.static.size = 'medium';
199 mw.Feedback.Dialog.static.actions = [
200 {
201 action: 'submit',
202 label: mw.msg( 'feedback-submit' ),
203 flags: [ 'primary', 'constructive' ]
204 },
205 {
206 action: 'external',
207 label: mw.msg( 'feedback-external-bug-report-button' ),
208 flags: 'constructive'
209 },
210 {
211 action: 'cancel',
212 label: mw.msg( 'feedback-cancel' ),
213 flags: 'safe'
214 }
215 ];
216
217 /**
218 * @inheritdoc
219 */
220 mw.Feedback.Dialog.prototype.initialize = function () {
221 var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
222 feedbackFieldsetLayout, termsOfUseLabel;
223
224 // Parent method
225 mw.Feedback.Dialog.parent.prototype.initialize.call( this );
226
227 this.feedbackPanel = new OO.ui.PanelLayout( {
228 scrollable: false,
229 expanded: false,
230 padded: true
231 } );
232
233 this.$spinner = $( '<div>' )
234 .addClass( 'feedback-spinner' );
235
236 // Feedback form
237 this.feedbackMessageLabel = new OO.ui.LabelWidget( {
238 classes: [ 'mw-feedbackDialog-welcome-message' ]
239 } );
240 this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
241 indicator: 'required',
242 multiline: false
243 } );
244 this.feedbackMessageInput = new OO.ui.TextInputWidget( {
245 autosize: true,
246 multiline: true
247 } );
248 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
249 label: mw.msg( 'feedback-subject' )
250 } );
251 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
252 label: mw.msg( 'feedback-message' )
253 } );
254 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
255 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
256 classes: [ 'mw-feedbackDialog-feedback-form' ]
257 } );
258
259 // Useragent terms of use
260 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
261 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
262 classes: [ 'mw-feedbackDialog-feedback-terms' ],
263 align: 'inline'
264 } );
265
266 termsOfUseLabel = new OO.ui.LabelWidget( {
267 classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
268 label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
269 } );
270
271 this.feedbackPanel.$element.append(
272 this.feedbackMessageLabel.$element,
273 feedbackFieldsetLayout.$element,
274 this.useragentFieldLayout.$element,
275 termsOfUseLabel.$element
276 );
277
278 // Events
279 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
280 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
281 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
282 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
283
284 this.$body.append( this.feedbackPanel.$element );
285 };
286
287 /**
288 * Validate the feedback form
289 */
290 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
291 var isValid = (
292 (
293 !this.useragentMandatory ||
294 this.useragentCheckbox.isSelected()
295 ) &&
296 this.feedbackSubjectInput.getValue()
297 );
298
299 this.actions.setAbilities( { submit: isValid } );
300 };
301
302 /**
303 * @inheritdoc
304 */
305 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
306 return this.feedbackPanel.$element.outerHeight( true );
307 };
308
309 /**
310 * @inheritdoc
311 */
312 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
313 return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
314 .next( function () {
315 var plainMsg, parsedMsg,
316 settings = data.settings;
317 data.contents = data.contents || {};
318
319 // Prefill subject/message
320 this.feedbackSubjectInput.setValue( data.contents.subject );
321 this.feedbackMessageInput.setValue( data.contents.message );
322
323 this.status = '';
324 this.messagePosterPromise = settings.messagePosterPromise;
325 this.setBugReportLink( settings.bugsTaskSubmissionLink );
326 this.feedbackPageTitle = settings.title;
327 this.feedbackPageName = settings.title.getNameText();
328 this.feedbackPageUrl = settings.title.getUrl();
329
330 // Useragent checkbox
331 if ( settings.useragentCheckbox.show ) {
332 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
333 }
334
335 this.useragentMandatory = settings.useragentCheckbox.mandatory;
336 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
337
338 // HACK: Setting a link in the messages doesn't work. There is already a report
339 // about this, and the bug report offers a somewhat hacky work around that
340 // includes setting a separate message to be parsed.
341 // We want to make sure the user can configure both the title of the page and
342 // a separate url, so this must be allowed to parse correctly.
343 // See https://phabricator.wikimedia.org/T49395#490610
344 mw.messages.set( {
345 'feedback-dialog-temporary-message':
346 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
347 } );
348 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
349 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
350 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
351 this.feedbackMessageLabel.setLabel(
352 // Double-parse
353 $( '<span>' )
354 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
355 );
356
357 this.validateFeedbackForm();
358 }, this );
359 };
360
361 /**
362 * @inheritdoc
363 */
364 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
365 return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
366 .next( function () {
367 this.feedbackSubjectInput.focus();
368 }, this );
369 };
370
371 /**
372 * @inheritdoc
373 */
374 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
375 if ( action === 'cancel' ) {
376 return new OO.ui.Process( function () {
377 this.close( { action: action } );
378 }, this );
379 } else if ( action === 'external' ) {
380 return new OO.ui.Process( function () {
381 // Open in a new window
382 window.open( this.getBugReportLink(), '_blank' );
383 // Close the dialog
384 this.close();
385 }, this );
386 } else if ( action === 'submit' ) {
387 return new OO.ui.Process( function () {
388 var fb = this,
389 userAgentMessage = ':' +
390 '<small>' +
391 mw.msg( 'feedback-useragent' ) +
392 ' ' +
393 mw.html.escape( navigator.userAgent ) +
394 '</small>\n\n',
395 subject = this.feedbackSubjectInput.getValue(),
396 message = this.feedbackMessageInput.getValue();
397
398 // Add user agent if checkbox is selected
399 if ( this.useragentCheckbox.isSelected() ) {
400 message = userAgentMessage + message;
401 }
402
403 // Post the message
404 return this.messagePosterPromise.then( function ( poster ) {
405 return fb.postMessage( poster, subject, message );
406 }, function () {
407 fb.status = 'error4';
408 mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
409 } ).then( function () {
410 fb.close();
411 }, function () {
412 return fb.getErrorMessage();
413 } );
414 }, this );
415 }
416 // Fallback to parent handler
417 return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
418 };
419
420 /**
421 * Returns an error message for the current status.
422 *
423 * @private
424 *
425 * @return {OO.ui.Error}
426 */
427 mw.Feedback.Dialog.prototype.getErrorMessage = function () {
428 switch ( this.status ) {
429 case 'error1':
430 case 'error2':
431 case 'error3':
432 case 'error4':
433 // Messages: feedback-error1, feedback-error2, feedback-error3, feedback-error4
434 return new OO.ui.Error( mw.msg( 'feedback-' + this.status ) );
435 }
436 };
437
438 /**
439 * Posts the message
440 *
441 * @private
442 *
443 * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
444 * @param {string} subject Subject of message
445 * @param {string} message Body of message
446 * @return {jQuery.Promise} Promise representing success of message posting action
447 */
448 mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
449 var fb = this;
450
451 return poster.post(
452 subject,
453 message
454 ).then( function () {
455 fb.status = 'submitted';
456 }, function ( mainCode, secondaryCode, details ) {
457 if ( mainCode === 'api-fail' ) {
458 if ( secondaryCode === 'http' ) {
459 fb.status = 'error3';
460 // ajax request failed
461 mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
462 } else {
463 fb.status = 'error2';
464 mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
465 }
466 } else {
467 fb.status = 'error1';
468 }
469 } );
470 };
471
472 /**
473 * @inheritdoc
474 */
475 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
476 return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
477 .first( function () {
478 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
479 // Cleanup
480 this.status = '';
481 this.feedbackPageTitle = null;
482 this.feedbackSubjectInput.setValue( '' );
483 this.feedbackMessageInput.setValue( '' );
484 this.useragentCheckbox.setSelected( false );
485 }, this );
486 };
487
488 /**
489 * Set the bug report link
490 *
491 * @param {string} link Link to the external bug report form
492 */
493 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
494 this.bugReportLink = link;
495 };
496
497 /**
498 * Get the bug report link
499 *
500 * @return {string} Link to the external bug report form
501 */
502 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
503 return this.bugReportLink;
504 };
505
506 }( mediaWiki, jQuery ) );