Merge "Selenium: replace UserLoginPage with BlankPage where possible"
[lhc/web/wiklou.git] / resources / src / mediawiki.feedback / 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 ( function () {
10 /**
11 * This is a way of getting simple feedback from users. It's useful
12 * for testing new features -- users can give you feedback without
13 * the difficulty of opening a whole new talk page. For this reason,
14 * it also tends to collect a wider range of both positive and negative
15 * comments. However you do need to tend to the feedback page. It will
16 * get long relatively quickly, and you often get multiple messages
17 * reporting the same issue.
18 *
19 * It takes the form of thing on your page which, when clicked, opens a small
20 * dialog box. Submitting that dialog box appends its contents to a
21 * wiki page that you specify, as a new section.
22 *
23 * This feature works with any content model that defines a
24 * `mw.messagePoster.MessagePoster`.
25 *
26 * Minimal usage example:
27 *
28 * var feedback = new mw.Feedback();
29 * $( '#myButton' ).click( function () { feedback.launch(); } );
30 *
31 * You can also launch the feedback form with a prefilled subject and body.
32 * See the docs for the #launch() method.
33 *
34 * @class
35 * @constructor
36 * @param {Object} [config] Configuration object
37 * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
38 * feedback.
39 * @cfg {string} [apiUrl] api.php URL if the feedback page is on another wiki
40 * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
41 * title of the dialog box
42 * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/edit/form/1/"] URL where
43 * bugs can be posted
44 * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL
45 * where bugs can be listed
46 * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form.
47 * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory.
48 * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox.
49 * defaults to the message 'feedback-terms'.
50 */
51 mw.Feedback = function MwFeedback( config ) {
52 config = config || {};
53
54 this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
55
56 // Feedback page title
57 this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
58
59 this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle, config.apiUrl );
60 this.foreignApi = config.apiUrl ? new mw.ForeignApi( config.apiUrl ) : null;
61
62 // Links
63 this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/edit/form/1/';
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 successfully, open a MessageDialog to thank the user.
88 *
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 * @param {string} feedbackPageName
94 * @param {string} feedbackPageUrl
95 */
96 mw.Feedback.prototype.onDialogSubmit = function ( status, feedbackPageName, feedbackPageUrl ) {
97 var dialogConfig;
98
99 if ( status !== 'submitted' ) {
100 return;
101 }
102
103 dialogConfig = {
104 title: mw.msg( 'feedback-thanks-title' ),
105 message: $( '<span>' ).msg(
106 'feedback-thanks',
107 feedbackPageName,
108 $( '<a>' ).attr( {
109 target: '_blank',
110 href: feedbackPageUrl
111 } )
112 ),
113 actions: [
114 {
115 action: 'accept',
116 label: mw.msg( 'feedback-close' ),
117 flags: 'primary'
118 }
119 ]
120 };
121
122 // Show the message dialog
123 this.constructor.static.windowManager.openWindow(
124 this.thankYouDialog,
125 dialogConfig
126 );
127 };
128
129 /**
130 * Modify the display form, and then open it, focusing interface on the subject.
131 *
132 * @param {Object} [contents] Prefilled contents for the feedback form.
133 * @param {string} [contents.subject] The subject of the feedback, as plaintext
134 * @param {string} [contents.message] The content of the feedback, as wikitext
135 */
136 mw.Feedback.prototype.launch = function ( contents ) {
137 // Dialog
138 if ( !this.constructor.static.dialog ) {
139 this.constructor.static.dialog = new mw.Feedback.Dialog();
140 this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
141 }
142 if ( !this.constructor.static.windowManager ) {
143 this.constructor.static.windowManager = new OO.ui.WindowManager();
144 this.constructor.static.windowManager.addWindows( [
145 this.constructor.static.dialog,
146 this.thankYouDialog
147 ] );
148 $( 'body' )
149 .append( this.constructor.static.windowManager.$element );
150 }
151 // Open the dialog
152 this.constructor.static.windowManager.openWindow(
153 this.constructor.static.dialog,
154 {
155 title: mw.msg( this.dialogTitleMessageKey ),
156 foreignApi: this.foreignApi,
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', 'progressive' ]
204 },
205 {
206 action: 'external',
207 label: mw.msg( 'feedback-external-bug-report-button' ),
208 flags: 'progressive'
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 // Feedback form
234 this.feedbackMessageLabel = new OO.ui.LabelWidget( {
235 classes: [ 'mw-feedbackDialog-welcome-message' ]
236 } );
237 this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
238 indicator: 'required'
239 } );
240 this.feedbackMessageInput = new OO.ui.MultilineTextInputWidget( {
241 autosize: true
242 } );
243 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
244 label: mw.msg( 'feedback-subject' )
245 } );
246 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
247 label: mw.msg( 'feedback-message' )
248 } );
249 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
250 items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
251 classes: [ 'mw-feedbackDialog-feedback-form' ]
252 } );
253
254 // Useragent terms of use
255 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
256 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
257 classes: [ 'mw-feedbackDialog-feedback-terms' ],
258 align: 'inline'
259 } );
260
261 termsOfUseLabel = new OO.ui.LabelWidget( {
262 classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
263 label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
264 } );
265
266 this.feedbackPanel.$element.append(
267 this.feedbackMessageLabel.$element,
268 feedbackFieldsetLayout.$element,
269 this.useragentFieldLayout.$element,
270 termsOfUseLabel.$element
271 );
272
273 // Events
274 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
275 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
276 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
277 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
278
279 this.$body.append( this.feedbackPanel.$element );
280 };
281
282 /**
283 * Validate the feedback form
284 */
285 mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
286 var isValid = (
287 (
288 !this.useragentMandatory ||
289 this.useragentCheckbox.isSelected()
290 ) &&
291 this.feedbackSubjectInput.getValue()
292 );
293
294 this.actions.setAbilities( { submit: isValid } );
295 };
296
297 /**
298 * @inheritdoc
299 */
300 mw.Feedback.Dialog.prototype.getBodyHeight = function () {
301 return this.feedbackPanel.$element.outerHeight( true );
302 };
303
304 /**
305 * @inheritdoc
306 */
307 mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
308 return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
309 .next( function () {
310 // Get the URL of the target page, we want to use that in links in the intro
311 // and in the success dialog
312 var dialog = this;
313 if ( data.foreignApi ) {
314 return data.foreignApi.get( {
315 action: 'query',
316 prop: 'info',
317 inprop: 'url',
318 formatversion: 2,
319 titles: data.settings.title.getPrefixedText()
320 } ).then( function ( data ) {
321 dialog.feedbackPageUrl = OO.getProp( data, 'query', 'pages', 0, 'canonicalurl' );
322 } );
323 } else {
324 this.feedbackPageUrl = data.settings.title.getUrl();
325 }
326 }, this )
327 .next( function () {
328 var $link,
329 settings = data.settings;
330 data.contents = data.contents || {};
331
332 // Prefill subject/message
333 this.feedbackSubjectInput.setValue( data.contents.subject );
334 this.feedbackMessageInput.setValue( data.contents.message );
335
336 this.status = '';
337 this.messagePosterPromise = settings.messagePosterPromise;
338 this.setBugReportLink( settings.bugsTaskSubmissionLink );
339 this.feedbackPageTitle = settings.title;
340 this.feedbackPageName = settings.title.getMainText();
341
342 // Useragent checkbox
343 if ( settings.useragentCheckbox.show ) {
344 this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
345 }
346
347 this.useragentMandatory = settings.useragentCheckbox.mandatory;
348 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
349
350 $link = $( '<a>' )
351 .attr( 'href', this.feedbackPageUrl )
352 .attr( 'target', '_blank' )
353 .text( this.feedbackPageName );
354 this.feedbackMessageLabel.setLabel(
355 mw.message( 'feedback-dialog-intro', $link ).parseDom()
356 );
357
358 this.validateFeedbackForm();
359 }, this );
360 };
361
362 /**
363 * @inheritdoc
364 */
365 mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
366 return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
367 .next( function () {
368 this.feedbackSubjectInput.focus();
369 }, this );
370 };
371
372 /**
373 * @inheritdoc
374 */
375 mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
376 if ( action === 'cancel' ) {
377 return new OO.ui.Process( function () {
378 this.close( { action: action } );
379 }, this );
380 } else if ( action === 'external' ) {
381 return new OO.ui.Process( function () {
382 // Open in a new window
383 window.open( this.getBugReportLink(), '_blank' );
384 // Close the dialog
385 this.close();
386 }, this );
387 } else if ( action === 'submit' ) {
388 return new OO.ui.Process( function () {
389 var fb = this,
390 userAgentMessage = ':' +
391 '<small>' +
392 mw.msg( 'feedback-useragent' ) +
393 ' ' +
394 mw.html.escape( navigator.userAgent ) +
395 '</small>\n\n',
396 subject = this.feedbackSubjectInput.getValue(),
397 message = this.feedbackMessageInput.getValue();
398
399 // Add user agent if checkbox is selected
400 if ( this.useragentCheckbox.isSelected() ) {
401 message = userAgentMessage + message;
402 }
403
404 // Post the message
405 return this.messagePosterPromise.then( function ( poster ) {
406 return fb.postMessage( poster, subject, message );
407 }, function () {
408 fb.status = 'error4';
409 mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
410 } ).then( function () {
411 fb.close();
412 }, function () {
413 return fb.getErrorMessage();
414 } );
415 }, this );
416 }
417 // Fallback to parent handler
418 return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
419 };
420
421 /**
422 * Returns an error message for the current status.
423 *
424 * @private
425 *
426 * @return {OO.ui.Error}
427 */
428 mw.Feedback.Dialog.prototype.getErrorMessage = function () {
429 // Messages: feedback-error1, feedback-error2, feedback-error3, feedback-error4
430 return new OO.ui.Error( mw.msg( 'feedback-' + this.status ) );
431 };
432
433 /**
434 * Posts the message
435 *
436 * @private
437 *
438 * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
439 * @param {string} subject Subject of message
440 * @param {string} message Body of message
441 * @return {jQuery.Promise} Promise representing success of message posting action
442 */
443 mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
444 var fb = this;
445
446 return poster.post(
447 subject,
448 message
449 ).then( function () {
450 fb.status = 'submitted';
451 }, function ( mainCode, secondaryCode, details ) {
452 if ( mainCode === 'api-fail' ) {
453 if ( secondaryCode === 'http' ) {
454 fb.status = 'error3';
455 // ajax request failed
456 mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
457 } else {
458 fb.status = 'error2';
459 mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
460 }
461 } else {
462 fb.status = 'error1';
463 }
464 } );
465 };
466
467 /**
468 * @inheritdoc
469 */
470 mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
471 return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
472 .first( function () {
473 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
474 // Cleanup
475 this.status = '';
476 this.feedbackPageTitle = null;
477 this.feedbackSubjectInput.setValue( '' );
478 this.feedbackMessageInput.setValue( '' );
479 this.useragentCheckbox.setSelected( false );
480 }, this );
481 };
482
483 /**
484 * Set the bug report link
485 *
486 * @param {string} link Link to the external bug report form
487 */
488 mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
489 this.bugReportLink = link;
490 };
491
492 /**
493 * Get the bug report link
494 *
495 * @return {string} Link to the external bug report form
496 */
497 mw.Feedback.Dialog.prototype.getBugReportLink = function () {
498 return this.bugReportLink;
499 };
500
501 }() );