forked from p15670423/monkey
ui: Enable mixed-state behavior for master checkbox in AdavncedMultiSelect
The AdvancedMultiSelect should adhere to some set of human interface guidelines. In the absence of a formal, agreed upon set of guidelines for Infection Monkey, this commit uses KDE's guidelines for checkboxes: https://hig.kde.org/components/editing/checkbox.html When child checkboxes are not all checked, the master checkbox displays a mixed-state icon, instead of a checked icon. Clicking the mixed-state icon checks all child checkboxes. Clicking an unchecked master checkbox also enables all child checkboxes. In the past, clicking an unchecked master checkbox checked only the *default* child checkboxes. While this may seem desirable so that unsafe exploits do not accidentally get selected by the user, it will confuse and frustrate users, as master/child checkboxes do not normally function this way. If there is concern that users may unknowingly select unsafe exploits/options, we should pop up a warning to inform the user when the config is saved/submitted. Issue #891
This commit is contained in:
parent
878f959a8f
commit
19bc09196f
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import {Card, Button, Form} from 'react-bootstrap';
|
import {Card, Button, Form} from 'react-bootstrap';
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
import {faCheckSquare} from '@fortawesome/free-solid-svg-icons';
|
import {faCheckSquare} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {faMinusSquare} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {faSquare} from '@fortawesome/free-regular-svg-icons';
|
import {faSquare} from '@fortawesome/free-regular-svg-icons';
|
||||||
import {cloneDeep} from 'lodash';
|
import {cloneDeep} from 'lodash';
|
||||||
|
|
||||||
|
@ -9,6 +10,12 @@ import {getComponentHeight} from './utils/HeightCalculator';
|
||||||
import {resolveObjectPath} from './utils/ObjectPathResolver';
|
import {resolveObjectPath} from './utils/ObjectPathResolver';
|
||||||
import InfoPane from './InfoPane';
|
import InfoPane from './InfoPane';
|
||||||
|
|
||||||
|
const MasterCheckboxState = {
|
||||||
|
NONE: 0,
|
||||||
|
MIXED: 1,
|
||||||
|
ALL: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Definitions passed to components only contains value and label,
|
// Definitions passed to components only contains value and label,
|
||||||
// custom fields like "info" or "links" must be pulled from registry object using this function
|
// custom fields like "info" or "links" must be pulled from registry object using this function
|
||||||
|
@ -40,12 +47,19 @@ function MasterCheckbox(props) {
|
||||||
checkboxState
|
checkboxState
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
var newCheckboxIcon = faCheckSquare;
|
||||||
|
|
||||||
|
if (checkboxState == MasterCheckboxState.NONE)
|
||||||
|
newCheckboxIcon = faSquare;
|
||||||
|
else if (checkboxState == MasterCheckboxState.MIXED)
|
||||||
|
newCheckboxIcon = faMinusSquare;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Button key={`${title}-button`} value={value}
|
<Button key={`${title}-button`} value={value}
|
||||||
variant={'link'} disabled={disabled}
|
variant={'link'} disabled={disabled}
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
<FontAwesomeIcon icon={checkboxState ? faCheckSquare : faSquare}/>
|
<FontAwesomeIcon icon={newCheckboxIcon}/>
|
||||||
</Button>
|
</Button>
|
||||||
<span className={'header-title'}>{title}</span>
|
<span className={'header-title'}>{title}</span>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -77,42 +91,57 @@ function ChildCheckbox(props) {
|
||||||
class AdvancedMultiSelect extends React.Component {
|
class AdvancedMultiSelect extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {masterCheckbox: true, infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)};
|
this.state = {
|
||||||
|
masterCheckboxState: this.getMasterCheckboxState(props.value),
|
||||||
|
infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)
|
||||||
|
};
|
||||||
this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this);
|
this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this);
|
||||||
this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this);
|
this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this);
|
||||||
this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry);
|
this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMasterCheckboxClick() {
|
onMasterCheckboxClick() {
|
||||||
if (this.state.masterCheckbox) {
|
var newValues = this.props.options.enumOptions.map(({value}) => value);
|
||||||
this.props.onChange([]);
|
|
||||||
} else {
|
if (this.state.masterCheckboxState == MasterCheckboxState.ALL) {
|
||||||
this.props.onChange(this.props.schema.default);
|
newValues = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleMasterCheckbox();
|
this.props.onChange(newValues);
|
||||||
|
this.setMasterCheckboxState(newValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChildCheckboxClick(value) {
|
onChildCheckboxClick(value) {
|
||||||
this.props.onChange(this.getSelectValuesAfterClick(value));
|
var selectValues = this.getSelectValuesAfterClick(value)
|
||||||
|
this.props.onChange(selectValues);
|
||||||
|
|
||||||
|
this.setMasterCheckboxState(selectValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectValuesAfterClick(clickedValue) {
|
getSelectValuesAfterClick(clickedValue) {
|
||||||
const valueArray = cloneDeep(this.props.value);
|
const valueArray = cloneDeep(this.props.value);
|
||||||
|
|
||||||
if (valueArray.includes(clickedValue)) {
|
if (valueArray.includes(clickedValue)) {
|
||||||
return valueArray.filter((e) => {
|
return valueArray.filter(e => e !== clickedValue);
|
||||||
return e !== clickedValue;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
valueArray.push(clickedValue);
|
valueArray.push(clickedValue);
|
||||||
return valueArray;
|
return valueArray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMasterCheckbox() {
|
getMasterCheckboxState(selectValues) {
|
||||||
this.setState((state) => ({
|
if (selectValues.length == 0)
|
||||||
masterCheckbox: !state.masterCheckbox
|
return MasterCheckboxState.NONE;
|
||||||
|
|
||||||
|
if (selectValues.length != this.props.options.enumOptions.length)
|
||||||
|
return MasterCheckboxState.MIXED;
|
||||||
|
|
||||||
|
return MasterCheckboxState.ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMasterCheckboxState(selectValues) {
|
||||||
|
this.setState(() => ({
|
||||||
|
masterCheckboxState: this.getMasterCheckboxState(selectValues)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +161,6 @@ class AdvancedMultiSelect extends React.Component {
|
||||||
readonly,
|
readonly,
|
||||||
multiple,
|
multiple,
|
||||||
autofocus,
|
autofocus,
|
||||||
onChange,
|
|
||||||
registry
|
registry
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -143,7 +171,7 @@ class AdvancedMultiSelect extends React.Component {
|
||||||
<div className={'advanced-multi-select'}>
|
<div className={'advanced-multi-select'}>
|
||||||
<MasterCheckbox title={schema.title} value={value}
|
<MasterCheckbox title={schema.title} value={value}
|
||||||
disabled={disabled} onClick={this.onMasterCheckboxClick}
|
disabled={disabled} onClick={this.onMasterCheckboxClick}
|
||||||
checkboxState={this.state.masterCheckbox}/>
|
checkboxState={this.state.masterCheckboxState}/>
|
||||||
<Form.Group
|
<Form.Group
|
||||||
style={{height: `${getComponentHeight(enumOptions.length)}px`}}
|
style={{height: `${getComponentHeight(enumOptions.length)}px`}}
|
||||||
id={id}
|
id={id}
|
||||||
|
|
Loading…
Reference in New Issue