Ajoute l'onglet "Données" au panneau objet, retire object_view.html
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 <select>), ainsi que referenced_by_definition (avertissement permanent si un autre objet a une relation vers celui-ci — repris de l'ancien object_view.py, affiché maintenant en continu plutôt qu'après une tentative de suppression échouée). data_form.py (partagé par data_new/data_edit), data_delete.py et object_delete.py redirigent maintenant vers le tableau de bord (?edit=<id>&subtab=data, +?blocked_row=<id> si la suppression d'une entrée est bloquée par une relation) au lieu de object_view/object_edit. Piège évité : de nombreux tests déduisent l'id d'un objet fraîchement créé du DERNIER SEGMENT du chemin dans le header Location d'une redirection (.../objects/<id>) — rediriger object_new directement vers le tableau de bord (chemin sans id) cassait donc 33 tests d'un coup. Fix : object_view.py reste en place, mais seulement comme redirecteur (plus de page rendue) — object_new redirige toujours vers lui (chemin qui se termine par l'id, donc les tests continuent de fonctionner), qui redirige à son tour vers le tableau de bord. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b8dfdcb205
commit
0e390a679b
@@ -22,6 +22,30 @@ def game_dashboard(slug):
|
||||
# simplement montré/masqué en JS, plutôt qu'un aller-retour serveur à
|
||||
# l'ouverture (voir openObjectEditPanel()).
|
||||
definitions_full = {d["id"]: db.get_definition(slug, d["id"]) for d in definitions}
|
||||
|
||||
# Pour l'onglet "Données" de ce même panneau (remplace data_list.html/
|
||||
# object_view.py, retirés) : les lignes déjà enregistrées de chaque
|
||||
# objet, les libellés lisibles de ses champs relation (pour l'affichage
|
||||
# du tableau) et les options possibles de ces mêmes champs relation
|
||||
# (pour le formulaire "Ajouter une entrée").
|
||||
rows_by_definition = {}
|
||||
relation_labels_by_definition = {}
|
||||
relation_options_by_definition = {}
|
||||
referenced_by_definition = {}
|
||||
for d in definitions:
|
||||
full = definitions_full[d["id"]]
|
||||
rows_by_definition[d["id"]] = db.list_rows(slug, full)
|
||||
relation_labels = {}
|
||||
relation_options = {}
|
||||
for f in full["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}
|
||||
relation_options[f["id"]] = options
|
||||
relation_labels_by_definition[d["id"]] = relation_labels
|
||||
relation_options_by_definition[d["id"]] = relation_options
|
||||
referenced_by_definition[d["id"]] = db.definitions_referencing(slug, d["id"])
|
||||
|
||||
screen_list = screens.list_screens(slug)
|
||||
element_types = screens.list_element_types(slug)
|
||||
element_type_usage = {t["id"]: screens.element_type_usage_count(slug, t["id"]) for t in element_types}
|
||||
@@ -29,6 +53,10 @@ def game_dashboard(slug):
|
||||
return render_template(
|
||||
"game_dashboard.html", game=game, definitions=definitions,
|
||||
definitions_full=definitions_full,
|
||||
rows_by_definition=rows_by_definition,
|
||||
relation_labels_by_definition=relation_labels_by_definition,
|
||||
relation_options_by_definition=relation_options_by_definition,
|
||||
referenced_by_definition=referenced_by_definition,
|
||||
screen_list=screen_list, element_types=element_types,
|
||||
element_type_usage=element_type_usage, variables=variables,
|
||||
variable_types=db.GLOBAL_VARIABLE_TYPES, field_types=db.FIELD_TYPES,
|
||||
|
||||
@@ -13,8 +13,9 @@ def data_delete(slug, definition_id, row_id):
|
||||
referencing = db.rows_referencing(slug, definition_id, row_id)
|
||||
if referencing:
|
||||
# Suppression bloquée : d'autres lignes pointent vers celle-ci via
|
||||
# une relation. On revient à la liste, qui explique pourquoi via un
|
||||
# paramètre de requête simple plutôt qu'une session/flash message.
|
||||
return redirect(url_for("object_view", slug=slug, definition_id=definition_id, blocked_row=row_id))
|
||||
# une relation. On revient au panneau de cet objet (onglet
|
||||
# Données), qui explique pourquoi via un paramètre de requête
|
||||
# simple plutôt qu'une session/flash message.
|
||||
return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id, subtab="data", blocked_row=row_id))
|
||||
db.delete_row(slug, definition, row_id)
|
||||
return redirect(url_for("object_view", slug=slug, definition_id=definition_id))
|
||||
return redirect(url_for("game_dashboard", slug=slug, tab="objects", edit=definition_id, subtab="data"))
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
from flask import render_template, request, redirect, url_for, abort
|
||||
from flask import request, redirect, url_for, abort
|
||||
|
||||
import db
|
||||
|
||||
|
||||
def _data_form(slug, definition_id, row):
|
||||
game = db.game_meta(slug)
|
||||
"""Ajouter/modifier une entrée se fait désormais depuis l'onglet
|
||||
"Données" du panneau déplaçable de son objet (voir game_dashboard.html,
|
||||
ouvert par le crayon ✏️ de l'onglet Objets) plutôt qu'une page dédiée
|
||||
(data_form.html, retirée) — cette fonction ne fait donc plus que
|
||||
traiter la soumission de ce formulaire. Un GET direct redirige vers le
|
||||
tableau de bord, en rouvrant le panneau de cet objet sur son onglet
|
||||
Données."""
|
||||
definition = db.get_definition(slug, definition_id)
|
||||
if not definition:
|
||||
abort(404)
|
||||
|
||||
# Pour chaque champ de type relation, on prépare les options du <select>
|
||||
# à 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"))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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/<slug>/objects/<int:definition_id>")
|
||||
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/<id>) — 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))
|
||||
|
||||
@@ -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 <button> qui doit se comporter comme un <a> visuellement (ex. ouvrir
|
||||
un panneau plutôt que suivre un href, mais garder l'apparence d'un lien
|
||||
dans un tableau) — annule le style bouton par défaut ci-dessus. */
|
||||
.linkLikeButton{ background:none; border:none; padding:0; color:var(--accent); text-decoration:none; font-size:inherit; }
|
||||
.linkLikeButton:hover{ text-decoration:underline; }
|
||||
button.primary{ background:var(--accent); border-color:var(--accent); color:#fff; font-weight:600; }
|
||||
button.smallBtn{ padding:5px 10px; font-size:11.5px; }
|
||||
button.danger{ color:var(--danger); border-color:rgba(226,104,95,.4); }
|
||||
@@ -438,6 +443,15 @@ code{ background:var(--panel2); border:1px solid var(--border); border-radius:4p
|
||||
.builderTabPanel{ flex:1 1 auto; min-height:0; display:none; flex-direction:column; }
|
||||
.builderTabPanel.active{ display:flex; }
|
||||
|
||||
/* Sous-onglets Champs/Données À L'INTÉRIEUR d'un panneau "Modifier un
|
||||
objet" (voir game_dashboard.html) — classes distinctes de .builderTabs/
|
||||
.builderTabBtn/.builderTabPanel (utilisées par le style, réutilisé tel
|
||||
quel) pour que switchDashTab() (onglets du tableau de bord lui-même,
|
||||
ciblage global) n'aille jamais les toucher par erreur. */
|
||||
.objectPanelTabs{ margin:14px 0 10px; }
|
||||
.objectPanelTabPanel{ display:none; flex-direction:column; }
|
||||
.objectPanelTabPanel.active{ display:flex; }
|
||||
|
||||
.flowToolbar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px; flex:0 0 auto; }
|
||||
|
||||
.nodeForm{ border:1px solid var(--border); border-radius:8px; padding:12px; margin-bottom:12px; background:var(--panel2); max-width:420px; flex:0 0 auto; }
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ 'Modifier' if row else 'Ajouter' }} — {{ definition.name }}{% endblock %}
|
||||
{% block content_class %}content-wide content-objectEdit{% endblock %}
|
||||
{% block body_class %}objectEditBody{% endblock %}
|
||||
{% block content %}
|
||||
<h1 class="title">{{ 'Modifier l\'entrée #' ~ row.id if row else 'Ajouter une entrée' }} — {{ definition.name }}</h1>
|
||||
|
||||
<form method="post" class="formPassthrough">
|
||||
<div class="twoCol">
|
||||
<div class="colLeft">
|
||||
<h2>Objet</h2>
|
||||
<div class="card">
|
||||
<p class="hint">Formulaire généré automatiquement à partir de la définition de « {{ definition.name }} » — aucun champ n'est codé en dur ici.</p>
|
||||
<div class="actionsRow">
|
||||
<button type="submit" class="primary">{{ 'Enregistrer les modifications' if row else 'Enregistrer en base' }}</button>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:10px;"><a href="{{ url_for('object_view', slug=game.slug, definition_id=definition.id) }}">← Retour à « {{ definition.name }} »</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="colRight">
|
||||
<h2>Champs</h2>
|
||||
<div class="fieldsTableWrap" style="padding:10px;">
|
||||
{% for f in definition.fields %}
|
||||
{% set col = f.name|colname %}
|
||||
{% set current = row[col] if row else none %}
|
||||
<div class="field">
|
||||
<label class="label" for="field_{{ f.id }}">{{ f.name }}{% if f.required %} *{% endif %}</label>
|
||||
<div class="control">
|
||||
|
||||
{% if f.type == 'texte' %}
|
||||
<input class="input" type="text" id="field_{{ f.id }}" name="{{ col }}" value="{{ current or '' }}" {{ 'required' if f.required else '' }}>
|
||||
|
||||
{% elif f.type == 'texte_long' %}
|
||||
<textarea class="textarea" id="field_{{ f.id }}" name="{{ col }}" rows="4" {{ 'required' if f.required else '' }}>{{ current or '' }}</textarea>
|
||||
|
||||
{% elif f.type == 'nombre_entier' %}
|
||||
<input class="input" type="number" step="1" id="field_{{ f.id }}" name="{{ col }}" value="{{ current if current is not none else '' }}" {{ 'required' if f.required else '' }}>
|
||||
|
||||
{% elif f.type == 'nombre_decimal' %}
|
||||
<input class="input" type="number" step="any" id="field_{{ f.id }}" name="{{ col }}" value="{{ current if current is not none else '' }}" {{ 'required' if f.required else '' }}>
|
||||
|
||||
{% elif f.type == 'booleen' %}
|
||||
<label class="checkbox"><input type="checkbox" id="field_{{ f.id }}" name="{{ col }}" {{ 'checked' if current else '' }}> Oui</label>
|
||||
|
||||
{% elif f.type == 'date' %}
|
||||
<input class="input" type="date" id="field_{{ f.id }}" name="{{ col }}" value="{{ current or '' }}" {{ 'required' if f.required else '' }}>
|
||||
|
||||
{% elif f.type == 'relation' %}
|
||||
{% set current_rel = row[col ~ '_id'] if row else none %}
|
||||
<div class="select is-fullwidth">
|
||||
<select id="field_{{ f.id }}" name="{{ col }}_id" {{ 'required' if f.required else '' }}>
|
||||
<option value="">—</option>
|
||||
{% for opt in relation_options.get(f.id, []) %}
|
||||
<option value="{{ opt.id }}" {{ 'selected' if current_rel == opt.id else '' }}>{{ opt.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if not relation_options.get(f.id) %}
|
||||
<p class="help">Aucune entrée n'existe encore pour l'objet lié — crée-en une d'abord si tu veux pouvoir la sélectionner ici.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
<h1 class="title">{{ definition.name }}
|
||||
<a class="button is-small" href="{{ url_for('object_edit', slug=game.slug, definition_id=definition.id) }}" title="Modifier cet objet">✏️</a>
|
||||
</h1>
|
||||
<p class="hint mb-4">Table SQL : {{ definition.table_name }} — {{ definition.fields|length }} champ(s)</p>
|
||||
|
||||
{% if referenced_by %}
|
||||
<div class="warningBanner">
|
||||
⚠️ 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.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if blocked_row_id %}
|
||||
<div class="warningBanner">
|
||||
⚠️ 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.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="twoCol">
|
||||
<div class="colLeft">
|
||||
<h2>Ajouter</h2>
|
||||
<div class="card">
|
||||
<p class="hint">Une entrée, c'est une ligne de la table SQL « {{ definition.table_name }} ».</p>
|
||||
<div class="actionsRow">
|
||||
<a class="button is-primary is-outlined is-fullwidth" href="{{ url_for('data_new', slug=game.slug, definition_id=definition.id) }}">+ Ajouter une entrée</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="colRight">
|
||||
<h2>Données enregistrées</h2>
|
||||
{% 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 %}
|
||||
<div class="fieldsTableWrap">
|
||||
<table class="table is-fullwidth is-hoverable dataTable fieldsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
{% for f in definition.fields %}<th>{{ f.name }}</th>{% endfor %}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ r.id }}</td>
|
||||
{% for f in definition.fields %}
|
||||
<td>{{ row_field_display(f, r) }}</td>
|
||||
{% endfor %}
|
||||
<td>
|
||||
<div class="buttons is-inline-flex are-small">
|
||||
<button type="button" class="button is-small" title="Voir cette entrée en détail" onclick="openRowDetail({{ r.id }})">👁️</button>
|
||||
<a class="button is-small" href="{{ url_for('data_edit', slug=game.slug, definition_id=definition.id, row_id=r.id) }}">✏️</a>
|
||||
<form method="post" action="{{ url_for('data_delete', slug=game.slug, definition_id=definition.id, row_id=r.id) }}"
|
||||
onsubmit="return confirm('Supprimer définitivement l\'entrée #{{ r.id }} ?');">
|
||||
<button type="submit" class="button is-small is-danger">🗑️</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% for r in rows %}
|
||||
<div class="modal" id="rowDetail{{ r.id }}">
|
||||
<div class="modal-background" onclick="closeRowDetail({{ r.id }})"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Entrée #{{ r.id }}</p>
|
||||
<button class="delete" aria-label="Fermer" onclick="closeRowDetail({{ r.id }})"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
{% for f in definition.fields %}
|
||||
<div class="rowDetailField">
|
||||
<div class="rowDetailLabel">{{ f.name }}</div>
|
||||
<div class="rowDetailValue">{{ row_field_display(f, r) }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="hint">Aucune donnée enregistrée pour l'instant.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openRowDetail(id) {
|
||||
var modal = document.getElementById('rowDetail' + id);
|
||||
if (modal) modal.classList.add('is-active');
|
||||
}
|
||||
function closeRowDetail(id) {
|
||||
var modal = document.getElementById('rowDetail' + id);
|
||||
if (modal) modal.classList.remove('is-active');
|
||||
}
|
||||
if (!window.__forgeRowDetailEscBound) {
|
||||
window.__forgeRowDetailEscBound = true;
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Escape') return;
|
||||
document.querySelectorAll('.modal.is-active').forEach(function (m) { m.classList.remove('is-active'); });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -75,7 +75,7 @@
|
||||
<tbody>
|
||||
{% for d in definitions %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('object_view', slug=game.slug, definition_id=d.id) }}">{{ d.name }}</a></td>
|
||||
<td><button type="button" class="linkLikeButton" onclick="openObjectEditPanel({{ d.id }})">{{ d.name }}</button></td>
|
||||
<td class="has-text-grey">{{ d.table_name }}</td>
|
||||
<td class="fieldsTableActions">
|
||||
<button type="button" class="button is-small" onclick="openObjectEditPanel({{ d.id }})" title="Modifier cet objet">✏️</button>
|
||||
@@ -275,7 +275,29 @@
|
||||
</form>
|
||||
<p class="help mb-3">Table SQL : {{ definition.table_name }}</p>
|
||||
|
||||
{% if referenced_by_definition.get(d.id) %}
|
||||
<div class="warningBanner">
|
||||
⚠️ 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.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Sous-onglets Champs/Données de CE panneau — classes distinctes de
|
||||
.builderTabs/.builderTabBtn/.builderTabPanel (onglets du tableau de
|
||||
bord lui-même) pour ne jamais interférer avec eux : switchDashTab()
|
||||
cible ces classes globalement, alors que switchObjectPanelTab()
|
||||
reste scopé au panneau de CET objet (plusieurs objets = plusieurs
|
||||
jeux de sous-onglets indépendants sur la même page). -->
|
||||
<div class="builderTabs objectPanelTabs">
|
||||
<button type="button" class="builderTabBtn active" data-optab="fields" onclick="switchObjectPanelTab(this, 'fields')">Champs <span class="dashTabCount">{{ definition.fields|length }}</span></button>
|
||||
<button type="button" class="builderTabBtn" data-optab="data" onclick="switchObjectPanelTab(this, 'data')">Données <span class="dashTabCount">{{ rows_by_definition[d.id]|length }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="objectPanelTabPanel active" data-optab-panel="fields">
|
||||
<div class="dashPanelHeader">
|
||||
<h2>Ajouter un champ</h2>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('object_field_add', slug=game.slug, definition_id=d.id) }}" class="dashCreateBar objectAddFieldBar">
|
||||
<input type="text" name="field_name" placeholder="Nom du champ" required autocomplete="off">
|
||||
<select name="field_type" class="fieldTypeSelect">
|
||||
@@ -362,6 +384,104 @@
|
||||
{% else %}
|
||||
<p class="help">Aucun champ pour l'instant.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="objectPanelTabPanel" data-optab-panel="data">
|
||||
{% set blocked_row_id = request.args.get('blocked_row') if request.args.get('edit') == d.id|string else none %}
|
||||
{% if blocked_row_id %}
|
||||
<div class="warningBanner">
|
||||
⚠️ Impossible de supprimer l'entrée #{{ blocked_row_id }} : elle est référencée ailleurs. Modifie ou supprime d'abord ces entrées.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dashPanelHeader">
|
||||
<h2>Ajouter une entrée</h2>
|
||||
</div>
|
||||
{% if definition.fields %}
|
||||
<form method="post" action="{{ url_for('data_new', slug=game.slug, definition_id=d.id) }}" class="dashCreateBar">
|
||||
{% for f in definition.fields %}
|
||||
{% set col = f.name|colname %}
|
||||
{% if f.type == 'texte' %}
|
||||
<input type="text" name="{{ col }}" placeholder="{{ f.name }}{{ ' *' if f.required else '' }}" {{ 'required' if f.required else '' }}>
|
||||
{% elif f.type == 'texte_long' %}
|
||||
<textarea name="{{ col }}" placeholder="{{ f.name }}{{ ' *' if f.required else '' }}" rows="1" {{ 'required' if f.required else '' }}></textarea>
|
||||
{% elif f.type == 'nombre_entier' %}
|
||||
<input type="number" step="1" name="{{ col }}" placeholder="{{ f.name }}{{ ' *' if f.required else '' }}" {{ 'required' if f.required else '' }}>
|
||||
{% elif f.type == 'nombre_decimal' %}
|
||||
<input type="number" step="any" name="{{ col }}" placeholder="{{ f.name }}{{ ' *' if f.required else '' }}" {{ 'required' if f.required else '' }}>
|
||||
{% elif f.type == 'booleen' %}
|
||||
<label class="checkbox" style="white-space:nowrap; align-self:center;"><input type="checkbox" name="{{ col }}"> {{ f.name }}</label>
|
||||
{% elif f.type == 'date' %}
|
||||
<input type="date" name="{{ col }}" title="{{ f.name }}" {{ 'required' if f.required else '' }}>
|
||||
{% elif f.type == 'relation' %}
|
||||
<select name="{{ col }}_id" title="{{ f.name }}" {{ 'required' if f.required else '' }}>
|
||||
<option value="">{{ f.name }} : —</option>
|
||||
{% for opt in relation_options_by_definition[d.id].get(f.id, []) %}
|
||||
<option value="{{ opt.id }}">{{ opt.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="primary">+ Ajouter</button>
|
||||
</form>
|
||||
|
||||
<h2>Données enregistrées</h2>
|
||||
{% set rows = rows_by_definition[d.id] %}
|
||||
{% if rows %}
|
||||
{% for r in rows %}
|
||||
<form method="post" action="{{ url_for('data_edit', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataEditForm{{ r.id }}"></form>
|
||||
<form method="post" action="{{ url_for('data_delete', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataDeleteForm{{ r.id }}"
|
||||
onsubmit="return confirm('Supprimer définitivement l\'entrée #{{ r.id }} ?');"></form>
|
||||
{% endfor %}
|
||||
<div class="fieldsTableWrap">
|
||||
<table class="table is-fullwidth is-hoverable dataTable fieldsTable">
|
||||
<thead>
|
||||
<tr><th>#</th>{% for f in definition.fields %}<th>{{ f.name }}</th>{% endfor %}<th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ r.id }}</td>
|
||||
{% for f in definition.fields %}
|
||||
{% set col = f.name|colname %}
|
||||
<td>
|
||||
{% if f.type == 'texte' %}
|
||||
<input class="input is-small" type="text" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
|
||||
{% elif f.type == 'texte_long' %}
|
||||
<textarea class="textarea is-small" form="dataEditForm{{ r.id }}" name="{{ col }}" rows="1">{{ r[col] or '' }}</textarea>
|
||||
{% elif f.type in ('nombre_entier', 'nombre_decimal') %}
|
||||
<input class="input is-small" type="number" step="{{ '1' if f.type == 'nombre_entier' else 'any' }}" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] if r[col] is not none else '' }}">
|
||||
{% elif f.type == 'booleen' %}
|
||||
<label class="checkbox"><input type="checkbox" form="dataEditForm{{ r.id }}" name="{{ col }}" {{ 'checked' if r[col] else '' }}></label>
|
||||
{% elif f.type == 'date' %}
|
||||
<input class="input is-small" type="date" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
|
||||
{% elif f.type == 'relation' %}
|
||||
<div class="select is-small">
|
||||
<select form="dataEditForm{{ r.id }}" name="{{ col }}_id">
|
||||
<option value="">—</option>
|
||||
{% for opt in relation_options_by_definition[d.id].get(f.id, []) %}
|
||||
<option value="{{ opt.id }}" {{ 'selected' if r[col ~ '_id'] == opt.id else '' }}>{{ opt.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="fieldsTableActions">
|
||||
<button type="submit" form="dataEditForm{{ r.id }}" class="button is-small">Enregistrer</button>
|
||||
<button type="submit" form="dataDeleteForm{{ r.id }}" class="button is-small is-danger">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="hint">Aucune donnée enregistrée pour l'instant.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="hint">Ajoute d'abord au moins un champ pour pouvoir enregistrer des données.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="dangerZone">
|
||||
<h3>Supprimer cet objet</h3>
|
||||
@@ -531,10 +651,15 @@ function makeFloatPanelResizable(panel, handle) {
|
||||
typeSelect.addEventListener('change', syncAddFieldVisibility);
|
||||
syncAddFieldVisibility();
|
||||
});
|
||||
window.openObjectEditPanel = function (definitionId) {
|
||||
window.openObjectEditPanel = function (definitionId, subtab) {
|
||||
document.querySelectorAll('.objectEditPanel').forEach(function (panel) {
|
||||
panel.classList.toggle('hidden', panel.dataset.definitionId !== String(definitionId));
|
||||
});
|
||||
if (subtab) {
|
||||
var panel = document.querySelector('.objectEditPanel[data-definition-id="' + definitionId + '"]');
|
||||
var btn = panel && panel.querySelector('.objectPanelTabs [data-optab="' + subtab + '"]');
|
||||
if (btn) switchObjectPanelTab(btn, subtab);
|
||||
}
|
||||
};
|
||||
window.closeObjectEditPanel = function (definitionId) {
|
||||
var panel = document.querySelector('.objectEditPanel[data-definition-id="' + definitionId + '"]');
|
||||
@@ -542,12 +667,28 @@ function makeFloatPanelResizable(panel, handle) {
|
||||
};
|
||||
|
||||
// Réouverture automatique après un aller-retour serveur (ajout/édition/
|
||||
// suppression d'un champ, renommage...) : ?edit=<id>, posé par les
|
||||
// suppression d'un champ ou d'une entrée, renommage...) : ?edit=<id>
|
||||
// (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);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user