{# 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. #}
{% 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. #}
{% 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 %}