diff --git a/backend/src/baserow/contrib/builder/elements/element_types.py b/backend/src/baserow/contrib/builder/elements/element_types.py index b39e68d3c6..db9fdabc36 100644 --- a/backend/src/baserow/contrib/builder/elements/element_types.py +++ b/backend/src/baserow/contrib/builder/elements/element_types.py @@ -1519,13 +1519,13 @@ def is_valid( :return: Whether the value is valid or not for this element. """ - if value == "": + if value == "" or value is None: if element.required: raise ValueError("The value is required") elif element.validation_type == "integer": try: - return ensure_numeric(value) + return ensure_numeric(value, True) except (InvalidOperation, ValidationError) as exc: raise TypeError(f"{value} is not a valid number") from exc diff --git a/backend/src/baserow/contrib/builder/handler.py b/backend/src/baserow/contrib/builder/handler.py index 7dcb216c05..ad59db0c84 100644 --- a/backend/src/baserow/contrib/builder/handler.py +++ b/backend/src/baserow/contrib/builder/handler.py @@ -1,6 +1,7 @@ -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from django.conf import settings +from django.contrib.auth import get_user_model from django.db.models.query import QuerySet from baserow.contrib.builder.formula_property_extractor import ( @@ -25,6 +26,8 @@ SENTINEL = "__no_results__" +User = get_user_model() + class BuilderHandler: def get_builder(self, builder_id: int) -> Builder: @@ -56,14 +59,14 @@ def _get_builder_public_properties_version_cache(cls, builder: Builder) -> str: return f"{USED_PROPERTIES_CACHE_KEY_PREFIX}_version_{builder.id}" def get_builder_used_properties_cache_key( - self, user: UserSourceUser, builder: Builder + self, user: Union[User, UserSourceUser], builder: Builder ) -> str: """ Returns a cache key that can be used to key the results of making the expensive function call to get_builder_used_property_names(). """ - if user.is_anonymous or not user.role: + if user.is_anonymous or not hasattr(user, "role"): # When the user is anonymous, only use the prefix + page ID. role = "" else: diff --git a/backend/src/baserow/contrib/database/rows/data_providers.py b/backend/src/baserow/contrib/database/rows/data_providers.py index 43f961574e..6fb6d6b5f4 100644 --- a/backend/src/baserow/contrib/database/rows/data_providers.py +++ b/backend/src/baserow/contrib/database/rows/data_providers.py @@ -1,14 +1,14 @@ from typing import List, Union -from baserow.contrib.builder.data_sources.builder_dispatch_context import ( - BuilderDispatchContext, +from baserow.contrib.database.rows.runtime_formula_contexts import ( + HumanReadableRowContext, ) from baserow.core.formula.registries import DataProviderType class HumanReadableFieldsDataProviderType(DataProviderType): """ - This data provider type is used to read the human readable values for the row + This data provider type is used to read the human-readable values for the row fields. This is used for example in the AI field to be able to reference other fields in the same row to generate a different prompt for each row based on the values of the other fields. @@ -17,8 +17,8 @@ class HumanReadableFieldsDataProviderType(DataProviderType): type = "fields" def get_data_chunk( - self, dispatch_context: BuilderDispatchContext, path: List[str] - ) -> Union[int, str]: + self, dispatch_context: HumanReadableRowContext, path: List[str] + ) -> Union[int, str] | None: """ When a page parameter is read, returns the value previously saved from the request object. diff --git a/backend/tests/baserow/contrib/builder/elements/test_element_types.py b/backend/tests/baserow/contrib/builder/elements/test_element_types.py index 69582e5e42..5b483f34bb 100644 --- a/backend/tests/baserow/contrib/builder/elements/test_element_types.py +++ b/backend/tests/baserow/contrib/builder/elements/test_element_types.py @@ -524,6 +524,7 @@ def test_choice_element_import_export_formula(data_fixture): (True, "integer", "42", 42), (True, "integer", "horse", TypeError), (False, "integer", "", ""), + (False, "integer", None, None), (True, "email", "", ValueError), (True, "email", "foo@bar.com", "foo@bar.com"), (True, "email", "foobar.com", ValueError), diff --git a/backend/tests/baserow/contrib/builder/test_builder_handler.py b/backend/tests/baserow/contrib/builder/test_builder_handler.py index 4e3ff9e982..8d51678bee 100644 --- a/backend/tests/baserow/contrib/builder/test_builder_handler.py +++ b/backend/tests/baserow/contrib/builder/test_builder_handler.py @@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model import pytest +from faker import Faker from baserow.contrib.builder.handler import ( USED_PROPERTIES_CACHE_KEY_PREFIX, @@ -11,9 +12,18 @@ from baserow.core.exceptions import ApplicationDoesNotExist from baserow.core.user_sources.user_source_user import UserSourceUser +fake = Faker() User = get_user_model() +def fake_user_source_user(role: str = "") -> UserSourceUser: + user_source = MagicMock() + original_user = MagicMock() + return UserSourceUser( + user_source, original_user, 123, fake.name(), fake.email(), role=role + ) + + @pytest.mark.django_db def test_get_builder(data_fixture): builder = data_fixture.create_builder_application() @@ -44,49 +54,38 @@ def test_get_builder_select_related_theme_config( @pytest.mark.parametrize( - "is_anonymous,user_role,expected_cache_key", + "user,expected_cache_key", [ + # An anonymous User. ( - True, - "", + MagicMock(is_anonymous=True, spec=["is_anonymous"]), f"{USED_PROPERTIES_CACHE_KEY_PREFIX}_100", ), + # An authenticated User ( - True, - "foo_role", + MagicMock(is_anonymous=False, spec=["is_anonymous"]), f"{USED_PROPERTIES_CACHE_KEY_PREFIX}_100", ), + # A UserSourceUser, with no role. ( - False, - "foo_role", - f"{USED_PROPERTIES_CACHE_KEY_PREFIX}_100_foo_role", + fake_user_source_user(role=""), + f"{USED_PROPERTIES_CACHE_KEY_PREFIX}_100_", + ), + # A UserSourceUser, with a role. + ( + fake_user_source_user(role="admin"), + f"{USED_PROPERTIES_CACHE_KEY_PREFIX}_100_admin", ), ], ) def test_get_builder_used_properties_cache_key_returned_expected_cache_key( - is_anonymous, user_role, expected_cache_key + user, expected_cache_key ): - """ - Test the BuilderHandler::get_builder_used_properties_cache_key() method. - - Ensure the expected cache key is returned. - """ - - user_source_user = MagicMock() - user_source_user.is_anonymous = is_anonymous - user_source_user.role = user_role - - mock_builder = MagicMock() - mock_builder.id = 100 - - handler = BuilderHandler() - - cache_key = handler.get_builder_used_properties_cache_key( - user_source_user, mock_builder + assert ( + BuilderHandler().get_builder_used_properties_cache_key(user, MagicMock(id=100)) + == expected_cache_key ) - assert cache_key == expected_cache_key - @pytest.mark.django_db def test_public_allowed_properties_is_cached(data_fixture, django_assert_num_queries): diff --git a/changelog/entries/unreleased/bug/4860_resolved_a_bug_which_prevented_builder_data_source_and_workf.json b/changelog/entries/unreleased/bug/4860_resolved_a_bug_which_prevented_builder_data_source_and_workf.json new file mode 100644 index 0000000000..3e7c34a06d --- /dev/null +++ b/changelog/entries/unreleased/bug/4860_resolved_a_bug_which_prevented_builder_data_source_and_workf.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Resolved a bug which prevented builder data source and workflow action filtering with formula values from working correctly.", + "issue_origin": "github", + "issue_number": 4860, + "domain": "integration", + "bullet_points": [], + "created_at": "2026-02-24" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/bug/4862_resolved_a_bug_which_caused_table_and_repeat_element_load_mo.json b/changelog/entries/unreleased/bug/4862_resolved_a_bug_which_caused_table_and_repeat_element_load_mo.json new file mode 100644 index 0000000000..43f498c74a --- /dev/null +++ b/changelog/entries/unreleased/bug/4862_resolved_a_bug_which_caused_table_and_repeat_element_load_mo.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Resolved a bug which caused table and repeat element load more buttons to only load more after a second click.", + "issue_origin": "github", + "issue_number": 4862, + "domain": "builder", + "bullet_points": [], + "created_at": "2026-02-25" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/bug/resolved_a_caching_issue_when_applications_users_had_no_role.json b/changelog/entries/unreleased/bug/resolved_a_caching_issue_when_applications_users_had_no_role.json new file mode 100644 index 0000000000..8217ce2bdc --- /dev/null +++ b/changelog/entries/unreleased/bug/resolved_a_caching_issue_when_applications_users_had_no_role.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Resolved a caching issue when applications users had no role set.", + "issue_origin": "github", + "issue_number": null, + "domain": "builder", + "bullet_points": [], + "created_at": "2026-02-25" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/bug/resolved_a_data_input_bug_which_prevented_optional_numeric_i.json b/changelog/entries/unreleased/bug/resolved_a_data_input_bug_which_prevented_optional_numeric_i.json new file mode 100644 index 0000000000..4f1bdb7248 --- /dev/null +++ b/changelog/entries/unreleased/bug/resolved_a_data_input_bug_which_prevented_optional_numeric_i.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Resolved a data input bug which prevented optional numeric inputs from being accepted.", + "issue_origin": "github", + "issue_number": null, + "domain": "builder", + "bullet_points": [], + "created_at": "2026-02-25" +} \ No newline at end of file diff --git a/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue b/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue index 4f7a0fd6d3..3a1c647873 100644 --- a/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue +++ b/web-frontend/modules/builder/components/dataSource/DataSourceCreateEditModal.vue @@ -88,7 +88,7 @@ export default { computed: { submitIsDisabled() { return ( - this.loading || !this.changed || this.$refs.dataSourceForm.v$.$anyError + this.loading || !this.changed || this.$refs.dataSourceForm?.v$.$anyError ) }, dataSources() { diff --git a/web-frontend/modules/builder/components/elements/baseComponents/ABDateTimePicker.vue b/web-frontend/modules/builder/components/elements/baseComponents/ABDateTimePicker.vue index bdd7b3f7f1..079176bb1b 100644 --- a/web-frontend/modules/builder/components/elements/baseComponents/ABDateTimePicker.vue +++ b/web-frontend/modules/builder/components/elements/baseComponents/ABDateTimePicker.vue @@ -111,6 +111,9 @@ export default { }, }, emits: ['update:modelValue'], + setup() { + return useDatePickerLanguage() + }, data() { return { dateInputValue: '', @@ -138,9 +141,6 @@ export default { immediate: true, }, }, - setup() { - return useDatePickerLanguage() - }, methods: { refreshDate(value) { if (!value) { diff --git a/web-frontend/modules/builder/components/elements/components/TableElement.vue b/web-frontend/modules/builder/components/elements/components/TableElement.vue index 54bc3e5f71..e9a7bca27f 100644 --- a/web-frontend/modules/builder/components/elements/components/TableElement.vue +++ b/web-frontend/modules/builder/components/elements/components/TableElement.vue @@ -83,7 +83,7 @@ export default { * @property {Object} fields - The fields of the data source. * @property {int} items_per_page - The number of items per page. * @property {string} button_color - The color of the button. - * @property {string} orientation - The orientation for eaceh device. + * @property {string} orientation - The orientation for each device. */ element: { type: Object, diff --git a/web-frontend/modules/builder/composables/useCollectionElement.js b/web-frontend/modules/builder/composables/useCollectionElement.js index 1e5dacf21d..562ef22d27 100644 --- a/web-frontend/modules/builder/composables/useCollectionElement.js +++ b/web-frontend/modules/builder/composables/useCollectionElement.js @@ -28,7 +28,7 @@ export function useCollectionElement(props) { const adhocFilters = ref() const adhocSortings = ref() const adhocSearch = ref() - const currentOffset = ref(0) + const currentOffset = useState(`element-offset-${unref(element).id}`, () => 0) const errorNotified = ref(false) const resetTimeout = ref(null) const contentFetchEnabled = ref(true) diff --git a/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowTableServiceConditionalForm.vue b/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowTableServiceConditionalForm.vue index 850b4da7f0..c82b86642c 100644 --- a/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowTableServiceConditionalForm.vue +++ b/web-frontend/modules/integrations/localBaserow/components/services/LocalBaserowTableServiceConditionalForm.vue @@ -32,7 +32,7 @@ >