/**
* Defines a generic RulesEditor class. This class can't be used directly, but
* should be inherited, with the inheriting class defining a list of column
* types and corresponding relations and condition data types, like so:
*
* class MyRulesEditor extends RulesEditor {};
* MyRulesEditor.defaultProps = {
* columns: {
* title: {
* label: 'Product title',
* column: 'title',
* relations: RulesEditor.buildRelationsObj([RulesEditor.EQUALS_STRING])
* }
* }
*/
class RulesEditor extends React.Component {
/**
* Initialise the Rules Editor, and set the initial state.
* @param props
*/
constructor(props) {
super(props);
this.state = {
rules: props.rules.map((rule, i) => {
return Object.assign({}, rule, {
column: Object.keys(this.props.columns).filter((columnKey) => {
return this.props.columns[columnKey].column == rule.column;
})[0]
});
})
}
}
componentDidMount() {
if(this.state.rules.length === 0 && this.props.blankOk === false) {
this.onAddRule();
}
}
/**
* Add a new rule, with an immutable state change.
*/
onAddRule() {
const column = Object.keys(this.props.columns)[0];
const relation = this.getNextRelation(column);
const condition = this.getNextCondition(column, relation);
this.setState({
rules: this.state.rules.concat([{
column,
relation,
condition
}])
});
}
/**
* Remove a rule, with an immutable state change.
*/
onRemoveRule(index) {
this.setState({
rules: [
...this.state.rules.slice(0, index),
...this.state.rules.slice(index + 1)
]
});
}
/**
* Handle a change in a rule's column attribute.
*
* @param index
* @param column
*/
onRuleColumnChange(index, column) {
const relation = this.getNextRelation(column, this.state.rules[index]);
const condition = this.getNextCondition(column, relation, this.state.rules[index]);
this.updateRule(index, {
column,
relation,
condition
})
}
/**
* Handle a change in a rule's relation attribute.
*
* @param index
* @param relation
*/
onRuleRelationChange(index, relation) {
const condition = this.getNextCondition(this.state.rules[index].column, relation, this.state.rules[index]);
this.updateRule(index, {
relation,
condition
});
}
/**
* Handle a change in a rule's condition attribute.
*
* @param index
* @param condition
*/
onRuleConditionChange(index, condition) {
this.updateRule(index, {
condition
});
}
/**
* Handle a change in a rule's variables
*
* @param index
* @param name
* @param value
*/
onRuleVariableChange(index, name, value) {
this.updateRule(index, {
[name]: value
});
}
/**
* Given the column we're changing to and the current rule, return the next
* relation value.
*
* @param nextColumn
* @param currentRule
*/
getNextRelation(nextColumn, currentRule) {
// If the new column provides for the same relation, keep it.
if(currentRule && (this.props.columns[nextColumn].relations[currentRule.relation] !== undefined)) {
return currentRule.relation;
}
// Otherwise, return the first relation for the new column.
return Object.keys(this.props.columns[nextColumn].relations)[0];
}
/**
* Given the column and relation we're changing to and the current condition,
* return the value that the condition should be changed to.
*
* @param nextColumn
* @param nextRelation
* @param currentRule
*/
getNextCondition(nextColumn, nextRelation, currentRule) {
// If the new relation provides for the same condition type, keep it.
if(currentRule) {
const currentConditionType = this.props.columns[currentRule.column].relations[currentRule.relation].type;
const nextConditionType = this.props.columns[nextColumn].relations[nextRelation].type;
if(currentConditionType === nextConditionType) {
return currentRule.condition;
}
}
// Otherwise, reset the condition to an empty string.
return '';
}
/**
* Handle the updating of a rule in our array in an immutable manner.
*
* @param index
* @param updates
*/
updateRule(index, updates) {
let updatedRule = Object.assign({}, this.state.rules[index], updates);
// Ensure only valid variables are present in the rule
const columnPath = this.props.columns[updatedRule.column].column;
const columnVariables = RulesEditor.getColumnPathVariables(columnPath);
Object.keys(updatedRule).forEach(function (key) {
if ('$' === key[0] && -1 === columnVariables.indexOf(key)) {
delete updatedRule[key];
}
});
this.setState({
rules: [
...this.state.rules.slice(0, index),
updatedRule,
...this.state.rules.slice(index + 1)
]
});
}
/**
* Render the Rules Editor.
*/
render() {
const { name } = this.props;
const { rules } = this.state;
const ruleElements = rules.map((rule, i) => {
return
});
// Convert the current rules JSON into a format using the correct column
// format used by our more advanced key checker.
const rulesJSON = JSON.stringify(this.state.rules.map((rule, i) => {
return Object.assign({}, rule, {
column: this.props.columns[rule.column].column
});
}));
return(
{ruleElements}
);
}
/**
* Get the variable names present in a column definition
*
* @param column
* @returns {Array.}
*/
static getColumnPathVariables(column) {
return column.split(/[^$a-z_A-Z]/).filter((key) => '$' === key[0]);
}
/**
* Return a relations object, which is just the passed array
* turned into an object which is keyed by the `relation` value
*
* @param relations
* @returns {{}}
*/
static buildRelationsObj(relations) {
var relationsObj = {};
relations.forEach(function (relation) {
return relationsObj[relation.relation] = relation;
});
return relationsObj;
}
}
RulesEditor.EQUALS_STRING = {
label: 'is equal to',
relation: 'is_equal_to',
type: 'text'
};
RulesEditor.NOT_EQUALS_STRING = {
label: 'is not equal to',
relation: 'is_not_equal_to',
type: 'text'
};
RulesEditor.EQUALS_COUNTRY_CODE = {
label: 'is equal to',
relation: 'is_equal_to',
type: 'country_code'
};
RulesEditor.NOT_EQUALS_COUNTRY_CODE = {
label: 'is not equal to',
relation: 'is_not_equal_to',
type: 'country_code'
};
RulesEditor.CONTAINS_STRING = {
label: 'contains',
relation: 'contains_string',
type: 'text'
};
RulesEditor.DOES_NOT_CONTAIN_STRING = {
label: 'does not contain',
relation: 'does_not_contain_string',
type: 'text'
};
RulesEditor.EQUALS_TAG = {
label: 'is equal to',
relation: 'find_in_set',
type: 'tag'
};
RulesEditor.GREATER_THAN = {
label: 'is greater than',
relation: 'is_greater_than',
type: 'numeric'
};
RulesEditor.LESS_THAN = {
label: 'is less than',
relation: 'is_less_than',
type: 'numeric'
};
const RulesEditorRule = ({ rule, columns, onRemove, onColumnChange, onVariableChange, onRelationChange, onConditionChange, ruleCount, blankOk }) => {
const { column, relation, condition } = rule;
const currentColumn = columns[column];
const currentRelation = currentColumn.relations[relation];
let conditionEditor = null;
switch(currentRelation.type) {
case 'text':
case 'numeric':
case 'tag':
conditionEditor = ;
break;
case 'country_code':
conditionEditor = ;
break;
}
let deleteIconCell = null;
if(ruleCount > 1 || blankOk === true) {
deleteIconCell = (