1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-06 22:08:52 +00:00

Fix uid bug with menu element

This commit is contained in:
Jérémie Pardou 2025-04-01 14:19:55 +02:00
parent 64b6472417
commit 42a019bdce
9 changed files with 103 additions and 93 deletions
backend
src/baserow
contrib/builder
core
handler.py
management/commands
tests/baserow/contrib/builder
changelog/entries/unreleased/bug

View file

@ -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)

View file

@ -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

View file

@ -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 (

View file

@ -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(

View file

@ -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
)

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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"
}