From 23a314703632f479ff7c275380791368fe81b3f1 Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Mon, 6 Feb 2017 18:30:33 -0800 Subject: [PATCH] RCFilters UI: Filter interaction: conflicts Some filters conflict with other filters, and the state should be shown properly. Bug: T156861 Change-Id: I1649e01a80e5a2a576af0a90df20302887631284 --- .../dm/mw.rcfilters.dm.FilterGroup.js | 28 +++++ .../dm/mw.rcfilters.dm.FilterItem.js | 2 +- .../dm/mw.rcfilters.dm.FiltersViewModel.js | 66 +++++++++++ .../dm.FiltersViewModel.test.js | 106 ++++++++++++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) 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 6d9611e874..14a610b71e 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js @@ -117,6 +117,34 @@ } ); }; + /** + * Check whether all selected items are in conflict with the given item + * + * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test + * @return {boolean} All selected items are in conflict with this item + */ + 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 ); + } ); + }; + + /** + * Check whether any of the selected items are in conflict with the given item + * + * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test + * @return {boolean} Any of the selected items are in conflict with this item + */ + 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 ); + } ); + }; + /** * Get group type * 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 acf53c1cfc..39c7667f9e 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js @@ -216,7 +216,7 @@ * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item * @return {boolean} This item has a conflict with the given item */ - mw.rcfilters.dm.FilterItem.prototype.hasConflictWith = function ( filterItem ) { + mw.rcfilters.dm.FilterItem.prototype.existsInConflicts = function ( filterItem ) { return this.conflicts.indexOf( filterItem.getName() ) > -1; }; 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 14306f1270..13f7d31292 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -79,6 +79,72 @@ } ); } } ); + + // Check for conflicts + // In this case, we must go over all items, since + // conflicts are bidirectional and depend not only on + // individual items, but also on the selected states of + // the groups they're in. + this.getItems().forEach( function ( filterItem ) { + var inConflict = false, + filterItemGroup = filterItem.getGroupModel(); + + // For each item, see if that item is still conflicting + $.each( model.groups, function ( groupName, groupModel ) { + if ( filterItem.getGroupName() === groupName ) { + // Check inside the group + inConflict = groupModel.areAnySelectedInConflictWith( filterItem ); + } else { + // According to the spec, if two items conflict from two different + // groups, the conflict only lasts if the groups **only have selected + // items that are conflicting**. If a group has selected items that + // are conflicting and non-conflicting, the scope of the result has + // expanded enough to completely remove the conflict. + + // For example, see two groups with conflicts: + // userExpLevel: [ + // { + // name: 'experienced', + // conflicts: [ 'unregistered' ] + // } + // ], + // registration: [ + // { + // name: 'registered', + // }, + // { + // name: 'unregistered', + // } + // ] + // If we select 'experienced', then 'unregistered' is in conflict (and vice versa), + // because, inherently, 'experienced' filter only includes registered users, and so + // both filters are in conflict with one another. + // However, the minute we select 'registered', the scope of our results + // has expanded to no longer have a conflict with 'experienced' filter, and + // so the conflict is removed. + + // In our case, we need to check if the entire group conflicts with + // the entire item's group, so we follow the above spec + inConflict = ( + // The foreign group is in conflict with this item + groupModel.areAllSelectedInConflictWith( filterItem ) && + // Every selected member of the item's own group is also + // in conflict with the other group + filterItemGroup.getSelectedItems().every( function ( otherGroupItem ) { + return groupModel.areAllSelectedInConflictWith( otherGroupItem ); + } ) + ); + } + + // If we're in conflict, this will return 'false' which + // will break the loop. Otherwise, we're not in conflict + // and the loop continues + return !inConflict; + } ); + + // Toggle the item state + filterItem.toggleConflicted( inConflict ); + } ); }; /** 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 884be8cd07..49a5b18018 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -1051,4 +1051,110 @@ 'Not all items in the group are checked - all items are non-muted regardless of group coverage' ); } ); + + QUnit.test( 'Filter interaction: conflicts', function ( assert ) { + var definition = { + group1: { + title: 'Group 1', + type: 'string_options', + filters: [ + { + name: 'filter1', + conflicts: [ 'filter2', 'filter4' ] + }, + { + name: 'filter2', + conflicts: [ 'filter6' ] + }, + { + name: 'filter3' + } + ] + }, + group2: { + title: 'Group 2', + type: 'send_unselected_if_any', + filters: [ + { + name: 'filter4' + }, + { + name: 'filter5', + conflicts: [ 'filter3' ] + }, + { + name: 'filter6', + } + ] + } + }, + baseFullState = { + filter1: { selected: false, conflicted: false, included: false }, + filter2: { selected: false, conflicted: false, included: false }, + filter3: { selected: false, conflicted: false, included: false }, + filter4: { selected: false, conflicted: false, included: false }, + filter5: { selected: false, conflicted: false, included: false }, + filter6: { selected: false, conflicted: false, included: false } + }, + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( definition ); + + assert.deepEqual( + model.getFullState(), + baseFullState, + 'Initial state: no conflicts because no selections.' + ); + + // Select a filter that has a conflict with another + model.updateFilters( { + filter1: true // conflicts: filter2, filter4 + } ); + + model.reassessFilterInteractions( model.getItemByName( 'filter1' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullState, { + filter1: { selected: true }, + filter2: { conflicted: true }, + filter4: { conflicted: true }, + } ), + 'Selecting a filter set its conflicts list as "conflicted".' + ); + + // Select one of the conflicts (both filters are now conflicted and selected) + model.updateFilters( { + filter4: true // conflicts: filter 1 + } ); + model.reassessFilterInteractions( model.getItemByName( 'filter4' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullState, { + filter1: { selected: true, conflicted: true }, + filter2: { conflicted: true }, + filter4: { selected: true, conflicted: true }, + } ), + 'Selecting a conflicting filter sets both sides to conflicted and selected.' + ); + + // Select another filter from filter4 group, meaning: + // now filter1 no longer conflicts with filter4 + model.updateFilters( { + 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 } + } ), + 'Selecting a non-conflicting filter from a conflicting group removes the conflict' + ); + } ); }( mediaWiki, jQuery ) ); -- 2.20.1