{% load humanize common_extras %}{% comment %} The flow list: the summary cards, then the rows in Live / Offline / Draft sections (HANDOFF §3). Rendered both inside list.html and on its own for every HTMX refresh — one renderer, so a filtered view and a post-mutation refresh can never disagree about what a row looks like, and the counts above the rows cannot go stale when a switch moves a flow between sections. **Seven buttons became one plus a menu.** Every row used to carry Edit, Stats, Rename, Duplicate, Export, Export bundle and Archive at equal weight, which made the action wanted on nine visits out of ten exactly as prominent as the one wanted twice a year. The named action is now Open — or Continue, for a draft somebody started and left — and the rest sit behind the "…". The menu panel is `position: fixed` and anchored on open, the same way components/ui_select.html does it and for the same reason: these rows live in a scrolling container, and an absolutely positioned panel is clipped by that overflow. The coordinates are captured once, and partials/_list_behaviours.html closes the menu on scroll so it cannot drift away from its button — that listener is registered once for the page, and in the capture phase, because .app-content is what scrolls and a scroll event never bubbles out of it. {% endcomment %} {% comment %} The filter popover's footer reads this after every swap, so "Show N flows" stays true as the selection changes. {% endcomment %} {% if summary_oob %}{% include "flows/_summary.html" with oob=True %}{% endif %} {% if not groups %} {% if query or filters.status or filters.folder %} {% include "components/empty_state.html" with icon="search" title="No flows match" body="Nothing matches these filters. Try a different word, or clear them." action_label="Clear filters" action_click="clearFilters()" %} {% else %} {% include "components/empty_state.html" with icon="flows" title="No flows here yet" body="Start from a template above, or create one from scratch." %} {% endif %} {% else %} {% for group in groups %}

{{ group.label }}

{{ group.flows|length }}
{% for flow in group.flows %}
{% include "partials/_nav_icon.html" with name="flows" icon_size=19 %}
{{ flow.name }} {{ group.label }}
{% comment %} When this flow runs, in the reader's words rather than SPEC §10's — see apps/flows/triggers/phrasing.py. A flow with no trigger says so right here, which is where somebody notices before wondering why nothing happened. The folder rides along, now that rows are grouped by status rather than by folder. Led by the channels it runs on (triggers/platforms.py shown_platforms), so a list of a dozen flows can be scanned by channel before anybody reads a sentence. {% endcomment %}
{% for platform in flow.platforms %} {% include "partials/_platform_icon.html" with platform=platform.key size="sm" %} {% endfor %} {% if flow.folder %}{{ flow.folder }} · {% endif %}{{ flow.trigger_summary }} · edited {{ flow.updated_at|naturaltime }}
{% comment %} Live or not, in one control (HANDOFF §3). It does not flip on its own click: @click.prevent keeps the box as it is, htmx posts the explicit target state, and the flowsChanged refresh redraws it from the server. So a cancelled confirm, a refusal or a stale page never leaves a switch showing a state the flow is not in. Off asks first, with the builder's own words: it cannot be taken back — the conversations it stops stay stopped. On is only offered when nothing changed since the flow last ran; otherwise it would publish edits nobody has reviewed (see views.flow_set_live), so the switch is disabled and says why. A draft has never run and is set live from the builder. {% endcomment %} {% if can_edit and flow.status != "archived" %} {% endif %} {% if not can_edit %}View{% elif flow.status == "draft" %}Continue{% else %}Open{% endif %} {% comment %} Full-width and ordered last, so it drops onto its own line under the row rather than colliding with the action buttons. {% endcomment %} {% if can_edit %}
{% csrf_token %}
{% endif %}
{% endfor %}
{% endfor %} {% endif %}