From: jenkins-bot Date: Sat, 13 May 2017 00:32:10 +0000 (+0000) Subject: Merge "resourceloader: Simplify validateScriptFile() with getWithSetCallback" X-Git-Tag: 1.31.0-rc.0~3257 X-Git-Url: http://git.cyclocoop.org/?a=commitdiff_plain;h=89444ffd768af55138a6e217c0d69066c2d659f7;hp=702bce40d0e611961c5d14e6f9321355d5b34793;p=lhc%2Fweb%2Fwiklou.git Merge "resourceloader: Simplify validateScriptFile() with getWithSetCallback" --- diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index b0a8468428..a4f0315e9f 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -219,26 +219,26 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } // Convert startid/endid to timestamps (T163532) - if ( $params['startid'] !== null || $params['endid'] !== null ) { - $ids = [ - (int)$params['startid'] => true, - (int)$params['endid'] => true, - ]; - unset( $ids[0] ); // null - $ids = array_keys( $ids ); - + $revids = []; + if ( $params['startid'] !== null ) { + $revids[] = (int)$params['startid']; + } + if ( $params['endid'] !== null ) { + $revids[] = (int)$params['endid']; + } + if ( $revids ) { $db = $this->getDB(); $sql = $db->unionQueries( [ $db->selectSQLText( 'revision', [ 'id' => 'rev_id', 'ts' => 'rev_timestamp' ], - [ 'rev_id' => $ids ], + [ 'rev_id' => $revids ], __METHOD__ ), $db->selectSQLText( 'archive', [ 'id' => 'ar_rev_id', 'ts' => 'ar_timestamp' ], - [ 'ar_rev_id' => $ids ], + [ 'ar_rev_id' => $revids ], __METHOD__ ), ], false ); diff --git a/languages/i18n/af.json b/languages/i18n/af.json index 40cad08386..99b135d551 100644 --- a/languages/i18n/af.json +++ b/languages/i18n/af.json @@ -1240,7 +1240,7 @@ "sourcefilename": "Oorspronklike lêernaam:", "sourceurl": "Bron-URL:", "destfilename": "Teikenlêernaam:", - "upload-maxfilesize": "Maksimum lêer grootte: $1", + "upload-maxfilesize": "Maksimum-lêergrootte: $1", "upload-description": "Lêerbeskrywing", "upload-options": "Oplaai-opsies", "watchthisupload": "Hou die lêer dop", diff --git a/languages/i18n/az.json b/languages/i18n/az.json index ef53cca5bc..891147446e 100644 --- a/languages/i18n/az.json +++ b/languages/i18n/az.json @@ -2264,6 +2264,8 @@ "confirmemail_body_set": "Kimsə, ehtimal ki, siz özünüz, $1 IP adresindən \n{{SITENAME}} layihəsindəki \"$2\" adlı istifadəçi hesabı üçün e-poçt adresi olaraq bu adresi göstərib.\n\nBu hesabın həqiqətən sizə aid olduğunu təsdiq etmək və {{SITENAME}} saytındakı\ne-poçt əməliyyatlarını aktivləşdirmək üçün aşağıdakı linki brauzerinizdə açın:\n\n$3\n\nƏgər hesab sizə aid *deyilsə*, e-poçt adresi təsdiqlənməsini ləğv etmək\nüçün aşağıdakı linkə daxil olun:\n\n$5\n\nBu təsdiq kodu $4 tarixinə qədər aktiv olacaqdır.", "confirmemail_invalidated": "E-mail təsdiqlənməsi dayandırıldı", "invalidateemail": "E-mail təsdiqlənməsindən imtina", + "notificationemail_body_changed": "Kimsə, ehtimal ki, siz özünüz, $1 IP adresindən {{SITENAME}} layihəsindəki \n\"$2\" adlı istifadəçi hesabının e-poçt adresini \"$3\" ilə əvəz edib.\n\nƏgər bunu siz etməmisinizsə, o zaman dərhal saytın administratoru ilə əlaqə saxlayın.", + "notificationemail_body_removed": "Kimsə, ehtimal ki, siz özünüz, $1 IP adresindən {{SITENAME}} layihəsindəki \n\"$2\" adlı istifadəçi hesabının e-poçt adresini silib.\n\nƏgər bunu siz etməmisinizsə, o zaman dərhal saytın administratoru ilə əlaqə saxlayın.", "scarytranscludedisabled": "[«Interwiki transcluding»dən çıxılmışdır]", "scarytranscludetoolong": "[URL uzundur]", "deletedwhileediting": "'''Diqqət!''' Bu səhifə siz redaktə etməyə başladıqdan sonra silinmişdir!", diff --git a/languages/i18n/azb.json b/languages/i18n/azb.json index 5e23f36977..a65a7d1fee 100644 --- a/languages/i18n/azb.json +++ b/languages/i18n/azb.json @@ -228,7 +228,7 @@ "redirectedfrom": "($1-دن يوْل‌لاندیریلمیش)", "redirectpagesub": "یوْللاندیرما صفحه‌سی", "redirectto": "مسیزپرین دَییشیب:", - "lastmodifiedat": "بۇ صفحه‌‌ سوْن دفعه $1، $2 تاریخینده دَییشیلمیشدیر.", + "lastmodifiedat": "بۇ صفحه‌‌ سوْن دفعه $1، $2 تاریخینده دَییشدیریلمیشدیر.", "viewcount": "بۇ صحیفه {{PLURAL:$1|بیر|$1}} دفعه گؤرولوبدور.", "protectedpage": "قوْرونموش صفحه", "jumpto": "آتیل:", diff --git a/languages/i18n/be-tarask.json b/languages/i18n/be-tarask.json index b846b5017a..1709efce3d 100644 --- a/languages/i18n/be-tarask.json +++ b/languages/i18n/be-tarask.json @@ -1304,6 +1304,7 @@ "rcfilters-activefilters": "Актыўныя фільтры", "rcfilters-quickfilters": "Хуткія спасылкі", "rcfilters-savedqueries-defaultlabel": "Захаваныя фільтры", + "rcfilters-savedqueries-rename": "Перайменаваць", "rcfilters-restore-default-filters": "Аднавіць фільтры па змоўчаньні", "rcfilters-clear-all-filters": "Ачысьціць усе фільтры", "rcfilters-search-placeholder": "Фільтар апошніх зьменаў (праглядзець або пачніце друкаваць)", diff --git a/languages/i18n/bs.json b/languages/i18n/bs.json index f36f73e5e5..1ddde024cd 100644 --- a/languages/i18n/bs.json +++ b/languages/i18n/bs.json @@ -947,7 +947,7 @@ "search-file-match": "(podudara se sadržaj datoteke)", "search-suggest": "Jeste li mislili: $1", "search-rewritten": "Prikazujem rezultate za $1. Umjesto toga potraži $2.", - "search-interwiki-caption": "Srodni projekti", + "search-interwiki-caption": "Rezultati s bratskih projekata", "search-interwiki-default": "$1 rezultati:", "search-interwiki-more": "(više)", "search-interwiki-more-results": "više rezultata", @@ -1304,6 +1304,14 @@ "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|spisak novih stranica]])", "recentchanges-submit": "Prikaži", "rcfilters-activefilters": "Aktivni filteri", + "rcfilters-quickfilters": "Brzi linkovi", + "rcfilters-savedqueries-defaultlabel": "Sačuvani filteri", + "rcfilters-savedqueries-rename": "Preimenuj", + "rcfilters-savedqueries-setdefault": "Postavi kao predodređeno", + "rcfilters-savedqueries-remove": "Ukloni", + "rcfilters-savedqueries-new-name-label": "Naziv", + "rcfilters-savedqueries-apply-label": "Napravi brzi link", + "rcfilters-savedqueries-cancel-label": "Otkaži", "rcfilters-restore-default-filters": "Vrati predodređene filtere", "rcfilters-clear-all-filters": "Ukloni sve filtere", "rcfilters-search-placeholder": "Filtriraj nedavne izmjene (prelistajte mogućnosti ili počnite kucati)", @@ -1331,7 +1339,7 @@ "rcfilters-filter-user-experience-level-newcomer-label": "Novajlije", "rcfilters-filter-user-experience-level-newcomer-description": "Manje od 10 izmjena i 4 dana aktivnosti.", "rcfilters-filter-user-experience-level-learner-label": "Učenici", - "rcfilters-filter-user-experience-level-learner-description": "Više dana aktivnosti i izmjena od \"novajlija\", ali manje od \"iskusnih korisnika\".", + "rcfilters-filter-user-experience-level-learner-description": "Više iskustva od \"novajlija\", ali manje od \"iskusnih korisnika\".", "rcfilters-filter-user-experience-level-experienced-label": "Iskusni korisnici", "rcfilters-filter-user-experience-level-experienced-description": "Preko 30 dana aktivnosti i 500 izmjena.", "rcfilters-filtergroup-automated": "Automatski doprinosi", @@ -1349,7 +1357,13 @@ "rcfilters-filter-minor-description": "Izmjene koje je njihov autor označio manjim.", "rcfilters-filter-major-label": "Obične izmjene", "rcfilters-filter-major-description": "Izmjene koje nisu označene manjim.", + "rcfilters-filtergroup-watchlist": "Stranice na spisku praćenja", "rcfilters-filter-watchlist-watched-label": "Na spisku praćenja", + "rcfilters-filter-watchlist-watched-description": "Izmjene na stranicama na Vašem spisku praćenja.", + "rcfilters-filter-watchlist-watchednew-label": "Nove izmjene na spisku praćenja", + "rcfilters-filter-watchlist-watchednew-description": "Izmjene na stranicama koje se nalaze na spisku praćenja, a koje još niste posjetili otkako su izmijenjene.", + "rcfilters-filter-watchlist-notwatched-label": "Nije na spisku praćenja", + "rcfilters-filter-watchlist-notwatched-description": "Sve osim izmjena na stranicama koje pratite.", "rcfilters-filtergroup-changetype": "Vrsta izmjene", "rcfilters-filter-pageedits-label": "Izmjene stranica", "rcfilters-filter-pageedits-description": "Izmjene wiki sadržaja, rasprava, opisa kategorija....", @@ -1362,7 +1376,9 @@ "rcfilters-typeofchange-conflicts-hideminor": "Ovaj filter za vrstu izmjene u sukobu je s filterom za \"manje izmjene\". Izvjesne vrste izmjena ne mogu se označiti kao \"manje\".", "rcfilters-filtergroup-lastRevision": "Posljednja izmjena", "rcfilters-filter-lastrevision-label": "Posljednja izmjena", + "rcfilters-filter-lastrevision-description": "Najnovija izmjena na stranici.", "rcfilters-filter-previousrevision-label": "Ranije izmjene", + "rcfilters-filter-previousrevision-description": "Sve izmjene koje nisu najnovije na stranici.", "rcnotefrom": "Ispod {{PLURAL:$5|je izmjena|su izmjene}} od $3, $4 (do $1 prikazano).", "rclistfromreset": "Resetiraj izbor datuma", "rclistfrom": "Prikaži nove izmjene počev od $3 u $2", @@ -1836,6 +1852,7 @@ "apihelp": "API pomoć", "apihelp-no-such-module": "Modul \"$1\" nije pronađen.", "apisandbox-unfullscreen": "Prikaži stranicu", + "apisandbox-submit": "Postavi zahtjev", "apisandbox-reset": "Očisti", "apisandbox-retry": "Pokušaj ponovo", "apisandbox-loading": "Učitavam podatke o API modulu \"$1\"...", @@ -3361,7 +3378,7 @@ "tags-source-extension": "Definirano softverom", "tags-source-manual": "Ručno postavili korisnici ili botovi", "tags-source-none": "Više se ne koristi", - "tags-edit": "uređivanje", + "tags-edit": "uredi", "tags-delete": "obriši", "tags-activate": "aktiviraj", "tags-deactivate": "dekativiraj", diff --git a/languages/i18n/csb.json b/languages/i18n/csb.json index 4057941a75..a4f1d8f801 100644 --- a/languages/i18n/csb.json +++ b/languages/i18n/csb.json @@ -966,6 +966,7 @@ "confirm_purge_button": "Jo!", "imgmultigo": "Biéj!", "autoredircomment": "Przeczérowanié do [[$1]]", + "autosumm-new": "Pòwsta nowô starna:", "watchlisttools-view": "Òbaczë wôżnészé zmianë", "watchlisttools-edit": "Òbaczë a editëjë lëstã ùzérónëch artiklów", "watchlisttools-raw": "Editëjë sërą lëstã", diff --git a/languages/i18n/fi.json b/languages/i18n/fi.json index c82fffed27..37a0abc247 100644 --- a/languages/i18n/fi.json +++ b/languages/i18n/fi.json @@ -1377,12 +1377,12 @@ "rcfilters-filter-patrolled-label": "Tarkastetut", "rcfilters-filter-patrolled-description": "Tarkastetut muokkaukset", "rcfilters-filter-unpatrolled-label": "Ei tarkastetut", - "rcfilters-filter-unpatrolled-description": "Muutokset joita ei ole tarkastettu", + "rcfilters-filter-unpatrolled-description": "Muutokset, joita ei ole tarkastettu", "rcfilters-filtergroup-significance": "Merkitys", "rcfilters-filter-minor-label": "Pienet muutokset", "rcfilters-filter-minor-description": "Muokkaukset, jotka on merkitty pieniksi.", "rcfilters-filter-major-label": "Ei-pienet muutokset", - "rcfilters-filter-major-description": "Muokkaukset joita ei ole merkitty pieniksi.", + "rcfilters-filter-major-description": "Muokkaukset, joita ei ole merkitty pieniksi.", "rcfilters-filtergroup-watchlist": "Tarkkailulistalla olevat sivut", "rcfilters-filter-watchlist-watched-label": "Tarkkailulistalla", "rcfilters-filter-watchlist-watched-description": "Muutokset tarkkailulistalla oleviin sivuihin.", @@ -1391,7 +1391,7 @@ "rcfilters-filter-pageedits-label": "Sivun muokkaukset", "rcfilters-filter-pageedits-description": "Muokkaukset wikin sisältöön, keskusteluihin, luokkakuvauksiin....", "rcfilters-filter-newpages-label": "Sivujen luonnit", - "rcfilters-filter-newpages-description": "Muokkaukset joilla on luotu uusia sivuja.", + "rcfilters-filter-newpages-description": "Muokkaukset, joilla on luotu uusia sivuja.", "rcfilters-filter-categorization-label": "Luokkamuutokset", "rcfilters-filter-categorization-description": "Tulokset sivuista, joita on lisätty tai poistettu luokista.", "rcfilters-filter-logactions-label": "Kirjatut toimet", diff --git a/languages/i18n/he.json b/languages/i18n/he.json index d326bd2d96..4dcf6476a4 100644 --- a/languages/i18n/he.json +++ b/languages/i18n/he.json @@ -1327,6 +1327,7 @@ "recentchanges-submit": "הצגה", "rcfilters-activefilters": "מסננים פעילים", "rcfilters-quickfilters": "קישורים מהירים", + "rcfilters-quickfilters-placeholder": "שמירת ההגדרות המועדפות שלך לשימוש בעתיד.", "rcfilters-savedqueries-defaultlabel": "מסננים שמורים", "rcfilters-savedqueries-rename": "שינוי שם", "rcfilters-savedqueries-setdefault": "הגדרה כברירת מחדל", @@ -3842,6 +3843,8 @@ "mw-widgets-titleinput-description-redirect": "הפניה ל{{GRAMMAR:תחילית|$1}}", "mw-widgets-categoryselector-add-category-placeholder": "הוספת קטגוריה...", "mw-widgets-usersmultiselect-placeholder": "הוספת עוד...", + "date-range-from": "מתאריך:", + "date-range-to": "עד תאריך:", "sessionmanager-tie": "לא ניתן לצרף מספר סוגי אימות זהות: $1.", "sessionprovider-generic": "התחברויות של $1", "sessionprovider-mediawiki-session-cookiesessionprovider": "התחברויות המבוססות על עוגיות", diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js index 63e13fdd8a..22b2619c1e 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js @@ -38,6 +38,7 @@ this.whatsThis = config.whatsThis || {}; this.conflicts = config.conflicts || {}; + this.defaultParams = {}; this.aggregate( { update: 'filterItemUpdate' } ); this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } ); @@ -58,6 +59,96 @@ /* Methods */ + /** + * Initialize the group and create its filter items + * + * @param {Object} filterDefinition Filter definition for this group + * @param {string|Object} [groupDefault] Definition of the group default + */ + mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) { + var supersetMap = {}, + model = this, + items = []; + + filterDefinition.forEach( function ( filter ) { + // Instantiate an item + var subsetNames = [], + filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, { + group: model.getName(), + label: mw.msg( filter.label ), + description: mw.msg( filter.description ), + cssClass: filter.cssClass + } ); + + filter.subset = filter.subset || []; + filter.subset = filter.subset.map( function ( el ) { + return el.filter; + } ); + + if ( filter.subset ) { + subsetNames = []; + filter.subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func + // Subsets (unlike conflicts) are always inside the same group + // We can re-map the names of the filters we are getting from + // the subsets with the group prefix + var subsetName = model.getPrefixedName( subsetFilterName ); + // For convenience, we should store each filter's "supersets" -- these are + // the filters that have that item in their subset list. This will just + // make it easier to go through whether the item has any other items + // that affect it (and are selected) at any given time + supersetMap[ subsetName ] = supersetMap[ subsetName ] || []; + mw.rcfilters.utils.addArrayElementsUnique( + supersetMap[ subsetName ], + filterItem.getName() + ); + + // Translate subset param name to add the group name, so we + // get consistent naming. We know that subsets are only within + // the same group + subsetNames.push( subsetName ); + } ); + + // Set translated subset + filterItem.setSubset( subsetNames ); + } + + items.push( filterItem ); + + // Store default parameter state; in this case, default is defined per filter + if ( model.getType() === 'send_unselected_if_any' ) { + // Store the default parameter state + // For this group type, parameter values are direct + model.defaultParams[ filter.name ] = Number( !!filter.default ); + } + } ); + + // Add items + this.addItems( items ); + + // Now that we have all items, we can apply the superset map + this.getItems().forEach( function ( filterItem ) { + filterItem.setSuperset( supersetMap[ filterItem.getName() ] ); + } ); + + // Store default parameter state; in this case, default is defined per the + // entire group, given by groupDefault method parameter + if ( this.getType() === 'string_options' ) { + // Store the default parameter group state + // For this group, the parameter is group name and value is the names + // of selected items + this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions( + // Current values + groupDefault ? + groupDefault.split( this.getSeparator() ) : + [], + // Legal values + this.getItems().map( function ( item ) { + return item.getParamName(); + } ) + ).join( this.getSeparator() ); + } + }; + /** * Respond to filterItem update event * @@ -91,6 +182,15 @@ return this.name; }; + /** + * Get the default param state of this group + * + * @return {Object} Default param state + */ + mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () { + return this.defaultParams; + }; + /** * Get the messags defining the 'whats this' popup for this group * @@ -143,6 +243,21 @@ this.conflicts = conflicts; }; + /** + * Set conflicts for each filter item in the group based on the + * given conflict map + * + * @param {Object} conflicts Object representing the conflict map, + * keyed by the item name, where its value is an object for all its conflicts + */ + mw.rcfilters.dm.FilterGroup.prototype.setFilterConflicts = function ( conflicts ) { + this.getItems().forEach( function ( filterItem ) { + if ( conflicts[ filterItem.getName() ] ) { + filterItem.setConflicts( conflicts[ filterItem.getName() ] ); + } + } ); + }; + /** * Check whether this item has a potential conflict with the given item * @@ -316,6 +431,97 @@ return result; }; + /** + * Get the filter representation this group would provide + * based on given parameter states. + * + * @param {Object|string} [paramRepresentation] An object defining a parameter + * state to translate the filter state from. If not given, an object + * representing all filters as falsey is returned; same as if the parameter + * given were an empty object, or had some of the filters missing. + * @return {Object} Filter representation + */ + mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) { + var areAnySelected, paramValues, + model = this, + paramToFilterMap = {}, + result = {}; + + if ( this.getType() === 'send_unselected_if_any' ) { + paramRepresentation = paramRepresentation || {}; + // Expand param representation to include all filters in the group + this.getItems().forEach( function ( filterItem ) { + paramRepresentation[ filterItem.getParamName() ] = !!paramRepresentation[ filterItem.getParamName() ]; + paramToFilterMap[ filterItem.getParamName() ] = filterItem; + + if ( paramRepresentation[ filterItem.getParamName() ] ) { + areAnySelected = true; + } + } ); + + $.each( paramRepresentation, function ( paramName, paramValue ) { + var filterItem = paramToFilterMap[ paramName ]; + + result[ filterItem.getName() ] = areAnySelected ? + // Flip the definition between the parameter + // state and the filter state + // This is what the 'toggleSelected' value of the filter is + !Number( paramValue ) : + // Otherwise, there are no selected items in the + // group, which means the state is false + false; + } ); + } else if ( this.getType() === 'string_options' ) { + paramRepresentation = paramRepresentation || ''; + + // Normalize the given parameter values + paramValues = mw.rcfilters.utils.normalizeParamOptions( + // Given + paramRepresentation.split( + this.getSeparator() + ), + // Allowed values + this.getItems().map( function ( filterItem ) { + return filterItem.getParamName(); + } ) + ); + // Translate the parameter values into a filter selection state + this.getItems().forEach( function ( filterItem ) { + result[ filterItem.getName() ] = ( + // If it is the word 'all' + paramValues.length === 1 && paramValues[ 0 ] === 'all' || + // All values are written + paramValues.length === model.getItemCount() + ) ? + // All true (either because all values are written or the term 'all' is written) + // is the same as all filters set to true + true : + // Otherwise, the filter is selected only if it appears in the parameter values + paramValues.indexOf( filterItem.getParamName() ) > -1; + } ); + } + + // Go over result and make sure all filters are represented. + // If any filters are missing, they will get a falsey value + this.getItems().forEach( function ( filterItem ) { + result[ filterItem.getName() ] = !!result[ filterItem.getName() ]; + } ); + + return result; + }; + + /** + * Get item by its parameter name + * + * @param {string} paramName Parameter name + * @return {mw.rcfilters.dm.FilterItem} Filter item + */ + mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) { + return this.getItems().filter( function ( item ) { + return item.getParamName() === paramName; + } )[ 0 ]; + }; + /** * Get group type * @@ -326,14 +532,25 @@ }; /** - * Get the prefix used for the filter names inside this group + * Get the prefix used for the filter names inside this group. * + * @param {string} [name] Filter name to prefix * @return {string} Group prefix */ mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () { return this.getName() + '__'; }; + /** + * Get a filter name with the prefix used for the filter names inside this group. + * + * @param {string} name Filter name to prefix + * @return {string} Group prefix + */ + mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) { + return this.getNamePrefix() + name; + }; + /** * Get group's title * diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js index 9054fe4a4f..88ce33c19a 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -192,23 +192,21 @@ * @param {Array} filters Filter group definition */ mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) { - var i, filterItem, filterConflictResult, groupConflictResult, subsetNames, + var filterItem, filterConflictResult, groupConflictResult, model = this, items = [], - supersetMap = {}, groupConflictMap = {}, filterConflictMap = {}, - addArrayElementsUnique = function ( arr, elements ) { - elements = Array.isArray( elements ) ? elements : [ elements ]; - - elements.forEach( function ( element ) { - if ( arr.indexOf( element ) === -1 ) { - arr.push( element ); - } - } ); - - return arr; - }, + /*! + * Expand a conflict definition from group name to + * the list of all included filters in that group. + * We do this so that the direct relationship in the + * models are consistently item->items rather than + * mixing item->group with item->item. + * + * @param {Object} obj Conflict definition + * @return {Object} Expanded conflict definition + */ expandConflictDefinitions = function ( obj ) { var result = {}; @@ -220,7 +218,7 @@ var filter; if ( conflict.filter ) { - filterName = model.groups[ conflict.group ].getNamePrefix() + conflict.filter; + filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter ); filter = model.getItemByName( filterName ); // Rename @@ -262,7 +260,8 @@ this.groups = {}; filters.forEach( function ( data ) { - var group = data.name; + var i, + group = data.name; if ( !model.groups[ group ] ) { model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, { @@ -278,77 +277,25 @@ } } ); } + model.groups[ group ].initializeFilters( data.filters, data.default ); + items = items.concat( model.groups[ group ].getItems() ); + // Prepare conflicts if ( data.conflicts ) { + // Group conflicts groupConflictMap[ group ] = data.conflicts; } for ( i = 0; i < data.filters.length; i++ ) { - data.filters[ i ].subset = data.filters[ i ].subset || []; - data.filters[ i ].subset = data.filters[ i ].subset.map( function ( el ) { - return el.filter; - } ); - - filterItem = new mw.rcfilters.dm.FilterItem( data.filters[ i ].name, model.groups[ group ], { - group: group, - label: mw.msg( data.filters[ i ].label ), - description: mw.msg( data.filters[ i ].description ), - cssClass: data.filters[ i ].cssClass - } ); - - if ( data.filters[ i ].subset ) { - subsetNames = []; - data.filters[ i ].subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func - var subsetName = model.groups[ group ].getNamePrefix() + subsetFilterName; - // For convenience, we should store each filter's "supersets" -- these are - // the filters that have that item in their subset list. This will just - // make it easier to go through whether the item has any other items - // that affect it (and are selected) at any given time - supersetMap[ subsetName ] = supersetMap[ subsetName ] || []; - addArrayElementsUnique( - supersetMap[ subsetName ], - filterItem.getName() - ); - - // Translate subset param name to add the group name, so we - // get consistent naming. We know that subsets are only within - // the same group - subsetNames.push( subsetName ); - } ); - - // Set translated subset - filterItem.setSubset( subsetNames ); - } - - // Store conflicts + // Filter conflicts if ( data.filters[ i ].conflicts ) { + filterItem = model.groups[ group ].getItemByParamName( data.filters[ i ].name ); filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts; } - - if ( data.type === 'send_unselected_if_any' ) { - // Store the default parameter state - // For this group type, parameter values are direct - model.defaultParams[ data.filters[ i ].name ] = Number( !!data.filters[ i ].default ); - } - - model.groups[ group ].addItems( filterItem ); - items.push( filterItem ); - } - - if ( data.type === 'string_options' ) { - // Store the default parameter group state - // For this group, the parameter is group name and value is the names - // of selected items - model.defaultParams[ group ] = model.sanitizeStringOptionGroup( - group, - data.default ? - data.default.split( model.groups[ group ].getSeparator() ) : - [] - ).join( model.groups[ group ].getSeparator() ); } } ); - // Add items to the model + // Add item references to the model, for lookup this.addItems( items ); // Expand conflicts @@ -360,14 +307,11 @@ model.groups[ group ].setConflicts( conflicts ); } ); - items.forEach( function ( filterItem ) { - // Apply the superset map - filterItem.setSuperset( supersetMap[ filterItem.getName() ] ); - - // set conflicts for item - if ( filterConflictResult[ filterItem.getName() ] ) { - filterItem.setConflicts( filterConflictResult[ filterItem.getName() ] ); - } + // Set conflicts for items + $.each( filterConflictResult, function ( filterName, conflicts ) { + var filterItem = model.getItemByName( filterName ); + // set conflicts for items in the group + filterItem.setConflicts( conflicts ); } ); // Create a map between known parameters and their models @@ -383,6 +327,7 @@ } } ); + // Finish initialization this.emit( 'initialize' ); }; @@ -453,12 +398,18 @@ }; /** - * Get the default parameters object + * Get an object representing default parameters state * * @return {Object} Default parameter values */ mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () { - return this.defaultParams; + var result = {}; + + $.each( this.groups, function ( name, model ) { + result = $.extend( true, {}, result, model.getDefaultParams() ); + } ); + + return result; }; /** @@ -501,6 +452,52 @@ return result; }; + /** + * This is the opposite of the #getParametersFromFilters method; this goes over + * the given parameters and translates into a selected/unselected value in the filters. + * + * @param {Object} params Parameters query object + * @return {Object} Filter state object + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) { + var groupMap = {}, + model = this, + result = {}; + + // Go over the given parameters, break apart to groupings + // The resulting object represents the group with its parameter + // values. For example: + // { + // group1: { + // param1: "1", + // param2: "0", + // param3: "1" + // }, + // group2: "param4|param5" + // } + $.each( params, function ( paramName, paramValue ) { + var itemOrGroup = model.parameterMap[ paramName ]; + + if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) { + groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {}; + groupMap[ itemOrGroup.getGroupName() ][ itemOrGroup.getParamName() ] = paramValue; + } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) { + // This parameter represents a group (values are the filters) + // this is equivalent to checking if the group is 'string_options' + groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {}; + groupMap[ itemOrGroup.getName() ] = paramValue; + } + } ); + + // Go over all groups, so we make sure we get the complete output + // even if the parameters don't include a certain group + $.each( this.groups, function ( groupName, groupModel ) { + result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) ); + } ); + + return result; + }; + /** * Get the highlight parameters based on current filter configuration * @@ -527,33 +524,11 @@ * @return {string[]} Array of valid values */ mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) { - var result = [], - validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) { - return filterItem.getParamName(); - } ); - - if ( valueArray.indexOf( 'all' ) > -1 ) { - // If anywhere in the values there's 'all', we - // treat it as if only 'all' was selected. - // Example: param=valid1,valid2,all - // Result: param=all - return [ 'all' ]; - } - - // Get rid of any dupe and invalid parameter, only output - // valid ones - // Example: param=valid1,valid2,invalid1,valid1 - // Result: param=valid1,valid2 - valueArray.forEach( function ( value ) { - if ( - validNames.indexOf( value ) > -1 && - result.indexOf( value ) === -1 - ) { - result.push( value ); - } + var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) { + return filterItem.getParamName(); } ); - return result; + return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames ); }; /** @@ -580,7 +555,7 @@ if ( this.defaultFiltersEmpty !== null ) { // We only need to do this test once, // because defaults are set once per session - defaultFilters = this.getFiltersFromParameters(); + defaultFilters = this.getFiltersFromParameters( this.getDefaultParams() ); this.defaultFiltersEmpty = Object.keys( defaultFilters ).every( function ( filterName ) { return !defaultFilters[ filterName ]; } ); @@ -589,93 +564,6 @@ return this.defaultFiltersEmpty; }; - /** - * This is the opposite of the #getParametersFromFilters method; this goes over - * the given parameters and translates into a selected/unselected value in the filters. - * - * @param {Object} params Parameters query object - * @return {Object} Filter state object - */ - mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) { - var i, - groupMap = {}, - model = this, - base = this.getDefaultParams(), - result = {}; - - params = $.extend( {}, base, params ); - - // Go over the given parameters - $.each( params, function ( paramName, paramValue ) { - var itemOrGroup = model.parameterMap[ paramName ]; - - if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) { - // Mark the group if it has any items that are selected - groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {}; - groupMap[ itemOrGroup.getGroupName() ].hasSelected = ( - groupMap[ itemOrGroup.getGroupName() ].hasSelected || - !!Number( paramValue ) - ); - - // Add filters - groupMap[ itemOrGroup.getGroupName() ].filters = groupMap[ itemOrGroup.getGroupName() ].filters || []; - groupMap[ itemOrGroup.getGroupName() ].filters.push( itemOrGroup ); - } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) { - groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {}; - // This parameter represents a group (values are the filters) - // this is equivalent to checking if the group is 'string_options' - groupMap[ itemOrGroup.getName() ].filters = itemOrGroup.getItems(); - } - } ); - - // Now that we know the groups' selection states, we need to go over - // the filters in the groups and mark their selected states appropriately - $.each( groupMap, function ( group, data ) { - var paramValues, filterItem, - allItemsInGroup = data.filters; - - if ( model.groups[ group ].getType() === 'send_unselected_if_any' ) { - for ( i = 0; i < allItemsInGroup.length; i++ ) { - filterItem = allItemsInGroup[ i ]; - - result[ filterItem.getName() ] = groupMap[ filterItem.getGroupName() ].hasSelected ? - // Flip the definition between the parameter - // state and the filter state - // This is what the 'toggleSelected' value of the filter is - !Number( params[ filterItem.getParamName() ] ) : - // Otherwise, there are no selected items in the - // group, which means the state is false - false; - } - } else if ( model.groups[ group ].getType() === 'string_options' ) { - paramValues = model.sanitizeStringOptionGroup( - group, - params[ group ].split( - model.groups[ group ].getSeparator() - ) - ); - - for ( i = 0; i < allItemsInGroup.length; i++ ) { - filterItem = allItemsInGroup[ i ]; - - result[ filterItem.getName() ] = ( - // If it is the word 'all' - paramValues.length === 1 && paramValues[ 0 ] === 'all' || - // All values are written - paramValues.length === model.groups[ group ].getItemCount() - ) ? - // All true (either because all values are written or the term 'all' is written) - // is the same as all filters set to true - true : - // Otherwise, the filter is selected only if it appears in the parameter values - paramValues.indexOf( filterItem.getParamName() ) > -1; - } - } - } ); - - return result; - }; - /** * Get the item that matches the given name * diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.js index 3ddb5a04b9..8cea27e46e 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.js @@ -1,3 +1,45 @@ ( function ( mw ) { - mw.rcfilters = { dm: {}, ui: {} }; + mw.rcfilters = { + dm: {}, + ui: {}, + utils: { + addArrayElementsUnique: function ( arr, elements ) { + elements = Array.isArray( elements ) ? elements : [ elements ]; + + elements.forEach( function ( element ) { + if ( arr.indexOf( element ) === -1 ) { + arr.push( element ); + } + } ); + + return arr; + }, + normalizeParamOptions: function ( givenOptions, legalOptions ) { + var result = []; + + if ( givenOptions.indexOf( 'all' ) > -1 ) { + // If anywhere in the values there's 'all', we + // treat it as if only 'all' was selected. + // Example: param=valid1,valid2,all + // Result: param=all + return [ 'all' ]; + } + + // Get rid of any dupe and invalid parameter, only output + // valid ones + // Example: param=valid1,valid2,invalid1,valid1 + // Result: param=valid1,valid2 + givenOptions.forEach( function ( value ) { + if ( + legalOptions.indexOf( value ) > -1 && + result.indexOf( value ) === -1 + ) { + result.push( value ); + } + } ); + + return result; + } + } + }; }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js index bc266fbc5f..c61c2883d8 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -109,6 +109,97 @@ ); } ); + QUnit.test( 'Default filters', function ( assert ) { + var definition = [ { + name: 'group1', + title: 'Group 1', + type: 'send_unselected_if_any', + filters: [ + { + name: 'hidefilter1', + label: 'Show filter 1', + description: 'Description of Filter 1 in Group 1', + default: true + }, + { + name: 'hidefilter2', + label: 'Show filter 2', + description: 'Description of Filter 2 in Group 1' + }, + { + name: 'hidefilter3', + label: 'Show filter 3', + description: 'Description of Filter 3 in Group 1', + default: true + } + ] + }, { + name: 'group2', + title: 'Group 2', + type: 'send_unselected_if_any', + filters: [ + { + name: 'hidefilter4', + label: 'Show filter 4', + description: 'Description of Filter 1 in Group 2' + }, + { + name: 'hidefilter5', + label: 'Show filter 5', + description: 'Description of Filter 2 in Group 2', + default: true + }, + { + name: 'hidefilter6', + label: 'Show filter 6', + description: 'Description of Filter 3 in Group 2' + } + ] + }, { + + name: 'group3', + title: 'Group 3', + type: 'string_options', + separator: ',', + default: 'filter8', + filters: [ + { + name: 'filter7', + label: 'Group 3: Filter 1', + description: 'Description of Filter 1 in Group 3' + }, + { + name: 'filter8', + label: 'Group 3: Filter 2', + description: 'Description of Filter 2 in Group 3' + }, + { + name: 'filter9', + label: 'Group 3: Filter 3', + description: 'Description of Filter 3 in Group 3' + } + ] + } ], + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( definition ); + + // Empty query = only default values + assert.deepEqual( + model.getDefaultParams(), + { + hidefilter1: 1, + hidefilter2: 0, + hidefilter3: 1, + hidefilter4: 0, + hidefilter5: 1, + hidefilter6: 0, + group3: 'filter8' + }, + 'Default parameters are stored properly per filter and group' + ); + } ); + QUnit.test( 'Finding matching filters', function ( assert ) { var matches, definition = [ { @@ -608,17 +699,15 @@ } ] } ], - defaultFilterRepresentation = { - // Group 1 and 2, "send_unselected_if_any", the values of the filters are "flipped" from the values of the parameters + baseFilterRepresentation = { group1__hidefilter1: false, - group1__hidefilter2: true, + group1__hidefilter2: false, group1__hidefilter3: false, - group2__hidefilter4: true, + group2__hidefilter4: false, group2__hidefilter5: false, - group2__hidefilter6: true, - // Group 3, "string_options", default values correspond to parameters and filters + group2__hidefilter6: false, group3__filter7: false, - group3__filter8: true, + group3__filter8: false, group3__filter9: false }, model = new mw.rcfilters.dm.FiltersViewModel(); @@ -628,18 +717,18 @@ // Empty query = only default values assert.deepEqual( model.getFiltersFromParameters( {} ), - defaultFilterRepresentation, - 'Empty parameter query results in filters in initial default state' + baseFilterRepresentation, + 'Empty parameter query results in an object representing all filters set to false' ); assert.deepEqual( model.getFiltersFromParameters( { hidefilter2: '1' } ), - $.extend( {}, defaultFilterRepresentation, { - group1__hidefilter1: false, // The text is "show filter 1" + $.extend( {}, baseFilterRepresentation, { + group1__hidefilter1: true, // The text is "show filter 1" group1__hidefilter2: false, // The text is "show filter 2" - group1__hidefilter3: false // The text is "show filter 3" + group1__hidefilter3: true // The text is "show filter 3" } ), 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)' ); @@ -650,7 +739,7 @@ hidefilter2: '1', hidefilter3: '1' } ), - $.extend( {}, defaultFilterRepresentation, { + $.extend( {}, baseFilterRepresentation, { group1__hidefilter1: false, // The text is "show filter 1" group1__hidefilter2: false, // The text is "show filter 2" group1__hidefilter3: false // The text is "show filter 3" @@ -680,11 +769,11 @@ ); // The result here is ignoring the first toggleFiltersSelected call - // We should receive default values + hidefilter6 as false assert.deepEqual( model.getSelectedState(), - $.extend( {}, defaultFilterRepresentation, { - group2__hidefilter5: false, + $.extend( {}, baseFilterRepresentation, { + group2__hidefilter4: true, + group2__hidefilter5: true, group2__hidefilter6: false } ), 'getFiltersFromParameters does not care about previous or existing state.' @@ -694,25 +783,6 @@ model = new mw.rcfilters.dm.FiltersViewModel(); model.initializeFilters( definition ); - model.toggleFiltersSelected( - model.getFiltersFromParameters( { - hidefilter1: '0' - } ) - ); - model.toggleFiltersSelected( - model.getFiltersFromParameters( { - hidefilter1: '1' - } ) - ); - - // Simulates minor edits being hidden in preferences, then unhidden via URL - // override. - assert.deepEqual( - model.getSelectedState(), - defaultFilterRepresentation, - 'After checking and then unchecking a \'send_unselected_if_any\' filter (without touching other filters in that group), results are default' - ); - model.toggleFiltersSelected( model.getFiltersFromParameters( { group3: 'filter7' @@ -720,7 +790,7 @@ ); assert.deepEqual( model.getSelectedState(), - $.extend( {}, defaultFilterRepresentation, { + $.extend( {}, baseFilterRepresentation, { group3__filter7: true, group3__filter8: false, group3__filter9: false @@ -735,7 +805,7 @@ ); assert.deepEqual( model.getSelectedState(), - $.extend( {}, defaultFilterRepresentation, { + $.extend( {}, baseFilterRepresentation, { group3__filter7: true, group3__filter8: true, group3__filter9: false @@ -750,7 +820,7 @@ ); assert.deepEqual( model.getSelectedState(), - $.extend( {}, defaultFilterRepresentation, { + $.extend( {}, baseFilterRepresentation, { group3__filter7: true, group3__filter8: true, group3__filter9: true @@ -765,7 +835,7 @@ ); assert.deepEqual( model.getSelectedState(), - $.extend( {}, defaultFilterRepresentation, { + $.extend( {}, baseFilterRepresentation, { group3__filter7: true, group3__filter8: true, group3__filter9: true @@ -780,7 +850,7 @@ ); assert.deepEqual( model.getSelectedState(), - $.extend( {}, defaultFilterRepresentation, { + $.extend( {}, baseFilterRepresentation, { group3__filter7: true, group3__filter8: false, group3__filter9: true