ArchiveBox/archivebox/templates/admin/base.html
2026-06-01 00:08:27 -07:00

2480 lines
97 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% load i18n static tz core_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% get_current_language_bidi as LANGUAGE_BIDI %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
<head>
<title>{% block title %}Home{% endblock %} | ArchiveBox</title>
{% block blockbots %}
<meta name="robots" content="NONE,NOARCHIVE">
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
{% api_token as api_token %}
<script>
window.ARCHIVEBOX_API_KEY = "{{ api_token|escapejs }}";
</script>
{% block extrastyle %}
<style>
#upgrade-banner {
position: fixed;
right: 20px;
bottom: 20px;
background-color: #f8f8f8;
color: #333333;
border: 2px solid #772948;
padding: 10px 20px;
z-index: 1000;
text-align: center;
}
#dismiss-btn {
background: #aa1e55;
color: white;
cursor: pointer;
}
/* ============================================
Modern card-based admin UI (shadcn-inspired)
============================================ */
/* Base font improvements */
body, html {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 15px;
line-height: 1.6;
color: #0f172a;
background: #f8fafc;
}
#container {
background: #f8fafc;
}
#content {
padding: 24px;
}
/* Main form container - flexbox grid */
body:not(.change-list) #content-main form > div,
body:not(.change-list) #content form > div {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: stretch;
}
/* Each fieldset becomes a card */
#content-main form fieldset,
#content form fieldset,
#content-main form .module:not(.inline-group),
#content form .module:not(.inline-group) {
background: #fff !important;
border: 1px solid #e2e8f0 !important;
border-top: 1px solid #e2e8f0 !important;
border-left: 1px solid #e2e8f0 !important;
border-right: 1px solid #e2e8f0 !important;
border-bottom: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
padding: 0 !important;
margin: 0 !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
flex: 1 1 340px;
min-width: 320px;
max-width: calc(33.33% - 14px);
box-sizing: border-box;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
overflow: hidden;
}
/* Wide fieldsets MUST override card max-width - placed after card rules for specificity */
#content-main form fieldset.wide,
#content form fieldset.wide,
#content-main form fieldset:has(.field-archiveresults_list),
#content form fieldset:has(.field-archiveresults_list),
#content-main form fieldset:has(.field-snapshots),
#content form fieldset:has(.field-snapshots) {
flex: 1 1 100% !important;
max-width: 100% !important;
min-width: 100% !important;
width: 100% !important;
flex-basis: 100% !important;
}
/* Inline groups should NOT have card constraints */
#content-main form .inline-group,
#content form .inline-group,
.inline-group fieldset,
.inline-group .module {
flex: 1 1 100% !important;
max-width: 100% !important;
min-width: 100% !important;
width: 100% !important;
}
#content-main form fieldset:hover,
#content form fieldset:hover {
box-shadow: 0 4px 6px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.06);
border-color: #cbd5e1;
}
/* Archive results list content should take full width */
.field-archiveresults_list,
.field-archiveresults_list .readonly,
.field-snapshots,
.field-snapshots .readonly {
width: 100% !important;
max-width: 100% !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
}
/* Card headers - no borders, just background */
#content-main form fieldset h2,
#content form fieldset h2,
#content-main form .module h2,
#content form .module h2 {
margin: 0 !important;
padding: 8px 16px !important;
background: #f1f5f9 !important;
color: #334155 !important;
font-size: 12px !important;
font-weight: 600 !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
border: none !important;
border-top: none !important;
border-left: none !important;
border-right: none !important;
border-bottom: none !important;
border-radius: 0 !important;
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
-webkit-font-smoothing: antialiased;
box-shadow: none !important;
outline: none !important;
}
/* Collapse toggle styling */
#content-main form fieldset h2 a.collapse-toggle,
#content form fieldset h2 a.collapse-toggle {
color: #64748b;
}
/* Card content area */
#content-main form fieldset > div,
#content form fieldset > div {
padding: 20px;
flex: 1;
overflow-x: hidden;
overflow-y: visible;
min-width: 0;
}
#content-main form fieldset > .description,
#content form fieldset > .description {
flex: 0 0 auto;
padding: 6px 16px 0;
margin: 0;
}
/* Form rows inside cards */
#content-main form fieldset .form-row,
#content form fieldset .form-row {
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
min-width: 0;
min-height: auto;
}
#content-main form fieldset .form-row:first-child,
#content form fieldset .form-row:first-child {
padding-top: 0;
}
#content-main form fieldset .form-row:last-child,
#content form fieldset .form-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
/* Remove borders from nested fieldsets and flex-containers inside cards */
#content-main form fieldset fieldset,
#content form fieldset fieldset,
#content-main form fieldset .flex-container,
#content form fieldset .flex-container,
#content-main form .module fieldset,
#content form .module fieldset {
background: transparent !important;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
min-width: 0 !important;
max-width: 94% !important;
flex: none !important;
display: block !important;
}
/* Nested fieldset headers should be invisible */
#content-main form fieldset fieldset h2,
#content form fieldset fieldset h2,
#content-main form fieldset .flex-container legend,
#content form fieldset .flex-container legend {
background: transparent !important;
padding: 0 0 4px 0 !important;
font-size: 13px !important;
color: #374151 !important;
text-transform: none !important;
letter-spacing: normal !important;
}
/* Ensure form elements inside cards don't overflow */
#content-main form fieldset input,
#content-main form fieldset select,
#content-main form fieldset textarea,
#content form fieldset input,
#content form fieldset select,
#content form fieldset textarea {
max-width: 100%;
box-sizing: border-box;
}
/* Related widget wrapper should fit within card */
#content-main form fieldset .related-widget-wrapper,
#content form fieldset .related-widget-wrapper {
max-width: 100%;
}
#content-main form fieldset .related-widget-wrapper select,
#content form fieldset .related-widget-wrapper select {
min-width: 0;
flex: 1;
}
/* Labels inside cards */
#content-main form fieldset .form-row > label,
#content form fieldset .form-row > label,
#content-main form fieldset .form-row > .flex-container > label,
#content form fieldset .form-row > .flex-container > label,
#content-main form label,
#content form label,
.aligned label,
legend {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-weight: 500;
color: #374151;
display: block;
margin-bottom: 8px;
float: none !important;
width: auto !important;
padding: 0 !important;
font-size: 13px;
letter-spacing: -0.01em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Snapshot admin actions: hide label/colon and remove card border */
#content-main form .field-admin_actions > label,
#content form .field-admin_actions > label,
#content-main form .field-admin_actions label,
#content form .field-admin_actions label,
#content-main form .field-snapshot_summary > label,
#content form .field-snapshot_summary > label,
#content-main form .field-snapshot_summary label,
#content form .field-snapshot_summary label,
#content-main form .field-url_favicon > label,
#content form .field-url_favicon > label,
#content-main form .field-url_favicon label,
#content form .field-url_favicon label,
#content-main form .field-archiveresults_list > label,
#content form .field-archiveresults_list > label,
#content-main form .field-archiveresults_list label,
#content form .field-archiveresults_list label {
display: none !important;
}
#content-main form .field-url_favicon .readonly,
#content form .field-url_favicon .readonly {
background: transparent;
border: none;
padding: 0;
border-radius: 0;
font-family: inherit;
font-size: inherit;
line-height: 1;
}
#content-main form .form-row.field-url_favicon.field-url .form-multiline,
#content form .form-row.field-url_favicon.field-url .form-multiline {
display: flex !important;
align-items: flex-end;
gap: 12px;
flex-wrap: nowrap;
width: 100%;
}
#content-main form .form-row.field-url_favicon.field-url .form-multiline > div:first-child,
#content form .form-row.field-url_favicon.field-url .form-multiline > div:first-child {
flex: 0 0 32px;
width: 32px;
margin: 0 !important;
}
#content-main form .form-row.field-url_favicon.field-url .form-multiline > div:last-child,
#content form .form-row.field-url_favicon.field-url .form-multiline > div:last-child {
flex: 1 1 auto;
min-width: 0;
margin: 0 !important;
}
#content-main form .form-row.field-url_favicon.field-url .field-url_favicon,
#content form .form-row.field-url_favicon.field-url .field-url_favicon {
width: 32px !important;
max-width: 32px !important;
}
#content-main form .form-row.field-url_favicon.field-url .field-url,
#content form .form-row.field-url_favicon.field-url .field-url {
width: 100% !important;
max-width: none !important;
}
#content-main form fieldset.actions-card,
#content form fieldset.actions-card {
border: none !important;
box-shadow: none !important;
background: transparent !important;
}
/* Readonly fields styling */
#content-main form fieldset .readonly,
#content form fieldset .readonly {
background: #f8fafc;
padding: 12px 14px;
border-radius: 8px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
font-size: 13px;
word-break: break-word;
line-height: 1.6;
border: 1px solid #e2e8f0;
color: #475569;
}
/* Long content in readonly */
#content-main form fieldset .readonly pre,
#content form fieldset .readonly pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: inherit;
}
/* Input styling */
#content-main form input[type="text"],
#content-main form input[type="number"],
#content-main form input[type="url"],
#content-main form input[type="email"],
#content-main form input[type="password"],
#content form input[type="text"],
#content form input[type="number"],
#content form input[type="url"],
#content form input[type="email"],
#content form input[type="password"] {
width: 100%;
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
box-sizing: border-box;
background: #fff;
color: #1e293b;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
-webkit-font-smoothing: antialiased;
}
#content-main form select,
#content form select {
width: 100%;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
box-sizing: border-box;
background: #fff;
color: #1e293b;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
-webkit-font-smoothing: antialiased;
}
#content-main form input::placeholder,
#content form input::placeholder {
color: #94a3b8;
}
/* Focus states */
#content-main form input:focus,
#content-main form select:focus,
#content-main form textarea:focus,
#content form input:focus,
#content form select:focus,
#content form textarea:focus {
border-color: #3b82f6;
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
/* Textarea styling */
#content-main form textarea,
#content form textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 12px 14px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
resize: vertical;
min-height: 80px;
color: #1e293b;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
-webkit-font-smoothing: antialiased;
}
/* Fix vTextField width */
.vTextField {
width: 100% !important;
}
/* ============================================
Button styling (shadcn-inspired)
============================================ */
/* Base button styles */
input[type="submit"],
button,
.button,
.btn,
a.button,
.submit-row input,
.submit-row a.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 18px;
font-size: 14px;
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.4;
border-radius: 8px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
white-space: nowrap;
-webkit-font-smoothing: antialiased;
}
/* Primary button (default) */
input[type="submit"],
button[type="submit"],
.button.default,
.submit-row input[type="submit"] {
background: #0f172a;
color: #fff;
border-color: #0f172a;
}
input[type="submit"]:hover,
button[type="submit"]:hover,
.button.default:hover,
.submit-row input[type="submit"]:hover {
background: #1e293b;
border-color: #1e293b;
}
input[type="submit"]:active,
button[type="submit"]:active {
background: #334155;
transform: translateY(1px);
}
/* Secondary/outline buttons */
button:not([type="submit"]),
.button:not(.default),
a.button {
background: #fff;
color: #374151;
border-color: #d1d5db;
}
button:not([type="submit"]):hover,
.button:not(.default):hover,
a.button:hover {
background: #f9fafb;
border-color: #9ca3af;
color: #1f2937;
}
/* Danger button */
.deletelink,
a.deletelink,
button.deletelink,
input[name="delete"],
.button.delete {
background: #fff;
color: #dc2626;
border-color: #fecaca;
}
.deletelink:hover,
a.deletelink:hover,
button.deletelink:hover,
input[name="delete"]:hover,
.button.delete:hover {
background: #fef2f2;
border-color: #f87171;
color: #b91c1c;
}
/* Small buttons */
.btn-sm,
.object-tools a,
.datetimeshortcuts a {
padding: 6px 12px;
font-size: 13px;
border-radius: 6px;
}
/* Object tools (top action buttons) */
.object-tools {
margin-bottom: 20px;
}
.object-tools li {
margin-left: 10px;
}
.object-tools a {
background: #fff;
color: #374151;
border: 1px solid #d1d5db;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.object-tools a:hover {
background: #f9fafb;
border-color: #9ca3af;
}
/* Submit row styling */
.submit-row {
margin-top: 24px;
padding: 20px;
background: #fff;
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
clear: both;
flex: 1 1 100%;
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.submit-row p {
margin: 0;
}
.submit-row .deletelink-box {
margin-left: auto;
}
/* Responsive: 2 columns on medium screens */
@media (max-width: 1400px) {
#content-main form fieldset,
#content form fieldset {
max-width: calc(50% - 10px);
flex: 1 1 320px;
}
}
/* Responsive: stack on smaller screens */
@media (max-width: 900px) {
#content-main form fieldset,
#content form fieldset {
flex: 1 1 100%;
max-width: 100%;
min-width: auto;
}
#content {
padding: 16px;
}
}
/* Module content padding */
#content-main form .module > div,
#content form .module > div {
padding: 12px;
}
/* Fix for JSON/config editor */
.field-config .readonly,
.field-config textarea {
width: 100%;
min-height: 120px;
max-height: none;
}
/* Related widget styling */
.related-widget-wrapper {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.related-widget-wrapper select {
flex: 1;
min-width: 150px;
}
.related-widget-wrapper a {
flex-shrink: 0;
padding: 8px;
border-radius: 6px;
color: #64748b;
transition: color 0.15s ease, background 0.15s ease;
}
.related-widget-wrapper a:hover {
color: #1e293b;
background: #f1f5f9;
}
/* Help text styling */
.help {
font-size: 13px;
color: #64748b;
margin-top: 6px;
line-height: 1.5;
}
/* Error styling */
.errorlist {
color: #dc2626;
font-size: 13px;
margin: 6px 0;
padding: 0;
list-style: none;
}
.errorlist li {
background: #fef2f2;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #fecaca;
}
/* Inline related objects - force full width */
.inline-group,
#archiveresult_set-group,
#content-main form .inline-group,
#content-main form > div > .inline-group,
#content form > div > .inline-group,
.change-form .inline-group,
div.inline-group {
flex: 1 1 100% !important;
max-width: 100% !important;
min-width: 100% !important;
width: 100% !important;
margin-top: 20px;
flex-basis: 100% !important;
}
/* Ensure inline-group breaks out of card grid */
#content-main form > div,
#content form > div {
flex-wrap: wrap;
}
/* TabularInline table full width */
.inline-group .tabular,
.inline-group table {
width: 100% !important;
}
.inline-related {
margin: 12px 0;
padding: 16px;
background: #fff;
border-radius: 10px;
border: 1px solid #e2e8f0;
}
.inline-related h3 {
margin: -16px -16px 16px -16px;
padding: 12px 16px;
background: #f8fafc;
border-radius: 9px 9px 0 0;
border-bottom: 1px solid #e2e8f0;
font-size: 13px;
font-weight: 600;
color: #374151;
}
/* Tabular inline styling */
.tabular {
border-radius: 8px;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.tabular td, .tabular th {
padding: 12px 14px;
font-size: 13px;
border-bottom: 1px solid #f1f5f9;
}
.tabular th {
background: #f8fafc;
font-weight: 600;
color: #374151;
text-align: left;
}
.tabular tr:last-child td {
border-bottom: none;
}
/* Delete checkbox */
.inline-deletelink {
color: #dc2626;
font-size: 13px;
}
/* Datetime widgets */
.datetimeshortcuts {
margin-left: 10px;
}
.datetimeshortcuts a {
background: #f1f5f9;
color: #475569;
border: none;
padding: 4px 10px;
}
.datetimeshortcuts a:hover {
background: #e2e8f0;
color: #1e293b;
}
/* Aligned forms - fix label positioning */
.aligned .form-row > div {
margin-left: 0 !important;
}
/* Checkbox styling */
input[type="checkbox"] {
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid #d1d5db;
cursor: pointer;
accent-color: #3b82f6;
}
/* Links styling */
a {
color: #2563eb;
text-decoration: none;
transition: color 0.15s ease;
}
a:hover {
color: #1d4ed8;
}
/* Messages/alerts */
.messagelist {
padding: 0;
margin: 0 0 20px 0;
}
.messagelist li {
padding: 14px 18px;
border-radius: 10px;
font-size: 14px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
ul.messagelist li.success {
background: #f0fdf4 !important;
background-image: none !important;
border: 1px solid #bbf7d0;
color: #166534;
}
.messagelist li.warning {
background: #fffbeb !important;
background-image: none !important;
border: 1px solid #fde68a;
color: #92400e;
}
.messagelist li.error {
background: #fef2f2 !important;
background-image: none !important;
border: 1px solid #fecaca;
color: #991b1b;
}
/* Breadcrumbs */
.breadcrumbs {
background: transparent;
padding: 12px 24px;
font-size: 13px;
color: #64748b;
}
.breadcrumbs a {
color: #64748b;
}
.breadcrumbs a:hover {
color: #1e293b;
}
/* Action buttons in cards */
.card .btn,
.card button {
margin-top: 10px;
}
/* Select2 overrides */
.select2-container--default .select2-selection--single,
.select2-container--default .select2-selection--multiple {
border: 1px solid #d1d5db;
border-radius: 8px;
min-height: 42px;
}
.select2-container--default .select2-selection--single:focus,
.select2-container--default .select2-selection--multiple:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
/* ============================================
Admin List/Changelist Page Styling
============================================ */
/* Results table container */
#changelist {
background: #fff;
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
overflow: hidden;
}
/* Table styling */
#result_list {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
#result_list thead th {
background: #f8fafc;
border-bottom: 2px solid #e2e8f0;
padding: 12px 16px;
font-weight: 600;
font-size: 13px;
color: #475569;
text-align: left;
text-transform: uppercase;
letter-spacing: 0.025em;
white-space: nowrap;
}
#result_list thead th a {
color: #475569;
text-decoration: none;
}
#result_list thead th a:hover {
color: #1e293b;
}
#result_list thead th.sorted {
background: #f1f5f9;
}
#result_list thead th .text span {
padding-right: 5px;
}
#result_list tbody tr {
border-bottom: 1px solid #f1f5f9;
transition: background-color 0.15s ease;
}
#result_list tbody tr:hover {
background-color: #f8fafc;
}
#result_list tbody tr.selected {
background-color: #eff6ff;
}
#result_list tbody td {
padding: 12px 16px;
color: #334155;
vertical-align: middle;
}
#result_list tbody td a {
color: #2563eb;
font-weight: 500;
}
#result_list tbody td a:hover {
color: #1d4ed8;
text-decoration: underline;
}
/* Checkbox column */
#result_list .action-checkbox,
#result_list th.action-checkbox-column {
width: 40px;
text-align: center;
padding: 12px 8px;
}
/* Pagination */
.paginator {
background: #f8fafc;
padding: 12px 16px;
border-top: 1px solid #e2e8f0;
font-size: 14px;
color: #64748b;
}
.paginator a {
color: #2563eb;
padding: 6px 12px;
border-radius: 6px;
margin: 0 2px;
text-decoration: none;
}
.paginator a:hover {
background: #e2e8f0;
}
/* Toolbar / search bar */
#changelist #toolbar {
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
gap: 12px;
}
#toolbar form,
#changelist-search {
display: flex;
align-items: center;
gap: 8px;
flex: 1 1 auto;
max-width: none;
min-width: 0;
}
body.change-list #toolbar form > div {
display: flex !important;
align-items: center;
gap: 8px;
flex-wrap: nowrap !important;
white-space: nowrap;
width: auto;
flex: 0 1 auto;
min-width: 0;
}
body.change-list #toolbar label {
margin: 0;
display: inline-flex;
align-items: center;
}
body.change-list #toolbar input[type="submit"] {
margin: 0;
}
#searchbar {
flex: 0 1 420px;
width: 100%;
min-width: 180px;
max-width: none;
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
}
#searchbar:focus {
border-color: #3b82f6;
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
/* Filter sidebar */
#changelist-filter {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
overflow: hidden;
}
#changelist-filter h2 {
background: #f8fafc;
padding: 12px 16px;
font-size: 13px;
font-weight: 600;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.025em;
margin: 0;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.filter-toggle {
border: 1px solid #e2e8f0;
background: #ffffff;
color: #64748b;
font-size: 11px;
padding: 4px 8px;
border-radius: 999px;
cursor: pointer;
text-transform: none;
letter-spacing: normal;
}
.filter-toggle:hover {
background: #f1f5f9;
color: #334155;
}
#changelist-toolbar-filter-toggle {
margin-left: auto;
flex: 0 0 auto;
}
#changelist-filter h3 {
padding: 12px 16px 8px;
font-size: 12px;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0;
}
#changelist-filter ul {
padding: 0 8px 12px;
margin: 0;
list-style: none;
}
#changelist-filter li {
margin: 0;
}
#changelist-filter li a {
display: block;
padding: 8px 12px;
color: #475569;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: background-color 0.15s ease;
}
#changelist-filter li a:hover {
background: #f1f5f9;
color: #1e293b;
}
#changelist-filter li.selected a {
background: #eff6ff;
color: #2563eb;
font-weight: 500;
}
body.filters-collapsed #changelist-filter {
display: none !important;
}
/* Actions bar */
body.change-list #changelist .actions {
padding: 12px 16px;
background: #f8fafc;
border-bottom: 0;
display: flex !important;
align-items: center;
gap: 8px;
flex-wrap: nowrap !important;
overflow-x: auto;
}
body.change-list #content-main {
clear: both;
float: none;
width: auto;
}
body.change-list #changelist {
border: 0 !important;
}
body.change-list #changelist .actions .button,
body.change-list #changelist .actions select {
display: inline-flex;
align-items: center;
box-sizing: border-box;
}
body.change-list #changelist .actions-left {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap !important;
flex: 1 1 auto;
min-width: 0;
white-space: nowrap;
}
body.change-list #changelist .actions-right {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
flex: 0 0 auto;
}
body.change-list #changelist .actions .action-buttons[hidden],
body.change-list #changelist .actions .actions-tags-with-buttons[hidden] {
display: none !important;
}
.actions label {
display: inline-flex;
align-items: center;
font-size: 14px;
color: #475569;
}
.actions select {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
background: #fff;
}
.actions .button {
padding: 8px 16px;
font-size: 14px;
}
.action-permissions-wrapper {
position: relative;
display: inline-flex;
align-items: center;
}
.action-permissions-menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 40;
min-width: 138px;
padding: 4px;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
}
.action-permissions-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
height: 30px;
padding: 0 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: #334155;
font-size: 12px;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.action-permissions-menu-item:hover {
background: #eff6ff;
color: #1d4ed8;
}
.action-rearchive-wrapper {
position: relative;
display: inline-block;
}
.action-rearchive-select,
.action-permissions-select {
position: absolute;
left: 0;
bottom: 0;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
border: 0;
padding: 0;
margin: 0;
}
/* Object count */
.actions .action-counter {
color: #64748b;
font-size: 14px;
}
/* Empty results */
#changelist-form .results + p,
.paginator + p {
padding: 40px;
text-align: center;
color: #64748b;
font-size: 15px;
}
.search-empty-state {
padding: 28px 18px;
text-align: center;
color: #475569;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
}
.search-empty-state p {
margin: 0;
}
.search-empty-state code {
display: inline-block;
margin-left: 4px;
padding: 2px 6px;
border-radius: 5px;
background: #e2e8f0;
color: #0f172a;
font-size: 13px;
}
/* Date hierarchy */
.xfull {
padding: 12px 16px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.xfull a {
color: #2563eb;
margin-right: 8px;
}
/* ============================================
Tag Editor Widget Styles
============================================ */
/* Main container - acts as input field */
.tag-editor-container {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 8px 12px;
min-height: 42px;
background: #fff;
border: 1px solid #d1d5db;
border-radius: 8px;
cursor: text;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.tag-editor-container:focus-within {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
/* Pills container */
.tag-pills {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
/* Individual tag pill */
.tag-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px 4px 10px;
background: var(--tag-bg, #e2e8f0);
color: var(--tag-fg, #1e293b);
font-size: 13px;
font-weight: 500;
border-radius: 16px;
white-space: nowrap;
transition: all 0.15s ease;
-webkit-font-smoothing: antialiased;
border: 1px solid var(--tag-border, #cbd5e1);
}
.tag-pill:hover {
filter: brightness(0.98);
}
.tag-pill a.tag-link {
color: inherit;
text-decoration: none;
}
.tag-pill a.tag-link:hover {
text-decoration: underline;
}
/* Remove button on pills */
.tag-remove-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
margin: 0;
background: rgba(15, 23, 42, 0.08);
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 50%;
color: inherit;
font-size: 14px;
font-weight: 600;
line-height: 1;
cursor: pointer;
opacity: 0.7;
transition: all 0.15s ease;
}
.tag-remove-btn:hover {
background: rgba(15, 23, 42, 0.18);
opacity: 1;
}
/* Inline input for adding tags */
.tag-inline-input {
flex: 1;
min-width: 120px;
padding: 4px 0;
border: none;
outline: none;
font-size: 14px;
font-family: inherit;
background: transparent;
color: #1e293b;
}
.tag-inline-input::placeholder {
color: #94a3b8;
}
/* Inline editor for list view - more compact */
.tag-editor-inline {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
padding: 2px 4px;
background: transparent;
border-radius: 4px;
cursor: text;
vertical-align: middle;
}
.tag-pills-inline {
display: inline-flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.tag-editor-inline .tag-pill {
padding: 2px 6px 2px 8px;
font-size: 11px;
border-radius: 12px;
}
.tag-editor-inline .tag-remove-btn {
width: 14px;
height: 14px;
font-size: 12px;
}
#content .tag-editor-inline input.tag-inline-input-sm {
width: 22px;
min-width: 22px;
max-width: 140px;
height: 22px;
padding: 0 6px;
border: 1px solid #e2e8f0;
outline: none;
font-size: 12px;
font-family: inherit;
background: #f1f5f9;
color: #94a3b8;
border-radius: 999px;
text-align: center;
cursor: text;
transition: width 0.15s ease, color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
#content .tag-editor-inline input.tag-inline-input-sm:focus {
width: 120px;
color: #1e293b;
border-color: #94a3b8;
background: #ffffff;
text-align: left;
}
#content .tag-editor-inline input.tag-inline-input-sm::placeholder {
color: #94a3b8;
}
/* Actions bar tag editor (compact to avoid crowding buttons) */
body.change-list #changelist .actions .tag-editor-container {
padding: 2px 6px;
min-height: 24px;
height: 24px;
width: 160px;
max-width: 160px;
flex: 0 0 160px;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
gap: 4px;
}
body.change-list #changelist .actions-tags {
display: none;
align-items: center;
}
/* Changelist layout */
body.change-list #changelist {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
align-items: flex-start;
gap: 0;
clear: both;
}
body.change-list #changelist .changelist-form-container {
display: block;
min-width: 0;
width: auto;
}
body.change-list #changelist-filter {
width: 320px;
min-width: 320px;
max-width: 320px;
display: block;
margin: 0 !important;
align-self: flex-start;
}
body.change-list #changelist .changelist-form-container > div,
body.change-list #changelist .changelist-form-container > form {
min-width: 0;
width: auto;
max-width: none;
}
body.filters-collapsed.change-list #changelist {
grid-template-columns: minmax(0, 1fr);
}
.actions .tag-pills {
gap: 4px;
flex-wrap: nowrap;
}
.actions .tag-pill {
padding: 1px 6px 1px 8px;
font-size: 10px;
}
.actions .tag-inline-input {
min-width: 40px;
padding: 0;
margin: 0;
height: auto;
min-height: 0;
border: 0;
border-radius: 0;
box-shadow: none;
background: transparent;
font-size: 11px;
}
.model-snapshot.change-list #changelist .actions-tags-with-buttons {
display: grid;
grid-template-columns: minmax(0, 1fr) 34px 34px;
gap: 0;
align-items: stretch;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #fff;
overflow: hidden;
}
.model-snapshot.change-list #changelist .actions-tags-with-buttons .tag-editor-container {
width: auto;
max-width: none;
height: 34px;
min-height: 34px;
padding: 4px 9px;
border: 0;
border-radius: 0;
box-shadow: none;
overflow: hidden;
}
.model-snapshot.change-list #changelist .actions-tags-with-buttons .tag-inline-input {
height: auto;
min-height: 0;
padding: 0;
margin: 0;
border: 0;
border-radius: 0;
box-shadow: none;
background: transparent;
}
.model-snapshot.change-list #changelist .actions-tags-with-buttons .button[name="add_tags"],
.model-snapshot.change-list #changelist .actions-tags-with-buttons .button[name="remove_tags"] {
width: 34px;
min-width: 34px;
height: 34px;
padding: 0;
margin: 0;
border: 0;
border-left: 1px solid #cbd5e1;
border-radius: 0;
justify-content: center;
}
/* Container in list view title column */
.tags-inline-editor {
display: inline;
margin-left: 8px;
}
/* Existing tag styles (keep for backwards compat) */
.tags .tag {
display: inline-block;
padding: 2px 8px;
margin: 1px 2px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #fff;
font-size: 11px;
font-weight: 500;
border-radius: 12px;
text-decoration: none;
transition: all 0.15s ease;
}
.tags .tag:hover {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
}
</style>
{% endblock %}
{% if LANGUAGE_BIDI %}
<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}">
{% endif %}
{% block responsive %}
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="{% static "admin/css/responsive.css" %}">
{% if LANGUAGE_BIDI %}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/responsive_rtl.css" %}">
{% endif %}
{% endblock %}
<script
src="{% static 'jquery-3.7.1.slim.min.js' %}"
integrity="sha256-kmHvs0B+OpCW5GVHUNjv9rOmY0IvSIRcf7zGUDTDQM8="
crossorigin="anonymous"></script>
<link href="{% static 'select2.min.css' %}" rel="stylesheet"/>
<script src="{% static 'select2.min.js' %}"></script>
<link rel="stylesheet" type="text/css" href="{% static "admin.css" %}?v={{ STATIC_CACHE_KEY|default:VERSION|urlencode }}">
<script>
function selectSnapshotListView(e) {
e && e.stopPropagation()
e && e.preventDefault()
console.log('Switching to Snapshot list view...')
localStorage.setItem('preferred_snapshot_view_mode', 'list')
window.location = "{% url 'admin:core_snapshot_changelist' %}" + document.location.search
return false
}
function selectSnapshotGridView(e) {
e && e.stopPropagation()
e && e.preventDefault()
console.log('Switching to Snapshot grid view...')
localStorage.setItem('preferred_snapshot_view_mode', 'grid')
window.location = "{% url 'admin:grid' %}" + document.location.search
return false
}
const preferred_view = localStorage.getItem('preferred_snapshot_view_mode') || 'unset'
const current_view = (
window.location.pathname === "{% url 'admin:core_snapshot_changelist' %}"
? 'list'
: 'grid')
console.log('Preferred snapshot view is:', preferred_view, 'Current view mode is:', current_view)
if (preferred_view === 'grid' && current_view !== 'grid') {
selectSnapshotGridView()
}
</script>
{% block extrahead %}{% endblock %}
</head>
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}" data-admin-utc-offset="{% now "Z" %}">
{% system_warnings_banner %}
{% include 'progressbar.html' %}
<div id="container">
{% if not is_popup %}
<div id="header">
<div id="branding">
<h1 id="site-name">
<a href="{% url 'Home' %}">
<img src="{% static 'archive.png' %}" id="logo">
ArchiveBox
</a>
</h1>
</div>
{% block usertools %}
{% if has_permission %}
{% include 'navigation.html' %}
{% endif %}
{% endblock %}
{% block nav-global %}{% endblock %}
</div>
{% if has_permission %}
{% include 'progressmonitor/progress_monitor.html' %}
{% endif %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
{% if title %} &rsaquo; {{ title }}{% endif %}
</div>
{% endblock %}
{% endif %}
{% block messages %}
{% if messages %}
<ul class="messagelist">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message|capfirst }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock messages %}
<div id="content" class="{% block coltype %}colM{% endblock %}">
{% block pretitle %}{% endblock %}
{% block content_title %}{# {% if title %}<h1>{{ title }}</h1>{% endif %} #}{% endblock %}
{% block content %}
{% block object-tools %}{% endblock %}
{{ content }}
{% endblock %}
{% block sidebar %}{% endblock %}
<br class="clear">
</div>
{% block footer %}<div id="footer"></div>{% endblock %}
</div>
{% comment %}
{% if user.is_authenticated and user.is_superuser and CAN_UPGRADE %}
<script>
if (!localStorage.getItem("bannerDismissed")) {
const upgradeVersionTag = "{{VERSIONS_AVAILABLE.recommended_version.tag_name}}"
const upgradeVersionURL = "{{VERSIONS_AVAILABLE.recommended_version.html_url}}"
const currentVersionTag = "{{VERSION}}"
const currentVersionURL = "{{VERSIONS_AVAILABLE.recommended_version.html_url}}"
createBanner(currentVersionTag, currentVersionURL, upgradeVersionTag, upgradeVersionURL)
}
function createBanner(currentVersionTag, currentVersionURL, upgradeVersionTag, upgradeVersionURL) {
const banner = document.createElement('div')
banner.setAttribute('id', 'upgrade-banner');
banner.innerHTML = `
<p>There's a new version of ArchiveBox available!</p>
Your version: <a href=${currentVersionURL}>${currentVersionTag}</a> | New version: <a href=${upgradeVersionURL}>${upgradeVersionTag}</a>
<p>
<a href=https://github.com/ArchiveBox/ArchiveBox/wiki/Upgrading-or-Merging-Archives>Upgrade Instructions</a> | <a href=https://github.com/ArchiveBox/ArchiveBox/releases>Changelog</a> | <a href=https://github.com/ArchiveBox/ArchiveBox/wiki/Roadmap>Roadmap</a>
</p>
<button id="dismiss-btn">Dismiss</button>
`
document.body.appendChild(banner);
const dismissButton = document.querySelector("#dismiss-btn")
if (dismissButton) {
dismissButton.addEventListener("click", dismissBanner)
}
}
function dismissBanner() {
const banner = document.getElementById("upgrade-banner")
banner.style.display = "none"
localStorage.setItem("bannerDismissed", "true")
}
</script>
{% endif %}
{% endcomment %}
<script>
const archiveboxAdminJQuery = (window.django && window.django.jQuery) || window.jQuery;
const $ = archiveboxAdminJQuery;
if ($) {
window.$ = $;
$.fn.reverse = [].reverse;
}
// hide images that fail to load
document.querySelector('body').addEventListener('error', function (e) {
e.target.style.opacity = 0;
}, true)
// setup timezone
{% get_current_timezone as TIME_ZONE %}
window.TIME_ZONE = '{{TIME_ZONE}}'
window.setCookie = function(name, value, days) {
let expires = ""
if (days) {
const date = new Date()
date.setTime(date.getTime() + (days*24*60*60*1000))
expires = "; expires=" + date.toUTCString()
}
document.cookie = name + "=" + (value || "") + expires + "; path=/"
}
function setTimeOffset() {
if (window.GMT_OFFSET) return
window.GMT_OFFSET = -(new Date).getTimezoneOffset()
window.setCookie('GMT_OFFSET', window.GMT_OFFSET, 365)
}
// change the admin actions button from a dropdown to buttons across
function fix_actions() {
const container = $('div.actions')
if (container.find('.action-buttons').length) return
// too many actions to turn into buttons
if (container.find('select[name=action] option').length >= 11) return
// hide the empty default option thats just a placeholder with no value
container.find('label:nth-child(1), button[type=submit][name=index][value=0]').hide()
const buttons = $('<div></div>')
.insertAfter('div.actions button[type=submit]')
.css('display', 'inline')
.addClass('action-buttons');
function flushPendingActionTags() {
const tagContainer = document.querySelector('.actions-tags')
const input = tagContainer?.querySelector('.tag-inline-input')
const hidden = tagContainer?.querySelector('input[type="hidden"][name="tags"]')
const pending = (input?.value || '').trim()
if (!pending || !hidden) return
const seen = new Set()
const tags = (hidden.value ? hidden.value.split(',') : [])
.concat(pending.split(','))
.map((tag) => tag.trim())
.filter((tag) => {
const key = tag.toLowerCase()
if (!tag || seen.has(key)) return false
seen.add(key)
return true
})
hidden.value = tags.join(',')
input.value = ''
hidden.dispatchEvent(new Event('input', { bubbles: true }))
hidden.dispatchEvent(new Event('change', { bubbles: true }))
}
const rearchiveActions = ['resnapshot_snapshot', 'update_snapshots', 'overwrite_snapshots']
const rearchiveItems = {
resnapshot_snapshot: ['', 'Create fresh snapshot(s)'],
update_snapshots: ['▶️', 'Retry only failed subtasks'],
overwrite_snapshots: ['♻️', 'Reset & retry from scratch'],
}
let rearchiveBuilt = false
// for each action in the dropdown, turn it into a button instead
container.find('select[name=action] option:gt(0)').each(function () {
const action_type = this.value
if (rearchiveActions.includes(action_type)) {
if (rearchiveBuilt) return
rearchiveBuilt = true
const wrapper = $('<span></span>')
.addClass('action-rearchive-wrapper')
.appendTo(buttons)
const select = $('<select></select>')
.addClass('action-rearchive-select')
.attr('aria-label', 'Re-Archive')
.appendTo(wrapper)
$('<button>')
.attr('type', 'button')
.addClass('button')
.html('Re-Archive <span aria-hidden="true">▾</span>')
.click(function (e) {
e.preventDefault()
e.stopPropagation()
const el = select[0]
if (typeof el.showPicker === 'function') {
try { el.showPicker() } catch (err) { el.focus(); el.click() }
} else {
el.focus()
el.click()
}
return false
})
.appendTo(wrapper)
$('<option></option>').attr('value', '').attr('hidden', true).appendTo(select)
rearchiveActions.forEach(function (action_value) {
const opt = container.find('select[name=action] option[value="' + action_value + '"]')[0]
if (!opt) return
const [icon, label] = rearchiveItems[action_value]
$('<option></option>')
.attr('value', action_value)
.text(icon + ' ' + label)
.appendTo(select)
})
select.change(function () {
const action_value = this.value
if (!action_value) return
const num_selected = (
document.querySelector('.action-selected-count')?.textContent.split('/')[0].trim()
|| document.querySelector('.action-counter')?.textContent.split(' ')[0]
|| '0'
)
if (action_value === 'overwrite_snapshots') {
const message = (
'Are you sure you want to re-archive (overwrite) ' + num_selected + ' Snapshots?\n\n' +
'This will delete all previously saved files from these Snapshots and re-archive them from scratch.\n\n'
)
if (!window.confirm(message)) {
this.value = ''
return
}
}
container.find('select[name=action]')
.val(action_value)
.trigger('change')
$('#changelist-form button[name="index"]').click()
document.querySelector('#logo').outerHTML = '<div class="loader"></div>'
})
return
}
if (action_type === 'set_crawl_permissions' || action_type === 'set_snapshot_permissions') {
const wrapper = $('<span></span>')
.addClass('action-permissions-wrapper')
.appendTo(buttons)
const select = $('<select></select>')
.addClass('action-permissions-select')
.attr('aria-label', 'Permissions')
.appendTo(wrapper)
$('<button>')
.attr('type', 'button')
.attr('name', action_type)
.addClass('button')
.text(this.text)
.click(function (e) {
e.preventDefault()
e.stopPropagation()
const el = select[0]
if (typeof el.showPicker === 'function') {
try { el.showPicker() } catch (err) { el.focus(); el.click() }
} else {
el.focus()
el.click()
}
return false
})
.appendTo(wrapper)
$('<option></option>').attr('value', '').attr('hidden', true).appendTo(select)
;[
['public', '👥', 'Public'],
['unlisted', '🔗', 'Unlisted'],
['private', '🔒', 'Private'],
].forEach(function (choice) {
$('<option></option>')
.attr('value', choice[0])
.text(choice[1] + ' ' + choice[2])
.appendTo(select)
})
select.change(function () {
const permissions = this.value
if (!permissions) return
let permissionsInput = container.find('input[name=permissions]')
if (!permissionsInput.length) {
permissionsInput = $('<input type="hidden" name="permissions">').appendTo(container)
}
permissionsInput.val(permissions)
container.find('select[name=action]')
.val(action_type)
.trigger('change')
$('#changelist-form button[name="index"]').click()
document.querySelector('#logo').outerHTML = '<div class="loader"></div>'
})
return
}
$('<button>')
.attr('type', 'button')
.attr('name', action_type)
.addClass('button')
.text(this.text)
.click(function (e) {
e.preventDefault()
e.stopPropagation()
const num_selected = (
document.querySelector('.action-selected-count')?.textContent.split('/')[0].trim()
|| document.querySelector('.action-counter')?.textContent.split(' ')[0]
|| '0'
)
if (action_type === 'overwrite_snapshots') {
const message = (
'Are you sure you want to re-archive (overwrite) ' + num_selected + ' Snapshots?\n\n' +
'This will delete all previously saved files from these Snapshots and re-archive them from scratch.\n\n'
)
if (!window.confirm(message)) return false
}
if (action_type === 'delete_snapshots') {
const message = (
'Are you sure you want to permanently delete ' + num_selected + ' Snapshots?\n\n' +
'They will be removed from your index, and all their Snapshot content on disk will be permanently deleted.'
)
if (!window.confirm(message)) return false
}
if (action_type === 'add_tags' || action_type === 'remove_tags') {
flushPendingActionTags()
}
// select the action from the original Django admin dropdown
container.find('select[name=action]')
.val(action_type)
.trigger('change')
// click submit & replace the archivebox logo with a spinner
$('#changelist-form button[name="index"]').click()
document.querySelector('#logo').outerHTML = '<div class="loader"></div>'
return false
})
.appendTo(buttons)
})
console.log('Converted', buttons.children().length, 'admin actions from dropdown to buttons')
;['.model-crawl.change-list', '.model-snapshot.change-list'].forEach(function(selector) {
const scope = document.querySelector(selector)
const addLink = scope?.querySelector('.object-tools a.addlink')
const actionButtons = scope?.querySelector('.actions-top .action-buttons')
if (addLink && actionButtons && !actionButtons.querySelector('.admin-add-action')) {
const addButton = addLink.cloneNode(true)
addButton.classList.add('button', 'admin-add-action')
addButton.classList.remove('addlink')
addButton.textContent = ' Add'
actionButtons.appendChild(addButton)
}
})
const tagContainer = document.querySelector('.actions-tags')
if (tagContainer) {
const tagButtons = buttons.find('button[name="add_tags"], button[name="remove_tags"]')
if (tagButtons.length) {
tagContainer.classList.add('actions-tags-with-buttons')
tagButtons.appendTo(tagContainer)
}
}
if (window.jQuery && window.jQuery.fn.select2) {
window.jQuery('select[multiple]').select2();
}
updateActionControlVisibility()
}
function updateActionControlVisibility() {
const selectAcross = document.querySelector('div.actions input.select-across')?.value === '1'
const checked = selectAcross
? Number(document.querySelector('.action-summary')?.dataset.resultCount || 0)
: document.querySelectorAll('#changelist-form input.action-select:checked').length
document.querySelectorAll('.action-buttons, .actions-tags-with-buttons').forEach(function(el) {
el.hidden = checked === 0
})
}
function setupActionSummary() {
const summary = document.querySelector('.action-summary')
if (!summary || summary.dataset.summaryReady) return
summary.dataset.summaryReady = '1'
const selectedCount = summary.querySelector('.action-selected-count')
const selectedCurrent = summary.querySelector('.action-selected-current')
const pageSelect = summary.querySelector('.action-page-select')
const counter = summary.querySelector('.action-counter')
const formatter = new Intl.NumberFormat()
const selectAllParam = '_archivebox_select_all'
const getSelectedCount = function() {
const selectAcross = document.querySelector('div.actions input.select-across')?.value === '1'
if (selectAcross) {
return Number(summary.dataset.resultCount || 0)
}
const match = counter?.textContent.match(/(\d+)\s+of\s+(\d+)\s+selected/)
return match ? Number(match[1]) : document.querySelectorAll('#changelist-form input.action-select:checked').length
}
const selectCurrentResultsWithDjango = function() {
const allToggle = document.getElementById('action-toggle')
const questionLink = summary.querySelector('.question a')
const selectAcross = document.querySelector('div.actions input.select-across')
const pageCount = Number(summary.dataset.pageCount || 0)
const resultCount = Number(summary.dataset.resultCount || pageCount)
if (allToggle) {
if (!allToggle.checked) {
allToggle.click()
}
if (questionLink && resultCount > pageCount) {
questionLink.click()
}
return true
}
document.querySelectorAll('#changelist-form input.action-select').forEach(function(checkbox) {
if (!checkbox.checked) {
checkbox.checked = true
checkbox.dispatchEvent(new Event('change', { bubbles: true }))
}
const card = checkbox.closest('.card')
if (card) {
card.classList.add('selected-card')
}
})
if (selectAcross && resultCount > pageCount) {
selectAcross.value = '1'
}
return false
}
const selectCurrentPageWithDjango = function() {
const allToggle = document.getElementById('action-toggle')
if (allToggle) {
if (!allToggle.checked) {
allToggle.click()
}
return true
}
document.querySelectorAll('#changelist-form input.action-select').forEach(function(checkbox) {
if (!checkbox.checked) {
checkbox.checked = true
checkbox.dispatchEvent(new Event('change', { bubbles: true }))
}
const card = checkbox.closest('.card')
if (card) {
card.classList.add('selected-card')
}
})
return false
}
const selectAllRowsWithDjango = function() {
const resultCount = Number(summary.dataset.resultCount || 0)
const fullResultCount = Number(summary.dataset.fullResultCount || resultCount)
if (fullResultCount > resultCount) {
const url = new URL(window.location.href)
url.search = '?' + selectAllParam + '=1'
window.location.href = url.toString()
return
}
selectCurrentResultsWithDjango()
}
const update = function() {
if (!selectedCount || !counter) return
const match = counter.textContent.match(/(\d+)\s+of\s+(\d+)\s+selected/)
const selectAcross = document.querySelector('div.actions input.select-across')?.value === '1'
const selected = selectAcross
? Number(summary.dataset.resultCount || 0)
: (match ? Number(match[1]) : document.querySelectorAll('#changelist-form input.action-select:checked').length)
const pageCount = match ? Number(match[2]) : Number(summary.dataset.pageCount || 0)
const selectedLimit = selectAcross ? Number(summary.dataset.resultCount || pageCount) : pageCount
if (selectedCurrent && pageSelect) {
selectedCurrent.textContent = formatter.format(selected)
pageSelect.textContent = formatter.format(selectedLimit)
} else {
selectedCount.textContent = formatter.format(selected) + ' / ' + formatter.format(selectedLimit) + ' selected'
}
summary.classList.toggle('action-summary-has-selection', selected > 0)
summary.classList.toggle('action-summary-select-across', selectAcross)
const totalCount = summary.querySelector('.action-total-count')
if (totalCount) {
totalCount.hidden = false
}
updateActionControlVisibility()
}
new MutationObserver(update).observe(counter, { childList: true, characterData: true, subtree: true })
document.querySelector('#changelist-form')?.addEventListener('change', function() {
window.setTimeout(update, 0)
})
summary.addEventListener('click', function(event) {
const pageSelect = event.target.closest('.action-page-select')
const explicitSelectAll = event.target.closest('.action-total-select, .action-total-count .question a')
const totalReset = event.target.closest('.action-total-reset')
if (pageSelect || explicitSelectAll || (totalReset && getSelectedCount() === 0)) {
event.preventDefault()
if (pageSelect) {
selectCurrentPageWithDjango()
} else if (totalReset) {
selectAllRowsWithDjango()
} else {
selectCurrentResultsWithDjango()
}
}
window.setTimeout(update, 0)
})
if (new URLSearchParams(window.location.search).get(selectAllParam) === '1') {
const cleanUrl = new URL(window.location.href)
cleanUrl.searchParams.delete(selectAllParam)
window.history.replaceState(null, '', cleanUrl.toString())
window.setTimeout(function() {
selectAllRowsWithDjango()
update()
}, 0)
}
update()
}
function setupSearchModeSelect() {
const storageKey = 'archivebox-admin-search-mode'
const params = new URLSearchParams(window.location.search)
document.querySelectorAll('.search-mode-select').forEach(function(select) {
if (select.dataset.searchModeReady) return
select.dataset.searchModeReady = '1'
const values = Array.from(select.options).map(option => option.value)
const stored = localStorage.getItem(storageKey)
if (!params.has('search_mode') && values.includes(stored)) {
select.value = stored
if ((params.get('q') || '').trim()) {
params.set('search_mode', stored)
params.delete('p')
window.location.replace(window.location.pathname + '?' + params.toString() + window.location.hash)
return
}
}
select.addEventListener('change', function() {
localStorage.setItem(storageKey, select.value)
const search = select.closest('#changelist-search')
if (search && search.dataset.embeddedSearch === '1') {
submitEmbeddedChangelistSearch(search)
return
}
if (search && search.tagName === 'FORM' && document.activeElement === select) {
search.requestSubmit()
}
})
})
}
function submitEmbeddedChangelistSearch(search) {
const params = new URLSearchParams(window.location.search)
search.querySelectorAll('[name]').forEach(function(input) {
const value = (input.value || '').trim()
if (value) {
params.set(input.name, value)
} else {
params.delete(input.name)
}
})
params.delete('p')
const query = params.toString()
window.location.href = window.location.pathname + (query ? '?' + query : '') + window.location.hash
}
function setupEmbeddedChangelistSearch() {
document.querySelectorAll('#changelist-search[data-embedded-search="1"]').forEach(function(search) {
if (search.dataset.searchReady) return
search.dataset.searchReady = '1'
search.querySelector('.changelist-search-submit')?.addEventListener('click', function() {
submitEmbeddedChangelistSearch(search)
})
search.querySelector('#searchbar')?.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault()
submitEmbeddedChangelistSearch(search)
}
})
})
}
function setupSnapshotPermissionsQuickEdit() {
if (document.body.dataset.snapshotPermissionsReady) return
document.body.dataset.snapshotPermissionsReady = '1'
document.addEventListener('click', function(event) {
const toggle = event.target.closest('.snapshot-permissions-button')
const item = event.target.closest('.snapshot-permissions-menu-item')
document.querySelectorAll('.snapshot-permissions-quick.is-open').forEach(function(openMenu) {
if (!openMenu.contains(event.target)) {
openMenu.classList.remove('is-open')
openMenu.querySelector('.snapshot-permissions-button')?.setAttribute('aria-expanded', 'false')
const menu = openMenu.querySelector('.snapshot-permissions-menu')
if (menu) menu.hidden = true
}
})
if (toggle) {
event.preventDefault()
const wrapper = toggle.closest('.snapshot-permissions-quick')
const menu = wrapper?.querySelector('.snapshot-permissions-menu')
if (!wrapper || !menu) return
const isOpen = wrapper.classList.toggle('is-open')
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false')
menu.hidden = !isOpen
return
}
if (!item) return
event.preventDefault()
const wrapper = item.closest('.snapshot-permissions-quick')
const permissions = item.dataset.permissions
const csrf = document.querySelector('input[name="csrfmiddlewaretoken"]')?.value
if (!wrapper || !permissions || !csrf || wrapper.dataset.saving === '1') return
wrapper.dataset.saving = '1'
const body = new URLSearchParams({permissions: permissions, csrfmiddlewaretoken: csrf})
fetch(wrapper.dataset.permissionsUrl, {
method: 'POST',
headers: {'X-CSRFToken': csrf, 'Content-Type': 'application/x-www-form-urlencoded'},
body: body.toString(),
}).then(function(response) {
if (!response.ok) throw new Error('Failed to update permissions')
return response.json()
}).then(function(data) {
wrapper.dataset.currentPermissions = data.permissions
wrapper.querySelectorAll('.snapshot-permissions-menu-item').forEach(function(button) {
button.classList.toggle('is-active', button.dataset.permissions === data.permissions)
})
const icon = wrapper.querySelector('.snapshot-permissions-icon')
if (icon) {
icon.textContent = data.icon
icon.style.color = data.fg
icon.style.background = data.bg
}
const toggle = wrapper.querySelector('.snapshot-permissions-button')
if (toggle) {
toggle.className = 'snapshot-permissions-button snapshot-permissions-' + data.permissions
toggle.title = data.label
toggle.setAttribute('aria-label', 'Change snapshot permissions: ' + data.label)
toggle.setAttribute('aria-expanded', 'false')
}
wrapper.classList.remove('is-open')
const menu = wrapper.querySelector('.snapshot-permissions-menu')
if (menu) menu.hidden = true
}).catch(function(error) {
window.alert(error.message)
}).finally(function() {
wrapper.dataset.saving = ''
})
})
}
function setupChangelistFormHandlers() {
const form = document.querySelector('#changelist-form')
if (form && !form.dataset.archiveboxActionsReady) {
form.dataset.archiveboxActionsReady = '1'
form.addEventListener('change', updateActionControlVisibility)
}
}
function fixInlineAddRow() {
$('#id_snapshottag-MAX_NUM_FORMS').val('1000')
$('.add-row').show()
}
function setupSnapshotGridListToggle() {
$("#snapshot-view-list").click(selectSnapshotListView)
$("#snapshot-view-grid").click(selectSnapshotGridView)
// Set active class based on current view
const isGridView = window.location.pathname === "{% url 'admin:grid' %}"
if (isGridView) {
$("#snapshot-view-grid").addClass('active')
$("#snapshot-view-list").removeClass('active')
} else {
$("#snapshot-view-list").addClass('active')
$("#snapshot-view-grid").removeClass('active')
}
$('#changelist-form .card input:checkbox').change(function() {
if ($(this).is(':checked'))
$(this).parents('.card').addClass('selected-card')
else
$(this).parents('.card').removeClass('selected-card')
})
};
function selectSnapshotIfHotlinked() {
// if we arrive at the index with a url like ??id__startswith=...
// we were hotlinked here with the intention of making it easy for the user to perform some
// actions on the given snapshot. therefore we should preselect the snapshot to save them a click
if (window.location.search.startsWith('?')) {
const result_checkboxes = [...document.querySelectorAll('#result_list .action-checkbox input[type=checkbox]')]
if (result_checkboxes.length === 1) {
result_checkboxes[0].click()
}
}
}
window.archiveboxHandleZipClick = function(link, event) {
if (!link || link.dataset.loading === '1') {
if (event) event.preventDefault()
return false
}
const resetZipButton = function(target) {
if (!target) return
target.dataset.loading = ''
target.classList.remove('is-loading')
target.removeAttribute('aria-busy')
const targetLabel = target.querySelector('.archivebox-zip-label')
const targetSpinner = target.querySelector('.archivebox-zip-spinner')
if (targetLabel) {
targetLabel.textContent = target.dataset.originalLabel || targetLabel.textContent
targetLabel.style.opacity = ''
targetLabel.style.display = ''
targetLabel.style.visibility = ''
}
if (targetSpinner) {
targetSpinner.style.display = ''
}
target.style.pointerEvents = ''
target.style.opacity = ''
target.style.width = target.dataset.originalWidth || ''
target.style.minWidth = target.dataset.originalMinWidth || ''
if (target._zipResetTimer) {
window.clearTimeout(target._zipResetTimer)
target._zipResetTimer = null
}
}
if (event) event.preventDefault()
const lockedWidth = Math.ceil(link.getBoundingClientRect().width)
link.dataset.originalWidth = link.style.width || ""
link.dataset.originalMinWidth = link.style.minWidth || ""
link.style.width = lockedWidth + "px"
link.style.minWidth = lockedWidth + "px"
link.dataset.loading = '1'
link.classList.add('is-loading')
link.setAttribute('aria-busy', 'true')
const label = link.querySelector('.archivebox-zip-label')
const spinner = link.querySelector('.archivebox-zip-spinner')
const loadingMode = link.dataset.loadingMode || 'label'
if (label) {
link.dataset.originalLabel = label.textContent || ''
if (loadingMode !== 'spinner-only') {
label.textContent = link.dataset.loadingLabel || 'Preparing...'
label.style.opacity = '0.92'
}
}
if (spinner) {
spinner.style.display = 'inline-block'
}
link.style.pointerEvents = 'none'
link.style.opacity = '0.96'
const destination = link.href
let iframe = document.getElementById('archivebox-zip-download-frame')
if (!iframe) {
iframe = document.createElement('iframe')
iframe.id = 'archivebox-zip-download-frame'
iframe.name = 'archivebox-zip-download-frame'
iframe.hidden = true
iframe.style.display = 'none'
document.body.appendChild(iframe)
}
iframe.onload = function() {
window.setTimeout(function() {
resetZipButton(link)
}, 250)
}
link._zipResetTimer = window.setTimeout(function() {
resetZipButton(link)
}, 12000)
void link.offsetWidth
requestAnimationFrame(function() {
requestAnimationFrame(function() {
iframe.src = destination
})
})
return false
}
window.archiveboxInitAdminChangelist = function() {
if (window.jQuery) {
fix_actions()
fixInlineAddRow()
setupSnapshotGridListToggle()
}
updateActionControlVisibility()
setupActionSummary()
setupSearchModeSelect()
setupEmbeddedChangelistSearch()
setupChangelistFormHandlers()
selectSnapshotIfHotlinked()
}
if ($) {
$(document).ready(function() {
window.archiveboxInitAdminChangelist()
setupSnapshotPermissionsQuickEdit()
setTimeOffset()
})
} else {
document.addEventListener('DOMContentLoaded', function() {
window.archiveboxInitAdminChangelist()
setupSnapshotPermissionsQuickEdit()
setTimeOffset()
})
}
</script>
<script src="{% static 'admin-inline-tags.js' %}"></script>
</body>
</html>