reactjs-query-builder
Version:
565 lines (496 loc) • 23.9 kB
JavaScript
import Immutable from 'immutable';
import {expandTreePath, expandTreeSubpath, getItemByPath, fixPathsInTree} from '../utils/treeUtils';
import {defaultRuleProperties, defaultGroupProperties, defaultOperator, defaultOperatorOptions, defaultRoot} from '../utils/defaultUtils';
import {getFirstOperator} from '../utils/configUtils';
import * as constants from '../constants';
import uuid from '../utils/uuid';
import omit from 'lodash/omit';
import {getFieldConfig, getOperatorConfig, getFieldWidgetConfig, getValueSourcesForFieldOp} from "../utils/configUtils";
import {defaultValue, eqArrSet} from "../utils/stuff";
import {getOperatorsForField, getWidgetForFieldOp} from "../utils/configUtils";
var stringify = require('json-stringify-safe');
const hasChildren = (tree, path) => tree.getIn(expandTreePath(path, 'children1')).size > 0;
/**
* @param {object} config
* @param {Immutable.List} path
* @param {object} properties
*/
const addNewGroup = (state, path, properties, config) => {
//console.log("Adding group");
const groupUuid = uuid();
state = addItem(state, path, 'group', groupUuid, defaultGroupProperties(config).merge(properties || {}));
const groupPath = path.push(groupUuid);
// If we don't set the empty map, then the following merge of addItem will create a Map rather than an OrderedMap for some reason
state = state.setIn(expandTreePath(groupPath, 'children1'), new Immutable.OrderedMap());
state = addItem(state, groupPath, 'rule', uuid(), defaultRuleProperties(config).merge(properties || {}));
state = fixPathsInTree(state);
return state;
};
/**
* @param {object} config
* @param {Immutable.List} path
* @param {object} properties
*/
const removeGroup = (state, path, config) => {
state = removeItem(state, path);
const parentPath = path.slice(0, -1);
let isEmptyGroup = !hasChildren(state, parentPath);
let isEmptyRoot = isEmptyGroup && parentPath.size == 1;
let canLeaveEmpty = isEmptyGroup && config.settings.canLeaveEmptyGroup && !isEmptyRoot;
if (isEmptyGroup && !canLeaveEmpty) {
state = addItem(state, parentPath, 'rule', uuid(), defaultRuleProperties(config));
}
state = fixPathsInTree(state);
return state;
};
/**
* @param {object} config
* @param {Immutable.List} path
*/
const removeRule = (state, path, config) => {
state = removeItem(state, path);
const parentPath = path.slice(0, -1);
let isEmptyGroup = !hasChildren(state, parentPath);
let isEmptyRoot = isEmptyGroup && parentPath.size == 1;
let canLeaveEmpty = isEmptyGroup && config.settings.canLeaveEmptyGroup && !isEmptyRoot;
if (isEmptyGroup && !canLeaveEmpty) {
state = addItem(state, parentPath, 'rule', uuid(), defaultRuleProperties(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 {string} conjunction
*/
const setConjunction = (state, path, conjunction) =>
state.setIn(expandTreePath(path, 'properties', 'conjunction'), conjunction);
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} type
* @param {string} id
* @param {Immutable.OrderedMap} properties
*/
const addItem = (state, path, type, id, properties) => {
state = state.mergeIn(expandTreePath(path, 'children1'), new Immutable.OrderedMap({
[id]: new Immutable.Map({type, id, properties})
}));
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) => {
let from = getItemByPath(state, fromPath);
let sourcePath = fromPath.pop();
let source = fromPath.size > 1 ? getItemByPath(state, sourcePath) : null;
let sourceChildren = source ? source.get('children1') : null;
let to = getItemByPath(state, toPath);
let targetPath = (placement == constants.PLACEMENT_APPEND || placement == constants.PLACEMENT_PREPEND) ? toPath : toPath.pop();
let target = (placement == constants.PLACEMENT_APPEND || placement == constants.PLACEMENT_PREPEND) ?
to
: toPath.size > 1 ? getItemByPath(state, targetPath) : null;
let targetChildren = target ? target.get('children1') : null;
if (!source || !target)
return state;
let isSameParent = (source.get('id') == target.get('id'));
let isSourceInsideTarget = targetPath.size < sourcePath.size
&& JSON.stringify(targetPath.toArray()) == JSON.stringify(sourcePath.toArray().slice(0, targetPath.size));
let isTargetInsideSource = targetPath.size > sourcePath.size
&& JSON.stringify(sourcePath.toArray()) == JSON.stringify(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 => {
let itemId, item, i = 0, size = newTargetChildren.size;
for ([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 {object} config
* @param {object} oldConfig
* @param {Immutable.Map} current
* @param {string} newField
* @param {string} newOperator
* @param {string} changedField
* @return {object} - {canReuseValue, newValue, newValueSrc, newValueType}
*/
export const _getNewValueForFieldOp = function (config, oldConfig = null, current, newField, newOperator, changedField = null) {
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 currentOperatorConfig = getOperatorConfig(oldConfig, currentOperator, currentField);
const newOperatorConfig = getOperatorConfig(config, newOperator, newField);
const operatorCardinality = newOperator ? defaultValue(newOperatorConfig.cardinality, 1) : null;
const currentFieldConfig = getFieldConfig(currentField, oldConfig);
const currentWidgets = Array.from({length: operatorCardinality}, (_ignore, i) => {
let vs = currentValueSrc.get(i) || null;
let w = getWidgetForFieldOp(oldConfig, currentField, currentOperator, vs);
return w;
});
const newFieldConfig = getFieldConfig(newField, config);
const newWidgets = Array.from({length: operatorCardinality}, (_ignore, i) => {
let vs = currentValueSrc.get(i) || null;
let w = getWidgetForFieldOp(config, newField, newOperator, vs);
return w;
});
const commonWidgetsCnt = Math.min(newWidgets.length, currentWidgets.length);
const firstWidgetConfig = getFieldWidgetConfig(config, newField, newOperator, null, currentValueSrc.first());
const valueSources = getValueSourcesForFieldOp(config, newField, newOperator);
let canReuseValue = currentField && currentOperator && newOperator
&& (!changedField
|| changedField == 'field' && !config.settings.clearValueOnChangeField
|| changedField == 'operator' && !config.settings.clearValueOnChangeOp)
&& (currentFieldConfig && newFieldConfig && currentFieldConfig.type == newFieldConfig.type)
&& JSON.stringify(currentWidgets.slice(0, commonWidgetsCnt)) == JSON.stringify(newWidgets.slice(0, commonWidgetsCnt))
;
if (canReuseValue) {
for (let i = 0 ; i < commonWidgetsCnt ; i++) {
let v = currentValue.get(i);
let vType = currentValueType.get(i) || null;
let vSrc = currentValueSrc.get(i) || null;
let isValidSrc = (valueSources.find(v => v == vSrc) != null);
let isValid = _validateValue(config, newField, newOperator, v, vType, vSrc);
if (!isValidSrc || !isValid) {
canReuseValue = false;
break;
}
}
}
let newValue = null, newValueSrc = null, newValueType = null;
newValue = new Immutable.List(Array.from({length: operatorCardinality}, (_ignore, i) => {
let v = undefined;
if (canReuseValue) {
if (i < currentValue.size)
v = currentValue.get(i);
} else if (operatorCardinality == 1 && firstWidgetConfig && firstWidgetConfig.defaultValue !== undefined) {
v = 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;
}));
newValueType = new Immutable.List(Array.from({length: operatorCardinality}, (_ignore, i) => {
let v = null;
if (canReuseValue) {
if (i < currentValueType.size)
v = currentValueType.get(i);
} else if (operatorCardinality == 1 && firstWidgetConfig && firstWidgetConfig.type !== undefined) {
v = firstWidgetConfig.type;
}
return v;
}));
return {canReuseValue, newValue, newValueSrc, newValueType};
};
const _validateValue = (config, field, operator, value, valueType, valueSrc) => {
let v = value,
vType = valueType,
vSrc = valueSrc;
const fieldConfig = getFieldConfig(field, config);
let w = getWidgetForFieldOp(config, field, operator, vSrc);
let wConfig = config.widgets[w];
let wType = wConfig.type;
let fieldWidgetDefinition = omit(getFieldWidgetConfig(config, field, operator, w, vSrc), ['factory', 'formatValue']);
let isValid = true;
if (v != null) {
const rightFieldDefinition = (vSrc == 'field' ? getFieldConfig(v, config) : null);
if (vSrc == 'field') {
if (v == field || !rightFieldDefinition) {
//can't compare field with itself or no such field
isValid = false;
}
} else if (vSrc == 'value') {
if (vType != wType) {
isValid = false;
}
if (fieldConfig && fieldConfig.listValues) {
if (v instanceof Array) {
for (let _v of v) {
if (fieldConfig.listValues[_v] == undefined) {
//prev value is not in new list of values!
isValid = false;
break;
}
}
} else {
if (fieldConfig.listValues[v] == undefined) {
//prev value is not in new list of values!
isValid = false;
}
}
}
const fieldSettings = fieldConfig.fieldSettings;
if (fieldSettings) {
if (fieldSettings.min != null) {
isValid = isValid && (v >= fieldSettings.min);
}
if (fieldSettings.max != null) {
isValid = isValid && (v <= fieldSettings.max);
}
}
}
let fn = fieldWidgetDefinition.validateValue;
if (typeof fn == 'function') {
let args = [
v,
//field,
fieldConfig,
];
if (vSrc == 'field')
args.push(rightFieldDefinition);
isValid = isValid && fn(...args);
}
}
return isValid;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} field
*/
const setField = (state, path, newField, config) => {
if (!newField)
return removeItem(state, path);
return state.updateIn(expandTreePath(path, 'properties'), (map) => map.withMutations((current) => {
const currentField = current.get('field');
const currentOperator = current.get('operator');
const currentOperatorOptions = current.get('operatorOptions');
//const currentValue = current.get('value');
//const currentValueSrc = current.get('valueSrc', new Immutable.List());
//const currentValueType = current.get('valueType', new Immutable.List());
// If the newly selected field supports the same operator the rule currently
// uses, keep it selected.
const newFieldConfig = getFieldConfig(newField, config);
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 config.settings.setOpOnChangeField || []) {
if (strategy == 'keep')
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;
}
}
let {canReuseValue, newValue, newValueSrc, newValueType} = _getNewValueForFieldOp (config, config, current, newField, newOperator, 'field');
let 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);
}))
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {string} operator
*/
const setOperator = (state, path, newOperator, config) => {
return state.updateIn(expandTreePath(path, 'properties'), (map) => map.withMutations((current) => {
const currentValue = current.get('value', new Immutable.List());
const currentValueSrc = current.get('valueSrc', new Immutable.List());
const currentField = current.get('field');
const currentOperator = current.get('operator');
const currentOperatorOptions = current.get('operatorOptions');
let {canReuseValue, newValue, newValueSrc, newValueType} = _getNewValueForFieldOp (config, config, current, currentField, newOperator, 'operator');
let newOperatorOptions = canReuseValue ? currentOperatorOptions : defaultOperatorOptions(config, newOperator, currentField);
return current
.set('operator', newOperator)
.set('operatorOptions', newOperatorOptions)
.set('value', newValue)
.set('valueSrc', newValueSrc)
.set('valueType', newValueType);
}));
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {integer} delta
* @param {*} value
* @param {string} valueType
*/
const setValue = (state, path, delta, value, valueType, config) => {
const valueSrc = state.getIn(expandTreePath(path, 'properties', 'valueSrc', delta + '')) || null;
const field = state.getIn(expandTreePath(path, 'properties', 'field')) || null;
const operator = state.getIn(expandTreePath(path, 'properties', 'operator')) || null;
const calculatedValueType = valueType || (valueSrc === 'field' && value ? getFieldConfig(value, config).type : valueType);
let isValid = _validateValue(config, field, operator, value, calculatedValueType, valueSrc);
if (isValid) {
if (typeof value === "undefined") {
state = state.setIn(expandTreePath(path, 'properties', 'value', delta + ''), undefined);
state = state.setIn(expandTreePath(path, 'properties', 'valueType', delta + ''), null);
} else {
state = state.setIn(expandTreePath(path, 'properties', 'value', delta + ''), value);
state = state.setIn(expandTreePath(path, 'properties', 'valueType', delta + ''), calculatedValueType);
}
}
return state;
};
/**
* @param {Immutable.Map} state
* @param {Immutable.List} path
* @param {integer} delta
* @param {*} srcKey
*/
const setValueSrc = (state, path, delta, srcKey) => {
state = state.setIn(expandTreePath(path, 'properties', 'value', delta + ''), undefined);
state = state.setIn(expandTreePath(path, 'properties', 'valueType', delta + ''), null);
if (typeof srcKey === "undefined") {
state = state.setIn(expandTreePath(path, 'properties', 'valueSrc', delta + ''), null);
} else {
state = state.setIn(expandTreePath(path, 'properties', 'valueSrc', delta + ''), srcKey);
}
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);
};
const emptyDrag = {
dragging: {
id: null,
x: null,
y: null,
w: null,
h: null
},
mousePos: {},
dragStart: {
id: null,
},
};
/**
* @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) => {
switch (action.type) {
case constants.SET_TREE:
return Object.assign({}, state, {tree: action.tree});
case constants.ADD_NEW_GROUP:
return Object.assign({}, state, {tree: addNewGroup(state.tree, action.path, action.properties, action.config)});
case constants.ADD_GROUP:
return Object.assign({}, state, {tree: addItem(state.tree, action.path, 'group', action.id, action.properties)});
case constants.REMOVE_GROUP:
return Object.assign({}, state, {tree: removeGroup(state.tree, action.path, action.config)});
case constants.ADD_RULE:
return Object.assign({}, state, {tree: addItem(state.tree, action.path, 'rule', action.id, action.properties)});
case constants.REMOVE_RULE:
return Object.assign({}, state, {tree: removeRule(state.tree, action.path, action.config)});
case constants.SET_CONJUNCTION:
return Object.assign({}, state, {tree: setConjunction(state.tree, action.path, action.conjunction)});
case constants.SET_NOT:
return Object.assign({}, state, {tree: setNot(state.tree, action.path, action.not)});
case constants.SET_FIELD:
return Object.assign({}, state, {tree: setField(state.tree, action.path, action.field, action.config)});
case constants.SET_OPERATOR:
return Object.assign({}, state, {tree: setOperator(state.tree, action.path, action.operator, action.config)});
case constants.SET_VALUE:
return Object.assign({}, state, {tree: setValue(state.tree, action.path, action.delta, action.value, action.valueType, action.config)});
case constants.SET_VALUE_SRC:
return Object.assign({}, state, {tree: setValueSrc(state.tree, action.path, action.delta, action.srcKey)});
case constants.SET_OPERATOR_OPTION:
return Object.assign({}, state, {tree: setOperatorOption(state.tree, action.path, action.name, action.value)});
case constants.MOVE_ITEM:
return Object.assign({}, state, {tree: moveItem(state.tree, action.fromPath, action.toPath, action.placement, action.config)});
case constants.SET_DRAG_START:
return Object.assign({}, state, {dragStart: action.dragStart, dragging: action.dragging, mousePos: action.mousePos});
case constants.SET_DRAG_PROGRESS:
return Object.assign({}, state, {mousePos: action.mousePos, dragging: action.dragging});
case constants.SET_DRAG_END:
return Object.assign({}, state, emptyDrag);
default:
return state;
}
};
};