r96309 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r96308‎ | r96309 | r96310 >
Date:22:28, 5 September 2011
Author:krinkle
Status:resolved (Comments)
Tags:
Comment:
[ResourceLoader2] Gadget manager (start of ajax editor)
* Add column to SpecialGadgetManager front-end for last modified timestamp.
* Use getTitleMessage. Logic is now centralized (yay r95970).
* Expose gadget internal name to JavaScript via data- attribute.
* Expose array of all rights to JavaScript via OutputPage's getJSVars hook.
* Only load ajax editor if user is allowed to edit NS_GADGET_DEFINITION.
* Introduce three resource modules:
** ext.gadgetmanager.api: Group of (partially stub) function to interact with the API. Getting gadget metadata, list of categories, save gadget, delete gadget etc.
** ext.gadgetmanager.ui: Binds event handlers to the table outputted by PHP and takes care of the form/dialog generation and the autocompletion of all input fields through various apis and arrays.
** jquery.createPropcloud: jQuery plugin written from scratch for the gadget manager. Turns an input field into a properties "cloud" with autocompletion and visual bubbles to remove items.


TODO:
* ext.gadgetmanager.api
** Finish stub functions for saving and deleting a gadget.
* ext.gadgetmanager.ui
** Nofication system
** Add "Create" tab to the special page, which just brings up a regular modify-form except that it will feature a title-input field.
** Add dynamic "(new category)" option in the category dropdown of the gadget editor.
* SpecialGadgetManager.php
** generateGadgetView


(Follows-up r95818)
Modified paths:
  • /branches/RL2/extensions/Gadgets/GadgetHooks.php (modified) (history)
  • /branches/RL2/extensions/Gadgets/Gadgets.i18n.php (modified) (history)
  • /branches/RL2/extensions/Gadgets/Gadgets.php (modified) (history)
  • /branches/RL2/extensions/Gadgets/SpecialGadgetManager.php (modified) (history)
  • /branches/RL2/extensions/Gadgets/backend/Gadget.php (modified) (history)
  • /branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.api.js (added) (history)
  • /branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.prejs.css (modified) (history)
  • /branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.ui.css (added) (history)
  • /branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.ui.js (added) (history)
  • /branches/RL2/extensions/Gadgets/modules/images (added) (history)
  • /branches/RL2/extensions/Gadgets/modules/images/close.png (added) (history)
  • /branches/RL2/extensions/Gadgets/modules/jquery.createPropCloud.js (added) (history)

Diff [purge]

