runique 2.1.21

A Django-inspired web framework for Rust with ORM, templates, and comprehensive security middleware
Documentation
{# Partial HTMX — contenu swapé dans #list-content lors des navigations liste #}
{# Variables requises : identiques à admin/list #}
{% set_global can_create = false %}
{% if current_user %}
  {% if current_user.is_superuser %}
    {% set_global can_create = true %}
  {% else %}
    {% for groupe in current_user.groupes %}
      {% for perm in groupe.permissions %}
        {% if perm.resource_key == resource.key and perm.can_create %}
          {% set_global can_create = true %}
        {% endif %}
      {% endfor %}
    {% endfor %}
  {% endif %}
{% endif %}
{% set has_filters = resource.display.list_filter | length > 0 %}
<div id="list-content">

  {# ── Barre de recherche ── #}
  {% block list_search %}
  <div class="admin-search__row">
    <form method="GET" action="" class="admin-search__form"

          hx-get=""

          hx-target="#list-content"

          hx-swap="innerHTML"

          hx-push-url="true">
      <div class="admin-search__wrapper">
        <input

          type="text"

          name="search"

          value="{{ search }}"

          placeholder="{% if admin_list_search_placeholder %}{{ admin_list_search_placeholder }}{% else %}Search...{% endif %}"

          class="admin-search__input"

          hx-get=""

          hx-trigger="input changed delay:400ms"

          hx-target="#list-content"

          hx-swap="innerHTML"

          hx-push-url="true"

          hx-include="closest form"

        >
        {% if sort_by %}<input type="hidden" name="sort_by" value="{{ sort_by }}">{% endif %}
        {% if sort_dir %}<input type="hidden" name="sort_dir" value="{{ sort_dir }}">{% endif %}
        {% for col, val in active_filters %}<input type="hidden" name="filter_{{ col }}" value="{{ val }}">{% endfor %}
        <button type="submit" class="btn btn-secondary admin-search__btn">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
            <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
          </svg>
        </button>
        {% if search %}
        <a href="?{% if sort_by %}sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{{ filter_qs }}"

            hx-get="?{% if sort_by %}sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{{ filter_qs }}"

            hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

            class="btn btn-secondary">✕</a>
        {% endif %}
      </div>
    </form>
    {% if has_filters %}
    <button class="btn btn-ghost btn-icon admin-search__filter-toggle" id="mobileFilterToggle" type="button" aria-label="Filtres">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
        <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="9" y2="18"/>
      </svg>
      {% if active_filters %}<span class="admin-search__filter-badge">{{ active_filters | length }}</span>{% endif %}
    </button>
    {% endif %}
  </div>
  {% endblock list_search %}

  {# ── Barre d'actions bulk ── #}
  {% set_global can_bulk = false %}
  {% if current_user %}
    {% if current_user.is_superuser %}
      {% set_global can_bulk = true %}
    {% else %}
      {% for groupe in current_user.groupes %}
        {% for perm in groupe.permissions %}
          {% if perm.resource_key == resource.key and perm.can_delete %}
            {% set_global can_bulk = true %}
          {% endif %}
        {% endfor %}
      {% endfor %}
    {% endif %}
  {% endif %}

  {% block list_group_action %}
  {% if can_bulk %}
  <form id="bulk-form" method="POST" action="{{ resource_base }}/bulk">
    {% csrf %}
    <input type="hidden" name="bulk_action" id="bulk-action-input" value="">
    <input type="hidden" name="ids" id="bulk-ids-input" value="">
    <div id="bulk-action-bar" class="admin-group-action__bar">
      <span class="admin-group-action__info"><span id="bulk-count">0</span> sélectionné(s)</span>
      <div class="admin-group-action__btns">
        {% if group_actions | length > 0 %}
        <div class="admin-group-action__selects">
          {% for ga in group_actions %}
          <select name="ga_{{ ga.field }}" class="admin-group-action__select">
            <option value="">{{ ga.label }} — sans changement</option>
            {% for choice in ga.choices %}
            <option value="{{ choice[0] }}">{{ ga.label }} — {{ choice[1] }}</option>
            {% endfor %}
          </select>
          {% endfor %}
          <button type="button" class="btn btn-sm btn-primary" data-bulk-action="group_set">Modifier</button>
        </div>
        {% endif %}
        {% if can_update %}
        <button type="button" class="btn btn-sm btn-secondary" data-bulk-action="bulk-edit">Modifier en groupe</button>
        {% endif %}
        <button type="button" class="btn btn-sm btn-danger" data-bulk-action="delete">Supprimer</button>
      </div>
    </div>
  </form>
  {% endif %}
  {% endblock list_group_action %}

  <div class="{% if has_filters %}admin-list__layout{% endif %}">

    {# ── Colonne principale : table + pagination ── #}
    <div>

      {% block list_table %}
      <div class="admin-card">
        {% if entries is defined and entries | length > 0 %}
          <div class="admin-table__wrapper">
            <table class="admin-table">
              <colgroup>
                {% if can_bulk %}<col class="admin-table__col-bulk">{% endif %}
                <col class="admin-table__col-id">
                {% for col in visible_columns %}
                  <col{% if loop.index0 > 0 %} class="admin-table__col-secondary"{% endif %}>
                {% endfor %}
                <col class="admin-table__col-expand">
                <col class="admin-table__col-actions">
              </colgroup>
              <thead>
                <tr>
                  {% if can_bulk %}<th class="admin-table__th-bulk"><input type="checkbox" id="bulk-check-all" title="Tout sélectionner"></th>{% endif %}
                  <th>
                    <a href="?sort_by=id&sort_dir={% if sort_by == "id" %}{{ sort_dir_toggle }}{% else %}asc{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}&page=1"

                      hx-get="?sort_by=id&sort_dir={% if sort_by == "id" %}{{ sort_dir_toggle }}{% else %}asc{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}&page=1"

                      hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

                      class="admin-table__sort-link">
                      {% if admin_list_th_id %}{{ admin_list_th_id }}{% endif %}
                      {% if sort_by == "id" %}<span class="admin-table__sort-indicator">{% if sort_dir == "asc" %}▲{% else %}▼{% endif %}</span>{% endif %}
                    </a>
                  </th>
                  {% for col in visible_columns %}
                    <th {% if loop.index0 > 0 %} class="admin-table__col-secondary"{% endif %}>
                      <a href="?sort_by={{ col }}&sort_dir={% if sort_by == col %}{{ sort_dir_toggle }}{% else %}asc{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}&page=1"

                        hx-get="?sort_by={{ col }}&sort_dir={% if sort_by == col %}{{ sort_dir_toggle }}{% else %}asc{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}&page=1"

                        hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

                        class="admin-table__sort-link">
                        {% if column_labels[col] %}{{ column_labels[col] }}{% else %}{{ col | replace(from="_", to=" ") }}{% endif %}
                        {% if sort_by == col %}<span class="admin-table__sort-indicator">{% if sort_dir == "asc" %}▲{% else %}▼{% endif %}</span>{% endif %}
                      </a>
                    </th>
                  {% endfor %}
                  <th class="admin-table__th-expand"></th>
                  <th class="admin-table__th-right">{% if admin_list_th_actions %}{{ admin_list_th_actions }}{% endif %}</th>
                </tr>
              </thead>
              <tbody>
                {% for entry in entries %}
                  <tr>
                    {% if can_bulk %}<td class="admin-table__td-bulk"><input type="checkbox" class="admin-table__bulk-check" value="{{ entry.id }}"></td>{% endif %}
                    <td><span class="admin-badge--id">#{{ entry.id }}</span></td>
                    {% for col in visible_columns %}
                      {% set value = entry[col] %}
                      <td class="admin-table__td-data{% if loop.index0 > 0 %} admin-table__col-secondary{% endif %}" data-label="{% if column_labels[col] %}{{ column_labels[col] }}{% else %}{{ col | replace(from="_", to=" ") }}{% endif %}">
                        {% if value == true %}
                          <span class="admin-badge admin-badge--green">{% if admin_list_bool_true %}{{ admin_list_bool_true }}{% endif %}</span>
                        {% elif value == false %}
                          <span class="admin-badge admin-badge--neutral">{% if admin_list_bool_false %}{{ admin_list_bool_false }}{% endif %}</span>
                        {% elif value %}
                          <span class="admin-table__td-content">{% if rich_fields and col in rich_fields %}{{ value | plaintext }}{% else %}{{ value }}{% endif %}</span>
                        {% else %}
                          <span class="admin-text--muted">—</span>
                        {% endif %}
                      </td>
                    {% endfor %}
                    <td class="admin-table__td-expand">
                      <button class="admin-table__expand-btn" type="button" aria-expanded="false" aria-label="Détails">
                        <svg class="admin-table__expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
                          <polyline points="9 18 15 12 9 6"/>
                        </svg>
                      </button>
                    </td>
                    <td class="admin-table__td-actions">
                      {% set resource_key = resource.key %}
                      {% set entry_id = entry.id %}
                      {% include "admin/kebab.html" %}
                    </td>
                  </tr>
                  <tr class="admin-table__row-detail">
                    <td colspan="999">
                      <div class="admin-table__detail-grid">
                        {% for col in visible_columns %}
                          {% if loop.index0 > 0 %}
                            {% set value = entry[col] %}
                            <div class="admin-table__detail-item">
                              <span class="admin-table__detail-label">{% if column_labels[col] %}{{ column_labels[col] }}{% else %}{{ col | replace(from="_", to=" ") }}{% endif %}</span>
                              <span class="admin-table__detail-value">
                                {% if value == true %}<span class="admin-badge admin-badge--green">{% if admin_list_bool_true %}{{ admin_list_bool_true }}{% endif %}</span>
                                {% elif value == false %}<span class="admin-badge admin-badge--neutral">{% if admin_list_bool_false %}{{ admin_list_bool_false }}{% endif %}</span>
                                {% elif value %}{% if rich_fields and col in rich_fields %}{{ value | plaintext }}{% else %}{{ value }}{% endif %}
                                {% else %}<span class="admin-text--muted">—</span>{% endif %}
                              </span>
                            </div>
                          {% endif %}
                        {% endfor %}
                      </div>
                    </td>
                  </tr>
                {% endfor %}
              </tbody>
            </table>
          </div>
        {% else %}
          <div class="admin-empty-state">
            <svg class="admin-empty-state__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
              <path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/>
              <rect x="9" y="3" width="6" height="4" rx="1"/>
            </svg>
            <p class="admin-empty-state__title">{% if admin_list_empty_title %}{{ admin_list_empty_title }}{% endif %}</p>
            <p class="admin-empty-state__desc">{% if admin_list_empty_desc %}{{ admin_list_empty_desc }}{% endif %}</p>
            {% if can_create %}<a href="{{ resource_base }}/create" class="btn btn-primary mt-sm">{% if admin_list_btn_create_first %}{{ admin_list_btn_create_first }}{% endif %}</a>{% endif %}
          </div>
        {% endif %}
      </div>
      {% endblock list_table %}

      {% block list_pagination %}
      {% if page_count is defined and page_count > 1 %}
      <div class="admin-pagination">
        {% if has_prev %}
          <a href="?page={{ prev_page }}{% if sort_by %}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}"

            hx-get="?page={{ prev_page }}{% if sort_by %}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}"

            hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

            class="btn btn-sm btn-secondary">&laquo;</a>
        {% else %}
          <span class="btn btn-sm btn-secondary disabled">&laquo;</span>
        {% endif %}
        <span class="admin-pagination__info">{{ page }} / {{ page_count }}</span>
        {% if has_next %}
          <a href="?page={{ next_page }}{% if sort_by %}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}"

            hx-get="?page={{ next_page }}{% if sort_by %}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}{{ filter_qs }}"

            hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

            class="btn btn-sm btn-secondary">&raquo;</a>
        {% else %}
          <span class="btn btn-sm btn-secondary disabled">&raquo;</span>
        {% endif %}
      </div>
      {% endif %}
      {% endblock list_pagination %}

    </div>

    {# ── Sidebar filtres ── #}
    {% block list_filters %}
    {% if has_filters %}
    <div class="admin-filter__overlay" id="filterOverlay"></div>
    <aside class="admin-filter__sidebar" id="filterSidebar" data-resource="{{ resource.key }}">
      <div class="admin-filter__header">
        <span class="admin-filter__title">Filtres</span>
        <button class="admin-filter__toggle" id="filterToggle" title="Masquer les filtres">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
            <polyline points="9 18 15 12 9 6"/>
          </svg>
        </button>
      </div>
      <div class="admin-filter__body">
        {% 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 and values | length > 0 %}
          <div class="admin-filter__group" data-filter-col="{{ col }}">
            <button class="admin-filter__group-title" type="button" aria-expanded="true">
              {{ label }}
              <svg class="admin-filter__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12">
                <polyline points="6 9 12 15 18 9"/>
              </svg>
            </button>
            <div class="admin-filter__group-body">
              {% if active_filters[col] %}
              <a href="?{% for k, v in active_filters %}{% if k != col and v %}filter_{{ k }}={{ v | urlencode }}&{% endif %}{% endfor %}{% if sort_by %}sort_by={{ sort_by }}&sort_dir={{ sort_dir }}&{% endif %}{% if search %}search={{ search | urlencode }}&{% endif %}page=1"

                hx-get="?{% for k, v in active_filters %}{% if k != col and v %}filter_{{ k }}={{ v | urlencode }}&{% endif %}{% endfor %}{% if sort_by %}sort_by={{ sort_by }}&sort_dir={{ sort_dir }}&{% endif %}{% if search %}search={{ search | urlencode }}&{% endif %}page=1"

                hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

                class="admin-filter__option admin-filter__option--clear">✕ Tout</a>
              {% endif %}
              {% for val in values %}
                {% set is_active = active_filters[col] == val %}
                <a href="?filter_{{ col }}={{ val | urlencode }}{% for k, v in active_filters %}{% if k != col and v %}&filter_{{ k }}={{ v | urlencode }}{% endif %}{% endfor %}{% if sort_by %}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}&page=1"

                  hx-get="?filter_{{ col }}={{ val | urlencode }}{% for k, v in active_filters %}{% if k != col and v %}&filter_{{ k }}={{ v | urlencode }}{% endif %}{% endfor %}{% if sort_by %}&sort_by={{ sort_by }}&sort_dir={{ sort_dir }}{% endif %}{% if search %}&search={{ search | urlencode }}{% endif %}&page=1"

                  hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

                  class="admin-filter__option {% if is_active %}admin-filter__option--active{% endif %}">
                  {{ val | replace(from="_", to=" ") }}
                </a>
              {% endfor %}
              {% if meta.total_pages > 1 %}
              <div class="admin-filter__pagination">
                {% if meta.has_prev %}
                  <a href="?{{ meta.prev_qs }}&page=1"

                    hx-get="?{{ meta.prev_qs }}&page=1"

                    hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

                    class="admin-filter__page-btn">&#8249;</a>
                {% else %}
                  <span class="admin-filter__page-btn disabled">&#8249;</span>
                {% endif %}
                <span class="admin-filter__page-info">{{ meta.current_page + 1 }} / {{ meta.total_pages }}</span>
                {% if meta.has_next %}
                  <a href="?{{ meta.next_qs }}&page=1"

                    hx-get="?{{ meta.next_qs }}&page=1"

                    hx-target="#list-content" hx-swap="innerHTML" hx-push-url="true"

                    class="admin-filter__page-btn">&#8250;</a>
                {% else %}
                  <span class="admin-filter__page-btn disabled">&#8250;</span>
                {% endif %}
              </div>
              {% endif %}
            </div>
          </div>
          {% endif %}
        {% endfor %}
      </div>
    </aside>
    {% endif %}
    {% endblock list_filters %}

  </div>{# fin admin-list__layout #}
</div>