|
| 1 | +{% extends "admin/base_site.html" %} |
| 2 | +{% load i18n %} |
| 3 | + |
| 4 | +{% block title %}{{ title }}{% endblock %} |
| 5 | + |
| 6 | +{% block extrahead %} |
| 7 | +{{ block.super }} |
| 8 | +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"> |
| 9 | +<style> |
| 10 | + .diff-container { display: flex; gap: 0; font-family: monospace; font-size: 13px; border: 1px solid #ccc; border-radius: 4px; overflow: hidden; } |
| 11 | + .diff-panel { flex: 1; overflow: auto; max-height: 75vh; } |
| 12 | + .diff-panel-header { position: sticky; top: 0; z-index: 10; background: #f0f0f0; padding: 8px 12px; font-weight: bold; border-bottom: 1px solid #ccc; font-family: -apple-system, sans-serif; } |
| 13 | + .diff-panel:not(:last-child) { border-right: 1px solid #ccc; } |
| 14 | + .diff-line { display: flex; padding: 0 8px; min-height: 20px; line-height: 20px; white-space: pre; } |
| 15 | + .diff-line-num { color: #999; width: 40px; text-align: right; padding-right: 8px; flex-shrink: 0; user-select: none; } |
| 16 | + .diff-line-content { flex: 1; } |
| 17 | + .diff-added { background: #e6ffed; } |
| 18 | + .diff-removed { background: #ffeef0; } |
| 19 | + .diff-changed { background: #fff8c5; } |
| 20 | + |
| 21 | + .summary-panel { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 16px; margin-bottom: 20px; } |
| 22 | + .summary-panel h3 { margin: 0 0 12px; font-size: 16px; } |
| 23 | + .summary-item { padding: 4px 0; font-size: 14px; } |
| 24 | + .summary-item .badge { font-size: 11px; padding: 3px 8px; border-radius: 3px; margin-right: 6px; } |
| 25 | + .badge-added { background: #28a745; color: #fff; } |
| 26 | + .badge-removed { background: #dc3545; color: #fff; } |
| 27 | + .badge-changed { background: #ffc107; color: #333; } |
| 28 | + .badge-identical { background: #6c757d; color: #fff; } |
| 29 | + .badge-meta { background: #17a2b8; color: #fff; } |
| 30 | + .diff-toggle-bar { margin: 12px 0; display: flex; gap: 8px; } |
| 31 | + .diff-toggle-bar button { padding: 6px 14px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; background: #fff; font-size: 13px; } |
| 32 | + .diff-toggle-bar button.active { background: #007bff; color: #fff; border-color: #007bff; } |
| 33 | +</style> |
| 34 | +{% endblock %} |
| 35 | + |
| 36 | +{% block content %} |
| 37 | +<h2 style="margin-bottom: 16px;">{{ title }}</h2> |
| 38 | + |
| 39 | +{% if summary %} |
| 40 | +{% for s in summary %} |
| 41 | +<div class="summary-panel"> |
| 42 | + <h3> |
| 43 | + {% if s.identical %} |
| 44 | + <span class="badge badge-identical">Identical</span> |
| 45 | + {% endif %} |
| 46 | + {{ s.left }} ↔ {{ s.right }} |
| 47 | + </h3> |
| 48 | + {% if s.diffs %} |
| 49 | + <div> |
| 50 | + {% for d in s.diffs %} |
| 51 | + <div class="summary-item"> |
| 52 | + {% if "added" in d|lower %} |
| 53 | + <span class="badge badge-added">+</span> |
| 54 | + {% elif "removed" in d|lower %} |
| 55 | + <span class="badge badge-removed">−</span> |
| 56 | + {% elif "modified" in d|lower or "→" in d %} |
| 57 | + <span class="badge badge-changed">Δ</span> |
| 58 | + {% else %} |
| 59 | + <span class="badge badge-meta">ℹ</span> |
| 60 | + {% endif %} |
| 61 | + {{ d }} |
| 62 | + </div> |
| 63 | + {% endfor %} |
| 64 | + </div> |
| 65 | + {% else %} |
| 66 | + <p style="color: #6c757d; margin: 0;">The two forms are identical.</p> |
| 67 | + {% endif %} |
| 68 | +</div> |
| 69 | +{% endfor %} |
| 70 | +{% endif %} |
| 71 | + |
| 72 | +<div class="diff-toggle-bar"> |
| 73 | + <button id="btn-side" class="active" onclick="setMode('side')">Side by Side</button> |
| 74 | + <button id="btn-inline" onclick="setMode('inline')">Inline</button> |
| 75 | + <button id="btn-raw" onclick="setMode('raw')">Raw JSON</button> |
| 76 | +</div> |
| 77 | + |
| 78 | +<div id="diff-output"></div> |
| 79 | + |
| 80 | +<!-- Raw JSON panels (hidden by default) --> |
| 81 | +<div id="raw-panels" style="display:none;"> |
| 82 | + <div class="diff-container"> |
| 83 | + {% for f in forms_json %} |
| 84 | + <div class="diff-panel"> |
| 85 | + <div class="diff-panel-header">{{ f.name }} ({{ f.slug }})</div> |
| 86 | + <pre style="margin:0; padding:8px; font-size:12px; white-space:pre-wrap;">{{ f.json }}</pre> |
| 87 | + </div> |
| 88 | + {% endfor %} |
| 89 | + </div> |
| 90 | +</div> |
| 91 | + |
| 92 | +{% endblock %} |
| 93 | + |
| 94 | +{% block extrajs %} |
| 95 | +{{ block.super }} |
| 96 | +<script src="https://cdn.jsdelivr.net/npm/diff@5.2.0/dist/diff.min.js"></script> |
| 97 | +<script> |
| 98 | +const formsJson = {{ forms_json_escaped|safe }}; |
| 99 | +const formNames = {{ form_names|safe }}; |
| 100 | +let currentMode = 'side'; |
| 101 | + |
| 102 | +function escapeHtml(s) { |
| 103 | + return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
| 104 | +} |
| 105 | + |
| 106 | +function renderSideBySide(a, b, nameA, nameB) { |
| 107 | + const diff = Diff.diffLines(a, b); |
| 108 | + let leftLines = [], rightLines = []; |
| 109 | + let leftNum = 0, rightNum = 0; |
| 110 | + |
| 111 | + diff.forEach(part => { |
| 112 | + const lines = part.value.replace(/\n$/, '').split('\n'); |
| 113 | + lines.forEach(line => { |
| 114 | + if (part.added) { |
| 115 | + rightNum++; |
| 116 | + leftLines.push({num: '', text: '', cls: 'diff-removed'}); |
| 117 | + rightLines.push({num: rightNum, text: line, cls: 'diff-added'}); |
| 118 | + } else if (part.removed) { |
| 119 | + leftNum++; |
| 120 | + leftLines.push({num: leftNum, text: line, cls: 'diff-removed'}); |
| 121 | + rightLines.push({num: '', text: '', cls: 'diff-added'}); |
| 122 | + } else { |
| 123 | + leftNum++; rightNum++; |
| 124 | + leftLines.push({num: leftNum, text: line, cls: ''}); |
| 125 | + rightLines.push({num: rightNum, text: line, cls: ''}); |
| 126 | + } |
| 127 | + }); |
| 128 | + }); |
| 129 | + |
| 130 | + let html = '<div class="diff-container">'; |
| 131 | + html += '<div class="diff-panel"><div class="diff-panel-header">' + escapeHtml(nameA) + '</div>'; |
| 132 | + leftLines.forEach(l => { |
| 133 | + html += '<div class="diff-line ' + l.cls + '"><span class="diff-line-num">' + l.num + '</span><span class="diff-line-content">' + escapeHtml(l.text) + '</span></div>'; |
| 134 | + }); |
| 135 | + html += '</div>'; |
| 136 | + html += '<div class="diff-panel"><div class="diff-panel-header">' + escapeHtml(nameB) + '</div>'; |
| 137 | + rightLines.forEach(l => { |
| 138 | + html += '<div class="diff-line ' + l.cls + '"><span class="diff-line-num">' + l.num + '</span><span class="diff-line-content">' + escapeHtml(l.text) + '</span></div>'; |
| 139 | + }); |
| 140 | + html += '</div></div>'; |
| 141 | + return html; |
| 142 | +} |
| 143 | + |
| 144 | +function renderInline(a, b, nameA, nameB) { |
| 145 | + const diff = Diff.diffLines(a, b); |
| 146 | + let html = '<div style="border:1px solid #ccc; border-radius:4px; overflow:auto; max-height:75vh; font-family:monospace; font-size:13px;">'; |
| 147 | + html += '<div style="position:sticky;top:0;z-index:10;background:#f0f0f0;padding:8px 12px;border-bottom:1px solid #ccc;font-weight:bold;font-family:-apple-system,sans-serif;">' + escapeHtml(nameA) + ' ↔ ' + escapeHtml(nameB) + '</div>'; |
| 148 | + let num = 0; |
| 149 | + diff.forEach(part => { |
| 150 | + const lines = part.value.replace(/\n$/, '').split('\n'); |
| 151 | + const cls = part.added ? 'diff-added' : part.removed ? 'diff-removed' : ''; |
| 152 | + const prefix = part.added ? '+' : part.removed ? '-' : ' '; |
| 153 | + lines.forEach(line => { |
| 154 | + if (!part.added && !part.removed) num++; |
| 155 | + html += '<div class="diff-line ' + cls + '"><span class="diff-line-num">' + (cls ? '' : num) + '</span><span class="diff-line-content">' + prefix + ' ' + escapeHtml(line) + '</span></div>'; |
| 156 | + }); |
| 157 | + }); |
| 158 | + html += '</div>'; |
| 159 | + return html; |
| 160 | +} |
| 161 | + |
| 162 | +function setMode(mode) { |
| 163 | + currentMode = mode; |
| 164 | + document.getElementById('btn-side').classList.toggle('active', mode === 'side'); |
| 165 | + document.getElementById('btn-inline').classList.toggle('active', mode === 'inline'); |
| 166 | + document.getElementById('btn-raw').classList.toggle('active', mode === 'raw'); |
| 167 | + document.getElementById('raw-panels').style.display = mode === 'raw' ? '' : 'none'; |
| 168 | + const out = document.getElementById('diff-output'); |
| 169 | + if (mode === 'raw') { out.innerHTML = ''; return; } |
| 170 | + |
| 171 | + let html = ''; |
| 172 | + for (let i = 1; i < formsJson.length; i++) { |
| 173 | + if (mode === 'side') { |
| 174 | + html += renderSideBySide(formsJson[0], formsJson[i], formNames[0], formNames[i]); |
| 175 | + } else { |
| 176 | + html += renderInline(formsJson[0], formsJson[i], formNames[0], formNames[i]); |
| 177 | + } |
| 178 | + if (i < formsJson.length - 1) html += '<hr>'; |
| 179 | + } |
| 180 | + out.innerHTML = html; |
| 181 | +} |
| 182 | + |
| 183 | +// Initial render |
| 184 | +document.addEventListener('DOMContentLoaded', () => setMode('side')); |
| 185 | +</script> |
| 186 | +{% endblock %} |
| 187 | + |
0 commit comments