Index: branches/RL2/extensions/Gadgets/Gadgets.i18n.php
@@ -1,37 +1,36 @@
22 <?php
33 /**
4 - * Internationalisation file for extension Gadgets.
 4+ * Internationalisation file for Gadgets extension.
55 *
66 * @file
77 * @ingroup Extensions
8 - * @author Daniel Kinzler, brightbyte.de
9 - * @copyright © 2007 Daniel Kinzler
10 - * @license GNU General Public Licence 2.0 or later
118 */
129
1310 $messages = array();
1411
1512 /** English
1613 * @author Daniel Kinzler, brightbyte.de
 14+ * @author Roan Kattouw
 15+ * @author Timo Tijhof
1716 */
1817 $messages['en'] = array(
1918 # For Special:Version
20 - 'gadgets-desc' => 'Lets users select custom [[Special:Gadgets|CSS and JavaScript gadgets]] in their [[Special:Preferences|preferences]]',
 19+ 'gadgets-desc' => 'Lets users select custom [[Special:Gadgets|CSS and JavaScript gadgets]] in their [[Special:Preferences#mw-prefsection-gadgets|preferences]].',
2120
2221 # For Special:Preferences
2322 'prefs-gadgets' => 'Gadgets',
24 - 'gadgets-prefstext' => 'Below is a list of special gadgets you can enable for your account.
 23+ 'gadgets-prefstext' => 'Below is a list of gadgets you can enable for your account.
2524 These gadgets are mostly based on JavaScript, so JavaScript has to be enabled in your browser for them to work.
2625 Note that these gadgets will have no effect on this preferences page.
2726
28 -Also note that these special gadgets are not part of the MediaWiki software, and are usually developed and maintained by users on your local wiki.
29 -Local administrators can edit the [[MediaWiki:Gadgets-definition|definitions]] and [[Special:Gadgets|descriptions]] of available gadgets.',
 27+Also note that these gadgets are not part of the MediaWiki software, and are usually developed and maintained by users of the wiki.
 28+Administrators manage to the [[Special:GadgetManager|gadget definitions]] and the [[Special:Gadgets|titles and descriptions]] of available gadgets.',
3029 'gadgets-preference-description' => '$1: $2',
3130
32 - # For Special:Gadgets
 31+ # For Special:Gadgets (overview for users and people with editinterface)
3332 'gadgets' => 'Gadgets',
3433 'gadgets-title' => 'Gadgets',
35 - 'gadgets-pagetext' => "Below is a list of special gadgets users can enable on their [[Special:Preferences|preferences page]], as defined by the [[MediaWiki:Gadgets-definition|definitions]].
 34+ 'gadgets-pagetext' => "Below is a list of special gadgets users can enable on their [[Special:Preferences#mw-prefsection-gadgets|preferences page]], as defined by the [[MediaWiki:Gadgets-definition|definitions]].
3635 This overview provides easy access to the system message pages that define each gadget's description and code.",
3736 'gadgets-uses' => 'Uses',
3837 'gadgets-required-rights' => 'Requires the {{PLURAL:$2|$1 right|following rights: $1}}.',
@@ -44,17 +43,23 @@
4544 <pre>$2</pre>
4645 You must have appropriate permissions on destination wiki (including the right to edit system messages) and import from file uploads must be enabled.',
4746 'gadgets-export-download' => 'Download',
48 -
49 - # For Special:GadgetManager
 47+
 48+ # For Special:GadgetManager (for gadget meta-data management)
5049 'gadgetmanager' => 'Gadget manager',
5150 'gadgetmanager-title' => 'Gadget manager',
52 - 'gadgetmanager-pagetext' => 'Below is an overview of all gadgets defined on this wiki. Users can opt in or opt out of these through their [[Special:Preferences|preferences page]].',
 51+ 'gadgetmanager-pagetext' => 'Below is an overview of all gadgets defined on this wiki. Users can opt in or opt out of these through their [[Special:Preferences#mw-prefsection-gadgets|preferences page]].',
5352 'gadgetmanager-nogadgets' => 'This wiki currently has no gadgets defined.',
5453 'gadgetmanager-uncategorized' => 'Uncategorized',
5554 'gadgetmanager-tablehead-title' => 'Gadget title',
5655 'gadgetmanager-tablehead-default' => 'Default',
5756 'gadgetmanager-tablehead-hidden' => 'Hidden',
5857 'gadgetmanager-tablehead-shared' => 'Shared',
 58+ 'gadgetmanager-tablehead-lastmod' => 'Last modified',
 59+ 'gadgetmanager-tablecell-lastmod' => '$1 by $2',
 60+ 'gadgetmanager-editor-title' => 'Editing $1:',
 61+ 'gadgetmanager-editor-removeprop-tooltip' => 'Remove this item',
 62+ 'gadgetmanager-editor-save' => 'Save gadget',
 63+ 'gadgetmanager-editor-cancel' => 'Cancel',
5964 'gadgetmanager-propsgroup-settings' => 'Gadget settings',
6065 'gadgetmanager-propsgroup-module' => 'Module properties',
6166 'gadgetmanager-prop-scripts' => 'Scripts',
@@ -62,7 +67,10 @@
6368 'gadgetmanager-prop-dependencies' => 'Dependencies',
6469 'gadgetmanager-prop-messages' => 'Messages',
6570 'gadgetmanager-prop-category' => 'Category',
66 - 'gadgetmanager-prop-rights' => 'Rights',
 71+ 'gadgetmanager-prop-rights' => 'Required user rights',
 72+ 'gadgetmanager-prop-default' => 'Enable by default',
 73+ 'gadgetmanager-prop-hidden' => 'Hide gadget',
 74+ 'gadgetmanager-prop-shared' => 'Share gadget',
6775 'gadgetmanager-prop-default-yes' => 'This gadget is loaded by default.',
6876 'gadgetmanager-prop-hidden-yes' => 'This is a hidden gadget.',
6977 'gadgetmanager-prop-shared-yes' => 'This gadget is shared.',
@@ -70,7 +78,7 @@
7179 'gadgetmanager-modifylink-tooltip' => 'Modify this gadget',
7280 'gadgetmanager-deletelink' => 'delete',
7381 'gadgetmanager-deletelink-tooltip' => 'Delete ths gadget',
74 -
 82+
7583 # Permissions
7684 'gadgets-cant-create' => 'You do not have the right to create new Gadgets.',
7785 'gadgets-cant-delete' => 'You do not have the right to delete Gadgets.',
@@ -92,6 +100,7 @@
93101 * @author SPQRobin
94102 * @author Siebrand
95103 * @author The Evil IP address
 104+ * @author Timo Tijhof
96105 */
97106 $messages['qqq'] = array(
98107 'gadgets-desc' => '{{desc}}',
@@ -112,6 +121,13 @@
113122 {{Identical|Export}}',
114123 'gadgets-export-download' => 'Use the verb for this message. Submit button.
115124 {{Identical|Download}}',
 125+ 'gadgetmanager-tablehead-lastmodified' => '{{Identical|Last modified}}',
 126+ 'gadgetmanager-tablecell-lastmod' => 'This message is used on Special:GadgetManager to indicate the last modified date, time and user for gadget definitions.
 127+* $1 is a time and date (duplicated in $3 and $4)
 128+* $2 is a link to a user page with a user name as link text, followed by a series of related links
 129+* $3 is the date
 130+* $4 is the time
 131+* $5 is the user name which can be used with GENDER'
116132 );
117133
118134 /** Afrikaans (Afrikaans)
Index: branches/RL2/extensions/Gadgets/Gadgets.php
@@ -99,6 +99,7 @@
100100 $wgHooks['ArticleUndelete'][] = 'GadgetHooks::gadgetDefinitionUndelete';
101101 $wgHooks['ArticleUndelete'][] = 'GadgetHooks::cssOrJsPageUndelete';
102102 $wgHooks['BeforePageDisplay'][] = 'GadgetHooks::beforePageDisplay';
 103+$wgHooks['MakeGlobalVariablesScript'][] = 'GadgetHooks::makeGlobalVariablesScript';
103104 $wgHooks['CanonicalNamespaces'][] = 'GadgetHooks::canonicalNamespaces';
104105 $wgHooks['GetPreferences'][] = 'GadgetHooks::getPreferences';
105106 $wgHooks['LoadExtensionSchemaUpdates'][] = 'GadgetHooks::loadExtensionSchemaUpdates';
@@ -148,4 +149,44 @@
149150 'styles' => 'ext.gadgets.gadgetmanager.prejs.css',
150151 'position' => 'top',
151152 ),
 153+ // Method to interact with API
 154+ 'ext.gadgets.gadgetmanager.api' => $gadResourceTemplate + array(
 155+ 'scripts' => 'ext.gadgets.gadgetmanager.api.js',
 156+ 'dependencies' => 'mediawiki.util',
 157+ ),
 158+ // jQuery plugin
 159+ 'jquery.createPropCloud' => $gadResourceTemplate + array(
 160+ 'scripts' => 'jquery.createPropCloud.js',
 161+ 'messages' => array(
 162+ 'gadgetmanager-editor-prop-remove',
 163+ ),
 164+ ),
 165+ // Event handling, UI components, initiates on document ready
 166+ 'ext.gadgets.gadgetmanager.ui' => $gadResourceTemplate + array(
 167+ 'scripts' => 'ext.gadgets.gadgetmanager.ui.js',
 168+ 'styles' => 'ext.gadgets.gadgetmanager.ui.css',
 169+ 'dependencies' => array(
 170+ 'ext.gadgets.gadgetmanager.api',
 171+ 'jquery.localize',
 172+ 'jquery.ui.autocomplete',
 173+ 'jquery.ui.dialog',
 174+ 'mediawiki.Title',
 175+ 'jquery.createPropCloud',
 176+ ),
 177+ 'messages' => array(
 178+ 'gadgetmanager-editor-title',
 179+ 'gadgetmanager-editor-removeprop-tooltip',
 180+ 'gadgetmanager-editor-save',
 181+ 'gadgetmanager-editor-cancel',
 182+ 'gadgetmanager-prop-scripts',
 183+ 'gadgetmanager-prop-styles',
 184+ 'gadgetmanager-prop-dependencies',
 185+ 'gadgetmanager-prop-messages',
 186+ 'gadgetmanager-prop-category',
 187+ 'gadgetmanager-prop-rights',
 188+ 'gadgetmanager-prop-default',
 189+ 'gadgetmanager-prop-hidden',
 190+ 'gadgetmanager-prop-shared',
 191+ ),
 192+ ),
152193 );
Index: branches/RL2/extensions/Gadgets/GadgetHooks.php
@@ -289,6 +289,23 @@
290290 }
291291
292292 /**
 293+ * MakeGlobalVariablesScript hook handler
 294+ * @param $vars Array: Key/value pars for mw.config.set on this page.
 295+ * @param $out OutputPage
 296+ */
 297+ public static function makeGlobalVariablesScript( &$vars, $out ) {
 298+ if ( $out->getTitle()->equals( SpecialPage::getTitleFor( 'GadgetManager' ) ) ) {
 299+ global $wgGadgetEnableSharing;
 300+
 301+ $vars['gadgetManagerConf'] = array(
 302+ 'enableSharing' => $wgGadgetEnableSharing,
 303+ 'allRights' => User::getAllRights(),
 304+ );
 305+ }
 306+ return true;
 307+ }
 308+
 309+ /**
293310 * UnitTestsList hook handler
294311 * @param $files Array: List of extension test files
295312 */
Index: branches/RL2/extensions/Gadgets/backend/Gadget.php
@@ -8,7 +8,7 @@
99 * // The rights required to be able to enable/load this gadget
1010 * "rights": ["delete", "block"],
1111 * // Whether this gadget is enabled by default
12 - * "default": true
 12+ * "default": true,
1313 * // Whether this gadget is hidden from preferences
1414 * "hidden": false,
1515 * // Whether this gadget is shared
Index: branches/RL2/extensions/Gadgets/modules/jquery.createPropCloud.js
@@ -0,0 +1,107 @@
 2+/**
 3+ * jQuery PropCloud plugin
 4+ * @author Timo Tijhof, 2011
 5+ */
 6+( function() {
 7+
 8+ function newPropHtml( label, o ) {
 9+ return $( '<span class="' + o.prefix + 'prop"></span>' )
 10+ .append(
 11+ $( '<span class="' + o.prefix + 'prop-label"></span>' ).text( label )
 12+ )
 13+ .append(
 14+ $( '<span class="' + o.prefix + 'prop-delete"></span> ')
 15+ .attr( 'title', o.removeTooltip )
 16+ .click( function() {
 17+ $(this).parent().remove();
 18+ o.onRemove( label );
 19+ }
 20+ )
 21+ );
 22+ }
 23+
 24+ /**
 25+ * Create prop cloud around an input field.
 26+ *
 27+ * @example This is the HTML structure being created:
 28+ *
 29+ * <div class="editor-propcloud">
 30+ * <div class="editor-propcontainer">
 31+ * <span editor="jquery-prop">
 32+ * <span class="editor-prop-label"> .. </span>
 33+ * <span class="editor-prop-delete" title="Remove this item"></span>
 34+ * </span>
 35+ * <span class="editor-prop">
 36+ * <span class="editor-prop-label"> .. </span>
 37+ * <span class="editor-prop-delete" title="Remove this item"></span>
 38+ * </span>
 39+ * </div>
 40+ * <input class="editor-propinput" />
 41+ * </div>
 42+ *
 43+ * @context {jQuery}
 44+ * @param o {Object} All optional
 45+ * - prefix {String} Class name prefix
 46+ * - props {Array} Array of properties to start with
 47+ * - autocompleteSource {Function|Array} Source of autocomplete suggestions (required)
 48+ * See also http://jqueryui.com/demos/autocomplete/#options (source)
 49+ * - onAdd {Function} Callback when an item is added
 50+ * - onRemove {Function} Callback when an item is deleted
 51+ * - removeTooltip {String} Tooltip for the remove-icon
 52+ *
 53+ * @return {jQuery} prop cloud (input field inside)
 54+ */
 55+ $.fn.createPropCloud = function( o ) {
 56+ // Some defaults
 57+ o = $.extend({
 58+ prefix: 'editor',
 59+ props: [],
 60+ autocompleteSource: [],
 61+ onAdd: function( prop ) {},
 62+ onRemove: function( prop ) {},
 63+ removeTooltip: 'Remove this item'
 64+ }, o );
 65+ o.prefix = o.prefix + '-';
 66+
 67+ var $el = this.eq(0),
 68+ $input = $el.addClass( o.prefix + 'propinput' ),
 69+ $cloud = $input.wrap( '<div class="' + o.prefix + 'propcloud"></div>' ).parent(),
 70+ $container = $( '<div class="' + o.prefix + 'propcontainer"></div>' );
 71+
 72+ // Append while container is still off the dom
 73+ // This is faster and prevents visible build up
 74+ for ( var i = 0, props = o.props, len = props.length; i < len; i++ ) {
 75+ $container.append( newPropHtml( '' + props[i], o ) );
 76+ }
 77+
 78+ $input.autocomplete( {
 79+ // The source is entirly up to you
 80+ source: o.autocompleteSource,
 81+
 82+ // A value is choosen
 83+ // (ie. by pressing return/tab, clicking on suggestion, etc.)
 84+ select: function( e, data ){
 85+ var val = data.item.value;
 86+
 87+ // Prevent duplicate values
 88+ if ( o.props.indexOf( val ) === -1 ) {
 89+ $container.append( newPropHtml( val, o ) );
 90+ o.onAdd( val );
 91+ }
 92+
 93+ // Clear input whether duplicate (and ignored),
 94+ // or unique (and added to the PropCloud by now)
 95+ $input.val( '' );
 96+
 97+ // Return false,
 98+ // otherwise jQuery UI calls .val( val ) again
 99+ return false;
 100+ }
 101+ });
 102+
 103+ $cloud.prepend( $container );
 104+
 105+ return $cloud;
 106+ };
 107+
 108+})();
\ No newline at end of file
Index: branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.api.js
@@ -0,0 +1,187 @@
 2+/**
 3+ * Implement the editing API for the gadget manager.
 4+ *
 5+ * @author Timo Tijhof
 6+ * @copyright © 2011 Timo Tijhof
 7+ * @license GNU General Public Licence 2.0 or later
 8+ */
 9+( function() {
 10+
 11+ var
 12+ /**
 13+ * @var {Object} Keyed by gadget id, contains the metadata as an object.
 14+ */
 15+ gadgetCache = {},
 16+ /**
 17+ * @var {Object|Null} If cache, object with category ids and the member counts */
 18+ gadgetCategoryCache = null;
 19+
 20+ /* Local functions */
 21+
 22+ /**
 23+ * For most returns from api.* functions, a clone is made when data from
 24+ * cached is used. This is to avoid situations where later modifications
 25+ * (ie. by the ajax editor) on the object affecting the cache (because
 26+ * the object would otherwise be passed by reference).
 27+ */
 28+ function objClone( obj ) {
 29+ /**
 30+ * A normal `$.extend( {}, obj );` is not suffecient,
 31+ * it has to be recursive, because the values of this
 32+ * object are also refererenes to objects.
 33+ * Consider:
 34+ * <code>
 35+ * var a = { words: [ 'foo', 'bar','baz' ] };
 36+ * var b = $.extend( {}, a );
 37+ * b.words.push( 'quux' );
 38+ * a.words[4]; // quux !
 39+ * </code>
 40+ */
 41+ return $.extend( true /* resursive */, {}, obj );
 42+ }
 43+ function arrClone( arr ) {
 44+ return arr.slice();
 45+ }
 46+
 47+ /* Public functions */
 48+
 49+ mw.gadgetManager = {
 50+
 51+ conf: mw.config.get( 'gadgetManagerConf' ),
 52+
 53+ api: {
 54+
 55+ /**
 56+ * Get gadget blob from the API (or from cache if available).
 57+ *
 58+ * @param id {String} Gadget id.
 59+ * @param callback {Function} To be called with an object as first argument,
 60+ * and status as second argument (success or error).
 61+ * @return {jqXHR|Null}: Null if served from cache, otherwise the jqXHR.
 62+ */
 63+ getGadgetMetadata: function( id, callback ) {
 64+ // Check cache
 65+ if ( id in gadgetCache && gadgetCache[id] !== null ) {
 66+ callback( objClone( gadgetCache[id] ), 'success' );
 67+ return null;
 68+ }
 69+ // Get from API if not cached
 70+ return $.ajax({
 71+ url: mw.util.wikiScript( 'api' ),
 72+ data: {
 73+ format: 'json',
 74+ action: 'query',
 75+ list: 'gadgets',
 76+ gaprop: 'name|metadata|desc',
 77+ ganames: id,
 78+ galanguage: mw.config.get( 'wgUserLanguage' )
 79+ },
 80+ type: 'GET',
 81+ dataType: 'json',
 82+ success: function( data ) {
 83+ if ( data && data.query && data.query.gadgets && data.query.gadgets[0] ) {
 84+ data = data.query.gadgets[0].metadata;
 85+ // Update cache
 86+ gadgetCache[id] = data;
 87+ callback( objClone( data ), 'success' );
 88+ } else {
 89+ // Invalidate cache
 90+ gadgetCache[id] = null;
 91+ callback( {}, 'error' );
 92+ }
 93+ },
 94+ error: function() {
 95+ // Invalidate cache
 96+ gadgetCache[id] = null;
 97+ callback( {}, 'error' );
 98+ }
 99+ });
 100+ },
 101+
 102+ /**
 103+ * @param callback {Function} To be called with an array as first argument,
 104+ * and status as second argument (success or error).
 105+ * @return {jqXHR|Null}: Null if served from cache, otherwise the jqXHR.
 106+ */
 107+ getGadgetCategories: function( callback ) {
 108+ // Check cache
 109+ if ( gadgetCategoryCache !== null ) {
 110+ callback( arrClone( gadgetCategoryCache ) );
 111+ return null;
 112+ }
 113+ // Get from API if not cached
 114+ return $.ajax({
 115+ url: mw.util.wikiScript( 'api' ),
 116+ data: {
 117+ format: 'json',
 118+ action: 'query',
 119+ list: 'gadgetcategories',
 120+ gcprop: 'name|title|members',
 121+ gclanguage: mw.config.get( 'wgUserLanguage' )
 122+ },
 123+ type: 'GET',
 124+ dataType: 'json',
 125+ success: function( data ) {
 126+ if ( data && data.query && data.query.gadgetcategories
 127+ && data.query.gadgetcategories[0] )
 128+ {
 129+ data = data.query.gadgetcategories;
 130+ // Update cache
 131+ gadgetCategoryCache = data;
 132+ callback( arrClone( data ), 'success' );
 133+ } else {
 134+ // Invalidate cache
 135+ gadgetCategoryCache = null;
 136+ callback( [], 'error' );
 137+ }
 138+ },
 139+ error: function() {
 140+ // Invalidate cache
 141+ gadgetCategoryCache = null;
 142+ callback( [], 'error' );
 143+ }
 144+ });
 145+ },
 146+
 147+ /**
 148+ * Creates or edits an existing gadget definition.
 149+ *
 150+ * @param gadget {Object}
 151+ * - id {String} Id of the gadget to modify
 152+ * - blob {Object} Gadget meta data
 153+ * @param callback {Function} Called with two arguments:
 154+ * - status ('ok' or 'error')
 155+ * - msg (localized, something like "Successfull", "Conflict occurred" etc.)
 156+ * @return {jqXHR|Null}: Null if served from cache, otherwise the jqXHR.
 157+ */
 158+ doModifyGadget: function( gadget, callback ) {
 159+ mw.log( gadget );
 160+ // @todo
 161+ // Get token
 162+ // JSON.stringify
 163+ // Do with ApiEdit
 164+ // Invalidate cache
 165+ gadgetCache[gadget.id] = null;
 166+ callback( 'error', '@todo: Saving not implemented yet. Check console for object that would be saved.' );
 167+ return null;
 168+ },
 169+
 170+ /**
 171+ * Deletes a gadget definition.
 172+ *
 173+ * @param id {String} Id of the gadget to delete.
 174+ * @param callback {Function} Called with one argument (ok', 'error' or 'conflict').
 175+ * @return {jqXHR|Null}: Null if served from cache, otherwise the jqXHR.
 176+ */
 177+ doDeleteGadget: function( id, callback ) {
 178+ // @todo
 179+ // Do with ApiDelete
 180+ // Invalidate cache
 181+ gadgetCache[id] = null;
 182+ callback( 'error' );
 183+ return null;
 184+ }
 185+ }
 186+ };
 187+
 188+})();
Index: branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.ui.css
@@ -0,0 +1,67 @@
 2+/**
 3+ * Styling for the gadget manager javascript-generated user interface.
 4+ */
 5+
 6+/**
 7+ * Form
 8+ */
 9+.mw-gadgetmanager-form fieldset {
 10+ margin: 0;
 11+}
 12+
 13+.mw-gadgetmanager-form table {
 14+ width: 100%;
 15+}
 16+
 17+.mw-gadgetmanager-form td,
 18+.mw-gadgetmanager-form th {
 19+ vertical-align: top;
 20+}
 21+
 22+/**
 23+ * The PropCloud
 24+ */
 25+.mw-gadgetmanager-propcloud {
 26+ background: white;
 27+ border: 1px solid grey;
 28+ overflow: hidden;
 29+}
 30+
 31+.mw-gadgetmanager-propcontainer {}
 32+
 33+.mw-gadgetmanager-prop {
 34+ float: left;
 35+ margin: 2px 5px 5px 2px;
 36+ padding: 2px 5px;
 37+ background: #e5eff6;
 38+ border: 1px solid #a4d2fb;
 39+ border-radius: 10px;
 40+ line-height: 1;
 41+}
 42+
 43+.mw-gadgetmanager-prop-label {}
 44+
 45+.mw-gadgetmanager-prop-delete {
 46+ display: inline-block;
 47+ width: 10px;
 48+ height: 10px;
 49+ /* @embed */
 50+ background: url(images/close.png) 50% 50% no-repeat;
 51+ opacity: 0.4;
 52+}
 53+
 54+.mw-gadgetmanager-prop:hover .mw-gadgetmanager-prop-delete {
 55+ opacity: 0.7;
 56+}
 57+
 58+.mw-gadgetmanager-prop:hover .mw-gadgetmanager-prop-delete:hover {
 59+ opacity: 1;
 60+ cursor: pointer;
 61+}
 62+
 63+.mw-gadgetmanager-propinput,
 64+.mw-gadgetmanager-propinput:focus {
 65+ border: 0;
 66+ outline: 0;
 67+ background: transparent;
 68+}
Index: branches/RL2/extensions/Gadgets/modules/images/close.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: branches/RL2/extensions/Gadgets/modules/images/close.png
___________________________________________________________________
Added: svn:mime-type
169 + application/octet-stream
Index: branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.ui.js
@@ -0,0 +1,417 @@
 2+/**
 3+ * JavaScript to initialize the UI of the gadget manager.
 4+ *
 5+ * @author Timo Tijhof
 6+ * @copyright © 2011 Timo Tijhof
 7+ * @license GNU General Public Licence 2.0 or later
 8+ */
 9+( function() {
 10+
 11+ var
 12+ /**
 13+ * @var {Object} Local alias to gadgetmananger
 14+ */
 15+ gm = mw.gadgetManager,
 16+ /**
 17+ * @var {Object} HTML fragements
 18+ */
 19+ tpl = {
 20+ fancyForm: '<form class="mw-gadgetmanager-form">\
 21+ <fieldset>\
 22+ <legend>Module properties</legend>\
 23+ <table>\
 24+ <tr>\
 25+ <td><label for="mw-gadgetmanager-input-scripts"><html:msg key="gadgetmanager-prop-scripts"></label></td>\
 26+ <td><input type="text" id="mw-gadgetmanager-input-scripts" /></td>\
 27+ </tr>\
 28+ <tr>\
 29+ <td><label for="mw-gadgetmanager-input-styles"><html:msg key="gadgetmanager-prop-styles"></label></td>\
 30+ <td><input type="text" id="mw-gadgetmanager-input-styles" /></td>\
 31+ </tr>\
 32+ <tr>\
 33+ <td><label for="mw-gadgetmanager-input-dependencies"><html:msg key="gadgetmanager-prop-dependencies"></label></td>\
 34+ <td><input type="text" id="mw-gadgetmanager-input-dependencies" /></td>\
 35+ </tr>\
 36+ <tr>\
 37+ <td><label for="mw-gadgetmanager-input-messages"><html:msg key="gadgetmanager-prop-messages"></label></td>\
 38+ <td><input type="text" id="mw-gadgetmanager-input-messages" /></td>\
 39+ </tr>\
 40+ </table>\
 41+ </fieldset>\
 42+ <fieldset>\
 43+ <legend>Gadget settings</legend>\
 44+ <table>\
 45+ <tr>\
 46+ <td><label for="mw-gadgetmanager-input-category"><html:msg key="gadgetmanager-prop-category"></label></td>\
 47+ <td><select id="mw-gadgetmanager-input-category"></select></td>\
 48+ </tr>\
 49+ <tr>\
 50+ <td><label for="mw-gadgetmanager-input-rights"><html:msg key="gadgetmanager-prop-rights"></label></td>\
 51+ <td><input type="text" id="mw-gadgetmanager-input-rights" /></td>\
 52+ </tr>\
 53+ <tr>\
 54+ <td><label for="mw-gadgetmanager-input-default"><html:msg key="gadgetmanager-prop-default"></label></td>\
 55+ <td><input type="checkbox" id="mw-gadgetmanager-input-default" /></td>\
 56+ </tr>\
 57+ <tr>\
 58+ <td><label for="mw-gadgetmanager-input-hidden"><html:msg key="gadgetmanager-prop-hidden"></label></td>\
 59+ <td><input type="checkbox" id="mw-gadgetmanager-input-hidden" /></td>\
 60+ </tr>\
 61+ ' + ( gm.conf.enableSharing ? '<tr>\
 62+ <td><label for="mw-gadgetmanager-input-shared"><html:msg key="gadgetmanager-prop-shared"></label></td>\
 63+ <td><input type="checkbox" id="mw-gadgetmanager-input-shared" /></td>\
 64+ </tr>\
 65+ ' : '' ) + '</table>\
 66+ </fieldset>\
 67+ </form>'
 68+ },
 69+ /**
 70+ * @var {Object} Static cache for suggestions by script prefix.
 71+ */
 72+ suggestCacheScripts = {},
 73+ /**
 74+ * @var {Object} Static cache for suggestions by style prefix.
 75+ */
 76+ suggestCacheStyles = {},
 77+ /**
 78+ * @var {Object} Static cache for suggestions by messages prefix.
 79+ */
 80+ suggestCacheMsgs = {},
 81+ /**
 82+ * @var {Object} Complete static cache for module names. Lazy loaded from null.
 83+ */
 84+ suggestCacheDependencies = null,
 85+ /**
 86+ * @var {Object} Complete static cache for all rights.
 87+ */
 88+ suggestCacheRights = gm.conf.allRights,
 89+ /**
 90+ * @var {Number} Maximum number of autocomplete suggestions in the gadget editor input fields.
 91+ */
 92+ suggestLimit = 7,
 93+ /**
 94+ * @var {Array} List of category objects with their name, localized title and member count.
 95+ */
 96+ gadgetCategoriesCache = [];
 97+
 98+ /* Local functions */
 99+
 100+ /**
 101+ * Remove all occurences of a value from an array.
 102+ *
 103+ * @param arr {Array} Array to be changed
 104+ * @param val {Mixed} Value to be removed from the array
 105+ * @return {Array} May or may not be changed, reference kept
 106+ */
 107+ function arrayRemove( arr, val ) {
 108+ var i;
 109+ // Parentheses are crucial here. Without them, var i will be a
 110+ // boolean instead of a number, resulting in an infinite loop!
 111+ while ( ( i = arr.indexOf( val ) ) !== -1 ) {
 112+ arr.splice( i, 1 );
 113+ }
 114+ return arr;
 115+ }
 116+
 117+ /* Public functions */
 118+
 119+ gm.ui = {
 120+ /**
 121+ * Initilizes the the page. For now just binding click handlers
 122+ * to the anchor tags in the table.
 123+ */
 124+ initUI: function() {
 125+ // Bind trigger to the links
 126+ $( '.mw-gadgetmanager-gadgets .mw-gadgetmanager-gadgets-title a' )
 127+ .click( function( e ) {
 128+ e.preventDefault();
 129+ var $el = $( this );
 130+ var gadget = {
 131+ id: $el.data( 'gadgetname' ),
 132+ displayTitle: $el.text(),
 133+ metadata: null
 134+ };
 135+ gm.ui.startGadgetEditor( gadget );
 136+ });
 137+ },
 138+
 139+ /**
 140+ * Initialize the gadget editor dialog.
 141+ *
 142+ * @asynchronous
 143+ * @param id {String}
 144+ * @param displayTitle {String}
 145+ */
 146+ startGadgetEditor: function( gadget ) {
 147+ // We need two things. Gadget meta-data and category information.
 148+ var done = 0, ready = 2;
 149+
 150+ gm.api.getGadgetMetadata( gadget.id, function( metadata, status ) {
 151+ // @todo Notification: If status is 'error'
 152+ gadget.metadata = metadata;
 153+ done++;
 154+ if ( done >= ready ) {
 155+ gm.ui.showFancyForm( gadget );
 156+ }
 157+ });
 158+
 159+ gm.api.getGadgetCategories( function( cats ) {
 160+ gadgetCategoriesCache = cats;
 161+ done++;
 162+ if ( done >= ready ) {
 163+ gm.ui.showFancyForm( gadget );
 164+ }
 165+ });
 166+ },
 167+
 168+ /**
 169+ * Generate form, create a dialog and open it into view.
 170+ *
 171+ * @param gadget {Object}
 172+ * @return {jQuery} The (dialogged) form.
 173+ */
 174+ showFancyForm: function( gadget ) {
 175+ var $form = gm.ui.getFancyForm( gadget.metadata );
 176+ var buttons = {};
 177+ buttons[mw.msg( 'gadgetmanager-editor-save' )] = function() {
 178+ gm.api.doModifyGadget( gadget, function( status, msg ) {
 179+ alert( "Save result: \n- status: " + status + "\n- msg: " + msg );
 180+ /* @todo Notification
 181+ addNotification( {
 182+ msg: msg,
 183+ type: status !== 'error' ? 'success' : status,
 184+ timedActionDelay: 5,
 185+ timedAction: function(){
 186+ // refresh page
 187+ }
 188+ });
 189+ */
 190+ });
 191+ };
 192+ return $form
 193+ .dialog({
 194+ autoOpen: true,
 195+ width: 800,
 196+ modal: true,
 197+ draggable: false,
 198+ resizable: false,
 199+ title: mw.message( 'gadgetmanager-editor-title', gadget.displayTitle ).escaped(),
 200+ buttons: buttons,
 201+ open: function() {
 202+ // Dialog is ready for action.
 203+ // Push out any notifications if some were queued up already between
 204+ // getting the gadget data and the display of the form.
 205+ /* @todo Notification
 206+ if ( gm.ui.notifications.length ) {
 207+ for ( in ) {
 208+ slice(i,1)_remove;
 209+ gm.ui.addNotification( $form, n[i] );
 210+ }
 211+ }
 212+ */
 213+ }
 214+ });
 215+ },
 216+
 217+ /**
 218+ * Generate a <form> for the given module.
 219+ * Also binds events for submission and autocompletion.
 220+ *
 221+ * @param metadata {Object} Object to read and write to, used when saving
 222+ * the gadget metadata back through the API.
 223+ * @return {jQuery} The form.
 224+ */
 225+ getFancyForm: function( metadata ) {
 226+ var nsGadgetId = mw.config.get( 'wgNamespaceIds' ).gadget,
 227+ $form = $( tpl.fancyForm ).localize();
 228+
 229+ // Module properties: scripts
 230+ $form.find( '#mw-gadgetmanager-input-scripts' ).createPropCloud({
 231+ props: metadata.module.scripts,
 232+ autocompleteSource: function( data, response ) {
 233+ // Use cache if available
 234+ if ( data.term in suggestCacheScripts ) {
 235+ response( suggestCacheScripts[data.term] );
 236+ return;
 237+ }
 238+ $.getJSON( mw.util.wikiScript( 'api' ), {
 239+ format: 'json',
 240+ action: 'query',
 241+ list: 'gadgetpages',
 242+ gpnamespace: nsGadgetId,
 243+ gpextension: 'js',
 244+ gpprefix: data.term
 245+ }, function( json ) {
 246+ if ( json && json.query && json.query.gadgetpages ) {
 247+ var suggestions = $.map( json.query.gadgetpages, function( val, i ) {
 248+ return val.pagename;
 249+ });
 250+ suggestions = suggestions.splice( 0, suggestLimit );
 251+
 252+ // Update cache
 253+ suggestCacheScripts[data.term] = suggestions;
 254+
 255+ response( suggestions );
 256+ } else {
 257+ response( [] );
 258+ }
 259+ }
 260+ );
 261+ },
 262+ prefix: 'mw-gadgetmanager',
 263+ removeTooltip: mw.msg( 'gadgetmanager-editor-removeprop-tooltip' ),
 264+ onAdd: function( prop ) { metadata.module.scripts.push( prop ); },
 265+ onRemove: function( prop ) { arrayRemove( metadata.module.scripts, prop ); }
 266+ });
 267+
 268+ // Module properties: styles
 269+ $form.find( '#mw-gadgetmanager-input-styles' ).createPropCloud({
 270+ props: metadata.module.styles,
 271+ autocompleteSource: function( data, response ) {
 272+ // Use cache if available
 273+ if ( data.term in suggestCacheStyles ) {
 274+ response( suggestCacheStyles[data.term] );
 275+ return;
 276+ }
 277+ $.getJSON( mw.util.wikiScript( 'api' ), {
 278+ format: 'json',
 279+ action: 'query',
 280+ list: 'gadgetpages',
 281+ gpnamespace: nsGadgetId,
 282+ gpextension: 'css',
 283+ gpprefix: data.term
 284+ }, function( json ) {
 285+ if ( json && json.query && json.query.gadgetpages ) {
 286+ var suggestions = $.map( json.query.gadgetpages, function( val, i ) {
 287+ return val.pagename;
 288+ });
 289+ suggestions = suggestions.splice( 0, suggestLimit );
 290+
 291+ // Update cache
 292+ suggestCacheStyles[data.term] = suggestions;
 293+
 294+ response( suggestions );
 295+ } else {
 296+ response( [] );
 297+ }
 298+ }
 299+ );
 300+ },
 301+ prefix: 'mw-gadgetmanager',
 302+ removeTooltip: mw.msg( 'gadgetmanager-editor-removeprop-tooltip' ),
 303+ onAdd: function( prop ) { metadata.module.styles.push( prop ); },
 304+ onRemove: function( prop ) { arrayRemove( metadata.module.styles, prop ); }
 305+ });
 306+
 307+ // Module properties: dependencies
 308+ $form.find( '#mw-gadgetmanager-input-dependencies' ).createPropCloud({
 309+ props: metadata.module.dependencies,
 310+ autocompleteSource: function( data, response ) {
 311+ if ( suggestCacheDependencies === null ) {
 312+ suggestCacheDependencies = mw.loader.getModuleNames();
 313+ }
 314+ var output = $.ui.autocomplete.filter( suggestCacheDependencies, data.term );
 315+ response( output.slice( 0, suggestLimit ) );
 316+ },
 317+ prefix: 'mw-gadgetmanager',
 318+ removeTooltip: mw.msg( 'gadgetmanager-editor-removeprop-tooltip' ),
 319+ onAdd: function( prop ) { metadata.module.dependencies.push( prop ); },
 320+ onRemove: function( prop ) { arrayRemove( metadata.module.dependencies, prop ); }
 321+ });
 322+
 323+ // Module properties: messages
 324+ $form.find( '#mw-gadgetmanager-input-messages' ).createPropCloud({
 325+ props: metadata.module.messages,
 326+ autocompleteSource: function( data, response ) {
 327+ // Use cache if available
 328+ if ( data.term in suggestCacheMsgs ) {
 329+ response( suggestCacheMsgs[data.term] );
 330+ return;
 331+ }
 332+ $.getJSON( mw.util.wikiScript( 'api' ), {
 333+ format: 'json',
 334+ action: 'query',
 335+ meta: 'allmessages',
 336+ amprefix: data.term,
 337+ amnocontent: true,
 338+ amlang: mw.config.get( 'wgContentLanguage' )
 339+ }, function( json ) {
 340+ if ( json && json.query && json.query.allmessages ) {
 341+ var suggestions = $.map( json.query.allmessages, function( val, i ) {
 342+ return val.name;
 343+ });
 344+ suggestions = suggestions.splice( 0, suggestLimit );
 345+
 346+ // Update cache
 347+ suggestCacheMsgs[data.term] = suggestions;
 348+
 349+ response( suggestions );
 350+ } else {
 351+ response( [] );
 352+ }
 353+ }
 354+ );
 355+ },
 356+ prefix: 'mw-gadgetmanager',
 357+ removeTooltip: mw.msg( 'gadgetmanager-editor-removeprop-tooltip' ),
 358+ onAdd: function( prop ) { metadata.module.messages.push( prop ); },
 359+ onRemove: function( prop ) { arrayRemove( metadata.module.messages, prop ); }
 360+ });
 361+
 362+ // Gadget settings: category
 363+ $form.find( '#mw-gadgetmanager-input-category' ).append( function() {
 364+ var current = metadata.settings.category,
 365+ opts = '',
 366+ i = 0,
 367+ cat;
 368+ for ( ; i < gadgetCategoriesCache.length; i++ ) {
 369+ cat = gadgetCategoriesCache[i];
 370+ opts += mw.html.element( 'option', {
 371+ value: cat.name,
 372+ selected: cat.name === current
 373+ }, cat.title );
 374+ }
 375+ return opts;
 376+ }).change( function() {
 377+ metadata.settings.category = $(this).val();
 378+ });
 379+
 380+ // Gadget settings: rights
 381+ $form.find( '#mw-gadgetmanager-input-rights' ).createPropCloud({
 382+ props: metadata.settings.rights,
 383+ autocompleteSource: function( data, response ) {
 384+ var output = $.ui.autocomplete.filter( suggestCacheRights, data.term );
 385+ response( output.slice( 0, suggestLimit ) );
 386+ },
 387+ prefix: 'mw-gadgetmanager',
 388+ removeTooltip: mw.msg( 'gadgetmanager-editor-removeprop-tooltip' ),
 389+ onAdd: function( prop ) { metadata.settings.rights.push( prop ); },
 390+ onRemove: function( prop ) { arrayRemove( metadata.settings.rights, prop ); }
 391+ });
 392+
 393+ // Gadget settings: Default
 394+ $form.find( '#mw-gadgetmanager-input-default' )
 395+ .prop( 'checked', metadata.settings['default'] )
 396+ .change( function() {
 397+ metadata.settings['default'] = this.checked;
 398+ });
 399+
 400+ // Gadget settings: Hidden
 401+ $form.find( '#mw-gadgetmanager-input-hidden' )
 402+ .prop( 'checked', metadata.settings.hidden )
 403+ .change( function() { metadata.settings.hidden = this.checked; });
 404+
 405+ // Gadget settings: Shared
 406+ $form.find( '#mw-gadgetmanager-input-shared' )
 407+ .prop( 'checked', metadata.settings.shared )
 408+ .change( function() { metadata.settings.shared = this.checked; });
 409+
 410+
 411+ return $form;
 412+ }
 413+ };
 414+
 415+ // Launch on document ready
 416+ $( document ).ready( gm.ui.initUI );
 417+
 418+})();
Index: branches/RL2/extensions/Gadgets/modules/ext.gadgets.gadgetmanager.prejs.css
@@ -1,5 +1,6 @@
2 -.mw-gadgetmanager-gadgets {
3 - width: 100%;
 2+.mw-gadgetmanager-gadgets.mw-datatable th,
 3+.mw-gadgetmanager-gadgets.mw-datatable td {
 4+ padding: 2px 5px;
45 }
56
67 .mw-gadgetmanager-gadgets-default,
Index: branches/RL2/extensions/Gadgets/SpecialGadgetManager.php
@@ -22,6 +22,11 @@
2323 $out->setPagetitle( wfMsg( 'gadgetmanager-title' ) );
2424 $out->addModuleStyles( 'ext.gadgets.gadgetmanager.prejs' );
2525
 26+ // Only load ajax editor if user is allowed to edit
 27+ if ( $this->getUser()->isAllowed( 'gadgets-definition-edit' ) ) {
 28+ $out->addModules( 'ext.gadgets.gadgetmanager.ui' );
 29+ }
 30+
2631 // Determine view
2732 if ( is_string( $par ) && $par !== '' ) {
2833 $html = $this->generateGadgetView( $par );
@@ -91,7 +96,7 @@
9297 if ( $wgGadgetEnableSharing ) {
9398 $html .= '<th>' . wfMessage( 'gadgetmanager-tablehead-shared' )->escaped() . '</th>';
9499 }
95 - $html .= '</tr>';
 100+ $html .= '<th>' . wfMessage( 'gadgetmanager-tablehead-lastmod' )->escaped() . '</tr>';
96101
97102 // Populate table rows for the current category
98103 foreach ( $gadgets as $gadgetName => $gadget ) {
@@ -105,14 +110,10 @@
106111 ) );
107112
108113 // Title
109 - $titleMsg = wfMessage( $gadget->getTitleMsg() );
110114 $titleLink = Linker::link(
111115 $this->getTitle( $gadget->getName() ),
112 - // MediaWiki-message is optional. This is for backwards compatibility (since
113 - // the previous version didn't have titles), and to a allow wikis that only
114 - // care about one language to save from creating NS_MEDIAWIKI pages.
115 - // @todo: Centralize this logic.
116 - $titleMsg->exists() ? $titleMsg->plain() : $this->getLang()->ucfirst( $gadget->getName() )
 116+ $gadget->getTitleMessage(),
 117+ array( 'data-gadgetname' => $gadget->getName() )
117118 );
118119 $html .= "<td class=\"mw-gadgetmanager-gadgets-title\">$titleLink</td>";
119120 // Default
@@ -127,6 +128,39 @@
128129 . ( $gadget->isShared() ? $tickedCheckboxHtml : '' ) . '</td>';
129130 }
130131
 132+ // Last modified
 133+ $lastModText = '';
 134+ $definitionTitle = Title::newFromText( $gadget->getName() . '.js', NS_GADGET_DEFINITION );
 135+ if ( $definitionTitle ) {
 136+ $definitionRev = Revision::newFromTitle( $definitionTitle );
 137+ if ( $definitionRev ) {
 138+ $userLang = $this->getLang();
 139+ $revTimestamp = $definitionRev->getTimestamp();
 140+ $userText = $definitionRev->getUserText();
 141+ $userLinks =
 142+ Linker::userLink(
 143+ $definitionRev->getUser(),
 144+ $userText
 145+ ) .
 146+ Linker::userToolLinks(
 147+ $definitionRev->getUser(),
 148+ $userText
 149+ );
 150+ $lastModText = wfMsgExt(
 151+ 'gadgetmanager-tablecell-lastmod',
 152+ array( 'replaceafter', 'parseinline' ),
 153+ array(
 154+ $userLang->timeanddate( $revTimestamp, true ),
 155+ $userLinks,
 156+ $userLang->date( $revTimestamp, true ),
 157+ $userLang->time( $revTimestamp, true ),
 158+ $userText
 159+ )
 160+ );
 161+ }
 162+ $html .= "<td>$lastModText</td>";
 163+ }
 164+
131165 $html .= '</tr>';
132166 }
133167
@@ -141,6 +175,8 @@
142176 * @return String: HTML
143177 */
144178 public function generateGadgetView( $gadgetName ) {
145 - return "Stub page where there will be some info about the gadget ($gadgetName). This is also used for permalinks to a gadget's config page.";
 179+ return 'TODO - This page is about "'
 180+ . htmlspecialchars( $gadgetName )
 181+ . '". Also used as permalink from other places.';
146182 }
147183 }

