umbral-admin 0.0.12

Auto-generated CRUD admin UI for umbral models.
Documentation
{#
  inlines.html — the shared inline-formset markup.

  ONE source of truth for the inline formset, used by BOTH
  the full-page change form (`admin/form.html`) and the slide-over sheet
  (`admin/_macros/sheet.html`). Rendering the same markup in both places
  guarantees the formset field NAMES are byte-identical, so the single
  `crud::create` / `crud::update` handler parses + saves them unchanged
  (the `inline-<child>-<i>-<field>` naming scheme).

  Public macro: `inline_formset(inlines)` where `inlines` is the list of
  `InlineView` structs built by `inlines::build_inline_views`. Each view
  carries its child table name, layout kind ("tabular"|"stacked"),
  delete flag, header fields, and the existing+blank rows.

  The "Add another" JS is scoped per-formset and re-binds on every
  render (it keys off `data-inline-bound` so an HTMX-swapped sheet that
  re-loads the partial wires the buttons exactly once).
#}

{# Per-field input widget for one inline cell. `idx` may be a real index
   or the `__IDX__` placeholder (string) for the blank template; the
   value is then empty. Compact inputs (no label row) so the tabular
   layout stays a tidy grid; the stacked layout adds its own labels. #}
{% macro inline_field_input(name, idx, f, blank=false) %}
  {% set fname = "inline-" ~ name ~ "-" ~ idx ~ "-" ~ f.name %}
  {% set fval = "" if blank else f.value %}
  {% if f.kind == "bool" %}
    <input type="checkbox" name="{{ fname }}" value="true"{% if fval == "true" %} checked{% endif %}{% if f.readonly %} disabled{% endif %}>
  {% elif f.kind == "select" %}
    <select name="{{ fname }}"{% if f.readonly %} disabled{% endif %} style="width:100%;padding:4px;">
      {% if f.nullable %}<option value=""></option>{% endif %}
      {% for opt in f.choices %}<option value="{{ opt.value }}"{% if fval == opt.value %} selected{% endif %}>{{ opt.label }}</option>{% endfor %}
    </select>
  {% elif f.kind == "textarea" or f.kind == "json" %}
    <textarea name="{{ fname }}" rows="2"{% if f.readonly %} readonly{% endif %} style="width:100%;padding:4px;">{{ fval }}</textarea>
  {% else %}
    <input type="{% if f.kind == "number" %}number{% elif f.kind == "date" %}date{% elif f.kind == "time" %}time{% elif f.kind == "datetime-local" %}datetime-local{% else %}text{% endif %}" name="{{ fname }}" value="{{ fval }}"{% if f.readonly %} readonly{% endif %} style="width:100%;padding:4px;">
  {% endif %}
{% endmacro %}

{% macro inline_formset(inlines) %}
{% if inlines %}
  {% for inline in inlines %}
    <fieldset class="inline-group" data-inline="{{ inline.name }}" style="margin:1.25rem 0;border:1px solid #ddd;border-radius:8px;padding:0.75rem 1rem;">
      <legend style="font-weight:600;padding:0 0.4rem;">{{ inline.label }}</legend>
      <input type="hidden" name="inline-{{ inline.name }}-TOTAL" value="{{ inline.total }}" data-inline-total>

      {% if inline.kind == "tabular" %}
        <table class="inline-table" style="width:100%;border-collapse:collapse;">
          <thead>
            <tr>
              {% for f in inline.fields %}<th style="text-align:left;padding:0.3rem;font-size:0.85rem;">{{ f.name }}</th>{% endfor %}
              {% if inline.can_delete %}<th style="padding:0.3rem;font-size:0.85rem;">Delete</th>{% endif %}
            </tr>
          </thead>
          <tbody data-inline-body>
            {% for r in inline.rows %}
            {% set row_idx = loop.index0 %}
            <tr data-inline-row data-index="{{ row_idx }}">
              <input type="hidden" name="inline-{{ inline.name }}-{{ row_idx }}-id" value="{{ r.id }}">
              {% for f in r.fields %}
              <td style="padding:0.3rem;">{{ inline_field_input(inline.name, row_idx, f) }}</td>
              {% endfor %}
              {% if inline.can_delete %}
              <td style="padding:0.3rem;text-align:center;">
                {% if r.id %}<input type="checkbox" name="inline-{{ inline.name }}-{{ row_idx }}-DELETE" value="on">{% endif %}
              </td>
              {% endif %}
            </tr>
            {% endfor %}
          </tbody>
        </table>
      {% else %}
        <div data-inline-body>
          {% for r in inline.rows %}
          {% set row_idx = loop.index0 %}
          <div data-inline-row data-index="{{ row_idx }}" style="border:1px solid #eee;border-radius:6px;padding:0.5rem 0.75rem;margin-bottom:0.5rem;">
            <input type="hidden" name="inline-{{ inline.name }}-{{ row_idx }}-id" value="{{ r.id }}">
            {% for f in r.fields %}
            <div class="field" style="margin-bottom:0.5rem;">
              <label style="display:block;font-size:0.85rem;">{{ f.name }}</label>
              {{ inline_field_input(inline.name, row_idx, f) }}
            </div>
            {% endfor %}
            {% if inline.can_delete and r.id %}
            <label style="font-size:0.85rem;"><input type="checkbox" name="inline-{{ inline.name }}-{{ row_idx }}-DELETE" value="on"> Delete</label>
            {% endif %}
          </div>
          {% endfor %}
        </div>
      {% endif %}

      {# Blank-row template for the "Add another" JS. Index placeholder
         __IDX__ is substituted on clone. #}
      <template data-inline-template>
        {% if inline.kind == "tabular" %}
        <tr data-inline-row>
          <input type="hidden" name="inline-{{ inline.name }}-__IDX__-id" value="">
          {% for f in inline.fields %}
          <td style="padding:0.3rem;">{{ inline_field_input(inline.name, "__IDX__", f, true) }}</td>
          {% endfor %}
          {% if inline.can_delete %}<td></td>{% endif %}
        </tr>
        {% else %}
        <div data-inline-row style="border:1px solid #eee;border-radius:6px;padding:0.5rem 0.75rem;margin-bottom:0.5rem;">
          <input type="hidden" name="inline-{{ inline.name }}-__IDX__-id" value="">
          {% for f in inline.fields %}
          <div class="field" style="margin-bottom:0.5rem;">
            <label style="display:block;font-size:0.85rem;">{{ f.name }}</label>
            {{ inline_field_input(inline.name, "__IDX__", f, true) }}
          </div>
          {% endfor %}
        </div>
        {% endif %}
      </template>

      <button type="button" class="btn" data-inline-add style="margin-top:0.5rem;">+ Add another {{ inline.label }}</button>
    </fieldset>
  {% endfor %}

  {# Scoped "Add another" wiring. Idempotent: each `.inline-group` is
     flagged `data-inline-bound` once so re-running this block (the HTMX
     sheet re-loads the partial on every open / save-and-continue) never
     double-binds the click handler. Plain DOM cloning — no dependency. #}
  <script>
  (function() {
    document.querySelectorAll('.inline-group').forEach(function(group) {
      if (group.getAttribute('data-inline-bound') === '1') return;
      var totalInput = group.querySelector('[data-inline-total]');
      var body = group.querySelector('[data-inline-body]');
      var tpl = group.querySelector('[data-inline-template]');
      var addBtn = group.querySelector('[data-inline-add]');
      if (!addBtn || !tpl || !body || !totalInput) return;
      group.setAttribute('data-inline-bound', '1');
      addBtn.addEventListener('click', function() {
        var idx = parseInt(totalInput.value, 10) || 0;
        var html = tpl.innerHTML.replace(/__IDX__/g, String(idx));
        var wrap = document.createElement('tbody');
        wrap.innerHTML = html.trim();
        var node = wrap.firstElementChild;
        body.appendChild(node);
        totalInput.value = String(idx + 1);
      });
    });
  })();
  </script>
{% endif %}
{% endmacro %}