Some filters conflict with other filters, and the state should be shown properly.
Bug: T156861
Change-Id: I1649e01a80e5a2a576af0a90df20302887631284
} );
};
+ /**
+ * 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
*
* @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;
};
} );
}
} );
+
+ // 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 );
+ } );
};
/**
'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 ) );