{# Inline related records -- see docs/inlines.md. One entry in the `inlines` context list per ModelAdmin.inlines declaration; render_inline_section dispatches on .mode and .layout. #} {% from "admin/components/ui/field.html" import render_form_input with context %} {% from "admin/components/field.html" import render_field_value with context %} {% from "admin/components/icons.html" import icon %} {% from "admin/components/ui/combobox.html" import combobox %} {% from "admin/components/ui/select.html" import select %} {% from "admin/components/ui/multi-select.html" import multi_select %} {% from "admin/components/ui/delete-preview.html" import delete_refusal %} {% macro render_inline_section(inline) %}
{# Set only by the inline delete route, when the child's delete_preview refused; undefined (and so falsy) everywhere else. #} {% if refusal %}
{{ delete_refusal(refusal) }}
{% endif %}

{{ _(inline.label) }}

{% if inline.mode == "placeholder" %}
{{ _("Save to add %(label)s.", label=_(inline.label)) }}
{% elif inline.mode == "readonly" %} {% if inline.layout == "tabular" %}{{ render_inline_readonly_table(inline) }} {% else %}{{ render_inline_readonly_stacked(inline) }}{% endif %} {% else %} {% if inline.layout == "tabular" %}{{ render_inline_editable_table(inline) }} {% else %}{{ render_inline_editable_stacked(inline) }}{% endif %} {% endif %}
{% endmacro %} {# -- stacked, editable ---------------------------------------------- #} {% macro render_inline_editable_stacked(inline) %} {% for row in inline.rows %}
{% for name in inline.field_names %} {% set field = inline.child_admin.get_field(name) %} {% set value = row.data.get(name) if row.data is not none else field.get_value(row.obj) %} {{ render_form_input(field, value, row.errors.get(name), row.relation_options.get(name), key=inline.slug ~ "-" ~ row.pk) }} {% endfor %}
{% if inline.can_change %} {% endif %} {% if inline.can_delete %} {% endif %}
{% endfor %} {% if inline.add_row %}
{% for name in inline.field_names %} {% set field = inline.child_admin.get_field(name) %} {% set value = inline.add_row.data.get(name) if inline.add_row.data is not none else none %} {{ render_form_input(field, value, inline.add_row.errors.get(name), inline.add_row.relation_options.get(name), key=inline.slug ~ "-new") }} {% endfor %}
{% endif %} {% if not inline.rows and not inline.add_row %}

{{ _("No %(label)s.", label=_(inline.label)) }}

{% endif %} {% endmacro %} {# No
element: HTML forbids one as a table row or cell descendant. Save uses hx-post plus hx-include="closest tr", since there is no enclosing to submit. #} {% macro render_inline_table_cell_input(field, value, errors, relation, key="") %} {# Called, not quoted. This was a string literal containing Jinja delimiters -- Jinja never re-renders a value, so the raw text landed in the class attribute and every control in an inline edit row rendered unstyled: white on white in dark mode. #} {% set field_classes = ui('input', 'size-sm') %} {% set select_classes = ui('select', 'cell') %} {% if field.field_type == "boolean" %} {% elif field.field_type in ("foreignkey", "onetoone") and relation and relation.autocomplete %} {# An autocomplete relation loads no options into the page, so the plain select below would be empty. Same combobox the full form uses. #} {{ combobox(name=field.name, id="combobox-results-" ~ key ~ "-" ~ field.name, selected_pk=relation.selected_pk, selected_label=relation.selected_label, lookup_url=base_path ~ "/" ~ relation.lookup_target ~ "/lookup", placeholder=_('Search…')) }} {% elif field.field_type in ("foreignkey", "onetoone") %} {{ select(field.name, [(none, "—")] + (relation.options|list if relation else []), selected=(relation.selected_pk if relation else none), placeholder="—", compact=true) }} {% elif field.field_type == "manytomany" %} {# The same shadcn control the full form uses: a trigger with chips and a portalled popover, so a row stays one line tall however many options there are -- the native {% for choice in field.choices %} {% endfor %} {% else %} {% endif %} {% for error in errors or [] %}

{{ error }}

{% endfor %} {% endmacro %} {% macro render_inline_editable_table(inline) %}
{# The wrapper is overflow-hidden (it owns the rounded border), so without this the table is clipped at the card's edge and the row's own actions become unreachable. Bounded (not the plain list's "scroll") because this sits inside a parent's own page instead of being the page -- many child rows should scroll in place, not stretch it. bg-card (not bg-muted/50) keeps the sticky header opaque over rows scrolling underneath it. #}
{% for name in inline.field_names %} {% endfor %} {% for row in inline.rows %} {% for name in inline.field_names %} {% set field = inline.child_admin.get_field(name) %} {% set value = row.data.get(name) if row.data is not none else field.get_value(row.obj) %} {% endfor %} {% endfor %} {% if inline.add_row %} {% for name in inline.field_names %} {% set field = inline.child_admin.get_field(name) %} {% set value = inline.add_row.data.get(name) if inline.add_row.data is not none else none %} {% endfor %} {% endif %}
{{ _(inline.child_admin.get_field(name).label) }}
{{ render_inline_table_cell_input(field, value, row.errors.get(name), row.relation_options.get(name), inline.slug ~ "-" ~ row.pk) }} {% if inline.can_change %} {% endif %} {% if inline.can_delete %} {% endif %}
{{ render_inline_table_cell_input(field, value, inline.add_row.errors.get(name), inline.add_row.relation_options.get(name), inline.slug ~ "-new") }}
{# The empty state sits outside the scroller: there is nothing to scroll, and a centred message inside a scrolling box drifts. #} {% if not inline.rows and not inline.add_row %}

{{ _("No %(label)s.", label=_(inline.label)) }}

{% endif %}
{% endmacro %} {# -- read-only (detail page) ------------------------------------------ #} {% macro render_inline_readonly_stacked(inline) %} {% for row in inline.rows %}
{{ row.pk }}
{% for name in inline.detail_field_names %} {% set field = inline.child_admin.get_field(name) %}
{{ _(field.label) }}
{{ render_field_value(field, field.get_value(row.obj), inline.empty_value) }}
{% endfor %}
{% endfor %} {% if not inline.rows %}

{{ _("No %(label)s.", label=_(inline.label)) }}

{% endif %} {% endmacro %} {% macro render_inline_readonly_table(inline) %}
{# The wrapper is overflow-hidden (it owns the rounded border), so without this the table is clipped at the card's edge and the row's own actions become unreachable. Bounded (not the plain list's "scroll") because this sits inside a parent's own page instead of being the page -- many child rows should scroll in place, not stretch it. bg-card (not bg-muted/50) keeps the sticky header opaque over rows scrolling underneath it. #}
{% for name in inline.detail_field_names %} {% endfor %} {% for row in inline.rows %} {% for name in inline.detail_field_names %} {% set field = inline.child_admin.get_field(name) %} {% endfor %} {% endfor %}
{{ _(inline.child_admin.get_field(name).label) }}
{# A relation with several targets renders as a comma-joined run of links, and in a table cell that wraps: one record with three of them made the row three lines tall and widened the column enough to push the row's own actions off the card. #} {% if field.field_type == "manytomany" %}{{ render_field_value(field, field.get_value(row.obj), inline.empty_value) }} {% elif name == inline.link_field %} {# The primary key's cell opens the record, so the row needs no column of its own for a View link. Never a relation cell, which is already a link -- nesting one in another is invalid HTML that browsers silently restructure. #} {{ render_field_value(field, field.get_value(row.obj), inline.empty_value) }} {% else %}{{ render_field_value(field, field.get_value(row.obj), inline.empty_value) }}{% endif %}
{% if not inline.rows %}

{{ _("No %(label)s.", label=_(inline.label)) }}

{% endif %}
{% endmacro %}