Mooeypoo has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/350763 )

Change subject: [wip] Add a 'saved queries' quick filters feature
......................................................................

[wip] Add a 'saved queries' quick filters feature

Change-Id: I5cede87633147736d3b4ee5b8ea178ae21bd441f
---
M includes/Preferences.php
M languages/i18n/en.json
M languages/i18n/qqq.json
M resources/Resources.php
A resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js
A resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
A 
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less
A resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less
A 
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less
M 
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
M resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
A 
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js
16 files changed, 827 insertions(+), 7 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/63/350763/1

diff --git a/includes/Preferences.php b/includes/Preferences.php
index b428e87..4017619 100644
--- a/includes/Preferences.php
+++ b/includes/Preferences.php
@@ -915,6 +915,9 @@
                        'label-message' => 'tog-hideminor',
                        'section' => 'rc/advancedrc',
                ];
+               $defaultPreferences['rcfilters-saved-queries'] = [
+                       'type' => 'api',
+               ];
 
                if ( $config->get( 'RCWatchCategoryMembership' ) ) {
                        $defaultPreferences['hidecategorization'] = [
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 8129205..92586a9 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -1365,6 +1365,11 @@
        "recentchanges-legend-plusminus": "(<em>±123</em>)",
        "recentchanges-submit": "Show",
        "rcfilters-activefilters": "Active filters",
+       "rcfilters-quickfilters": "Quick links",
+       "rcfilters-savedqueries-defaultlabel": "Saved filters",
+       "rcfilters-savedqueries-rename": "Rename",
+       "rcfilters-savedqueries-setdefault": "Set as default",
+       "rcfilters-savedqueries-remove": "Remove",
        "rcfilters-restore-default-filters": "Restore default filters",
        "rcfilters-clear-all-filters": "Clear all filters",
        "rcfilters-search-placeholder": "Filter recent changes (browse or start 
typing)",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 88f6ead..2286e19 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -1553,6 +1553,11 @@
        "recentchanges-legend-plusminus": "{{optional}}\nA plus/minus sign with 
a number for the legend.",
        "recentchanges-submit": "Label for submit button in 
[[Special:RecentChanges]]\n{{Identical|Show}}",
        "rcfilters-activefilters": "Title for the filters selection showing the 
active filters.",
+       "rcfilters-quickfilters": "Label for the button that opens the quick 
filters menu in [[Special:RecentChanges]]",
+       "rcfilters-savedqueries-defaultlabel": "Default name for saving a new 
set of quick filters [[Special:RecentChanges]]",
+       "rcfilters-savedqueries-rename": "Label for the menu option that edits 
a quick filter in [[Special:RecentChanges]]",
+       "rcfilters-savedqueries-setdefault": "Label for the menu option that 
sets a quick filter as default in [[Special:RecentChanges]]",
+       "rcfilters-savedqueries-remove": "Label for the menu option that 
removes a quick filter as default in [[Special:RecentChanges]]",
        "rcfilters-restore-default-filters": "Label for the button that resets 
filters to defaults",
        "rcfilters-clear-all-filters": "Title for the button that clears all 
filters",
        "rcfilters-search-placeholder": "Placeholder for the filter search 
input.",
diff --git a/resources/Resources.php b/resources/Resources.php
index 1721de8..4e03398 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1746,6 +1746,8 @@
                        
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js',
                        
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js',
                        
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js',
+                       
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js',
+                       
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js',
                        
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js',
                        
'resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js',
                ],
