Skip to content

Commit f4ce3a7

Browse files
Add JSON pretty printing to EventLog admin change form (#106)
1 parent c4ba8ad commit f4ce3a7

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- Added custom admin change form for `EventLog` with JSON pretty printing and copy functionality for webhook payloads.
24+
2125
## [0.9.0]
2226

2327
### Changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{# inspired by https://til.simonwillison.net/django/pretty-print-json-admin #}
2+
{% extends "admin/change_form.html" %}
3+
{% block admin_change_form_document_ready %}
4+
{{ block.super }}
5+
<script>
6+
// Recursively sort object keys (case-insensitive)
7+
const sortObject = (obj, depth = 0) => {
8+
if (depth > 100) return obj;
9+
10+
if (obj === null || typeof obj !== 'object') {
11+
return obj;
12+
}
13+
14+
if (Array.isArray(obj)) {
15+
return obj.map(item => sortObject(item, depth + 1));
16+
}
17+
18+
return Object.keys(obj)
19+
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
20+
.reduce((sorted, key) => {
21+
sorted[key] = sortObject(obj[key], depth + 1);
22+
return sorted;
23+
}, {});
24+
};
25+
26+
Array.from(document.querySelectorAll('div.readonly')).forEach(div => {
27+
let data;
28+
try {
29+
data = JSON.parse(div.textContent || div.innerText);
30+
} catch {
31+
return;
32+
}
33+
34+
Object.assign(div.style, {
35+
backgroundColor: 'var(--darkened-bg, #f5f5f5)',
36+
border: '1px solid var(--border-color, #ddd)',
37+
borderRadius: '0.25rem',
38+
fontFamily: "ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace",
39+
fontSize: '.75rem',
40+
lineHeight: '1.4',
41+
padding: '0.75rem',
42+
overflow: 'auto',
43+
maxHeight: '600px',
44+
whiteSpace: 'pre'
45+
});
46+
div.textContent = JSON.stringify(sortObject(data), null, 2);
47+
48+
const label = div.previousElementSibling;
49+
if (label && label.tagName === 'LABEL') {
50+
label.style.display = 'flex';
51+
label.style.flexDirection = 'column';
52+
label.style.alignItems = 'flex-start';
53+
label.style.gap = '0.5rem';
54+
55+
const copyBtn = document.createElement('button');
56+
copyBtn.textContent = 'Copy';
57+
copyBtn.type = 'button';
58+
Object.assign(copyBtn.style, {
59+
backgroundColor: 'var(--button-bg, var(--body-bg, #fff))',
60+
border: '1px solid var(--border-color, #ddd)',
61+
borderRadius: '0.2rem',
62+
color: 'var(--body-fg, #333)',
63+
cursor: 'pointer',
64+
opacity: '0.8',
65+
padding: '0.2rem 0.5rem',
66+
transition: 'opacity 0.2s',
67+
});
68+
69+
copyBtn.onmouseover = () => copyBtn.style.opacity = '1';
70+
copyBtn.onmouseout = () => copyBtn.style.opacity = '0.8';
71+
copyBtn.onclick = async (e) => {
72+
e.preventDefault();
73+
e.stopPropagation();
74+
try {
75+
await navigator.clipboard.writeText(div.textContent);
76+
const originalText = copyBtn.textContent;
77+
copyBtn.textContent = 'Copied!';
78+
setTimeout(() => copyBtn.textContent = originalText, 1500);
79+
} catch {
80+
const selection = window.getSelection();
81+
const range = document.createRange();
82+
range.selectNodeContents(div);
83+
selection.removeAllRanges();
84+
selection.addRange(range);
85+
document.execCommand('copy');
86+
selection.removeAllRanges();
87+
}
88+
};
89+
90+
label.appendChild(copyBtn);
91+
}
92+
});
93+
</script>
94+
{% endblock %}

0 commit comments

Comments
 (0)