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:
parent
64b6472417
commit
42a019bdce
9 changed files with 103 additions and 93 deletions
backend
src/baserow
contrib/builder
core
tests/baserow/contrib/builder
changelog/entries/unreleased/bug
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
Loading…
Add table
Reference in a new issue