Mooeypoo has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/342284 )
Change subject: RCFilters UI: Rework conflicts to be objects in filter or group context ...................................................................... RCFilters UI: Rework conflicts to be objects in filter or group context Allow conflicts to be defined in either the filter or the group context and represent a whole object rather than an array of filter names. Bug: T152754 Change-Id: I2423eb2618aa64bf30395b1a1912589e0c71f283 --- M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js M resources/src/mediawiki.rcfilters/mw.rcfilters.init.js M tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js 5 files changed, 209 insertions(+), 61 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/84/342284/1 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 14a610b..d51f43d 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js @@ -29,6 +29,8 @@ this.active = !!config.active; this.fullCoverage = !!config.fullCoverage; + this.conflicts = config.conflicts || {}; + this.aggregate( { update: 'filterItemUpdate' } ); this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } ); }; @@ -81,6 +83,27 @@ return this.name; }; + mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () { + return this.conflicts; + }; + mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) { + this.conflicts = conflicts; + }; + + /** + * Check whether this item has a potential conflict with the given item + * + * This checks whether the given item is in the list of conflicts of + * the current item, but makes no judgment about whether the conflict + * is currently at play (either one of the items may not be selected) + * + * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item + * @return {boolean} This item has a conflict with the given item + */ + mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) { + return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() ); + }; + /** * Check whether there are any items selected * @@ -126,9 +149,15 @@ mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) { var selectedItems = this.getSelectedItems( filterItem ); - return selectedItems.length > 0 && selectedItems.every( function ( selectedFilter ) { - return selectedFilter.existsInConflicts( filterItem ); - } ); + return selectedItems.length > 0 && + ( + // The group as a whole is in conflict with this item + this.existsInConflicts( filterItem ) || + // All selected items are in conflict individually + selectedItems.every( function ( selectedFilter ) { + return selectedFilter.existsInConflicts( filterItem ); + } ) + ); }; /** @@ -140,9 +169,14 @@ mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) { var selectedItems = this.getSelectedItems( filterItem ); - return selectedItems.length > 0 && selectedItems.some( function ( selectedFilter ) { - return selectedFilter.existsInConflicts( filterItem ); - } ); + return selectedItems.length > 0 && ( + // The group as a whole is in conflict with this item + this.existsInConflicts( filterItem ) || + // Any selected items are in conflict individually + selectedItems.some( function ( selectedFilter ) { + return selectedFilter.existsInConflicts( filterItem ); + } ) + ); }; /** diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js index 0df34f8..a019f6a 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js @@ -34,7 +34,7 @@ // Interaction definitions this.subset = config.subset || []; - this.conflicts = config.conflicts || []; + this.conflicts = config.conflicts || {}; this.superset = []; // Interaction states @@ -195,7 +195,7 @@ * @return {string[]} Filter conflicts */ mw.rcfilters.dm.FilterItem.prototype.getConflicts = function () { - return this.conflicts; + return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() ); }; /** @@ -204,7 +204,7 @@ * @param {string[]} conflicts Filter conflicts */ mw.rcfilters.dm.FilterItem.prototype.setConflicts = function ( conflicts ) { - this.conflicts = conflicts || []; + this.conflicts = conflicts || {}; }; /** @@ -237,7 +237,7 @@ * @return {boolean} This item has a conflict with the given item */ mw.rcfilters.dm.FilterItem.prototype.existsInConflicts = function ( filterItem ) { - return this.conflicts.indexOf( filterItem.getName() ) > -1; + return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() ); }; /** 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 5be3656..65e8248 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -162,9 +162,12 @@ * @param {Object} filters Filter group definition */ mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) { - var i, filterItem, selectedFilterNames, + var i, filterItem, selectedFilterNames, filterConflictResult, groupConflictResult, model = this, items = [], + supersetMap = {}, + groupConflictMap = {}, + filterConflictMap = {}, addArrayElementsUnique = function ( arr, elements ) { elements = Array.isArray( elements ) ? elements : [ elements ]; @@ -176,8 +179,31 @@ return arr; }, - conflictMap = {}, - supersetMap = {}; + expandConflictDefinitions = function ( obj ) { + var result = {}; + + $.each( obj, function ( group, conflicts ) { + var adjustedConflicts = {}; + conflicts.forEach( function ( conflict ) { + if ( conflict.filter ) { + adjustedConflicts[ conflict.filter ] = conflict; + } else { + // This conflict is for an entire group. Split it up to + // represent each filter + + // Get the relevant group items + model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) { + // Rebuild the conflict + adjustedConflicts[ groupItem.getName() ] = $.extend( {}, conflict, { filter: groupItem.getName() } ); + } ); + } + } ); + + result[ group ] = adjustedConflicts; + } ); + + return result; + }; // Reset this.clearItems(); @@ -191,6 +217,10 @@ separator: data.separator, fullCoverage: !!data.fullCoverage } ); + } + + if ( data.conflicts ) { + groupConflictMap[ group ] = data.conflicts; } selectedFilterNames = []; @@ -217,26 +247,9 @@ } ); } - // Conflicts are bi-directional, which means FilterA can define having - // a conflict with FilterB, and this conflict should appear in **both** - // filter definitions. - // We need to remap all the 'conflicts' so they reflect the entire state - // in either direction regardless of which filter defined the other as conflicting. + // Store conflicts if ( data.filters[ i ].conflicts ) { - conflictMap[ filterItem.getName() ] = conflictMap[ filterItem.getName() ] || []; - addArrayElementsUnique( - conflictMap[ filterItem.getName() ], - data.filters[ i ].conflicts - ); - - data.filters[ i ].conflicts.forEach( function ( conflictingFilterName ) { // eslint-disable-line no-loop-func - // Add this filter to the conflicts of each of the filters in its list - conflictMap[ conflictingFilterName ] = conflictMap[ conflictingFilterName ] || []; - addArrayElementsUnique( - conflictMap[ conflictingFilterName ], - filterItem.getName() - ); - } ); + filterConflictMap[ data.filters[ i ].name ] = data.filters[ i ].conflicts; } if ( data.type === 'send_unselected_if_any' ) { @@ -262,14 +275,23 @@ } } ); - items.forEach( function ( filterItem ) { - // Apply conflict map to the items - // Now that we mapped all items and conflicts bi-directionally - // we need to apply the definition to each filter again - filterItem.setConflicts( conflictMap[ filterItem.getName() ] ); + // Expand conflicts + groupConflictResult = expandConflictDefinitions( groupConflictMap ); + filterConflictResult = expandConflictDefinitions( filterConflictMap ); + // Set conflicts for groups + $.each( groupConflictResult, function ( group, conflicts ) { + 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() ] ); + } } ); // Add items to the model diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js index a0b785d..1790b54 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js @@ -39,7 +39,12 @@ name: 'hideanons', label: mw.msg( 'rcfilters-filter-unregistered-label' ), description: mw.msg( 'rcfilters-filter-unregistered-description' ), - 'class': 'mw-changeslist-anon' + 'class': 'mw-changeslist-anon', + conflicts: [ + { + group: 'userExpLevel' + } + ] } ] }, @@ -53,26 +58,29 @@ type: 'string_options', separator: ',', fullCoverage: false, + conflicts: [ + { + group: 'registration', + filter: 'hideanons' + } + ], filters: [ { name: 'newcomer', label: mw.msg( 'rcfilters-filter-userExpLevel-newcomer-label' ), description: mw.msg( 'rcfilters-filter-userExpLevel-newcomer-description' ), - conflicts: [ 'hideanons' ], 'class': 'mw-changeslist-user-newcomer' }, { name: 'learner', label: mw.msg( 'rcfilters-filter-userExpLevel-learner-label' ), description: mw.msg( 'rcfilters-filter-userExpLevel-learner-description' ), - conflicts: [ 'hideanons' ], 'class': 'mw-changeslist-user-learner' }, { name: 'experienced', label: mw.msg( 'rcfilters-filter-userExpLevel-experienced-label' ), description: mw.msg( 'rcfilters-filter-userExpLevel-experienced-description' ), - conflicts: [ 'hideanons' ], 'class': 'mw-changeslist-user-experienced' } ] 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 ad0ed54..2fcd125 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -1060,11 +1060,15 @@ filters: [ { name: 'filter1', - conflicts: [ 'filter2', 'filter4' ] + conflicts: [ + { group: 'group2' } + ] }, { name: 'filter2', - conflicts: [ 'filter6' ] + conflicts: [ + { group: 'group2', filter: 'filter6' } + ] }, { name: 'filter3' @@ -1074,16 +1078,21 @@ group2: { title: 'Group 2', type: 'send_unselected_if_any', + conflicts: [ + { group: 'group1', filter: 'filter1' } + ], filters: [ { name: 'filter4' }, { - name: 'filter5', - conflicts: [ 'filter3' ] + name: 'filter5' }, { - name: 'filter6' + name: 'filter6', + conflicts: [ + { group: 'group1', filter: 'filter2' } + ] } ] } @@ -1106,9 +1115,9 @@ 'Initial state: no conflicts because no selections.' ); - // Select a filter that has a conflict with another + // Select a filter that has a conflict with an entire group model.toggleFiltersSelected( { - filter1: true // conflicts: filter2, filter4 + filter1: true // conflicts: entire of group 2 ( filter4, filter5, filter6) } ); model.reassessFilterInteractions( model.getItemByName( 'filter1' ) ); @@ -1117,10 +1126,11 @@ model.getFullState(), $.extend( true, {}, baseFullState, { filter1: { selected: true }, - filter2: { conflicted: true }, - filter4: { conflicted: true } + filter4: { conflicted: true }, + filter5: { conflicted: true }, + filter6: { conflicted: true } } ), - 'Selecting a filter set its conflicts list as "conflicted".' + 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".' ); // Select one of the conflicts (both filters are now conflicted and selected) @@ -1133,28 +1143,102 @@ model.getFullState(), $.extend( true, {}, baseFullState, { filter1: { selected: true, conflicted: true }, - filter2: { conflicted: true }, - filter4: { selected: true, conflicted: true } + filter4: { selected: true, conflicted: true }, + filter5: { conflicted: true }, + filter6: { conflicted: true } } ), - 'Selecting a conflicting filter sets both sides to conflicted and selected.' + 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.' ); - // Select another filter from filter4 group, meaning: - // now filter1 no longer conflicts with filter4 + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( definition ); + + // Select a filter that has a conflict with a specific filter + model.toggleFiltersSelected( { + filter2: true // conflicts: filter6 + } ); + + model.reassessFilterInteractions( model.getItemByName( 'filter2' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullState, { + filter2: { selected: true }, + filter6: { conflicted: true } + } ), + 'Selecting a filter that conflicts with another filter sets the other as "conflicted".' + ); + + // Select the conflicting filter model.toggleFiltersSelected( { filter6: true // conflicts: filter2 } ); + model.reassessFilterInteractions( model.getItemByName( 'filter6' ) ); assert.deepEqual( model.getFullState(), $.extend( true, {}, baseFullState, { - filter1: { selected: true, conflicted: false }, // No longer conflicts (filter4 is not the only in the group) - filter2: { conflicted: true }, // While not selected, still in conflict with filter1, which is selected - filter4: { selected: true, conflicted: false }, // No longer conflicts with filter1 - filter6: { selected: true, conflicted: false } + filter2: { selected: true, conflicted: true }, + filter6: { selected: true, conflicted: true }, + filter1: { conflicted: true } // This is added to the conflicts because filter6 is part of group2, who is in conflict with filter1 } ), - 'Selecting a non-conflicting filter from a conflicting group removes the conflict' + 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.' + ); + + // Now choose a non-conflicting filter from the group + model.toggleFiltersSelected( { + filter5: true + } ); + + model.reassessFilterInteractions( model.getItemByName( 'filter5' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullState, { + filter2: { selected: true }, + filter6: { selected: true }, + filter5: { selected: true } + // Filter1 is no longer in conflict because filter2 is selected + // within its own group, which removes the conflict + } ), + 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.' + ); + + // Followup on the previous test, unselect filter2 so filter1 is once again conflicted + model.toggleFiltersSelected( { + filter2: false + } ); + + model.reassessFilterInteractions( model.getItemByName( 'filter5' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullState, { + filter1: { conflicted: true }, + filter6: { selected: true }, + filter5: { selected: true } + } ), + 'Unselecting an item that did not conflict returns the conflict state.' + ); + + // Followup #2: Now actually select filter1, and make everything conflicted + model.toggleFiltersSelected( { + filter1: true + } ); + + model.reassessFilterInteractions( model.getItemByName( 'filter5' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullState, { + filter1: { selected: true, conflicted: true }, + filter6: { selected: true, conflicted: true }, + filter5: { selected: true, conflicted: true }, + filter4: { conflicted: true } // Not selected but conflicted because it's in group2 + } ), + 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.' ); } ); -- To view, visit https://gerrit.wikimedia.org/r/342284 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I2423eb2618aa64bf30395b1a1912589e0c71f283 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