Show existing fields as a scrollable table on the object-edit page

Replaces the stacked per-field cards with a compact table (one row per field) for a clearer overview when an object has many fields. The page itself no longer scrolls — only the table body and the left column do, mirroring the screen editor's fixed-viewport layout. Each row stays independently editable/deletable via hidden forms targeted with the HTML form= attribute.
This commit is contained in:
william
2026-08-24 10:28:03 +02:00
parent 2bb0c253f9
commit d8a80b8a8a
2 changed files with 99 additions and 51 deletions
+35 -3
View File
@@ -67,14 +67,46 @@ button.danger{ color:var(--danger); border-color:rgba(226,104,95,.4); }
.twoCol{ display:flex; gap:24px; align-items:flex-start; margin-top:10px; } .twoCol{ display:flex; gap:24px; align-items:flex-start; margin-top:10px; }
.twoCol .colLeft{ flex:0 0 340px; } .twoCol .colLeft{ flex:0 0 340px; }
.twoCol .colRight{ flex:1; min-width:0; } .twoCol .colRight{ flex:1; min-width:0; }
.fieldEditCard{ display:flex; flex-direction:column; gap:10px; }
.fieldEditCard input[disabled]{ color:var(--text-dim); opacity:.8; }
.fieldEditActions{ margin-top:6px; }
@media (max-width: 820px){ @media (max-width: 820px){
.twoCol{ flex-direction:column; } .twoCol{ flex-direction:column; }
.twoCol .colLeft{ flex:none; width:100%; } .twoCol .colLeft{ flex:none; width:100%; }
/* En dessous de 820px, les deux colonnes s'empilent : le défilement
interne séparé par colonne n'a plus de sens (il n'y a plus qu'une
colonne) — on revient à une page classique qui défile normalement,
comme pour l'éditeur d'écran ci-dessous. */
body.objectEditBody{ height:auto; overflow:visible; }
body.objectEditBody > main.content{ overflow:visible; }
.content-objectEdit .colLeft, .content-objectEdit .colRight{ height:auto; overflow:visible; }
.fieldsTableWrap{ max-height:60vh; }
} }
/* ---------- Modifier un objet : tableau des champs existants ---------- */
/* Comme l'éditeur d'écran (body.builderBody, plus bas) : la page ne défile
jamais elle-même, seules les zones internes (colonne de gauche, tableau
des champs) défilent chacune de leur côté — indispensable dès qu'un objet
a beaucoup de champs, pour garder une vue d'ensemble sans perdre le
formulaire "Ajouter un champ" hors de l'écran. */
body.objectEditBody{ display:flex; flex-direction:column; height:100vh; margin:0; overflow:hidden; }
body.objectEditBody > .topbar{ flex:0 0 auto; }
body.objectEditBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; display:flex; flex-direction:column; }
.content-objectEdit{ width:100%; display:flex; flex-direction:column; min-height:0; }
.content-objectEdit > h1, .content-objectEdit > .hint, .content-objectEdit > form{ flex:0 0 auto; }
.content-objectEdit .twoCol{ flex:1 1 auto; min-height:0; align-items:stretch; }
.content-objectEdit .colLeft{ overflow-y:auto; height:100%; }
.content-objectEdit .colRight{ display:flex; flex-direction:column; min-height:0; height:100%; }
.content-objectEdit .colRight h2{ flex:0 0 auto; }
.content-objectEdit .colRight > .hint{ flex:0 0 auto; }
.fieldsTableWrap{ flex:1 1 auto; min-height:0; overflow:auto; border:1px solid var(--border); border-radius:8px; }
.fieldsTable{ width:100%; border-collapse:collapse; font-size:12.5px; }
.fieldsTable th, .fieldsTable td{ border-bottom:1px solid var(--border); padding:8px 10px; text-align:left; white-space:nowrap; }
.fieldsTable th{ position:sticky; top:0; background:var(--panel2); color:var(--text-dim); z-index:1; }
.fieldsTable tbody tr:hover{ background:rgba(255,255,255,.02); }
.fieldsTable tbody tr:last-child td{ border-bottom:none; }
.fieldsTable td input, .fieldsTable td select{ min-width:120px; width:auto; }
.fieldsTableActions{ display:flex; gap:6px; }
/* ---------- Éditeur d'écrans (façon Elementor/Bricks) ---------- */ /* ---------- Éditeur d'écrans (façon Elementor/Bricks) ---------- */
/* La page de l'éditeur d'écran ne doit jamais défiler elle-même : elle /* La page de l'éditeur d'écran ne doit jamais défiler elle-même : elle
occupe exactement la hauteur de la fenêtre (topbar + zone de travail), et occupe exactement la hauteur de la fenêtre (topbar + zone de travail), et
+53 -37
View File
@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Modifier {{ definition.name }} — {{ game.name }}{% endblock %} {% block title %}Modifier {{ definition.name }} — {{ game.name }}{% endblock %}
{% block content_class %}content-wide{% endblock %} {% block content_class %}content-wide content-objectEdit{% endblock %}
{% block body_class %}objectEditBody{% endblock %}
{% block breadcrumb %} {% block breadcrumb %}
<a class="crumb" href="{{ url_for('game_dashboard', slug=game.slug) }}">{{ game.name }}</a> <a class="crumb" href="{{ url_for('game_dashboard', slug=game.slug) }}">{{ game.name }}</a>
<span class="crumb-sep">/</span> <span class="crumb-sep">/</span>
@@ -82,54 +83,69 @@
<h2>Champs existants</h2> <h2>Champs existants</h2>
{% if definition.fields %} {% if definition.fields %}
{% for f in definition.fields %} {% for f in definition.fields %}
<div class="card fieldEditCard"> <form method="post" action="{{ url_for('object_field_edit', slug=game.slug, definition_id=definition.id, field_id=f.id) }}" id="fieldEditForm{{ f.id }}"></form>
<form method="post" action="{{ url_for('object_field_edit', slug=game.slug, definition_id=definition.id, field_id=f.id) }}"> <form method="post" action="{{ url_for('object_field_delete', slug=game.slug, definition_id=definition.id, field_id=f.id) }}" id="fieldDeleteForm{{ f.id }}"
<div class="fieldRow"> onsubmit="return confirm('Retirer le champ « {{ f.name }} » ? Les données déjà enregistrées dans ce champ seront perdues.');"></form>
<div> {% endfor %}
<label>Nom du champ</label> <div class="fieldsTableWrap">
<input type="text" name="field_name" value="{{ f.name }}" required> <table class="fieldsTable">
</div> <thead>
<div> <tr>
<label>Type</label> <th>Nom du champ</th>
<input type="text" value="{{ field_types.get(f.type, {}).get('label', f.type) }}" disabled> <th>Type</th>
</div> <th>Objet lié</th>
</div> <th>Mini</th>
<th>Maxi</th>
<th>Obligatoire ?</th>
<th></th>
</tr>
</thead>
<tbody>
{% for f in definition.fields %}
<tr>
<td><input type="text" form="fieldEditForm{{ f.id }}" name="field_name" value="{{ f.name }}" aria-label="Nom du champ « {{ f.name }} »" required></td>
<td>{{ field_types.get(f.type, {}).get('label', f.type) }}</td>
<td>
{% if f.type == 'relation' %} {% if f.type == 'relation' %}
<label>Objet lié</label> <select form="fieldEditForm{{ f.id }}" name="field_relation" aria-label="Objet lié pour « {{ f.name }} »">
<select name="field_relation">
{% for d in existing_definitions %} {% for d in existing_definitions %}
<option value="{{ d.id }}" {{ 'selected' if d.id == f.relation_definition_id else '' }}>{{ d.name }}</option> <option value="{{ d.id }}" {{ 'selected' if d.id == f.relation_definition_id else '' }}>{{ d.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% else %}
<span class="hint">—</span>
{% endif %} {% endif %}
</td>
<td>
{% if f.type in ('nombre_entier', 'nombre_decimal') %} {% if f.type in ('nombre_entier', 'nombre_decimal') %}
<div class="fieldRow"> <input type="number" step="any" form="fieldEditForm{{ f.id }}" name="field_min" value="{{ f.min_value if f.min_value is not none else '' }}" placeholder="ex. 0" aria-label="Valeur mini pour « {{ f.name }} »">
<div> {% else %}
<label>Valeur mini (bornage automatique)</label> <span class="hint">—</span>
<input type="number" step="any" name="field_min" value="{{ f.min_value if f.min_value is not none else '' }}" placeholder="ex. 0">
</div>
<div>
<label>Valeur maxi (bornage automatique)</label>
<input type="number" step="any" name="field_max" value="{{ f.max_value if f.max_value is not none else '' }}" placeholder="ex. 100">
</div>
</div>
<p class="hint">Laisser vide = pas de limite. S'applique à "Donner cette valeur", "Augmenter de..." et "Diminuer de..." dans le graphe de logique.</p>
{% endif %} {% endif %}
<label>Obligatoire ?</label> </td>
<select name="field_required"> <td>
{% if f.type in ('nombre_entier', 'nombre_decimal') %}
<input type="number" step="any" form="fieldEditForm{{ f.id }}" name="field_max" value="{{ f.max_value if f.max_value is not none else '' }}" placeholder="ex. 100" aria-label="Valeur maxi pour « {{ f.name }} »">
{% else %}
<span class="hint">—</span>
{% endif %}
</td>
<td>
<select form="fieldEditForm{{ f.id }}" name="field_required" aria-label="« {{ f.name }} » obligatoire ?">
<option value="0" {{ 'selected' if not f.required else '' }}>Non</option> <option value="0" {{ 'selected' if not f.required else '' }}>Non</option>
<option value="1" {{ 'selected' if f.required else '' }}>Oui</option> <option value="1" {{ 'selected' if f.required else '' }}>Oui</option>
</select> </select>
<div class="actionsRow fieldEditActions"> </td>
<button type="submit">Enregistrer</button> <td class="fieldsTableActions">
</div> <button type="submit" form="fieldEditForm{{ f.id }}" class="smallBtn">Enregistrer</button>
</form> <button type="submit" form="fieldDeleteForm{{ f.id }}" class="smallBtn danger">🗑️</button>
<form method="post" action="{{ url_for('object_field_delete', slug=game.slug, definition_id=definition.id, field_id=f.id) }}" </td>
onsubmit="return confirm('Retirer le champ « {{ f.name }} » ? Les données déjà enregistrées dans ce champ seront perdues.');"> </tr>
<button type="submit" class="smallBtn danger">🗑️ Retirer ce champ</button>
</form>
</div>
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
<p class="hint">Mini/Maxi (bornage automatique) : laisser vide = pas de limite. S'applique à "Donner cette valeur", "Augmenter de..." et "Diminuer de..." dans le graphe de logique.</p>
{% else %} {% else %}
<p class="hint">Aucun champ pour l'instant.</p> <p class="hint">Aucun champ pour l'instant.</p>
{% endif %} {% endif %}