{% extends "base.html" %} {% block content %}

{{ model_title }} {% if trashed %}{{ _("Trash") }} {% endif %}

{% if page.total %} {{ "{:,}".format(page.total) }} {{ (model_title if page.total != 1 else model_singular)|lower }} {% elif trashed %} {{ _("Nothing in the trash.") }} {% else %} {{ _("Nothing here yet") }} {% endif %}

{% if add_url or trash_url or list_url_no_trash %}
{% if trash_url %} {# A separate view, not a filter: it changes which rows exist on the page rather than which of the current rows are shown, which is why this is a link beside Add rather than a control inside the toolbar form. #} {{ ff_icon("trash", size=16) }} {{ _("Trash") }} {% endif %} {% if list_url_no_trash %} {{ ff_icon("chevron-left", size=16) }} {{ _("Back to list") }} {% endif %} {% if add_url %} {{ ff_icon("plus", size=16) }} {{ _("Add {name}", name=model_singular) }} {% endif %}
{% endif %}
{# data-ff-live turns this into an in-place update when JavaScript is running. Without it the form and its links still work as a plain GET, which is why every control below is a real input and every sort header a real link. #}
{% if admin.searchable() %} {% endif %} {% if filters %} {# One button, not a row of controls. Every filterable column as its own box across the top does not survive a model with eight of them, and it leaves nowhere to put a filter that needs more than a line. The panel holds them all and states how many are on. #} {# Without script this is not a panel at all: it lays out inline, below the search box, and Apply submits it. A popover nobody can open would make every filter unreachable. #} {% endif %} {# A filter is already shareable -- it lives in the query string, so the URL on screen is the whole story. What is missing is a name for it, which is what this adds: a client-only shelf of named URLs, kept in this browser rather than in a table, because the query string is already the source of truth and a second copy of it server-side would only be a second place for the two to disagree. #}
{# The ordering rides along in a hidden field, so applying a filter does not silently reset the column the user sorted by. #} {% if ordering_param %} {% endif %}
{% if columns %} {# A client-only preference -- remembered per list, applied straight to the table's cells rather than the row that requested them, so it works whether or not this project has ever heard of it. The trigger is JS-only because there is nothing to fall back to: every column already shows without script, which is what this control's own default draws too. Row density is a separate, project-wide setting already -- `UISettings.density`, which retunes `--ff-density` for the whole admin, not just one table -- so this menu does not repeat it. #}
{% endif %} {% if exports %} {# Downloads what is on screen, filters and ordering included. A button that ignored them would export the whole model, which is never what someone who has just narrowed a list is asking for. #}
{% endif %} {% if import_url %} {# Beside the export, because the two are a pair: the file this downloads is the file that one takes back. A plain link rather than a menu -- the format is decided by whatever gets uploaded, not chosen up front. #} {{ ff_icon("upload", size=15) }} {{ _("Import") }} {% endif %} {# Without script nothing applies on change, and a model with no filters would otherwise offer no submit at all -- leaving Enter in the search box as the only way to run it, which is not discoverable. #} {% if active_filters %} {{ ff_icon("filter-off", size=15) }} {{ _("Clear") }} {% endif %}
{% if active_filters %} {# What is currently narrowing the list, in words, each removable on its own. The commonest support question about any admin is "why can I not find this row", answered by a filter someone left set. #}
{{ _("Filtered by") }} {% for chip in active_filters %} {{ chip.field }}: {{ chip.value }} {{ _("Remove this filter") }} {% endfor %}
{% endif %}
{% include "model/_results.html" %}
{% if actions %} {# Outside #ff-results so it survives a live update, and so the selection it describes is not wiped out by the fragment that replaces the rows. #} {% endif %} {% endblock %}