Skip to content

Commit 2515aa6

Browse files
add JSON pretty printing to Installation admin change form (#112)
1 parent f4ce3a7 commit 2515aa6

File tree

4 files changed

+100
-91
lines changed

4 files changed

+100
-91
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
2020

2121
### Added
2222

23-
- Added custom admin change form for `EventLog` with JSON pretty printing and copy functionality for webhook payloads.
23+
- Added JSON pretty printing and copy functionality to `EventLog` and `Installation` admin change forms
2424

2525
## [0.9.0]
2626

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,5 @@
1-
{# inspired by https://til.simonwillison.net/django/pretty-print-json-admin #}
21
{% extends "admin/change_form.html" %}
32
{% block admin_change_form_document_ready %}
43
{{ 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>
4+
{% include "admin/django_github_app/includes/json_prettify_script.html" %}
945
{% endblock %}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{# inspired by https://til.simonwillison.net/django/pretty-print-json-admin #}
2+
<script>
3+
// Recursively sort object keys (case-insensitive)
4+
const sortObject = (obj, depth = 0) => {
5+
if (depth > 100) return obj;
6+
7+
if (obj === null || typeof obj !== 'object') {
8+
return obj;
9+
}
10+
11+
if (Array.isArray(obj)) {
12+
return obj.map(item => sortObject(item, depth + 1));
13+
}
14+
15+
return Object.keys(obj)
16+
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
17+
.reduce((sorted, key) => {
18+
sorted[key] = sortObject(obj[key], depth + 1);
19+
return sorted;
20+
}, {});
21+
};
22+
23+
// Make sure to only target the two fields we care about:
24+
// - EventLog.payload
25+
// - Installation.data
26+
Array.from(document.querySelectorAll('.field-data div.readonly, .field-payload 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>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% extends "admin/change_form.html" %}
2+
{% block admin_change_form_document_ready %}
3+
{{ block.super }}
4+
{% include "admin/django_github_app/includes/json_prettify_script.html" %}
5+
{% endblock %}

0 commit comments

Comments
 (0)