{% extends "base.html" %} {% set title_text = ('Add ' ~ verbose_name) if is_create else ('Edit ' ~ verbose_name ~ ' #' ~ pk) %} {% block title %}{{ title_text }}{% endblock %} {% block extra_head %} {% endblock %} {% block content %} {# ── Breadcrumb ── #}
Dashboard {{ verbose_name_plural|default(verbose_name ~ 's') }} {{ title_text }}

{{ title_text }}

{% if not is_create %} {% endif %} ⌘S Save
{# ── Form completion progress ── #}
Form Completion 0%
{# ── Field search/filter ── #}
{# ═══ MAIN COLUMN ═══ #}
{% set action_url = (url_prefix|default('/admin') ~ '/' ~ model_name|lower ~ '/add') if is_create else (url_prefix|default('/admin') ~ '/' ~ model_name|lower ~ '/' ~ pk) %}
{% if csrf_token %}{% endif %} {# ── Unsaved changes indicator ── #}
You have unsaved changes
{# ── Render fields (with fieldset support) ── #} {% if fieldsets and fieldsets|length > 0 %} {% for section_name, section_opts in fieldsets %}
{{ section_name }} {% if section_opts is mapping and section_opts.get('fields') %} ({{ section_opts['fields']|length }} fields) {% endif %}
{% set section_fields = section_opts.get('fields', []) if section_opts is mapping else section_opts['fields'] if section_opts is mapping else [] %} {% for fld in fields %} {% if fld.name in section_fields %} {{ _render_field(fld) }} {% endif %} {% endfor %}
{% endfor %} {# Render any fields not in fieldsets #} {% set fieldseted = [] %} {% for section_name, section_opts in fieldsets %} {% if section_opts is mapping and section_opts.get('fields') %} {% for fn in section_opts['fields'] %} {% if fieldseted.append(fn) %}{% endif %} {% endfor %} {% endif %} {% endfor %} {% for fld in fields %} {% if fld.name not in fieldseted %} {{ _render_field(fld) }} {% endif %} {% endfor %} {% else %} {# No fieldsets — render all directly #} {% for fld in fields %} {{ _render_field(fld) }} {% endfor %} {% endif %} {# ── Inline Models ── #} {% if inlines is defined and inlines|length > 0 %} {% for inline in inlines %}
{{ inline.verbose_name_plural|default(inline.model_name ~ 's') }}
{{ inline.style|default('tabular') }} inline
{% if inline.style == 'tabular' and inline.fields|length > 0 %}
{% for f in inline.fields %} {% endfor %} {% if inline.can_delete|default(true) %} {% endif %} {% for row_idx in range(inline.extra|default(3)) %} {% for f in inline.fields %} {% endfor %} {% if inline.can_delete|default(true) %} {% endif %} {% endfor %}
{{ f.label|default(f.name|replace('_', ' ')|title) }}
{% elif inline.style == 'stacked' and inline.fields|length > 0 %}
{% for row_idx in range(inline.extra|default(3)) %}
{{ inline.verbose_name|default(inline.model_name) }} #{{ row_idx + 1 }} {% if inline.can_delete|default(true) %} {% endif %}
{% for f in inline.fields %}
{% if f.type == 'textarea' %} {% elif f.type == 'checkbox' %} {% else %} {% endif %}
{% endfor %}
{% endfor %}
{% endif %}
{% endfor %} {% endif %} {# ── Floating Action Bar (sticky bottom) ── #}
{% if save_as is defined and save_as and not is_create %} {% endif %} {% if not is_create %} {% endif %} Cancel {% if not is_create %}
History
{% endif %}
{# ── Change Inspector (SQL Preview + Diff) ── #} {% if not is_create %} {% endif %}
{% if can_delete and not is_create %}
Danger Zone
Permanently delete this {{ verbose_name }}. This action cannot be undone.
{% endif %}
{# ═══ SIDEBAR ═══ #}
{# ── Metadata card ── #} {% if not is_create %} {% endif %} {# ── Quick actions ── #} {# ── Field navigation (TOC) ── #} {# ── Keyboard shortcuts ── #}
{# /form-layout #} {# ── Query Inspector Panel (post-update SQL log) ── #} {% if query_inspection and query_inspection|length > 0 %}

Query Inspector

{{ query_inspection|length }} quer{{ 'y' if query_inspection|length == 1 else 'ies' }} executed
{% set total_ms = query_inspection | map(attribute='duration_ms') | sum %} {{ "%.2f"|format(total_ms) }} ms
{% for op in ['SELECT', 'UPDATE', 'INSERT', 'DELETE'] %} {% set count = 0 %} {% for q in query_inspection if q.operation == op %} {% set count = count + 1 %} {% endfor %} {% if count > 0 %} {{ count }} {{ op }} {% endif %} {% endfor %} {% set slow_count = query_inspection | selectattr('is_slow') | list | length %} {% if slow_count > 0 %} ⚠ {{ slow_count }} slow {% endif %}
{% for q in query_inspection %}
{{ q.operation }} {{ q.id }}
{% if q.rows_affected is defined and q.rows_affected > 0 %} {{ q.rows_affected }} row{{ 's' if q.rows_affected != 1 else '' }} {% endif %} {{ "%.3f"|format(q.duration_ms) }} ms
{{ q.sql }}
{% if q.params %}
Params: {{ q.params }}
{% endif %} {% if q.stack_summary %}
{{ q.stack_summary }}
{% endif %}
{% endfor %}
Open full Query Inspector →
{% endif %} {% endblock %} {# ── MACRO: render a single field based on type ── #} {% macro _render_field(fld) %} {% set name = fld.name|default('') %} {% set label = fld.label|default(name) %} {% set field_type = fld.type|default('text') %} {% set value = fld.value if fld.value is not none else '' %} {% set required = fld.required|default(false) and not fld.readonly|default(false) %} {% set readonly = fld.readonly|default(false) %} {% set help_text = fld.help_text|default('') %} {% set choices = fld.choices %} {% set max_length = fld.max_length|default(0, true) %}
{# ── Field toolbar (copy / undo) ── #} {% if not readonly %}
{% endif %} {# ── Label row ── #} {# ── CHECKBOX → Toggle switch ── #} {% if field_type == 'checkbox' %}
{# ── TEXTAREA (plain or JSON) ── #} {% elif field_type == 'textarea' %} {% set looks_json = (value is string and value.strip().startswith('{')) or (value is string and value.strip().startswith('[')) or (value is mapping) or (value is iterable and value is not string and value is not none) %} {% if looks_json %}
{% else %} {% if max_length and max_length > 0 %}
0 / {{ max_length }}
{% endif %} {% endif %} {# ── SELECT with choices ── #} {% elif field_type == 'select' and choices %} {# ── DATETIME-LOCAL ── #} {% elif field_type == 'datetime-local' %}
{% if not readonly %} {% endif %}
{# ── DATE ── #} {% elif field_type == 'date' %}
{% if not readonly %} {% endif %}
{# ── TIME ── #} {% elif field_type == 'time' %}
{% if not readonly %} {% endif %}
{# ── EMAIL ── #} {% elif field_type == 'email' %}
{# ── URL ── #} {% elif field_type == 'url' %}
{# ── NUMBER ── #} {% elif field_type == 'number' %}
{# ── DEFAULT TEXT ── #} {% else %} 0 %}maxlength="{{ max_length }}"{% endif %}> {% if max_length and max_length > 0 %}
{{ value|length if value else 0 }} / {{ max_length }}
{% endif %} {% endif %} {# ── Help text & validation ── #} {% if help_text %}
{{ help_text }}
{% endif %}
{% endmacro %} {% block extra_js %} (function() { var MODEL = '{{ model_name|lower }}'; var BASE_URL = '{{ url_prefix|default("/admin") }}/' + MODEL; var IS_CREATE = {{ 'true' if is_create else 'false' }}; var PK = '{{ pk|default("") }}'; var STORAGE_KEY = 'aq_form_draft_' + MODEL + '_' + (PK || 'new'); // ── Query Inspector toggle ────────────────────────────────────── window.toggleQueryInspector = function() { var content = document.getElementById('qi-content'); var chevron = document.getElementById('qi-chevron'); if (!content) return; if (content.style.display === 'none') { content.style.display = 'block'; if (chevron) chevron.style.transform = 'rotate(0deg)'; } else { content.style.display = 'none'; if (chevron) chevron.style.transform = 'rotate(-90deg)'; } }; // ── Fieldset collapse ─────────────────────────────────────────── window.toggleFieldset = function(header) { var body = header.nextElementSibling; var chevron = header.querySelector('.fieldset-chevron'); if (body.classList.contains('collapsed')) { body.classList.remove('collapsed'); if (chevron) chevron.classList.remove('collapsed'); } else { body.classList.add('collapsed'); if (chevron) chevron.classList.add('collapsed'); } }; // ── Toggle switch ─────────────────────────────────────────────── window.updateToggle = function(cb) { var track = document.getElementById('toggle-' + cb.name); var text = document.getElementById('toggle-text-' + cb.name); if (cb.checked) { if (track) track.classList.add('on'); if (text) text.textContent = 'Yes'; } else { if (track) track.classList.remove('on'); if (text) text.textContent = 'No'; } }; // ── Set Now buttons ───────────────────────────────────────────── window.setNow = function(fieldName, type) { var el = document.getElementById('field-' + fieldName); if (!el) return; var now = new Date(); if (type === 'datetime-local') { var y=now.getFullYear(), mo=String(now.getMonth()+1).padStart(2,'0'), d=String(now.getDate()).padStart(2,'0'); var h=String(now.getHours()).padStart(2,'0'), mi=String(now.getMinutes()).padStart(2,'0'), s=String(now.getSeconds()).padStart(2,'0'); el.value = y+'-'+mo+'-'+d+'T'+h+':'+mi+':'+s; } else if (type === 'date') { el.value = now.toISOString().split('T')[0]; } else if (type === 'time') { el.value = String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0'); } el.dispatchEvent(new Event('input', {bubbles:true})); el.dispatchEvent(new Event('change', {bubbles:true})); }; // ── JSON editor helpers ───────────────────────────────────────── window.formatJson = function(name) { var el = document.getElementById('field-' + name); if (!el) return; try { el.value = JSON.stringify(JSON.parse(el.value), null, 2); validateJson(name); } catch(e) {} }; window.minifyJson = function(name) { var el = document.getElementById('field-' + name); if (!el) return; try { el.value = JSON.stringify(JSON.parse(el.value)); validateJson(name); } catch(e) {} }; window.validateJson = function(name) { var el = document.getElementById('field-' + name), st = document.getElementById('json-status-' + name), wrap = document.getElementById('json-wrap-' + name); if (!el || !st) return; try { JSON.parse(el.value); st.className = 'json-status valid'; st.textContent = '✓ Valid JSON'; if (wrap) { wrap.classList.add('json-valid'); wrap.classList.remove('json-invalid'); } } catch(e) { st.className = 'json-status invalid'; st.textContent = '✗ ' + e.message.substring(0, 40); if (wrap) { wrap.classList.add('json-invalid'); wrap.classList.remove('json-valid'); } } }; window.copyFieldValue = function(name) { var el = document.getElementById('field-' + name); if (!el) return; var val = el.type === 'checkbox' ? (el.checked ? 'true' : 'false') : el.value; navigator.clipboard.writeText(val).then(function() { showToast('Copied to clipboard', 'success'); }); }; // Init JSON validators document.querySelectorAll('.json-editor-textarea').forEach(function(ta) { var nm = ta.name; if (nm) validateJson(nm); }); // ── Character counters ────────────────────────────────────────── document.querySelectorAll('.char-counter').forEach(function(counter) { var id = counter.id.replace('counter-', ''); var el = document.getElementById('field-' + id); if (!el) return; var max = parseInt(el.getAttribute('maxlength') || '0'); if (!max) { var m = counter.textContent.match(/\/ (\d+)/); if (m) max = parseInt(m[1]); } function update() { var len = el.value.length; counter.textContent = len + ' / ' + max; counter.className = 'char-counter' + (len > max ? ' over' : len > max * 0.9 ? ' warn' : ''); } el.addEventListener('input', update); update(); }); // ── Live field validation ─────────────────────────────────────── function validateField(el) { var grp = el.closest('.form-group-enhanced'); if (!grp) return; var vDiv = grp.querySelector('.field-validation'); if (!vDiv) return; var dt = grp.getAttribute('data-type') || 'text'; var val = el.value; var msg = ''; if (el.required && !val && el.type !== 'checkbox') msg = 'This field is required'; else if (dt === 'email' && val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) msg = 'Enter a valid email address'; else if (dt === 'url' && val && !/^https?:\/\/.+/.test(val)) msg = 'Enter a valid URL (https://...)'; else if (dt === 'number' && val && isNaN(Number(val))) msg = 'Enter a valid number'; if (msg) { vDiv.className = 'field-validation error'; vDiv.innerHTML = ' ' + msg; vDiv.style.display = 'flex'; } else if (val) { vDiv.style.display = 'none'; } else { vDiv.style.display = 'none'; } } // ── Form completion progress ──────────────────────────────────── function updateFormProgress() { var groups = document.querySelectorAll('.form-group-enhanced'); var total = 0, filled = 0; groups.forEach(function(grp) { if (grp.classList.contains('field-hidden')) return; var inp = grp.querySelector('input:not([type="hidden"]), textarea, select'); if (!inp || inp.readOnly || inp.disabled) return; total++; if (inp.type === 'checkbox') { filled++; } // checkboxes always count else if (inp.value && inp.value.trim() !== '') { filled++; } }); var pct = total > 0 ? Math.round((filled / total) * 100) : 0; var fill = document.getElementById('formProgressFill'); var pctEl = document.getElementById('formProgressPct'); if (fill) fill.style.width = pct + '%'; if (pctEl) pctEl.textContent = pct + '%'; // Update TOC dots document.querySelectorAll('.field-toc-item').forEach(function(item) { var target = item.getAttribute('data-target'); if (!target) return; var fieldId = target; var fieldEl = document.getElementById(fieldId); if (fieldEl) { var hasVal = fieldEl.type === 'checkbox' ? true : (fieldEl.value && fieldEl.value.trim() !== ''); if (hasVal) item.classList.add('filled'); else item.classList.remove('filled'); } }); } // ── Field search/filter ───────────────────────────────────────── var searchInput = document.getElementById('fieldSearchInput'); var searchClear = document.getElementById('fieldSearchClear'); if (searchInput) { searchInput.addEventListener('input', function() { var q = this.value.trim().toLowerCase(); searchClear.style.display = q ? 'block' : 'none'; var groups = document.querySelectorAll('.form-group-enhanced'); groups.forEach(function(grp) { var fieldName = (grp.getAttribute('data-field') || '').toLowerCase(); var labelEl = grp.querySelector('.form-label'); var labelText = labelEl ? labelEl.textContent.toLowerCase() : ''; if (!q || fieldName.indexOf(q) !== -1 || labelText.indexOf(q) !== -1) { grp.classList.remove('field-hidden'); } else { grp.classList.add('field-hidden'); } }); // Also filter fieldsets document.querySelectorAll('.fieldset-section').forEach(function(fs) { var visibleFields = fs.querySelectorAll('.form-group-enhanced:not(.field-hidden)'); if (q && visibleFields.length === 0) { fs.classList.add('field-hidden'); } else { fs.classList.remove('field-hidden'); } }); }); } window.clearFieldSearch = function() { if (searchInput) { searchInput.value = ''; searchInput.dispatchEvent(new Event('input')); } }; // ── Scroll to field (from TOC) ────────────────────────────────── window.scrollToField = function(name) { var el = document.getElementById('field-' + name); if (!el) return; var grp = el.closest('.form-group-enhanced'); if (grp) { grp.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Flash highlight grp.style.boxShadow = '0 0 0 3px var(--accent-subtle)'; grp.style.borderColor = 'var(--accent)'; grp.style.background = 'var(--accent-subtle)'; setTimeout(function() { grp.style.boxShadow = ''; grp.style.borderColor = ''; grp.style.background = ''; }, 1500); el.focus(); } // Update active TOC item document.querySelectorAll('.field-toc-item').forEach(function(item) { item.classList.remove('active'); }); var tocItem = document.querySelector('.field-toc-item[data-target="field-' + name + '"]'); if (tocItem) tocItem.classList.add('active'); }; // ── Form dirty tracking ───────────────────────────────────────── var form = document.getElementById('adminForm'); if (!form) return; var initialValues = {}; var inputs = form.querySelectorAll('input, textarea, select'); inputs.forEach(function(el) { if (!el.name || el.type === 'hidden') return; initialValues[el.name] = el.type === 'checkbox' ? el.checked : el.value; }); function getChangedFields() { var changes = []; inputs.forEach(function(el) { if (!el.name || el.type === 'hidden') return; var oldVal = initialValues[el.name]; var newVal = el.type === 'checkbox' ? el.checked : el.value; if (oldVal !== undefined && String(oldVal) !== String(newVal)) { var grp = el.closest('.form-group-enhanced') || el.closest('.form-group'); var lbl = grp ? grp.querySelector('.form-label') : null; var labelText = lbl ? lbl.textContent.replace(/\*|Read-only|datetime-local|checkbox|text|number|textarea|select|email|url|time|date/g, '').trim() : el.name; var ft = grp ? (grp.getAttribute('data-type') || 'text') : 'text'; changes.push({ name: el.name, label: labelText, oldVal: String(oldVal), newVal: String(newVal), fieldType: ft }); } }); return changes; } function updateDirtyState() { var changes = getChangedFields(); var bar = document.getElementById('unsaved-bar'); var count = document.getElementById('changed-count'); var inspBtn = document.getElementById('inspectorBtn'); var resetBtn = document.getElementById('resetBtn'); var fab = document.getElementById('floatingActionBar'); var sidebarStatus = document.getElementById('sidebarStatus'); if (changes.length > 0) { form.dataset.dirty = 'true'; if (bar) bar.style.display = 'flex'; if (count) count.textContent = changes.length + ' field' + (changes.length > 1 ? 's' : ''); if (inspBtn) inspBtn.style.display = 'inline-flex'; if (resetBtn) resetBtn.style.display = 'inline-flex'; if (fab) fab.classList.add('has-changes'); if (sidebarStatus) { sidebarStatus.textContent = 'Unsaved'; sidebarStatus.style.color = '#f59e0b'; } // Show undo buttons for changed fields changes.forEach(function(c) { var ub = document.getElementById('undo-' + c.name); if (ub) ub.style.display = 'inline-flex'; }); } else { form.dataset.dirty = 'false'; if (bar) bar.style.display = 'none'; if (inspBtn) inspBtn.style.display = 'none'; if (resetBtn) resetBtn.style.display = 'none'; if (fab) fab.classList.remove('has-changes'); if (sidebarStatus) { sidebarStatus.textContent = 'Saved'; sidebarStatus.style.color = 'var(--accent)'; } var ci = document.getElementById('change-inspector'); if (ci) ci.style.display = 'none'; document.querySelectorAll('[id^="undo-"]').forEach(function(b) { b.style.display = 'none'; }); } updateFormProgress(); } inputs.forEach(function(el) { el.addEventListener('input', function() { updateDirtyState(); validateField(el); saveDraft(); }); el.addEventListener('change', function() { updateDirtyState(); validateField(el); saveDraft(); }); }); window.addEventListener('beforeunload', function(e) { if (form.dataset.dirty === 'true') { e.preventDefault(); e.returnValue = ''; } }); form.addEventListener('submit', function() { form.dataset.dirty = 'false'; clearDraft(); }); // ── Field-level undo ──────────────────────────────────────────── window.undoField = function(name) { var el = document.getElementById('field-' + name); if (!el) return; var orig = initialValues[name]; if (orig === undefined) return; if (el.type === 'checkbox') { el.checked = orig; updateToggle(el); } else { el.value = String(orig); } var ub = document.getElementById('undo-' + name); if (ub) ub.style.display = 'none'; updateDirtyState(); showToast('Reverted "' + name + '"', 'info'); }; // ── Reset form ────────────────────────────────────────────────── window.resetForm = function() { inputs.forEach(function(el) { if (!el.name || el.type === 'hidden') return; var orig = initialValues[el.name]; if (orig === undefined) return; if (el.type === 'checkbox') { el.checked = orig; updateToggle(el); } else { el.value = String(orig); } }); updateDirtyState(); clearDraft(); showToast('Form reset to original values', 'info'); }; // ── Autosave drafts to localStorage ───────────────────────────── function saveDraft() { try { var data = {}; inputs.forEach(function(el) { if (!el.name || el.type === 'hidden' || el.readOnly || el.disabled) return; data[el.name] = el.type === 'checkbox' ? el.checked : el.value; }); localStorage.setItem(STORAGE_KEY, JSON.stringify({ ts: Date.now(), data: data })); var st = document.getElementById('autosaveStatus'), txt = document.getElementById('autosaveText'); if (st) st.className = 'autosave-indicator saved'; if (txt) txt.textContent = 'Draft saved'; setTimeout(function() { if (st) st.className = 'autosave-indicator'; if (txt) txt.textContent = ''; }, 2000); } catch(e) {} } function clearDraft() { try { localStorage.removeItem(STORAGE_KEY); } catch(e) {} } function restoreDraft() { try { var raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; var saved = JSON.parse(raw); if (!saved.data) return; // Only restore if saved less than 24h ago if (Date.now() - saved.ts > 86400000) { clearDraft(); return; } var hasChanges = false; for (var k in saved.data) { var el = document.getElementById('field-' + k); if (!el || el.readOnly || el.disabled) continue; var sv = saved.data[k]; if (el.type === 'checkbox') { if (sv !== el.checked) { el.checked = sv; updateToggle(el); hasChanges = true; } } else { if (String(sv) !== el.value) { el.value = String(sv); hasChanges = true; } } } if (hasChanges) { updateDirtyState(); showToast('Draft restored from autosave', 'info'); } } catch(e) {} } if (!IS_CREATE) restoreDraft(); // ── Change Inspector (SQL preview) ────────────────────────────── window.toggleChangeInspector = function() { var ci = document.getElementById('change-inspector'); if (!ci) return; if (ci.style.display !== 'none') { ci.style.display = 'none'; return; } var changes = getChangedFields(); if (!changes.length) { ci.style.display = 'none'; return; } renderChangeInspector(changes); ci.style.display = 'block'; ci.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }; function renderChangeInspector(changes) { var body = document.getElementById('inspectorContent'); var subtitle = document.getElementById('inspectorSubtitle'); if (!body) return; if (subtitle) subtitle.textContent = changes.length + ' field' + (changes.length > 1 ? 's' : '') + ' modified'; var html = '
'; html += '' + changes.length + ' change' + (changes.length > 1 ? 's' : '') + ''; html += 'Table: ' + MODEL + ''; html += 'PK: ' + PK + ''; html += '
'; // Diff table html += ''; changes.forEach(function(c) { var dispOld = c.oldVal === 'true' ? '✓ Yes' : c.oldVal === 'false' ? '✗ No' : (c.oldVal || 'empty'); var dispNew = c.newVal === 'true' ? '✓ Yes' : c.newVal === 'false' ? '✗ No' : (c.newVal || 'empty'); html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
FieldTypeCurrentNew
' + escHtml(c.label) + '' + escHtml(c.fieldType) + '− ' + dispOld + '+ ' + dispNew + '
'; // SQL preview html += '
'; html += 'SQL Preview (estimated)'; html += 'Preview only — actual query may differ'; html += '
'; var setClauses = changes.map(function(c) { var val = c.newVal; if (c.fieldType === 'checkbox') val = (c.newVal === 'true' ? '1' : '0'); else if (c.fieldType === 'number') val = c.newVal; else val = "'" + c.newVal.replace(/'/g, "''") + "'"; return '"' + c.name + '" = ' + val; }); var sqlText = 'UPDATE "' + MODEL + '"\n SET ' + setClauses.join(',\n ') + '\n WHERE "id" = ' + PK + ';'; var sqlHtml = sqlText .replace(/\b(UPDATE|SET|WHERE|AND|OR|INSERT INTO|VALUES|DELETE FROM|SELECT|FROM|JOIN|ON|LEFT|RIGHT|INNER|GROUP BY|ORDER BY|LIMIT|OFFSET)\b/g, '$1') .replace(/'([^']*)'/g, "'$1'") .replace(/"([^"]*)"/g, '"$1"') .replace(/\b(=|!=|<|>|<=|>=|LIKE|IN|IS|NOT|NULL|BETWEEN)\b/g, '$1'); html += '
'; html += ''; html += sqlHtml + '
'; html += '
'; html += ''; html += 'Estimated SQL. Actual query may include auto_now fields, defaults, and ORM-level validations.'; html += '
'; body.innerHTML = html; } window.copySqlPreview = function() { var block = document.getElementById('sqlPreviewBlock'); if (!block) return; var text = block.textContent.replace('Copy', '').trim(); navigator.clipboard.writeText(text).then(function() { showToast('SQL copied to clipboard', 'success'); }); }; function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // ── Keyboard shortcuts ────────────────────────────────────────── document.addEventListener('keydown', function(e) { if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); form.submit(); } if ((e.metaKey || e.ctrlKey) && e.key === 'f' && !e.shiftKey) { // Focus field search if visible (only intercept when not in an input) var active = document.activeElement; var isInField = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.tagName === 'SELECT'); if (!isInField && searchInput) { e.preventDefault(); searchInput.focus(); searchInput.select(); } } if (e.key === 'Escape') { // Clear field search first if (searchInput && document.activeElement === searchInput && searchInput.value) { clearFieldSearch(); searchInput.blur(); return; } var ci = document.getElementById('change-inspector'); if (ci && ci.style.display !== 'none') { ci.style.display = 'none'; return; } var modal = document.getElementById('delete-modal-overlay'); if (modal) { modal.remove(); } } }); // ── Custom delete confirmation modal ──────────────────────────── window.showDeleteModal = function() { var isDark = document.documentElement.getAttribute('data-theme') !== 'light'; var overlay = document.createElement('div'); overlay.id = 'delete-modal-overlay'; overlay.style.cssText = 'position:fixed;inset:0;background:' + (isDark ? 'rgba(0,0,0,0.65)' : 'rgba(0,0,0,0.3)') + ';z-index:10003;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);'; var box = document.createElement('div'); box.style.cssText = 'background:' + (isDark ? 'var(--bg-elevated)' : '#fff') + ';border:1px solid var(--border-color);border-radius:14px;padding:28px 32px;max-width:420px;width:90%;box-shadow:0 24px 64px rgba(0,0,0,' + (isDark ? '0.5' : '0.15') + ');'; box.innerHTML = '
' + '
' + '' + '
' + '
' + '

Delete this record?

' + '

This action cannot be undone. The record will be permanently removed from the database.

' + '
' + '
' + '
' + '' + '' + '
'; overlay.appendChild(box); document.body.appendChild(overlay); overlay.querySelector('#del-cancel').onclick = function() { overlay.remove(); }; overlay.querySelector('#del-confirm').onclick = function() { overlay.remove(); document.getElementById('deleteRecordForm').submit(); }; overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); }); document.addEventListener('keydown', function handler(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', handler); } }); overlay.querySelector('#del-confirm').focus(); }; // ── Inline Row Management ─────────────────────────────────────── window.addInlineRow = function(btn) { var section = btn.closest('.inline-section'); if (!section) return; var container = section.querySelector('.inline-rows'); if (!container) return; var maxNum = parseInt(container.getAttribute('data-max') || '0'); var existing = container.querySelectorAll('.inline-row'); if (maxNum > 0 && existing.length >= maxNum) { showToast('Maximum number of entries reached (' + maxNum + ')', 'warning'); return; } var newIdx = existing.length; var templateRow = existing[existing.length - 1]; if (!templateRow) return; var clone = templateRow.cloneNode(true); clone.setAttribute('data-row', newIdx); // Update input names and clear values clone.querySelectorAll('input, textarea, select').forEach(function(inp) { var name = inp.getAttribute('name') || ''; // Replace the old row index with new var parts = name.split('__'); if (parts.length >= 4) { parts[2] = String(newIdx); inp.setAttribute('name', parts.join('__')); } if (inp.type === 'checkbox') { inp.checked = false; } else { inp.value = ''; } }); // Update stacked row label var label = clone.querySelector('[style*="font-weight:600"]'); if (label && label.textContent.includes('#')) { var baseName = label.textContent.replace(/#\d+/, '').trim(); label.textContent = baseName + ' #' + (newIdx + 1); } container.appendChild(clone); // Focus first input var firstInput = clone.querySelector('input:not([readonly]), textarea:not([readonly])'); if (firstInput) firstInput.focus(); }; // ── Active field tracking (for TOC) ───────────────────────────── var observer = new IntersectionObserver(function(entries) { entries.forEach(function(entry) { if (entry.isIntersecting) { var grp = entry.target; var fieldName = grp.getAttribute('data-field'); if (!fieldName) return; document.querySelectorAll('.field-toc-item').forEach(function(item) { item.classList.remove('active'); }); var tocItem = document.querySelector('.field-toc-item[data-target="field-' + fieldName + '"]'); if (tocItem) tocItem.classList.add('active'); } }); }, { threshold: 0.5, rootMargin: '-20% 0px -60% 0px' }); document.querySelectorAll('.form-group-enhanced').forEach(function(grp) { observer.observe(grp); }); // ── Initial progress update ───────────────────────────────────── updateFormProgress(); // ── Toast ─────────────────────────────────────────────────────── if (typeof showToast === 'undefined') { window.showToast = function(msg, type) { type = type || 'info'; var el = document.createElement('div'); el.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:10010;padding:12px 20px;border-radius:10px;font-size:0.82rem;font-weight:500;box-shadow:0 8px 32px rgba(0,0,0,0.3);max-width:360px;font-family:var(--font-sans);backdrop-filter:blur(8px);'; var c = { success:'background:rgba(34,197,94,0.15);color:#22c55e;border:1px solid rgba(34,197,94,0.3);', error:'background:rgba(239,68,68,0.15);color:#ef4444;border:1px solid rgba(239,68,68,0.3);', warning:'background:rgba(234,179,8,0.15);color:#eab308;border:1px solid rgba(234,179,8,0.3);', info:'background:rgba(59,130,246,0.15);color:#3b82f6;border:1px solid rgba(59,130,246,0.3);' }; el.style.cssText += (c[type] || c.info); el.textContent = msg; document.body.appendChild(el); setTimeout(function() { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(function() { el.remove(); }, 300); }, 3500); }; } })(); {% endblock %}