@coocoon/react-awesome-query-builder
Version:
User-friendly query builder for React. Demo: https://ukrbublik.github.io/react-awesome-query-builder
434 lines (396 loc) • 17.6 kB
JavaScript
import {
getFieldConfig, getOperatorConfig, getFieldWidgetConfig, getFieldRawConfig
} from "./configUtils";
import {defaultValue, getFirstDefined} from "../utils/stuff";
import Immutable from "immutable";
import {validateValue} from "../utils/validation";
import last from "lodash/last";
const selectTypes = [
"select",
"multiselect",
"treeselect",
"treemultiselect",
];
/**
* @param {object} config
* @param {object} oldConfig
* @param {Immutable.Map} current
* @param {string} newField
* @param {string} newOperator
* @param {string} changedProp
* @return {object} - {canReuseValue, newValue, newValueSrc, newValueType, newValueError}
*/
export const getNewValueForFieldOp = function (config, oldConfig = null, current, newField, newOperator, changedProp = null, canFix = true, isFunc) {
if (!oldConfig)
oldConfig = config;
const currentField = current.get("field");
const currentOperator = current.get("operator");
const currentValue = current.get("value");
const currentValueSrc = current.get("valueSrc", new Immutable.List());
const currentValueType = current.get("valueType", new Immutable.List());
const currentAsyncListValues = current.get("asyncListValues");
//const isValidatingTree = (changedProp === null);
const {convertableWidgets, clearValueOnChangeField, clearValueOnChangeOp, showErrorMessage} = config.settings;
//const currentOperatorConfig = getOperatorConfig(oldConfig, currentOperator, currentField);
const newOperatorConfig = getOperatorConfig(config, newOperator, newField);
//const currentOperatorCardinality = currentOperator ? defaultValue(currentOperatorConfig.cardinality, 1) : null;
const operatorCardinality = newOperator ? defaultValue(newOperatorConfig.cardinality, 1) : null;
const currentFieldConfig = getFieldConfig(oldConfig, currentField,isFunc);
const newFieldConfig = getFieldConfig(config, newField, isFunc);
let canReuseValue = currentField && currentOperator && newOperator && currentValue != undefined
&& (!changedProp
|| changedProp == "field" && !clearValueOnChangeField
|| changedProp == "operator" && !clearValueOnChangeOp)
&& (currentFieldConfig && newFieldConfig && currentFieldConfig.type == newFieldConfig.type);
if (canReuseValue && selectTypes.includes(currentFieldConfig.type) && changedProp == "field") {
// different fields of select types has different listValues
canReuseValue = false;
}
// compare old & new widgets
for (let i = 0 ; i < operatorCardinality ; i++) {
const vs = currentValueSrc.get(i) || null;
const currentWidget = getWidgetForFieldOp(oldConfig, currentField, currentOperator, vs, isFunc);
const newWidget = getWidgetForFieldOp(config, newField, newOperator, vs, isFunc);
// need to also check value widgets if we changed operator and current value source was 'field'
// cause for select type op '=' requires single value and op 'in' requires array value
const currentValueWidget = vs == "value" ? currentWidget : getWidgetForFieldOp(oldConfig, currentField, currentOperator, "value", isFunc);
const newValueWidget = vs == "value" ? newWidget : getWidgetForFieldOp(config, newField, newOperator, "value", isFunc);
const canReuseWidget = newValueWidget == currentValueWidget || (convertableWidgets[currentValueWidget] || []).includes(newValueWidget);
if (!canReuseWidget)
canReuseValue = false;
}
if (currentOperator != newOperator && [currentOperator, newOperator].includes("proximity"))
canReuseValue = false;
const firstWidgetConfig = getFieldWidgetConfig(config, newField, newOperator, null, currentValueSrc.first(), isFunc);
const valueSources = getValueSourcesForFieldOp(config, newField, newOperator,null, null,isFunc);
let valueFixes = {};
let valueErrors = Array.from({length: operatorCardinality}, () => null);
if (canReuseValue) {
for (let i = 0 ; i < operatorCardinality ; i++) {
const v = currentValue.get(i);
const vType = currentValueType.get(i) || null;
const vSrc = currentValueSrc.get(i) || null;
let isValidSrc = (valueSources.find(v => v == vSrc) != null);
if (!isValidSrc && i > 0 && vSrc == null)
isValidSrc = true; // make exception for range widgets (when changing op from '==' to 'between')
const isEndValue = !canFix;
const asyncListValues = currentAsyncListValues;
const [validateError, fixedValue] = validateValue(
config, newField, newField, newOperator, v, vType, vSrc, asyncListValues, canFix, isEndValue,true,isFunc
);
const isValid = !validateError;
if (!isValid && showErrorMessage && changedProp != "field") {
// allow bad value
// but not on field change - in that case just drop bad value that can't be reused
// ? maybe we should also drop bad value on op change?
valueErrors[i] = validateError;
} else if (!isValidSrc || !isValid) {
canReuseValue = false;
break;
} else if (canFix && fixedValue !== v) {
valueFixes[i] = fixedValue;
}
}
}
// reuse value OR get defaultValue for cardinality 1 (it means default range values is not supported yet, todo)
let newValue = null, newValueSrc = null, newValueType = null, newValueError = null;
newValue = new Immutable.List(Array.from({length: operatorCardinality}, (_ignore, i) => {
let v = undefined;
if (canReuseValue) {
if (i < currentValue.size) {
v = currentValue.get(i);
if (valueFixes[i] !== undefined) {
v = valueFixes[i];
}
}
} else if (operatorCardinality == 1) {
v = getFirstDefined([
newFieldConfig?.defaultValue,
newFieldConfig?.fieldSettings?.defaultValue,
firstWidgetConfig?.defaultValue
]);
}
return v;
}));
newValueSrc = new Immutable.List(Array.from({length: operatorCardinality}, (_ignore, i) => {
let vs = null;
if (canReuseValue) {
if (i < currentValueSrc.size)
vs = currentValueSrc.get(i);
} else if (valueSources.length == 1) {
vs = valueSources[0];
} else if (valueSources.length > 1) {
vs = valueSources[0];
}
return vs;
}));
if (showErrorMessage) {
if (newOperatorConfig && newOperatorConfig.validateValues && newValueSrc.toJS().filter(vs => vs == "value" || vs == null).length == operatorCardinality) {
// last element in `valueError` list is for range validation error
const jsValues = firstWidgetConfig && firstWidgetConfig.toJS
? newValue.toJS().map(v => firstWidgetConfig.toJS(v, firstWidgetConfig))
: newValue.toJS();
const rangeValidateError = newOperatorConfig.validateValues(jsValues);
if (showErrorMessage) {
valueErrors.push(rangeValidateError);
}
}
newValueError = new Immutable.List(valueErrors);
}
newValueType = new Immutable.List(Array.from({length: operatorCardinality}, (_ignore, i) => {
let vt = null;
if (canReuseValue) {
if (i < currentValueType.size)
vt = currentValueType.get(i);
} else if (operatorCardinality == 1 && firstWidgetConfig && firstWidgetConfig.type !== undefined) {
vt = firstWidgetConfig.type;
} else if (operatorCardinality == 1 && newFieldConfig && newFieldConfig.type !== undefined) {
vt = newFieldConfig.type == "!group" ? "number" : newFieldConfig.type;
}
return vt;
}));
return {canReuseValue, newValue, newValueSrc, newValueType, newValueError, operatorCardinality};
};
export const getFirstField = (config, parentRuleGroupPath = null) => {
const fieldSeparator = config.settings.fieldSeparator;
const parentPathArr = typeof parentRuleGroupPath == "string" ? parentRuleGroupPath.split(fieldSeparator) : parentRuleGroupPath;
const parentField = parentRuleGroupPath ? getFieldRawConfig(config, parentRuleGroupPath) : config;
let firstField = parentField, key = null, keysPath = [];
do {
const subfields = firstField === config ? config.fields : firstField.subfields;
if (!subfields || !Object.keys(subfields).length) {
firstField = key = null;
break;
}
key = Object.keys(subfields)[0];
keysPath.push(key);
firstField = subfields[key];
} while (firstField.type == "!struct" || firstField.type == "!group");
return (parentPathArr || []).concat(keysPath).join(fieldSeparator);
};
export const getOperatorsForField = (config, field) => {
const fieldConfig = getFieldConfig(config, field);
const fieldOps = fieldConfig ? fieldConfig.operators : [];
return fieldOps;
};
export const getFirstOperator = (config, field) => {
const fieldOps = getOperatorsForField(config, field);
return fieldOps ? fieldOps[0] : null;
};
export const getFieldPath = (field, config, onlyKeys = false) => {
if (!field)
return null;
const fieldSeparator = config.settings.fieldSeparator;
const parts = Array.isArray(field) ? field : field.split(fieldSeparator);
if (onlyKeys)
return parts;
else
return parts
.map((_curr, ind, arr) => arr.slice(0, ind+1))
.map((parts) => parts.join(fieldSeparator));
};
export const getFuncPathLabels = (field, config, parentField = null) => {
return getFieldPathLabels(field, config, parentField, "funcs", "subfields");
};
export const getFieldPathLabels = (field, config, parentField = null, fieldsKey = "fields", subfieldsKey = "subfields") => {
if (!field)
return null;
const fieldSeparator = config.settings.fieldSeparator;
const parts = Array.isArray(field) ? field : field.split(fieldSeparator);
const parentParts = parentField ? (Array.isArray(parentField) ? parentField : parentField.split(fieldSeparator)) : [];
return parts
.slice(parentParts.length)
.map((_curr, ind, arr) => arr.slice(0, ind+1))
.map((parts) => [...parentParts, ...parts].join(fieldSeparator))
.map(part => {
const cnf = getFieldRawConfig(config, part, fieldsKey, subfieldsKey);
return cnf && cnf.label || cnf && last(part.split(fieldSeparator));
})
.filter(label => label != null);
};
export const getFieldPartsConfigs = (field, config, parentField = null) => {
if (!field)
return null;
const parentFieldDef = parentField && getFieldRawConfig(config, parentField) || null;
const fieldSeparator = config.settings.fieldSeparator;
const parts = Array.isArray(field) ? field : field.split(fieldSeparator);
const parentParts = parentField ? (Array.isArray(parentField) ? parentField : parentField.split(fieldSeparator)) : [];
return parts
.slice(parentParts.length)
.map((_curr, ind, arr) => arr.slice(0, ind+1))
.map((parts) => ({
part: [...parentParts, ...parts].join(fieldSeparator),
key: parts[parts.length - 1]
}))
.map(({part, key}) => {
const cnf = getFieldRawConfig(config, part);
return {key, cnf};
})
.map(({key, cnf}, ind, arr) => {
const parentCnf = ind > 0 ? arr[ind - 1].cnf : parentFieldDef;
return [key, cnf, parentCnf];
});
};
export const getValueLabel = (config, field, operator, delta, valueSrc = null, isSpecialRange = false) => {
const isFuncArg = field && typeof field == "object" && !!field.func && !!field.arg;
const {showLabels} = config.settings;
const fieldConfig = getFieldConfig(config, field);
const fieldWidgetConfig = getFieldWidgetConfig(config, field, operator, null, valueSrc) || {};
const mergedOpConfig = getOperatorConfig(config, operator, field) || {};
const cardinality = isSpecialRange ? 1 : mergedOpConfig.cardinality;
let ret = null;
if (cardinality > 1) {
const valueLabels = fieldWidgetConfig.valueLabels || mergedOpConfig.valueLabels;
if (valueLabels)
ret = valueLabels[delta];
if (ret && typeof ret != "object") {
ret = {label: ret, placeholder: ret};
}
if (!ret) {
ret = {
label: config.settings.valueLabel + " " + (delta+1),
placeholder: config.settings.valuePlaceholder + " " + (delta+1),
};
}
} else {
let label = fieldWidgetConfig.valueLabel;
let placeholder = fieldWidgetConfig.valuePlaceholder;
if (isFuncArg) {
if (!label)
label = fieldConfig.label || field.arg;
if (!placeholder && !showLabels)
placeholder = fieldConfig.label || field.arg;
}
ret = {
label: label || config.settings.valueLabel,
placeholder: placeholder || config.settings.valuePlaceholder,
};
}
return ret;
};
function _getWidgetsAndSrcsForFieldOp (config, field, operator = null, valueSrc = null, isFunc=false) {
let widgets = [];
let valueSrcs = [];
if (!field)
return {widgets, valueSrcs};
const isFuncArg = typeof field == "object" && (!!field.func && !!field.arg || field._isFuncArg);
const fieldConfig = getFieldConfig(config, field, isFunc);
const opConfig = operator ? config.operators[operator] : null;
if (fieldConfig && fieldConfig.widgets) {
for (const widget in fieldConfig.widgets) {
const widgetConfig = fieldConfig.widgets[widget];
// if (!config.widgets[widget]) {
// continue;
// }
const widgetValueSrc = config.widgets[widget].valueSrc || "value";
let canAdd = true;
if (widget == "field") {
canAdd = canAdd && filterValueSourcesForField(config, ["field"], fieldConfig).length > 0;
}
if (widget == "func") {
canAdd = canAdd && filterValueSourcesForField(config, ["func"], fieldConfig).length > 0;
}
// If can't check operators, don't add
// Func args don't have operators
if (valueSrc == "value" && !widgetConfig.operators && !isFuncArg && field != "!case_value")
canAdd = false;
if (widgetConfig.operators && operator)
canAdd = canAdd && widgetConfig.operators.indexOf(operator) != -1;
if (valueSrc && valueSrc != widgetValueSrc && valueSrc != "const")
canAdd = false;
if (opConfig && opConfig.cardinality == 0 && (widgetValueSrc != "value"))
canAdd = false;
if (canAdd) {
widgets.push(widget);
let canAddValueSrc = fieldConfig.valueSources && fieldConfig.valueSources.indexOf(widgetValueSrc) != -1;
if (opConfig && opConfig.valueSources && opConfig.valueSources.indexOf(widgetValueSrc) == -1)
canAddValueSrc = false;
if (canAddValueSrc && !valueSrcs.find(v => v == widgetValueSrc))
valueSrcs.push(widgetValueSrc);
}
}
}
const widgetWeight = (w) => {
let wg = 0;
if (fieldConfig.preferWidgets) {
if (fieldConfig.preferWidgets.includes(w))
wg += (10 - fieldConfig.preferWidgets.indexOf(w));
} else if (w == fieldConfig.mainWidget) {
wg += 100;
}
if (w == "field") {
wg -= 1;
}
if (w == "func") {
wg -= 2;
}
return wg;
};
widgets.sort((w1, w2) => (widgetWeight(w2) - widgetWeight(w1)));
return {widgets, valueSrcs};
}
export const getWidgetsForFieldOp = (config, field, operator, valueSrc = null, isFunc=false) => {
const {widgets} = _getWidgetsAndSrcsForFieldOp(config, field, operator, valueSrc, isFunc);
return widgets;
};
export const filterValueSourcesForField = (config, valueSrcs, fieldDefinition) => {
if (!fieldDefinition)
return valueSrcs;
return valueSrcs.filter(vs => {
let canAdd = true;
if (vs == "field") {
if (config._fieldsCntByType) {
// tip: LHS field can be used as arg in RHS function
const minCnt = fieldDefinition._isFuncArg ? 0 : 1;
canAdd = canAdd && config._fieldsCntByType[fieldDefinition.type] > minCnt;
}
}
if (vs == "func") {
if (config._funcsCntByType)
canAdd = canAdd && !!config._funcsCntByType[fieldDefinition.type];
if (fieldDefinition.funcs)
canAdd = canAdd && fieldDefinition.funcs.length > 0;
}
return canAdd;
});
};
export const getValueSourcesForFieldOp = (config, field, operator, fieldDefinition = null, leftFieldForFunc = null, isFunc=false) => {
const {valueSrcs} = _getWidgetsAndSrcsForFieldOp(config, field, operator, null, isFunc);
const filteredValueSrcs = filterValueSourcesForField(config, valueSrcs, fieldDefinition);
return filteredValueSrcs;
};
export const getWidgetForFieldOp = (config, field, operator, valueSrc = null,isFunc=false) => {
const {widgets} = _getWidgetsAndSrcsForFieldOp(config, field, operator, valueSrc, isFunc);
let widget = null;
if (widgets.length)
widget = widgets[0];
return widget;
};
export const formatFieldName = (field, config, meta, parentField = null) => {
if (!field) return;
const fieldDef = getFieldConfig(config, field) || {};
const {fieldSeparator} = config.settings;
const fieldParts = Array.isArray(field) ? field : field.split(fieldSeparator);
let fieldName = Array.isArray(field) ? field.join(fieldSeparator) : field;
if (fieldDef.tableName) { // legacy
const fieldPartsCopy = [...fieldParts];
fieldPartsCopy[0] = fieldDef.tableName;
fieldName = fieldPartsCopy.join(fieldSeparator);
}
if (fieldDef.fieldName) {
fieldName = fieldDef.fieldName;
}
if (parentField) {
const parentFieldDef = getFieldConfig(config, parentField) || {};
let parentFieldName = parentField;
if (parentFieldDef.fieldName) {
parentFieldName = parentFieldDef.fieldName;
}
if (fieldName.indexOf(parentFieldName + fieldSeparator) == 0) {
fieldName = fieldName.slice((parentFieldName + fieldSeparator).length);
} else {
meta.errors.push(`Can't cut group ${parentFieldName} from field ${fieldName}`);
}
}
return fieldName;
};