Follow-up revisions

RevisionCommit summaryAuthorDate
r96661RL2: Some typo fixes in the JS files introduced in r96309catrope14:07, 9 September 2011
r96759[ResourceLoader2] Address issues raised in r96309 CR...krinkle23:38, 10 September 2011
r97513[ResourceLoader2] Merge SpecialGadgets w/ SpecialGadgetManager...krinkle16:45, 19 September 2011

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r95818[ResourceLoader2]: Initial go at the GadgetManager....krinkle22:25, 30 August 2011
r95970RL2: Rename getTitleMsg() to getTitleMessageKey(), and introduce getTitleMess...catrope12:19, 1 September 2011

Comments

#Comment by Catrope (talk | contribs)   13:14, 9 September 2011

None of the JS files added in this revision alias jQuery to $ in their closures.

+		return $( '<span class="' + o.prefix + 'prop"></span>' )

Eww. Generic functions should always escape their arguments. Never make the caller responsible for escaping. This pattern is repeated throughout the plugin; only one line uses addClass().

+	 *         <span editor="jquery-prop">

Weird typo?

+	 *     - onAdd {Function} Callback when an item is added
+	 *     - onRemove {Function} Callback when an item is deleted

You should document the parameter lists of these callbacks.

+		o.prefix = o.prefix + '-';

