Merge "Fix order of @var parameter in PHP"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / SavedQueriesModel.js
1 var SavedQueryItemModel = require( './SavedQueryItemModel.js' ),
2 SavedQueriesModel;
3
4 /**
5 * View model for saved queries
6 *
7 * @class mw.rcfilters.dm.SavedQueriesModel
8 * @mixins OO.EventEmitter
9 * @mixins OO.EmitterList
10 *
11 * @constructor
12 * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
13 * @param {Object} [config] Configuration options
14 * @cfg {string} [default] Default query ID
15 */
16 SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
17 config = config || {};
18
19 // Mixin constructor
20 OO.EventEmitter.call( this );
21 OO.EmitterList.call( this );
22
23 this.default = config.default;
24 this.filtersModel = filtersModel;
25 this.converted = false;
26
27 // Events
28 this.aggregate( { update: 'itemUpdate' } );
29 };
30
31 /* Initialization */
32
33 OO.initClass( SavedQueriesModel );
34 OO.mixinClass( SavedQueriesModel, OO.EventEmitter );
35 OO.mixinClass( SavedQueriesModel, OO.EmitterList );
36
37 /* Events */
38
39 /**
40 * @event initialize
41 *
42 * Model is initialized
43 */
44
45 /**
46 * @event itemUpdate
47 * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
48 *
49 * An item has changed
50 */
51
52 /**
53 * @event default
54 * @param {string} New default ID
55 *
56 * The default has changed
57 */
58
59 /* Methods */
60
61 /**
62 * Initialize the saved queries model by reading it from the user's settings.
63 * The structure of the saved queries is:
64 * {
65 * version: (string) Version number; if version 2, the query represents
66 * parameters. Otherwise, the older version represented filters
67 * and needs to be readjusted,
68 * default: (string) Query ID
69 * queries:{
70 * query_id_1: {
71 * data:{
72 * filters: (Object) Minimal definition of the filters
73 * highlights: (Object) Definition of the highlights
74 * },
75 * label: (optional) Name of this query
76 * }
77 * }
78 * }
79 *
80 * @param {Object} [savedQueries] An object with the saved queries with
81 * the above structure.
82 * @fires initialize
83 */
84 SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
85 var model = this;
86
87 savedQueries = savedQueries || {};
88
89 this.clearItems();
90 this.default = null;
91 this.converted = false;
92
93 if ( savedQueries.version !== '2' ) {
94 // Old version dealt with filter names. We need to migrate to the new structure
95 // The new structure:
96 // {
97 // version: (string) '2',
98 // default: (string) Query ID,
99 // queries: {
100 // query_id: {
101 // label: (string) Name of the query
102 // data: {
103 // params: (object) Representing all the parameter states
104 // highlights: (object) Representing all the filter highlight states
105 // }
106 // }
107 // }
108 // eslint-disable-next-line no-jquery/no-each-util
109 $.each( savedQueries.queries || {}, function ( id, obj ) {
110 if ( obj.data && obj.data.filters ) {
111 obj.data = model.convertToParameters( obj.data );
112 }
113 } );
114
115 this.converted = true;
116 savedQueries.version = '2';
117 }
118
119 // Initialize the query items
120 // eslint-disable-next-line no-jquery/no-each-util
121 $.each( savedQueries.queries || {}, function ( id, obj ) {
122 var normalizedData = obj.data,
123 isDefault = String( savedQueries.default ) === String( id );
124
125 if ( normalizedData && normalizedData.params ) {
126 // Backwards-compat fix: Remove sticky parameters from
127 // the given data, if they exist
128 normalizedData.params = model.filtersModel.removeStickyParams( normalizedData.params );
129
130 // Correct the invert state for effective selection
131 if ( normalizedData.params.invert && !normalizedData.params.namespace ) {
132 delete normalizedData.params.invert;
133 }
134
135 model.cleanupHighlights( normalizedData );
136
137 id = String( id );
138
139 // Skip the addNewQuery method because we don't want to unnecessarily manipulate
140 // the given saved queries unless we literally intend to (like in backwards compat fixes)
141 // And the addNewQuery method also uses a minimization routine that checks for the
142 // validity of items and minimizes the query. This isn't necessary for queries loaded
143 // from the backend, and has the risk of removing values if they're temporarily
144 // invalid (example: if we temporarily removed a cssClass from a filter in the backend)
145 model.addItems( [
146 new SavedQueryItemModel(
147 id,
148 obj.label,
149 normalizedData,
150 { default: isDefault }
151 )
152 ] );
153
154 if ( isDefault ) {
155 model.default = id;
156 }
157 }
158 } );
159
160 this.emit( 'initialize' );
161 };
162
163 /**
164 * Clean up highlight parameters.
165 * 'highlight' used to be stored, it's not inferred based on the presence of absence of
166 * filter colors.
167 *
168 * @param {Object} data Saved query data
169 */
170 SavedQueriesModel.prototype.cleanupHighlights = function ( data ) {
171 if (
172 data.params.highlight === '0' &&
173 data.highlights && Object.keys( data.highlights ).length
174 ) {
175 data.highlights = {};
176 }
177 delete data.params.highlight;
178 };
179
180 /**
181 * Convert from representation of filters to representation of parameters
182 *
183 * @param {Object} data Query data
184 * @return {Object} New converted query data
185 */
186 SavedQueriesModel.prototype.convertToParameters = function ( data ) {
187 var newData = {},
188 defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
189 fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
190 highlightEnabled = data.highlights.highlight;
191
192 delete data.highlights.highlight;
193
194 // Filters
195 newData.params = this.filtersModel.getMinimizedParamRepresentation(
196 this.filtersModel.getParametersFromFilters( fullFilterRepresentation )
197 );
198
199 // Highlights: appending _color to keys
200 newData.highlights = {};
201 // eslint-disable-next-line no-jquery/no-each-util
202 $.each( data.highlights, function ( highlightedFilterName, value ) {
203 if ( value ) {
204 newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
205 }
206 } );
207
208 // Add highlight
209 newData.params.highlight = String( Number( highlightEnabled || 0 ) );
210
211 return newData;
212 };
213
214 /**
215 * Add a query item
216 *
217 * @param {string} label Label for the new query
218 * @param {Object} fulldata Full data representation for the new query, combining highlights and filters
219 * @param {boolean} isDefault Item is default
220 * @param {string} [id] Query ID, if exists. If this isn't given, a random
221 * new ID will be created.
222 * @return {string} ID of the newly added query
223 */
224 SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) {
225 var normalizedData = { params: {}, highlights: {} },
226 highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ),
227 randomID = String( id || ( new Date() ).getTime() ),
228 data = this.filtersModel.getMinimizedParamRepresentation( fulldata );
229
230 // Split highlight/params
231 // eslint-disable-next-line no-jquery/no-each-util
232 $.each( data, function ( param, value ) {
233 if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) {
234 normalizedData.highlights[ param ] = value;
235 } else {
236 normalizedData.params[ param ] = value;
237 }
238 } );
239
240 // Correct the invert state for effective selection
241 if ( normalizedData.params.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
242 delete normalizedData.params.invert;
243 }
244
245 // Add item
246 this.addItems( [
247 new SavedQueryItemModel(
248 randomID,
249 label,
250 normalizedData,
251 { default: isDefault }
252 )
253 ] );
254
255 if ( isDefault ) {
256 this.setDefault( randomID );
257 }
258
259 return randomID;
260 };
261
262 /**
263 * Remove query from model
264 *
265 * @param {string} queryID Query ID
266 */
267 SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
268 var query = this.getItemByID( queryID );
269
270 if ( query ) {
271 // Check if this item was the default
272 if ( String( this.getDefault() ) === String( queryID ) ) {
273 // Nulify the default
274 this.setDefault( null );
275 }
276
277 this.removeItems( [ query ] );
278 }
279 };
280
281 /**
282 * Get an item that matches the requested query
283 *
284 * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
285 * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
286 */
287 SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
288 // Minimize before comparison
289 fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison );
290
291 // Correct the invert state for effective selection
292 if ( fullQueryComparison.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
293 delete fullQueryComparison.invert;
294 }
295
296 return this.getItems().filter( function ( item ) {
297 return OO.compare(
298 item.getCombinedData(),
299 fullQueryComparison
300 );
301 } )[ 0 ];
302 };
303
304 /**
305 * Get query by its identifier
306 *
307 * @param {string} queryID Query identifier
308 * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
309 * the search. Undefined if not found.
310 */
311 SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
312 return this.getItems().filter( function ( item ) {
313 return item.getID() === queryID;
314 } )[ 0 ];
315 };
316
317 /**
318 * Get the full data representation of the default query, if it exists
319 *
320 * @return {Object|null} Representation of the default params if exists.
321 * Null if default doesn't exist or if the user is not logged in.
322 */
323 SavedQueriesModel.prototype.getDefaultParams = function () {
324 return ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
325 };
326
327 /**
328 * Get a full parameter representation of an item data
329 *
330 * @param {Object} queryID Query ID
331 * @return {Object} Parameter representation
332 */
333 SavedQueriesModel.prototype.getItemParams = function ( queryID ) {
334 var item = this.getItemByID( queryID ),
335 data = item ? item.getData() : {};
336
337 return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {};
338 };
339
340 /**
341 * Build a full parameter representation given item data and model sticky values state
342 *
343 * @param {Object} data Item data
344 * @return {Object} Full param representation
345 */
346 SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
347 data = data || {};
348 // Return parameter representation
349 return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
350 data.params,
351 data.highlights
352 ) );
353 };
354
355 /**
356 * Get the object representing the state of the entire model and items
357 *
358 * @return {Object} Object representing the state of the model and items
359 */
360 SavedQueriesModel.prototype.getState = function () {
361 var obj = { queries: {}, version: '2' };
362
363 // Translate the items to the saved object
364 this.getItems().forEach( function ( item ) {
365 obj.queries[ item.getID() ] = item.getState();
366 } );
367
368 if ( this.getDefault() ) {
369 obj.default = this.getDefault();
370 }
371
372 return obj;
373 };
374
375 /**
376 * Set a default query. Null to unset default.
377 *
378 * @param {string} itemID Query identifier
379 * @fires default
380 */
381 SavedQueriesModel.prototype.setDefault = function ( itemID ) {
382 if ( this.default !== itemID ) {
383 this.default = itemID;
384
385 // Set for individual itens
386 this.getItems().forEach( function ( item ) {
387 item.toggleDefault( item.getID() === itemID );
388 } );
389
390 this.emit( 'default', itemID );
391 }
392 };
393
394 /**
395 * Get the default query ID
396 *
397 * @return {string} Default query identifier
398 */
399 SavedQueriesModel.prototype.getDefault = function () {
400 return this.default;
401 };
402
403 /**
404 * Check if the saved queries were converted
405 *
406 * @return {boolean} Saved queries were converted from the previous
407 * version to the new version
408 */
409 SavedQueriesModel.prototype.isConverted = function () {
410 return this.converted;
411 };
412
413 module.exports = SavedQueriesModel;