Daniel Werner has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/79146


Change subject: Introduces jQuery.AnimationEvent and jQuery.fn.animateWithEvent
......................................................................

Introduces jQuery.AnimationEvent and jQuery.fn.animateWithEvent

jQuery.fn.animateWithEvent allows for efficient event handling during 
animations and for simple
extensibility of widgets dealing with animations.

Change-Id: I3be9cd0fefed0152bd3a14fc51c94ff0e11387a3
---
M ValueView/ValueView.resources.mw.php
M ValueView/ValueView.tests.qunit.php
A ValueView/resources/jquery/jquery.AnimationEvent.js
A ValueView/resources/jquery/jquery.animateWithEvent.js
A ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js
A ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js
6 files changed, 596 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DataValues 
refs/changes/46/79146/1

diff --git a/ValueView/ValueView.resources.mw.php 
b/ValueView/ValueView.resources.mw.php
index 6a67c0b..f4bd0ab 100644
--- a/ValueView/ValueView.resources.mw.php
+++ b/ValueView/ValueView.resources.mw.php
@@ -61,6 +61,16 @@
                        ),
                ),
 
+               'jquery.animateWithEvent' => $moduleTemplate + array(
+                       'scripts' => array(
+                               'jquery/jquery.AnimationEvent.js',
+                               'jquery/jquery.animateWithEvent.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery.PurposedCallbacks',
+                       ),
+               ),
+
                'jquery.eachchange' => $moduleTemplate + array(
                        'scripts' => array(
                                'jquery/jquery.eachchange.js'
diff --git a/ValueView/ValueView.tests.qunit.php 
b/ValueView/ValueView.tests.qunit.php
index 97682fa..e909c30 100644
--- a/ValueView/ValueView.tests.qunit.php
+++ b/ValueView/ValueView.tests.qunit.php
@@ -44,6 +44,16 @@
                        ),
                ),
 