This is why mankind invented += ;) . Also, would it be that unreasonable to require that the caller put in the dash? That seems much more logical to me.

+				// Prevent duplicate values
+				if ( o.props.indexOf( val ) === -1 ) {
+					$container.append( newPropHtml( val, o ) );
+					o.onAdd( val );
+				}

Doesn't this fail to prevent duplicate added values, because val isn't added to o.props ? The way it looks like to me at a quick glance is that if you've got o.props = ['foo','bar'] it won't let you add foo or bar, but it will let you add baz twice.

		 * @var {Object|Null} If cache, object with category ids and the member counts */

That sentence doesn't make much sense to me. It's also nice to document the structure of these things, e.g. // { gadgetID: metadataObject }

I don't really understand the architectural decision to have a single callback with a parameter that specifies success or error rather than having separate callbacks for both cases (the latter pattern is used a lot in UploadWizard). When would the success callback and the error callback ever share code? You're also using your pattern inconsistently: getGadgetMetadata() and getGadgetCategories() use callback( {}, 'error' ) but doModifyGadget() uses callback( 'error', msg ) .

+.mw-gadgetmanager-propcontainer {}
[...]
+.mw-gadgetmanager-prop-label {}

???

						<legend>Module properties</legend>\

Hardcoded English. Isn't this jquery.localize-able?

		 * @param id {String}
		 * @param displayTitle {String}
		 */
		startGadgetEditor: function( gadget ) {

Documentation doesn't match the function signature.

					alert( "Save result: \n- status: " + status + "\n- msg: " + msg );

alert()? Really? :D

								var suggestions = $.map( json.query.gadgetpages, function( val, i ) {
									return val.pagename;
								});
								suggestions = suggestions.splice( 0, suggestLimit );

Wouldn't it make more sense to do the splice before the map, to eliminate wasted effort?

There's a lot of code duplication between the createPropCloud() calls for JS and CSS suggestions, and less so between the others. At least the former looks like it could be factored out.

			$form.find( '#mw-gadgetmanager-input-category' ).append( function() {

I had no idea that worked; cool feature.

+			$html .= '<th>' . wfMessage( 'gadgetmanager-tablehead-lastmod' )->escaped() . '</tr>';

No closing th tag. Probably cool in HTML5 mode but not so much in other modes.

+				$definitionTitle = Title::newFromText( $gadget->getName() . '.js', NS_GADGET_DEFINITION );

This doesn't work correctly if you happen to have a gadget called Talk:Foo. Yes, I know that's idiotic, but code needs to be idiot-proof when it comes to dealing with user-supplied data. Use Title::makeTitleSafe( NS_GADGET_DEFINITION, $str ) instead.

Marking fixme because of the escaping, closing th tag and newFromText issues.

#Comment by Krinkle (talk | contribs)   23:38, 10 September 2011

See follow-up r96759.

Regarding the following:

+				// Prevent duplicate values
+				if ( o.props.indexOf( val ) === -1 ) {
+					$container.append( newPropHtml( val, o ) );
+					o.onAdd( val );
+				}

Doesn't this fail to prevent duplicate added values, because val isn't added to o.props ? The way it looks like to me at a quick glance is that if you've got o.props = ['foo','bar'] it won't let you add foo or bar, but it will let you add baz twice.

I agree it's not optimal. Right now the updating of o.props is expected to be handled by the callback (among other things). However I realize this is not handy, there was a reason for it though, however it turns out to be no longer valid. The reason for this behavior was because the options are (or could be) passed by value, instead of reference. Which means updating it here would not update the real object, hence we should let the callback handle this. However this rationale is no longer valid since the actual check is looking in the options' props, not some unknown 'real object'.

The plugin is not receiving the props by value, it's maintaining the reference very well (it does call $.extend on options, but it doesn't do it recursively, it merely copies them over by key from one object to another, possibly overwriting a default but maintaining the reference)

I changed plugin in the follow-up to do handle it inside the plugin. This got rid of a fair amount of code duplication as in most (if not all) cases all the callback did was updating the props object. Nice!

+.mw-gadgetmanager-propcontainer {}
[...]
+.mw-gadgetmanager-prop-label {}

???

Still a work in progress. Already used in my local copy, accidentally left it in here while I was separating the local copy up into two parts so that I could do this commit first, before continuing.


						<legend>Module properties</legend>\

Hardcoded English. Isn't this jquery.localize-able?

The interface is still changing, so to avoid having to change 3 or 4 files all the time, left it hardcoded until a somewhat final decision is made. Next phase will take care of this. Thanks for the reminder (iirc there are some other untranslated parts as well)

+				$definitionTitle = Title::newFromText( $gadget->getName() . '.js', NS_GADGET_DEFINITION );

Aha. Now that I re-read the documentation for Title::newFromText it makes sense. The second argument isn't "namespace" but "defaultNamespace", not obvious imho since it's a pretty generic function name. Anway, what I want here is makeTitleSafe indeed. Fixed.

Status & tagging log