Skip to content

Commit 9ad89fd

Browse files
add JSON pretty printing to Installation admin change form
1 parent f4ce3a7 commit 9ad89fd

File tree

4 files changed

+97
-91
lines changed

4 files changed

+97
-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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
Array.from(document.querySelectorAll('.field-data div.readonly, .field-payload div.readonly')).forEach(div => {
24+
let data;
25+
try {
26+
data = JSON.parse(div.textContent || div.innerText);
27+
} catch {
28+
return;
29+
}
30+
31+
Object.assign(div.style, {
32+
backgroundColor: 'var(--darkened-bg, #f5f5f5)',
33+
border: '1px solid var(--border-color, #ddd)',
34+
borderRadius: '0.25rem',
35+
fontFamily: "ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace",
36+
fontSize: '.75rem',
37+
lineHeight: '1.4',
38+
padding: '0.75rem',
39+
overflow: 'auto',
40+
maxHeight: '600px',
41+
whiteSpace: 'pre'
42+
});
43+
div.textContent = JSON.stringify(sortObject(data), null, 2);
44+
45+
const label = div.previousElementSibling;
46+
if (label && label.tagName === 'LABEL') {
47+
label.style.display = 'flex';
48+
label.style.flexDirection = 'column';
49+
label.style.alignItems = 'flex-start';
50+
label.style.gap = '0.5rem';
51+
52+
const copyBtn = document.createElement('button');
53+
copyBtn.textContent = 'Copy';
54+
copyBtn.type = 'button';
55+
Object.assign(copyBtn.style, {
56+
backgroundColor: 'var(--button-bg, var(--body-bg, #fff))',
57+
border: '1px solid var(--border-color, #ddd)',
58+
borderRadius: '0.2rem',
59+
color: 'var(--body-fg, #333)',
60+
cursor: 'pointer',
61+
opacity: '0.8',
62+
padding: '0.2rem 0.5rem',
63+
transition: 'opacity 0.2s',
64+
});
65+
66+
copyBtn.onmouseover = () => copyBtn.style.opacity = '1';
67+
copyBtn.onmouseout = () => copyBtn.style.opacity = '0.8';
68+
copyBtn.onclick = async (e) => {
69+
e.preventDefault();
70+
e.stopPropagation();
71+
try {
72+
await navigator.clipboard.writeText(div.textContent);
73+
const originalText = copyBtn.textContent;
74+
copyBtn.textContent = 'Copied!';
75+
setTimeout(() => copyBtn.textContent = originalText, 1500);
76+
} catch {
77+
const selection = window.getSelection();
78+
const range = document.createRange();
79+
range.selectNodeContents(div);
80+
selection.removeAllRanges();
81+
selection.addRange(range);
82+
document.execCommand('copy');
83+
selection.removeAllRanges();
84+
}
85+
};
86+
87+
label.appendChild(copyBtn);
88+
}
89+
});
90+
</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)