diff --git a/backend/src/baserow/contrib/builder/elements/element_types.py b/backend/src/baserow/contrib/builder/elements/element_types.py index 537f7a01a..1d23e8791 100644 --- a/backend/src/baserow/contrib/builder/elements/element_types.py +++ b/backend/src/baserow/contrib/builder/elements/element_types.py @@ -1,4 +1,5 @@ import abc +import uuid from datetime import datetime from typing import ( Any, @@ -2380,17 +2381,31 @@ class MenuElementType(ElementType): menu_items_to_create = [] child_uids_parent_uids = {} + # Generate new uids to prevent conflicts + updated_uids = {i["uid"]: str(uuid.uuid4()) for i in menu_items} + ids_uids = {i["id"]: i["uid"] for i in menu_items} keys_to_remove = ["id", "menu_item_order", "children"] + for index, item in enumerate(menu_items): for key in keys_to_remove: item.pop(key, None) + old_uid = item.pop("uid") + new_uid = updated_uids[old_uid] + # Keep track of child-parent relationship via the uid if parent_id := item.pop("parent_menu_item", None): - child_uids_parent_uids[item["uid"]] = ids_uids[parent_id] + child_uids_parent_uids[new_uid] = updated_uids[ids_uids[parent_id]] - menu_items_to_create.append(MenuItemElement(**item, menu_item_order=index)) + # Map the old uid to the new uid. This ensures that any workflow + # actions with an `event` pointing to the old uid will have the + # pointer to the new uid. + id_mapping["builder_element_event_uids"][old_uid] = new_uid + + menu_items_to_create.append( + MenuItemElement(**item, uid=new_uid, menu_item_order=index) + ) created_menu_items = MenuItemElement.objects.bulk_create(menu_items_to_create) instance.menu_items.add(*created_menu_items) diff --git a/backend/src/baserow/contrib/builder/elements/registries.py b/backend/src/baserow/contrib/builder/elements/registries.py index 394faba8d..548843cf2 100644 --- a/backend/src/baserow/contrib/builder/elements/registries.py +++ b/backend/src/baserow/contrib/builder/elements/registries.py @@ -196,6 +196,11 @@ class ElementType( ) -> ElementSubClass: from baserow.contrib.builder.elements.handler import ElementHandler + # Add mapping for builder element event uids (for collection field or other + # elements that are using dynamic events. + if "builder_element_event_uids" not in id_mapping: + id_mapping["builder_element_event_uids"] = {} + if cache is None: cache = {} @@ -504,14 +509,10 @@ class CollectionFieldType( deserialized_uid = str(uuid.uuid4()) if "uid" in serialized_values: - # Ensure we have a mapping for the collection field uids. - if "builder_collection_fields_uids" not in id_mapping: - id_mapping["builder_collection_fields_uids"] = {} - # Map the old uid to the new uid. This ensures that any workflow # actions with an `event` pointing to the old uid will have the # pointer to the new uid. - id_mapping["builder_collection_fields_uids"][ + id_mapping["builder_element_event_uids"][ serialized_values["uid"] ] = deserialized_uid diff --git a/backend/src/baserow/contrib/builder/workflow_actions/models.py b/backend/src/baserow/contrib/builder/workflow_actions/models.py index 4fcb7d2fb..cb4dab698 100644 --- a/backend/src/baserow/contrib/builder/workflow_actions/models.py +++ b/backend/src/baserow/contrib/builder/workflow_actions/models.py @@ -1,13 +1,7 @@ -from typing import Union - from django.contrib.contenttypes.models import ContentType from django.db import models -from baserow.contrib.builder.elements.models import ( - CollectionField, - Element, - NavigationElementMixin, -) +from baserow.contrib.builder.elements.models import Element, NavigationElementMixin from baserow.contrib.builder.pages.models import Page from baserow.core.formula.field import FormulaField from baserow.core.mixins import OrderableMixin @@ -43,33 +37,14 @@ class BuilderWorkflowAction( ) @classmethod - def is_collection_field_action(cls, event: str) -> bool: + def is_dynamic_event(cls, event: str) -> bool: """ - Returns whether this workflow action is associated with a collection field. - - :return: Whether this workflow action is associated with a collection field. + :return: Whether the given event is dynamically generated. """ default_event_types = [e.value for e in EventTypes] return event and event not in default_event_types - @property - def target(self) -> Union[Element, CollectionField]: - """ - If this workflow action's `event` is in the format `{uid}_{event_type}`, then - it's associated with a collection element with fields. If that's the case, the - target is the field with the matching `uid`. Otherwise, the target is the - element itself. - - :return: The target of the workflow action. - """ - - if BuilderWorkflowAction.is_collection_field_action(self.event): - uid, event = self.event.split("_", 1) - return self.element.fields.get(uid=uid) - - return self.element - @staticmethod def get_type_registry() -> ModelRegistryMixin: from baserow.contrib.builder.workflow_actions.registries import ( diff --git a/backend/src/baserow/contrib/builder/workflow_actions/registries.py b/backend/src/baserow/contrib/builder/workflow_actions/registries.py index 321685ca9..ac0be66aa 100644 --- a/backend/src/baserow/contrib/builder/workflow_actions/registries.py +++ b/backend/src/baserow/contrib/builder/workflow_actions/registries.py @@ -61,9 +61,9 @@ class BuilderWorkflowActionType( :return: The new workflow action instance. """ - if BuilderWorkflowAction.is_collection_field_action(serialized_values["event"]): + if BuilderWorkflowAction.is_dynamic_event(serialized_values["event"]): exported_uid, exported_event = serialized_values["event"].split("_", 1) - imported_uid = id_mapping["builder_collection_fields_uids"][exported_uid] + imported_uid = id_mapping["builder_element_event_uids"][exported_uid] serialized_values["event"] = f"{imported_uid}_{exported_event}" return super().create_instance_from_serialized( diff --git a/backend/src/baserow/core/handler.py b/backend/src/baserow/core/handler.py index c8aee16a4..713795c68 100755 --- a/backend/src/baserow/core/handler.py +++ b/backend/src/baserow/core/handler.py @@ -1930,6 +1930,9 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)): return slug = ".".join(template_file_path.name.split(".")[:-1]) + + logger.info(f"Importing template {slug}") + installed_template = next( (t for t in installed_templates if t.slug == slug), None ) diff --git a/backend/src/baserow/core/management/commands/sync_templates.py b/backend/src/baserow/core/management/commands/sync_templates.py index 2943506cd..2c22ba6b4 100644 --- a/backend/src/baserow/core/management/commands/sync_templates.py +++ b/backend/src/baserow/core/management/commands/sync_templates.py @@ -35,10 +35,14 @@ class Command(BaseCommand): elif options.get("only", None): try: templates = options["only"][0] - CoreHandler().sync_templates(pattern=templates) except (KeyError, IndexError): + from loguru import logger + + logger.exception("Error while importing template") self.stdout.write( self.style.ERROR("Provide a pattern to match templates") ) + + CoreHandler().sync_templates(pattern=templates) else: CoreHandler().sync_templates() diff --git a/backend/tests/baserow/contrib/builder/elements/test_menu_element_type.py b/backend/tests/baserow/contrib/builder/elements/test_menu_element_type.py index e75fd69a1..01e705ef9 100644 --- a/backend/tests/baserow/contrib/builder/elements/test_menu_element_type.py +++ b/backend/tests/baserow/contrib/builder/elements/test_menu_element_type.py @@ -416,24 +416,68 @@ def test_import_export(menu_element_fixture, data_fixture): assert MenuElement.objects.count() == 0 assert MenuItemElement.objects.count() == 0 - assert NotificationWorkflowAction.objects.count() == 0 # After importing the Menu element the menu items should be correctly # imported as well. - id_mapping = defaultdict(lambda: MirrorDict()) + id_mapping = defaultdict(MirrorDict) menu_element_type.import_serialized(page, exported, id_mapping) menu_element = MenuElement.objects.first() # Ensure the Menu Items have been imported correctly - button_item = menu_element.menu_items.get(uid=uid_1) - assert button_item.name == "Greet" + # and uids should have been updated + button_item = menu_element.menu_items.get(name="Greet") + assert button_item.type == MenuItemElement.TYPES.BUTTON + assert button_item.uid != uid_1 - link_item = menu_element.menu_items.get(uid=uid_2) - assert link_item.name == "Link A" + link_item = menu_element.menu_items.get(name="Link A") + assert link_item.type == MenuItemElement.TYPES.LINK + assert link_item.uid != uid_2 - sublinks_item = menu_element.menu_items.get(uid=uid_3) - assert sublinks_item.name == "Sublinks" + sublinks_item = menu_element.menu_items.get(name="Sublinks") + assert sublinks_item.uid != uid_3 - sublink_a = menu_element.menu_items.get(uid=uid_4) - assert sublink_a.name == "Sublink A" + sublink_a = menu_element.menu_items.get(name="Sublink A") + assert sublink_a.uid != uid_4 + + +@pytest.mark.django_db +def test_delete_duplicated_menu_doesnt_affect_initial_element( + menu_element_fixture, data_fixture +): + menu_element = menu_element_fixture["menu_element"] + + # Create a Menu Element with Menu items. + uid_1 = uuid.uuid4() + menu_item_1 = { + "name": "Greet", + "type": MenuItemElement.TYPES.BUTTON, + "menu_item_order": 0, + "uid": uid_1, + "children": [], + } + + data = {"menu_items": [menu_item_1]} + ElementHandler().update_element(menu_element, **data) + + workflow_action = data_fixture.create_workflow_action( + NotificationWorkflowAction, + page=menu_element.page, + element=menu_element, + event=f"{uid_1}_click", + ) + + duplicated = ElementHandler().duplicate_element(menu_element) + + element = duplicated["elements"][0] + duplicated_workflow_action = duplicated["workflow_actions"][0] + + duplicated_workflow_action.event != workflow_action.event + + assert NotificationWorkflowAction.objects.count() == 2 + + ElementHandler().delete_element(element) + + # We want to make sure that deleting the clone doesn't delete the initial event + assert NotificationWorkflowAction.objects.count() == 1 + assert NotificationWorkflowAction.objects.first().event == workflow_action.event diff --git a/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_models.py b/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_models.py index 816ffda05..a7dda8304 100644 --- a/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_models.py +++ b/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_models.py @@ -1,49 +1,9 @@ -import pytest - -from baserow.contrib.builder.workflow_actions.models import ( - BuilderWorkflowAction, - EventTypes, - NotificationWorkflowAction, -) +from baserow.contrib.builder.workflow_actions.models import BuilderWorkflowAction -def test_builder_workflow_action_is_collection_field_action(): - assert not BuilderWorkflowAction.is_collection_field_action("") - assert not BuilderWorkflowAction.is_collection_field_action("click") - assert BuilderWorkflowAction.is_collection_field_action( +def test_builder_workflow_action_is_dynamic_event(): + assert not BuilderWorkflowAction.is_dynamic_event("") + assert not BuilderWorkflowAction.is_dynamic_event("click") + assert BuilderWorkflowAction.is_dynamic_event( "f1594a0a-3ff0-4c8c-a175-992039b11411_click" ) - - -@pytest.mark.django_db -def test_builder_workflow_action_target(data_fixture): - page = data_fixture.create_builder_page() - table_element = data_fixture.create_builder_table_element( - page=page, - fields=[ - { - "name": "FieldA", - "type": "button", - "config": {"value": f"'Click me')"}, - }, - ], - ) - collection_field = table_element.fields.get() - collection_field_workflow_action = data_fixture.create_workflow_action( - NotificationWorkflowAction, - page=page, - element=table_element, - event=f"{collection_field.uid}_click", - ) - - assert collection_field_workflow_action.target == collection_field - - button_element = data_fixture.create_builder_button_element(page=page) - button_element_workflow_action = data_fixture.create_workflow_action( - NotificationWorkflowAction, - page=page, - element=button_element, - event=EventTypes.CLICK, - ) - - assert button_element_workflow_action.target == button_element diff --git a/changelog/entries/unreleased/bug/fix_bug_when_deleting_a_duplicated_menu_element_was_deleting.json b/changelog/entries/unreleased/bug/fix_bug_when_deleting_a_duplicated_menu_element_was_deleting.json new file mode 100644 index 000000000..b627420e9 --- /dev/null +++ b/changelog/entries/unreleased/bug/fix_bug_when_deleting_a_duplicated_menu_element_was_deleting.json @@ -0,0 +1,8 @@ +{ + "type": "bug", + "message": "Fix bug when deleting a duplicated menu element was deleting the initial actions", + "domain": "builder", + "issue_number": null, + "bullet_points": [], + "created_at": "2025-03-31" +} \ No newline at end of file