Filtre admin

Filtre admin

Vue d'ensemble

Le système de filtres affiche dans une sidebar les valeurs distinctes d'une colonne, et filtre la liste en cliquant dessus. Tout est SQL — aucune donnée en mémoire.

Déclaration (admin.rs)
      ↓
    Parser      → lit la macro admin!
      ↓
    Générateur  → produit admin_panel.rs
      ↓
    Handler     → orchestre les requêtes
      ↓
    Template    → rend la sidebar

Étape 1 — Déclarer un filtre

Dans src/admin.rs, le dev déclare les colonnes à filtrer :

list_filter: [
    ["lang", "Langue"],          // défaut : 10 valeurs par page
    ["block_type", "Type", 5],   // limite explicite : 5 valeurs par page
]

Chaque entrée = ["colonne", "Libellé"] ou ["colonne", "Libellé", limite].


Étape 2 — Le parser

Le daemon (runique start) lit src/admin.rs token par token et construit une structure intermédiaire :

pub struct ResourceDef {
    pub list_filter: Vec<(String, String, u64)>,
    //                    col    label  limit
}

Le 3ème élément est optionnel — si absent, la limite par défaut est 10.


Étape 3 — Le générateur

À partir de la structure, le daemon génère deux blocs dans admin_panel.rs.

La configuration d'affichage

let meta = meta.display(
    DisplayConfig::new()
        .list_filter(vec![
            ("lang",       "Langue", 10u64),
            ("block_type", "Type",    5u64),
        ])
);

Cette config est sérialisée et accessible dans Tera via resource.display.list_filter.

La closure de filtres

Pour chaque colonne, deux requêtes SQL sont générées :

Comptage — pour savoir combien de pages existent :

SELECT COUNT(DISTINCT lang) FROM doc_page WHERE lang IS NOT NULL

Valeurs paginées — seulement ce qui est affiché :

SELECT DISTINCT CAST(lang AS TEXT)
FROM doc_page
WHERE lang IS NOT NULL
ORDER BY lang ASC
LIMIT 10 OFFSET 20   -- page 2 × 10

CAST(... AS TEXT) uniformise le type : booléens, entiers et chaînes passent tous par là.

Le résultat est un HashMap<String, (Vec<String>, u64)> : chaque colonne → ses valeurs + son total distinct.


Étape 4 — Le handler

Dans admin_main.rs, deux séries de paramètres URL sont parsées :

filter_lang=fr    → filtre actif sur la colonne lang
fp_lang=2         → page 2 dans la sidebar du groupe lang

Les trois requêtes tournent en parallèle grâce à tokio::join! :

tokio::join!(
    list_fn(db, list_params),      // entrées de la table
    count_fn(db, search),          // total pour la pagination principale
    filter_fn(db, filter_pages),   // valeurs distinctes par colonne
)

Étape 5 — La pagination des filtres

Plutôt que charger 100 valeurs en JS, on pagine côté serveur.

Pour chaque colonne, le handler calcule filter_meta :

current_page  → page affichée actuellement
total_pages   → nombre total de pages
prev_qs       → query string complet du lien "page précédente"
next_qs       → query string complet du lien "page suivante"

prev_qs et next_qs sont précalculés en Rust car Tera ne peut pas construire des query strings complexes. Le template n'a plus qu'à écrire :

<a href="?{{ meta.prev_qs }}&page=1">‹</a>
<a href="?{{ meta.next_qs }}&page=1">›</a>

Ces liens préservent automatiquement le tri, la recherche et les autres filtres actifs.


Étape 6 — Le template

La sidebar s'affiche uniquement si list_filter est non vide :

{% for filter_entry in resource.display.list_filter %}
  {% set col    = filter_entry[0] %}
  {% set label  = filter_entry[1] %}
  {% set values = filter_values[col] %}
  {% set meta   = filter_meta[col] %}

  {% if values | length > 0 %}
    <!-- groupe visible -->
  {% endif %}
{% endfor %}

Chaque valeur est un lien qui ajoute filter_{col}={val} à l'URL. Cliquer applique un WHERE col = val côté SQL.


Étape 7 — Repli par groupe

Chaque groupe peut être replié individuellement. L'état est sauvegardé dans localStorage par ressource + colonne :

clé : runique_fg_doc_page_lang
      → '1' = ouvert
      → '0' = replié

Au chargement, chaque groupe restaure son état. Au clic sur le titre, le corps est caché/montré et l'état est sauvegardé.


Étape 8 — Diagnostic

Si une colonne n'existe pas en base, le code généré log une erreur :

ERROR [runique admin] list_filter `doc_block.lang` :
      colonne introuvable en DB — column "lang" does not exist

Le dev voit immédiatement quelle colonne dans list_filter est invalide.


Résumé des choix de conception

⚡ Runique exercise Beta
Say hello to start an exercise for this course.