@coocoon/react-awesome-query-builder
Version:
User-friendly query builder for React. Demo: https://ukrbublik.github.io/react-awesome-query-builder
1,275 lines (1,168 loc) • 34.5 kB
JavaScript
import Immutable from "immutable";
import {
expandTreePath,
expandTreeSubpath,
getItemByPath,
fixPathsInTree,
getTotalRulesCountInTree,
fixEmptyGroupsInTree,
isEmptyTree,
hasChildren,
removeIsLockedInTree,
} from "../utils/treeUtils";
import {
defaultRuleProperties,
defaultGroupProperties,
defaultOperator,
defaultOperatorOptions,
defaultRoot,
defaultItemProperties,
} from "../utils/defaultUtils";
import * as constants from "../constants";
import uuid from "../utils/uuid";
import {
getFuncConfig,
getFieldConfig,
getFieldWidgetConfig,
getOperatorConfig,
} from "../utils/configUtils";
import {
getOperatorsForField,
getFirstOperator,
getWidgetForFieldOp,
getNewValueForFieldOp,
} from "../utils/ruleUtils";
import { deepEqual, defaultValue, applyToJS } from "../utils/stuff";
import { validateValue } from "../utils/validation";
import omit from "lodash/omit";
import mapValues from "lodash/mapValues";
/**
* @param {object} config
* @param {Immutable.List} path
* @param {Immutable.Map} properties
*/
const addNewGroup = (
state,
path,
type,
groupUuid,
properties,
config,
children = null,
meta = {}
) => {
const { shouldCreateEmptyGroup } = config.settings;
const groupPath = path.push(groupUuid);
const canAddNewRule = !shouldCreateEmptyGroup;
const isDefaultCase = !!meta?.isDefaultCase;
const origState = state;
state = addItem(
state,
path,
type,
groupUuid,
defaultGroupProperties(config).merge(properties || {}),
config,
children
);
if (state !== origState) {
if (!children && !isDefaultCase) {
state = state.setIn(
expandTreePath(groupPath, "children1"),
new Immutable.OrderedMap()
);
// Add one empty rule into new group
if (canAddNewRule) {
state = addItem(
state,
groupPath,
"rule",
uuid(),
defaultRuleProperties(config),
config
);
}
}
state = fixPathsInTree(state);
}
return state;
};
/**
* @param {object} config
* @param {Immutable.List} path
* @param {Immutable.Map} properties
*/
const removeGroup = (state, path, config) => {
state = removeItem(state, path);
const { canLeaveEmptyGroup } = config.settings;
const parentPath = path.slice(0, -1);
const isEmptyParentGroup = !hasChildren(state, parentPath);
if (isEmptyParentGroup && !canLeaveEmptyGroup) {
// check ancestors for emptiness (and delete 'em if empty)
state = fixEmptyGroupsInTree(state);
if (isEmptyTree(state) && !canLeaveEmptyGroup) {
// if whole query is empty, add one empty rule to root
state = addItem(
state,
new Immutable.List(),
"rule",
uuid(),
defaultRuleProperties(config),
config
);
}
}
state = fixPathsInTree(state);
return state;
};
/**
* @param {object} config
* @param {Immutable.List} path
*/
const removeRule = (state, path, config) => {
state = removeItem(state, path);
const { canLeaveEmptyGroup } = config.settings;
const parentPath = path.pop();
const parent = state.getIn(expandTreePath(parentPath));
const parentField = parent.getIn(["properties", "field"]);
const parentOperator = parent.getIn(["properties", "operator"]);
const parentValue = parent.getIn(["properties", "value", 0]);
const parentFieldConfig = parentField
? getFieldConfig(config, parentField)
: null;
const parentOperatorConfig = parentOperator
? getOperatorConfig(config, parentOperator, parentField)
: null;
const hasGroupCountRule =
parentField && parentOperator && parentOperatorConfig.cardinality != 0; // && parentValue != undefined;
const isParentRuleGroup = parent.get("type") == "rule_group";
const isEmptyParentGroup = !hasChildren(state, parentPath);
const canLeaveEmpty = isParentRuleGroup
? hasGroupCountRule && parentFieldConfig.initialEmptyWhere
: canLeaveEmptyGroup;
if (isEmptyParentGroup && !canLeaveEmpty) {
if (isParentRuleGroup) {
// deleted last rule from rule_group, so delete whole rule_group
state = state.deleteIn(expandTreePath(parentPath));
}
// check ancestors for emptiness (and delete 'em if empty)
state = fixEmptyGroupsInTree(state);
if (isEmptyTree(state) && !canLeaveEmptyGroup) {
// if whole query is empty, add one empty rule to root
state = addItem(
state,
new Immutable.List(),
"rule",
uuid(),
defaultRuleProperties(config),
config
);
}
}
state = fixPathsInTree(state);
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {bool} not
*/
const setNot = (state, path, not) =>
state.setIn(expandTreePath(path, "properties", "not"), not);
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {bool} lock
*/
const setLock = (state, path, lock) =>
removeIsLockedInTree(
state.setIn(expandTreePath(path, "properties", "isLocked"), lock)
);
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} conjunction
*/
const setConjunction = (state, path, conjunction) =>
state.setIn(expandTreePath(path, "properties", "conjunction"), conjunction);
// convert children deeply from JS to Immutable
const _addChildren1 = (config, item, children) => {
if (children && Array.isArray(children)) {
item.children1 = new Immutable.OrderedMap(
children.reduce((map, it) => {
const id1 = uuid();
const it1 = {
...it,
properties: defaultItemProperties(config, it).merge(
it.properties || {}
),
id: id1,
};
_addChildren1(config, it1, it1.children1);
//todo: guarantee order
return {
...map,
[id1]: new Immutable.Map(it1),
};
}, {})
);
}
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} type
* @param {string} id
* @param {Immutable.OrderedMap} properties
* @param {object} config
*/
const addItem = (
state,
path,
type,
id,
properties,
config,
children = null
) => {
if (type == "switch_group")
throw new Error("Can't add switch_group programmatically");
const { maxNumberOfCases, maxNumberOfRules, maxNesting } = config.settings;
const rootType = state.get("type");
const isTernary = rootType == "switch_group";
const targetItem = state.getIn(expandTreePath(path));
const caseGroup = isTernary
? state.getIn(expandTreePath(path.take(2)))
: null;
const childrenPath = expandTreePath(path, "children1");
const targetChildren = state.getIn(childrenPath);
const hasChildren = !!targetChildren && targetChildren.size;
const targetChildrenSize = hasChildren ? targetChildren.size : null;
let currentNumber, maxNumber;
if (type == "case_group") {
currentNumber = targetChildrenSize;
maxNumber = maxNumberOfCases;
} else if (type == "group") {
currentNumber = path.size;
maxNumber = maxNesting;
} else if (targetItem?.get("type") == "rule_group") {
// don't restrict
} else {
currentNumber = isTernary
? getTotalRulesCountInTree(caseGroup)
: getTotalRulesCountInTree(state);
maxNumber = maxNumberOfRules;
}
const canAdd = maxNumber && currentNumber ? currentNumber < maxNumber : true;
const item = { type, id, properties };
_addChildren1(config, item, children);
const isLastDefaultCase =
type == "case_group" &&
hasChildren &&
targetChildren.last().get("children1") == null;
if (canAdd) {
const newChildren = new Immutable.OrderedMap({
[id]: new Immutable.Map(item),
});
if (!hasChildren) {
state = state.setIn(childrenPath, newChildren);
} else if (isLastDefaultCase) {
const last = targetChildren.last();
const newChildrenWithLast = new Immutable.OrderedMap({
[id]: new Immutable.Map(item),
[last.get("id")]: last,
});
state = state.deleteIn(
expandTreePath(childrenPath, "children1", last.get("id"))
);
state = state.mergeIn(childrenPath, newChildrenWithLast);
} else {
state = state.mergeIn(childrenPath, newChildren);
}
state = fixPathsInTree(state);
}
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
*/
const removeItem = (state, path) => {
state = state.deleteIn(expandTreePath(path));
state = fixPathsInTree(state);
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} fromPath
* @param {Immutable.List} toPath
* @param {string} placement, see constants PLACEMENT_*: PLACEMENT_AFTER, PLACEMENT_BEFORE, PLACEMENT_APPEND, PLACEMENT_PREPEND
* @param {object} config
*/
const moveItem = (state, fromPath, toPath, placement, config) => {
const from = getItemByPath(state, fromPath);
const sourcePath = fromPath.pop();
const source = fromPath.size > 1 ? getItemByPath(state, sourcePath) : null;
const sourceChildren = source ? source.get("children1") : null;
const to = getItemByPath(state, toPath);
const targetPath =
placement == constants.PLACEMENT_APPEND ||
placement == constants.PLACEMENT_PREPEND
? toPath
: toPath.pop();
const target =
placement == constants.PLACEMENT_APPEND ||
placement == constants.PLACEMENT_PREPEND
? to
: toPath.size > 1
? getItemByPath(state, targetPath)
: null;
const targetChildren = target ? target.get("children1") : null;
if (!source || !target) return state;
const isSameParent = source.get("id") == target.get("id");
const isSourceInsideTarget =
targetPath.size < sourcePath.size &&
deepEqual(
targetPath.toArray(),
sourcePath.toArray().slice(0, targetPath.size)
);
const isTargetInsideSource =
targetPath.size > sourcePath.size &&
deepEqual(
sourcePath.toArray(),
targetPath.toArray().slice(0, sourcePath.size)
);
let sourceSubpathFromTarget = null;
let targetSubpathFromSource = null;
if (isSourceInsideTarget) {
sourceSubpathFromTarget = Immutable.List(
sourcePath.toArray().slice(targetPath.size)
);
} else if (isTargetInsideSource) {
targetSubpathFromSource = Immutable.List(
targetPath.toArray().slice(sourcePath.size)
);
}
let newTargetChildren = targetChildren,
newSourceChildren = sourceChildren;
if (!isTargetInsideSource)
newSourceChildren = newSourceChildren.delete(from.get("id"));
if (isSameParent) {
newTargetChildren = newSourceChildren;
} else if (isSourceInsideTarget) {
newTargetChildren = newTargetChildren.updateIn(
expandTreeSubpath(sourceSubpathFromTarget, "children1"),
(_oldChildren) => newSourceChildren
);
}
if (
placement == constants.PLACEMENT_BEFORE ||
placement == constants.PLACEMENT_AFTER
) {
newTargetChildren = Immutable.OrderedMap().withMutations((r) => {
for (let [itemId, item] of newTargetChildren.entries()) {
if (itemId == to.get("id") && placement == constants.PLACEMENT_BEFORE) {
r.set(from.get("id"), from);
}
r.set(itemId, item);
if (itemId == to.get("id") && placement == constants.PLACEMENT_AFTER) {
r.set(from.get("id"), from);
}
}
});
} else if (placement == constants.PLACEMENT_APPEND) {
newTargetChildren = newTargetChildren.merge({ [from.get("id")]: from });
} else if (placement == constants.PLACEMENT_PREPEND) {
newTargetChildren = Immutable.OrderedMap({ [from.get("id")]: from }).merge(
newTargetChildren
);
}
if (isTargetInsideSource) {
newSourceChildren = newSourceChildren.updateIn(
expandTreeSubpath(targetSubpathFromSource, "children1"),
(_oldChildren) => newTargetChildren
);
newSourceChildren = newSourceChildren.delete(from.get("id"));
}
if (!isSameParent && !isSourceInsideTarget)
state = state.updateIn(
expandTreePath(sourcePath, "children1"),
(_oldChildren) => newSourceChildren
);
if (!isTargetInsideSource)
state = state.updateIn(
expandTreePath(targetPath, "children1"),
(_oldChildren) => newTargetChildren
);
state = fixPathsInTree(state);
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} field
*/
const setField = (state, path, newField, config) => {
if (!newField) return removeItem(state, path);
const { fieldSeparator, setOpOnChangeField, showErrorMessage } =
config.settings;
if (Array.isArray(newField)) newField = newField.join(fieldSeparator);
const currentType = state.getIn(expandTreePath(path, "type"));
const currentProperties = state.getIn(expandTreePath(path, "properties"));
if (newField.isFunc) {
const { field } = newField;
if (newField.arg) {
//change arg
const { index, value } = newField.arg;
if (currentProperties.has("args")) {
const args = currentProperties.get("args");
if (index < args.size) {
var argMap = args.get(index);
argMap = argMap.set("value", value);
state = state.setIn(
expandTreePath(path, "properties", "args", index),
argMap
);
}
}
} else if (newField.args) {
//change func
var newArgs = Immutable.List();
for (let index = 0; index < newField.args.length; index++) {
var argMap = Immutable.OrderedMap();
var newArg = newField.args[index];
argMap = argMap.set("valueSrc", newArg.valueSrc);
argMap = argMap.set("valueType", newArg.valueType);
argMap = argMap.set("value", newArg.value);
newArgs = newArgs.push(argMap);
}
state = state.setIn(expandTreePath(path, "properties", "args"), newArgs);
state = state.setIn(
expandTreePath(path, "properties", "field"),
newField.field
);
state = state.setIn(
expandTreePath(path, "properties", "operator"),
newField.operator
);
state = state.setIn(
expandTreePath(path, "properties", "isFunc"),
true
);
}
return state;
}
const wasRuleGroup = currentType == "rule_group";
const newFieldConfig = getFieldConfig(config, newField);
const isRuleGroup = newFieldConfig.type == "!group";
const isRuleGroupExt = isRuleGroup && newFieldConfig.mode == "array";
const isChangeToAnotherType = wasRuleGroup != isRuleGroup;
const currentOperator = currentProperties.get("operator");
const currentOperatorOptions = currentProperties.get("operatorOptions");
const _currentField = currentProperties.get("field");
const _currentValue = currentProperties.get("value");
const _currentValueSrc = currentProperties.get(
"valueSrc",
new Immutable.List()
);
const _currentValueType = currentProperties.get(
"valueType",
new Immutable.List()
);
// If the newly selected field supports the same operator the rule currently
// uses, keep it selected.
const lastOp =
newFieldConfig && newFieldConfig.operators.indexOf(currentOperator) !== -1
? currentOperator
: null;
let newOperator = null;
const availOps = getOperatorsForField(config, newField);
if (availOps && availOps.length == 1) newOperator = availOps[0];
else if (availOps && availOps.length > 1) {
for (let strategy of setOpOnChangeField || []) {
if (strategy == "keep" && !isChangeToAnotherType) newOperator = lastOp;
else if (strategy == "default")
newOperator = defaultOperator(config, newField, false);
else if (strategy == "first")
newOperator = getFirstOperator(config, newField);
if (newOperator)
//found op for strategy
break;
}
}
if (!isRuleGroup && !newFieldConfig.operators) {
console.warn(`Type ${newFieldConfig.type} is not supported`);
return state;
}
if (wasRuleGroup && !isRuleGroup) {
state = state.setIn(expandTreePath(path, "type"), "rule");
state = state.deleteIn(expandTreePath(path, "children1"));
state = state.setIn(
expandTreePath(path, "properties"),
new Immutable.OrderedMap()
);
}
if (isRuleGroup) {
state = state.setIn(expandTreePath(path, "type"), "rule_group");
const {
canReuseValue,
newValue,
newValueSrc,
newValueType,
operatorCardinality,
} = getNewValueForFieldOp(
config,
config,
currentProperties,
newField,
newOperator,
"field",
true
);
let groupProperties = defaultGroupProperties(config, newFieldConfig).merge({
field: newField,
mode: newFieldConfig.mode,
});
if (isRuleGroupExt) {
groupProperties = groupProperties.merge({
operator: newOperator,
value: newValue,
valueSrc: newValueSrc,
valueType: newValueType,
});
}
state = state.setIn(
expandTreePath(path, "children1"),
new Immutable.OrderedMap()
);
state = state.setIn(expandTreePath(path, "properties"), groupProperties);
if (newFieldConfig.initialEmptyWhere && operatorCardinality == 1) {
// just `COUNT(grp) > 1` without `HAVING ..`
// no childeren
} else {
state = addItem(
state,
path,
"rule",
uuid(),
defaultRuleProperties(config, newField),
config
);
}
state = fixPathsInTree(state);
return state;
}
return state.updateIn(expandTreePath(path, "properties"), (map) =>
map.withMutations((current) => {
const {
canReuseValue,
newValue,
newValueSrc,
newValueType,
newValueError,
} = getNewValueForFieldOp(
config,
config,
current,
newField,
newOperator,
"field",
true
);
if (showErrorMessage) {
current = current.set("valueError", newValueError);
}
const newOperatorOptions = canReuseValue
? currentOperatorOptions
: defaultOperatorOptions(config, newOperator, newField);
return current
.set("field", newField)
.set("operator", newOperator)
.set("operatorOptions", newOperatorOptions)
.set("value", newValue)
.set("valueSrc", newValueSrc)
.set("valueType", newValueType)
.delete("asyncListValues");
})
);
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} operator
*/
const setOperator = (state, path, newOperator, config) => {
const { showErrorMessage } = config.settings;
const properties = state.getIn(expandTreePath(path, "properties"));
const children = state.getIn(expandTreePath(path, "children1"));
const currentField = properties.get("field");
const isFunc = properties.get("isFunc");
const fieldConfig = getFieldConfig(config, currentField, isFunc);
const isRuleGroup = fieldConfig.type == "!group";
const operatorConfig = getOperatorConfig(
config,
newOperator,
currentField,
isFunc
);
const operatorCardinality = operatorConfig
? defaultValue(operatorConfig.cardinality, 1)
: null;
state = state.updateIn(expandTreePath(path, "properties"), (map) =>
map.withMutations((current) => {
const currentField = current.get("field");
const currentOperatorOptions = current.get("operatorOptions");
const _currentValue = current.get("value", new Immutable.List());
const _currentValueSrc = current.get("valueSrc", new Immutable.List());
const _currentOperator = current.get("operator");
const {
canReuseValue,
newValue,
newValueSrc,
newValueType,
newValueError,
} = getNewValueForFieldOp(
config,
config,
current,
currentField,
newOperator,
"operator",
true,
isFunc
);
if (showErrorMessage) {
current = current.set("valueError", newValueError);
}
const newOperatorOptions = canReuseValue
? currentOperatorOptions
: defaultOperatorOptions(config, newOperator, currentField);
if (!canReuseValue) {
current = current.delete("asyncListValues");
}
return current
.set("operator", newOperator)
.set("operatorOptions", newOperatorOptions)
.set("value", newValue)
.set("valueSrc", newValueSrc)
.set("valueType", newValueType);
})
);
if (isRuleGroup) {
if (operatorCardinality == 0 && children.size == 0) {
state = addItem(
state,
path,
"rule",
uuid(),
defaultRuleProperties(config, currentField),
config
);
}
}
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {integer} delta
* @param {*} value
* @param {string} valueType
* @param {*} asyncListValues
* @param {boolean} __isInternal
*/
const setValue = (
state,
path,
delta,
value,
valueType,
config,
asyncListValues,
__isInternal
) => {
const { fieldSeparator, showErrorMessage } = config.settings;
let isInternalValueChange;
const valueSrc =
state.getIn(expandTreePath(path, "properties", "valueSrc", delta + "")) ||
null;
if (valueSrc === "field" && Array.isArray(value))
value = value.join(fieldSeparator);
const field =
state.getIn(expandTreePath(path, "properties", "field")) || null;
const operator =
state.getIn(expandTreePath(path, "properties", "operator")) || null;
const operatorConfig = getOperatorConfig(config, operator, field);
const operatorCardinality = operator
? defaultValue(operatorConfig.cardinality, 1)
: null;
const isFunc =
state.getIn(expandTreePath(path, "properties", "isFunc")) || false;
const isEndValue = false;
const canFix = false;
const calculatedValueType =
valueType || calculateValueType(value, valueSrc, config);
const [validateError, fixedValue] = validateValue(
config,
field,
field,
operator,
value,
calculatedValueType,
valueSrc,
asyncListValues,
canFix,
isEndValue,
true,
isFunc
);
const isValid = !validateError;
if (isValid && fixedValue !== value) {
// eg, get exact value from listValues (not string)
value = fixedValue;
}
// Additional validation for range values
if (showErrorMessage) {
const w = getWidgetForFieldOp(config, field, operator, valueSrc);
const fieldWidgetDefinition = getFieldWidgetConfig(
config,
field,
operator,
w,
valueSrc
);
const valueSrcs = Array.from(
{ length: operatorCardinality },
(_, i) =>
state.getIn(expandTreePath(path, "properties", "valueSrc", i + "")) ||
null
);
if (
operatorConfig &&
operatorConfig.validateValues &&
valueSrcs.filter((vs) => vs == "value" || vs == null).length ==
operatorCardinality
) {
const values = Array.from({ length: operatorCardinality }, (_, i) =>
i == delta
? value
: state.getIn(expandTreePath(path, "properties", "value", i + "")) ||
null
);
const jsValues =
fieldWidgetDefinition && fieldWidgetDefinition.toJS
? values.map((v) =>
fieldWidgetDefinition.toJS(v, fieldWidgetDefinition)
)
: values;
const rangeValidateError = operatorConfig.validateValues(jsValues);
state = state.setIn(
expandTreePath(path, "properties", "valueError", operatorCardinality),
rangeValidateError
);
}
}
const lastValueArr = state.getIn(expandTreePath(path, "properties", "value"));
if (!lastValueArr) {
state = state
.setIn(
expandTreePath(path, "properties", "value"),
new Immutable.List(new Array(operatorCardinality))
)
.setIn(
expandTreePath(path, "properties", "valueType"),
new Immutable.List(new Array(operatorCardinality))
)
.setIn(
expandTreePath(path, "properties", "valueError"),
new Immutable.List(new Array(operatorCardinality))
);
}
const lastValue = state.getIn(
expandTreePath(path, "properties", "value", delta + "")
);
const lastError = state.getIn(
expandTreePath(path, "properties", "valueError", delta)
);
const isLastEmpty = lastValue == undefined;
const isLastError = !!lastError;
if (isValid || showErrorMessage) {
state = state.deleteIn(
expandTreePath(path, "properties", "asyncListValues")
);
// set only good value
if (typeof value === "undefined") {
state = state.setIn(
expandTreePath(path, "properties", "value", delta + ""),
undefined
);
state = state.setIn(
expandTreePath(path, "properties", "valueType", delta + ""),
null
);
} else {
if (asyncListValues) {
state = state.setIn(
expandTreePath(path, "properties", "asyncListValues"),
asyncListValues
);
}
state = state.setIn(
expandTreePath(path, "properties", "value", delta + ""),
value
);
state = state.setIn(
expandTreePath(path, "properties", "valueType", delta + ""),
calculatedValueType
);
isInternalValueChange = __isInternal && !isLastEmpty && !isLastError;
}
}
if (showErrorMessage) {
state = state.setIn(
expandTreePath(path, "properties", "valueError", delta),
validateError
);
}
if (
__isInternal &&
((isValid && isLastError) || (!isValid && !isLastError))
) {
state = state.setIn(
expandTreePath(path, "properties", "valueError", delta),
validateError
);
isInternalValueChange = false;
}
return { tree: state, isInternalValueChange };
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {integer} delta
* @param {*} srcKey
*/
const setValueSrc = (state, path, delta, srcKey, config) => {
const { showErrorMessage } = config.settings;
const field =
state.getIn(expandTreePath(path, "properties", "field")) || null;
const operator =
state.getIn(expandTreePath(path, "properties", "operator")) || null;
state = state.setIn(
expandTreePath(path, "properties", "value", delta + ""),
undefined
);
state = state.setIn(
expandTreePath(path, "properties", "valueType", delta + ""),
null
);
state = state.deleteIn(expandTreePath(path, "properties", "asyncListValues"));
if (showErrorMessage) {
// clear value error
state = state.setIn(
expandTreePath(path, "properties", "valueError", delta),
null
);
// if current operator is range, clear possible range error
const operatorConfig = getOperatorConfig(config, operator, field);
const operatorCardinality = operator
? defaultValue(operatorConfig.cardinality, 1)
: null;
if (operatorConfig.validateValues) {
state = state.setIn(
expandTreePath(path, "properties", "valueError", operatorCardinality),
null
);
}
}
// set valueSrc
if (typeof srcKey === "undefined") {
state = state.setIn(
expandTreePath(path, "properties", "valueSrc", delta + ""),
null
);
} else {
state = state.setIn(
expandTreePath(path, "properties", "valueSrc", delta + ""),
srcKey
);
}
// maybe set default value
if (srcKey) {
const properties = state.getIn(expandTreePath(path, "properties"));
// this call should return canReuseValue = false and provide default value
const {
canReuseValue,
newValue,
newValueSrc,
newValueType,
newValueError,
} = getNewValueForFieldOp(
config,
config,
properties,
field,
operator,
"valueSrc",
true
);
if (!canReuseValue && newValueSrc.get(delta) == srcKey) {
state = state.setIn(
expandTreePath(path, "properties", "value", delta + ""),
newValue.get(delta)
);
state = state.setIn(
expandTreePath(path, "properties", "valueType", delta + ""),
newValueType.get(delta)
);
}
}
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} name
* @param {*} value
*/
const setOperatorOption = (state, path, name, value) => {
return state.setIn(
expandTreePath(path, "properties", "operatorOptions", name),
value
);
};
/**
* @param {Immutable.Map} state
*/
const checkEmptyGroups = (state, config) => {
const { canLeaveEmptyGroup } = config.settings;
if (!canLeaveEmptyGroup) {
state = fixEmptyGroupsInTree(state);
}
return state;
};
/**
*
*/
const calculateValueType = (value, valueSrc, config) => {
let calculatedValueType = null;
if (value) {
if (valueSrc === "field") {
const fieldConfig = getFieldConfig(config, value);
if (fieldConfig) {
calculatedValueType = fieldConfig.type;
}
} else if (valueSrc === "func") {
const funcKey = value.get("func");
if (funcKey) {
const funcConfig = getFuncConfig(config, funcKey);
if (funcConfig) {
calculatedValueType = funcConfig.returnType;
}
}
}
}
return calculatedValueType;
};
const getField = (state, path) => {
const field =
state.getIn(expandTreePath(path, "properties", "field")) || null;
return field;
};
const emptyDrag = {
dragging: {
id: null,
x: null,
y: null,
w: null,
h: null,
},
mousePos: {},
dragStart: {
id: null,
},
};
const getActionMeta = (action, state) => {
const actionKeysToOmit = ["config", "asyncListValues", "__isInternal"];
const actionTypesToIgnore = [
constants.SET_TREE,
constants.SET_DRAG_START,
constants.SET_DRAG_PROGRESS,
constants.SET_DRAG_END,
];
let meta = mapValues(omit(action, actionKeysToOmit), applyToJS);
let affectedField =
(action.path && getField(state.tree, action.path)) || action.field;
if (affectedField) meta.affectedField = affectedField;
if (
actionTypesToIgnore.includes(action.type) ||
action.type.indexOf("@@redux") == 0
)
meta = null;
return meta;
};
/**
* @param {Immutable.Map} state
* @param {object} action
*/
export default (config) => {
const emptyTree = defaultRoot(config);
const emptyState = Object.assign({}, { tree: emptyTree }, emptyDrag);
return (state = emptyState, action) => {
const unset = {
__isInternalValueChange: undefined,
__lastAction: undefined,
};
let set = {};
let actionMeta = getActionMeta(action, state);
switch (action.type) {
case constants.SET_TREE: {
set.tree = action.tree;
break;
}
case constants.ADD_CASE_GROUP: {
set.tree = addNewGroup(
state.tree,
action.path,
"case_group",
action.id,
action.properties,
action.config,
action.children,
action.meta
);
break;
}
case constants.ADD_GROUP: {
set.tree = addNewGroup(
state.tree,
action.path,
"group",
action.id,
action.properties,
action.config,
action.children,
action.meta
);
break;
}
case constants.REMOVE_GROUP: {
set.tree = removeGroup(state.tree, action.path, action.config);
break;
}
case constants.ADD_RULE: {
set.tree = addItem(
state.tree,
action.path,
action.ruleType,
action.id,
action.properties,
action.config,
action.children
);
break;
}
case constants.REMOVE_RULE: {
set.tree = removeRule(state.tree, action.path, action.config);
break;
}
case constants.SET_CONJUNCTION: {
set.tree = setConjunction(state.tree, action.path, action.conjunction);
break;
}
case constants.SET_NOT: {
set.tree = setNot(state.tree, action.path, action.not);
break;
}
case constants.SET_FIELD: {
set.tree = setField(
state.tree,
action.path,
action.field,
action.config
);
break;
}
case constants.SET_LOCK: {
set.tree = setLock(state.tree, action.path, action.lock);
break;
}
case constants.SET_OPERATOR: {
set.tree = setOperator(
state.tree,
action.path,
action.operator,
action.config
);
break;
}
case constants.SET_VALUE: {
const { tree, isInternalValueChange } = setValue(
state.tree,
action.path,
action.delta,
action.value,
action.valueType,
action.config,
action.asyncListValues,
action.__isInternal
);
set.__isInternalValueChange = isInternalValueChange;
set.tree = tree;
break;
}
case constants.SET_VALUE_SRC: {
set.tree = setValueSrc(
state.tree,
action.path,
action.delta,
action.srcKey,
action.config
);
break;
}
case constants.SET_OPERATOR_OPTION: {
set.tree = setOperatorOption(
state.tree,
action.path,
action.name,
action.value
);
break;
}
case constants.MOVE_ITEM: {
set.tree = moveItem(
state.tree,
action.fromPath,
action.toPath,
action.placement,
action.config
);
break;
}
case constants.SET_DRAG_START: {
set.dragStart = action.dragStart;
set.dragging = action.dragging;
set.mousePos = action.mousePos;
break;
}
case constants.SET_DRAG_PROGRESS: {
set.mousePos = action.mousePos;
set.dragging = action.dragging;
break;
}
case constants.SET_DRAG_END: {
set.tree = checkEmptyGroups(state.tree, config);
set = { ...set, ...emptyDrag };
break;
}
default: {
break;
}
}
if (actionMeta) {
set.__lastAction = actionMeta;
}
return { ...state, ...unset, ...set };
};
};