From 0e390a679b57fcab55be2113bd2ecc7d286cb968 Mon Sep 17 00:00:00 2001 From: william Date: Thu, 27 Aug 2026 12:13:05 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20l'onglet=20"Donn=C3=A9es"=20au=20panne?= =?UTF-8?q?au=20objet,=20retire=20object=5Fview.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suite de la demande : le panneau "Modifier un objet" (crayon ✏️, onglet Objets du tableau de bord) a maintenant 2 sous-onglets — "Champs" (déjà en place) et "Données", qui reprend data_list.html + data_form.html (retirés) : ajouter une entrée (une ligne compacte avec le bon type de champ par colonne — texte/nombre/case à cocher/date/relation, comme l'ancien formulaire), modifier/supprimer une entrée existante (tableau dense, cellules éditables en ligne, même principe que "Champs existants"). Sous-onglets scopés au panneau de LEUR objet (switchObjectPanelTab(), classes .objectPanelTabs/.objectPanelTabPanel distinctes de .builderTabs/ .builderTabPanel) — plusieurs objets ont chacun leurs propres sous-onglets indépendants sur la même page, sans jamais interférer avec les onglets du tableau de bord lui-même. routes/games/game_dashboard.py fournit maintenant, par objet : ses lignes (rows_by_definition), les libellés lisibles de ses champs relation (relation_labels_by_definition, pour l'affichage) et leurs options (relation_options_by_definition, pour les - # à partir des lignes déjà enregistrées de l'objet lié. - relation_options = {} - for f in definition["fields"]: - if f["type"] == "relation": - relation_options[f["id"]] = db.relation_options(slug, f["relation_definition_id"]) - if request.method == "POST": if row is None: db.insert_row(slug, definition, request.form) else: db.update_row(slug, definition, row["id"], request.form) - return redirect(url_for("object_view", slug=slug, definition_id=definition_id)) - return render_template( - "data_form.html", - game=game, - definition=definition, - relation_options=relation_options, - row=row, - ) + return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id, subtab="data")) diff --git a/routes/objects/object_delete.py b/routes/objects/object_delete.py index 1a087a38..53623134 100644 --- a/routes/objects/object_delete.py +++ b/routes/objects/object_delete.py @@ -10,9 +10,10 @@ def object_delete(slug, definition_id): referenced_by = db.definitions_referencing(slug, definition_id) if referenced_by: # Suppression bloquée : d'autres objets ont une relation vers - # celui-ci. On revient à l'écran de l'objet, qui affiche déjà cet - # avertissement (referenced_by), plutôt que de casser ces relations - # silencieusement. - return redirect(url_for("object_view", slug=slug, definition_id=definition_id)) + # celui-ci. On revient à son panneau, qui affiche déjà cet + # avertissement (referenced_by_definition, voir game_dashboard.py) + # en permanence, pas seulement après une tentative — plutôt que de + # casser ces relations silencieusement. + return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id)) db.delete_definition(slug, definition_id) - return redirect(url_for("game_dashboard", slug=slug)) + return redirect(url_for("game_dashboard", slug=slug, tab="objects")) diff --git a/routes/objects/object_view.py b/routes/objects/object_view.py index 52474d36..d05b504e 100644 --- a/routes/objects/object_view.py +++ b/routes/objects/object_view.py @@ -1,33 +1,18 @@ -from flask import render_template, request, abort - -import db +from flask import redirect, url_for from core.flask_app import app @app.route("/game//objects/") def object_view(slug, definition_id): - game = db.game_meta(slug) - definition = db.get_definition(slug, definition_id) - if not definition: - abort(404) - rows = db.list_rows(slug, definition) - - # Pour l'affichage : traduit chaque id de relation en un libellé lisible - # (ex. "Salle serveur (#1)") au lieu d'un identifiant numérique brut. - relation_labels = {} - for f in definition["fields"]: - if f["type"] == "relation": - options = db.relation_options(slug, f["relation_definition_id"]) - relation_labels[f["id"]] = {opt["id"]: opt["label"] for opt in options} - - referenced_by = db.definitions_referencing(slug, definition_id) - - blocked_row_id = request.args.get("blocked_row", type=int) - blocked_row_refs = db.rows_referencing(slug, definition_id, blocked_row_id) if blocked_row_id else None - - return render_template( - "data_list.html", game=game, definition=definition, rows=rows, - relation_labels=relation_labels, referenced_by=referenced_by, - blocked_row_id=blocked_row_id, blocked_row_refs=blocked_row_refs, - ) + """Ancienne page de visualisation des données d'un objet (data_list.html) — + retirée : les données vivent désormais dans l'onglet "Données" du + panneau déplaçable de cet objet (voir game_dashboard.html). Cette + route reste néanmoins en place, comme pur redirecteur, pour deux + raisons : un lien externe/ancien vers cette URL continue de mener + quelque part de sensé, et surtout beaucoup de tests (et certains + formulaires) déduisent l'id d'un objet fraîchement créé du DERNIER + segment du chemin renvoyé par le header Location d'une redirection + (ex. .../objects/) — object_new redirige donc ICI (chemin qui se + termine par l'id), qui redirige ensuite vers le tableau de bord.""" + return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id)) diff --git a/static/style.css b/static/style.css index e0c0d947..2d1291ba 100644 --- a/static/style.css +++ b/static/style.css @@ -38,6 +38,11 @@ input[type=checkbox]{ width:auto; margin-top:6px; } .formField{ margin-bottom:6px; } button{ background:var(--panel2); border:1px solid var(--border); color:var(--text); border-radius:6px; padding:9px 14px; cursor:pointer; font-size:13px; } +/* Un - -

