diff --git a/backend/src/baserow/core/apps.py b/backend/src/baserow/core/apps.py index 1747bc2465..db643b7141 100755 --- a/backend/src/baserow/core/apps.py +++ b/backend/src/baserow/core/apps.py @@ -37,13 +37,69 @@ def ready(self): from baserow.core.formula.registries import formula_runtime_function_registry from baserow.core.formula.runtime_formula_types import ( RuntimeAdd, + RuntimeCapitalize, RuntimeConcat, + RuntimeDateTimeFormat, + RuntimeDay, + RuntimeDivide, + RuntimeEqual, + RuntimeGenerateUUID, RuntimeGet, + RuntimeGetProperty, + RuntimeGreaterThan, + RuntimeGreaterThanOrEqual, + RuntimeHour, + RuntimeIsEven, + RuntimeIsOdd, + RuntimeLessThan, + RuntimeLower, + RuntimeMinus, + RuntimeMinute, + RuntimeMonth, + RuntimeMultiply, + RuntimeNotEqual, + RuntimeNow, + RuntimeRandomBool, + RuntimeRandomFloat, + RuntimeRandomInt, + RuntimeRound, + RuntimeSecond, + RuntimeToday, + RuntimeUpper, + RuntimeYear, ) formula_runtime_function_registry.register(RuntimeConcat()) formula_runtime_function_registry.register(RuntimeGet()) formula_runtime_function_registry.register(RuntimeAdd()) + formula_runtime_function_registry.register(RuntimeMinus()) + formula_runtime_function_registry.register(RuntimeMultiply()) + 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(RuntimeGreaterThanOrEqual()) + formula_runtime_function_registry.register(RuntimeUpper()) + formula_runtime_function_registry.register(RuntimeLower()) + formula_runtime_function_registry.register(RuntimeCapitalize()) + formula_runtime_function_registry.register(RuntimeRound()) + formula_runtime_function_registry.register(RuntimeIsEven()) + formula_runtime_function_registry.register(RuntimeIsOdd()) + formula_runtime_function_registry.register(RuntimeDateTimeFormat()) + formula_runtime_function_registry.register(RuntimeDay()) + formula_runtime_function_registry.register(RuntimeMonth()) + formula_runtime_function_registry.register(RuntimeYear()) + formula_runtime_function_registry.register(RuntimeHour()) + formula_runtime_function_registry.register(RuntimeMinute()) + formula_runtime_function_registry.register(RuntimeSecond()) + formula_runtime_function_registry.register(RuntimeNow()) + formula_runtime_function_registry.register(RuntimeToday()) + formula_runtime_function_registry.register(RuntimeGetProperty()) + formula_runtime_function_registry.register(RuntimeRandomInt()) + formula_runtime_function_registry.register(RuntimeRandomFloat()) + formula_runtime_function_registry.register(RuntimeRandomBool()) + formula_runtime_function_registry.register(RuntimeGenerateUUID()) 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 979ac47289..ac6d40c97f 100644 --- a/backend/src/baserow/core/formula/argument_types.py +++ b/backend/src/baserow/core/formula/argument_types.py @@ -1,3 +1,13 @@ +from django.core.exceptions import ValidationError + +from baserow.core.formula.validator import ( + ensure_datetime, + ensure_numeric, + ensure_object, + ensure_string, +) + + class BaserowRuntimeFormulaArgumentType: def test(self, value): return True @@ -9,22 +19,62 @@ def parse(self, value): class NumberBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): def test(self, value): try: - float(value) + ensure_numeric(value) return True - except ValueError: + except ValidationError: return False def parse(self, value): - return float(value) + return ensure_numeric(value) class TextBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): def test(self, value): try: - str(value) + ensure_string(value) + return True + except ValidationError: + return False + + 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: + ensure_datetime(value) + return True + except ValidationError: + return False + + def parse(self, value): + return ensure_datetime(value) + + +class DictBaserowRuntimeFormulaArgumentType(BaserowRuntimeFormulaArgumentType): + def test(self, value): + try: + ensure_object(value) return True - except TypeError: + except ValidationError: return False def parse(self, value): - return str(value) + return ensure_object(value) diff --git a/backend/src/baserow/core/formula/runtime_formula_types.py b/backend/src/baserow/core/formula/runtime_formula_types.py index 95b762bf4b..1dcf91b592 100644 --- a/backend/src/baserow/core/formula/runtime_formula_types.py +++ b/backend/src/baserow/core/formula/runtime_formula_types.py @@ -1,20 +1,40 @@ +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, + DateTimeBaserowRuntimeFormulaArgumentType, + DictBaserowRuntimeFormulaArgumentType, NumberBaserowRuntimeFormulaArgumentType, + SubtractableBaserowRuntimeFormulaArgumentType, TextBaserowRuntimeFormulaArgumentType, ) from baserow.core.formula.registries import RuntimeFormulaFunction -from baserow.core.formula.types import FormulaArgs, FormulaContext +from baserow.core.formula.types import FormulaArg, FormulaArgs, FormulaContext from baserow.core.formula.validator import ensure_string class RuntimeConcat(RuntimeFormulaFunction): type = "concat" - def execute(self, context: FormulaContext, args: FormulaArgs): - return "".join([ensure_string(a) for a in args]) + 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) >= 2 + return len(args) > 1 + + def execute(self, context: FormulaContext, args: FormulaArgs): + return "".join([ensure_string(a) for a in args]) class RuntimeGet(RuntimeFormulaFunction): @@ -27,10 +47,352 @@ 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 + + def execute(self, context: FormulaContext, args: FormulaArgs): + return reduce(operator.add, args) + + +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 + + def execute(self, context: FormulaContext, args: FormulaArgs): + return reduce(operator.sub, args) + + +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] + + +class RuntimeEqual(RuntimeFormulaFunction): + type = "equal" 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 RuntimeNotEqual(RuntimeFormulaFunction): + type = "not_equal" + 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 RuntimeGreaterThan(RuntimeFormulaFunction): + type = "greater_than" + 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 RuntimeLessThan(RuntimeFormulaFunction): + type = "less_than" + 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 RuntimeGreaterThanOrEqual(RuntimeFormulaFunction): + type = "greater_than_or_equal" + 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 RuntimeLessThanOrEqual(RuntimeFormulaFunction): + type = "less_than_or_equal" + 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 RuntimeUpper(RuntimeFormulaFunction): + type = "upper" + + args = [TextBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].upper() + + +class RuntimeLower(RuntimeFormulaFunction): + type = "lower" + + args = [TextBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].lower() + + +class RuntimeCapitalize(RuntimeFormulaFunction): + type = "capitalize" + + args = [TextBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].capitalize() + + +class RuntimeRound(RuntimeFormulaFunction): + type = "round" + + args = [ + NumberBaserowRuntimeFormulaArgumentType(), + NumberBaserowRuntimeFormulaArgumentType(), + ] + + def execute(self, context: FormulaContext, args: FormulaArgs): + # Default to 2 places + decimal_places = 2 + + if len(args) == 2: + # Avoid negative numbers + decimal_places = max(args[1], 0) + + return round(args[0], decimal_places) + + +class RuntimeIsEven(RuntimeFormulaFunction): + type = "is_even" + + args = [NumberBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0] % 2 == 0 + + +class RuntimeIsOdd(RuntimeFormulaFunction): + type = "is_odd" + + args = [NumberBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0] % 2 != 0 + + +class RuntimeDateTimeFormat(RuntimeFormulaFunction): + type = "datetime_format" + + args = [ + DateTimeBaserowRuntimeFormulaArgumentType(), + TextBaserowRuntimeFormulaArgumentType(), + TextBaserowRuntimeFormulaArgumentType(), + ] + + def execute(self, context: FormulaContext, args: FormulaArgs): + tz_name = None + tz_format = "%Y-%m-%d %H:%M:%S" + + if len(args) == 3: + tz_name = args[2] + tz_format = args[1] + elif len(args) == 2: + tz_format = args[1] + + if tz_name: + return args[0].replace(tzinfo=ZoneInfo(tz_name)).strftime(tz_format) + + return args[0].strftime(tz_format) + + +class RuntimeDay(RuntimeFormulaFunction): + type = "day" + + args = [DateTimeBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].day + + +class RuntimeMonth(RuntimeFormulaFunction): + type = "month" + + args = [DateTimeBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].month + + +class RuntimeYear(RuntimeFormulaFunction): + type = "year" + + args = [DateTimeBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].year + + +class RuntimeHour(RuntimeFormulaFunction): + type = "hour" + + args = [DateTimeBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].hour + + +class RuntimeMinute(RuntimeFormulaFunction): + type = "minute" + + args = [DateTimeBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].minute + + +class RuntimeSecond(RuntimeFormulaFunction): + type = "second" + + args = [DateTimeBaserowRuntimeFormulaArgumentType()] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].second + + +class RuntimeNow(RuntimeFormulaFunction): + type = "now" + + def execute(self, context: FormulaContext, args: FormulaArgs): + return timezone.now() + + +class RuntimeToday(RuntimeFormulaFunction): + type = "today" + + def execute(self, context: FormulaContext, args: FormulaArgs): + return timezone.localdate() + + +class RuntimeGetProperty(RuntimeFormulaFunction): + type = "get_property" + + args = [ + DictBaserowRuntimeFormulaArgumentType(), + TextBaserowRuntimeFormulaArgumentType(), + ] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return args[0].get(args[1]) + + +class RuntimeRandomInt(RuntimeFormulaFunction): + type = "random_int" + + args = [ + NumberBaserowRuntimeFormulaArgumentType(), + NumberBaserowRuntimeFormulaArgumentType(), + ] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return random.randint(int(args[0]), int(args[1])) # nosec: B311 + + +class RuntimeRandomFloat(RuntimeFormulaFunction): + type = "random_float" + + args = [ + NumberBaserowRuntimeFormulaArgumentType(), + NumberBaserowRuntimeFormulaArgumentType(), + ] + + def execute(self, context: FormulaContext, args: FormulaArgs): + return random.uniform(float(args[0]), float(args[1])) # nosec: B311 + + +class RuntimeRandomBool(RuntimeFormulaFunction): + type = "random_bool" + + def execute(self, context: FormulaContext, args: FormulaArgs): + return random.choice([True, False]) # nosec: B311 + + +class RuntimeGenerateUUID(RuntimeFormulaFunction): + type = "generate_uuid" + def execute(self, context: FormulaContext, args: FormulaArgs): - return args[0] + args[1] + return str(uuid.uuid4()) diff --git a/backend/src/baserow/core/formula/validator.py b/backend/src/baserow/core/formula/validator.py index 46f55d443e..7144995a28 100644 --- a/backend/src/baserow/core/formula/validator.py +++ b/backend/src/baserow/core/formula/validator.py @@ -2,6 +2,7 @@ import re from datetime import date, datetime from decimal import Decimal +from json.decoder import JSONDecodeError from typing import Any, List, Optional, Union from django.core.exceptions import ValidationError @@ -215,3 +216,24 @@ def ensure_datetime(value: Any) -> Optional[datetime]: return FormattedDateTime(value).datetime if value is not None else None except (ValueError, TypeError) as exc: raise ValidationError("Value cannot be converted to a datetime.") from exc + + +def ensure_object(value: Any) -> Optional[dict]: + """ + Ensures that the value is a dict or can be converted to a dict. + :param value: The value to ensure as a dict. + :return: The value as a dict. + :raises ValidationError: If the value is not a valid dict or convertible to a + dict. + """ + + if isinstance(value, dict): + return value + + if isinstance(value, str): + try: + return json.loads(value) + except (TypeError, JSONDecodeError) as exc: + raise ValidationError("Value is not a JSON.") from exc + + raise ValidationError("Value cannot be converted to a dict.") diff --git a/backend/tests/baserow/core/formula/test_argument_types.py b/backend/tests/baserow/core/formula/test_argument_types.py new file mode 100644 index 0000000000..cf830bc5b2 --- /dev/null +++ b/backend/tests/baserow/core/formula/test_argument_types.py @@ -0,0 +1,255 @@ +from datetime import date, datetime + +import pytest + +from baserow.core.formula.argument_types import ( + AddableBaserowRuntimeFormulaArgumentType, + DateTimeBaserowRuntimeFormulaArgumentType, + DictBaserowRuntimeFormulaArgumentType, + NumberBaserowRuntimeFormulaArgumentType, + SubtractableBaserowRuntimeFormulaArgumentType, + TextBaserowRuntimeFormulaArgumentType, +) + + +@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), + ("foo", False), + (" 1 ", False), + ("", False), + (None, False), + ], +) +def test_number_test_method(value, expected): + assert NumberBaserowRuntimeFormulaArgumentType().test(value) is expected + + +@pytest.mark.parametrize( + "value,expected", + [ + (-5, -5), + ("-5", -5), + (0, 0), + ("0", 0), + (15.6, 15.6), + ("15.6", 15.6), + ], +) +def test_number_parse_method(value, expected): + assert NumberBaserowRuntimeFormulaArgumentType().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), + ([], True), + (None, True), + ], +) +def test_text_test_method(value, expected): + assert TextBaserowRuntimeFormulaArgumentType().test(value) is expected + + +@pytest.mark.parametrize( + "value,expected", + [ + (-5, "-5"), + ("-5", "-5"), + (0, "0"), + ("0", "0"), + (15.6, "15.6"), + ("15.6", "15.6"), + ({"foo": "bar"}, '{"foo": "bar"}'), + (["a", "b"], "a,b"), + (None, ""), + ], +) +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", + [ + ("2025-12-31", True), + ("2025-12-31 14:22", True), + ("2025-12-31 14:22:11", True), + (date.today(), True), + (datetime.now(), True), + # None just returns True + (None, True), + # Invalid date format + ("31-12-2025 14:22:11", False), + (-5, False), + (-5.5, False), + (0, False), + (10, False), + (16.25, False), + ("", False), + ("-5.5", False), + ("10", False), + ("16.25", False), + ([], False), + ({}, False), + ], +) +def test_datetime_test_method(value, expected): + assert DateTimeBaserowRuntimeFormulaArgumentType().test(value) == expected + + +@pytest.mark.parametrize( + "value,expected", + [ + # A date is converted to datetime with hour/minute/second to 0 + ( + "2025-12-31", + datetime(year=2025, month=12, day=31, hour=0, minute=0, second=0), + ), + ( + "2025-12-31 14:22", + datetime(year=2025, month=12, day=31, hour=14, minute=22, second=0), + ), + ( + "2025-12-31 14:22:11", + datetime(year=2025, month=12, day=31, hour=14, minute=22, second=11), + ), + ( + date(year=2025, month=10, day=22), + datetime(year=2025, month=10, day=22, hour=0, minute=0, second=0), + ), + ( + datetime(year=2025, month=10, day=22, hour=1, minute=2, second=3), + datetime(year=2025, month=10, day=22, hour=1, minute=2, second=3), + ), + (None, None), + ], +) +def test_datetime_parse_method(value, expected): + assert DateTimeBaserowRuntimeFormulaArgumentType().parse(value) == expected + + +@pytest.mark.parametrize( + "value,expected", + [ + ({}, True), + ("{}", True), + ('{"foo": "bar"}', True), + # Invalid JSON due to single quotes + ("{'foo': 'bar'}", False), + ("", False), + ([], False), + (0, False), + (100, False), + ("foo", False), + (None, False), + ], +) +def test_dict_test_method(value, expected): + assert DictBaserowRuntimeFormulaArgumentType().test(value) == expected + + +@pytest.mark.parametrize( + "value,expected", + [ + ({}, {}), + ('{"foo": "bar"}', {"foo": "bar"}), + ({"foo": "bar"}, {"foo": "bar"}), + ], +) +def test_dict_parse_method(value, expected): + assert DictBaserowRuntimeFormulaArgumentType().parse(value) == expected diff --git a/backend/tests/baserow/core/test_core_formula_runtime_formula_types.py b/backend/tests/baserow/core/formula/test_runtime_formula_types.py similarity index 100% rename from backend/tests/baserow/core/test_core_formula_runtime_formula_types.py rename to backend/tests/baserow/core/formula/test_runtime_formula_types.py diff --git a/web-frontend/locales/en.json b/web-frontend/locales/en.json index ff06422aca..4a52b13a26 100644 --- a/web-frontend/locales/en.json +++ b/web-frontend/locales/en.json @@ -639,5 +639,37 @@ }, "assistantMessageSources": { "sources": "{count} source | {count} sources" + }, + "runtimeFormulaTypes": { + "concatDescription": "Joined arguments together as a single text.", + "getDescription": "Returns the resolved value of a Baserow formula.", + "addDescription": "Adds two arguments together.", + "minusDescription": "Subtracts the second argument from the first.", + "multiplyDescription": "Multiplies the two arguments.", + "divideDescription": "Divides the first argument from the second.", + "equalDescription": "Checks if both arguments are equal.", + "notEqualDescription": "Checks if both arguments are not equal.", + "greaterThanDescription": "Checks if the first argument is greater than the second.", + "lessThanDescription": "Checks if the first argument is less than the second.", + "greaterThanOrEqualDescription": "Checks if the first argument is greater than or equal to the second.", + "upperDescription": "Converts the argument to upper case.", + "lowerDescription": "Converts the argument to lower case.", + "capitalizeDescription": "Capitalizes the argument's first letter.", + "roundDescription": "Rounds the first argument to the number of decimal places specified by the second argument.", + "evenDescription": "Return true if the argument is even, false otherwise.", + "oddDescription": "Return true if the argument is odd, false otherwise.", + "dateTimeDescription": "Formats the date time argument using the format and timezone arguments.", + "dayDescription": "Returns the day from the date time argument.", + "monthDescription": "Returns the month from the date time argument.", + "yearDescription": "Returns the year from the date time argument.", + "hourDescription": "Returns the hour from the date time argument.", + "minuteDescription": "Returns the minute from the date time argument.", + "secondDescription": "Returns the second from the date time argument.", + "todayDescription": "Returns the current date.", + "getPropertyDescription": "Returns the property from the object.", + "randomIntDescription": "Returns a random integer from the range specified by the arguments.", + "randomFloatDescription": "Returns a random float from the range specified by the arguments.", + "randomBoolDescription": "Returns a random boolean of true or false.", + "generateUUIDDescription": "Returns a random UUID4 string." } } diff --git a/web-frontend/modules/automation/components/AutomationBuilderFormulaInput.vue b/web-frontend/modules/automation/components/AutomationBuilderFormulaInput.vue index ad0837c92c..65ac6297a6 100644 --- a/web-frontend/modules/automation/components/AutomationBuilderFormulaInput.vue +++ b/web-frontend/modules/automation/components/AutomationBuilderFormulaInput.vue @@ -5,11 +5,15 @@ :value="formulaStr" :data-providers="dataProviders" :application-context="applicationContext" + enable-advanced-mode + :mode="currentMode" @input="updatedFormulaStr" + @mode-changed="updateMode" /> diff --git a/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue b/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue index 0cbb5eed74..a46ce174d2 100644 --- a/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue +++ b/web-frontend/modules/builder/components/ApplicationBuilderFormulaInput.vue @@ -2,11 +2,14 @@ @@ -51,6 +54,9 @@ export default { formulaStr() { return this.value.formula }, + formulaMode() { + return this.value.mode + }, dataSourceLoading() { return this.$store.getters['dataSource/getLoading'](this.elementPage) }, @@ -91,6 +97,9 @@ export default { updatedFormulaStr(newFormulaStr) { this.$emit('input', { ...this.value, formula: newFormulaStr }) }, + updateMode(newMode) { + this.$emit('input', { ...this.value, mode: newMode }) + }, }, } diff --git a/web-frontend/modules/core/assets/scss/components/form.scss b/web-frontend/modules/core/assets/scss/components/form.scss index e12ce36b07..76b561fdb6 100644 --- a/web-frontend/modules/core/assets/scss/components/form.scss +++ b/web-frontend/modules/core/assets/scss/components/form.scss @@ -33,7 +33,7 @@ .control__elements--flex { display: flex; - align-items: center; + align-items: flex-start; gap: 5px; } diff --git a/web-frontend/modules/core/assets/scss/components/formula_input_field.scss b/web-frontend/modules/core/assets/scss/components/formula_input_field.scss index 9b1a8c72d9..794e80ec33 100644 --- a/web-frontend/modules/core/assets/scss/components/formula_input_field.scss +++ b/web-frontend/modules/core/assets/scss/components/formula_input_field.scss @@ -39,3 +39,13 @@ pointer-events: none; height: 0; } + +.formula-input-field__advanced-input { + width: 100%; + padding: 12px 16px; + line-height: 100%; + + &::placeholder { + color: $color-neutral-500; + } +} diff --git a/web-frontend/modules/core/components/formula/FormulaInputField.vue b/web-frontend/modules/core/components/formula/FormulaInputField.vue index 143e3adb24..8abb09db6a 100644 --- a/web-frontend/modules/core/components/formula/FormulaInputField.vue +++ b/web-frontend/modules/core/components/formula/FormulaInputField.vue @@ -1,5 +1,5 @@