mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-29 06:49:55 +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 abc
|
||||||
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
@ -2380,17 +2381,31 @@ class MenuElementType(ElementType):
|
||||||
menu_items_to_create = []
|
menu_items_to_create = []
|
||||||
child_uids_parent_uids = {}
|
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}
|
ids_uids = {i["id"]: i["uid"] for i in menu_items}
|
||||||
keys_to_remove = ["id", "menu_item_order", "children"]
|
keys_to_remove = ["id", "menu_item_order", "children"]
|
||||||
|
|
||||||
for index, item in enumerate(menu_items):
|
for index, item in enumerate(menu_items):
|
||||||
for key in keys_to_remove:
|
for key in keys_to_remove:
|
||||||
item.pop(key, None)
|
item.pop(key, None)
|
||||||
|
|
||||||
|
old_uid = item.pop("uid")
|
||||||
|
new_uid = updated_uids[old_uid]
|
||||||
|
|
||||||
# Keep track of child-parent relationship via the uid
|
# Keep track of child-parent relationship via the uid
|
||||||
if parent_id := item.pop("parent_menu_item", None):
|
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)
|
created_menu_items = MenuItemElement.objects.bulk_create(menu_items_to_create)
|
||||||
instance.menu_items.add(*created_menu_items)
|
instance.menu_items.add(*created_menu_items)
|
||||||
|
|
|
@ -196,6 +196,11 @@ class ElementType(
|
||||||
) -> ElementSubClass:
|
) -> ElementSubClass:
|
||||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
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:
|
if cache is None:
|
||||||
cache = {}
|
cache = {}
|
||||||
|
|
||||||
|
@ -504,14 +509,10 @@ class CollectionFieldType(
|
||||||
deserialized_uid = str(uuid.uuid4())
|
deserialized_uid = str(uuid.uuid4())
|
||||||
|
|
||||||
if "uid" in serialized_values:
|
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
|
# 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
|
# actions with an `event` pointing to the old uid will have the
|
||||||
# pointer to the new uid.
|
# pointer to the new uid.
|
||||||
id_mapping["builder_collection_fields_uids"][
|
id_mapping["builder_element_event_uids"][
|
||||||
serialized_values["uid"]
|
serialized_values["uid"]
|
||||||
] = deserialized_uid
|
] = deserialized_uid
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from baserow.contrib.builder.elements.models import (
|
from baserow.contrib.builder.elements.models import Element, NavigationElementMixin
|
||||||
CollectionField,
|
|
||||||
Element,
|
|
||||||
NavigationElementMixin,
|
|
||||||
)
|
|
||||||
from baserow.contrib.builder.pages.models import Page
|
from baserow.contrib.builder.pages.models import Page
|
||||||
from baserow.core.formula.field import FormulaField
|
from baserow.core.formula.field import FormulaField
|
||||||
from baserow.core.mixins import OrderableMixin
|
from baserow.core.mixins import OrderableMixin
|
||||||
|
@ -43,33 +37,14 @@ class BuilderWorkflowAction(
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@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 the given event is dynamically generated.
|
||||||
|
|
||||||
:return: Whether this workflow action is associated with a collection field.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_event_types = [e.value for e in EventTypes]
|
default_event_types = [e.value for e in EventTypes]
|
||||||
return event and event not in default_event_types
|
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
|
@staticmethod
|
||||||
def get_type_registry() -> ModelRegistryMixin:
|
def get_type_registry() -> ModelRegistryMixin:
|
||||||
from baserow.contrib.builder.workflow_actions.registries import (
|
from baserow.contrib.builder.workflow_actions.registries import (
|
||||||
|
|
|
@ -61,9 +61,9 @@ class BuilderWorkflowActionType(
|
||||||
:return: The new workflow action instance.
|
: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)
|
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}"
|
serialized_values["event"] = f"{imported_uid}_{exported_event}"
|
||||||
|
|
||||||
return super().create_instance_from_serialized(
|
return super().create_instance_from_serialized(
|
||||||
|
|
|
@ -1930,6 +1930,9 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
|
||||||
return
|
return
|
||||||
|
|
||||||
slug = ".".join(template_file_path.name.split(".")[:-1])
|
slug = ".".join(template_file_path.name.split(".")[:-1])
|
||||||
|
|
||||||
|
logger.info(f"Importing template {slug}")
|
||||||
|
|
||||||
installed_template = next(
|
installed_template = next(
|
||||||
(t for t in installed_templates if t.slug == slug), None
|
(t for t in installed_templates if t.slug == slug), None
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,10 +35,14 @@ class Command(BaseCommand):
|
||||||
elif options.get("only", None):
|
elif options.get("only", None):
|
||||||
try:
|
try:
|
||||||
templates = options["only"][0]
|
templates = options["only"][0]
|
||||||
CoreHandler().sync_templates(pattern=templates)
|
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
logger.exception("Error while importing template")
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.ERROR("Provide a pattern to match templates")
|
self.style.ERROR("Provide a pattern to match templates")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CoreHandler().sync_templates(pattern=templates)
|
||||||
else:
|
else:
|
||||||
CoreHandler().sync_templates()
|
CoreHandler().sync_templates()
|
||||||
|
|
|
@ -416,24 +416,68 @@ def test_import_export(menu_element_fixture, data_fixture):
|
||||||
|
|
||||||
assert MenuElement.objects.count() == 0
|
assert MenuElement.objects.count() == 0
|
||||||
assert MenuItemElement.objects.count() == 0
|
assert MenuItemElement.objects.count() == 0
|
||||||
assert NotificationWorkflowAction.objects.count() == 0
|
|
||||||
|
|
||||||
# After importing the Menu element the menu items should be correctly
|
# After importing the Menu element the menu items should be correctly
|
||||||
# imported as well.
|
# imported as well.
|
||||||
id_mapping = defaultdict(lambda: MirrorDict())
|
id_mapping = defaultdict(MirrorDict)
|
||||||
menu_element_type.import_serialized(page, exported, id_mapping)
|
menu_element_type.import_serialized(page, exported, id_mapping)
|
||||||
|
|
||||||
menu_element = MenuElement.objects.first()
|
menu_element = MenuElement.objects.first()
|
||||||
|
|
||||||
# Ensure the Menu Items have been imported correctly
|
# Ensure the Menu Items have been imported correctly
|
||||||
button_item = menu_element.menu_items.get(uid=uid_1)
|
# and uids should have been updated
|
||||||
assert button_item.name == "Greet"
|
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)
|
link_item = menu_element.menu_items.get(name="Link A")
|
||||||
assert link_item.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)
|
sublinks_item = menu_element.menu_items.get(name="Sublinks")
|
||||||
assert sublinks_item.name == "Sublinks"
|
assert sublinks_item.uid != uid_3
|
||||||
|
|
||||||
sublink_a = menu_element.menu_items.get(uid=uid_4)
|
sublink_a = menu_element.menu_items.get(name="Sublink A")
|
||||||
assert sublink_a.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
|
||||||
|
|
||||||
from baserow.contrib.builder.workflow_actions.models import (
|
|
||||||
BuilderWorkflowAction,
|
|
||||||
EventTypes,
|
|
||||||
NotificationWorkflowAction,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_builder_workflow_action_is_collection_field_action():
|
def test_builder_workflow_action_is_dynamic_event():
|
||||||
assert not BuilderWorkflowAction.is_collection_field_action("")
|
assert not BuilderWorkflowAction.is_dynamic_event("")
|
||||||
assert not BuilderWorkflowAction.is_collection_field_action("click")
|
assert not BuilderWorkflowAction.is_dynamic_event("click")
|
||||||
assert BuilderWorkflowAction.is_collection_field_action(
|
assert BuilderWorkflowAction.is_dynamic_event(
|
||||||
"f1594a0a-3ff0-4c8c-a175-992039b11411_click"
|
"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
Add a link
Reference in a new issue