feat: add bulk images and tenant autofill

This commit is contained in:
2026-08-03 15:35:44 +02:00
parent cd542f04f7
commit d54fc646ee
16 changed files with 641 additions and 9 deletions
@@ -0,0 +1,106 @@
(() => {
'use strict';
const dataElement = document.getElementById('netbox-utilities-frontend-data');
if (!dataElement) return;
let config;
try {
config = JSON.parse(dataElement.textContent);
} catch (_error) {
return;
}
function rewriteImageUploadLinks(root = document) {
if (!config.image_attachment_add_url || !config.bulk_image_upload_url) return;
const singleUploadPath = new URL(config.image_attachment_add_url, document.baseURI).pathname;
root.querySelectorAll('a[href]').forEach((link) => {
const current = new URL(link.href, document.baseURI);
if (current.pathname !== singleUploadPath) return;
if (!current.searchParams.has('object_type') || !current.searchParams.has('object_id')) return;
const bulkUpload = new URL(config.bulk_image_upload_url, document.baseURI);
bulkUpload.search = current.search;
link.href = bulkUpload.href;
link.title = 'Mehrere Bilder gleichzeitig hochladen';
link.replaceChildren();
const icon = document.createElement('i');
icon.className = 'mdi mdi-image-multiple me-1';
icon.setAttribute('aria-hidden', 'true');
link.append(icon, document.createTextNode('Mehrere Bilder hochladen'));
});
}
function configureTenantConfirmation(root = document) {
root.querySelectorAll('[data-netbox-utilities-autofilled-tenant]').forEach((field) => {
const form = field.form;
if (!form || form.dataset.netboxUtilitiesTenantConfirmation === 'true') return;
form.dataset.netboxUtilitiesTenantConfirmation = 'true';
form.addEventListener('submit', (event) => {
const candidateFields = form.querySelectorAll('[data-netbox-utilities-autofilled-tenant]');
for (const candidateField of candidateFields) {
const candidate = candidateField.dataset.netboxUtilitiesAutofilledTenant;
if (!candidate || String(candidateField.value) !== candidate) continue;
if (candidateField.dataset.netboxUtilitiesTenantConfirmed === candidate) continue;
const selectedOption = candidateField.selectedOptions?.[0];
const tenantLabel = selectedOption?.textContent.trim() || `ID ${candidate}`;
const confirmed = window.confirm(
`Der Mandant „${tenantLabel}“ wurde automatisch erkannt. Bitte bestätigen Sie diese Zuordnung.`
);
if (!confirmed) {
event.preventDefault();
candidateField.focus();
return;
}
candidateField.dataset.netboxUtilitiesTenantConfirmed = candidate;
}
});
});
}
function configureImagePreview(root = document) {
const input = root.querySelector('.netbox-utilities-multi-image-input');
const preview = document.getElementById('netbox-utilities-image-preview');
if (!input || !preview || input.dataset.netboxUtilitiesPreview === 'true') return;
input.dataset.netboxUtilitiesPreview = 'true';
let objectUrls = [];
input.addEventListener('change', () => {
objectUrls.forEach((url) => URL.revokeObjectURL(url));
objectUrls = [];
preview.replaceChildren();
Array.from(input.files).forEach((file) => {
const objectUrl = URL.createObjectURL(file);
objectUrls.push(objectUrl);
const item = document.createElement('div');
item.className = 'netbox-utilities-image-preview-item';
const image = document.createElement('img');
image.src = objectUrl;
image.alt = '';
const name = document.createElement('div');
name.className = 'netbox-utilities-image-preview-name text-secondary mt-1';
name.title = file.name;
name.textContent = file.name;
item.append(image, name);
preview.appendChild(item);
});
});
}
function initialize(root = document) {
rewriteImageUploadLinks(root);
configureTenantConfirmation(root);
configureImagePreview(root);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => initialize(), {once: true});
} else {
initialize();
}
document.addEventListener('htmx:afterSettle', (event) => initialize(event.target));
})();
@@ -10,6 +10,25 @@
white-space: nowrap;
}
.netbox-utilities-image-preview-item {
width: 9rem;
}
.netbox-utilities-image-preview-item img {
width: 9rem;
height: 7rem;
object-fit: cover;
border: 1px solid var(--tblr-border-color);
border-radius: var(--tblr-border-radius);
}
.netbox-utilities-image-preview-name {
overflow: hidden;
font-size: 0.75rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.netbox-utilities-sidebar-resizer {
display: none;
}