Merge "Remove perf tracking code that was moved to WikimediaEvents in Ib300af5c"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.SavedQueriesModel.js
1 ( function ( mw, $ ) {
2 /**
3 * View model for saved queries
4 *
5 * @class
6 * @mixins OO.EventEmitter
7 * @mixins OO.EmitterList
8 *
9 * @constructor
10 * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
11 * @param {Object} [config] Configuration options
12 * @cfg {string} [default] Default query ID
13 */
14 mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
15 config = config || {};
16
17 // Mixin constructor
18 OO.EventEmitter.call( this );
19 OO.EmitterList.call( this );
20
21 this.default = config.default;
22 this.filtersModel = filtersModel;
23 this.converted = false;
24
25 // Events
26 this.aggregate( { update: 'itemUpdate' } );
27 };
28
29 /* Initialization */
30
31 OO.initClass( mw.rcfilters.dm.SavedQueriesModel );
32 OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EventEmitter );
33 OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EmitterList );
34
35 /* Events */
36
37 /**
38 * @event initialize
39 *
40 * Model is initialized
41 */
42
43 /**
44 * @event itemUpdate
45 * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
46 *
47 * An item has changed
48 */
49
50 /**
51 * @event default
52 * @param {string} New default ID
53 *
54 * The default has changed
55 */
56
57 /* Methods */
58
59 /**
60 * Initialize the saved queries model by reading it from the user's settings.
61 * The structure of the saved queries is:
62 * {
63 * version: (string) Version number; if version 2, the query represents
64 * parameters. Otherwise, the older version represented filters
65 * and needs to be readjusted,
66 * default: (string) Query ID
67 * queries:{
68 * query_id_1: {
69 * data:{
70 * filters: (Object) Minimal definition of the filters
71 * highlights: (Object) Definition of the highlights
72 * },
73 * label: (optional) Name of this query
74 * }
75 * }
76 * }
77 *
78 * @param {Object} [savedQueries] An object with the saved queries with
79 * the above structure.
80 * @fires initialize
81 */
82 mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
83 var model = this,
84 excludedParams = this.filtersModel.getExcludedParams();
85
86 savedQueries = savedQueries || {};
87
88 this.clearItems();
89 this.default = null;
90 this.converted = false;
91
92 if ( savedQueries.version !== '2' ) {
93 // Old version dealt with filter names. We need to migrate to the new structure
94 // The new structure:
95 // {
96 // version: (string) '2',
97 // default: (string) Query ID,
98 // queries: {
99 // query_id: {
100 // label: (string) Name of the query
101 // data: {
102 // params: (object) Representing all the parameter states
103 // highlights: (object) Representing all the filter highlight states
104 // }
105 // }
106 // }
107 $.each( savedQueries.queries || {}, function ( id, obj ) {
108 if ( obj.data && obj.data.filters ) {
109 obj.data = model.convertToParameters( obj.data );
110 }
111 } );
112
113 this.converted = true;
114 savedQueries.version = '2';
115 }
116
117 // Initialize the query items
118 $.each( savedQueries.queries || {}, function ( id, obj ) {
119 var normalizedData = obj.data,
120 isDefault = String( savedQueries.default ) === String( id );
121
122 if ( normalizedData && normalizedData.params ) {
123 // Backwards-compat fix: Remove excluded parameters from
124 // the given data, if they exist
125 excludedParams.forEach( function ( name ) {
126 delete normalizedData.params[ name ];
127 } );
128
129 id = String( id );
130 model.addNewQuery( obj.label, normalizedData, isDefault, id );
131
132 if ( isDefault ) {
133 model.default = id;
134 }
135 }
136 } );
137
138 this.emit( 'initialize' );
139 };
140
141 /**
142 * Convert from representation of filters to representation of parameters
143 *
144 * @param {Object} data Query data
145 * @return {Object} New converted query data
146 */
147 mw.rcfilters.dm.SavedQueriesModel.prototype.convertToParameters = function ( data ) {
148 var newData = {},
149 defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
150 fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
151 highlightEnabled = data.highlights.highlight;
152
153 delete data.highlights.highlight;
154
155 // Filters
156 newData.params = this.filtersModel.getParametersFromFilters( fullFilterRepresentation );
157
158 // Highlights (taking out 'highlight' itself, appending _color to keys)
159 newData.highlights = {};
160 Object.keys( data.highlights ).forEach( function ( highlightedFilterName ) {
161 newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
162 } );
163
164 // Add highlight and invert toggles to params
165 newData.params.highlight = String( Number( highlightEnabled || 0 ) );
166 newData.params.invert = String( Number( data.invert || 0 ) );
167
168 return newData;
169 };
170
171 /**
172 * Get an object representing the base state of parameters
173 * and highlights.
174 *
175 * This is meant to make sure that the saved queries that are
176 * in memory are always the same structure as what we would get
177 * by calling the current model's "getSelectedState" and by checking
178 * highlight items.
179 *
180 * In cases where a user saved a query when the system had a certain
181 * set of params, and then a filter was added to the system, we want
182 * to make sure that the stored queries can still be comparable to
183 * the current state, which means that we need the base state for
184 * two operations:
185 *
186 * - Saved queries are stored in "minimal" view (only changed params
187 * are stored); When we initialize the system, we merge each minimal
188 * query with the base state (using 'getMinimalParamList') so all
189 * saved queries have the exact same structure as what we would get
190 * by checking the getSelectedState of the filter.
191 * - When we save the queries, we minimize the object to only represent
192 * whatever has actually changed, rather than store the entire
193 * object. To check what actually is different so we can store it,
194 * we need to obtain a base state to compare against, this is
195 * what #getMinimalParamList does
196 *
197 * @return {Object} Base parameter state
198 */
199 mw.rcfilters.dm.SavedQueriesModel.prototype.getBaseParamState = function () {
200 var allParams,
201 highlightedItems = {};
202
203 if ( !this.baseParamState ) {
204 allParams = this.filtersModel.getParametersFromFilters( {} );
205
206 // Prepare highlights
207 this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
208 highlightedItems[ item.getName() + '_color' ] = null;
209 } );
210
211 this.baseParamState = {
212 params: $.extend( true, { invert: '0', highlight: '0' }, allParams ),
213 highlights: highlightedItems
214 };
215 }
216
217 return this.baseParamState;
218 };
219
220 /**
221 * Get an object that holds only the parameters and highlights that have
222 * values different than the base value.
223 *
224 * This is the reverse of the normalization we do initially on loading and
225 * initializing the saved queries model.
226 *
227 * @param {Object} valuesObject Object representing the state of both
228 * filters and highlights in its normalized version, to be minimized.
229 * @return {Object} Minimal filters and highlights list
230 */
231 mw.rcfilters.dm.SavedQueriesModel.prototype.getMinimalParamList = function ( valuesObject ) {
232 var result = { params: {}, highlights: {} },
233 baseState = this.getBaseParamState();
234
235 // XOR results
236 $.each( valuesObject.params, function ( name, value ) {
237 if ( baseState.params !== undefined && baseState.params[ name ] !== value ) {
238 result.params[ name ] = value;
239 }
240 } );
241
242 $.each( valuesObject.highlights, function ( name, value ) {
243 if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) {
244 result.highlights[ name ] = value;
245 }
246 } );
247
248 return result;
249 };
250
251 /**
252 * Add a query item
253 *
254 * @param {string} label Label for the new query
255 * @param {Object} data Data for the new query
256 * @param {boolean} isDefault Item is default
257 * @param {string} [id] Query ID, if exists. If this isn't given, a random
258 * new ID will be created.
259 * @return {string} ID of the newly added query
260 */
261 mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, data, isDefault, id ) {
262 var randomID = String( id || ( new Date() ).getTime() ),
263 normalizedData = this.getMinimalParamList( data );
264
265 // Add item
266 this.addItems( [
267 new mw.rcfilters.dm.SavedQueryItemModel(
268 randomID,
269 label,
270 normalizedData,
271 { 'default': isDefault }
272 )
273 ] );
274
275 if ( isDefault ) {
276 this.setDefault( randomID );
277 }
278
279 return randomID;
280 };
281
282 /**
283 * Remove query from model
284 *
285 * @param {string} queryID Query ID
286 */
287 mw.rcfilters.dm.SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
288 var query = this.getItemByID( queryID );
289
290 if ( query ) {
291 // Check if this item was the default
292 if ( String( this.getDefault() ) === String( queryID ) ) {
293 // Nulify the default
294 this.setDefault( null );
295 }
296
297 this.removeItems( [ query ] );
298 }
299 };
300
301 /**
302 * Get an item that matches the requested query
303 *
304 * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
305 * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
306 */
307 mw.rcfilters.dm.SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
308 // Minimize before comparison
309 fullQueryComparison = this.getMinimalParamList( fullQueryComparison );
310
311 return this.getItems().filter( function ( item ) {
312 return OO.compare(
313 item.getData(),
314 fullQueryComparison
315 );
316 } )[ 0 ];
317 };
318
319 /**
320 * Get query by its identifier
321 *
322 * @param {string} queryID Query identifier
323 * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
324 * the search. Undefined if not found.
325 */
326 mw.rcfilters.dm.SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
327 return this.getItems().filter( function ( item ) {
328 return item.getID() === queryID;
329 } )[ 0 ];
330 };
331
332 /**
333 * Get an item's full data
334 *
335 * @param {string} queryID Query identifier
336 * @return {Object} Item's full data
337 */
338 mw.rcfilters.dm.SavedQueriesModel.prototype.getItemFullData = function ( queryID ) {
339 var item = this.getItemByID( queryID );
340
341 // Fill in the base params
342 return item ? $.extend( true, {}, this.getBaseParamState(), item.getData() ) : {};
343 };
344
345 /**
346 * Get the object representing the state of the entire model and items
347 *
348 * @return {Object} Object representing the state of the model and items
349 */
350 mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () {
351 var model = this,
352 obj = { queries: {}, version: '2' };
353
354 // Translate the items to the saved object
355 this.getItems().forEach( function ( item ) {
356 var itemState = item.getState();
357
358 itemState.data = model.getMinimalParamList( itemState.data );
359
360 obj.queries[ item.getID() ] = itemState;
361 } );
362
363 if ( this.getDefault() ) {
364 obj.default = this.getDefault();
365 }
366
367 return obj;
368 };
369
370 /**
371 * Set a default query. Null to unset default.
372 *
373 * @param {string} itemID Query identifier
374 * @fires default
375 */
376 mw.rcfilters.dm.SavedQueriesModel.prototype.setDefault = function ( itemID ) {
377 if ( this.default !== itemID ) {
378 this.default = itemID;
379
380 // Set for individual itens
381 this.getItems().forEach( function ( item ) {
382 item.toggleDefault( item.getID() === itemID );
383 } );
384
385 this.emit( 'default', itemID );
386 }
387 };
388
389 /**
390 * Get the default query ID
391 *
392 * @return {string} Default query identifier
393 */
394 mw.rcfilters.dm.SavedQueriesModel.prototype.getDefault = function () {
395 return this.default;
396 };
397
398 /**
399 * Check if the saved queries were converted
400 *
401 * @return {boolean} Saved queries were converted from the previous
402 * version to the new version
403 */
404 mw.rcfilters.dm.SavedQueriesModel.prototype.isConverted = function () {
405 return this.converted;
406 };
407 }( mediaWiki, jQuery ) );