diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a5a43617..a56abc8c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Changelog +- 1.0.12 + - Added `sqlFormatFunc`, `mongoFormatFunc`, `renderBrackets`, `renderSeps` (for func), `funcs` (for field) - 1.0.11 - Added css-class `qb-lite` for query builder (see readme if you wanna use it) - 1.0.10 diff --git a/CONFIG.adoc b/CONFIG.adoc index 3e9fcfc9c..a9aae529d 100644 --- a/CONFIG.adoc +++ b/CONFIG.adoc @@ -145,7 +145,7 @@ const myConfig = { [cols="1m,1,1,5a",options="header"] |=== -|key |requred |default |meaning +|key |required |default |meaning |type |+ | |One of types described in link:#configtypes[config.types] or `!struct` for complex field |subfields |+ for `!struct` type | |Config for subfields of complex field (multiple nesting is supported) |label |+ | |Label to be displayed in field list + @@ -168,6 +168,7 @@ const myConfig = { |operators, defaultOperator, widgets, valueSources | | |You can override config of corresponding type (see below at section link:#configtypes[config.types]) |mainWidgetProps | | |Shorthand for `widgets.
.widgetProps` |excludeOperators | | |Can exclude some operators, eg. `proximity` for `text` type +|funcs | | |If comparing with funcs is enabled for this field (`valueSources` contains `func`), you can also limit list of funcs to be compared (by default will be available all funcs from link:#configfuncs[config.funcs] with `returnType` matching field's `type`) |hideForSelect | |false |If true, field will appear only at right side (when you compare field with another field) |hideForCompare | |false |If true, field will appear only at left side |=== @@ -343,7 +344,7 @@ where `AND` and `OR` - available conjuctions (logical operators). You can add `N [cols="1m,1,4a",options="header",] |=== -|key |requred |meaning +|key |required |meaning |label |+ |Label to be displayed in conjunctions swicther |formatConj |+ |Function for formatting query, used to join rules into group with conjunction. + `(Immultable.List children, string conj, bool not, bool isForDisplay) => string` + @@ -379,7 +380,7 @@ where `AND` and `OR` - available conjuctions (logical operators). You can add `N [cols="1m,1,1,5a",options="header",] |=== -|key |requred |default |meaning +|key |required |default |meaning |label |+ | |Label to be displayed in operators select component |reversedOp |+ | |Opposite operator. |isUnary | |false |true for `is_empty` operator only @@ -464,7 +465,7 @@ const { [cols="1m,1,1,5a",options="header",] |=== -|key |requred |default |meaning +|key |required |default |meaning |type |+ | |One of types described in link:#configtypes[config.types] |factory |+ | |React function component |formatValue |+ | |Function for formatting widget's value in query string. + @@ -483,9 +484,7 @@ const { |valueFormat | | |Option for ``, ``, `` to format value to be passed in `formatValue()`. Example: `YYYY-MM-DD HH:mm` |labelYes, labelNo | | |Option for `` |customProps | | |You can pass any props directly to widget with `customProps`. + - For example enable search for https://ant.design/components/select/[``] widget: `customProps: {showSearch: true}` |=== NOTE: There is special `field` widget, rendered by ``. + @@ -531,7 +530,7 @@ To enable this feature set `valueSources` of type to `['value', 'func'']` (see b [cols="1m,1,1,5a",options="header",] |=== -|key |requred |default |meaning +|key |required |default |meaning |valueSources | |keys of `valueSourcesInfo` at link:#configsettings[config.settings] |Array with values `'value'`, `'field'`, `'func'`. If `'value'` is included, you can compare field with values. If `'field'` is included, you can compare field with another field of same type. If `'func'` is included, you can compare field with result of function (see link:#configfuncs[config.funcs]). |defaultOperator | | |If specified, it will be auto selected when user selects field |widgets.* |+ | |Available widgets for current type and their config. + @@ -570,15 +569,18 @@ To enable this feature set `valueSources` of type to `['value', 'func'']` (see b [cols="1m,1,1,5a",options="header",] |=== -|key |requred |default |meaning +|key |required |default |meaning |returnType |+ | |One of types described in link:#configtypes[config.types] |label | |same as func key |Label to be displayed in functions list -|formatFunc | |For `isForDisplay==false` - `FUNC(val1, val2)`, for `isForDisplay==true` - `FUNC(arg1: val1, arg2: val2)` |Function for formatting func expression in query rule. + +|formatFunc | |Example: for `isForDisplay==false` - `FUNC(val1, val2)`, for `isForDisplay==true` - `FUNC(arg1: val1, arg2: val2)` |Function for formatting func expression in query rule. + `(Object args, bool isForDisplay) => string` + - where `args` is map `{ => }`` -|sqlFunc |+ for SQL format |same as func key |Func name in SQL -|mongoFunc |+ for MongoDB format |same as func key |Func name in Mongo + where `args` is object `{ : }` +|sqlFunc |- for SQL format |same as func key |Func name in SQL +|sqlFormatFunc |- for SQL format | |Can be used instead of `sqlFunc`. Function with 1 param - args object `{ : }`, should return formatted function expression string. + + Example: SUM function can be formatted with `({a, b}) => a + " + " + b` +|mongoFunc |- for MongoDB format |same as func key |Func name in Mongo |mongoArgsAsObject | |false |Some functions like https://docs.mongodb.com/manual/reference/operator/aggregation/rtrim/[$rtrim] supports named args, other ones like https://docs.mongodb.com/manual/reference/operator/aggregation/slice/[$slice] takes args as array +|mongoFormatFunc |- for MongoDB format | |Can be used instead of `mongoFunc`. Function with 1 param - args object `{ : }`, should return formatted function expression object. |args.* | | |Arguments of function. Config is almost same as for simple link:#configfields[fields] |args..label | |arg's key |Label to be displayed in arg's label or placeholder (if `config.settings.showLabels` is false) |args..type |+ | |One of types described in link:#configtypes[config.types] @@ -589,4 +591,6 @@ To enable this feature set `valueSources` of type to `['value', 'func'']` (see b Example: `{ yellow: 'Yellow', green: 'Green' }` where `Yellow` - label to display at list of options |args..fieldSettings | | |Settings for widgets, will be passed as props. Example: `{min: 1, max: 10}` |args..isOptional | |false |Last args can be optional +|renderBrackets | |`['(', ')']` |Can render custom function brackets in UI (or not render). +|renderSeps | |`[', ']` |Can render custom arguments separators in UI (other than `,`). |=== diff --git a/README.md b/README.md index 73a40d246..6eb05e34e 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ class DemoQueryBuilder extends Component { - Please wrap `` in `div.query-builder`. Optionally you can add class `.qb-lite` to it for showing action buttons (like delete rule/group, add, etc.) only on hover, which will look cleaner. Wrapping in `div.query-builder-container` in not necessary, but if you want to make query builder scrollable, it's best place to apply appropriate styles. -- Use can save query value in `onChange` callback. +- You can save query value in `onChange` callback. Note that value will be in [`Immutable`](https://immutable-js.github.io/immutable-js/) format, so you can use `QbUtils.getTree()` to convert it into JS object. You can store it on backend, and load later by passing in `value` prop of ``. diff --git a/examples/demo/config.tsx b/examples/demo/config.tsx index d6ca32a01..efa7ae9ec 100644 --- a/examples/demo/config.tsx +++ b/examples/demo/config.tsx @@ -231,6 +231,7 @@ const fields: Fields = { min: -1, max: 5 }, + funcs: ['LINEAR_REGRESSION'], }, slider: { label: 'Slider', @@ -335,7 +336,35 @@ const funcs: Funcs = { valueSources: ['value', 'field'], }, } - } + }, + LINEAR_REGRESSION: { + label: 'Linear regression', + returnType: 'number', + formatFunc: ({coef, bias, val}, _) => `(${coef} * ${val} + ${bias})`, + sqlFormatFunc: ({coef, bias, val}) => `(${coef} * ${val} + ${bias})`, + mongoFormatFunc: ({coef, bias, val}) => ({'$sum': [{'$multiply': [coef, val]}, bias]}), + renderBrackets: ['', ''], + renderSeps: [' * ', ' + '], + args: { + coef: { + label: "Coef", + type: 'number', + defaultValue: 1, + valueSources: ['value'], + }, + val: { + label: "Value", + type: 'number', + valueSources: ['value'], + }, + bias: { + label: "Bias", + type: 'number', + defaultValue: 0, + valueSources: ['value'], + } + } + }, }; diff --git a/modules/components/FuncSelect.jsx b/modules/components/FuncSelect.jsx index 969b1cdc1..198a5eea2 100644 --- a/modules/components/FuncSelect.jsx +++ b/modules/components/FuncSelect.jsx @@ -101,6 +101,8 @@ export default class FuncSelect extends PureComponent { delete list[funcKey]; } else { let canUse = funcConfig.returnType == expectedType; + if (leftFieldConfig.funcs) + canUse = canUse && leftFieldConfig.funcs.includes(funcFullkey); if (canUseFuncForField) canUse = canUse && canUseFuncForField(leftFieldFullkey, leftFieldConfig, funcFullkey, funcConfig, operator); if (!canUse) diff --git a/modules/components/FuncWidget.jsx b/modules/components/FuncWidget.jsx index fd65746e6..3eea36ef1 100644 --- a/modules/components/FuncWidget.jsx +++ b/modules/components/FuncWidget.jsx @@ -130,27 +130,27 @@ export default class FuncWidget extends PureComponent { ); }; - renderArgSep = (argKey, argDefinition, argIndex) => { + renderArgSep = (argKey, argDefinition, argIndex, {renderSeps}) => { if (!argIndex) return null; return ( - {", "} + {renderSeps ? renderSeps[argIndex - 1] : ", "} ); }; - renderBracketBefore = (funcDefinition) => { + renderBracketBefore = ({renderBrackets}) => { return ( - {"("} + {renderBrackets ? renderBrackets[0] : "("} ); }; - renderBracketAfter = (funcDefinition) => { + renderBracketAfter = ({renderBrackets}) => { return ( - {")"} + {renderBrackets ? renderBrackets[1] : ")"} ); }; @@ -167,7 +167,7 @@ export default class FuncWidget extends PureComponent { {Object.keys(args).map((argKey, argIndex) => ( - {this.renderArgSep(argKey, args[argKey], argIndex)} + {this.renderArgSep(argKey, args[argKey], argIndex, funcDefinition)} {this.renderArgLabel(argKey, args[argKey])} {this.renderArgLabelSep(argKey, args[argKey])} {this.renderArgVal(funcKey, argKey, args[argKey])} diff --git a/modules/index.d.ts b/modules/index.d.ts index 3d38ed6c2..b74667a9e 100644 --- a/modules/index.d.ts +++ b/modules/index.d.ts @@ -295,6 +295,7 @@ interface ValueField extends BaseField { type: String, preferWidgets?: Array, valueSources?: Array, + funcs?: Array, tableName?: String, fieldSettings?: FieldSettings, defaultValue?: RuleValue, @@ -441,7 +442,7 @@ export type Settings = LocaleSettings & RenderSettings & BehaviourSettings & Oth type SqlFormatFunc = (formattedArgs: { [key: string]: string }) => String; type FormatFunc = (formattedArgs: { [key: string]: string }, isForDisplay: Boolean) => String; -type MongoFormatFunc = (formattedArgs: { [key: string]: MongoValue }) => String; +type MongoFormatFunc = (formattedArgs: { [key: string]: MongoValue }) => MongoValue; interface FuncGroup { type?: "!struct", @@ -459,6 +460,8 @@ export interface Func { formatFunc?: FormatFunc, sqlFormatFunc?: SqlFormatFunc, mongoFormatFunc?: MongoFormatFunc, + renderBrackets?: Array, + renderSeps?: Array, }; export interface FuncArg extends ValueField { isOptional?: Boolean, diff --git a/modules/utils/configUtils.js b/modules/utils/configUtils.js index b80768a3e..8e0af8251 100644 --- a/modules/utils/configUtils.js +++ b/modules/utils/configUtils.js @@ -426,7 +426,11 @@ export const getValueSourcesForFieldOp = (config, field, operator, fieldDefiniti return config._fieldsCntByType[fieldDefinition.type] > 1; } if (vs == "func" && fieldDefinition) { - return config._funcsCntByType[fieldDefinition.type] > 0; + if (!config._funcsCntByType[fieldDefinition.type]) + return false; + if (fieldDefinition.funcs) + return fieldDefinition.funcs.length > 0; + return true; } return true; }); diff --git a/modules/utils/mongodbFormat.js b/modules/utils/mongodbFormat.js index 032bf0716..2e7880ec9 100644 --- a/modules/utils/mongodbFormat.js +++ b/modules/utils/mongodbFormat.js @@ -52,13 +52,12 @@ const mongoFormatValue = (config, currentValue, valueSrc, valueType, fieldWidget const argVal = args ? args.get(argKey) : undefined; const argValue = argVal ? argVal.get('value') : undefined; const argValueSrc = argVal ? argVal.get('valueSrc') : undefined; - const argName = argKey; const [formattedArgVal, _argUseExpr] = mongoFormatValue(config, argValue, argValueSrc, argConfig.type, fieldDef, argConfig, null, null); if (argValue != undefined && formattedArgVal === undefined) return [undefined, false]; argsCnt++; if (formattedArgVal !== undefined) { // skip optional in the end - formattedArgs[argName] = formattedArgVal; + formattedArgs[argKey] = formattedArgVal; lastArg = formattedArgVal; } } diff --git a/modules/utils/queryString.js b/modules/utils/queryString.js index e0577b713..d9a3b283f 100644 --- a/modules/utils/queryString.js +++ b/modules/utils/queryString.js @@ -34,7 +34,8 @@ const formatValue = (config, currentValue, valueSrc, valueType, fieldWidgetDefin const args = currentValue.get('args'); const funcConfig = getFuncConfig(funcKey, config); const funcName = isForDisplay && funcConfig.label || funcKey; - const formattedArgs = []; + const formattedArgs = {}; + const formattedArgsWithNames = {}; for (const argKey in funcConfig.args) { const argConfig = funcConfig.args[argKey]; const fieldDef = getFieldConfig(argConfig, config); @@ -43,8 +44,10 @@ const formatValue = (config, currentValue, valueSrc, valueType, fieldWidgetDefin const argValueSrc = argVal ? argVal.get('valueSrc') : undefined; const formattedArgVal = formatValue(config, argValue, argValueSrc, argConfig.type, fieldDef, argConfig, null, null, isForDisplay); const argName = isForDisplay && argConfig.label || argKey; - if (formattedArgVal !== undefined) // skip optional in the end - formattedArgs.push([argName, formattedArgVal]); + if (formattedArgVal !== undefined) { // skip optional in the end + formattedArgs[argKey] = formattedArgVal; + formattedArgsWithNames[argName] = formattedArgVal; + } } if (typeof funcConfig.formatFunc === 'function') { const fn = funcConfig.formatFunc; @@ -54,7 +57,7 @@ const formatValue = (config, currentValue, valueSrc, valueType, fieldWidgetDefin ]; ret = fn(...args); } else { - ret = `${funcName}(${formattedArgs.map(([k, v]) => (isForDisplay ? `${k}: ${v}` : `${v}`)).join(', ')})`; + ret = `${funcName}(${Object.entries(formattedArgsWithNames).map(([k, v]) => (isForDisplay ? `${k}: ${v}` : `${v}`)).join(', ')})`; } } else { if (typeof fieldWidgetDefinition.formatValue === 'function') { diff --git a/modules/utils/sqlFormat.js b/modules/utils/sqlFormat.js index 8b979cc12..be3967f33 100644 --- a/modules/utils/sqlFormat.js +++ b/modules/utils/sqlFormat.js @@ -64,7 +64,7 @@ const sqlFormatValue = (config, currentValue, valueSrc, valueType, fieldWidgetDe const args = currentValue.get('args'); const funcConfig = getFuncConfig(funcKey, config); const funcName = funcConfig.sqlFunc || funcKey; - const formattedArgs = []; + const formattedArgs = {}; for (const argKey in funcConfig.args) { const argConfig = funcConfig.args[argKey]; const fieldDef = getFieldConfig(argConfig, config); @@ -73,7 +73,7 @@ const sqlFormatValue = (config, currentValue, valueSrc, valueType, fieldWidgetDe const argValueSrc = argVal ? argVal.get('valueSrc') : undefined; const formattedArgVal = sqlFormatValue(config, argValue, argValueSrc, argConfig.type, fieldDef, argConfig, null, null); if (formattedArgVal !== undefined) // skip optional in the end - formattedArgs.push([argKey, formattedArgVal]); + formattedArgs[argKey] = formattedArgVal; } if (typeof funcConfig.sqlFormatFunc === 'function') { const fn = funcConfig.sqlFormatFunc; @@ -82,7 +82,7 @@ const sqlFormatValue = (config, currentValue, valueSrc, valueType, fieldWidgetDe ]; ret = fn(...args); } else { - ret = `${funcName}(${formattedArgs.map(([k, v]) => v).join(', ')})`; + ret = `${funcName}(${Object.entries(formattedArgs).map(([k, v]) => v).join(', ')})`; } } else { if (typeof fieldWidgetDefinition.sqlFormatValue === 'function') { diff --git a/package.json b/package.json index 501e82c8a..4be61749c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-awesome-query-builder", - "version": "1.0.11", + "version": "1.0.12", "description": "User-friendly query builder for React. Demo: https://ukrbublik.github.io/react-awesome-query-builder", "keywords": [ "reactjs", diff --git a/sandbox/package.json b/sandbox/package.json index 885068075..614e7624f 100644 --- a/sandbox/package.json +++ b/sandbox/package.json @@ -1,6 +1,6 @@ { "name": "react-awesome-query-builder-demo", - "version": "0.2.4", + "version": "0.2.5", "description": "Demo for react-awesome-query-builder", "main": "src/index.jsx", "readme": "README.md", @@ -19,7 +19,7 @@ "dependencies": { "react": "^16.4.1", "react-dom": "^16.4.1", - "react-awesome-query-builder": "^1.0.11", + "react-awesome-query-builder": "^1.0.12", "react-scripts": "3.0.1" }, "browserslist": { diff --git a/sandbox/src/demo/config.js b/sandbox/src/demo/config.js index bf19d9e13..3fe9dc2d9 100644 --- a/sandbox/src/demo/config.js +++ b/sandbox/src/demo/config.js @@ -221,6 +221,7 @@ const fields = { min: -1, max: 5 }, + funcs: ['LINEAR_REGRESSION'], }, slider: { label: 'Slider', @@ -317,7 +318,35 @@ const funcs = { valueSources: ['value', 'field'], }, } - } + }, + LINEAR_REGRESSION: { + label: 'Linear regression', + returnType: 'number', + formatFunc: ({coef, bias, val}, _) => `(${coef} * ${val} + ${bias})`, + sqlFormatFunc: ({coef, bias, val}) => `(${coef} * ${val} + ${bias})`, + mongoFormatFunc: ({coef, bias, val}) => ({'$sum': [{'$multiply': [coef, val]}, bias]}), + renderBrackets: ['', ''], + renderSeps: [' * ', ' + '], + args: { + coef: { + label: "Coef", + type: 'number', + defaultValue: 1, + valueSources: ['value'], + }, + val: { + label: "Value", + type: 'number', + valueSources: ['value'], + }, + bias: { + label: "Bias", + type: 'number', + defaultValue: 0, + valueSources: ['value'], + } + } + }, };