Filtre de colonnes + panneau de détail d'une entrée (onglet Données)
Un objet avec beaucoup de champs rendait le tableau "Données enregistrées" illisible (toutes les colonnes serrées sur une ligne, valeurs tronquées) — deux ajouts pour compenser : 1) "🔧 Colonnes" : menu déroulant (une case à cocher par champ, toutes cochées par défaut) au-dessus du tableau — décocher un champ masque sa colonne (th + td, via [data-col]) sans reconstruire le tableau. Un seul menu ouvert à la fois, fermé au clic ailleurs sur la page. 2) 👁️ par ligne : ouvre un panneau de détail (déplaçable/redimensionnable/ plein écran par défaut, comme les autres) listant TOUS les champs de cette entrée, un par ligne, valeur complète non tronquée — reprend le style .rowDetailField/.rowDetailLabel/.rowDetailValue de l'ancienne modale Bulma de data_list.html (retirée, mais ce CSS ne dépendait pas du template et a été gardé). Un seul panneau PAR OBJET (pas par ligne) : son contenu est reconstruit à l'ouverture en lisant en direct la ligne du tableau déjà affiché (via [data-row-id] sur chaque <tr>) — reflète donc aussi une modification tout juste saisie mais pas encore "Enregistrer"ée, sans aller-retour serveur. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
943bf265de
commit
b21f56e364
+19
-2
@@ -78,8 +78,9 @@ button.danger{ color:var(--danger); border-color:rgba(226,104,95,.4); }
|
|||||||
.dataTable td{ max-width:220px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
.dataTable td{ max-width:220px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||||
|
|
||||||
/* ---------- Détail d'une entrée (bouton 👁️) ---------- */
|
/* ---------- Détail d'une entrée (bouton 👁️) ---------- */
|
||||||
/* Contenu de la modale Bulma (.modal/.modal-card, voir data_list.html) —
|
/* Contenu du panneau de détail (voir openRowDetailPanel() dans
|
||||||
affiche chaque champ en entier, sans troncature. */
|
game_dashboard.html) — affiche chaque champ en entier, sans troncature,
|
||||||
|
contrairement au tableau dense qui les tronque. */
|
||||||
.rowDetailField{ padding:10px 0; border-bottom:1px solid var(--border); }
|
.rowDetailField{ padding:10px 0; border-bottom:1px solid var(--border); }
|
||||||
.rowDetailField:last-child{ border-bottom:none; }
|
.rowDetailField:last-child{ border-bottom:none; }
|
||||||
.rowDetailLabel{ font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; margin-bottom:4px; }
|
.rowDetailLabel{ font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; margin-bottom:4px; }
|
||||||
@@ -535,3 +536,19 @@ code{ background:var(--panel2); border:1px solid var(--border); border-radius:4p
|
|||||||
.dashCreateBar{ display:flex; gap:8px; align-items:center; margin-bottom:12px; padding:10px; border:1px solid var(--border); border-radius:8px; background:var(--panel); }
|
.dashCreateBar{ display:flex; gap:8px; align-items:center; margin-bottom:12px; padding:10px; border:1px solid var(--border); border-radius:8px; background:var(--panel); }
|
||||||
.dashCreateBar input, .dashCreateBar select{ width:auto; flex:1 1 160px; }
|
.dashCreateBar input, .dashCreateBar select{ width:auto; flex:1 1 160px; }
|
||||||
.dashDangerZone{ margin-top:28px; }
|
.dashDangerZone{ margin-top:28px; }
|
||||||
|
|
||||||
|
/* ---------- Filtre de colonnes (onglet Données, game_dashboard.html) ---------- */
|
||||||
|
/* Menu déroulant compact sous le bouton "🔧 Colonnes" — un objet avec
|
||||||
|
beaucoup de champs rend le tableau illisible, ce menu permet d'en
|
||||||
|
masquer certains (voir toggleDataColumn() : agit directement sur
|
||||||
|
[data-col] plutôt que de reconstruire le tableau). */
|
||||||
|
.columnFilterWrap{ position:relative; display:inline-block; }
|
||||||
|
.columnFilterMenu{
|
||||||
|
position:absolute; top:100%; left:0; margin-top:4px; z-index:70;
|
||||||
|
background:var(--panel); border:1px solid var(--border); border-radius:8px;
|
||||||
|
padding:8px; box-shadow:0 12px 40px rgba(0,0,0,.55);
|
||||||
|
display:flex; flex-direction:column; gap:2px; min-width:180px; max-height:280px; overflow-y:auto;
|
||||||
|
}
|
||||||
|
.columnFilterMenu.hidden{ display:none; }
|
||||||
|
.columnFilterOption{ display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:4px; font-size:12.5px; white-space:nowrap; }
|
||||||
|
.columnFilterOption:hover{ background:var(--panel2); }
|
||||||
|
|||||||
@@ -376,10 +376,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="dashPanelHeader">
|
<div class="dashPanelHeader">
|
||||||
<h2>Données enregistrées</h2>
|
<h2>Données enregistrées</h2>
|
||||||
|
<div style="display:flex; gap:6px;">
|
||||||
{% if definition.fields %}
|
{% if definition.fields %}
|
||||||
|
<div class="columnFilterWrap">
|
||||||
|
<button type="button" class="button is-small" onclick="toggleColumnFilterMenu({{ d.id }})">🔧 Colonnes</button>
|
||||||
|
<div class="columnFilterMenu hidden" id="columnFilterMenu-{{ d.id }}">
|
||||||
|
{% for f in definition.fields %}
|
||||||
|
<label class="checkbox columnFilterOption"><input type="checkbox" checked data-col="field-{{ f.id }}" onchange="toggleDataColumn({{ d.id }}, 'field-{{ f.id }}', this.checked)"> {{ f.name }}</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button type="button" class="button is-small primary" onclick="openAddEntryPanel({{ d.id }})">+ Ajouter</button>
|
<button type="button" class="button is-small primary" onclick="openAddEntryPanel({{ d.id }})">+ Ajouter</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% if definition.fields %}
|
{% if definition.fields %}
|
||||||
{% set rows = rows_by_definition[d.id] %}
|
{% set rows = rows_by_definition[d.id] %}
|
||||||
{% if rows %}
|
{% if rows %}
|
||||||
@@ -389,17 +399,17 @@
|
|||||||
onsubmit="return confirm('Supprimer définitivement l\'entrée #{{ r.id }} ?');"></form>
|
onsubmit="return confirm('Supprimer définitivement l\'entrée #{{ r.id }} ?');"></form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="fieldsTableWrap">
|
<div class="fieldsTableWrap">
|
||||||
<table class="table is-fullwidth is-hoverable dataTable fieldsTable">
|
<table class="table is-fullwidth is-hoverable dataTable fieldsTable" id="dataTable-{{ d.id }}">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>#</th>{% for f in definition.fields %}<th>{{ f.name }}</th>{% endfor %}<th></th></tr>
|
<tr><th>#</th>{% for f in definition.fields %}<th data-col="field-{{ f.id }}">{{ f.name }}</th>{% endfor %}<th></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for r in rows %}
|
{% for r in rows %}
|
||||||
<tr>
|
<tr data-row-id="{{ r.id }}">
|
||||||
<td>{{ r.id }}</td>
|
<td>{{ r.id }}</td>
|
||||||
{% for f in definition.fields %}
|
{% for f in definition.fields %}
|
||||||
{% set col = f.name|colname %}
|
{% set col = f.name|colname %}
|
||||||
<td>
|
<td data-col="field-{{ f.id }}" data-field-label="{{ f.name }}">
|
||||||
{% if f.type == 'texte' %}
|
{% if f.type == 'texte' %}
|
||||||
<input class="input is-small" type="text" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
|
<input class="input is-small" type="text" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
|
||||||
{% elif f.type == 'texte_long' %}
|
{% elif f.type == 'texte_long' %}
|
||||||
@@ -423,6 +433,7 @@
|
|||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<td class="fieldsTableActions">
|
<td class="fieldsTableActions">
|
||||||
|
<button type="button" class="button is-small" title="Voir cette entrée en détail" onclick="openRowDetailPanel({{ d.id }}, {{ r.id }})">👁️</button>
|
||||||
<button type="submit" form="dataEditForm{{ r.id }}" class="button is-small">Enregistrer</button>
|
<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>
|
<button type="submit" form="dataDeleteForm{{ r.id }}" class="button is-small is-danger">🗑️</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -551,6 +562,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="floatPanelResizeHandle"></div>
|
<div class="floatPanelResizeHandle"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panneau de détail d'UNE entrée (voir openRowDetailPanel()) — un seul
|
||||||
|
panneau PAR OBJET (pas un par ligne, pour ne jamais alourdir la page
|
||||||
|
même avec beaucoup d'entrées) : son contenu est reconstruit à
|
||||||
|
l'ouverture en lisant directement la ligne correspondante du tableau
|
||||||
|
"Données enregistrées" (valeurs à jour, y compris une modification
|
||||||
|
pas encore enregistrée). Champ par ligne, étiquette bien visible —
|
||||||
|
pensé pour un objet avec beaucoup de colonnes, illisible en tableau
|
||||||
|
serré (voir la barre "🔧 Colonnes" pour l'inverse : cacher des
|
||||||
|
colonnes plutôt que tout regarder en détail). -->
|
||||||
|
<div class="floatPanel floatPanel--center hidden rowDetailPanel" data-definition-id="{{ d.id }}" id="rowDetailPanel-{{ d.id }}">
|
||||||
|
<div class="floatPanelHeader">
|
||||||
|
<span class="floatPanelTitle rowDetailPanelTitle">⋮⋮ Entrée — {{ definition.name }}</span>
|
||||||
|
<button type="button" class="button is-small floatPanelFullscreenBtn" title="Plein écran">⛶</button>
|
||||||
|
<button type="button" class="button is-small" onclick="closeRowDetailPanel({{ d.id }})">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="floatPanelBody rowDetailPanelBody"></div>
|
||||||
|
<div class="floatPanelResizeHandle"></div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
@@ -814,6 +844,85 @@ function makeFloatPanelFullscreenable(panel, toggleBtn) {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// ---------- Filtre de colonnes (onglet Données) ----------
|
||||||
|
function toggleColumnFilterMenu(definitionId) {
|
||||||
|
var menu = document.getElementById('columnFilterMenu-' + definitionId);
|
||||||
|
if (!menu) return;
|
||||||
|
var opening = menu.classList.contains('hidden');
|
||||||
|
// Un seul menu de filtre ouvert à la fois, tous objets confondus — évite
|
||||||
|
// d'en laisser traîner un ouvert par-dessus le panneau d'un autre objet.
|
||||||
|
document.querySelectorAll('.columnFilterMenu').forEach(function (m) { m.classList.add('hidden'); });
|
||||||
|
if (opening) menu.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (e.target.closest('.columnFilterWrap')) return;
|
||||||
|
document.querySelectorAll('.columnFilterMenu').forEach(function (m) { m.classList.add('hidden'); });
|
||||||
|
});
|
||||||
|
function toggleDataColumn(definitionId, colKey, visible) {
|
||||||
|
var table = document.getElementById('dataTable-' + definitionId);
|
||||||
|
if (!table) return;
|
||||||
|
table.querySelectorAll('[data-col="' + colKey + '"]').forEach(function (cell) {
|
||||||
|
cell.style.display = visible ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Panneau de détail d'une entrée (un par objet) ----------
|
||||||
|
(function () {
|
||||||
|
document.querySelectorAll('.rowDetailPanel').forEach(function (panel) {
|
||||||
|
makeFloatPanelDraggable(panel, panel.querySelector('.floatPanelHeader'));
|
||||||
|
makeFloatPanelResizable(panel, panel.querySelector('.floatPanelResizeHandle'));
|
||||||
|
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reconstruit le contenu du panneau à chaque ouverture, en lisant
|
||||||
|
// directement la ligne correspondante du tableau "Données enregistrées"
|
||||||
|
// déjà affiché — pas d'aller-retour serveur, et ça reflète une
|
||||||
|
// modification tout juste saisie mais pas encore "Enregistrer"ée.
|
||||||
|
window.openRowDetailPanel = function (definitionId, rowId) {
|
||||||
|
var panel = document.querySelector('.rowDetailPanel[data-definition-id="' + definitionId + '"]');
|
||||||
|
var table = document.getElementById('dataTable-' + definitionId);
|
||||||
|
if (!panel || !table) return;
|
||||||
|
var row = table.querySelector('tr[data-row-id="' + rowId + '"]');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
var body = panel.querySelector('.rowDetailPanelBody');
|
||||||
|
body.innerHTML = '';
|
||||||
|
var idField = document.createElement('div');
|
||||||
|
idField.className = 'rowDetailField';
|
||||||
|
idField.innerHTML = '<div class="rowDetailLabel">#</div><div class="rowDetailValue">' + rowId + '</div>';
|
||||||
|
body.appendChild(idField);
|
||||||
|
|
||||||
|
row.querySelectorAll('td[data-field-label]').forEach(function (cell) {
|
||||||
|
var label = cell.dataset.fieldLabel;
|
||||||
|
var control = cell.querySelector('input, textarea, select');
|
||||||
|
var value = '—';
|
||||||
|
if (control) {
|
||||||
|
if (control.type === 'checkbox') value = control.checked ? 'Oui' : 'Non';
|
||||||
|
else if (control.tagName === 'SELECT') value = control.options[control.selectedIndex] ? control.options[control.selectedIndex].text : '—';
|
||||||
|
else value = control.value || '—';
|
||||||
|
}
|
||||||
|
var field = document.createElement('div');
|
||||||
|
field.className = 'rowDetailField';
|
||||||
|
var labelEl = document.createElement('div');
|
||||||
|
labelEl.className = 'rowDetailLabel';
|
||||||
|
labelEl.textContent = label;
|
||||||
|
var valueEl = document.createElement('div');
|
||||||
|
valueEl.className = 'rowDetailValue';
|
||||||
|
valueEl.textContent = value;
|
||||||
|
field.appendChild(labelEl);
|
||||||
|
field.appendChild(valueEl);
|
||||||
|
body.appendChild(field);
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.classList.remove('hidden');
|
||||||
|
panel._fsToggle.enter();
|
||||||
|
};
|
||||||
|
window.closeRowDetailPanel = function (definitionId) {
|
||||||
|
var panel = document.querySelector('.rowDetailPanel[data-definition-id="' + definitionId + '"]');
|
||||||
|
if (panel) panel.classList.add('hidden');
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
// Sous-onglets Champs/Données d'UN panneau précis (voir plus haut) — scopé
|
// 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
|
// au panneau ancêtre du bouton cliqué, jamais global (plusieurs objets
|
||||||
// peuvent chacun avoir leurs sous-onglets sur leur propre panneau, en même
|
// peuvent chacun avoir leurs sous-onglets sur leur propre panneau, en même
|
||||||
|
|||||||
Reference in New Issue
Block a user