rustio-admin 0.18.4

Django Admin, but for Rust. A small, focused admin framework.
Documentation
{% extends "admin/_base.html" %}
{% block content %}
<header class="rio-page-header">
  <nav class="rio-breadcrumbs"><a href="/admin">Home</a> · <span>{{ display_name }}</span></nav>
  <div class="rio-page-actions">
    <h1>{{ display_name }}</h1>
    {% if not read_only %}
    <a class="rio-button rio-button--primary" href="/admin/{{ admin_name }}/new">{{ icon("plus", class="rio-icon") }} Add {{ singular_name }}</a>
    {% endif %}
  </div>
</header>

<div class="rio-toolbar">
  {# The search form carries hidden inputs for every active filter +
   # the current sort, so submitting a query keeps the rest of the
   # list-view state intact. Page intentionally resets to 1 — a new
   # query against page N rarely makes sense. #}
  <form method="get" action="/admin/{{ admin_name }}" class="rio-search-bar" role="search">
    <span class="rio-search-input-wrap">
      {{ icon("search", class="rio-search-icon") }}
      <input type="search" name="q" value="{{ search_query }}" placeholder="Search…" class="rio-search-input" aria-label="Search">
    </span>
    {% for pair in active_filter_pairs %}
    <input type="hidden" name="{{ pair[0] }}" value="{{ pair[1] }}">
    {% endfor %}
    {% if active_sort_field %}
    <input type="hidden" name="sort" value="{{ active_sort_field }}">
    <input type="hidden" name="dir" value="{{ active_sort_dir }}">
    {% endif %}
    {% if active_per_page_override %}
    <input type="hidden" name="per_page" value="{{ active_per_page_override }}">
    {% endif %}
    <button type="submit" class="rio-button">Search</button>
  </form>

  {% if filters %}
  <div class="rio-dropdown" data-rio-dropdown>
    <button type="button" class="rio-dropdown-toggle" aria-haspopup="true" aria-expanded="false">
      {{ icon("filter", class="rio-icon") }}
      Filters
      {% if active_filter_count > 0 %}
      <span class="rio-dropdown-badge" aria-label="{{ active_filter_count }} active filter">{{ active_filter_count }}</span>
      {% endif %}
      {{ icon("chevron-down", class="rio-chev") }}
    </button>
    <div class="rio-dropdown-panel" role="dialog" aria-label="Filters">
      {% for group in filters %}
      <div class="rio-dropdown-section">
        <span class="rio-dropdown-label">{{ group.label }}</span>
        {% if group.kind == "fk_autocomplete" %}
        <form method="get" action="/admin/{{ admin_name }}" class="rio-fk-autocomplete"
              data-rio-fk-autocomplete
              data-rio-fk-lookup-url="{{ group.fk_lookup_url }}"
              data-rio-fk-field-name="{{ group.field }}">
          {% for pair in group.hidden_pairs %}
          <input type="hidden" name="{{ pair[0] }}" value="{{ pair[1] }}">
          {% endfor %}
          {% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
          {% if active_sort_field %}
          <input type="hidden" name="sort" value="{{ active_sort_field }}">
          <input type="hidden" name="dir" value="{{ active_sort_dir }}">
          {% endif %}
          {% if active_per_page_override %}
          <input type="hidden" name="per_page" value="{{ active_per_page_override }}">
          {% endif %}
          {# The hidden id is the actual submitted value; the visible
           # search input is JS-driven (typeahead). Without JS, the
           # operator types a numeric id directly into the search box
           # — same fallback as Django's `raw_id_fields`. #}
          <input type="hidden" name="{{ group.field }}" value="{{ group.fk_selected_id }}" data-rio-fk-id>
          <div class="rio-fk-autocomplete-wrap">
            <input type="search"
                   class="rio-input rio-fk-autocomplete-input"
                   value="{{ group.fk_selected_label or group.fk_selected_id }}"
                   placeholder="Search {{ group.fk_target_label }}…"
                   autocomplete="off"
                   data-rio-fk-search>
            <ul class="rio-fk-autocomplete-results" data-rio-fk-results hidden></ul>
          </div>
          <div class="rio-fk-autocomplete-actions">
            <button type="submit" class="rio-button rio-button--primary">Apply</button>
            {% if group.fk_selected_id %}
            <a href="{{ group.all_link }}" class="rio-button">Clear</a>
            {% endif %}
          </div>
        </form>
        {% elif group.kind == "multi_select" %}
        <form method="get" action="/admin/{{ admin_name }}" class="rio-multi-select">
          {% for pair in group.hidden_pairs %}
          <input type="hidden" name="{{ pair[0] }}" value="{{ pair[1] }}">
          {% endfor %}
          {% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
          {% if active_sort_field %}
          <input type="hidden" name="sort" value="{{ active_sort_field }}">
          <input type="hidden" name="dir" value="{{ active_sort_dir }}">
          {% endif %}
          {% if active_per_page_override %}
          <input type="hidden" name="per_page" value="{{ active_per_page_override }}">
          {% endif %}
          <ul class="rio-multi-select-options">
            {% for opt in group.options %}
            <li>
              <label class="rio-multi-select-option">
                <input type="checkbox" name="{{ group.field }}" value="{{ opt.value }}"{% if opt.selected %} checked{% endif %}>
                <span>{{ opt.label }}</span>
              </label>
            </li>
            {% endfor %}
          </ul>
          <div class="rio-multi-select-actions">
            <button type="submit" class="rio-button rio-button--primary">Apply</button>
            {% if group.multi_selected %}
            <a href="{{ group.all_link }}" class="rio-button">Clear</a>
            {% endif %}
          </div>
        </form>
        {% elif group.kind == "date_range" %}
        <form method="get" action="/admin/{{ admin_name }}" class="rio-date-range">
          {% for pair in group.hidden_pairs %}
          <input type="hidden" name="{{ pair[0] }}" value="{{ pair[1] }}">
          {% endfor %}
          {% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
          {% if active_sort_field %}
          <input type="hidden" name="sort" value="{{ active_sort_field }}">
          <input type="hidden" name="dir" value="{{ active_sort_dir }}">
          {% endif %}
          {% if active_per_page_override %}
          <input type="hidden" name="per_page" value="{{ active_per_page_override }}">
          {% endif %}
          <label class="rio-date-range-field">
            <span class="rio-date-range-label-text">From</span>
            <input type="date" name="{{ group.date_from_name }}" value="{{ group.date_from_value }}" class="rio-input rio-date-range-input">
          </label>
          <label class="rio-date-range-field">
            <span class="rio-date-range-label-text">To</span>
            <input type="date" name="{{ group.date_to_name }}" value="{{ group.date_to_value }}" class="rio-input rio-date-range-input">
          </label>
          <div class="rio-date-range-actions">
            <button type="submit" class="rio-button rio-button--primary">Apply</button>
            {% if group.has_active_range %}
            <a href="{{ group.all_link }}" class="rio-button">Clear</a>
            {% endif %}
          </div>
        </form>
        {% else %}
        <div class="rio-dropdown-options">
          <a href="{{ group.all_link }}" class="rio-dropdown-chip{% if not group.current %} is-active{% endif %}">All</a>
          {% for opt in group.options %}
          <a href="{{ opt.link }}" class="rio-dropdown-chip{% if opt.selected %} is-active{% endif %}">{{ opt.label }}</a>
          {% endfor %}
        </div>
        {% endif %}
      </div>
      {% endfor %}
      {% if active_filter_count > 0 %}
      <div class="rio-dropdown-footer">
        <a href="{{ clear_all_filters_link }}" class="rio-button">Clear all</a>
      </div>
      {% endif %}
    </div>
  </div>
  {% endif %}

  {% if sort_options %}
  <div class="rio-dropdown" data-rio-dropdown>
    <button type="button" class="rio-dropdown-toggle" aria-haspopup="true" aria-expanded="false">
      Sort: <strong style="font-weight: 600; color: var(--rio-text-strong);">{{ current_sort_label }}</strong>
      {{ icon("chevron-down", class="rio-chev") }}
    </button>
    <div class="rio-dropdown-panel" role="dialog" aria-label="Sort">
      <div class="rio-dropdown-menu">
        {% for opt in sort_options %}
        <a href="{{ opt.link }}" class="rio-dropdown-item{% if opt.is_active %} is-active{% endif %}">{{ opt.label }}</a>
        {% endfor %}
      </div>
    </div>
  </div>
  {% endif %}

  {% if per_page_options %}
  <div class="rio-dropdown" data-rio-dropdown>
    <button type="button" class="rio-dropdown-toggle" aria-haspopup="true" aria-expanded="false">
      {{ current_per_page_label }}
      {{ icon("chevron-down", class="rio-chev") }}
    </button>
    <div class="rio-dropdown-panel" role="dialog" aria-label="Rows per page">
      <div class="rio-dropdown-menu">
        {% for opt in per_page_options %}
        <a href="{{ opt.link }}" class="rio-dropdown-item{% if opt.is_active %} is-active{% endif %}">{{ opt.label }}</a>
        {% endfor %}
      </div>
    </div>
  </div>
  {% endif %}

  {# Saved-filters dropdown — per-operator bookmarks for this
   # model. Always renders the toggle (so the "Save current view"
   # form is always reachable); the apply list is hidden when the
   # operator has no saved filters yet. #}
  <div class="rio-dropdown" data-rio-dropdown>
    <button type="button" class="rio-dropdown-toggle" aria-haspopup="true" aria-expanded="false">
      {{ icon("bookmark", class="rio-icon") }}
      Saved
      {% if saved_filters %}
      <span class="rio-dropdown-badge" aria-label="{{ saved_filters|length }} saved view">{{ saved_filters|length }}</span>
      {% endif %}
      {{ icon("chevron-down", class="rio-chev") }}
    </button>
    <div class="rio-dropdown-panel" role="dialog" aria-label="Saved filters">
      {% if saved_filters %}
      <div class="rio-dropdown-section">
        <span class="rio-dropdown-label">Apply</span>
        <ul class="rio-saved-list">
          {% for sf in saved_filters %}
          <li class="rio-saved-list__row{% if sf.is_current %} is-active{% endif %}">
            <a class="rio-saved-list__apply" href="{{ sf.apply_url }}">{{ sf.name }}</a>
            <form method="post" action="{{ sf.delete_url }}" class="rio-saved-list__delete" aria-label="Delete saved filter {{ sf.name }}">
              <input type="hidden" name="_csrf" value="{{ csrf_token }}">
              <button type="submit" class="rio-saved-list__delete-btn" title="Delete this saved view">×</button>
            </form>
          </li>
          {% endfor %}
        </ul>
      </div>
      {% endif %}
      <div class="rio-dropdown-section">
        <span class="rio-dropdown-label">Save current view</span>
        <form method="post" action="/admin/{{ admin_name }}/saved_filters" class="rio-saved-form">
          <input type="hidden" name="_csrf" value="{{ csrf_token }}">
          <input type="hidden" name="_query" value="{{ current_query_string }}">
          <input type="text" name="_name" class="rio-input rio-saved-form__name"
                 placeholder="Name this view" maxlength="120" required>
          <button type="submit" class="rio-button rio-button--primary rio-button--sm">Save</button>
        </form>
      </div>
    </div>
  </div>

  {# CSV export — same filter/search/sort state as the list page,
   # capped server-side at 10_000 rows. download attr nudges the
   # browser to save rather than navigate. #}
  <a class="rio-button" href="{{ csv_export_url }}" download>
    {{ icon("download", class="rio-icon") }} CSV
  </a>

  {# CSV import — sibling of export. Inline form opens a native
   # file picker via the styled button label. Same model's
   # `change` permission gates the POST. #}
  <form method="post" action="/admin/{{ admin_name }}/import.csv"
        enctype="multipart/form-data" class="rio-form-inline rio-toolbar-import">
    <input type="hidden" name="_csrf" value="{{ csrf_token }}">
    <label class="rio-button" tabindex="0">
      Import CSV…
      <input type="file" name="file" accept=".csv,text/csv"
             onchange="this.form.submit()" style="display:none">
    </label>
  </form>
</div>

{% if active_filter_pills %}
<div class="rio-active-filters" role="region" aria-label="Active filters">
  <span class="rio-active-filters-label">Active</span>
  {% for pill in active_filter_pills %}
  <span class="rio-active-pill">
    <span class="rio-active-pill-key">{{ pill.label }}:</span>
    {{ pill.value_label }}
    <a href="{{ pill.remove_link }}" class="rio-active-pill-x" aria-label="Remove {{ pill.label }} filter">×</a>
  </span>
  {% endfor %}
  <a href="{{ clear_all_filters_link }}" class="rio-active-filters-clear">Clear all</a>
</div>
{% endif %}

{# Bulk-select form wraps the entire table so checked rows submit as
 # a single comma-separated `_ids` field built by admin.js. The form
 # POSTs to `/admin/{name}/bulk_delete`; the server's two-step flow
 # (confirm → commit) takes over from there. JS toggles `.is-active`
 # on the form when ≥1 checkbox is checked, which reveals the bulk
 # bar. Without JS, the bulk bar stays hidden and users fall back to
 # per-row Delete actions. #}
{% if not read_only %}
<form method="post" action="/admin/{{ admin_name }}/bulk_delete" class="rio-bulk-form" data-rio-bulk>
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="hidden" name="_ids" value="" data-rio-bulk-ids>

<div class="rio-bulk-bar" role="region" aria-live="polite" aria-label="Bulk actions">
  <span class="rio-bulk-bar-count"><strong data-rio-bulk-count>0</strong> selected</span>
  {# Built-in Delete uses the form's default action (/bulk_delete).
   # Project-defined actions override via `formaction`, so the
   # same form + same selected ids dispatch to different routes. #}
  <button type="submit" class="rio-button rio-button--danger">{{ icon("trash", class="rio-icon") }} Delete selected</button>
  {% for btn in bulk_action_buttons %}
  <button type="submit" formaction="{{ btn.form_action }}" class="rio-button{% if btn.destructive %} rio-button--danger{% endif %}">{{ btn.label }}</button>
  {% endfor %}
  <button type="button" class="rio-bulk-bar-clear" data-rio-bulk-clear>Clear selection</button>
</div>
{% endif %}

<section class="rio-card rio-list">
  {% if rows %}
  <table class="rio-table rio-table--striped">
    <thead>
      <tr>
        {% if not read_only %}
        <th class="rio-th rio-th--checkbox">
          <input type="checkbox" class="rio-row-checkbox-all" data-rio-bulk-all aria-label="Select all rows">
        </th>
        {% endif %}
        {% for f in fields %}
        <th class="rio-th rio-th--{{ f.kind }}{% if f.sort_active %} rio-th--sort-{{ f.sort_active }}{% endif %}">
          <a href="{{ f.sort_link }}" class="rio-th-sort">
            {{ f.label }}{% if f.sort_active == "asc" %} ▲{% elif f.sort_active == "desc" %} ▼{% endif %}
          </a>
        </th>
        {% endfor %}
        <th class="rio-th rio-th--actions">Actions</th>
      </tr>
    </thead>
    <tbody>
      {% for row in rows %}
      <tr>
        {% if not read_only %}
        <td class="rio-td rio-td--checkbox">
          <input type="checkbox" class="rio-row-checkbox" data-rio-bulk-row value="{{ row.id }}" aria-label="Select row {{ row.id }}">
        </td>
        {% endif %}
        {% for f in fields %}
        <td class="rio-td rio-td--{{ f.kind }}"{% if f.kind == "text" %} title="{{ row[f.name]|default(value='') }}"{% endif %}>
          {% if f.name == "id" %}
            <a href="/admin/{{ admin_name }}/{{ row.id }}/edit">{{ row.id }}</a>
          {% elif f.kind == "checkbox" %}
            {% if row[f.name] %}Yes{% else %}No{% endif %}
          {% elif row.links[f.name] %}
            <a class="rio-fk-link" href="{{ row.links[f.name] }}">{% if row.highlights[f.name] %}{{ row.highlights[f.name]|safe }}{% else %}{{ row[f.name]|default(value='') }}{% endif %}</a>
          {% elif row.highlights[f.name] %}
            {{ row.highlights[f.name]|safe }}
          {% else %}
            {{ row[f.name]|default(value='') }}
          {% endif %}
        </td>
        {% endfor %}
        <td class="rio-td rio-td--actions">
          {% if read_only %}
          <span class="rio-meta">read-only</span>
          {% else %}
          <a class="rio-button rio-button--ghost rio-button--sm" href="/admin/{{ admin_name }}/{{ row.id }}/edit">{{ icon("pencil", class="rio-icon") }} Edit</a>
          <a class="rio-button rio-button--danger-ghost rio-button--sm" href="/admin/{{ admin_name }}/{{ row.id }}/delete">{{ icon("trash", class="rio-icon") }} Delete</a>
          {% endif %}
        </td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
  {% else %}
  <p class="rio-empty">
    No {{ display_name|lower }} yet.
    {% if not read_only %}<a href="/admin/{{ admin_name }}/new">Add the first one.</a>{% endif %}
  </p>
  {% endif %}
</section>

{% if not read_only %}
</form>
{% endif %}

{% if total_pages > 1 %}
<nav class="rio-pagination" aria-label="Pagination">
  <span class="rio-meta">{{ total_rows }} total · page {{ page }} of {{ total_pages }}</span>
  {% if prev_page_link %}<a class="rio-pagination-link" href="{{ prev_page_link }}">← Previous</a>{% endif %}
  {% for item in page_items %}
    {% if item.kind == "ellipsis" %}
      <span class="rio-pagination-ellipsis" aria-hidden="true"></span>
    {% elif item.is_active %}
      <span class="rio-pagination-num is-active" aria-current="page">{{ item.number }}</span>
    {% else %}
      <a class="rio-pagination-num" href="{{ item.link }}">{{ item.number }}</a>
    {% endif %}
  {% endfor %}
  {% if next_page_link %}<a class="rio-pagination-link" href="{{ next_page_link }}">Next →</a>{% endif %}
</nav>
{% endif %}
{% endblock %}