jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/370504 )
Change subject: Template search: Add template autosuggest ability
......................................................................
Template search: Add template autosuggest ability
Add a TagMultiselectWidget subclass that can search the MediaWiki for
templates. Does not show suggestions for items already selected.
Issue: T165302
Change-Id: I9c1c6ca8c1b6c3fdb2d0c2fb100d8d1b4a38770d
---
M AdvancedSearch.hooks.php
M extension.json
M modules/dm/ext.advancedSearch.SearchModel.js
M modules/ext.advancedSearch.init.js
A modules/ui/ext.advancedSearch.TemplateSearch.js
A tests/qunit/ui/TemplateSearch.test.js
6 files changed, 387 insertions(+), 5 deletions(-)
Approvals:
Gabriel Birke: Looks good to me, approved
jenkins-bot: Verified
diff --git a/AdvancedSearch.hooks.php b/AdvancedSearch.hooks.php
index 98c6c95..2ffab78 100644
--- a/AdvancedSearch.hooks.php
+++ b/AdvancedSearch.hooks.php
@@ -79,12 +79,14 @@
'tests/qunit/ui/NamespaceFilters.test.js',
'tests/qunit/ui/NamespacePresets.test.js',
'tests/qunit/ui/SearchPreview.test.js',
+ 'tests/qunit/ui/TemplateSearch.test.js',
'tests/qunit/dm/SearchModel.test.js'
],
'dependencies' => [
'ext.advancedSearch.ui.NamespaceFilters',
'ext.advancedSearch.ui.NamespacePresets',
'ext.advancedSearch.ui.SearchPreview',
+ 'ext.advancedSearch.ui.TemplateSearch',
'ext.advancedSearch.dm.SearchModel',
'oojs-ui'
],
diff --git a/extension.json b/extension.json
index 615a3e2..f0a73bd 100644
--- a/extension.json
+++ b/extension.json
@@ -90,6 +90,7 @@
"ext.advancedSearch.ui.OptionalElementLayout",
"ext.advancedSearch.ui.OptionalElementLayout",
"ext.advancedSearch.ui.SearchPreview",
+ "ext.advancedSearch.ui.TemplateSearch",
"ext.advancedSearch.ui.TextInput"
]
},
@@ -197,6 +198,14 @@
"advancedSearch-filesize-smaller-than"
]
},
+ "ext.advancedSearch.ui.TemplateSearch": {
+ "scripts": [
+
"modules/ui/ext.advancedSearch.TemplateSearch.js"
+ ],
+ "dependencies": [
+ "oojs-ui"
+ ]
+ },
"ext.advancedSearch.ui.TextInput": {
"scripts": [
"modules/ui/ext.advancedSearch.TextInput.js"
diff --git a/modules/dm/ext.advancedSearch.SearchModel.js
b/modules/dm/ext.advancedSearch.SearchModel.js
index a137045..e34cbeb 100644
--- a/modules/dm/ext.advancedSearch.SearchModel.js
+++ b/modules/dm/ext.advancedSearch.SearchModel.js
@@ -65,7 +65,7 @@
// TODO check for allowed options?
- if ( OO.compare( this.searchOptions[ optionId ], value ) ) {
+ if ( this.searchOptions[ optionId ] !== undefined &&
OO.compare( this.searchOptions[ optionId ], value ) ) {
return;
}
diff --git a/modules/ext.advancedSearch.init.js
b/modules/ext.advancedSearch.init.js
index f34b979..e923804 100644
--- a/modules/ext.advancedSearch.init.js
+++ b/modules/ext.advancedSearch.init.js
@@ -162,11 +162,10 @@
return optionalQuotes( val );
},
init: function () {
- var widget = new
mw.libs.advancedSearch.ui.ArbitraryWordInput(
+ return new
mw.libs.advancedSearch.ui.ArbitraryWordInput(
state,
{ optionId: 'or' }
);
- return widget;
}
},
@@ -190,7 +189,14 @@
} ).join( ' ' );
}
return 'hastemplate:' + optionalQuotes( val );
- }
+ },
+ init: function () {
+ return new
mw.libs.advancedSearch.ui.TemplateSearch(
+ state,
+ { optionId: 'hastemplate' }
+ );
+ },
+ customEventHandling: true
},
{
group: 'structure',
@@ -348,7 +354,10 @@
} );
},
widget = widgetInit();
- widget.on( 'change', createMultiSelectChangeHandler( option.id
) );
+
+ if ( !option.customEventHandling ) {
+ widget.on( 'change', createMultiSelectChangeHandler(
option.id ) );
+ }
if ( !optionSets[ option.group ] ) {
optionSets[ option.group ] = new OO.ui.FieldsetLayout( {
diff --git a/modules/ui/ext.advancedSearch.TemplateSearch.js
b/modules/ui/ext.advancedSearch.TemplateSearch.js
new file mode 100644
index 0000000..dba7f90
--- /dev/null
+++ b/modules/ui/ext.advancedSearch.TemplateSearch.js
@@ -0,0 +1,156 @@
+( function ( mw, $ ) {
+ 'use strict';
+
+ mw.libs = mw.libs || {};
+ mw.libs.advancedSearch = mw.libs.advancedSearch || {};
+ mw.libs.advancedSearch.ui = mw.libs.advancedSearch.ui || {};
+
+ /**
+ * @class
+ * @extends {OO.ui.TagMultiselectWidget}
+ * @constructor
+ *
+ * @param {ext.advancedSearch.dm.SearchModel} store
+ * @param {Object} config
+ */
+ mw.libs.advancedSearch.ui.TemplateSearch = function ( store, config ) {
+ var myConfig = $.extend( {}, config || {}, {
+ allowArbitrary: true,
+ input: {
+ autocomplete: false
+ }
+ } );
+ this.store = store;
+ this.optionId = config.optionId;
+ this.api = config.api || new mw.Api();
+
+ this.store.connect( this, { update: 'onStoreUpdate' } );
+
+ mw.libs.advancedSearch.ui.TemplateSearch.parent.call( this,
myConfig );
+
+ this.$input = this.input.$input;
+
+ this.input.connect( this, { change: 'onLookupInputChange' } );
+
+ // Mixin constructor
+ OO.ui.mixin.LookupElement.call( this, myConfig );
+
+ this.populateFromStore();
+
+ this.connect( this, { change: 'onValueUpdate' } );
+ };
+
+ OO.inheritClass( mw.libs.advancedSearch.ui.TemplateSearch,
OO.ui.TagMultiselectWidget );
+ OO.mixinClass( mw.libs.advancedSearch.ui.TemplateSearch,
OO.ui.mixin.LookupElement );
+
+ mw.libs.advancedSearch.ui.TemplateSearch.prototype.onStoreUpdate =
function () {
+ this.populateFromStore();
+ };
+
+ mw.libs.advancedSearch.ui.TemplateSearch.prototype.populateFromStore =
function () {
+ // protect parent class from working with an undefined value
+ var value = this.store.getOption( this.optionId ) || [];
+
+ // avoid redundant event triggering if no value change is
performed
+ if ( OO.compare( value, this.getValue() ) ) {
+ return;
+ }
+
+ this.setValue( value );
+ };
+
+ /**
+ * Update external states on internal updates
+ */
+ mw.libs.advancedSearch.ui.TemplateSearch.prototype.onValueUpdate =
function () {
+ this.store.storeOption( this.optionId, this.getValue() );
+ };
+
+ /**
+ * @inheritdoc OO.ui.mixin.LookupElement
+ */
+ mw.libs.advancedSearch.ui.TemplateSearch.prototype.getLookupRequest =
function () {
+ var value = this.input.getValue();
+
+ // @todo More elegant way to prevent empty API requests?
+ if ( value.trim() === '' ) {
+ return $.Deferred().reject();
+ }
+
+ return this.api.get( {
+ action: 'opensearch',
+ search: this.input.getValue(),
+ namespace: 10
+ } );
+ };
+
+ /**
+ * @inheritdoc OO.ui.mixin.LookupElement
+ */
+
mw.libs.advancedSearch.ui.TemplateSearch.prototype.getLookupCacheDataFromResponse
= function ( response ) {
+ return response || [];
+ };
+
+ /**
+ * @inheritdoc OO.ui.mixin.LookupElement
+ */
+
mw.libs.advancedSearch.ui.TemplateSearch.prototype.getLookupMenuOptionsFromData
= function ( data ) {
+ var
+ items = [],
+ i, templateNameWithoutNamespace,
+ currentValues = this.getValue();
+ for ( i = 0; i < data[ 1 ].length; i++ ) {
+ templateNameWithoutNamespace = this.removeNamespace(
data[ 1 ][ i ] );
+
+ // do not show suggestions for items already selected
+ if ( currentValues.indexOf(
templateNameWithoutNamespace ) !== -1 ) {
+ continue;
+ }
+
+ items.push( new OO.ui.MenuOptionWidget( {
+ data: templateNameWithoutNamespace,
+ label: templateNameWithoutNamespace
+ } ) );
+ }
+ return items;
+ };
+
+ /**
+ * Get the name part of a page title containing a namespace
+ *
+ * @param {string} pageTitle
+ * @return {string}
+ */
+ mw.libs.advancedSearch.ui.TemplateSearch.prototype.removeNamespace =
function ( pageTitle ) {
+ return mw.Title.newFromText( pageTitle ).getNameText();
+ };
+
+ /**
+ * Override behavior from OO.ui.mixin.LookupElement
+ *
+ * @param {OO.ui.TagItemWidget} item
+ */
+
mw.libs.advancedSearch.ui.TemplateSearch.prototype.onLookupMenuItemChoose =
function ( item ) {
+ this.addTag( item.getData() );
+ this.input.setValue( '' );
+ };
+
+ /**
+ * Override to make sure query caching is based on the correct (input)
value
+ *
+ * @inheritdoc
+ */
+ mw.libs.advancedSearch.ui.TemplateSearch.prototype.getRequestQuery =
function () {
+ return this.input.getValue();
+ };
+
+ /**
+ * Implemented because OO.ui.mixin.LookupElement expects it.
+ *
+ * @returns {boolean}
+ */
+ mw.libs.advancedSearch.ui.TemplateSearch.prototype.isReadOnly =
function () {
+ return false;
+ };
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/ui/TemplateSearch.test.js
b/tests/qunit/ui/TemplateSearch.test.js
new file mode 100644
index 0000000..86d1f66
--- /dev/null
+++ b/tests/qunit/ui/TemplateSearch.test.js
@@ -0,0 +1,206 @@
+( function ( $, QUnit, sinon, mw ) {
+ 'use strict';
+
+ var TemplateSearch,
+ sandbox,
+ store,
+ config;
+
+ QUnit.testStart( function () {
+ TemplateSearch = mw.libs.advancedSearch.ui.TemplateSearch;
+ sandbox = sinon.sandbox.create();
+ store = {
+ connect: sandbox.stub(),
+ getOption: sandbox.stub().withArgs( 'hastemplate'
).returns( [] ),
+ storeOption: sandbox.stub()
+ };
+ config = {
+ optionId: 'hastemplate'
+ };
+ } );
+
+ QUnit.testDone( function () {
+ sandbox.restore();
+ } );
+
+ QUnit.module( 'ext.advancedSearch.ui.TemplateSearch' );
+
+ QUnit.test( 'Store data subscribed to and synced initially', 4,
function ( assert ) {
+ var setValueSpy = sandbox.spy( TemplateSearch.prototype,
'setValue' );
+ store.getOption.withArgs( 'hastemplate' ).returns( [ 'Burg' ] );
+
+ var templateSearch = new TemplateSearch( store, config );
+
+ assert.ok( templateSearch );
+ assert.ok( store.connect.calledOnce );
+ assert.ok( setValueSpy.withArgs( [ 'Burg' ] ).calledOnce );
+ assert.deepEqual( templateSearch.getValue(), [ 'Burg' ] );
+ } );
+
+ QUnit.test( 'Store update is applied', 1, function ( assert ) {
+ store.getOption.withArgs( 'hastemplate' ).returns( [ 'from',
'beyond' ] );
+
+ var templateSearch = new TemplateSearch( store, config );
+
+ templateSearch.onStoreUpdate();
+
+ assert.deepEqual( templateSearch.getValue(), [ 'from', 'beyond'
] );
+ } );
+
+ QUnit.test( 'Mixin method overridden to prevent problems', 1, function
( assert ) {
+ var templateSearch = new TemplateSearch( store, config );
+ assert.notOk( templateSearch.isReadOnly() );
+ } );
+
+ QUnit.test( 'API response processed correctly', 8, function ( assert ) {
+ var templateSearch = new TemplateSearch( store, config );
+
+ var apiData = [
+ 'j',
+ [
+ 'Template:Jochen',
+ 'Template:Jens',
+ 'Template:Johannes'
+ ],
+ [
+ '',
+ '',
+ ''
+ ],
+ [
+ 'http://mywiki/index.php?title=Template:Jochen',
+ 'http://mywiki/index.php?title=Template:Jens',
+
'http://mywiki/index.php?title=Template:Johannes'
+ ]
+ ];
+
+ var result = templateSearch.getLookupMenuOptionsFromData(
apiData );
+ assert.ok( $.isArray( result ) );
+ assert.equal( result.length, 3 );
+
+ assert.equal( result[ 0 ].getLabel(), 'Jochen' );
+ assert.equal( result[ 0 ].getData(), 'Jochen' );
+
+ assert.equal( result[ 1 ].getLabel(), 'Jens' );
+ assert.equal( result[ 1 ].getData(), 'Jens' );
+
+ assert.equal( result[ 2 ].getLabel(), 'Johannes' );
+ assert.equal( result[ 2 ].getData(), 'Johannes' );
+ } );
+
+ QUnit.test( 'Items already selected are not suggested', 4, function (
assert ) {
+ var templateSearch = new TemplateSearch( store, config );
+ templateSearch.setValue( [ 'Jochen', 'Johannes' ] );
+ var apiData = [
+ 'j',
+ [
+ 'Template:Jochen',
+ 'Template:Jens',
+ 'Template:Johannes'
+ ],
+ [
+ '',
+ '',
+ ''
+ ],
+ [
+ 'http://mywiki/index.php?title=Template:Jochen',
+ 'http://mywiki/index.php?title=Template:Jens',
+
'http://mywiki/index.php?title=Template:Johannes'
+ ]
+ ];
+
+ var result = templateSearch.getLookupMenuOptionsFromData(
apiData );
+ assert.ok( $.isArray( result ) );
+ assert.equal( result.length, 1 );
+
+ assert.equal( result[ 0 ].getLabel(), 'Jens' );
+ assert.equal( result[ 0 ].getData(), 'Jens' );
+ } );
+
+ QUnit.test( 'Page titles post-processes nicely', 2, function ( assert )
{
+ var templateSearch = new TemplateSearch( store, config );
+ assert.equal( templateSearch.removeNamespace( 'Test' ), 'Test'
);
+ assert.equal( templateSearch.removeNamespace( 'Template:Test'
), 'Test' );
+ } );
+
+ QUnit.test( 'Value picked from menu is added to tags and stored', 6,
function ( assert ) {
+ var templateSearch = new TemplateSearch( store, config );
+ templateSearch.addTag( 'Preexisting' );
+ templateSearch.input.setValue( 'My Templ' );
+ var item = new OO.ui.TagItemWidget();
+ item.setData( 'My Template' );
+
+ // reset storeOption as is was invoked by addTag( 'Preexisting'
) before
+ store.storeOption = sandbox.stub();
+
+ templateSearch.onLookupMenuItemChoose( item );
+
+ var tags = templateSearch.getItems();
+ assert.ok( $.isArray( tags ) );
+ assert.equal( tags.length, 2 );
+ assert.equal( tags[ 0 ].getData(), 'Preexisting' );
+ assert.equal( tags[ 1 ].getData(), 'My Template' );
+
+ assert.ok( store.storeOption.withArgs( 'hastemplate', [
'Preexisting', 'My Template' ] ).calledOnce );
+
+ assert.equal( templateSearch.input.getValue(), '' );
+ } );
+
+ QUnit.test( 'Native browser autocomplete is not used', 1, function (
assert ) {
+ var templateSearch = new TemplateSearch( store, config );
+
+ assert.equal( $( templateSearch.$input ).attr( 'autocomplete'
), 'off' );
+ } );
+
+ QUnit.test( 'Well-formed API request yields result', 4, function (
assert ) {
+ config.api = new mw.Api();
+ var getStub = sandbox.stub( config.api, 'get' ).withArgs( {
+ action: 'opensearch',
+ search: 'Burg',
+ namespace: 10
+ } ).returns( $.Deferred().resolve( [
+ 'Burg',
+ [
+ 'Template:Burg'
+ ],
+ [
+ ''
+ ],
+ [
+ 'http://mywiki/index.php?title=Template:Burg'
+ ]
+ ] ).promise() );
+
+ var templateSearch = new TemplateSearch( store, config );
+ $( templateSearch.$input ).val( 'Burg' );
+
+ var result = templateSearch.getLookupRequest();
+
+ assert.ok( getStub.calledOnce );
+
+ assert.equal( result.state(), 'resolved' );
+ result.done( function ( doneData ) {
+ assert.equal( doneData[ 0 ], 'Burg' );
+ assert.deepEqual( doneData[ 1 ], [ 'Template:Burg' ] );
+ } );
+ } );
+
+ QUnit.test( 'Empty query does not trigger API request', 3, function (
assert ) {
+ config.api = new mw.Api();
+ var getStub = sandbox.stub( config.api, 'get' );
+
+ var templateSearch = new TemplateSearch( store, config );
+ $( templateSearch.$input ).val( '' );
+
+ var result = templateSearch.getLookupRequest();
+
+ assert.notOk( getStub.called );
+
+ assert.equal( result.state(), 'rejected' );
+ result.fail( function () {
+ assert.ok( true, 'A failed promise is returned' );
+ } );
+ } );
+
+}( jQuery, QUnit, sinon, mediaWiki ) );
--
To view, visit https://gerrit.wikimedia.org/r/370504
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I9c1c6ca8c1b6c3fdb2d0c2fb100d8d1b4a38770d
Gerrit-PatchSet: 21
Gerrit-Project: mediawiki/extensions/AdvancedSearch
Gerrit-Branch: master
Gerrit-Owner: Gabriel Birke <[email protected]>
Gerrit-Reviewer: Gabriel Birke <[email protected]>
Gerrit-Reviewer: Jeroen De Dauw <[email protected]>
Gerrit-Reviewer: Kai Nissen (WMDE) <[email protected]>
Gerrit-Reviewer: Pablo Grass (WMDE) <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits