From e507ce9a893ea8f86b96f2c8e071ff6d6762b10d Mon Sep 17 00:00:00 2001 From: Tsering Paljor Date: Mon, 10 Nov 2025 15:19:31 +0400 Subject: [PATCH] Improvements to Advanced Formulas (#4169) * Allow lexicographical comparisons for >, <, >=, and <= operators. * Improve/correct examples * Ensure arithmetic operators can accept 2 or more args. Ensure frontend/backend formula functions are implemented similarly. * Add and formula types. * Fix bug reported by Frederik: concat should use Text arg type * Update examples and small fixes. * Lint fix * Update and/or examples to use symbols. Add useStrict option to ensureBoolean(). * For operators, specify 2 args, since operators already reduce * Replace manual checking of arg len * Ensure backend implementation matches frontend * Add castToInt option and make 2nd arg to round() an optional int * Lint fix --- backend/src/baserow/core/apps.py | 8 +- .../baserow/core/formula/argument_types.py | 23 +-- .../core/formula/parser/python_executor.py | 4 + .../core/formula/runtime_formula_types.py | 125 ++++++------ .../core/formula/test_argument_types.py | 78 -------- web-frontend/locales/en.json | 2 + web-frontend/modules/core/plugin.js | 4 + .../core/runtimeFormulaArgumentTypes.js | 12 +- .../modules/core/runtimeFormulaTypes.js | 188 +++++++++++++----- web-frontend/modules/core/utils/validator.js | 8 +- 10 files changed, 237 insertions(+), 215 deletions(-) diff --git a/backend/src/baserow/core/apps.py b/backend/src/baserow/core/apps.py index 3bbf16dc62..88ad043caf 100755 --- a/backend/src/baserow/core/apps.py +++ b/backend/src/baserow/core/apps.py @@ -37,6 +37,7 @@ def ready(self): from baserow.core.formula.registries import formula_runtime_function_registry from baserow.core.formula.runtime_formula_types import ( RuntimeAdd, + RuntimeAnd, RuntimeCapitalize, RuntimeConcat, RuntimeDateTimeFormat, @@ -53,6 +54,7 @@ def ready(self): RuntimeIsEven, RuntimeIsOdd, RuntimeLessThan, + RuntimeLessThanOrEqual, RuntimeLower, RuntimeMinus, RuntimeMinute, @@ -60,6 +62,7 @@ def ready(self): RuntimeMultiply, RuntimeNotEqual, RuntimeNow, + RuntimeOr, RuntimeRandomBool, RuntimeRandomFloat, RuntimeRandomInt, @@ -78,8 +81,9 @@ def ready(self): formula_runtime_function_registry.register(RuntimeDivide()) formula_runtime_function_registry.register(RuntimeEqual()) formula_runtime_function_registry.register(RuntimeNotEqual()) - formula_runtime_function_registry.register(RuntimeGreaterThan()) formula_runtime_function_registry.register(RuntimeLessThan()) + formula_runtime_function_registry.register(RuntimeLessThanOrEqual()) + formula_runtime_function_registry.register(RuntimeGreaterThan()) formula_runtime_function_registry.register(RuntimeGreaterThanOrEqual()) formula_runtime_function_registry.register(RuntimeUpper()) formula_runtime_function_registry.register(RuntimeLower()) @@ -102,6 +106,8 @@ def ready(self): formula_runtime_function_registry.register(RuntimeRandomBool()) formula_runtime_function_registry.register(RuntimeGenerateUUID()) formula_runtime_function_registry.register(RuntimeIf()) + formula_runtime_function_registry.register(RuntimeAnd()) + formula_runtime_function_registry.register(RuntimeOr()) from baserow.core.permission_manager import ( AllowIfTemplatePermissionManagerType, diff --git a/backend/src/baserow/core/formula/argument_types.py b/backend/src/baserow/core/formula/argument_types.py index d2acb19597..49624da264 100644 --- a/backend/src/baserow/core/formula/argument_types.py +++ b/backend/src/baserow/core/formula/argument_types.py @@ -25,6 +25,10 @@ def parse(self, value): class NumberBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): + def __init__(self, *args, **kwargs): + self.cast_to_int = kwargs.pop("cast_to_int", False) + super().__init__(*args, **kwargs) + def test(self, value): try: ensure_numeric(value) @@ -33,7 +37,8 @@ def test(self, value): return False def parse(self, value): - return ensure_numeric(value) + value = ensure_numeric(value) + return int(value) if self.cast_to_int else value class TextBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): @@ -48,22 +53,6 @@ def parse(self, value): return ensure_string(value) -class AddableBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): - def test(self, value): - return hasattr(value, "__add__") - - def parse(self, value): - return value - - -class SubtractableBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): - def test(self, value): - return hasattr(value, "__sub__") - - def parse(self, value): - return value - - class DateTimeBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): def test(self, value): try: diff --git a/backend/src/baserow/core/formula/parser/python_executor.py b/backend/src/baserow/core/formula/parser/python_executor.py index 97d18fbab9..f69d241b14 100644 --- a/backend/src/baserow/core/formula/parser/python_executor.py +++ b/backend/src/baserow/core/formula/parser/python_executor.py @@ -90,6 +90,10 @@ def visitBinaryOp(self, ctx: BaserowFormula.BinaryOpContext): op = "greater_than_or_equal" elif ctx.LTE(): op = "less_than_or_equal" + elif ctx.AMP_AMP(): + op = "and" + elif ctx.PIPE_PIPE(): + op = "or" else: raise UnknownOperator(ctx.getText()) diff --git a/backend/src/baserow/core/formula/runtime_formula_types.py b/backend/src/baserow/core/formula/runtime_formula_types.py index 4e21344e3a..c80a08e30d 100644 --- a/backend/src/baserow/core/formula/runtime_formula_types.py +++ b/backend/src/baserow/core/formula/runtime_formula_types.py @@ -1,20 +1,16 @@ -import operator import random import uuid -from functools import reduce from typing import Optional from zoneinfo import ZoneInfo from django.utils import timezone from baserow.core.formula.argument_types import ( - AddableBaserowRuntimeFormulaArgumentType, AnyBaserowRuntimeFormulaArgumentType, BooleanBaserowRuntimeFormulaArgumentType, DateTimeBaserowRuntimeFormulaArgumentType, DictBaserowRuntimeFormulaArgumentType, NumberBaserowRuntimeFormulaArgumentType, - SubtractableBaserowRuntimeFormulaArgumentType, TextBaserowRuntimeFormulaArgumentType, TimezoneBaserowRuntimeFormulaArgumentType, ) @@ -28,7 +24,7 @@ class RuntimeConcat(RuntimeFormulaFunction): type = "concat" def validate_type_of_args(self, args) -> Optional[FormulaArg]: - arg_type = AddableBaserowRuntimeFormulaArgumentType() + arg_type = TextBaserowRuntimeFormulaArgumentType() return next( (arg for arg in args if not arg_type.test(arg)), None, @@ -52,61 +48,47 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeAdd(RuntimeFormulaFunction): type = "add" - def validate_type_of_args(self, args) -> Optional[FormulaArg]: - arg_type = AddableBaserowRuntimeFormulaArgumentType() - return next( - (arg for arg in args if not arg_type.test(arg)), - None, - ) - - def validate_number_of_args(self, args): - return len(args) >= 1 + args = [ + NumberBaserowRuntimeFormulaArgumentType(), + NumberBaserowRuntimeFormulaArgumentType(), + ] def execute(self, context: FormulaContext, args: FormulaArgs): - return reduce(operator.add, args) + return args[0] + args[1] class RuntimeMinus(RuntimeFormulaFunction): type = "minus" - def validate_type_of_args(self, args) -> Optional[FormulaArg]: - arg_type = SubtractableBaserowRuntimeFormulaArgumentType() - return next( - (arg for arg in args if not arg_type.test(arg)), - None, - ) - - def validate_number_of_args(self, args): - return len(args) > 1 + args = [ + NumberBaserowRuntimeFormulaArgumentType(), + NumberBaserowRuntimeFormulaArgumentType(), + ] def execute(self, context: FormulaContext, args: FormulaArgs): - return reduce(operator.sub, args) + return args[0] - args[1] class RuntimeMultiply(RuntimeFormulaFunction): type = "multiply" + args = [ NumberBaserowRuntimeFormulaArgumentType(), NumberBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] * args[1] class RuntimeDivide(RuntimeFormulaFunction): type = "divide" + args = [ NumberBaserowRuntimeFormulaArgumentType(), NumberBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] / args[1] @@ -118,9 +100,6 @@ class RuntimeEqual(RuntimeFormulaFunction): AnyBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] == args[1] @@ -132,9 +111,6 @@ class RuntimeNotEqual(RuntimeFormulaFunction): AnyBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] != args[1] @@ -142,13 +118,10 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeGreaterThan(RuntimeFormulaFunction): type = "greater_than" args = [ - NumberBaserowRuntimeFormulaArgumentType(), - NumberBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] > args[1] @@ -156,13 +129,10 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeLessThan(RuntimeFormulaFunction): type = "less_than" args = [ - NumberBaserowRuntimeFormulaArgumentType(), - NumberBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] < args[1] @@ -170,13 +140,10 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeGreaterThanOrEqual(RuntimeFormulaFunction): type = "greater_than_or_equal" args = [ - NumberBaserowRuntimeFormulaArgumentType(), - NumberBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] >= args[1] @@ -184,13 +151,10 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeLessThanOrEqual(RuntimeFormulaFunction): type = "less_than_or_equal" args = [ - NumberBaserowRuntimeFormulaArgumentType(), - NumberBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), ] - def validate_number_of_args(self, args): - return len(args) == 2 - def execute(self, context: FormulaContext, args: FormulaArgs): return args[0] <= args[1] @@ -227,7 +191,7 @@ class RuntimeRound(RuntimeFormulaFunction): args = [ NumberBaserowRuntimeFormulaArgumentType(), - NumberBaserowRuntimeFormulaArgumentType(), + NumberBaserowRuntimeFormulaArgumentType(optional=True, cast_to_int=True), ] def execute(self, context: FormulaContext, args: FormulaArgs): @@ -272,7 +236,7 @@ def execute(self, context: FormulaContext, args: FormulaArgs): datetime_obj = args[0] moment_format = args[1] - if (len(args)) == 2: + if len(args) == 2: timezone_name = context.get_timezone_name() else: timezone_name = args[2] @@ -350,6 +314,8 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeNow(RuntimeFormulaFunction): type = "now" + args = [] + def execute(self, context: FormulaContext, args: FormulaArgs): return timezone.now() @@ -357,6 +323,8 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeToday(RuntimeFormulaFunction): type = "today" + args = [] + def execute(self, context: FormulaContext, args: FormulaArgs): return timezone.localdate() @@ -400,6 +368,8 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeRandomBool(RuntimeFormulaFunction): type = "random_bool" + args = [] + def execute(self, context: FormulaContext, args: FormulaArgs): return random.choice([True, False]) # nosec: B311 @@ -407,6 +377,8 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeGenerateUUID(RuntimeFormulaFunction): type = "generate_uuid" + args = [] + def execute(self, context: FormulaContext, args: FormulaArgs): return str(uuid.uuid4()) @@ -414,12 +386,35 @@ def execute(self, context: FormulaContext, args: FormulaArgs): class RuntimeIf(RuntimeFormulaFunction): type = "if" - def validate_type_of_args(self, args) -> Optional[FormulaArg]: - arg_type = BooleanBaserowRuntimeFormulaArgumentType() - if not arg_type.test(args[0]): - return args[0] - - return None + args = [ + BooleanBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), + AnyBaserowRuntimeFormulaArgumentType(), + ] def execute(self, context: FormulaContext, args: FormulaArgs): return args[1] if args[0] else args[2] + + +class RuntimeAnd(RuntimeFormulaFunction): + type = "and" + + args = [ + BooleanBaserowRuntimeFormulaArgumentType(), + BooleanBaserowRuntimeFormulaArgumentType(), + ] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0] and args[1] + + +class RuntimeOr(RuntimeFormulaFunction): + type = "or" + + args = [ + BooleanBaserowRuntimeFormulaArgumentType(), + BooleanBaserowRuntimeFormulaArgumentType(), + ] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0] or args[1] diff --git a/backend/tests/baserow/core/formula/test_argument_types.py b/backend/tests/baserow/core/formula/test_argument_types.py index cf830bc5b2..e498d3c9f4 100644 --- a/backend/tests/baserow/core/formula/test_argument_types.py +++ b/backend/tests/baserow/core/formula/test_argument_types.py @@ -3,11 +3,9 @@ import pytest from baserow.core.formula.argument_types import ( - AddableBaserowRuntimeFormulaArgumentType, DateTimeBaserowRuntimeFormulaArgumentType, DictBaserowRuntimeFormulaArgumentType, NumberBaserowRuntimeFormulaArgumentType, - SubtractableBaserowRuntimeFormulaArgumentType, TextBaserowRuntimeFormulaArgumentType, ) @@ -87,82 +85,6 @@ def test_text_parse_method(value, expected): assert TextBaserowRuntimeFormulaArgumentType().parse(value) == expected -@pytest.mark.parametrize( - "value,expected", - [ - (-5, True), - (-5.5, True), - ("-5.5", True), - (0, True), - (10, True), - ("10", True), - (16.25, True), - ("16.25", True), - ([], True), - ("", True), - ({}, False), - (None, False), - ], -) -def test_addable_test_method(value, expected): - assert AddableBaserowRuntimeFormulaArgumentType().test(value) == expected - - -@pytest.mark.parametrize( - "value", - [ - -5, - -5.5, - "-5.5", - 0, - 10, - "10", - 16.25, - "16.25", - [], - "", - {}, - ], -) -def test_addable_parse_method(value): - assert AddableBaserowRuntimeFormulaArgumentType().parse(value) == value - - -@pytest.mark.parametrize( - "value,expected", - [ - (-5, True), - (-5.5, True), - (0, True), - (10, True), - (16.25, True), - ([], False), - ("", False), - ("-5.5", False), - ("10", False), - ("16.25", False), - ({}, False), - (None, False), - ], -) -def test_subtractable_test_method(value, expected): - assert SubtractableBaserowRuntimeFormulaArgumentType().test(value) == expected - - -@pytest.mark.parametrize( - "value", - [ - -5, - -5.5, - 0, - 10, - 16.25, - ], -) -def test_subtractable_parse_method(value): - assert SubtractableBaserowRuntimeFormulaArgumentType().parse(value) == value - - @pytest.mark.parametrize( "value,expected", [ diff --git a/web-frontend/locales/en.json b/web-frontend/locales/en.json index 1627e23a17..31f5586079 100644 --- a/web-frontend/locales/en.json +++ b/web-frontend/locales/en.json @@ -673,6 +673,8 @@ "randomBoolDescription": "Returns a random boolean of true or false.", "generateUUIDDescription": "Returns a random UUID4 string.", "ifDescription": "If the first argument is true, returns the second argument, otherwise returns the third argument.", + "andDescription": "Returns true if all arguments are true, otherwise returns false.", + "orDescription": "Returns true if any argument is true, otherwise returns false.", "formulaTypeFormula": "Formula", "formulaTypeOperator": "Operator", "categoryText": "Text", diff --git a/web-frontend/modules/core/plugin.js b/web-frontend/modules/core/plugin.js index ac4958fc79..2d1dab0c09 100644 --- a/web-frontend/modules/core/plugin.js +++ b/web-frontend/modules/core/plugin.js @@ -126,6 +126,8 @@ import { RuntimeRandomBool, RuntimeGenerateUUID, RuntimeIf, + RuntimeAnd, + RuntimeOr, } from '@baserow/modules/core/runtimeFormulaTypes' import priorityBus from '@baserow/modules/core/plugins/priorityBus' @@ -305,6 +307,8 @@ export default (context, inject) => { registry.register('runtimeFormulaFunction', new RuntimeRandomBool(context)) registry.register('runtimeFormulaFunction', new RuntimeGenerateUUID(context)) registry.register('runtimeFormulaFunction', new RuntimeIf(context)) + registry.register('runtimeFormulaFunction', new RuntimeAnd(context)) + registry.register('runtimeFormulaFunction', new RuntimeOr(context)) registry.register('roles', new AdminRoleType(context)) registry.register('roles', new MemberRoleType(context)) diff --git a/web-frontend/modules/core/runtimeFormulaArgumentTypes.js b/web-frontend/modules/core/runtimeFormulaArgumentTypes.js index ba22b85eff..9d003980ac 100644 --- a/web-frontend/modules/core/runtimeFormulaArgumentTypes.js +++ b/web-frontend/modules/core/runtimeFormulaArgumentTypes.js @@ -36,9 +36,12 @@ export class BaserowRuntimeFormulaArgumentType { } export class NumberBaserowRuntimeFormulaArgumentType extends BaserowRuntimeFormulaArgumentType { + constructor(options = {}) { + super(options) + this.castToInt = options.castToInt ?? false + } + test(value) { - // get() formula can't be resolved in the frontend because we don't have - // the data/context. Return true so that the enclosing formula can be resolved. if (value === undefined) { return false } @@ -47,7 +50,8 @@ export class NumberBaserowRuntimeFormulaArgumentType extends BaserowRuntimeFormu } parse(value) { - return ensureNumeric(value, { allowNull: true }) + const val = ensureNumeric(value, { allowNull: true }) + return this.castToInt ? Math.trunc(val) : val } } @@ -104,7 +108,7 @@ export class BooleanBaserowRuntimeFormulaArgumentType extends BaserowRuntimeForm } parse(value) { - return ensureBoolean(value) + return ensureBoolean(value, { useStrict: false }) } } diff --git a/web-frontend/modules/core/runtimeFormulaTypes.js b/web-frontend/modules/core/runtimeFormulaTypes.js index bbffd6cf2e..211fa6d293 100644 --- a/web-frontend/modules/core/runtimeFormulaTypes.js +++ b/web-frontend/modules/core/runtimeFormulaTypes.js @@ -209,7 +209,7 @@ export class RuntimeConcat extends RuntimeFormulaFunction { } validateNumberOfArgs(args) { - return args.length >= 2 + return args.length > 1 } toNode(args) { @@ -228,7 +228,10 @@ export class RuntimeConcat extends RuntimeFormulaFunction { } getExamples() { - return ["concat('Hello,', ' World!') = 'Hello, world!'"] + return [ + "concat('Hello,', ' World!') = 'Hello, world!'", + "concat(get('data_source.1.0.field_1'), ' bar') = 'foo bar'", + ] } } @@ -341,8 +344,8 @@ export class RuntimeAdd extends RuntimeFormulaFunction { ] } - execute(context, [a, b]) { - return a + b + execute(context, args) { + return args[0] + args[1] } getDescription() { @@ -351,7 +354,7 @@ export class RuntimeAdd extends RuntimeFormulaFunction { } getExamples() { - return ['2 + 3 = 5'] + return ['2 + 3 = 5', '1 + 2 + 3 = 6'] } } @@ -379,8 +382,8 @@ export class RuntimeMinus extends RuntimeFormulaFunction { ] } - execute(context, [a, b]) { - return a - b + execute(context, args) { + return args[0] - args[1] } getDescription() { @@ -389,7 +392,7 @@ export class RuntimeMinus extends RuntimeFormulaFunction { } getExamples() { - return ['3 - 2 = 1'] + return ['3 - 2 = 1', '5 - 2 - 1 = 2'] } } @@ -417,8 +420,8 @@ export class RuntimeMultiply extends RuntimeFormulaFunction { ] } - execute(context, [a, b]) { - return a * b + execute(context, args) { + return args[0] * args[1] } getDescription() { @@ -427,7 +430,7 @@ export class RuntimeMultiply extends RuntimeFormulaFunction { } getExamples() { - return ['2 * 3 = 6'] + return ['2 * 3 = 6', '2 * 3 * 3 = 18'] } } @@ -455,8 +458,8 @@ export class RuntimeDivide extends RuntimeFormulaFunction { ] } - execute(context, [a, b]) { - return a / b + execute(context, args) { + return args[0] / args[1] } getDescription() { @@ -465,7 +468,7 @@ export class RuntimeDivide extends RuntimeFormulaFunction { } getExamples() { - return ['6 / 2 = 3'] + return ['6 / 2 = 3', '15 / 2 / 2 = 3.75'] } } @@ -564,8 +567,8 @@ export class RuntimeGreaterThan extends RuntimeFormulaFunction { get args() { return [ - new NumberBaserowRuntimeFormulaArgumentType(), - new NumberBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), ] } @@ -579,7 +582,7 @@ export class RuntimeGreaterThan extends RuntimeFormulaFunction { } getExamples() { - return ['5 > 4 = true'] + return ['5 > 4 = true', '"a" > "b" = false', '"Ambarella" > "fig" = false'] } } @@ -602,8 +605,8 @@ export class RuntimeLessThan extends RuntimeFormulaFunction { get args() { return [ - new NumberBaserowRuntimeFormulaArgumentType(), - new NumberBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), ] } @@ -617,7 +620,7 @@ export class RuntimeLessThan extends RuntimeFormulaFunction { } getExamples() { - return ['2 < 3 = true'] + return ['2 < 3 = true', '"b" < "a" = false', '"Ambarella" < "fig" = true'] } } @@ -640,8 +643,8 @@ export class RuntimeGreaterThanOrEqual extends RuntimeFormulaFunction { get args() { return [ - new NumberBaserowRuntimeFormulaArgumentType(), - new NumberBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), ] } @@ -655,13 +658,17 @@ export class RuntimeGreaterThanOrEqual extends RuntimeFormulaFunction { } getExamples() { - return ['3 >= 2 = false'] + return [ + '3 >= 2 = false', + '"b" >= "a" = true', + '"Ambarella" >= "fig" = false', + ] } } export class RuntimeLessThanOrEqual extends RuntimeFormulaFunction { static getType() { - return 'less_than' + return 'less_than_or_equal' } static getFormulaType() { @@ -678,8 +685,8 @@ export class RuntimeLessThanOrEqual extends RuntimeFormulaFunction { get args() { return [ - new NumberBaserowRuntimeFormulaArgumentType(), - new NumberBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), ] } @@ -693,7 +700,11 @@ export class RuntimeLessThanOrEqual extends RuntimeFormulaFunction { } getExamples() { - return ['3 <= 3 = true'] + return [ + '3 <= 3 = true', + '"a" <= "b" = false', + '"fig" <= "Ambarella" = false', + ] } } @@ -812,12 +823,15 @@ export class RuntimeRound extends RuntimeFormulaFunction { get args() { return [ new NumberBaserowRuntimeFormulaArgumentType(), - new NumberBaserowRuntimeFormulaArgumentType(), + new NumberBaserowRuntimeFormulaArgumentType({ + optional: true, + castToInt: true, + }), ] } execute(context, args) { - // Default to 2 decimal places? + // Default to 2 decimal places let decimalPlaces = 2 if (args.length === 2) { @@ -865,7 +879,7 @@ export class RuntimeIsEven extends RuntimeFormulaFunction { } getExamples() { - return ['is_even(12) = true'] + return ['is_even(12) = true', 'is_even(13) = false'] } } @@ -896,7 +910,7 @@ export class RuntimeIsOdd extends RuntimeFormulaFunction { } getExamples() { - return ['is_odd(11) = true'] + return ['is_odd(11) = true', 'is_odd(12) = false'] } } @@ -1097,7 +1111,7 @@ export class RuntimeMinute extends RuntimeFormulaFunction { } getExamples() { - return ["minute('2025-10-16T11:05:38') = '05'"] + return ["minute('2025-10-16T11:05:38') = '5'"] } } @@ -1145,6 +1159,10 @@ export class RuntimeNow extends RuntimeFormulaFunction { return FORMULA_CATEGORY.DATE } + get args() { + return [] + } + execute(context, args) { return new Date() } @@ -1167,6 +1185,10 @@ export class RuntimeToday extends RuntimeFormulaFunction { return FORMULA_CATEGORY.DATE } + get args() { + return [] + } + execute(context, args) { return new Date().toISOString().split('T')[0] } @@ -1238,7 +1260,7 @@ export class RuntimeRandomInt extends RuntimeFormulaFunction { execute(context, args) { const min = Math.ceil(args[0]) const max = Math.floor(args[1]) - return Math.floor(Math.random() * (max - min) + min) + return Math.floor(Math.random() * (max - min + 1) + min) } getDescription() { @@ -1298,8 +1320,8 @@ export class RuntimeRandomBool extends RuntimeFormulaFunction { return FORMULA_CATEGORY.BOOLEAN } - validateNumberOfArgs(args) { - return args.length === 0 + get args() { + return [] } execute(context, args) { @@ -1329,8 +1351,8 @@ export class RuntimeGenerateUUID extends RuntimeFormulaFunction { return FORMULA_CATEGORY.TEXT } - validateNumberOfArgs(args) { - return args.length === 0 + get args() { + return [] } execute(context, args) { @@ -1360,16 +1382,12 @@ export class RuntimeIf extends RuntimeFormulaFunction { return FORMULA_CATEGORY.CONDITION } - validateNumberOfArgs(args) { - return args.length === 3 - } - - validateTypeOfArgs(args) { - const argType = new BooleanBaserowRuntimeFormulaArgumentType() - if (!argType.test(args[0])) { - return args[0] - } - return null + get args() { + return [ + new BooleanBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), + new AnyBaserowRuntimeFormulaArgumentType(), + ] } execute(context, args) { @@ -1383,8 +1401,80 @@ export class RuntimeIf extends RuntimeFormulaFunction { getExamples() { return [ - 'if(true, true, false)', - "if(random_bool(), 'Random bool is true', 'Random bool is false')", + 'if(true, true, false) = true', + "if(random_bool(), 'Random bool is true', 'Random bool is false') = 'Random bool is false'", + ] + } +} + +export class RuntimeAnd extends RuntimeFormulaFunction { + static getType() { + return 'and' + } + + static getFormulaType() { + return FORMULA_TYPE.FUNCTION + } + + static getCategoryType() { + return FORMULA_CATEGORY.CONDITION + } + + get args() { + return [ + new BooleanBaserowRuntimeFormulaArgumentType(), + new BooleanBaserowRuntimeFormulaArgumentType(), + ] + } + + execute(context, args) { + return args[0] && args[1] + } + + getDescription() { + const { i18n } = this.app + return i18n.t('runtimeFormulaTypes.andDescription') + } + + getExamples() { + return ['true && true = true', 'true && true && false = false'] + } +} + +export class RuntimeOr extends RuntimeFormulaFunction { + static getType() { + return 'or' + } + + static getFormulaType() { + return FORMULA_TYPE.FUNCTION + } + + static getCategoryType() { + return FORMULA_CATEGORY.CONDITION + } + + get args() { + return [ + new BooleanBaserowRuntimeFormulaArgumentType(), + new BooleanBaserowRuntimeFormulaArgumentType(), + ] + } + + execute(context, args) { + return args[0] || args[1] + } + + getDescription() { + const { i18n } = this.app + return i18n.t('runtimeFormulaTypes.orDescription') + } + + getExamples() { + return [ + 'true || true = true', + 'true || true || false = true', + 'false || false = false', ] } } diff --git a/web-frontend/modules/core/utils/validator.js b/web-frontend/modules/core/utils/validator.js index cc474a3151..4555d1b486 100644 --- a/web-frontend/modules/core/utils/validator.js +++ b/web-frontend/modules/core/utils/validator.js @@ -182,14 +182,20 @@ export const ensureNonEmptyString = (value, options) => { /** * Ensures that the value is a boolean or convert it. * @param {*} value - The value to ensure as a boolean. + * @param {boolean} useStrict - Whether to be strict in how the value is interpreted. * @returns {boolean} The value as a boolean. */ -export const ensureBoolean = (value) => { +export const ensureBoolean = (value, { useStrict = true }) => { if (trueValues.includes(value)) { return true } else if (falseValues.includes(value)) { return false } + + if (!useStrict) { + return Boolean(value) + } + throw new Error('Value is not a valid boolean or convertible to a boolean.') }