OIDC Integration: Supports Authentik officially, other solutions currently untested. Social login not implemented
This commit is contained in:
1 parent
dfea7e869b
commit
68bbb53976
28 files changed
+2365
-751
No files matched your search
+354
-23
@@ -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('#', '');
|
||||
|
||||
|
||||
Reference in new issue
Block a user