@@ -1767,6 +1769,9 @@
                        
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FloatingMenuSelectWidget.js',
                        
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
                        
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
+                       
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js',
+                       
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js',
+                       
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js',
                        
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js',
                        
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js',
                        
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js',
@@ -1789,6 +1794,9 @@
                        
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
                        
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
                        
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less',
+                       
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less',
+                       
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less',
+                       
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less',
                ],
                'skinStyles' => [
                        'monobook' => [
@@ -1798,6 +1806,11 @@
                ],
                'messages' => [
                        'rcfilters-activefilters',
+                       'rcfilters-quickfilters',
+                       'rcfilters-savedqueries-defaultlabel',
+                       'rcfilters-savedqueries-rename',
+                       'rcfilters-savedqueries-setdefault',
+                       'rcfilters-savedqueries-remove',
                        'rcfilters-restore-default-filters',
                        'rcfilters-clear-all-filters',
                        'rcfilters-search-placeholder',
diff --git 
a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js 
b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js
new file mode 100644
index 0000000..d5df0dc
--- /dev/null
+++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js
@@ -0,0 +1,149 @@
+( function ( mw, $ ) {
+       /**
+        * View mdel for saved queries
+        *
+        * @mixins OO.EventEmitter
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        */
+       mw.rcfilters.dm.SavedQueriesModel = function 
MwRcfiltersDmSavedQueriesModel( config ) {
+               config = config || {};
+
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+               OO.EmitterList.call( this );
+
+               this.default = null;
+
+               // Events
+               this.aggregate( { update: 'queryItemUpdate' } );
+               this.connect( this, { queryItemUpdate: [ 'emit', 'itemUpdate' ] 
} );
+       };
+
+       /* Initialization */
+
+       OO.initClass( mw.rcfilters.dm.SavedQueriesModel );
+       OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EventEmitter );
+       OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EmitterList );
+
+       /* Events */
+
+       /**
+        * @event initialize
+        *
+        * Model is initialized
+        */
+
+       /**
+        * @event default
+        * @param {string} QueryID Query identifier
+        *
+        * Default value has changed
+        */
+
+       /* Methods */
+
+       /**
+        * Initialize the saved queries model by reading it from the user's 
settings.
+        * The structure of the saved queries is:
+        * {
+        *    query_id_1: {
+        *       data:{
+        *          params: (Object) Definition of the parameters
+        *          highlights: (Object) Definition of the highlights
+        *       },
+        *       label: (optional) Name of this query
+        *    }
+        * }
+        *
+        * @fires initialize
+        */
+       mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function () {
+               var savedQueries = JSON.parse( mw.user.options.get( 
'rcfilters-saved-queries' ) ) || {},
+                       items = [];
+
+               this.clearItems();
+               $.each( savedQueries, function ( id, data ) {
+                       items.push(
+                               new mw.rcfilters.dm.SavedQueryItemModel(
+                                       id,
+                                       data.label,
+                                       data.data
+                               )
+                       );
+               } );
+
+               this.addItems( items );
+
+               this.emit( 'initialize' );
+       };
+
+       /**
+        * Get query by its identifier
+        *
+        * @param {string} queryID Query identifier
+        * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
+        *  the search. Undefined if not found.
+        */
+       mw.rcfilters.dm.SavedQueriesModel.prototype.getItemByID = function ( 
queryID ) {
+               return this.getItems().filter( function ( item ) {
+                       return item.getID() === queryID;
+               } )[ 0 ];
+       };
+
+       /**
+        * Get the object representing the state of the entire model and items
+        *
+        * @return {Object} Object representing the state of the model and items
+        */
+       mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () {
+               var obj = {};
+
+               // Translate the items to the saved object
+               this.getItems().forEach( function ( item ) {
+                       obj[ item.getID() ] = item.getState();
+               } );
+
+               if ( this.getDefault() ) {
+                       obj.default = this.getDefault();
+               }
+
+               return obj;
+       };
+
+       /**
+        * Set a default query. Null to unset default.
+        *
+        * @param {string} itemID Query identifier
+        * @fires default
+        */
+       mw.rcfilters.dm.SavedQueriesModel.prototype.setDefault = function ( 
itemID ) {
+               if ( this.default !== itemID ) {
+                       this.default = itemID;
+                       this.emit( 'default', this.default );
+               }
+       };
+
+       /**
+        * Get the default query ID
+        *
+        * @return {string} Default query identifier
+        */
+       mw.rcfilters.dm.SavedQueriesModel.prototype.getDefault = function () {
+               return this.default;
+       };
+
+       /**
+        * Save the state in the user settings
+        */
+       mw.rcfilters.dm.SavedQueriesModel.prototype.save = function () {
+               var stringified = JSON.stringify( this.getState() );
+
+               // Save the preference in general
+               new mw.Api().saveOption( 'rcfilters-saved-queries', stringified 
);
+               // Save the preference for this session
+               mw.user.options.set( 'rcfilters-saved-queries', stringified );
+       };
+
+}( mediaWiki, jQuery ) );
diff --git 
a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js 
b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js
new file mode 100644
index 0000000..35067aa
--- /dev/null
+++ 
b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js
@@ -0,0 +1,67 @@
+( function ( mw, $ ) {
+       /**
+        * View model for a single saved query
+        *
+        * @mixins OO.EventEmitter
+        *
+        * @constructor
+        * @param {string} id Unique identifier
+        * @param {string} label Saved query label
+        * @param {Object} data Saved query data
+        * @param {Object} [config] Configuration options
+        */
+       mw.rcfilters.dm.SavedQueryItemModel = function 
MwRcfiltersDmSavedQueriesModel( id, label, data, config ) {
+               config = config || {};
+
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+
+               this.id = id;
+               this.label = label || mw.msg( 
'rcfilters-savedqueries-defaultlabel' );
+               this.data = data;
+       };
+
+       /* Initialization */
+
+       OO.initClass( mw.rcfilters.dm.SavedQueryItemModel );
+       OO.mixinClass( mw.rcfilters.dm.SavedQueryItemModel, OO.EventEmitter );
+
+       /* Events */
+
+       /**
+        * @update
+        *
+        * Model has been updated
+        */
+
+       /* Methods */
+
+       /**
+        * Get an object representing the state of this item
+        */
+       mw.rcfilters.dm.SavedQueryItemModel.prototype.getState = function () {
+               return {
+                       data: this.getData(),
+                       label: this.getLabel()
+               };
+       };
+
+       mw.rcfilters.dm.SavedQueryItemModel.prototype.getID = function () {
+               return this.id;
+       };
+
+       mw.rcfilters.dm.SavedQueryItemModel.prototype.getLabel = function () {
+               return this.label;
+       };
+
+       mw.rcfilters.dm.SavedQueryItemModel.prototype.updateLabel = function ( 
newLabel ) {
+               if ( this.label !== newLabel ) {
+                       this.label = newLabel;
+                       this.emit( 'update' );
+               }
+       };
+
+       mw.rcfilters.dm.SavedQueryItemModel.prototype.getData = function () {
+               return this.data;
+       };
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js 
b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
index 669420c..bcb59bb 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
@@ -4,10 +4,12 @@
         *
         * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view 
model
         * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel 
Changes list view model
+        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved 
queries model
         */
-       mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, 
changesListModel ) {
+       mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, 
changesListModel, savedQueriesModel ) {
                this.filtersModel = filtersModel;
                this.changesListModel = changesListModel;
+               this.savedQueriesModel = savedQueriesModel;
                this.requestCounter = 0;
        };
 
@@ -23,6 +25,7 @@
                var $changesList = $( '.mw-changeslist' ).first().contents();
                // Initialize the model
                this.filtersModel.initializeFilters( filterStructure );
+               this.savedQueriesModel.initialize();
                this.updateStateBasedOnUrl();
 
                // Update the changes list with the existing data
@@ -31,7 +34,6 @@
                        $changesList.length ? $changesList : 'NO_RESULTS',
                        $( 'fieldset.rcoptions' ).first()
                );