← Retour à « {{ definition.name }} »

- - - -
-

Champs

-
- {% for f in definition.fields %} - {% set col = f.name|colname %} - {% set current = row[col] if row else none %} -
- -
- - {% if f.type == 'texte' %} - - - {% elif f.type == 'texte_long' %} - - - {% elif f.type == 'nombre_entier' %} - - - {% elif f.type == 'nombre_decimal' %} - - - {% elif f.type == 'booleen' %} - - - {% elif f.type == 'date' %} - - - {% elif f.type == 'relation' %} - {% set current_rel = row[col ~ '_id'] if row else none %} -
- -
- {% if not relation_options.get(f.id) %} -

Aucune entrée n'existe encore pour l'objet lié — crée-en une d'abord si tu veux pouvoir la sélectionner ici.

- {% endif %} - {% endif %} -
-
- {% endfor %} -
-
- - -{% endblock %} diff --git a/templates/data_list.html b/templates/data_list.html deleted file mode 100644 index 8a045664..00000000 --- a/templates/data_list.html +++ /dev/null @@ -1,124 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ definition.name }} — {{ game.name }}{% endblock %} -{% block content_class %}content-wide content-objectEdit{% endblock %} -{% block body_class %}objectEditBody{% endblock %} -{% block content %} -

{{ definition.name }} - ✏️ -

-

Table SQL : {{ definition.table_name }} — {{ definition.fields|length }} champ(s)

- -{% if referenced_by %} -
- ⚠️ D'autres objets ont une relation vers « {{ definition.name }} » : - {{ referenced_by|map(attribute='name')|join(', ') }}. - La suppression de cet objet est bloquée tant que ces relations existent. -
-{% endif %} - -{% if blocked_row_id %} -
- ⚠️ Impossible de supprimer l'entrée #{{ blocked_row_id }} : elle est référencée par - {% for ref in blocked_row_refs %}{{ ref.count }} entrée(s) de « {{ ref.definition_name }} » (champ « {{ ref.field_name }} »){% if not loop.last %}, {% endif %}{% endfor %}. - Modifie ou supprime d'abord ces entrées. -
-{% endif %} - -
-
-

Ajouter

-
-

Une entrée, c'est une ligne de la table SQL « {{ definition.table_name }} ».

- -
-
- -
-

Données enregistrées

- {% if rows %} - {% macro row_field_display(f, r) %} - {%- if f.type == 'relation' -%} - {{ relation_labels.get(f.id, {}).get(r[(f.name|colname) ~ '_id'], '—') }} - {%- elif f.type == 'booleen' -%} - {{ 'Oui' if r[f.name|colname] else 'Non' }} - {%- else -%} - {{ r[f.name|colname] }} - {%- endif -%} - {% endmacro %} -
- - - - - {% for f in definition.fields %}{% endfor %} - - - - - {% for r in rows %} - - - {% for f in definition.fields %} - - {% endfor %} - - - {% endfor %} - -
#{{ f.name }}
{{ r.id }}{{ row_field_display(f, r) }} -
- - ✏️ -
- -
-
-
-
- - {% for r in rows %} - - {% endfor %} - {% else %} -

Aucune donnée enregistrée pour l'instant.

- {% endif %} -
-
- - -{% endblock %} diff --git a/templates/game_dashboard.html b/templates/game_dashboard.html index 5870b6d3..c391cf33 100644 --- a/templates/game_dashboard.html +++ b/templates/game_dashboard.html @@ -75,7 +75,7 @@ {% for d in definitions %} - {{ d.name }} + {{ d.table_name }} @@ -275,94 +275,214 @@

Table SQL : {{ definition.table_name }}

-

Ajouter un champ

-
- - - - - - - -
- -

Champs existants

- {% if definition.fields %} - {% for f in definition.fields %} -
-
- {% endfor %} -
- - - - - - {% for f in definition.fields %} - - - - - - - - - - {% endfor %} - -
NomTypeObjet liéMiniMaxiObligatoire
{{ field_types.get(f.type, {}).get('label', f.type) }} - {% if f.type == 'relation' %} -
- -
- {% else %} - — - {% endif %} -
- {% if f.type in ('nombre_entier', 'nombre_decimal') %} - - {% else %} - — - {% endif %} - - {% if f.type in ('nombre_entier', 'nombre_decimal') %} - - {% else %} - — - {% endif %} - -
- -
-
- - -
+ {% if referenced_by_definition.get(d.id) %} +
+ ⚠️ D'autres objets ont une relation vers « {{ definition.name }} » : + {{ referenced_by_definition[d.id]|map(attribute='name')|join(', ') }}. + La suppression de cet objet est bloquée tant que ces relations existent.
-

