@react-query-builder-express/core
Version:
User-friendly query builder for React. Core
468 lines (430 loc) • 16.8 kB
JavaScript
import {
getFieldConfig, getOperatorConfig, getFieldWidgetConfig, getFieldRawConfig, getFuncConfig, getFieldParts,
isFieldDescendantOfField, getFieldId, _getFromConfigCache, _saveToConfigCache,
} from "./configUtils";
import last from "lodash/last";
import {completeFuncValue} from "./funcUtils";
export const selectTypes = [
"select",
"multiselect",
"treeselect",
"treemultiselect",
];
export const getOperatorsForType = (config, fieldType) => {
return config.types[fieldType]?.operators || null;
};
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?.[0] ?? null;
};
export 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 || funcConfig.type;
}
}
}
}
return calculatedValueType;
};
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 = getFieldParts(field, config);
const parentParts = getFieldParts(parentField, config);
const res = 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 || last(part.split(fieldSeparator));
})
.filter(label => label != null);
return res;
};
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 = getFieldParts(field, config);
const isDescendant = isFieldDescendantOfField(field, parentField, config);
const parentParts = !isDescendant ? [] : getFieldParts(parentField, config);
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;
// tip: this logic moved to extendFieldConfig(), see comment "label for func arg"
// 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) {
let widgets = [];
let valueSrcs = [];
if (!field)
return {widgets, valueSrcs};
const fieldCacheKey = getFieldId(field);
const cacheKey = fieldCacheKey ? `${fieldCacheKey}__${operator}__${valueSrc}` : null;
const cached = _getFromConfigCache(config, "_getWidgetsAndSrcsForFieldOp", cacheKey);
if (cached)
return cached;
const isFuncArg = typeof field === "object" && (!!field.func && !!field.arg || field._isFuncArg);
const fieldConfig = getFieldConfig(config, field);
const opConfig = operator ? config.operators[operator] : null;
if (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?.indexOf(widgetValueSrc) != -1;
if (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)));
const res = { widgets, valueSrcs };
_saveToConfigCache(config, "_getWidgetsAndSrcsForFieldOp", cacheKey, res);
return res;
}
export const getWidgetsForFieldOp = (config, field, operator, valueSrc = null) => {
const {widgets} = _getWidgetsAndSrcsForFieldOp(config, field, operator, valueSrc);
return widgets;
};
export const filterValueSourcesForField = (config, valueSrcs, fieldDefinition) => {
if (!fieldDefinition)
return valueSrcs;
let fieldType = fieldDefinition.type ?? fieldDefinition.returnType;
if (fieldType === "!group") {
// todo: aggregation can be not only number?
fieldType = "number";
}
// const { _isCaseValue } = fieldDefinition;
if (!valueSrcs)
valueSrcs = Object.keys(config.settings.valueSourcesInfo);
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[fieldType] > minCnt;
}
}
if (vs === "func") {
if (fieldDefinition.funcs) {
canAdd = canAdd && fieldDefinition.funcs.length > 0;
}
if (config.__funcsCntByType) {
canAdd = canAdd && config.__funcsCntByType[fieldType] > 0;
}
}
return canAdd;
});
};
export const getValueSourcesForFieldOp = (config, field, operator, fieldDefinition = null) => {
const {valueSrcs} = _getWidgetsAndSrcsForFieldOp(config, field, operator, null);
const filteredValueSrcs = filterValueSourcesForField(config, valueSrcs, fieldDefinition);
return filteredValueSrcs;
};
export const getWidgetForFieldOp = (config, field, operator, valueSrc = null) => {
const {widgets} = _getWidgetsAndSrcsForFieldOp(config, field, operator, valueSrc);
let widget = null;
if (widgets.length)
widget = widgets[0];
return widget;
};
// can use alias (fieldName)
// even if `parentField` is provided, `field` is still a full path
export const formatFieldName = (field, config, meta, parentField = null, options = {}) => {
if (!field) return;
const fieldDef = getFieldConfig(config, field) || {};
const {fieldSeparator} = config.settings;
const fieldParts = getFieldParts(field, config);
let fieldName = Array.isArray(field) ? field.join(fieldSeparator) : field;
if (options?.useTableName && 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 (fieldName.indexOf(parentFieldName + fieldSeparator) == 0) {
fieldName = fieldName.slice((parentFieldName + fieldSeparator).length);
// fieldName = "#this." + fieldName; // ? for spel
} else {
if (fieldDef.fieldName) {
// ignore
} else {
meta.errors.push(`Can't cut group ${parentFieldName} from field ${fieldName}`);
}
}
}
return fieldName;
};
/**
* Used together with keepInputOnChangeFieldSrc
*/
export const isEmptyItem = (item, config) => {
const type = item.get("type");
const mode = item.getIn(["properties", "mode"]);
if (type == "rule_group" && mode == "array") {
return isEmptyRuleGroupExt(item, config);
} else if (type == "group" || type == "rule_group") {
return isEmptyGroup(item, config);
} else {
return isEmptyRule(item, config);
}
};
const isEmptyRuleGroupExt = (item, config) => {
const children = item.get("children1");
const properties = item.get("properties");
return isEmptyRuleGroupExtPropertiesAndChildren(properties.toObject(), children, config);
};
/**
* Used to remove group ext without confirmation
*
* If group operator is count, children can be empty.
* If group operator is some/none/all, there should be at least one non-empty (even incomplete) child.
*/
export const isEmptyRuleGroupExtPropertiesAndChildren = (properties, children, config) => {
const operator = properties.operator;
const cardinality = config.operators[operator]?.cardinality ?? 1;
const childrenAreRequired = cardinality == 0; // tip: for group operators some/none/all
const filledParts = {
group: !isEmptyRuleProperties(properties, config),
children: !isEmptyGroupChildren(children, config),
};
const hasEnough = filledParts.group && (childrenAreRequired ? filledParts.children : true);
return !hasEnough;
};
const isEmptyGroup = (group, config) => {
const children = group.get("children1");
return isEmptyGroupChildren(children, config);
};
/**
* Used to remove group without confirmation
* @returns {boolean} false if there is at least one (even incomplete) child
*/
export const isEmptyGroupChildren = (children, config) => {
const hasEnough = children?.size > 0 && children.filter(ch => !isEmptyItem(ch, config)).size > 0;
return !hasEnough;
};
const isEmptyRule = (rule, config) => {
const properties = rule.get("properties");
return isEmptyRuleProperties(properties?.toObject() || {}, config);
};
/**
* Used to remove rule without confirmation
* @param properties is an Object, but properties (like value) are Immutable
* @returns {boolean} true if there is no enough data in rule
*/
export const isEmptyRuleProperties = (properties, config) => {
const liteCheck = true;
const scoreThreshold = 3;
const compl = whatRulePropertiesAreCompleted(properties, config, liteCheck);
const hasEnough = compl.score >= scoreThreshold;
return !hasEnough;
};
/**
* Used to validate rule
* @param properties is an Object, but its properties (like `value`) are Immutable
* @param liteCheck true can be used to check that rule has enough data to ask confirmation before delete
* @return {{parts: {field: boolean, operator: boolean, value: boolean}, score: number}}
*/
export const whatRulePropertiesAreCompleted = ({
field, fieldSrc, fieldType,
operator,
value, valueSrc, valueType,
}, config, liteCheck = false) => {
const cardinality = config.operators[operator]?.cardinality ?? 1;
const valueSrcs = valueSrc?.get ? valueSrc.toJS() : valueSrc;
// tip: for liteCheck==true `score` should equal 3 if both LHS and RHS are at least partially filled
const res = { parts: {}, score: 0 };
res.parts.field = liteCheck ? (field != null) : isCompletedValue(field, fieldSrc, config);
res.parts.operator = !!operator;
res.parts.value = value?.filter((val, delta) =>
isCompletedValue(val, valueSrcs?.[delta], config, liteCheck)
)?.size >= (liteCheck ? Math.min(cardinality, 1) : cardinality);
res.score = Object.keys(res.parts).filter(k => !!res.parts[k]).length;
if (liteCheck && res.score < 3) {
// Boost score to confirm deletion:
// - if RHS is empty, but LHS is a completed function
// - if LHS is empty (only fieldType is set), but there is a completed function in RHS
const deepCheck = true;
if (!res.parts.value && fieldSrc === "func" && isCompletedValue(field, fieldSrc, config, false, deepCheck)) {
res.score++;
}
if (!res.parts.field) {
value?.map((val, delta) => {
if (valueSrcs?.[delta] === "func" && isCompletedValue(val, valueSrcs?.[delta], config, false, deepCheck)) {
res.score++;
}
});
}
}
return res;
};
const isCompletedValue = (value, valueSrc, config, liteCheck = false, deepCheck = true) => {
if (!liteCheck && valueSrc == "func" && value) {
const funcKey = value.get?.("func");
const funcConfig = getFuncConfig(config, funcKey);
if (funcConfig) {
const args = value.get("args");
for (const argKey in funcConfig.args) {
const argConfig = funcConfig.args[argKey];
const argVal = args ? args.get(argKey) : undefined;
// const argDef = getFieldConfig(config, argConfig);
const argValue = argVal ? argVal.get("value") : undefined;
const argValueSrc = argVal ? argVal.get("valueSrc") : undefined;
if (argValue == undefined && argConfig?.defaultValue === undefined && !argConfig?.isOptional) {
// arg is not completed
return false;
}
if (argValue != undefined) {
if (!isCompletedValue(argValue, argValueSrc, config, deepCheck ? liteCheck : true)) {
// arg is complex and is not completed
return false;
}
}
}
// all args are completed
return true;
}
}
return value != undefined;
};
/**
* @param {*} value
* @param {'value'|'field'|'func'} valueSrc
* @param {object} config
* @return {* | undefined} undefined if func value is not complete (missing required arg vals); can return completed value != value
*/
export const completeValue = (value, valueSrc, config) => {
if (valueSrc == "func")
return completeFuncValue(value, config);
else
return value;
};
// item - Immutable
export const getOneChildOrDescendant = (item) => {
const children = item.get("children1");
if (children?.size == 1) {
const child = children.first();
const childType = child.get("type");
if (childType === "group") {
return getOneChildOrDescendant(child);
}
return child;
}
return null;
};