Redesign the screen editor's element tree and add duplication

The "Éléments de cet écran" tree now renders every level of nesting
(previously stopped after one level of children) as a compact single-line
list, and right-clicking a row opens a context menu to duplicate the
element (and its full subtree) in place, in its current container.

Also: all property panels start collapsed instead of some being open by
default, the redundant nested element list inside "Ajouter DANS ce
conteneur" is removed (it only needs the widget picker), and the
now-unneeded "a container is selected" warning banner is gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-23 19:53:38 +02:00
co-authored by Claude Sonnet 5
parent 3039489e39
commit bb79f2f93d
7 changed files with 249 additions and 44 deletions
+64
View File
@@ -182,6 +182,70 @@ def test_image_gets_object_fit_cover_by_default(client, game):
assert "object-fit:cover" in html
def test_duplicate_top_level_element_creates_independent_copy(client, game):
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "titre"})
original_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
client.post(f"/game/{game}/elements/{original_id}/save", data={"ctrl_content": "Bienvenue"})
resp = client.post(f"/game/{game}/elements/{original_id}/duplicate", follow_redirects=False)
assert resp.status_code == 302
new_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
assert new_id != original_id
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
assert html.count("Bienvenue") == 2 # original + copie, chacun un élément séparé
# Modifier la copie ne doit pas toucher l'original (deux lignes indépendantes).
client.post(f"/game/{game}/elements/{new_id}/save", data={"ctrl_content": "Copie modifiée"})
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
assert "Bienvenue" in html
assert "Copie modifiée" in html
def test_duplicate_container_deep_copies_children(client, game):
"""Régression : dupliquer un conteneur doit copier tout son sous-arbre
(pas juste la boîte vide) — voir screens/elements/duplicate_element.py,
qui réutilise le même parcours récursif que la copie d'écran-modèle."""
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "conteneur"})
container_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
resp = client.post(f"/game/{game}/elements/{container_id}/children/add", data={"widget": "titre"})
child_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
client.post(f"/game/{game}/elements/{child_id}/save", data={"ctrl_content": "Titre enfant"})
resp = client.post(f"/game/{game}/elements/{container_id}/duplicate", follow_redirects=False)
new_container_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
assert new_container_id != container_id
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
assert html.count("Titre enfant") == 2 # l'enfant a bien été copié avec son parent
def test_duplicate_child_element_stays_in_same_container(client, game):
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "conteneur"})
container_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
resp = client.post(f"/game/{game}/elements/{container_id}/children/add", data={"widget": "texte"})
child_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
client.post(f"/game/{game}/elements/{child_id}/duplicate")
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
assert html.count("↳") == 0 # l'ancienne arborescence imbriquée dans "Ajouter DANS ce conteneur" a disparu
tree_html = html[html.find('class="elementTree"'):]
assert tree_html.count("treeChildren") >= 1 # le conteneur a bien 2 enfants imbriqués (le nouveau + l'original)
def test_screen_editor_property_panels_closed_by_default(client, game):
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "conteneur"})
container_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
html = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={container_id}").data.decode()
assert "<details class=\"propGroup\" open>" not in html
assert "Un conteneur est sélectionné" not in html
def test_delete_element_type_removes_hidden_template_screen(client, game):
resp = client.post(f"/game/{game}/element-types", data={"name": "ToDelete", "icon": "🧩"}, follow_redirects=False)
html = client.get(f"/game/{game}/element-types").data.decode()