-
        };
 
        /**
@@ -341,4 +343,49 @@
                        }
                );
        };
+
+       mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label ) 
{
+               var randomID = ( new Date() ).getTime();
+
+               label = label || mw.msg( 'rcfilters-savedqueries-defaultname' );
+
+               this.savedQueriesModel.addItems( [
+                       new mw.rcfilters.dm.SavedQueryItemModel(
+                               randomID,
+                               label,
+                               // Data
+                               {
+                                       params: 
this.filtersModel.getParametersFromFilters(),
+                                       highlights: 
this.filtersModel.getHighlightParameters()
+                               }
+                       )
+               ] );
+
+               this.savedQueriesModel.save();
+       };
+
+       mw.rcfilters.Controller.prototype.removeSavedQuery = function ( queryID 
) {
+               var query = this.savedQueriesModel.getItemByID( queryID );
+
+               this.savedQueriesModel.removeItems( [ query ] );
+               this.savedQueriesModel.save();
+       };
+
+       mw.rcfilters.Controller.prototype.renameSavedQuery = function ( 
queryID, newLabel ) {
+               var queryItem = this.savedQueriesModel.getItemByID( queryID );
+
+               if ( queryItem ) {
+                       queryItem.updateLabel( newLabel );
+               }
+               this.savedQueriesModel.save();
+       };
+
+       mw.rcfilters.Controller.prototype.loadSavedQuery = function ( queryID ) 
{
+               var queryItem = this.savedQueriesModel.getItemByID( queryID ),
+                       data = queryItem.getData();
+
+               this.updateChangesList( $.extend( {}, data.params, 
data.highlights ) );
+               this.updateStateBasedOnUrl();
+       };
+
 }( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js 
b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
index 4a586e4..dd8fae0 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
@@ -11,11 +11,12 @@
                init: function () {
                        var filtersModel = new 
mw.rcfilters.dm.FiltersViewModel(),
                                changesListModel = new 
mw.rcfilters.dm.ChangesListViewModel(),
-                               controller = new mw.rcfilters.Controller( 
filtersModel, changesListModel ),
+                               savedQueriesModel = new 
mw.rcfilters.dm.SavedQueriesModel(),
+                               controller = new mw.rcfilters.Controller( 
filtersModel, changesListModel, savedQueriesModel ),
                                $overlay = $( '<div>' )
                                        .addClass( 'mw-rcfilters-ui-overlay' ),
                                filtersWidget = new 
mw.rcfilters.ui.FilterWrapperWidget(
-                                       controller, filtersModel, { $overlay: 
$overlay } );
+                                       controller, filtersModel, 
savedQueriesModel, { $overlay: $overlay } );
 
                        // TODO: The changesListWrapperWidget should be able to 
initialize
                        // after the model is ready.
diff --git 
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less
 
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less
new file mode 100644
index 0000000..0d9add6
--- /dev/null
+++ 
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less
@@ -0,0 +1,39 @@
+.mw-rcfilters-ui-quickLinkMenuOptionWidget {
+       padding: 0.5em;
+       &:not( :last-child ) {
+               border-bottom: 1px solid #ccc;
+       }
+
+       .mw-rcfilters-ui-cell {
+               vertical-align: middle;
+       }
+
+       &-icon span {
+               display: inline-block;
+       }
+
+       &-input {
+               display: inline-block;
+               width: 15em;
+       }
+
+       &-label {
+               max-width: 15em;
+               display: inline-block;
+               vertical-align: middle;
+               text-overflow: ellipsis;
+               overflow: hidden;
+               cursor: pointer !important;
+               margin-left: 0.5px;
+       }
+
+       &-icon,
+       &-button {
+               width: 2em;
+       }
+
+       &-content {
+               width: 100%;
+       }
+
+}
diff --git 
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less
 
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less
new file mode 100644
index 0000000..c5436d6
--- /dev/null
+++ 
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less
@@ -0,0 +1,7 @@
+.mw-rcfilters-ui-quickLinksWidget {
+       float: right;
+
+       &-menu {
+               width: 100%;
+       }
+}
diff --git 
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less
 
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less
new file mode 100644
index 0000000..9b754fa
--- /dev/null
+++ 
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less
@@ -0,0 +1,22 @@
+.mw-rcfilters-ui-saveQuickLinkWidget {
+       &-popup {
+               &-layout {
+                       padding-bottom: 1.5em;
+               }
+
+               & > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-head {
+                       & > .oo-ui-iconWidget {
+                               margin: 0.75em 0.5em;
+                               float: left;
+                       }
+
+                       & > .oo-ui-labelElement-label {
+                               font-size: 1.2em;
+                               padding: 0.3em;
+                               margin-left: 0;
+                               font-weight: bold;
+                       }
+               }
+       }
+
+}
diff --git 
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
index 4192aad..a6e1c66 100644
--- 
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
+++ 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
@@ -8,10 +8,11 @@
         * @constructor
         * @param {mw.rcfilters.Controller} controller Controller
         * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved 
queries model
         * @param {Object} config Configuration object
         * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for 
popups
         */