+               'jquery.animateWithEvent.tests' => array(
+                       'scripts' => array(
+                               "$bp/jquery/jquery.AnimationEvent.tests.js",
+                               "$bp/jquery/jquery.animateWithEvent.tests.js",
+                       ),
+                       'dependencies' => array(
+                               'jquery.animateWithEvent',
+                       ),
+               ),
+
                'jquery.eachchange.tests' => array(
                        'scripts' => array(
                                "$bp/jquery/jquery.eachchange.tests.js",
diff --git a/ValueView/resources/jquery/jquery.AnimationEvent.js 
b/ValueView/resources/jquery/jquery.AnimationEvent.js
new file mode 100644
index 0000000..540f072
--- /dev/null
+++ b/ValueView/resources/jquery/jquery.AnimationEvent.js
@@ -0,0 +1,142 @@
+/**
+ * @licence GNU GPL v2+
+ * @version 0.1
+ * @author Daniel Werner < [email protected] >
+ *
+ * @dependency jQuery
+ */
+jQuery.AnimationEvent = ( function( $, PurposedCallbacks ) {
+       'use strict';
+
+       /**
+        * Event for notifying about an animation which is about to be added to 
an animation queue.
+        *
+        * @since 0.1
+        *
+        * @param {string} animationPurpose Will be available as 
"animationPurpose" field. This will
+        *        not end up as the event's "type". The "type" will always be 
set to "animation".
+        * @param {Object} props Additional event properties which will be 
copied into the object.
+        * @return {jQuery.AnimationEvent} Can be instantiated without "new".
+        * @constructor
+        * @extends jQuery.Event
+        */
+       var SELF = function AnimationEvent( animationPurpose, props ) {
+               if( !( this instanceof SELF ) ) {
+                       return new SELF( animationPurpose, props );
+               }
+               if( typeof animationPurpose !== 'string' || $.trim( 
animationPurpose ) === '' ) {
+                       throw new Error( 'An animation purpose has to be 
stated' );
+               }
+
+               // Apply "parent" constructor:
+               $.Event.call( this, 'animation', props );
+
+               var self = this;
+               var callbacksList = new PurposedCallbacks( SELF.ANIMATION_STEPS 
);
+
+               /**
+                * The purpose stated for the animation which is about to be 
started.
+                *
+                * @type {string}
+                * @since 0.1
+                */
+               this.animationPurpose = animationPurpose;
+
+               /**
+                * A jQuery.PurposedCallbacks.Facade which allows for 
registering callbacks for different
+                * stages of the animation which is about to be started. The 
possible stages are those
+                * defined in jQuery.AnimationEvent.ANIMATION_STEPS.
+                *
+                * @see http://api.jquery.com/animate For a detailed 
description for each animation stage.
+                * @example event.animationCallbacks.add( 'step', function() { 
... } );
+                *
+                * @type {jQuery.PurposedCallbacks.Facade}
+                * @since 0.1
+                */
+               this.animationCallbacks = callbacksList.facade();
+
+               /**
+                * The jQuery.Animation object associated with the event. This 
will only be set if the
+                * animationOptions() object generated by this instance is used 
as options for an animation
+                * and after the animation has been started (not just queued).
+                *
+                * @type {Object} A jQuery.Promise with some additional fields. 
See jQuery.Animation.
+                * @since 0.1
+                */
+               this.animation = null;
+
+               /**
+                * Returns an object which can be used as options for 
jQuery.animate or any shortcut
+                * version of it (e.g. jQuery.fadeIn). Defines all animation 
callback fields with functions
+                * which will trigger all registered callbacks and all 
callbacks still registered to the
+                * "animationCallbacks" field's jQuery.PurposedCallbacks.Facade 
object in the future.
+                * Optionally, an object of options to be mixed in can be 
given. If this object has
+                * callbacks defined already, then these callbacks will be 
mixed in and called first.
+                *
+                * IMPORTANT: The options generated by one AnimationEvent 
instance should only be used for
+                *  one animation.
+                *
+                * @since 0.1
+                *
+                * @param {Object} [baseOptions]
+                * @returns {*}
+                *
+                * @throws {Error} If animationOptions() has been called 
already and the returned options
+                *         have been passed to some animation whose execution 
has started already.
+                */
+               this.animationOptions = function( baseOptions ) {
+                       if( this.animation ) {
+                               throw new Error( 'The AnimationEvent 
instance\'s generated animation options are ' +
+                                       'used within some animation already, 
they can not be used in two animations.' );
+                       }
+                       baseOptions = baseOptions || {};
+                       var options = $.extend( {}, baseOptions );
+
+                       $.each( SELF.ANIMATION_STEPS, function( i, purpose ) {
+                               // Consider callbacks defined in the given 
options, they should be called first.
+                               var baseCallback = baseOptions[ purpose ];
+                               var finalCallback = function() {
+                                       if( baseCallback ) {
+                                               baseCallback.apply( this, 
arguments );
+                                       }
+                                       callbacksList.fireWith( this, [ purpose 
], arguments );
+                               };
+                               if( purpose === 'start' ) {
+                                       options.start = function() {
+                                               // If "start" gets called, this 
means the generated options are used within
+                                               // an jQuery.Animation. Tell 
the AnimationEvent instance which has created
+                                               // these options which 
jQuery.Animation object it is related to.
+                                               var animation = arguments[0];
+                                               if( self.animation && 
self.animation !== animation ) {
+                                                       throw new Error( 'Can 
not use the same AnimationEvent instance\'s '
+                                                               + 
'animationOptions() for two different animations.' );
+                                               }
+                                               self.animation = animation;
+                                               finalCallback.apply( this, 
arguments );
+                                       };
+                               } else {
+                                       options[ purpose ] = finalCallback;
+                               }
+                       } );
+                       return options;
+               };
+       };
+
+       // Inherit from $.Event but remove certain fields this will create. 
This should not actually
+       // matter since they will be overwritten when creating an instance, but 
do it the "clean" way
+       // anyhow:
+       SELF.prototype = new $.Event();
+       delete( SELF.prototype.timeStamp );
+       delete( SELF.prototype[ jQuery.expando ] );
+
+       /**
+        * All animation step callback option names usable in 
jQuery.Animation's options
+        *
+        * @type {string[]}
+        * @since 0.1
+        */
+       SELF.ANIMATION_STEPS = [ 'start', 'step', 'progress', 'complete', 
'done', 'fail', 'always' ];
+
+       return SELF;
+
+}( jQuery, jQuery.PurposedCallbacks ) );
diff --git a/ValueView/resources/jquery/jquery.animateWithEvent.js 
b/ValueView/resources/jquery/jquery.animateWithEvent.js
new file mode 100644
index 0000000..f45891f
--- /dev/null
+++ b/ValueView/resources/jquery/jquery.animateWithEvent.js
@@ -0,0 +1,95 @@
+/**
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ *
+ * @dependency jQuery.AnimationEvent
+ */
+jQuery.fn.animateWithEvent = ( function( $ ) {
+       'use strict';
+
+       /**
+        * Same as jQuery.fn.animate or any other animation function with the 
difference that for each
+        * element to be animated, a jQuery.AnimationEvent will be created. The 
AnimationEvent instance
+        * will be available as first parameter in the "startCallback" which 
will be called for each
+        * element's animation when the animation is about to start.
+        *
+        * The "startCallback" can be used to trigger an event, stating that an 
animation is about to
+        * be executed. In the event the AnimationEvent can be used to allow 
event listeners to add
+        * specialized callbacks per animation stage (see AnimationEvent 
documentation).
+        *
+        * @example <code>
+        * $.animationWithEvent(
+       *     'mywidgetsgreatanimation',
+        *     'fadeIn',
+        *     { duration: 200 },
+        *     function( animationEvent ) {
+        *         self._trigger( 'animation', animationEvent );
+        *     }
+        * );
+        * </code>
+        *
+        * @param {string} animationPurpose Will be forwarded to 
jQuery.AnimationEvent.
+        * @param {string|Object} animationProperties Name of a jQuery.fn 
member which is dedicated to
+        *        some animation (e.g. "fadeIn") and takes an "options" 
argument. Can also be an object
+        *        of properties to animate, in this case jQuery.fn.animate will 
be used.
+        * @param {Object} [options] Options passed to the animation 
("duration", "easing" etc.).
+        * @param {Function( jQuery.AnimationEvent event )} [startCallback] 
Callback which will be fired
+        *        before the animation starts. This is different from the 
options.start callback since
+        *        it will get a jQuery.AnimationEvent instance. Also, the 
callback will be triggered
+        *        before any options.start callback.
+        * @returns {*}
+        *
+        * @throws {Error} If animationProperties is a string but not a member 
of jQuery.fn
+        * @throws {Error}
+        */
+       return function animateWithEvent( animationPurpose, 
animationProperties, options, startCallback ) {
+               var animationFunction;
+               if( typeof animationProperties !== 'string' ) {
+                       // jQuery.fn.animate( animationProperties, options )
+                       animationFunction = 'animate';
+                       animationProperties = animationProperties || {}; // 
allow "empty" animation
+               } else {
+                       // E.g. "fadeIn", properties are predefined in 
jQuery.fn.fadeIn( options ).
+                       animationFunction = animationProperties;
+                       animationProperties = false;
+                       if( !$.isFunction( $.fn[ animationFunction ] ) ) {
+                               throw new Error( 'jQuery.fn."' + 
animationFunction + '" is not a function.' );
+                       }
+               }
+
+               if( $.isFunction( options ) || !options ) {
+                       startCallback = options;
+                       options = {};
+               }
+               startCallback = startCallback || $.noop;
+
+               $.each( this, function( i, elem ) {
+                       var animationEvent = $.AnimationEvent( animationPurpose 
);
+
+                       // The animation options generated by the event will 
have all animation stage fields
+                       // defined with callbacks that will fire the related 
callbacks registered to the
+                       // animationEvent's "animationCallbacks" field in the 
future.
+                       var animationOptions = animationEvent.animationOptions( 
$.extend( {}, options, {
+                               start: function() {
+                                       // startCallback could for example 
trigger an "animation" event within a widget.
+                                       // All event listeners can then 
register their callbacks for different animation
+                                       // stages to 
animationEvent.animationCallbacks.
+                                       startCallback.call( this, 
animationEvent );
+                                       if( options.start ) {
+                                               options.start.apply( this, 
arguments );
+                                       }
+                               }
+                       } ) );
+
+                       if( animationProperties ) {
+                               // animate()
+                               $( elem )[ animationFunction ]( 
animationProperties, animationOptions );
+                       } else {
+                               // any dedicated animation function, e.g. 
"fadeIn"
+                               $( elem )[ animationFunction ]( 
animationOptions );
+                       }
+               } );
+               return this;
+       };
+
+}( jQuery ) );
diff --git a/ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js 
b/ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js
new file mode 100644
index 0000000..98f5374
--- /dev/null
+++ b/ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js
@@ -0,0 +1,170 @@
+/**
+ * QUnit tests for 'jQuery.AnimationEvent'.
+ *
+ * @file
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ */
+( function( $, QUnit, AnimationEvent, PurposedCallbacks ) {
+       'use strict';
+       /* jshint newcap: false */
+
+       QUnit.module( 'jquery.AnimationEvent' );
+
+       function assertSuccessfulConstruction( assert, instance, purpose ) {
+               assert.ok(
+                       instance instanceof AnimationEvent,
+                       'Instantiated'
+               );
+               assert.ok(
+                       instance instanceof $.Event,
+                       'Instance of jQuery.Event.'
+               );
+               assert.strictEqual(
+                       instance.animationPurpose,
+                       purpose,
+                       'Animation purpose got copied into "animationPurpose" 
field.'
+               );
+               assert.ok(
+                       instance.animationCallbacks instanceof 
PurposedCallbacks.Facade,
+                       '"animationCallbacks" field is instance of 
jQuery.PurposedCallbacks.Facade'
+               );
+               assert.strictEqual(
+                       instance.type,
+                       'animation',
+                       '"type" field is set to "animation"'
+               );
+       }
+
+       QUnit.test( 'construction without "new"', function( assert ) {
+               assertSuccessfulConstruction( assert, AnimationEvent( 
'nopurpose' ), 'nopurpose' );
+       } );
+
+       QUnit.test( 'construction with "new"', function( assert ) {
+               assertSuccessfulConstruction( assert, new AnimationEvent( 'foo' 
), 'foo' );
+       } );
+
+       QUnit.test( 'construction with custom fields given', function( assert ) 
{
+               var fields = {
+                       someCustomField1: 'foo',
+                       someCustomField2: {}
+               };
+               var event = AnimationEvent( 'someanimation', fields );
+
+               assertSuccessfulConstruction( assert, event, 'someanimation' );
+
+               assert.ok(
+                       event.foo === fields.foo,
+                       'Custom field got copied.'
+               );
+               assert.ok(
+                       event.someCustomField2 === fields.someCustomField2,
+                       'Another custom field got copied, copy happens by 
reference, no deep extend.'
+               );
+       } );
+
+       QUnit.test( 'animationOptions()', AnimationEvent.ANIMATION_STEPS.length 
+ 2, function( assert ) {
+               var event = AnimationEvent( 'animationpurpose' );
+               var predefined = {
+                       easing: 'swing',
+                       queue: true,
+                       duration: 200
+               };
+
+               var options = event.animationOptions( predefined );
+
+               assert.ok(
+                       $.isPlainObject( options ),
+                       'Returns a plain object.'
+               );
+               assert.ok(
+                       options.easing === predefined.easing
+                       && options.queue === predefined.queue
+                       && options.duration === predefined.duration,
+                       'Returned object holds all values of the base object 
given to animationOptions().'
+               );
+
+               $.each( AnimationEvent.ANIMATION_STEPS, function( i, stepName ) 
{
+                       assert.ok(
+                               $.isFunction( options[ stepName ] ),
+                               'Returned options object\'s field "' + stepName 
+ '" is a function.'
+                       );
+               } );
+       } );
+
+       QUnit.test( 'ANIMATION_STEPS', function( assert ) {
+               assert.ok(
+                       $.isArray( AnimationEvent.ANIMATION_STEPS ),
+                       'Is an array.'
+               );
+               // This might be kind of pointless, but simply make sure that 
no one changes this without
+               // changing tests as well, being absolutely sure about it.
+               var expectedSteps =
+                       [ 'start', 'step', 'progress', 'complete', 'done', 
'fail', 'always' ];
+               assert.ok(
+                       $( AnimationEvent.ANIMATION_STEPS ).not( expectedSteps 
).length === 0
+                       && $( expectedSteps ).not( 
AnimationEvent.ANIMATION_STEPS ).length === 0,
+                       'Contains expected steps.'
+               );
+       } );
+
+       function testAnimationOptionsGeneratedCallbacks( assert, testStep ) {
+               var event = AnimationEvent( 'animationpurpose' );
+               var predefined = {};
+
+               var firedPredefined, firedCallbacksMember;
+               var resetFired = function() { firedPredefined = 
firedCallbacksMember = 0; };
+
+               resetFired();
+
+               predefined[ testStep ] = function() {
+                       firedPredefined++;
+                       assert.ok(
+                               !firedCallbacksMember,
+                               'Predefined "' + testStep + '" callback got 
executed first.'
+                       );
+               };
+               event.animationCallbacks.add( testStep, function() {
+                       firedCallbacksMember++;
+               } );
+
+               var options = event.animationOptions( predefined );
+               options[ testStep ]();
+
+               assert.strictEqual(
+                       firedPredefined,
+                       1,
+                       'Fired predefined callback.'
+               );
+               assert.strictEqual(
+                       firedCallbacksMember,
+                       1,
+                       'Fired callback registered to event\'s 
"animationCallbacks" field.'
+               );
+
+               resetFired();
+
+               // Execute all other generated step callbacks as well, verify 
that they execute and that
+               // they do not trigger the testStep's callback again.
+               $.each( AnimationEvent.ANIMATION_STEPS, function( i, step ) {
+                       if( step !== testStep ) {
+                               options[ step ]();
+                       }
+               } );
+               assert.ok(
+                       firedPredefined === 0 && firedCallbacksMember === 0,
+                       'Fired callbacks generated for all other option fields, 
they are independent of the "'
+                               + testStep + '" one.'
+               );
+       }
+
+       $.each( AnimationEvent.ANIMATION_STEPS, function( i, step ) {
+               QUnit.test(
+                       'animationOptions(). ' + step + ' callbacks test',
+                       4,
+                       function( assert ) {
+                               testAnimationOptionsGeneratedCallbacks( assert, 
step );
+                       } );
+       } );
+
+}( jQuery, QUnit, jQuery.AnimationEvent, jQuery.PurposedCallbacks ) );
diff --git a/ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js 
b/ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js
new file mode 100644
index 0000000..c51ab1e
--- /dev/null
+++ b/ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js
@@ -0,0 +1,169 @@
+/**
+ * QUnit tests for 'jQuery.AnimationEvent'.
+ *
+ * @file
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ */
+( function( $, QUnit ) {
+       'use strict';
+       /* jshint newcap: false */
+
+       QUnit.module( 'jquery.animateWithEvent' );
+
+       QUnit.test( 'special start callback execution before options.start', 
function( assert ) {
+               var optionsStartCallbackDone = 0;
+               var specialStartCallbackDone = 0;
+
+               QUnit.stop();
+               $( '<div/>').animateWithEvent(
+                       'fooeventpurpose',
+                       'fadeOut',
+                       {
+                               start: function( animation ) {
+                                       optionsStartCallbackDone++;
+                                       QUnit.start();
+                               }
+                       }, function( animationEvent ) {
+                               assert.ok(
+                                       !optionsStartCallbackDone,
+                                       'last argument start callback got fired 
before options.start callback.'
+                               );
+                               specialStartCallbackDone++;
+                       }
+               );
+
+               assert.strictEqual(
+                       optionsStartCallbackDone,
+                       1,
+                       'options.start callback got fired.'
+               );
+               assert.strictEqual(
+                       specialStartCallbackDone,
+                       1,
+                       'Last argument start callback got fired.'
+               );
+       } );
+
+       QUnit.test( 'special start callback', function( assert ) {
+               var $elem = $( '<div/>');
+
+               $elem.animateWithEvent(
+                       'foopurpose',
+                       { width: 200 },
+                       {},
+                       function( animationEvent ) {
+                               assert.ok(
+                                       this === $elem.get( 0 ),
+                                       'Context of the callback is the DOM 
node to be animated.'
+                               );
+                               assert.ok(
+                                       animationEvent instanceof 
$.AnimationEvent,
+                                       'Airst argument is an instance of 
jQuery.AnimationEvent.'
+                               );
+
+                       }
+               );
+       } );
+
+       QUnit.test( 'options.start callback', 2, function( assert ) {
+               var $elem = $( '<div/>');
+               var animationEventsAnimation;
+
+               $elem.animateWithEvent(
+                       'foopurpose',
+                       { width: 200 },
+                       {
+                               start: function( animation ) {
+                                       assert.ok(
+                                               this === $elem.get( 0 ),
+                                               'Context of the callback is the 
DOM node to be animated.'
+                                       );
+                                       assert.ok(
+                                               animation === 
animationEventsAnimation,
+                                               'First argument ist the 
animation object which is set to the '
+                                                       + 'AnimationEvent 
instance\'s "animation" field in the callback '
+                                                       + 'given as 
animateWithEvent\'s last argument.'
+                                       );
+
+                               }
+                       }, function( animationEvent ) {
+                               animationEventsAnimation = 
animationEvent.animation;
+                       }
+               );
+       } );
+
+       QUnit.test( 'On jQuery set of multiple elements', function( assert ) {
+               var $elems = $( '<div/>' ).add( $( '<span/> ') ).add( $( 
'<div/> ') );
+               var $confirmedElems = $();
+               var animationEventInstances = [];
+
+               QUnit.stop();
+               $elems.animateWithEvent( 'fadesomethingin', 'fadeIn', function( 
animationEvent ) {
+                       var elem = animationEvent.animation.elem;
+                       $confirmedElems = $confirmedElems.add( elem );
+
+                       if( $.inArray( animationEvent, animationEventInstances 
) < 0 ) {
+                               animationEventInstances.push( animationEvent );
+                       }
+
+                       if( $confirmedElems.length >= $elems.length ) {
+                               QUnit.start();
+                       }
+               } );
+
+               assert.ok(
+                       $elems.length === $confirmedElems.length
+                               && $elems.not( $confirmedElems ).length === 0,
+                       'Initial callback got called for all ' + $elems.length 
+ ' elements of the jQuery set.'
+               );
+
+               assert.strictEqual(
+                       animationEventInstances.length,
+                       $elems.length,
+                       'Each callback got its own instance of 
jQuery.AnimationEvent.'
+               );
+       } );
+
+       QUnit.test( 'Error cases', function( assert ) {
+               assert.throws(
+                       function() {
+                               $( '<div/>').animateWithEvent(
+                                       'fooeventpurpose',
+                                       'fooAnimateFunction'
+                               );
+                       },
+                       'Can not use unknown animation function in arguments.'
+               );
+
+               assert.throws(
+                       function() {
+                               $( '<div/>').animateWithEvent();
+                       },
+                       'Throws error if called without parameters. At least 
event purpose has to be given.'
+               );
+       } );
+
+       QUnit.test( 'Two arguments are sufficient', function( assert ) {
+               var $node = $( '<div/>');
+               var result;
+
+               result = $node.animateWithEvent(
+                       'fooeventpurpose',
+                       { width: 200 }
+               );
+               assert.ok(
+                       result === $node,
+                       'Can call with only first two arguments'
+               );
+
+               result = $node.animateWithEvent(
+                       'xxxevent'
+               );
+               assert.ok(
+                       result === $node,
+                       'Can call with only first argument'
+               );
+       } );
+
+}( jQuery, QUnit ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/79146
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3be9cd0fefed0152bd3a14fc51c94ff0e11387a3
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/DataValues
Gerrit-Branch: master
Gerrit-Owner: Daniel Werner <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to