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