-       mw.rcfilters.ui.FilterTagMultiselectWidget = function 
MwRcfiltersUiFilterTagMultiselectWidget( controller, model, config ) {
+       mw.rcfilters.ui.FilterTagMultiselectWidget = function 
MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, 
config ) {
                var title = new OO.ui.LabelWidget( {
                                label: mw.msg( 'rcfilters-activefilters' ),
                                classes: [ 
'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
@@ -23,6 +24,7 @@
 
                this.controller = controller;
                this.model = model;
+               this.queriesModel = savedQueriesModel;
                this.$overlay = config.$overlay || this.$element;
 
                // Parent
@@ -60,6 +62,11 @@
                        classes: [ 
'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
                } );
 
+               this.saveQueryButton = new mw.rcfilters.ui.SaveQuickLinkWidget(
+                       this.controller,
+                       this.savedQueriesModel
+               );
+
                this.emptyFilterMessage = new OO.ui.LabelWidget( {
                        label: mw.msg( 'rcfilters-empty-filter' ),
                        classes: [ 
'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
@@ -71,6 +78,7 @@
                // Stop propagation for mousedown, so that the widget doesn't
                // trigger the focus on the input and scrolls up when we click 
the reset button
                this.resetButton.$element.on( 'mousedown', function ( e ) { 
e.stopPropagation(); } );
+               this.saveQueryButton.$element.on( 'mousedown', function ( e ) { 
e.stopPropagation(); } );
                this.model.connect( this, {
                        initialize: 'onModelInitialize',
                        itemUpdate: 'onModelItemUpdate',
@@ -96,6 +104,10 @@
                                                                .addClass( 
'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' ),
                                                        $( '<div>' )
                                                                .addClass( 
'mw-rcfilters-ui-cell' )
+                                                               .addClass( 
'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
+                                                               .append( 
this.saveQueryButton.$element ),
+                                                       $( '<div>' )
+                                                               .addClass( 
'mw-rcfilters-ui-cell' )
                                                                .addClass( 
'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
                                                                .append( 
this.resetButton.$element )
                                                )
diff --git 
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
index b7ebf34..d9b7fab 100644
--- 
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
+++ 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
@@ -8,11 +8,12 @@
         * @constructor
         * @param {mw.rcfilters.Controller} controller Controller
         * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved 
queries model
         * @param {Object} [config] Configuration object
         * @cfg {Object} [filters] A definition of the filter groups in this 
list
         * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for 
popups
         */
-       mw.rcfilters.ui.FilterWrapperWidget = function 
MwRcfiltersUiFilterWrapperWidget( controller, model, config ) {
+       mw.rcfilters.ui.FilterWrapperWidget = function 
MwRcfiltersUiFilterWrapperWidget( controller, model, savedQueriesModel, config 
) {
                config = config || {};
 
                // Parent
@@ -22,18 +23,29 @@
 
                this.controller = controller;
                this.model = model;
+               this.queriesModel = savedQueriesModel;
                this.$overlay = config.$overlay || this.$element;
 
                this.filterTagWidget = new 
mw.rcfilters.ui.FilterTagMultiselectWidget(
                        this.controller,
                        this.model,
+                       this.savedQueriesModel,
+                       { $overlay: this.$overlay }
+               );
+
+               this.quickLinksWidget = new mw.rcfilters.ui.QuickLinksWidget(
+                       this.controller,
+                       this.queriesModel,
                        { $overlay: this.$overlay }
                );
 
                // Initialize
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
-                       .append( this.filterTagWidget.$element );
+                       .append(
+                               this.quickLinksWidget.$element,
+                               this.filterTagWidget.$element
+                       );
        };
 
        /* Initialization */
diff --git 
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js
 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js
new file mode 100644
index 0000000..0281032
--- /dev/null
+++ 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js
@@ -0,0 +1,216 @@
+( function ( mw ) {
+       /**
+        * Quick links menu option widget
+        *
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.LabelElement
+        * @mixins OO.ui.mixin.IconElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.SavedQueryItemModel} model View model
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for 
popups
+        */
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget = function 
MwRcfiltersUiQuickLinksWidget( model, config ) {
+               config = config || {};
+
+               this.model = model;
+
+               // Parent
+               mw.rcfilters.ui.QuickLinkMenuOptionWidget.parent.call( this, 
$.extend( {
+                       data: this.model.getID()
+               }, config ) );
+
+               // Mixin constructors
+               OO.ui.mixin.LabelElement.call( this, $.extend( {
+                       label: this.model.getLabel()
+               }, config ) );
+               OO.ui.mixin.IconElement.call( this, $.extend( {
+                       icon: 'bookmark'
+               }, config ) );
+
+               this.edit = false;
+               this.$overlay = config.$overlay || this.$element;
+
+               this.popupButton = new OO.ui.ButtonWidget( {
+                       classes: [ 
'mw-rcfilters-ui-quickLinkMenuOptionWidget-button' ],
+                       icon: 'ellipsis',
+                       framed: false
+               } );
+               this.menu = new OO.ui.FloatingMenuSelectWidget( {
+                       classes: [ 
'mw-rcfilters-ui-quickLinkMenuOptionWidget-menu' ],
+                       widget: this.popupButton,
+                       width: 200,
+                       horizontalPosition: 'end',
+                       $container: this.popupButton.$element,
+                       $autoCloseIgnore: this.$overlay,
+                       items: [
+                               new OO.ui.MenuOptionWidget( {
+                                       data: 'edit',
+                                       icon: 'edit',
+                                       label: mw.msg( 
'rcfilters-savedqueries-rename' )
+                               } ),
+                               new OO.ui.MenuOptionWidget( {
+                                       data: 'delete',
+                                       icon: 'close',
+                                       label: mw.msg( 
'rcfilters-savedqueries-remove' )
+                               } ),
+                               new OO.ui.MenuOptionWidget( {
+                                       data: 'default',
+                                       icon: 'heart',
+                                       label: mw.msg( 
'rcfilters-savedqueries-setdefault' )
+                               } ),
+                       ]
+               } );
+
+               this.editInput = new OO.ui.TextInputWidget( {
+                       classes: [ 
'mw-rcfilters-ui-quickLinkMenuOptionWidget-input' ]
+               } );
+               this.saveButton = new OO.ui.ButtonWidget( {
+                       icon: 'check',
+                       flags: [ 'primary', 'progressive' ]
+               } );
+               this.toggleEdit( false );
+
+               // Events
+               this.model.connect( this, { update: 'onModelUpdate' } );
+               this.popupButton.connect( this, { click: 'onPopupButtonClick' } 
);
+               this.menu.connect( this, {
+                       choose: 'onMenuChoose'
+               } );
+               this.saveButton.connect( this, { click: 'onSaveButtonClick' } );
+               this.editInput.$input.on( {
+                       blur: this.onInputBlur.bind( this ),
+                       keypress: this.onInputKeypress.bind( this ),
+                       keyup: this.onInputKeyup.bind( this )
+               } );
+               this.$element.on( { click: this.emit.bind( this, 'click' ) } );
+               this.$label.on( { click: this.emit.bind( this, 'click' ) } );
+
+               // Initialize
+               this.$overlay.append( this.menu.$element );
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget' )
+                       .addClass( 
'mw-rcfilters-ui-quickLinkMenuOptionWidget-query-' + this.model.getID() )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 
'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-rcfilters-ui-cell' )
+                                                                       
.addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-icon' )
+                                                                       
.append( this.$icon ),
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-rcfilters-ui-cell' )
+                                                                       
.addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-content' )
+                                                                       .append(
+                                                                               
this.$label
+                                                                               
        .addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-label' ),
+                                                                               
this.editInput.$element,
+                                                                               
this.saveButton.$element
+                                                                       ),
+                                                               
this.popupButton.$element
+                                                                       
.addClass( 'mw-rcfilters-ui-cell' )
+                                                       )
+                                       )
+                       );
+       };
+
+       /* Initialization */
+       OO.inheritClass( mw.rcfilters.ui.QuickLinkMenuOptionWidget, 
OO.ui.Widget );
+       OO.mixinClass( mw.rcfilters.ui.QuickLinkMenuOptionWidget, 
OO.ui.mixin.LabelElement );
+       OO.mixinClass( mw.rcfilters.ui.QuickLinkMenuOptionWidget, 
OO.ui.mixin.IconElement );
+
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onModelUpdate = 
function () {
+               this.setLabel( this.model.getLabel() );
+       };
+
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onPopupButtonClick 
= function () {
+               this.menu.toggle();
+       };
+
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onMenuChoose = 
function ( item ) {
+               var action = item.getData();
+
+               if ( action === 'edit' ) {
+                       this.toggleEdit( true );
+               } else if ( action === 'delete' ) {
+                       this.emit( 'delete' );
+               } else if ( action === 'default' ) {
+                       this.emit( 'default', !this.default );
+               }
+               this.menu.toggle( false );
+       };
+       /**
+        * Respond to save button click
+        */
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onSaveButtonClick = 
function () {
+               this.emit( 'edit', this.editInput.getValue() );
+               this.toggleEdit( false );
+       };
+
+       /**
+        * Respond to input keypress event
+        *
+        * @param {jQuery.Event} e Event data
+        */
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onInputKeypress = 
function ( e ) {
+               if ( e.which === OO.ui.Keys.ENTER ) {
+                       this.emit( 'edit', this.editInput.getValue() );
+                       this.toggleEdit( false );
+               }
+       };
+
+       /**
+        * Respond to input keyup event, this is the way to intercept 'escape' 
key
+        * @param {jQuery.Event} e Event data
+        */
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onInputKeyup = 
function ( e ) {
+               if ( e.which === OO.ui.Keys.ESCAPE ) {
+                       // Return the input to the original label
+                       this.editInput.setValue( this.getLabel() );
+                       this.toggleEdit( false );
+                       return false;
+               }
+       };
+
+       /**
+        * Respond to blur event on the input
+        */
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onInputBlur = 
function () {
+               this.emit( 'edit', this.editInput.getValue() );
+               this.toggleEdit( false );
+       };
+
+       /**
+        * Toggle edit mode on this widget
+        *
+        * @param {boolean} isEdit Widget is in edit mode
+        */
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.toggleEdit = 
function ( isEdit ) {
+               isEdit = isEdit === undefined ? !this.editing : isEdit;
+
+               if ( this.editing !== isEdit ) {
+                       this.$element.toggleClass( 
'mw-rcfilters-ui-quickLinkMenuOptionWidget-edit', isEdit );
+                       this.editInput.setValue( this.getLabel() );
+
+                       this.editInput.toggle( isEdit );
+                       this.$label.toggleClass( 'oo-ui-element-hidden', isEdit 
);
+                       this.popupButton.toggle( !isEdit );
+                       this.saveButton.toggle( isEdit );
+
+                       if ( isEdit ) {
+                               this.editInput.$input.focus();
+                       }
+                       this.editing = isEdit;
+               }
+       };
+
+       mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.getID = function () 
{
+               return this.model.getID();
+       };
+
+}( mediaWiki ) );
diff --git 
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js
new file mode 100644
index 0000000..334b35d
--- /dev/null
+++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js
@@ -0,0 +1,121 @@
+( function ( mw ) {
+       /**
+        * Quick links widget
+        *
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.PendingElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for 
popups
+        */
+       mw.rcfilters.ui.QuickLinksWidget = function 
MwRcfiltersUiQuickLinksWidget( controller, model, config ) {
+               config = config || {};
+
+               // Parent
+               mw.rcfilters.ui.QuickLinksWidget.parent.call( this, config );
+
+               this.controller = controller;
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
+               this.menu = new OO.ui.ButtonGroupWidget( {
+                       classes: [ 'mw-rcfilters-ui-quickLinksWidget-menu' ]
+               } );
+               this.button = new OO.ui.PopupButtonWidget( {
+                       classes: [ 'mw-rcfilters-ui-quickLinksWidget-button' ],
+                       label: mw.msg( 'rcfilters-quickfilters' ),
+                       icon: 'bookmark',
+                       $overlay: this.$overlay,
+                       popup: {
+                               anchor: false,
+                               $autoCloseIgnore: this.$overlay,
+                               $content: this.menu.$element
+                       }
+               } );
+
+               this.menu.aggregate( {
+                       click: 'menuItemClick',
+                       delete: 'menuItemDelete',
+                       default: 'menuItemDefault',
+                       edit: 'menuItemEdit'
+               } );
+
+               // Events
+               this.button.connect( this, { click: 'onButtonClick' } );
+               this.model.connect( this, {
+                       // initialize: 'onModelInitialize',
+                       add: 'onModelAddItem',
+                       remove: 'onModelRemoveItem'
+               } );
+               this.menu.connect( this, {
+                       menuItemClick: 'onMenuItemClick',
+                       menuItemRemove: 'onMenuItemRemove',
+                       menuItemDefault: 'onMenuItemDefault',
+                       menuItemEdit: 'onMenuItemEdit'
+               } );
+
+               this.button.toggle( !this.menu.isEmpty() );
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-quickLinksWidget' )
+                       .append( this.button.$element );
+
+       };
+
+       /* Initialization */
+       OO.inheritClass( mw.rcfilters.ui.QuickLinksWidget, OO.ui.Widget );
+
+       mw.rcfilters.ui.QuickLinksWidget.prototype.onButtonClick = function () {
+               this.menu.toggle( true );
+       };
+
+       mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemClick = function ( 
item ) {
+               this.controller.loadSavedQuery( item.getID() );
+       };
+       mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemRemove = function 
( item ) {
+               this.controller.removeSavedQuery( item.getID() );
+               this.menu.removeItems( [ item ] );
+       };
+
+       mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemDefault = function 
( item ) {
+               debugger;
+       };
+
+       mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemEdit = function ( 
item, newLabel ) {
+               this.controller.renameSavedQuery( item.getID(), newLabel );
+       };
+
+
+
+       // mw.rcfilters.ui.QuickLinksWidget.prototype.onModelInitialize = 
function () {
+       //      var menuOptions = [];
+
+       //      this.model.getItems().forEach( function ( item ) {
+       //              menuOptions.push(
+       //                      new mw.rcfilters.ui.QuickLinkMenuOptionWidget( 
item )
+       //              );
+       //      } );
+
+       //      this.menu.addItems( menuOptions );
+       //      this.button.toggle( !this.menu.isEmpty() );
+       // };
+
+
+       mw.rcfilters.ui.QuickLinksWidget.prototype.onModelAddItem = function ( 
item ) {
+               if ( this.menu.getItemFromData( item.getID() ) ) {
+                       return;
+               }
+
+               this.menu.addItems( [
+                       new mw.rcfilters.ui.QuickLinkMenuOptionWidget( item, { 
$overlay: this.$overlay } )
+               ] );
+               this.button.toggle( !this.menu.isEmpty() );
+       };
+       mw.rcfilters.ui.QuickLinksWidget.prototype.onModelRemoveItem = function 
( item ) {
+               this.menu.removeItems( [ this.model.getItemByID( item.getID() ) 
] );
+               this.button.toggle( !this.menu.isEmpty() );
+       };
+}( mediaWiki ) );
diff --git 
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js
new file mode 100644
index 0000000..68af930
--- /dev/null
+++ 
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js
@@ -0,0 +1,101 @@
+( function ( mw ) {
+       /**
+        * Save quick link widget
+        *
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
+        * @param {Object} [config] Configuration object
+        */
+       mw.rcfilters.ui.SaveQuickLinkWidget = function 
MwRcfiltersUiSaveQuickLinkWidget( controller, model, config ) {
+               var layout,
+                       $popupContent = $( '<div>' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+
+               // Parent
+               mw.rcfilters.ui.SaveQuickLinkWidget.parent.call( this, 
$.extend( {
+                       framed: false,
+                       icon: 'bookmark',
+                       $overlay: this.$overlay,
+                       popup: {
+                               classes: [ 
'mw-rcfilters-ui-saveQuickLinkWidget-popup' ],
+                               padded: true,
+                               head: true,
+                               label: 'Add as quick link',
+                               $content: $popupContent
+                       }
+               }, config ) );
+               // // HACK: Add an icon to the popup head label
+               this.popup.$head.prepend( ( new OO.ui.IconWidget( { icon: 
'bookmark' } ) ).$element );
+
+               this.input = new OO.ui.TextInputWidget( {
+                       validate: 'non-empty'
+               } );
+               layout = new OO.ui.FieldLayout( this.input, {
+                       label: 'Name',
+                       align: 'top'
+               } );
+
+               this.applyButton = new OO.ui.ButtonWidget( {
+                       label: 'Create quick link',
+                       classes: [ 
'mw-rcfilters-ui-saveQuickLinkWidget-popup-buttons-apply' ],
+                       flags: [ 'primary', 'progressive' ]
+               } );
+               this.cancelButton = new OO.ui.ButtonWidget( {
+                       label: 'Cancel',
+                       classes: [ 
'mw-rcfilters-ui-saveQuickLinkWidget-popup-buttons-cancel' ]
+               } );
+
+               $popupContent
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 
'mw-rcfilters-ui-saveQuickLinkWidget-popup-layout' )
+                                       .append( layout.$element ),
+                               $( '<div>' )
+                                       .addClass( 
'mw-rcfilters-ui-saveQuickLinkWidget-popup-buttons' )
+                                       .append(
+                                               this.cancelButton.$element,
+                                               this.applyButton.$element
+                                       )
+                       );
+
+               // Events
+               this.popup.connect( this, { ready: 'onPopupReady' } );
+               this.cancelButton.connect( this, { click: 'onCancelButtonClick' 
} );
+               this.applyButton.connect( this, { click: 'onApplyButtonClick' } 
);
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-saveQuickLinkWidget' );
+       };
+
+       /* Initialization */
+       OO.inheritClass( mw.rcfilters.ui.SaveQuickLinkWidget, 
OO.ui.PopupButtonWidget );
+
+       mw.rcfilters.ui.SaveQuickLinkWidget.prototype.onPopupReady = function 
() {
+               this.input.focus();
+       };
+
+       mw.rcfilters.ui.SaveQuickLinkWidget.prototype.onCancelButtonClick = 
function () {
+               this.popup.toggle( false );
+       };
+       mw.rcfilters.ui.SaveQuickLinkWidget.prototype.onApplyButtonClick = 
function () {
+               var widget = this,
+                       label = this.input.getValue();
+
+               this.input.getValidity()
+                       .then(
+                               function () {
+                                       widget.controller.saveCurrentQuery( 
label );
+                                       widget.input.setValue( '' );
+                                       widget.popup.toggle( false );
+                               }
+                       );
+       };
+}( mediaWiki ) );

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I5cede87633147736d3b4ee5b8ea178ae21bd441f
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <mor...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to