ukrbublik / react-awesome-query-builder

User-friendly query builder for React
https://ukrbublik.github.io/react-awesome-query-builder
MIT License
2k stars 498 forks source link

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

Open FlorianRuen opened 1 year ago

FlorianRuen commented 1 year ago

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

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 ?

ukrbublik commented 1 year ago

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.

FlorianRuen commented 1 year ago

@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

ukrbublik commented 1 year ago

As a workaround, you can have several functions:

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

FlorianRuen commented 1 year ago

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'],
            }
        }
    }
};

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

ukrbublik commented 1 year ago

Please use

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

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

FlorianRuen commented 1 year ago

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

ukrbublik commented 1 year ago

(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 ==

FlorianRuen commented 1 year ago

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) ?

ukrbublik commented 1 year ago

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

FlorianRuen commented 1 year ago

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)

ukrbublik commented 1 year ago

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

FlorianRuen commented 1 year ago

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

ukrbublik commented 1 year ago

Please add allowNesting: true in func config

FlorianRuen commented 1 year ago

@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 commented 1 year ago

@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)
ukrbublik commented 1 year ago

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 commented 1 year ago

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

FlorianRuen commented 1 year ago

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;
ukrbublik commented 1 year ago

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

FlorianRuen commented 1 year ago

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