From cd09129be2d2a1e6d302191357e191f11264e650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Pardou?= <571533+jrmi@users.noreply.github.com> Date: Mon, 25 May 2026 09:00:25 +0200 Subject: [PATCH 1/2] fix: form input/Meta field name/missing mapping (sentry bugs) (#5361) --- backend/src/baserow/api/utils.py | 17 +++++-- .../contrib/automation/application_types.py | 1 + .../contrib/automation/nodes/registries.py | 39 +++++++++++++++- .../contrib/automation/nodes/service.py | 6 ++- .../contrib/database/fields/field_types.py | 10 ++++- backend/tests/baserow/api/test_api_utils.py | 20 +++++++++ .../automation/nodes/test_node_handler.py | 20 +++++++++ .../automation/nodes/test_node_service.py | 45 +++++++++++++++++++ .../test_automation_application_types.py | 22 +++++++++ .../database/api/rows/test_row_serializers.py | 22 +++++++++ ...ng_into_inputs_that_unmount_during_an.json | 9 ++++ .../theme/ImageThemeConfigBlock.vue | 2 +- .../modules/core/components/FormInput.vue | 4 +- 13 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 changelog/entries/unreleased/bug/fixes_a_crash_when_typing_into_inputs_that_unmount_during_an.json diff --git a/backend/src/baserow/api/utils.py b/backend/src/baserow/api/utils.py index 7c05ea963a..ea32596a9e 100644 --- a/backend/src/baserow/api/utils.py +++ b/backend/src/baserow/api/utils.py @@ -389,9 +389,6 @@ class Meta(extends_meta): attrs = {"Meta": Meta} - if field_overrides: - attrs.update(field_overrides) - def validate(self, value): if required_fields: for field_name in required_fields: @@ -404,7 +401,7 @@ def validate(self, value): attrs["validate"] = validate mixins = base_mixins or [] - return type( + serializer_class = type( str(model_.__name__ + "Serializer"), ( *mixins, @@ -413,6 +410,18 @@ def validate(self, value): attrs, ) + if field_overrides: + # User-facing field names are valid serializer field names, but some of + # them, such as "Meta" or "validate", collide with DRF class internals. + # Register declared fields directly so those names can be serialized + # without replacing the serializer configuration or methods. + serializer_class._declared_fields = { + **serializer_class._declared_fields, + **field_overrides, + } + + return serializer_class + class MappingSerializer: """ diff --git a/backend/src/baserow/contrib/automation/application_types.py b/backend/src/baserow/contrib/automation/application_types.py index a4c9c8ea34..630ff37192 100644 --- a/backend/src/baserow/contrib/automation/application_types.py +++ b/backend/src/baserow/contrib/automation/application_types.py @@ -216,6 +216,7 @@ def import_serialized( progress.increment( state=IMPORT_SERIALIZED_IMPORTING, by=integration_progress ) + id_mapping.setdefault("integrations", {}) # Just in case else: self.import_integrations_serialized( automation, diff --git a/backend/src/baserow/contrib/automation/nodes/registries.py b/backend/src/baserow/contrib/automation/nodes/registries.py index 75152efe6b..27b6865dbc 100644 --- a/backend/src/baserow/contrib/automation/nodes/registries.py +++ b/backend/src/baserow/contrib/automation/nodes/registries.py @@ -5,7 +5,10 @@ from baserow.contrib.automation.automation_dispatch_context import ( AutomationDispatchContext, ) -from baserow.contrib.automation.nodes.exceptions import AutomationNodeNotReplaceable +from baserow.contrib.automation.nodes.exceptions import ( + AutomationNodeMisconfiguredService, + AutomationNodeNotReplaceable, +) from baserow.contrib.automation.nodes.models import AutomationNode from baserow.contrib.automation.nodes.types import AutomationNodeDict, NodePositionType from baserow.contrib.automation.workflows.models import AutomationWorkflow @@ -194,6 +197,12 @@ def deserialize_property( integration_id, integration_id ) integration = Integration.objects.get(id=integration_id) + workflow = kwargs.get("workflow") + if ( + workflow is not None + and integration.application_id != workflow.automation_id + ): + integration = None return ServiceHandler().import_service( integration, @@ -229,9 +238,32 @@ def import_serialized( parent, serialized_values, id_mapping, + workflow=parent, **kwargs, ) + def _validate_service_integration_belongs_to_workflow( + self, + workflow: Optional[AutomationWorkflow], + service_values: Dict[str, Any], + ) -> None: + if not workflow or "integration_id" not in service_values: + return + + integration_id = service_values["integration_id"] + if integration_id is None: + return + + integration = Integration.objects.filter(id=integration_id).first() + if integration is None: + return + + if integration.application_id != workflow.automation_id: + raise AutomationNodeMisconfiguredService( + f"The integration with ID {integration_id} is not related to the " + f"automation {workflow.automation_id}." + ) + def prepare_values( self, values: Dict[str, Any], @@ -263,6 +295,11 @@ def prepare_values( # If we received any service values, prepare them. service_values = values.pop("service", None) or {} + workflow = instance.workflow if instance else values.get("workflow", None) + self._validate_service_integration_belongs_to_workflow( + workflow, + service_values, + ) prepared_service_values = service_type.prepare_values( service_values, user, service if instance else None ) diff --git a/backend/src/baserow/contrib/automation/nodes/service.py b/backend/src/baserow/contrib/automation/nodes/service.py index 013344d9ba..a509b26fa7 100644 --- a/backend/src/baserow/contrib/automation/nodes/service.py +++ b/backend/src/baserow/contrib/automation/nodes/service.py @@ -174,7 +174,10 @@ def create_node( node_type.before_create(workflow, reference_node, position, output) - prepared_values = node_type.prepare_values(kwargs, user) + prepared_values = node_type.prepare_values( + {**kwargs, "workflow": workflow}, + user, + ) # Preselect first integration if exactly one exists if node_type.get_service_type().integration_type: @@ -190,7 +193,6 @@ def create_node( new_node = self.handler.create_node( node_type, - workflow=workflow, **prepared_values, ) diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py index 302bc1ff44..ac7cbc8cd7 100755 --- a/backend/src/baserow/contrib/database/fields/field_types.py +++ b/backend/src/baserow/contrib/database/fields/field_types.py @@ -53,6 +53,7 @@ from dateutil import parser from dateutil.parser import ParserError +from drf_spectacular.utils import extend_schema_serializer from loguru import logger from rest_framework import serializers @@ -598,8 +599,7 @@ class NumberFieldType(FieldType): "The number_type option has been removed and can no longer be provided. " "Instead set number_decimal_places to 0 for an integer or 1-5 for a " "decimal." - ), - "_spectacular_annotation": {"exclude_fields": ["number_type"]}, + ) } _can_group_by = True _db_column_fields = ["number_decimal_places"] @@ -616,6 +616,12 @@ def serialize_allowed_fields(self, field: Field) -> Dict[str, Any]: serialized[field_name] = value return serialized + def get_serializer_class(self, *args, **kwargs) -> serializers.ModelSerializer: + serializer_class = super().get_serializer_class(*args, **kwargs) + return extend_schema_serializer(exclude_fields=["number_type"])( + serializer_class + ) + def serialize_to_input_value(self, field: Field, value: any) -> any: if field.specific.number_decimal_places == 0: return int(value) diff --git a/backend/tests/baserow/api/test_api_utils.py b/backend/tests/baserow/api/test_api_utils.py index 52e1281eac..e38fa87ddf 100644 --- a/backend/tests/baserow/api/test_api_utils.py +++ b/backend/tests/baserow/api/test_api_utils.py @@ -391,6 +391,26 @@ def test_get_serializer_class(data_fixture): } +@pytest.mark.django_db +def test_get_serializer_class_with_fields_named_like_serializer_internals(data_fixture): + workspace = data_fixture.create_workspace(name="Workspace 1") + + workspace_serializer = get_serializer_class( + Workspace, + ["Meta", "validate"], + { + "Meta": CharField(source="name"), + "validate": CharField(source="name"), + }, + )(workspace) + + assert workspace_serializer.data == { + "Meta": "Workspace 1", + "validate": "Workspace 1", + } + assert workspace_serializer.__class__.Meta.model == Workspace + + @override_settings(DEBUG=False) def test_api_error_if_url_trailing_slash_is_missing(api_client): invalid_url = "/api/invalid-url" diff --git a/backend/tests/baserow/contrib/automation/nodes/test_node_handler.py b/backend/tests/baserow/contrib/automation/nodes/test_node_handler.py index 6cdb312151..01a4ac5550 100644 --- a/backend/tests/baserow/contrib/automation/nodes/test_node_handler.py +++ b/backend/tests/baserow/contrib/automation/nodes/test_node_handler.py @@ -204,3 +204,23 @@ def test_import_node_only(data_fixture): "automation_workflow_nodes": {node.id: new_node.id}, "services": {node.service_id: new_node.service_id}, } + + +@pytest.mark.django_db +def test_import_node_only_ignores_integration_from_another_application(data_fixture): + workflow = data_fixture.create_automation_workflow() + trigger = workflow.get_trigger() + other_integration = data_fixture.create_local_baserow_integration() + + exported_node = AutomationNodeHandler().export_node(trigger) + exported_node["service"]["integration_id"] = other_integration.id + id_mapping = { + "integrations": {}, + "automation_workflow_nodes": MirrorDict(), + } + + imported_node = AutomationNodeHandler().import_node_only( + workflow, exported_node, id_mapping + ) + + assert imported_node.service.specific.integration_id is None diff --git a/backend/tests/baserow/contrib/automation/nodes/test_node_service.py b/backend/tests/baserow/contrib/automation/nodes/test_node_service.py index 52f01b6046..4d990cb430 100644 --- a/backend/tests/baserow/contrib/automation/nodes/test_node_service.py +++ b/backend/tests/baserow/contrib/automation/nodes/test_node_service.py @@ -4,6 +4,7 @@ from baserow.contrib.automation.nodes.exceptions import ( AutomationNodeDoesNotExist, + AutomationNodeMisconfiguredService, AutomationNodeNotMovable, AutomationNodeReferenceNodeInvalid, ) @@ -144,6 +145,30 @@ def test_create_node_permission_error(data_fixture: Fixtures): ) +@pytest.mark.django_db +def test_create_node_rejects_integration_from_another_application(data_fixture): + user = data_fixture.create_user() + workflow = data_fixture.create_automation_workflow(user) + other_integration = data_fixture.create_local_baserow_integration(user=user) + + with pytest.raises(AutomationNodeMisconfiguredService) as exc: + AutomationNodeService().create_node( + user, + automation_node_type_registry.get("local_baserow_create_row"), + workflow, + reference_node_id=workflow.get_trigger().id, + position="south", + output="", + service={"integration_id": other_integration.id}, + ) + + assert ( + str(exc.value) + == f"The integration with ID {other_integration.id} is not related to the " + f"automation {workflow.automation_id}." + ) + + @pytest.mark.django_db def test_get_node(data_fixture: Fixtures): user = data_fixture.create_user() @@ -779,6 +804,26 @@ def test_update_node_updates_workflow_dirty_cache(data_fixture): assert global_cache.get(cache_key, default=False) is True +@pytest.mark.django_db +def test_update_node_rejects_integration_from_another_application(data_fixture): + user = data_fixture.create_user() + node = data_fixture.create_local_baserow_create_row_action_node(user=user) + other_integration = data_fixture.create_local_baserow_integration(user=user) + + with pytest.raises(AutomationNodeMisconfiguredService) as exc: + AutomationNodeService().update_node( + user, + node.id, + service={"integration_id": other_integration.id}, + ) + + assert ( + str(exc.value) + == f"The integration with ID {other_integration.id} is not related to the " + f"automation {node.workflow.automation_id}." + ) + + @pytest.mark.django_db def test_create_node_updates_workflow_dirty_cache(data_fixture): """ diff --git a/backend/tests/baserow/contrib/automation/test_automation_application_types.py b/backend/tests/baserow/contrib/automation/test_automation_application_types.py index c9b7674d62..881adac349 100644 --- a/backend/tests/baserow/contrib/automation/test_automation_application_types.py +++ b/backend/tests/baserow/contrib/automation/test_automation_application_types.py @@ -228,6 +228,28 @@ def test_automation_application_import(data_fixture): ) +@pytest.mark.django_db +def test_automation_application_import_initializes_integrations_mapping(data_fixture): + workspace = data_fixture.create_workspace() + id_mapping = {} + + AutomationApplicationType().import_serialized( + workspace, + { + "id": 1, + "name": "Sample automation", + "order": 1, + "type": "automation", + "integrations": [], + "workflows": [], + }, + ImportExportConfig(include_permission_data=True), + id_mapping, + ) + + assert id_mapping["integrations"] == {} + + @pytest.mark.django_db def test_fetch_workflows_to_serialize_without_user(data_fixture): workflow = data_fixture.create_automation_workflow(name="test") diff --git a/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py b/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py index f1be74a1ba..269ce51ba8 100644 --- a/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py +++ b/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py @@ -578,6 +578,28 @@ def test_get_row_serializer_with_user_field_names( assert test_result == expected_result +@pytest.mark.django_db +def test_get_row_serializer_with_user_field_names_named_meta(data_fixture): + table = data_fixture.create_database_table(name="Cars") + text_field = data_fixture.create_text_field(table=table, order=0, name="Meta") + + model = table.get_model() + row = model.objects.create(**{f"field_{text_field.id}": "Test value"}) + + serialized_row = get_row_serializer_class( + model, + RowSerializer, + is_response=True, + user_field_names=True, + )(row).data + + assert serialized_row == { + "id": 1, + "order": "1.00000000000000000000", + "Meta": "Test value", + } + + @pytest.mark.django_db def test_remap_serialized_row_to_user_field_names(data_fixture): user = data_fixture.create_user() diff --git a/changelog/entries/unreleased/bug/fixes_a_crash_when_typing_into_inputs_that_unmount_during_an.json b/changelog/entries/unreleased/bug/fixes_a_crash_when_typing_into_inputs_that_unmount_during_an.json new file mode 100644 index 0000000000..ac88084a02 --- /dev/null +++ b/changelog/entries/unreleased/bug/fixes_a_crash_when_typing_into_inputs_that_unmount_during_an.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixes a crash when typing into inputs that unmount during an event", + "issue_origin": "github", + "issue_number": null, + "domain": "core", + "bullet_points": [], + "created_at": "2026-05-13" +} \ No newline at end of file diff --git a/web-frontend/modules/builder/components/theme/ImageThemeConfigBlock.vue b/web-frontend/modules/builder/components/theme/ImageThemeConfigBlock.vue index 235cb0f193..6b4f6fd201 100644 --- a/web-frontend/modules/builder/components/theme/ImageThemeConfigBlock.vue +++ b/web-frontend/modules/builder/components/theme/ImageThemeConfigBlock.vue @@ -104,7 +104,7 @@ :disabled="constraintDisabled(name)" :description=" constraintDisabled(name) - ? $t(`imageThemeConfigBlock.imageConstraint${label}Disabled`) + ? $t(`imageThemeConfigBlock.imageConstraint${name}Disabled`) : '' " :name="label" diff --git a/web-frontend/modules/core/components/FormInput.vue b/web-frontend/modules/core/components/FormInput.vue index 62116fa1ef..31de4baf31 100644 --- a/web-frontend/modules/core/components/FormInput.vue +++ b/web-frontend/modules/core/components/FormInput.vue @@ -138,11 +138,11 @@ function updateValue(raw) { } function onInput(e) { - const raw = input.value.value + const raw = e.target.value if (!raw && props.defaultValueWhenEmpty !== null) return - updateValue(e.target.value) + updateValue(raw) } function onBlur(e) { From fb61bb788358d96b08d184668b3b1bc0877dd741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 09:29:51 +0200 Subject: [PATCH 2/2] chore(deps): bump js-cookie from 3.0.5 to 3.0.7 in /web-frontend (#5409) Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/js-cookie/js-cookie/releases) - [Commits](https://github.com/js-cookie/js-cookie/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: js-cookie dependency-version: 3.0.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web-frontend/yarn.lock | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/web-frontend/yarn.lock b/web-frontend/yarn.lock index 36a9b2d2b6..ed15e1450a 100644 --- a/web-frontend/yarn.lock +++ b/web-frontend/yarn.lock @@ -9147,9 +9147,9 @@ js-beautify@^1.14.9: nopt "^7.2.1" js-cookie@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" - integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + version "3.0.7" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.7.tgz#0a53abfc459c8e89c85d7a38eb6cb68714965b8c" + integrity sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw== js-sha256@^0.11.1: version "0.11.1" @@ -12362,7 +12362,16 @@ string-argv@~0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12403,7 +12412,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13733,7 +13749,16 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==