jenkins-bot has submitted this change and it was merged.
Change subject: ve.ui.MWTemplateDialog: Implement inferring of template data
......................................................................
ve.ui.MWTemplateDialog: Implement inferring of template data
Clean up of logic implemented during the template-sprint:
* Store spec inside the content model, directly associated
with the content-part. This allowed fixing the bug where
two spec-less template invocations overwrote each other's
made-up template data due to it using "target.wt" as key.
The opener now provides the fetcher with a "specId" which
is set to "part/<id>" for wt-generated template targets.
* Batching is now implemented inside the fetcher instead
of outside. This allows calling "getTemplateSpecs" inside
the loop with a dedicated callback for each spec to store
it in the content.parts[i] object passed by reference.
It also makes it easier to use by different code paths.
You call it as much as you like and it will queue up
naturally through javascript yielding and then make a batch
request. This is based on the pattern I used in MediaWiki core
for mw.loader#addEmbeddedCSS.
Follows-up e7af635, da679b7.
Change-Id: I4d7121229d060a96d927585c987a1a81a474b922
---
M modules/ve/ui/dialogs/ve.ui.MWTemplateDialog.js
1 file changed, 189 insertions(+), 65 deletions(-)
Approvals:
Catrope: Looks good to me, approved
jenkins-bot: Verified
diff --git a/modules/ve/ui/dialogs/ve.ui.MWTemplateDialog.js
b/modules/ve/ui/dialogs/ve.ui.MWTemplateDialog.js
index 3de1e65..eff6f6d 100644
--- a/modules/ve/ui/dialogs/ve.ui.MWTemplateDialog.js
+++ b/modules/ve/ui/dialogs/ve.ui.MWTemplateDialog.js
@@ -27,7 +27,9 @@
// Properties
this.node = null;
this.content = null;
- this.specs = {};
+ // Buffer for getTemplateSpecs
+ this.fetchQueue = [];
+ this.fetchCallbacks = $.Callbacks();
};
/* Inheritance */
@@ -50,77 +52,111 @@
* @method
*/
ve.ui.MWTemplateDialog.prototype.onOpen = function () {
- var i, len, template, title,
- templates = [];
+ var i, progress, len, template,
+ dialog = this;
- this.node = this.surface.getView().getFocusedNode();
- if ( !this.node ) {
+ function increaseProgress() {
+ progress++;
+ if ( progress === len ) {
+ dialog.setupPages();
+ }
+ }
+
+ function makeStoreTemplateSpec( template ) {
+ return function ( specs ) {
+ template.spec = specs[template.specId];
+ increaseProgress();
+ };
+ }
+
+ dialog.node = dialog.surface.getView().getFocusedNode();
+ if ( !dialog.node ) {
throw new Error( 'No focused node to edit' );
}
- // Get content values
- this.content = ve.copyObject( this.node.getModel().getAttribute( 'mw' )
);
+ // Get content values and copy it so we can safely change it to our
liking
+ dialog.content = ve.copyObject( dialog.node.getModel().getAttribute(
'mw' ) );
+
// Convert single template format to multiple template format
- if ( this.content.params ) {
- this.content = { 'parts': [ { 'template': this.content } ] };
+ if ( dialog.content.params ) {
+ dialog.content = {
+ 'parts': [
+ {
+ 'template': dialog.content
+ }
+ ]
+ };
}
- // Get all template data asynchronously
- for ( i = 0, len = this.content.parts.length; i < len; i++ ) {
- template = this.content.parts[i].template;
+
+ progress = -1;
+ len = dialog.content.parts.length;
+
+ // Get all template specs asynchronously
+ for ( i = 0; i < len; i++ ) {
+ template = dialog.content.parts[i].template;
if ( template ) {
- if ( template.target.url ) {
- try {
- title = new mw.Title(
template.target.url );
- templates.push( {
- 'title': title.toString(),
- 'params': template.params
- } );
- } catch ( e ) {}
- }
+ // Method #getTemplateSpecs will use the part id
instead of `target.url`
+ // if the target has no url property (which Parsoid
omits if the target is
+ // dynamically generated from wikitext). In that case
we want each template
+ // invocation to have its own inferred template spec.
+ template.specId = template.target.url || ( '#!/part/' +
i );
+ dialog.getTemplateSpecs( template,
makeStoreTemplateSpec( template ) );
} else {
- // Wrap plain wikitext in object so editor has
something to reference
- this.content.parts[i] = { 'wt': this.content.parts[i] };
+ // This is a raw wikitext part (between two associated
template invocations),
+ // wrap in object so editor has something to reference
+ dialog.content.parts[i] = { 'wt':
dialog.content.parts[i] };
+ increaseProgress();
}
}
- if ( templates.length ) {
- this.getTemplateData( templates )
- .done( ve.bind( function ( specs ) {
- this.specs = specs;
- }, this ) )
- .always( ve.bind( this.setupPages, this ) );
- } else {
- this.setupPages();
- }
+
+ increaseProgress();
};
/**
* Handle window close events.
*
- * @method
* @param {string} action Action that caused the window to be closed
*/
ve.ui.MWTemplateDialog.prototype.onClose = function ( action ) {
- var i, len, wt,
+ var i, len, parts,
surfaceModel = this.surface.getModel();
// Save changes
if ( action === 'apply' ) {
- // Expand wikitext content
- for ( i = 0, len = this.content.parts.length; i < len; i++ ) {
- wt = this.content.parts[i].wt;
- if ( typeof wt === 'string' ) {
- // Replace object wrapper with plain text
- this.content.parts[i] = wt;
+
+ // Undo non-standard changes we made to the content model in
#onOpen
+ parts = this.content.parts;
+
+ for ( i = 0, len = parts.length; i < len; i++ ) {
+
+ // Convert object part with wt property back to string
part
+ if ( typeof parts[i].wt === 'string' ) {
+ parts[i] = parts[i].wt;
+ }
+
+ // Remove the properties #onOpen put here
+ if ( parts[i].template ) {
+ if ( parts[i].template.spec ) {
+ delete parts[i].template.spec;
+ }
+ if ( parts[i].template.specId ) {
+ delete parts[i].template.specId;
+ }
}
}
+
// Restore single template format
if ( this.content.parts.length === 1 ) {
this.content = this.content.parts[0].template;
}
+
// TODO: Wrap attribute changes in ve.dm.SurfaceFragment
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChange(
- surfaceModel.getDocument(),
this.node.getOffset(), 'mw', this.content
+ surfaceModel.getDocument(),
+ this.node.getOffset(),
+ 'mw',
+ this.content
)
);
}
@@ -128,7 +164,6 @@
this.clearPages();
this.node = null;
this.content = null;
- this.specs = {};
// Parent method
ve.ui.PagedDialog.prototype.onClose.call( this );
@@ -142,8 +177,7 @@
ve.ui.MWTemplateDialog.prototype.setupPages = function () {
// Build pages from parts
var i, len, template, spec, param,
- parts = this.content.parts,
- specs = this.specs;
+ parts = this.content.parts;
// Parent method
ve.ui.PagedDialog.prototype.onOpen.call( this );
@@ -152,7 +186,7 @@
for ( i = 0, len = parts.length; i < len; i++ ) {
if ( parts[i].template ) {
template = parts[i].template;
- spec = specs[template.target.url];
+ spec = template.spec;
// Add template page
this.addTemplatePage( 'part_' + i, template );
// Add parameter pages
@@ -172,23 +206,111 @@
};
/**
- * Get a promise for template data.
- *
- * TODO: Backfill template info from params objects
- *
- * @method
- * @param {Object[]} templates Template information containing `title` and
`params` properties
- * @return {jQuery.Promise} Template data blob on success, or an error code on
failure
+ * Backfill missing template data based on template invocation.
+ * @param {Object} template Template invocation description
+ * @return {Object} Template data blob
*/
-ve.ui.MWTemplateDialog.prototype.getTemplateData = function ( templates ) {
+ve.ui.MWTemplateDialog.static.makeTemplateSpec = function ( params ) {
+ var key, blob;
+
+ blob = {
+ description: null,
+ params: {},
+ sets: []
+ };
+ for ( key in params ) {
+ blob.params[key] = {
+ 'label': {
+ en: key
+ },
+ 'required': false,
+ 'description': null,
+ 'deprecated': false,
+ 'aliases': [],
+ 'default': '',
+ 'type': 'string'
+
+ };
+ }
+ return blob;
+};
+
+/**
+ * Get template specs for one or more templates in the content model.
+ *
+ * @param {Object[]|undefined} templates List of template invocation
descriptions. Contains `title` and
+ * `params` properties. Or undefined to handle the queue built so far.
+ * @param {Function} callback
+ * @param {Object} callback.blobs Object containing template data blobs keyed
by page title.
+ */
+ve.ui.MWTemplateDialog.prototype.getTemplateSpecs = function ( templates,
callback ) {
+ var fillTemplateSpecs,
+ dialog = this;
+
+ // Yield once with setTimeout before fetching to allow batching
+ if ( callback ) {
+ dialog.fetchCallbacks.add( callback );
+ }
+ if ( templates ) {
+ templates = ve.isArray( templates ) ? templates : [ templates ];
+ // Push into the queue
+ dialog.fetchQueue.push.apply( dialog.fetchQueue, templates );
+ setTimeout( function () {
+ dialog.getTemplateSpecs();
+ } );
+ return;
+ } else if ( dialog.fetchQueue.length ) {
+ // Handle batch queue
+ templates = dialog.fetchQueue.slice();
+ dialog.fetchQueue.length = 0;
+ } else {
+ // This a delayed call but a previous delayed call already
+ // cleared the queue for us. This call has become redundant.
+ return;
+ }
+
+ fillTemplateSpecs = function ( specs ) {
+ var i, len, template, specId;
+ for ( i = 0, len = templates.length; i < len; i++ ) {
+ template = templates[i];
+ specId = template.specId;
+ if ( !specs[specId] ) {
+ specs[specId] =
dialog.constructor.static.makeTemplateSpec( template );
+ }
+ }
+ dialog.fetchCallbacks.fireWith( null, [ specs ] );
+ };
+
+ dialog.fetchTemplateSpecs( templates )
+ .done( fillTemplateSpecs )
+ .fail( function () {
+ fillTemplateSpecs( {} );
+ } );
+};
+
+/**
+ * Fetch template data from the TemplateData API.
+ *
+ * @param {Object[]} templates List of template invocation descriptions
+ * @return {jQuery.Promise}
+ */
+ve.ui.MWTemplateDialog.prototype.fetchTemplateSpecs = function ( templates ) {
var i, len,
+ d = $.Deferred(),
titles = [],
- specs = {},
- deferred = $.Deferred();
+ specs = {};
// Collect all titles
for ( i = 0, len = templates.length; i < len; i++ ) {
- titles.push( templates[i].title );
+ if ( templates[i].target.url ) {
+ titles.push( templates[i].target.url );
+ }
+ }
+
+ // Optimise for empty lists
+ if ( !templates.length ) {
+ setTimeout( d.reject );
+ return d.promise();
}
// Request template data from server
@@ -202,27 +324,31 @@
}
} )
.done( function ( data ) {
- var id;
+ var i, len, id;
if ( data && data.pages ) {
- // Add template data to spec
for ( id in data.pages ) {
specs[data.pages[id].title] =
data.pages[id];
}
- deferred.resolve( specs );
+ if ( data.normalized ) {
+ for ( i = 0, len =
data.normalized.length; i < len; i++ ) {
+ specs[ data.normalized[i].from
] = specs[ data.normalized[i].to ];
+ }
+ }
+ d.resolve( specs );
+ } else {
+ d.reject( 'unavailable', arguments );
}
- deferred.reject( 'unavailable', arguments );
} )
.fail( function () {
- deferred.reject( 'http', arguments );
+ d.reject( 'http', arguments );
} );
- return deferred.promise();
+ return d.promise();
};
/**
* Add page for wikitext.
*
- * @method
* @param {string} page Unique page name
* @param {Object} value Parameter value
*/
@@ -250,7 +376,6 @@
/**
* Add page for a template.
*
- * @method
* @param {string} page Unique page name
* @param {Object} template Template info
*/
@@ -271,7 +396,6 @@
/**
* Add page for a parameter.
*
- * @method
* @param {string} page Unique page name
* @param {string} name Parameter name
* @param {Object} value Parameter value
--
To view, visit https://gerrit.wikimedia.org/r/66101
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I4d7121229d060a96d927585c987a1a81a474b922
Gerrit-PatchSet: 6
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Krinkle <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits