@react-awesome-query-builder/core
Version:
User-friendly query builder for React. Core
1,176 lines (1,047 loc) • 42.3 kB
JavaScript
import Immutable, { fromJS } from "immutable";
import {
expandTreePath, expandTreeSubpath, getItemByPath, getAncestorRuleGroups, fixPathsInTree,
getTotalRulesCountInTree, fixEmptyGroupsInTree, isEmptyTree, hasChildren, removeIsLockedInTree
} from "../utils/treeUtils";
import {
defaultRuleProperties, defaultGroupProperties, getDefaultOperator,
defaultOperatorOptions, defaultItemProperties,
} from "../utils/defaultRuleUtils";
import * as constants from "./constants";
import uuid from "../utils/uuid";
import {
getFuncConfig, getFieldConfig, getOperatorConfig, selectTypes, getOperatorsForType, getOperatorsForField, getFirstOperator,
} from "../utils/configUtils";
import {
isEmptyItem, calculateValueType
} from "../utils/ruleUtils";
import {deepEqual, getOpCardinality, applyToJS} from "../utils/stuff";
import {validateValue, validateRange} from "../utils/validation";
import {getNewValueForFieldOp} from "../utils/getNewValueForFieldOp";
import {translateValidation} from "../i18n";
import omit from "lodash/omit";
import mapValues from "lodash/mapValues";
import {setFunc, setArgValue, setArgValueSrc, setArgValueAsyncListValues} from "../utils/funcUtils";
/**
* @param {object} config
* @param {Immutable.List} path
* @param {Immutable.Map} properties
*/
const addNewGroup = (state, path, type, generatedId, properties, config, children = null, meta = {}) => {
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
const groupUuid = properties?.get?.("id") || generatedId;
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(fromJS(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, meta?.parentRuleGroupField), config);
}
}
state = fixPathsInTree(state);
}
return state;
};
/**
* @param {object} config
* @param {Immutable.List} path
* @param {Immutable.Map} properties
*/
const removeGroup = (state, path, config) => {
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
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
const canUseDefaultFieldAndOp = false;
const canGetFirst = false;
state = addItem(
state, new Immutable.List(), "rule", uuid(),
defaultRuleProperties(config, undefined, undefined, canUseDefaultFieldAndOp, canGetFirst),
config
);
}
}
state = fixPathsInTree(state);
return state;
};
/**
* @param {object} config
* @param {Immutable.List} path
* @param {Immutable.Map} properties
*/
const removeGroupChildren = (state, path, config) => {
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
state = removeChildren(state, path);
state = fixPathsInTree(state);
return state;
};
/**
* @param {object} config
* @param {Immutable.List} path
*/
const removeRule = (state, path, config) => {
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
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
const canUseDefaultFieldAndOp = false;
const canGetFirst = false;
state = addItem(
state, new Immutable.List(), "rule", uuid(),
defaultRuleProperties(config, undefined, undefined, canUseDefaultFieldAndOp, canGetFirst),
config
);
}
}
state = fixPathsInTree(state);
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {bool} not
*/
const setNot = (state, path, not) => {
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
state = state.setIn(expandTreePath(path, "properties", "not"), not);
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {bool} lock
*/
const setLock = (state, path, lock) => {
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
state = removeIsLockedInTree(state.setIn(expandTreePath(path, "properties", "isLocked"), lock));
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} conjunction
*/
const setConjunction = (state, path, conjunction) => {
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
state = state.setIn(expandTreePath(path, "properties", "conjunction"), conjunction);
return state;
};
/**
* @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, generatedId, properties, config, children = null) => {
if (type === "switch_group")
throw new Error("Can't add switch_group programmatically");
const targetItem = state.getIn(expandTreePath(path));
if (!targetItem) {
// incorrect path
return state;
}
const id = properties?.get?.("id") || generatedId;
const { maxNumberOfCases, maxNumberOfRules, maxNesting } = config.settings;
const rootType = state.get("type");
const isTernary = rootType === "switch_group";
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") {
const ruleGroups = getAncestorRuleGroups(state, path);
if (ruleGroups.length) {
// closest rule-group
const { path: ruleGroupPath, field: ruleGroupField } = ruleGroups[0];
const ruleGroupFieldConfig = getFieldConfig(config, ruleGroupField);
currentNumber = path.size - ruleGroupPath.length;
maxNumber = ruleGroupFieldConfig?.maxNesting;
} else {
currentNumber = path.size;
maxNumber = maxNesting;
}
} else { // rule or rule_group
const ruleGroups = getAncestorRuleGroups(state, path);
if (ruleGroups.length) {
// closest rule-group
const { path: ruleGroupPath, field: ruleGroupField } = ruleGroups[0];
const ruleGroupFieldConfig = getFieldConfig(config, ruleGroupField);
const ruleGroupItem = getItemByPath(state, ruleGroupPath);
maxNumber = ruleGroupFieldConfig?.maxNumberOfRules;
currentNumber = getTotalRulesCountInTree(ruleGroupItem);
} 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} path
*/
const removeChildren = (state, path) => {
state = state.deleteIn(expandTreePath(path, "children1"));
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 || !from) {
// incorrect path
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(Immutable.OrderedMap({[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 {integer} delta
* @param {string} srcKey
*/
const setFieldSrc = (state, path, srcKey, config) => {
const currentRule = state.getIn(expandTreePath(path));
if (!currentRule) {
// incorrect path
return state;
}
const {keepInputOnChangeFieldSrc} = config.settings;
const currentProperties = currentRule.get("properties");
const currentField = currentProperties?.get("field");
const currentFielType = currentProperties?.get("fieldType");
const currentFieldConfig = getFieldConfig(config, currentField);
// const currentType = currentRule.get("type");
// const currentFieldSrc = currentProperties?.get("fieldSrc");
// get fieldType for "memory effect"
let fieldType = currentFieldConfig?.type || currentFielType;
if (!fieldType || fieldType === "!group" || fieldType === "!struct") {
fieldType = null;
}
const canReuseValue = !selectTypes.includes(fieldType);
const keepInput = keepInputOnChangeFieldSrc && !isEmptyItem(currentRule, config) && canReuseValue;
if (!keepInput) {
// clear ALL properties
state = state.setIn(
expandTreePath(path, "properties"),
defaultRuleProperties(config, null, null, false)
);
} else {
// clear non-relevant properties
state = state.setIn(expandTreePath(path, "properties", "field"), null);
state = state.deleteIn(expandTreePath(path, "properties", "fieldError"));
// set fieldType for "memory effect"
state = state.setIn(expandTreePath(path, "properties", "fieldType"), fieldType);
}
// set fieldSrc
state = state.setIn(expandTreePath(path, "properties", "fieldSrc"), srcKey);
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {integer} delta
* @param {Array} parentFuncs
* @param {string | null} argKey
* @param {*} argValue if argKey is null, it's new func key
* @param {string | "!valueSrc"} valueType
* @param {*} asyncListValues
*/
const setFuncValue = (config, state, path, delta, parentFuncs, argKey, argValue, valueType, asyncListValues, _meta = {}) => {
const currentRule = state.getIn(expandTreePath(path));
if (!currentRule) {
// incorrect path
return state;
}
const isLHS = delta === -1;
const currentProperties = currentRule.get("properties");
const currentField = currentProperties.get("field");
const currentValue = currentProperties.get("value");
const currentV = isLHS ? currentField : currentValue.getIn([delta]);
// go inwards
let funcsPath = [];
let targetFV = currentV;
for (const [funcK, argK] of parentFuncs || []) {
funcsPath.push([funcK, argK, targetFV]);
if (funcK !== targetFV.get("func")) {
const funcPath = funcsPath.map(([f, a]) => `${f}(${a})`).join("/") || "root";
throw new Error(
`In ${isLHS ? "LHS" : "RHS"} for path ${funcPath} expected func key ${funcK} but got ${targetFV.get("func")}`
);
}
targetFV = targetFV.getIn(["args", argK, "value"]);
}
// modify
if (!argKey) {
const newFuncKey = argValue;
const canFixArgs = true; // try to fix args to fit new func validations, otherwise - drop invalid args
targetFV = setFunc(targetFV, newFuncKey, config, canFixArgs);
// allow drop invalid args / reset to default, but don't trigger error if some arg is required
// (not same as setting isEndValue = true)
_meta.canDropArgs = true;
} else {
const funcKey = targetFV.get("func");
const funcDefinition = getFuncConfig(config, funcKey);
const {args} = funcDefinition;
const argDefinition = args[argKey];
if (valueType === "!valueSrc") {
targetFV = setArgValueSrc(targetFV, argKey, argValue, argDefinition, config);
} else {
targetFV = setArgValue(targetFV, argKey, argValue, argDefinition, config);
if (asyncListValues) {
targetFV = setArgValueAsyncListValues(targetFV, argKey, asyncListValues, argDefinition, config);
}
}
}
// go outwards
let newV = targetFV;
while (funcsPath.length) {
const [funcK, argK, parentFV] = funcsPath.pop();
const funcDefinition = getFuncConfig(config, funcK);
const {args} = funcDefinition;
const argDefinition = args[argK];
newV = setArgValue(parentFV, argK, newV, argDefinition, config);
}
if (isLHS) {
return setField(state, path, newV, config, undefined, _meta);
} else {
return setValue(state, path, delta, newV, undefined, config, undefined, _meta);
}
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string | Immutable.OrderedMap} newField
*/
const setField = (state, path, newField, config, asyncListValues, _meta = {}) => {
const currentRule = state.getIn(expandTreePath(path));
if (!currentRule) {
// incorrect path
return {state};
}
const { isEndValue, canDropArgs } = _meta;
if (!newField) {
state = removeItem(state, path);
return {state};
}
const {fieldSeparator, setOpOnChangeField, showErrorMessage} = config.settings;
if (Array.isArray(newField))
newField = newField.join(fieldSeparator);
const currentType = currentRule.get("type");
const currentProperties = currentRule.get("properties");
const wasRuleGroup = currentType == "rule_group";
const currentFieldSrc = currentProperties?.get("fieldSrc");
// const currentFieldError = currentProperties?.get("fieldError");
const newFieldConfig = getFieldConfig(config, newField);
if (!newFieldConfig) {
console.warn(`No config for LHS ${newField}`);
return {state};
}
let fieldType = newFieldConfig.type;
if (fieldType === "!group" || fieldType === "!struct") {
fieldType = null;
}
const currentOperator = currentProperties?.get("operator");
const currentOperatorOptions = currentProperties?.get("operatorOptions");
const currentField = currentProperties?.get("field");
// const currentValue = currentProperties?.get("value");
// const currentValueErrorStr = currentProperties?.get("valueError")?.join?.("|");
// const _currentValueSrc = currentProperties?.get("valueSrc", new Immutable.List());
// const _currentValueType = currentProperties?.get("valueType", new Immutable.List());
const isRuleGroup = newFieldConfig.type == "!group";
const isRuleGroupExt = isRuleGroup && newFieldConfig.mode == "array";
const isChangeToAnotherType = wasRuleGroup != isRuleGroup;
// const wasOkWithoutField = !currentField && currentFieldSrc && currentOperator;
// 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;
const isSameFunc = currentFieldSrc === "func" && currentField?.get?.("func") === newField?.get?.("func");
const forceKeepOp = isSameFunc && !!lastOp;
let newOperator = null;
const availOps = currentFieldSrc === "func"
? getOperatorsForType(config, fieldType)
: getOperatorsForField(config, newField);
if (availOps && availOps.length == 1)
newOperator = availOps[0];
else if (forceKeepOp)
newOperator = lastOp;
else if (availOps && availOps.length > 1) {
for (let strategy of setOpOnChangeField) {
if (strategy == "keep" && !isChangeToAnotherType)
newOperator = lastOp;
else if (strategy == "default")
newOperator = getDefaultOperator(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 (!currentProperties) {
state = state.setIn(expandTreePath(path, "properties"), new Immutable.OrderedMap());
}
const canFix = !showErrorMessage;
if (isRuleGroup) {
state = state.setIn(expandTreePath(path, "type"), "rule_group");
const {canReuseValue, newValue, newValueSrc, newValueType, operatorCardinality} = getNewValueForFieldOp(
{ validateValue, validateRange },
config, config, currentProperties, newField, newOperator, "field", canFix, isEndValue, canDropArgs
);
let groupProperties = defaultGroupProperties(config, newFieldConfig, newField).merge({
field: newField,
fieldSrc: "field",
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 children
} else {
state = addItem(state, path, "rule", uuid(), defaultRuleProperties(config, newField), config);
}
state = fixPathsInTree(state);
} else {
state = state.updateIn(expandTreePath(path, "properties"), (map) => map.withMutations((current) => {
const {
canReuseValue, newValue, newValueSrc, newValueType, newValueError, newFieldError, fixedField
} = getNewValueForFieldOp(
{ validateValue, validateRange },
config, config, current, newField, newOperator, "field", canFix, isEndValue, canDropArgs
);
// const newValueErrorStr = newValueError?.join?.("|");
let newCorrectField = newField;
const willFixField = (fixedField !== newField);
if (willFixField) {
newCorrectField = fixedField;
}
// tip: `newCorrectField` is SAFE to set: even if it can't be fixed, it is reverted to previous good field.
// Unlike logic in `setValue()` action where we need to calc `canUpdValue`
// const didFieldErrorChanged = showErrorMessage ? currentFieldError != newFieldError : !!currentFieldError != !!newFieldError;
// const didValueErrorChanged = showErrorMessage ? currentValueErrorStr != newValueErrorStr : !!currentValueErrorStr != !!newValueErrorStr;
// const didErrorChanged = didFieldErrorChanged || didValueErrorChanged;
// isInternalValueChange = !didErrorChanged && !willFixField;
if (showErrorMessage) {
current = current.set("fieldError", newFieldError);
current = current.set("valueError", newValueError);
}
const newOperatorOptions = canReuseValue ? currentOperatorOptions : defaultOperatorOptions(config, newOperator, newCorrectField);
current = current
.set("field", newCorrectField)
.delete("fieldType") // remove "memory effect"
.set("fieldSrc", currentFieldSrc)
.set("operator", newOperator)
.set("operatorOptions", newOperatorOptions)
.set("value", newValue)
.set("valueSrc", newValueSrc)
.set("valueType", newValueType);
if (!canReuseValue) {
current = current.delete("asyncListValues");
}
return current;
}));
}
return {state};
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} operator
*/
const setOperator = (state, path, newOperator, config) => {
const currentRule = state.getIn(expandTreePath(path));
if (!currentRule) {
// incorrect path
return state;
}
const {showErrorMessage} = config.settings;
const properties = currentRule.get("properties");
const children = currentRule.get("children1");
const currentField = properties.get("field");
const currentFieldSrc = properties.get("fieldSrc");
const fieldConfig = getFieldConfig(config, currentField);
const isRuleGroup = fieldConfig?.type == "!group";
const operatorConfig = getOperatorConfig(config, newOperator, currentField);
const operatorCardinality = operatorConfig ? getOpCardinality(operatorConfig) : null;
const canFix = true;
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(
{ validateValue, validateRange },
config, config, current, currentField, newOperator, "operator", canFix
);
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
*/
const setValue = (state, path, delta, value, valueType, config, asyncListValues, _meta = {}) => {
const currentRule = state.getIn(expandTreePath(path));
if (!currentRule) {
// incorrect path
return {state};
}
const { canDropArgs, isEndValue } = _meta;
const {fieldSeparator, showErrorMessage} = config.settings;
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 fieldSrc = state.getIn(expandTreePath(path, "properties", "fieldSrc")) || null;
const operator = state.getIn(expandTreePath(path, "properties", "operator")) || null;
const operatorConfig = getOperatorConfig(config, operator, field);
const operatorCardinality = operator ? getOpCardinality(operatorConfig) : null;
const calculatedValueType = valueType || calculateValueType(value, valueSrc, config);
const canFix = !showErrorMessage;
const [fixedValue, allErrors] = validateValue(
config, field, field, operator, value, calculatedValueType, valueSrc, asyncListValues, canFix, isEndValue, canDropArgs
);
const firstError = allErrors?.find(e => !e.fixed && !e.ignore);
const validationError = firstError ? translateValidation(firstError) : null;
// tip: even if canFix == false, use fixedValue, it can SAFELY fix value of select
// (get exact value from listValues, not string)
let willFix = fixedValue !== value;
if (willFix) {
value = fixedValue;
}
// init lists
state = initEmptyValueLists(state, path, config, operatorCardinality);
// Additional validation for range values
const values = Array.from({length: operatorCardinality}, (_, i) =>
(i == delta ? value : state.getIn(expandTreePath(path, "properties", "value", i + "")) || null));
const valueSrcs = Array.from({length: operatorCardinality}, (_, i) =>
(state.getIn(expandTreePath(path, "properties", "valueSrc", i + "")) || null));
const rangeErrorObj = validateRange(config, field, operator, values, valueSrcs);
const rangeValidationError = rangeErrorObj ? translateValidation(rangeErrorObj) : null;
const isValid = !validationError && !rangeValidationError;
const canUpdValue = showErrorMessage ? true : isValid || willFix; // set only good value
// const lastValue = state.getIn(expandTreePath(path, "properties", "value", delta));
// const lastError = state.getIn(expandTreePath(path, "properties", "valueError", delta));
// const lastRangeError = state.getIn(expandTreePath(path, "properties", "valueError", operatorCardinality));
// const didDeltaErrorChanged = showErrorMessage ? lastError != validationError : !!lastError != !!validationError;
// const didRangeErrorChanged = showErrorMessage ? lastRangeError != rangeValidationError : !!lastRangeError != !!rangeValidationError;
// const didErrorChanged = didDeltaErrorChanged || didRangeErrorChanged;
// const didEmptinessChanged = !!lastValue != !!value;
// isInternalValueChange = !didEmptinessChanged && !didErrorChanged && !willFix;
if (canUpdValue) {
state = state.deleteIn(expandTreePath(path, "properties", "asyncListValues"));
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);
}
}
if (showErrorMessage) {
// check list
const lastValueErrorArr = state.getIn(expandTreePath(path, "properties", "valueError"));
if (!lastValueErrorArr) {
state = state
.setIn(expandTreePath(path, "properties", "valueError"), new Immutable.List(new Array(operatorCardinality)));
}
// set error at delta
state = state.setIn(expandTreePath(path, "properties", "valueError", delta), validationError);
// set range error
if (operatorCardinality >= 2) {
state = state.setIn(expandTreePath(path, "properties", "valueError", operatorCardinality), rangeValidationError);
}
}
return {state};
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {integer} delta
* @param {*} srcKey
*/
const setValueSrc = (state, path, delta, srcKey, config, _meta = {}) => {
const currentRule = state.getIn(expandTreePath(path));
if (!currentRule) {
// incorrect path
return state;
}
const {showErrorMessage} = config.settings;
const field = state.getIn(expandTreePath(path, "properties", "field")) || null;
//const fieldSrc = state.getIn(expandTreePath(path, "properties", "fieldSrc")) || null;
const operator = state.getIn(expandTreePath(path, "properties", "operator")) || null;
const operatorConfig = getOperatorConfig(config, operator, field);
const operatorCardinality = operator ? getOpCardinality(operatorConfig) : null;
// init lists
state = initEmptyValueLists(state, path, config, operatorCardinality);
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
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 canFix = true;
const {canReuseValue, newValue, newValueSrc, newValueType, newValueError} = getNewValueForFieldOp(
{ validateValue, validateRange },
config, config, properties, field, operator, "valueSrc", canFix
);
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) => {
const currentRule = state.getIn(expandTreePath(path));
if (!currentRule) {
// incorrect path
return state;
}
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 initEmptyValueLists = (state, path, config, operatorCardinality) => {
if (!operatorCardinality) {
const field = state.getIn(expandTreePath(path, "properties", "field")) || null;
const operator = state.getIn(expandTreePath(path, "properties", "operator")) || null;
const operatorConfig = getOperatorConfig(config, operator, field);
operatorCardinality = operator ? getOpCardinality(operatorConfig) : null;
}
for (const k of ["value", "valueType", "valueError", "valueSrc"]) {
if (!state.getIn(expandTreePath(path, "properties", k))) {
state = state
.setIn(expandTreePath(path, "properties", k), new Immutable.List(
operatorCardinality ? Array.from({length: operatorCardinality}) : []
));
}
}
return state;
};
// 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 = it.id ?? uuid();
const it1 = {
...it,
properties: defaultItemProperties(config, it).merge(fromJS(it.properties) || {}),
id: id1
};
_addChildren1(config, it1, it1.children1);
//todo: guarantee order
return {
...map,
[id1]: new Immutable.Map(it1)
};
}, {})
);
}
};
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) => {
if (!action || !action.type)
return null;
const actionKeysToOmit = [
"config", "asyncListValues"
];
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) {
if (affectedField?.toJS)
affectedField = affectedField.toJS();
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 (initialConfig, tree, getMemoizedTree, setLastTree, getLastConfig) => {
const initTree = tree;
const emptyState = {
tree: initTree,
...emptyDrag
};
return (state = emptyState, action) => {
const config = getLastConfig?.() ?? action?.config ?? initialConfig;
const unset = {__lastAction: undefined};
let set = {};
let actionMeta = getActionMeta(action, state);
switch (action?.type) {
case constants.SET_TREE: {
const validatedTree = getMemoizedTree(config, action.tree);
set.tree = validatedTree;
break;
}
case constants.ADD_CASE_GROUP: {
set.tree = addNewGroup(state.tree, action.path, "case_group", action.id, action.properties, config, action.children, action.meta);
break;
}
case constants.ADD_GROUP: {
set.tree = addNewGroup(state.tree, action.path, "group", action.id, action.properties, config, action.children, action.meta);
break;
}
case constants.REMOVE_GROUP: {
set.tree = removeGroup(state.tree, action.path, config);
break;
}
case constants.REMOVE_GROUP_CHILDREN: {
set.tree = removeGroupChildren(state.tree, action.path, config);
break;
}
case constants.ADD_RULE: {
set.tree = addItem(state.tree, action.path, action.ruleType, action.id, action.properties, config, action.children);
break;
}
case constants.REMOVE_RULE: {
set.tree = removeRule(state.tree, action.path, 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: {
const {state: newTree} = setField(
state.tree, action.path, action.field, config,
action.asyncListValues, action._meta
);
set.tree = newTree;
break;
}
case constants.SET_FIELD_SRC: {
set.tree = setFieldSrc(state.tree, action.path, action.srcKey, 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, config);
break;
}
case constants.SET_VALUE: {
const {state: newTree} = setValue(
state.tree, action.path, action.delta, action.value, action.valueType, config,
action.asyncListValues, action._meta
);
set.tree = newTree;
break;
}
case constants.SET_FUNC_VALUE: {
const {state: newTree} = setFuncValue(
config, state.tree, action.path, action.delta, action.parentFuncs,
action.argKey, action.value, action.valueType,
action.asyncListValues, action._meta
);
set.tree = newTree;
break;
}
case constants.SET_VALUE_SRC: {
set.tree = setValueSrc(state.tree, action.path, action.delta, action.srcKey, config, action._meta);
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, 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;
}
if (setLastTree && set.tree && state.tree) {
setLastTree(state.tree);
}
return {...state, ...unset, ...set};
};
};