Mini/Maxi (bornage automatique) : laisser vide = pas de limite.

- {% else %} -

Aucun champ pour l'instant.

{% endif %} + +
+ + +
+ +
+
+

Ajouter un champ

+
+
+ + + + + + + +
+ +

Champs existants

+ {% if definition.fields %} + {% for f in definition.fields %} +
+
+ {% endfor %} +
+ + + + + + {% for f in definition.fields %} + + + + + + + + + + {% endfor %} + +
NomTypeObjet liéMiniMaxiObligatoire
{{ field_types.get(f.type, {}).get('label', f.type) }} + {% if f.type == 'relation' %} +
+ +
+ {% else %} + — + {% endif %} +
+ {% if f.type in ('nombre_entier', 'nombre_decimal') %} + + {% else %} + — + {% endif %} + + {% if f.type in ('nombre_entier', 'nombre_decimal') %} + + {% else %} + — + {% endif %} + +
+ +
+
+ + +
+
+

Mini/Maxi (bornage automatique) : laisser vide = pas de limite.

+ {% else %} +

Aucun champ pour l'instant.

+ {% endif %} +
+ +
+ {% set blocked_row_id = request.args.get('blocked_row') if request.args.get('edit') == d.id|string else none %} + {% if blocked_row_id %} +
+ ⚠️ Impossible de supprimer l'entrée #{{ blocked_row_id }} : elle est référencée ailleurs. Modifie ou supprime d'abord ces entrées. +
+ {% endif %} +
+

Ajouter une entrée

+
+ {% if definition.fields %} +
+ {% for f in definition.fields %} + {% set col = f.name|colname %} + {% if f.type == 'texte' %} + + {% elif f.type == 'texte_long' %} + + {% elif f.type == 'nombre_entier' %} + + {% elif f.type == 'nombre_decimal' %} + + {% elif f.type == 'booleen' %} + + {% elif f.type == 'date' %} + + {% elif f.type == 'relation' %} + + {% endif %} + {% endfor %} + +
+ +

Données enregistrées

+ {% set rows = rows_by_definition[d.id] %} + {% if rows %} + {% for r in rows %} +
+
+ {% endfor %} +
+ + + {% for f in definition.fields %}{% endfor %} + + + {% for r in rows %} + + + {% for f in definition.fields %} + {% set col = f.name|colname %} + + {% endfor %} + + + {% endfor %} + +
#{{ f.name }}
{{ r.id }} + {% if f.type == 'texte' %} + + {% elif f.type == 'texte_long' %} + + {% elif f.type in ('nombre_entier', 'nombre_decimal') %} + + {% elif f.type == 'booleen' %} + + {% elif f.type == 'date' %} + + {% elif f.type == 'relation' %} +
+ +
+ {% endif %} +
+ + +
+
+ {% else %} +

Aucune donnée enregistrée pour l'instant.

+ {% endif %} + {% else %} +

Ajoute d'abord au moins un champ pour pouvoir enregistrer des données.

+ {% endif %} +
+

Supprimer cet objet

, posé par les + // suppression d'un champ ou d'une entrée, renommage...) : ?edit= + // (et ?subtab=data pour rouvrir sur le bon sous-onglet), posés par les // routes concernées (voir routes/objects/) sur leur redirection vers ce // tableau de bord — sans ça, chaque action fermait le panneau et - // obligeait à recliquer sur le crayon pour voir le résultat. + // obligeait à recliquer pour voir le résultat. var requestedEdit = {{ (request.args.get('edit') or '')|tojson }}; - if (requestedEdit) openObjectEditPanel(requestedEdit); + var requestedSubtab = {{ (request.args.get('subtab') or '')|tojson }}; + if (requestedEdit) openObjectEditPanel(requestedEdit, requestedSubtab); })(); + +// Sous-onglets Champs/Données d'UN panneau précis (voir plus haut) — scopé +// au panneau ancêtre du bouton cliqué, jamais global (plusieurs objets +// peuvent chacun avoir leurs sous-onglets sur leur propre panneau, en même +// temps, sur la même page). +function switchObjectPanelTab(btn, tab) { + var panel = btn.closest('.objectEditPanel'); + panel.querySelectorAll('.objectPanelTabs .builderTabBtn').forEach(function (b) { + b.classList.toggle('active', b.dataset.optab === tab); + }); + panel.querySelectorAll('.objectPanelTabPanel').forEach(function (p) { + p.classList.toggle('active', p.dataset.optabPanel === tab); + }); +} {% endblock %}