react-awesome-query-builder icon indicating copy to clipboard operation
react-awesome-query-builder copied to clipboard

Dynamic number of args for function (eg. SUM of any number of numbers)

Open FlorianRuen opened this issue 2 years ago • 21 comments

Feature request

Allow special function arguments that can have dynamic number of values. Think about it as destructing functions arguments in JS like someFunc(a, ...args) - we want to support ...args where args is a special argument. Currently one argument can have one value (of type value, field or func depending on valueSources in func's arg configuration - meaning function can be argument to another function). So only someFunc(a, b) or someFunc(a, b, c) is possible.

Implementation hints

  • It worth adding new boolean flag smth like isDynamicNumberOfValues: true in arg config.
  • In UI show [+] (add) button after list of arg values. And [-] after each arg value to remove.
  • In store save arg value as array -OR- store arguments as flat object like now but add number prefixes/suffixes for new dynamic arguments. Eg. if func can have simple argument a and new dynamic argument b then args object is store can have shape like
      "args": {
        "a": {
          "value": 1,
          "valueSrc": "value"
        },
        "b__0": {
          "valueSrc": "value",
          "value": 2
        },
        "b__1": {
          "value": 3,
          "valueSrc": "value"
        }
      }

Get acquainted with current implementation details

Rendering function arguments: https://github.com/ukrbublik/react-awesome-query-builder/blob/fdc33cabc500179ae58b4a0e35143fcc3c153ac1/packages/ui/modules/components/rule/FuncWidget.jsx#L211

Example of function config (with args config): https://github.com/ukrbublik/react-awesome-query-builder/blob/fdc33cabc500179ae58b4a0e35143fcc3c153ac1/packages/core/modules/config/funcs.js#L558

Export function for JsonLogic: https://github.com/ukrbublik/react-awesome-query-builder/blob/fdc33cabc500179ae58b4a0e35143fcc3c153ac1/packages/core/modules/export/jsonLogic.js#L443

Import function from JsonLogic: https://github.com/ukrbublik/react-awesome-query-builder/blob/fdc33cabc500179ae58b4a0e35143fcc3c153ac1/packages/core/modules/import/jsonLogic.js#L545

Validation of functions in query: https://github.com/ukrbublik/react-awesome-query-builder/blob/fdc33cabc500179ae58b4a0e35143fcc3c153ac1/packages/core/modules/utils/validation.js#L966

Using in reducers: https://github.com/ukrbublik/react-awesome-query-builder/blob/fdc33cabc500179ae58b4a0e35143fcc3c153ac1/packages/core/modules/stores/tree.js#L462 https://github.com/ukrbublik/react-awesome-query-builder/blob/fdc33cabc500179ae58b4a0e35143fcc3c153ac1/packages/core/modules/utils/ruleUtils.js#L370

Note: You can search other occurences of functions usage in core/ui parts of project by searching getFuncConfig: https://github.com/search?q=repo%3Aukrbublik%2Freact-awesome-query-builder%20getFuncConfig&type=code

Original issue description

I'm using a jsonlogic standard to create some queries, but on react query builder I didn't see a way to create custom operators like plus, minus, multiply with some constraints

  • Multiply and plus, can take multiple value (array [2,2,2,2] for example)
  • Minus and Divide can take only two args (array [4,2] for example)

I tried to add an element to the operators, but it's not showing (very simple without logic, but not showing at this time)

const operators = {
    ...BasicConfig.operators,
    plus: {
        label: "Plus"
    }
};

There is a way to create this kind of arithmetic operators ? I saw a answer using https://github.com/ukrbublik/react-awesome-query-builder/issues/809 but I want to create an operator instead of function (easier to use and better UI for users)

Also, I tried to copy the LINEAR_REGRESSION and put the content in operators, but not showing anymore

In JSONLOGIC, I need to generate something like :

{
   ">=":[
      {
         "+":[
            {
               "var":"a"
            },
            {
               "var":"b"
            }
         ]
      },
      10
   ]
}

Any idea ?

FlorianRuen avatar Jul 25 '23 09:07 FlorianRuen

The correct way to achieve this is to add custom function (you can take LINEAR_REGRESSION as example).

With version 6.4.0 you can use functions in LHS. I will add support of simple math functions in next version. For now there is no support of dynamic arg length (like SUM(...numbers) instead of SUM(a, b)), but I'll work on it soon.

ukrbublik avatar Jul 25 '23 10:07 ukrbublik

@ukrbublik Thanks for the answer While waiting to have a list of arguments, would it be possible to accept at least 3 rather than 2? I saw on the LINEAR_REGRESSION we can ave multiple args, so my idea is to create multiple args here

FlorianRuen avatar Jul 25 '23 11:07 FlorianRuen

As a workaround, you can have several functions:

  • SUM2(a, b) with 2 args
  • SUM3(a, b, c) with 3 args
  • SUM4(a, b, c, d) with 4 args

Or (probably better) one function SUM4(a, b, c, d) where c and d are optional

ukrbublik avatar Jul 25 '23 12:07 ukrbublik

I tried using this, but I think, two problems will appear if I want var1 + var2 + var3 <= 21 (as example) :

const funcs = {
    SUM: {
        label: '+',
        returnType: 'number',
        jsonLogic: ({a, b, c, d}) => ({ "+": [a, b, c, d]}),
        renderBrackets: ['', ''],
        renderSeps: [' + ', ' + ', ' + ', ' + '],
        args: {
            a: {
                label: "A",
                type: 'number',
                defaultValue: 1,
                valueSources: ['value'],
            },
            b: {
                label: "B",
                type: 'number',
                defaultValue: 0,
                valueSources: ['value'],
            },
            c: {
                label: "C",
                type: 'number',
                defaultValue: 0,
                valueSources: ['field', 'value'],
            },
            d: {
                label: "D",
                type: 'number',
                defaultValue: 0,
                valueSources: ['value'],
            }
        }
    }
};
  • It seems the first operator is mandatory, when I select varA, it ask for an operator, I want only func sum
  • Using valueSources = ['field', 'value'] seems not working very well, because of the first field selector ;
  • Even if the function param are optional, it seems the fields selector can't be hidden for now ;

Want I want is sum(a, b, c, d) == 10 and I appear that I can only create varA = sum(a, b, c, d)

Capture d’écran du 2023-07-25 14-20-38

FlorianRuen avatar Jul 25 '23 12:07 FlorianRuen

Please use

fieldSources: ["field", "func"],

in config.settings, then you can put sum in left side and 10 in right side

ukrbublik avatar Jul 25 '23 19:07 ukrbublik

Please use

fieldSources: ["field", "func"],

in config.settings, then you can put sum in left side and 10 in right side

Indeed, I already had it is, but I had not installed the latest version from yesterday

The correct way to achieve this is to add custom function (you can take LINEAR_REGRESSION as example).

With version 6.4.0 you can use functions in LHS. I will add support of simple math functions in next version. For now there is no support of dynamic arg length (like SUM(...numbers) instead of SUM(a, b)), but I'll work on it soon.

Do you have an estimate of the release dates for these versions? For both simple math function (because should be better in operator than in funcs) and for the dynamic arg lenght

FlorianRuen avatar Jul 26 '23 08:07 FlorianRuen

(because should be better in operator than in funcs)

I don't have plans to extend operators. I have plans to add new math funcs. In your example you use operator ==

ukrbublik avatar Jul 26 '23 08:07 ukrbublik

All right. So the math functions will be considered as funcs

But in JsonLogic, the negative operator can take only two values (similar as division), the alternative should be A - (B + C) is equal to A - B - C, using funcs, this will not be possible (like func in func or something) ?

FlorianRuen avatar Jul 26 '23 09:07 FlorianRuen

It's already possible to put func in func, of you specify valueSources: ['value', 'func'] for arg

ukrbublik avatar Jul 26 '23 09:07 ukrbublik

If this is already possible, there might already be a way to manage the sum more easily:

Could the arg be of type multi select ? like this, a sum function could take the set of values selected in the list (like for cars / vendor on the demo)

If this is possible, it would be quite possible to be able to sum with several values (as for a dynamic arg number finally)

FlorianRuen avatar Jul 26 '23 09:07 FlorianRuen

Yes, it can be multiselect, but it provides array of strings, and you need array of integers. Probably you can customize multiselect widget to not allow any characters other than 0-9 I plan to support array of integers in future, but can't give you ETA as it's my hobby project and I have full time work

ukrbublik avatar Jul 26 '23 09:07 ukrbublik

Great, I will go deeper in the configuration, and maybe If I found some time, I can submit some pull request to improve the project

Last thing, you said, we can select func in func, but on my side, the result isn't working (the second func dropdown is always empty), am I doing something wrong?

Capture vidéo du 26-07-2023 11:33:01.webm

FlorianRuen avatar Jul 26 '23 09:07 FlorianRuen

Please add allowNesting: true in func config

ukrbublik avatar Jul 26 '23 09:07 ukrbublik

@ukrbublik Thanks for the advice, the property is allowSelfNesting I'm testing some custom multi select components, and if works, I will comment here, can be helpful

FlorianRuen avatar Jul 26 '23 10:07 FlorianRuen

@ukrbublik

One question : with custom function it seems the loadFromJsonLogic isn't working. Does that mean importing is only possible with default items?

The error in console is : Errors while importing from JsonLogic: ['Unknown LHS'] I tried to import a custom function called SUM

Also, a standard jsonLogic generated by the UI, cannot be imported using the same method

{"and":[{"==":[{"var":"a"},10]},{">=":[{"var":"b"},12]}]};

Got an error : Cannot real propety of null (reading type) happend in jsonLogic.js:465:1

    at Array.map (<anonymous>)
    at convertConj (http://localhost:4040/main.5cedf9f968dbbd461af5.hot-update.js:8803:83)
    at convertFromLogic (http://localhost:4040/main.5cedf9f968dbbd461af5.hot-update.js:8489:11)
    at _loadFromJsonLogic (http://localhost:4040/main.5cedf9f968dbbd461af5.hot-update.js:8394:28)
    at loadFromJsonLogic (http://localhost:4040/main.5cedf9f968dbbd461af5.hot-update.js:8386:10)

FlorianRuen avatar Jul 27 '23 08:07 FlorianRuen

You need to add jsonLogicImport

const SUM = {
  label: '+',
  returnType: 'number',
  // export
  jsonLogic: ({a, b, c, d}) => ({ "+": [a, b, c, d]}),
  // import
  jsonLogicImport: (v) => {
    const args = v["+"];
    return [...args];
  },
...

ukrbublik avatar Jul 27 '23 09:07 ukrbublik

As for the error, please provide your config, or codesandbox to reproduce

ukrbublik avatar Jul 27 '23 09:07 ukrbublik

Here is my config :

[... imports ...]

const operators = {
    ...BootstrapConfig.operators,
    between: {
        ...BootstrapConfig.operators.between,
        label: "Between",
        valueLabels: ['from', 'to'],
        textSeparators: ['from', 'to'],
    },
};

const widgets = {
    ...BootstrapConfig.widgets,
    number: { ...BootstrapConfig.widgets.number },
    select: { ...BootstrapConfig.widgets.select },
    func: { ...BootstrapConfig.widgets.func },
    time: {
        ...BootstrapConfig.widgets.time,
        timeFormat: 'HH:mm',
        valueFormat: 'HH:mm:ss',
    }
};

const types = {
    ...BootstrapConfig.types,
    boolean: merge(BootstrapConfig.types.boolean, {
        widgets: {
            boolean: {
                widgetProps: {
                    hideOperator: true,
                    operatorInlineLabel: "is",
                }
            },
        },
    }),
};

const localeSettings = { locale: { short: 'en', full: 'en-US' } };

const settings = {
    ...BootstrapConfig.settings,
    ...localeSettings,

    valueSourcesInfo: {
        value: { label: "Value" },
        field: { label: "Variable", widget: "field" },
        func: { label: "Function", widget: "func" }
    },
    maxNesting: 3,
    canLeaveEmptyGroup: false,
    canReorder: true,
    canRegroup: false,
    fieldSources: ["field", "func"],
    renderField: (props) => <BootstrapFieldSelect {...props} />,
    renderValueSources: (props) => <BootstrapValueSources {...props} />,
};

const fields = {
    "Pipeline": {
        label: 'Pipeline',
        tooltip: 'Pipeline',
        type: '!struct',
        subfields: GetPipelineQueryFields()
    }
};

const funcs = {
    ...BootstrapConfig.funcs,
    SUM: {
        label: 'Sum',
        returnType: 'number',
        allowSelfNesting: true,
        jsonLogic: ({firstNumber, secondNumber}) => ({ "+": [firstNumber, secondNumber]}),
        jsonLogicImport: (v) => {
            const args = v["+"];
            return [...args];
        },
        renderSeps: [' + '],
        args: {
            firstNumber: {
                label: "Number",
                type: "number",
                valueSources: ['field']
            },
            secondNumber: {
                label: "Number or func",
                type: "number",
                valueSources: ['field', 'func']
            },
        }
    },
    MINUS: {
        label: 'Subtraction',
        returnType: 'number',
        allowSelfNesting: true,
        jsonLogic: ({firstNumber, secondNumber}) => ({ "-": [firstNumber, secondNumber]}),
        jsonLogicImport: (v) => {
            const args = v["-"];
            return [...args];
        },
        renderSeps: [' - '],
        args: {
            firstNumber: {
                label: "Number",
                type: "number",
                valueSources: ['field']
            },
            secondNumber: {
                label: "Number or func",
                type: "number",
                valueSources: ['field', 'func']
            },
        }
    },
    MULTIPLY: {
        label: 'Multiply',
        returnType: 'number',
        allowSelfNesting: true,
        jsonLogic: ({firstNumber, secondNumber}) => ({ "*": [firstNumber, secondNumber]}),
        jsonLogicImport: (v) => {
            const args = v["*"];
            return [...args];
        },
        renderSeps: [' * '],
        args: {
            firstNumber: {
                label: "Number",
                type: "number",
                valueSources: ['field']
            },
            secondNumber: {
                label: "Number or func",
                type: "number",
                valueSources: ['field', 'func']
            },
        }
    },
    DIVIDE: {
        label: 'Division',
        returnType: 'number',
        allowSelfNesting: true,
        jsonLogic: ({firstNumber, secondNumber}) => ({ "/": [firstNumber, secondNumber]}),
        jsonLogicImport: (v) => {
            const args = v["/"];
            return [...args];
        },
        renderSeps: [' / '],
        args: {
            firstNumber: {
                label: "Number",
                type: "number",
                valueSources: ['field']
            },
            secondNumber: {
                label: "Number or func",
                type: "number",
                valueSources: ['field', 'func']
            },
        }
    }
};

const config = {
    ctx: BootstrapConfig.ctx,
    conjunctions,
    operators,
    widgets,
    types,
    settings,
    fields,
    funcs
};

export default config;

FlorianRuen avatar Jul 27 '23 09:07 FlorianRuen

Please try version 6.4.1 to fix the error Just triggered the release, should be available soon at NPM

ukrbublik avatar Jul 27 '23 11:07 ukrbublik

@ukrbublik seems working perfectly now! good and fast release !

FlorianRuen avatar Jul 27 '23 13:07 FlorianRuen