RCFilters: Add 'single_option' group type
Group type that only allows a single option to be selected from its range of items. Bonus: Add the ability to have a view that is hidden from the menus Bug: T162784 Bug: T162786 Change-Id: Ide93491a49c1405926ac171c7924a469e94c0e0a
This commit is contained in:
parent
788b6f88f4
commit
5ed72ed54c
4 changed files with 245 additions and 67 deletions
|
|
@ -12,6 +12,7 @@
|
|||
* @cfg {string} [view='default'] Name of the display group this group
|
||||
* is a part of.
|
||||
* @cfg {string} [title] Group title
|
||||
* @cfg {boolean} [hidden] This group is hidden from the regular menu views
|
||||
* @cfg {string} [separator='|'] Value separator for 'string_options' groups
|
||||
* @cfg {boolean} [active] Group is active
|
||||
* @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
this.type = config.type || 'send_unselected_if_any';
|
||||
this.view = config.view || 'default';
|
||||
this.title = config.title || name;
|
||||
this.hidden = !!config.hidden;
|
||||
this.separator = config.separator || '|';
|
||||
this.labelPrefixKey = config.labelPrefixKey;
|
||||
|
||||
|
|
@ -156,17 +158,34 @@
|
|||
return item.getParamName();
|
||||
} )
|
||||
).join( this.getSeparator() );
|
||||
} else if ( this.getType() === 'single_option' ) {
|
||||
// For this group, the parameter is the group name,
|
||||
// and a single item can be selected, or none at all
|
||||
// The item also must be recognized or none is set as
|
||||
// default
|
||||
model.defaultParams[ this.getName() ] = this.getItemByParamName( groupDefault ) ? groupDefault : '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to filterItem update event
|
||||
*
|
||||
* @param {mw.rcfilters.dm.FilterItem} item Updated filter item
|
||||
* @fires update
|
||||
*/
|
||||
mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function () {
|
||||
mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
|
||||
// Update state
|
||||
var active = this.areAnySelected();
|
||||
var active = this.areAnySelected(),
|
||||
itemName = item && item.getName();
|
||||
|
||||
if ( item.isSelected() && this.getType() === 'single_option' ) {
|
||||
// Change the selection to only be the newly selected item
|
||||
this.getItems().forEach( function ( filterItem ) {
|
||||
if ( filterItem.getName() !== itemName ) {
|
||||
filterItem.toggleSelected( false );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
if ( this.active !== active ) {
|
||||
this.active = active;
|
||||
|
|
@ -183,6 +202,15 @@
|
|||
return this.active;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get group hidden state
|
||||
*
|
||||
* @return {boolean} Hidden state
|
||||
*/
|
||||
mw.rcfilters.dm.FilterGroup.prototype.isHidden = function () {
|
||||
return this.hidden;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get group name
|
||||
*
|
||||
|
|
@ -388,7 +416,22 @@
|
|||
areAnySelected = false,
|
||||
buildFromCurrentState = !filterRepresentation,
|
||||
result = {},
|
||||
filterParamNames = {};
|
||||
model = this,
|
||||
filterParamNames = {},
|
||||
getSelectedParameter = function ( filters ) {
|
||||
var item,
|
||||
selected = [];
|
||||
|
||||
// Find if any are selected
|
||||
$.each( filters, function ( name, value ) {
|
||||
if ( value ) {
|
||||
selected.push( name );
|
||||
}
|
||||
} );
|
||||
|
||||
item = model.getItemByName( selected[ 0 ] );
|
||||
return ( item && item.getParamName() ) || '';
|
||||
};
|
||||
|
||||
filterRepresentation = filterRepresentation || {};
|
||||
|
||||
|
|
@ -438,6 +481,8 @@
|
|||
|
||||
result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
|
||||
'all' : values.join( this.getSeparator() );
|
||||
} else if ( this.getType() === 'single_option' ) {
|
||||
result[ this.getName() ] = getSelectedParameter( filterRepresentation );
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -513,6 +558,11 @@
|
|||
// Otherwise, the filter is selected only if it appears in the parameter values
|
||||
paramValues.indexOf( filterItem.getParamName() ) > -1;
|
||||
} );
|
||||
} else if ( this.getType() === 'single_option' ) {
|
||||
// There is parameter that fits a single filter, or none at all
|
||||
this.getItems().forEach( function ( filterItem ) {
|
||||
result[ filterItem.getName() ] = filterItem.getParamName() === paramRepresentation;
|
||||
} );
|
||||
}
|
||||
|
||||
// Go over result and make sure all filters are represented.
|
||||
|
|
@ -524,6 +574,18 @@
|
|||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get item by its filter name
|
||||
*
|
||||
* @param {string} filterName Filter name
|
||||
* @return {mw.rcfilters.dm.FilterItem} Filter item
|
||||
*/
|
||||
mw.rcfilters.dm.FilterGroup.prototype.getItemByName = function ( filterName ) {
|
||||
return this.getItems().filter( function ( item ) {
|
||||
return item.getName() === filterName;
|
||||
} )[ 0 ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get item by its parameter name
|
||||
*
|
||||
|
|
|
|||
|
|
@ -349,10 +349,12 @@
|
|||
viewData.groups.forEach( function ( groupData ) {
|
||||
var group = groupData.name;
|
||||
|
||||
model.groups[ group ] = new mw.rcfilters.dm.FilterGroup(
|
||||
group,
|
||||
$.extend( true, {}, groupData, { view: viewName } )
|
||||
);
|
||||
if ( !model.groups[ group ] ) {
|
||||
model.groups[ group ] = new mw.rcfilters.dm.FilterGroup(
|
||||
group,
|
||||
$.extend( true, {}, groupData, { view: viewName } )
|
||||
);
|
||||
}
|
||||
|
||||
model.groups[ group ].initializeFilters( groupData.filters, groupData.default );
|
||||
items = items.concat( model.groups[ group ].getItems() );
|
||||
|
|
@ -399,7 +401,10 @@
|
|||
groupModel.getItems().forEach( function ( filterItem ) {
|
||||
model.parameterMap[ filterItem.getParamName() ] = filterItem;
|
||||
} );
|
||||
} else if ( groupModel.getType() === 'string_options' ) {
|
||||
} else if (
|
||||
groupModel.getType() === 'string_options' ||
|
||||
groupModel.getType() === 'single_option'
|
||||
) {
|
||||
// Group
|
||||
model.parameterMap[ groupModel.getName() ] = groupModel;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,46 +133,50 @@
|
|||
|
||||
// Count groups per view
|
||||
$.each( groups, function ( groupName, groupModel ) {
|
||||
viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
|
||||
viewGroupCount[ groupModel.getView() ]++;
|
||||
if ( !groupModel.isHidden() ) {
|
||||
viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
|
||||
viewGroupCount[ groupModel.getView() ]++;
|
||||
}
|
||||
} );
|
||||
|
||||
$.each( groups, function ( groupName, groupModel ) {
|
||||
var currentItems = [],
|
||||
view = groupModel.getView();
|
||||
|
||||
if ( viewGroupCount[ view ] > 1 ) {
|
||||
// Only add a section header if there is more than
|
||||
// one group
|
||||
currentItems.push(
|
||||
// Group section
|
||||
new mw.rcfilters.ui.FilterMenuSectionOptionWidget(
|
||||
widget.controller,
|
||||
groupModel,
|
||||
{
|
||||
$overlay: widget.$overlay
|
||||
}
|
||||
)
|
||||
);
|
||||
if ( !groupModel.isHidden() ) {
|
||||
if ( viewGroupCount[ view ] > 1 ) {
|
||||
// Only add a section header if there is more than
|
||||
// one group
|
||||
currentItems.push(
|
||||
// Group section
|
||||
new mw.rcfilters.ui.FilterMenuSectionOptionWidget(
|
||||
widget.controller,
|
||||
groupModel,
|
||||
{
|
||||
$overlay: widget.$overlay
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Add items
|
||||
widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
|
||||
currentItems.push(
|
||||
new mw.rcfilters.ui.FilterMenuOptionWidget(
|
||||
widget.controller,
|
||||
filterItem,
|
||||
{
|
||||
$overlay: widget.$overlay
|
||||
}
|
||||
)
|
||||
);
|
||||
} );
|
||||
|
||||
// Cache the items per view, so we can switch between them
|
||||
// without rebuilding the widgets each time
|
||||
widget.views[ view ] = widget.views[ view ] || [];
|
||||
widget.views[ view ] = widget.views[ view ].concat( currentItems );
|
||||
}
|
||||
|
||||
// Add items
|
||||
widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
|
||||
currentItems.push(
|
||||
new mw.rcfilters.ui.FilterMenuOptionWidget(
|
||||
widget.controller,
|
||||
filterItem,
|
||||
{
|
||||
$overlay: widget.$overlay
|
||||
}
|
||||
)
|
||||
);
|
||||
} );
|
||||
|
||||
// Cache the items per view, so we can switch between them
|
||||
// without rebuilding the widgets each time
|
||||
widget.views[ view ] = widget.views[ view ] || [];
|
||||
widget.views[ view ] = widget.views[ view ].concat( currentItems );
|
||||
} );
|
||||
|
||||
this.switchView( this.model.getCurrentView() );
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@
|
|||
{ name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc' },
|
||||
{ name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc' }
|
||||
]
|
||||
}, {
|
||||
name: 'group4',
|
||||
type: 'single_option',
|
||||
default: 'option1',
|
||||
filters: [
|
||||
{ name: 'option1', label: 'group4option1-label', description: 'group4option1-desc' },
|
||||
{ name: 'option2', label: 'group4option2-label', description: 'group4option2-desc' },
|
||||
{ name: 'option3', label: 'group4option3-label', description: 'group4option3-desc' }
|
||||
]
|
||||
} ],
|
||||
viewsDefinition = {
|
||||
namespaces: {
|
||||
|
|
@ -81,6 +90,7 @@
|
|||
filter5: '1',
|
||||
filter6: '0',
|
||||
group3: 'filter8',
|
||||
group4: 'option1',
|
||||
namespace: ''
|
||||
},
|
||||
baseParamRepresentation = {
|
||||
|
|
@ -91,6 +101,7 @@
|
|||
filter5: '0',
|
||||
filter6: '0',
|
||||
group3: '',
|
||||
group4: '',
|
||||
namespace: ''
|
||||
},
|
||||
baseFilterRepresentation = {
|
||||
|
|
@ -103,6 +114,9 @@
|
|||
group3__filter7: false,
|
||||
group3__filter8: false,
|
||||
group3__filter9: false,
|
||||
group4__option1: false,
|
||||
group4__option2: false,
|
||||
group4__option3: false,
|
||||
namespace__0: false,
|
||||
namespace__1: false,
|
||||
namespace__2: false,
|
||||
|
|
@ -118,6 +132,9 @@
|
|||
group3__filter7: { selected: false, conflicted: false, included: false },
|
||||
group3__filter8: { selected: false, conflicted: false, included: false },
|
||||
group3__filter9: { selected: false, conflicted: false, included: false },
|
||||
group4__option1: { selected: false, conflicted: false, included: false },
|
||||
group4__option2: { selected: false, conflicted: false, included: false },
|
||||
group4__option3: { selected: false, conflicted: false, included: false },
|
||||
namespace__0: { selected: false, conflicted: false, included: false },
|
||||
namespace__1: { selected: false, conflicted: false, included: false },
|
||||
namespace__2: { selected: false, conflicted: false, included: false },
|
||||
|
|
@ -360,6 +377,36 @@
|
|||
} ),
|
||||
'All filters selected in "string_option" group returns \'all\'.'
|
||||
);
|
||||
|
||||
// Reset
|
||||
model = new mw.rcfilters.dm.FiltersViewModel();
|
||||
model.initializeFilters( filterDefinition, viewsDefinition );
|
||||
|
||||
// Select an option from single_option group
|
||||
model.toggleFiltersSelected( {
|
||||
group4__option2: true
|
||||
} );
|
||||
// All filters of the group are selected == this is the same as not selecting any
|
||||
assert.deepEqual(
|
||||
model.getParametersFromFilters(),
|
||||
$.extend( true, {}, baseParamRepresentation, {
|
||||
group4: 'option2'
|
||||
} ),
|
||||
'Selecting an option from "single_option" group returns that option as a value.'
|
||||
);
|
||||
|
||||
// Select a different option from single_option group
|
||||
model.toggleFiltersSelected( {
|
||||
group4__option3: true
|
||||
} );
|
||||
// All filters of the group are selected == this is the same as not selecting any
|
||||
assert.deepEqual(
|
||||
model.getParametersFromFilters(),
|
||||
$.extend( true, {}, baseParamRepresentation, {
|
||||
group4: 'option3'
|
||||
} ),
|
||||
'Selecting a different option from "single_option" group changes the selection.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'getParametersFromFilters (custom object)', function ( assert ) {
|
||||
|
|
@ -396,7 +443,26 @@
|
|||
{ name: 'filter8', label: 'Hide filter 8', description: '' },
|
||||
{ name: 'filter9', label: 'Hide filter 9', description: '' }
|
||||
]
|
||||
}, {
|
||||
name: 'group4',
|
||||
title: 'Group 4',
|
||||
type: 'single_option',
|
||||
filters: [
|
||||
{ name: 'filter10', label: 'Hide filter 10', description: '' },
|
||||
{ name: 'filter11', label: 'Hide filter 11', description: '' },
|
||||
{ name: 'filter12', label: 'Hide filter 12', description: '' }
|
||||
]
|
||||
} ],
|
||||
baseResult = {
|
||||
hidefilter1: '0',
|
||||
hidefilter2: '0',
|
||||
hidefilter3: '0',
|
||||
hidefilter4: '0',
|
||||
hidefilter5: '0',
|
||||
hidefilter6: '0',
|
||||
group3: '',
|
||||
group4: ''
|
||||
},
|
||||
cases = [
|
||||
{
|
||||
// This is mocking the cases above, both
|
||||
|
|
@ -413,17 +479,12 @@
|
|||
group3__filter8: true,
|
||||
group3__filter9: false
|
||||
},
|
||||
expected: {
|
||||
expected: $.extend( true, {}, baseResult, {
|
||||
// Group 1 (two selected, the others are true)
|
||||
hidefilter1: '0',
|
||||
hidefilter2: '0',
|
||||
hidefilter3: '1',
|
||||
// Group 2 (nothing is selected, all false)
|
||||
hidefilter4: '0',
|
||||
hidefilter5: '0',
|
||||
hidefilter6: '0',
|
||||
// Group 3 (two selected)
|
||||
group3: 'filter7,filter8'
|
||||
},
|
||||
} ),
|
||||
msg: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
|
||||
},
|
||||
{
|
||||
|
|
@ -432,30 +493,35 @@
|
|||
input: {
|
||||
group1__hidefilter1: 1
|
||||
},
|
||||
expected: {
|
||||
expected: $.extend( true, {}, baseResult, {
|
||||
// Group 1 (one selected, the others are true)
|
||||
hidefilter1: '0',
|
||||
hidefilter2: '1',
|
||||
hidefilter3: '1',
|
||||
// Group 2 (nothing is selected, all false)
|
||||
hidefilter4: '0',
|
||||
hidefilter5: '0',
|
||||
hidefilter6: '0',
|
||||
group3: ''
|
||||
},
|
||||
hidefilter3: '1'
|
||||
} ),
|
||||
msg: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
|
||||
},
|
||||
{
|
||||
input: {},
|
||||
expected: {
|
||||
hidefilter1: '0',
|
||||
hidefilter2: '0',
|
||||
hidefilter3: '0',
|
||||
hidefilter4: '0',
|
||||
hidefilter5: '0',
|
||||
hidefilter6: '0',
|
||||
group3: ''
|
||||
input: {
|
||||
group4__filter10: true
|
||||
},
|
||||
expected: $.extend( true, {}, baseResult, {
|
||||
group4: 'filter10'
|
||||
} ),
|
||||
msg: 'Given a single value for "single_option" that option is represented in the result.'
|
||||
},
|
||||
{
|
||||
input: {
|
||||
group4__filter10: true,
|
||||
group4__filter11: true
|
||||
},
|
||||
expected: $.extend( true, {}, baseResult, {
|
||||
group4: 'filter10'
|
||||
} ),
|
||||
msg: 'Given more than one true value for "single_option" (which should not happen!) only the first value counts, and the second is ignored.'
|
||||
},
|
||||
{
|
||||
input: {},
|
||||
expected: baseResult,
|
||||
msg: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
|
||||
}
|
||||
];
|
||||
|
|
@ -630,6 +696,47 @@
|
|||
} ),
|
||||
'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
|
||||
);
|
||||
|
||||
model.toggleFiltersSelected(
|
||||
model.getFiltersFromParameters( {
|
||||
group4: 'option1'
|
||||
} )
|
||||
);
|
||||
assert.deepEqual(
|
||||
model.getSelectedState(),
|
||||
$.extend( {}, baseFilterRepresentation, {
|
||||
group4__option1: true
|
||||
} ),
|
||||
'A \'single_option\' parameter reflects a single selected value.'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
model.getFiltersFromParameters( {
|
||||
group4: 'option1,option2'
|
||||
} ),
|
||||
baseFilterRepresentation,
|
||||
'An invalid \'single_option\' parameter is ignored.'
|
||||
);
|
||||
|
||||
// Change to one value
|
||||
model.toggleFiltersSelected(
|
||||
model.getFiltersFromParameters( {
|
||||
group4: 'option1'
|
||||
} )
|
||||
);
|
||||
// Change again to another value
|
||||
model.toggleFiltersSelected(
|
||||
model.getFiltersFromParameters( {
|
||||
group4: 'option2'
|
||||
} )
|
||||
);
|
||||
assert.deepEqual(
|
||||
model.getSelectedState(),
|
||||
$.extend( {}, baseFilterRepresentation, {
|
||||
group4__option2: true
|
||||
} ),
|
||||
'A \'single_option\' parameter always reflects the latest selected value.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'sanitizeStringOptionGroup', function ( assert ) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue