adminx 0.2.6

A powerful, modern admin panel framework for Rust built on Actix Web and MongoDB with automatic CRUD, role-based access control, and a beautiful responsive UI
Documentation
<section id="actions-section" class="mt-6" hx-boost="false" data-turbo="false">
  <div class="flex items-center justify-between">
    <h2 class="text-lg font-semibold">Actions</h2>
    <div id="action-toast" class="hidden text-sm px-3 py-2 rounded border"></div>
  </div>

  {% if actions is defined %}
    <div class="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-4">
      {% for act in actions %}
        {% set act_name = act.name %}
        {% set act_method = act.method | default(value="POST") %}

        <div class="p-4 rounded-lg border">
          <h3 class="font-medium mb-3">{{ act_name | upper }}</h3>

          {% set has_fields = act.ui is defined and act.ui and act.ui.fields is defined and act.ui.fields %}
          {% if has_fields %}
            <form novalidate>
              <div class="flex flex-col gap-3">
                {% for f in act.ui.fields %}
                  <div class="flex flex-col gap-1">
                    <label class="text-sm text-gray-600">
                      {% if f.label is defined and f.label %}{{ f.label }}{% else %}{{ f.name }}{% endif %}
                    </label>

                    {% if f.field_type == 'select' %}
                      <select name="{{ f.name }}" class="border rounded px-2 py-1">
                        {% if f.options is defined and f.options %}
                          {% for opt in f.options %}
                            {% if opt is object and opt.value is defined %}
                              <option value="{{ opt.value }}">{{ opt.label | default(value=opt.value) }}</option>
                            {% else %}
                              <option value="{{ opt }}">{{ opt }}</option>
                            {% endif %}
                          {% endfor %}
                        {% endif %}
                      </select>
                    {% elif f.field_type == 'boolean' %}
                      <select name="{{ f.name }}" class="border rounded px-2 py-1">
                        <option value="true">True</option>
                        <option value="false">False</option>
                      </select>
                    {% elif f.field_type == 'number' %}
                      <input name="{{ f.name }}" type="number" class="border rounded px-2 py-1"
                             {% if f.required is defined and f.required %}required{% endif %} />
                    {% elif f.field_type == 'json' %}
                      <textarea name="{{ f.name }}" rows="4" class="border rounded px-2 py-1 font-mono"
                                {% if f.required is defined and f.required %}required{% endif %}></textarea>
                    {% else %}
                      <input name="{{ f.name }}" type="text" class="border rounded px-2 py-1"
                             {% if f.required is defined and f.required %}required{% endif %} />
                    {% endif %}
                  </div>
                {% endfor %}
              </div>

              <div class="mt-3 flex items-center gap-2">
                <button
                  class="px-3 py-1 rounded bg-blue-600 text-white"
                  type="button"
                  data-run
                  data-base="{{ base_path | default(value='/adminx') }}"
                  data-id="{{ item_id }}"
                  data-name="{{ act_name }}"
                  data-method="{{ act_method }}"
                >Run</button>
              </div>
            </form>
          {% else %}
            <p class="text-sm text-gray-600 mb-2">Payload (JSON, optional)</p>
            <textarea id="json-{{ act_name }}" rows="4" class="w-full border rounded px-2 py-1 font-mono"></textarea>
            <div class="mt-2 flex items-center gap-2">
              <button
                class="px-3 py-1 rounded border"
                type="button"
                data-run
                data-base="{{ base_path | default(value='/adminx') }}"
                data-id="{{ item_id }}"
                data-name="{{ act_name }}"
                data-method="{{ act_method }}"
                data-json-target="json-{{ act_name }}"
              >Run</button>
            </div>
          {% endif %}
        </div>
      {% else %}
        <p class="text-sm text-gray-500 mt-2">No actions available.</p>
      {% endfor %}
    </div>
  {% else %}
    <p class="text-sm text-gray-500 mt-2">No actions available.</p>
  {% endif %}
</section>

