OIDC Integration: Supports Authentik officially, other solutions currently untested. Social login not implemented

This commit is contained in:
boris committed 2026-03-30 13:38:18 +01:00
1 parent dfea7e869b
commit 68bbb53976
28 files changed
+2365 -751

No files matched your search

@@ -1 +1,11 @@
(()=>{function le(o,t){var e=Object.keys(o);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(o);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(o,i).enumerable})),e.push.apply(e,n)}return e}function K(o){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?arguments[t]:{};t%2?le(Object(e),!0).forEach(function(n){Fe(o,n,e[n])}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(e)):le(Object(e)).forEach(function(n){Object.defineProperty(o,n,Object.getOwnPropertyDescriptor(e,n))})}return o}function Pt(o){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Pt=function(t){return typeof t}:Pt=function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Pt(o)}function Fe(o,t,e){return t in o?Object.defineProperty(o,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):o[t]=e,o}function U(){return U=Object.assign||function(o){for(var t=1;t<arguments.length;t++){var e=arguments[t];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(o[n]=e[n])}return o},U.apply(this,arguments)}function Re(o,t){if(o==null)return{};var e={},n=Object.keys(o),i,r;for(r=0;r<n.length;r++)i=n[r],!(t.indexOf(i)>=0)&&(e[i]=o[i]);return e}function ke(o,t){if(o==null)return{};var e=Re(o,t),n,i;if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(o);for(i=0;i<r.length;i++)n=r[i],!(t.indexOf(n)>=0)&&Object.prototype.propertyIsEnumerable.call(o,n)&&(e[n]=o[n])}return e}var He="1.15.2";function $(o){if(typeof window<"u"&&window.navigator)return!!navigator.userAgent.match(o)}var q=$(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),Ot=$(/Edge/i),se=$(/firefox/i),Et=$(/safari/i)&&!$(/chrome/i)&&!$(/android/i),me=$(/iP(ad|od|hone)/i),ve=$(/chrome/i)&&$(/android/i),be={capture:!1,passive:!1};function y(o,t,e){o.addEventListener(t,e,!q&&be)}function b(o,t,e){o.removeEventListener(t,e,!q&&be)}function Xt(o,t){if(t){if(t[0]===">"&&(t=t.substring(1)),o)try{if(o.matches)return o.matches(t);if(o.msMatchesSelector)return o.msMatchesSelector(t);if(o.webkitMatchesSelector)return o.webkitMatchesSelector(t)}catch{return!1}return!1}}function Xe(o){return o.host&&o!==document&&o.host.nodeType?o.host:o.parentNode}function G(o,t,e,n){if(o){e=e||document;do{if(t!=null&&(t[0]===">"?o.parentNode===e&&Xt(o,t):Xt(o,t))||n&&o===e)return o;if(o===e)break}while(o=Xe(o))}return null}var ue=/\s+/g;function F(o,t,e){if(o&&t)if(o.classList)o.classList[e?"add":"remove"](t);else{var n=(" "+o.className+" ").replace(ue," ").replace(" "+t+" "," ");o.className=(n+(e?" "+t:"")).replace(ue," ")}}function h(o,t,e){var n=o&&o.style;if(n){if(e===void 0)return document.defaultView&&document.defaultView.getComputedStyle?e=document.defaultView.getComputedStyle(o,""):o.currentStyle&&(e=o.currentStyle),t===void 0?e:e[t];!(t in n)&&t.indexOf("webkit")===-1&&(t="-webkit-"+t),n[t]=e+(typeof e=="string"?"":"px")}}function ct(o,t){var e="";if(typeof o=="string")e=o;else do{var n=h(o,"transform");n&&n!=="none"&&(e=n+" "+e)}while(!t&&(o=o.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(e)}function ye(o,t,e){if(o){var n=o.getElementsByTagName(t),i=0,r=n.length;if(e)for(;i<r;i++)e(n[i],i);return n}return[]}function W(){var o=document.scrollingElement;return o||document.documentElement}function O(o,t,e,n,i){if(!(!o.getBoundingClientRect&&o!==window)){var r,a,l,s,u,d,c;if(o!==window&&o.parentNode&&o!==W()?(r=o.getBoundingClientRect(),a=r.top,l=r.left,s=r.bottom,u=r.right,d=r.height,c=r.width):(a=0,l=0,s=window.innerHeight,u=window.innerWidth,d=window.innerHeight,c=window.innerWidth),(t||e)&&o!==window&&(i=i||o.parentNode,!q))do if(i&&i.getBoundingClientRect&&(h(i,"transform")!=="none"||e&&h(i,"position")!=="static")){var m=i.getBoundingClientRect();a-=m.top+parseInt(h(i,"border-top-width")),l-=m.left+parseInt(h(i,"border-left-width")),s=a+r.height,u=l+r.width;break}while(i=i.parentNode);if(n&&o!==window){var w=ct(i||o),v=w&&w.a,E=w&&w.d;w&&(a/=E,l/=v,c/=v,d/=E,s=a+d,u=l+c)}return{top:a,left:l,bottom:s,right:u,width:c,height:d}}}function fe(o,t,e){for(var n=tt(o,!0),i=O(o)[t];n;){var r=O(n)[e],a=void 0;if(e==="top"||e==="left"?a=i>=r:a=i<=r,!a)return n;if(n===W())break;n=tt(n,!1)}return!1}function dt(o,t,e,n){for(var i=0,r=0,a=o.children;r<a.length;){if(a[r].style.display!=="none"&&a[r]!==p.ghost&&(n||a[r]!==p.dragged)&&G(a[r],e.draggable,o,!1)){if(i===t)return a[r];i++}r++}return null}function oe(o,t){for(var e=o.lastElementChild;e&&(e===p.ghost||h(e,"display")==="none"||t&&!Xt(e,t));)e=e.previousElementSibling;return e||null}function H(o,t){var e=0;if(!o||!o.parentNode)return-1;for(;o=o.previousElementSibling;)o.nodeName.toUpperCase()!=="TEMPLATE"&&o!==p.clone&&(!t||Xt(o,t))&&e++;return e}function ce(o){var t=0,e=0,n=W();if(o)do{var i=ct(o),r=i.a,a=i.d;t+=o.scrollLeft*r,e+=o.scrollTop*a}while(o!==n&&(o=o.parentNode));return[t,e]}function Ye(o,t){for(var e inLine truncated
/*! Bundled license information:
sortablejs/modular/sortable.esm.js:
(**!
* Sortable 1.15.2
* @author RubaXa <trash@rubaxa.org>
* @author owenm <owen23355@gmail.com>
* @license MIT
*)
*/
+354 -23
View File
@@ -9,11 +9,49 @@ window.addEventListener("load", (e) => {
warnWithoutSaving();
inlines();
tabNavigation();
scrollSidebarNav();
});
/*************************************************************
* Switch theme
*************************************************************/
function theme(defaultTheme = "auto") {
return {
adminTheme: Alpine.$persist(defaultTheme).as('adminTheme'),
switchTheme(theme) {
this.adminTheme = theme;
},
themeBindings: {
['x-bind:class']() {
if (this.adminTheme === 'dark' || (this.adminTheme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
return 'dark';
}
if (this.adminTheme === 'light') {
return 'light';
}
return '';
},
['x-on:keydown.window'](event) {
if ((event.metaKey || event.ctrlKey) && event.key === "e") {
event.preventDefault();
if (this.adminTheme == "light") {
this.adminTheme = "dark";
} else {
this.adminTheme = "light";
}
}
}
}
}
}
/*************************************************************
* Scroll sidebar to active item
*************************************************************/
@@ -110,18 +148,36 @@ function tabNavigation() {
/*************************************************************
* Alpine.sort.js callback after sorting
*************************************************************/
const sortRecords = (e) => {
const orderingField = e.from.dataset.orderingField;
function sortRecords(e) {
e.from
.querySelectorAll(`.form-group.original`)
.forEach(function (row, index) {
input = row.querySelector(
`input[name$=-${e.from.dataset.orderingField}]`
);
input.value = index;
});
const weightInputs = Array.from(
e.from.querySelectorAll(
`.has_original input[name$=-${orderingField}], td.field-${orderingField} input[name$=-${orderingField}]`
e.from
.querySelectorAll(
`td.field-${e.from.dataset.orderingField} input[name$=-${e.from.dataset.orderingField}]`
)
);
.forEach(function (input, index) {
input.value = index;
});
}
weightInputs.forEach((input, index) => {
input.value = index;
});
/*************************************************************
* Alpine.sort.js callback before moving records
*************************************************************/
const moveRecords = (e) => {
if (
(e.related.classList.contains("template") ||
e.related.classList.contains("form-actions")) &&
e.willInsertAfter
) {
return false;
}
};
/*************************************************************
@@ -434,7 +490,7 @@ const watchClassChanges = (selector, callback) => {
/*************************************************************
* Calendar & clock
*************************************************************/
const dateTimeShortcutsOverlay = () => {
function dateTimeShortcutsOverlay() {
const observer = new MutationObserver((mutations) => {
for (const mutationRecord of mutations) {
const display = mutationRecord.target.style.display;
@@ -448,27 +504,32 @@ const dateTimeShortcutsOverlay = () => {
}
});
const targets = document.querySelectorAll(".calendarbox, .clockbox");
const findCalendars = () => {
for (const target of document.querySelectorAll(".calendarbox, .clockbox")) {
observer.observe(target, {
attributes: true,
attributeFilter: ["style"],
});
}
};
for (const target of targets) {
observer.observe(target, {
attributes: true,
attributeFilter: ["style"],
});
}
new MutationObserver(findCalendars).observe(document.body, {
childList: true,
subtree: true,
});
};
/*************************************************************
* File upload path
*************************************************************/
const fileInputUpdatePath = () => {
const checkInputChanged = () => {
function fileInputUpdatePath() {
function checkInputChanged() {
for (const input of document.querySelectorAll("input[type=file]")) {
if (input.hasChangeListener) {
continue;
}
input.addEventListener("change", (e) => {
input.addEventListener("change", function (e) {
const parts = e.target.value.split("\\");
const placeholder =
input.parentNode.parentNode.parentNode.querySelector(
@@ -479,9 +540,9 @@ const fileInputUpdatePath = () => {
input.hasChangeListener = true;
}
};
}
new MutationObserver(() => {
new MutationObserver(function () {
checkInputChanged();
}).observe(document.body, {
childList: true,
@@ -489,7 +550,7 @@ const fileInputUpdatePath = () => {
});
checkInputChanged();
};
}
/*************************************************************
* Chart
@@ -719,6 +780,276 @@ const renderCharts = () => {
});
};
/*************************************************************
* Inlines
*************************************************************/
function inlines() {
/**
* Replacing: https://github.com/django/django/blob/main/django/contrib/admin/static/admin/js/inlines.js
*
* Expected HTML structure:
*
* <div class="formset-wrapper">
* ... management form ...
* <div class="formset">
* <div class="form-group">
* <div class="form-row">inputs</div>
* <div class="form-nested">nested inlines if exists</div>
* </div>
*
* ... another form group ...
*
* <div class="form-actions">
* <div class="add-row">Add record button</div>
* </div>
* </div>
* </div>
*/
document.querySelectorAll(".add-row").forEach(function (el) {
el.addEventListener("click", addInlineTemplateHandler);
});
document.querySelectorAll(".delete-template").forEach(function (el) {
el.addEventListener("click", deleteInlineTemplateHandler);
});
}
function deleteInlineTemplateHandler(e) {
const formset = e.target.closest(".formset");
const options = JSON.parse(formset.dataset.inlineFormset).options;
const formsetWrapper = e.target.closest(".formset-wrapper");
const minNumForms = formsetWrapper.querySelector(
"input[type=hidden][name$='MIN_NUM_FORMS']"
);
const totalForms = formsetWrapper.querySelector(
"input[type=hidden][name$='TOTAL_FORMS']"
);
const maxNumForms = formsetWrapper.querySelector(
"input[type=hidden][name$='MAX_NUM_FORMS']"
);
const rowToDelete = e.target.closest(".form-group");
if (parseInt(totalForms.value) == parseInt(minNumForms.value)) {
return;
}
let reindex = false;
formset
.querySelectorAll(":scope > .form-group:not(.empty-form)")
.forEach((row, index) => {
if (rowToDelete.isSameNode(row)) {
reindex = true;
}
if (!rowToDelete.isSameNode(row) && reindex) {
const content = row.querySelector(":scope > .form-row");
content.innerHTML = content.innerHTML.replaceAll(
`${options.prefix}-${index}`,
`${options.prefix}-${index - 1}`
);
// Reindex nested formset
const nestedFormset = row.querySelector(":scope .form-nested");
if (nestedFormset) {
nestedFormset.innerHTML = nestedFormset.innerHTML.replaceAll(
`${options.prefix}-${index}`,
`${options.prefix}-${index - 1}`
);
}
}
});
const rowToDeleteParent = rowToDelete.parentElement;
rowToDelete.remove();
updateStackedCounter(rowToDeleteParent);
// Update total forms count
totalForms.value = parseInt(
formset.querySelectorAll(":scope > .form-group:not(.empty-form)").length,
10
);
// Apply delete event
formset.querySelectorAll(".delete-template").forEach(function (el) {
el.removeEventListener("click", deleteInlineTemplateHandler);
el.addEventListener("click", deleteInlineTemplateHandler);
});
// Show add button
if (parseInt(totalForms.value, 10) < parseInt(maxNumForms.value, 10)) {
formset.querySelector(".add-row").classList.remove("hidden");
}
applyLastClass(formset);
checkFormsetEmpty(formset);
document.dispatchEvent(
new CustomEvent("formset:removed", {
detail: {
formsetName: options.prefix,
},
})
);
}
function addInlineTemplateHandler(e) {
e.preventDefault();
const formset = e.target.closest(".formset");
const formsetWrapper = e.target.closest(".formset-wrapper");
const totalForms = formsetWrapper.querySelector(
"input[type=hidden][name$='TOTAL_FORMS']"
);
const maxNumForms = formsetWrapper.querySelector(
"input[type=hidden][name$='MAX_NUM_FORMS']"
);
const template = formset.querySelector(":scope >.empty-form");
const index = formset.querySelectorAll(":scope > .form-group").length - 1;
const options = JSON.parse(formset.dataset.inlineFormset).options;
const row = template.cloneNode(true);
row.classList.remove("empty-form");
if (parseInt(totalForms.value, 10) >= parseInt(maxNumForms.value, 10)) {
return;
}
// Insert new row BEFORE template row which is always last in formset
formset.insertBefore(row, template);
applyLastClass(formset);
checkFormsetEmpty(formset);
// Stacked inlines are having a counter in title
updateStackedCounter(row.parentElement);
// Replace __prefix__ in RECENTLY ADDED row
const formRow = row.querySelector(":scope > .form-row");
formRow.innerHTML = formRow.innerHTML.replaceAll(/__prefix__/g, index);
// Update hidden TOTAL_FORMS input value
totalForms.value = parseInt(totalForms.value, 10) + 1;
// Hide add row button if it is not possible to add more rows
if (totalForms.value === maxNumForms.value) {
e.target.parentElement.classList.add("hidden");
}
// Call special `formsetGroup:added` event to reinitialize inlines
row.dispatchEvent(
new CustomEvent("formsetGroup:added", {
bubbles: true,
detail: {
formsetName: options.prefix,
row: row,
index: index,
},
})
);
}
document.addEventListener("formsetGroup:added", function (e) {
if (!e.detail.row) {
return;
}
e.detail.row.innerHTML = e.detail.row.innerHTML.replaceAll(
`${e.detail.formsetName}-__prefix__`,
`${e.detail.formsetName}-${e.detail.index}`
);
if (typeof DateTimeShortcuts !== "undefined") {
document.querySelectorAll(".datetimeshortcuts").forEach(function (el) {
el.remove();
});
DateTimeShortcuts.init();
dateTimeShortcutsOverlay();
}
e.detail.row.querySelector(":scope > .form-row").dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: { formsetName: e.detail.formsetName },
})
);
e.detail.row
.querySelectorAll(".nested-formset > .form-group:not(.empty-form)")
.forEach(function (formGroup) {
formGroup.querySelector(".form-row").dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: { formsetName: e.detail.formsetName },
})
);
});
e.detail.row.querySelectorAll(".add-row").forEach(function (el) {
el.addEventListener("click", addInlineTemplateHandler);
});
e.detail.row.querySelectorAll(".delete-template").forEach(function (el) {
el.addEventListener("click", deleteInlineTemplateHandler);
});
});
function checkFormsetEmpty(formset) {
const formGroups = formset.querySelectorAll(
":scope > .form-group:not(.empty-form)"
);
if (formGroups.length === 0) {
formset.classList.add("empty");
} else {
formset.classList.remove("empty");
}
}
function applyLastClass(formset) {
formset.querySelectorAll(":scope > .form-group").forEach(function (row) {
if (row.classList.contains("last")) {
row.classList.remove("last");
}
if (
row.nextElementSibling.classList.contains("empty-form") ||
row.nextElementSibling.classList.contains("form-actions")
) {
row.classList.add("last");
}
});
}
function updateStackedCounter(rowsParent) {
rowsParent
.querySelectorAll(":scope > .form-group:not(.empty-form) > .form-row")
.forEach(function (row, index) {
const counter = row.querySelector(".stacked-counter");
if (counter) {
counter.textContent = index + 1;
}
});
}
/*************************************************************
* Reinitialize inlines after pagination AJAX request
*************************************************************/
document.addEventListener("htmx:afterSettle", function (e) {
e.target.querySelectorAll(".form-row").forEach(function (formRow) {
formRow.dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: {
formsetName: JSON.parse(e.target.dataset.inlineFormset).options
.prefix,
},
})
);
});
});
function getCurrentTab() {
const fragment = window.location.hash?.replace('#', '');