<script>
(function () {
  function __headers() {
    const base = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
    if (window.CSRF_TOKEN) base['X-CSRF-Token'] = window.CSRF_TOKEN;
    if (window.CUSTOM_HEADERS && typeof window.CUSTOM_HEADERS === 'object') Object.assign(base, window.CUSTOM_HEADERS);
    return base;
  }
  function __tryParseJson(s) { try { return JSON.parse(s); } catch { return {}; } }
  function __stripTrailing(p){ return String(p || '').replace(/\/+$/, ''); }

  // Infer resource from current URL when needed: /adminx/<resource>/...
  function __inferResourceFromLocation(){
    const parts = location.pathname.split('/').filter(Boolean); // ['adminx','events','view','<id>']
    const idx = parts.indexOf('adminx');
    if (idx >= 0 && parts[idx+1]) return parts[idx+1]; // 'events'
    return ''; // unknown
  }

  // Ensure base begins with /adminx/ and contains a resource segment
  function __resolveBase(maybeBase){
    let base = __stripTrailing((maybeBase || '').trim());

    // Normalize leading slash
    if (base && !base.startsWith('/')) base = '/' + base;

    // If empty or just '/adminx', we must infer resource
    if (!base || base === '/adminx') {
      const resource = __inferResourceFromLocation();
      return resource ? `/adminx/${resource}` : '/adminx'; // best effort
    }

    // If starts with '/adminx/<something>' we're good
    if (base.startsWith('/adminx/')) {
      const rest = base.slice('/adminx/'.length);
      // If someone passed '/adminx//' or '/adminx///events', compress
      return '/adminx/' + rest.replace(/^\/+/, '').split('/')[0];
    }

    // If they passed '/events' or 'events' -> coerce to '/adminx/events'
    const tail = base.replace(/^\/+/, ''); // 'events' or 'users/...'
    return '/adminx/' + tail.split('/')[0];
  }

  function __showToast(kind, msg) {
    const el = document.getElementById('action-toast'); if (!el) return;
    el.classList.remove('hidden','border-red-300','bg-red-50','text-red-700','border-green-300','bg-green-50','text-green-700');
    if (kind === 'success') el.classList.add('border-green-300','bg-green-50','text-green-700'); else el.classList.add('border-red-300','bg-red-50','text-red-700');
    el.textContent = String(msg || ''); clearTimeout(el.__t); el.__t = setTimeout(() => el.classList.add('hidden'), 3000);
  }

  async function __postAction(basePath, id, name, method, payload) {
    const base = __resolveBase(basePath);                   // always '/adminx/<resource>'
    const url  = `${base}/${encodeURIComponent(id)}/${encodeURIComponent(name)}`;
    const verb = (method || 'POST').toUpperCase();
    const opts = { method: verb, headers: __headers() };
    if (verb !== 'GET' && verb !== 'HEAD') opts.body = JSON.stringify(payload && typeof payload === 'object' ? payload : {});
    try {
      const res = await fetch(url, opts);
      const ct = res.headers.get('content-type') || '';
      const data = ct.includes('application/json') ? await res.json() : { status: res.status, ok: res.ok };
      if (!res.ok) { __showToast('error', (data && (data.message || data.error)) || `HTTP ${res.status}`); return; }
      __showToast('success', (data && (data.message || 'Done')) || 'Done');
      setTimeout(() => location.reload(), 600);
    } catch (e) { __showToast('error', (e && e.message) ? e.message : 'Request failed'); }
  }

  function buildPayloadFromForm(form) {
    const fd = new FormData(form); const payload = {};
    for (const [k, raw] of fd.entries()) {
      const v = String(raw).trim();
      if (v === 'true') payload[k] = true;
      else if (v === 'false') payload[k] = false;
      else if (v !== '' && !isNaN(v)) payload[k] = Number(v);
      else if (k.toLowerCase().includes('json') || k.toLowerCase().includes('payload')) {
        const maybe = __tryParseJson(v); payload[k] = (maybe && typeof maybe === 'object' && Object.keys(maybe).length) ? maybe : v;
      } else payload[k] = v;
    }
    return payload;
  }

  // Block native submits inside actions
  document.addEventListener('submit', function(e){
    const wrap = document.getElementById('actions-section'); if (!wrap) return;
    if (wrap.contains(e.target)) { e.preventDefault(); e.stopPropagation(); }
  }, true);

  // Delegated Run handler
  document.addEventListener('click', function(e){
    const wrap = document.getElementById('actions-section'); if (!wrap) return;
    const btn = e.target.closest('[data-run]');
    if (!btn || !wrap.contains(btn)) return;

    e.preventDefault();
    e.stopPropagation();

    const base   = btn.dataset.base || ''; // may be '/adminx', '/adminx/events', 'events', etc.
    const id     = btn.dataset.id   || '';
    const name   = btn.dataset.name || '';
    const method = btn.dataset.method || 'POST';
    const jsonId = btn.dataset.jsonTarget;

    let payload = {};
    if (jsonId) {
      const ta = document.getElementById(jsonId);
      payload = __tryParseJson(ta ? ta.value : '{}');
    } else {
      const form = btn.closest('form');
      payload = form ? buildPayloadFromForm(form) : {};
    }

    __postAction(base, id, name, method, payload);
  }, true); // capture to beat row-level click handlers
})();
</script>