feat: Ordnerstruktur, Medienupload und erweiterten Editor ergänzen

- NetBox-4.6-Verknüpfungsformular korrigieren
- hierarchische Dokumentationsordner hinzufügen
- breite Editoransicht und Fokusmodus implementieren
- sicheren direkten Bild-Upload integrieren
- Ordner über Suche und REST-API bereitstellen
This commit is contained in:
2026-07-22 11:04:10 +02:00
parent da0774b3aa
commit 59598fbd74
19 changed files with 324 additions and 47 deletions
@@ -7,6 +7,7 @@
<div class="card"><div class="card-body rendered-markdown">{{ object.rendered_body|safe }}</div></div>
</div>
<div class="col col-md-3">
{% if object.category %}<div class="card mb-3"><h5 class="card-header">Ordner</h5><a class="list-group-item list-group-item-action" href="{{ object.category.get_absolute_url }}"><i class="mdi mdi-folder"></i> {{ object.category }}</a></div>{% endif %}
<div class="card"><h5 class="card-header">Zuordnungen</h5><div class="list-group list-group-flush">
{% for assignment in object.assignments.all %}<a class="list-group-item" href="{{ assignment.assigned_object.get_absolute_url }}">{{ assignment.assigned_object_type }}: {{ assignment.assigned_object }}</a>{% empty %}<div class="list-group-item text-muted">Noch nicht zugeordnet</div>{% endfor %}
</div></div>
@@ -1,9 +1,20 @@
{% extends 'generic/object_edit.html' %}
{% block javascript %}
{{ block.super }}
<style>
body.documentation-editor-page .container-xl { max-width: none !important; }
body.documentation-editor-focus #form_fields .field-group:not(.documentation-content-group) { display: none; }
body.documentation-editor-focus #form_fields .documentation-content-group { margin-bottom: 0 !important; }
</style>
<script src="{% url 'plugins:netbox_documentation:editor_asset' asset_path='tinymce.min.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.body.classList.add('documentation-editor-page');
const bodyField = document.querySelector('#id_body');
if (bodyField) {
const contentGroup = bodyField.closest('.field-group');
if (contentGroup) contentGroup.classList.add('documentation-content-group');
}
const editorAssetBase = "{% url 'plugins:netbox_documentation:editor_asset' asset_path='_' %}".replace(/_$/, '');
if (typeof tinymce === 'undefined') {
const editor = document.querySelector('[data-rich-text-editor]');
@@ -25,7 +36,7 @@
plugins: 'advlist anchor autolink charmap code fullscreen image link lists media preview searchreplace table visualblocks wordcount',
toolbar: [
'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | forecolor backcolor',
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table link image media | removeformat code fullscreen'
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table link image media | removeformat code netboxfocus fullscreen'
],
toolbar_mode: 'sliding',
table_toolbar: 'tableprops tablecellprops | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol | tablemergecells tablesplitcells | tabledelete',
@@ -38,7 +49,43 @@
branding: false,
convert_unsafe_embeds: true,
relative_urls: false
{% if object.pk %},
images_file_types: 'jpg,jpeg,png,gif,webp',
automatic_uploads: true,
images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => {
const data = new FormData();
data.append('file', blobInfo.blob(), blobInfo.filename());
fetch("{% url 'plugins:netbox_documentation:document_media_upload' pk=object.pk %}", {
method: 'POST',
headers: {'X-CSRFToken': document.querySelector('input[name=csrfmiddlewaretoken]').value},
body: data,
credentials: 'same-origin'
}).then(async response => {
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || 'Upload fehlgeschlagen');
resolve(payload.location);
}).catch(error => reject(error.message));
})
{% endif %},
setup: (editor) => {
editor.ui.registry.addToggleButton('netboxfocus', {
icon: 'expand',
tooltip: 'Fokusmodus im NetBox-Fenster',
onAction: api => {
const active = document.body.classList.toggle('documentation-editor-focus');
const container = editor.getContainer();
container.style.height = active ? 'calc(100vh - 190px)' : '650px';
api.setActive(active);
}
});
}
});
});
</script>
{% endblock javascript %}
{% block pre_form_fields %}
{% if not object.pk %}
<div class="alert alert-info">Speichere die neue Dokumentation einmal. Danach können Bilder direkt über die Editor-Werkzeugleiste hochgeladen werden.</div>
{% endif %}
{% endblock pre_form_fields %}
@@ -0,0 +1,26 @@
{% extends 'generic/object.html' %}
{% block content %}
<div class="row">
<div class="col col-md-4">
<div class="card">
<h5 class="card-header">Unterordner</h5>
<div class="list-group list-group-flush">
{% for child in object.children.all %}
<a class="list-group-item list-group-item-action" href="{{ child.get_absolute_url }}"><i class="mdi mdi-folder"></i> {{ child.name }}</a>
{% empty %}<div class="list-group-item text-muted">Keine Unterordner</div>{% endfor %}
</div>
</div>
</div>
<div class="col col-md-8">
<div class="card">
<h5 class="card-header">Dokumentationen</h5>
<div class="list-group list-group-flush">
{% for document in object.documents.all %}
<a class="list-group-item list-group-item-action" href="{{ document.get_absolute_url }}"><strong>{{ document.title }}</strong>{% if document.summary %}<br><small>{{ document.summary }}</small>{% endif %}</a>
{% empty %}<div class="list-group-item text-muted">Keine Dokumentationen in diesem Ordner</div>{% endfor %}
</div>
</div>
</div>
</div>
{% endblock content %}
@@ -7,5 +7,5 @@
</a>
{% empty %}<div class="list-group-item text-muted">Keine Dokumentation zugeordnet.</div>{% endfor %}
</div>
{% if perms.netbox_documentation.add_documentassignment %}<div class="card-footer"><a href="{% url 'plugins:netbox_documentation:documentassignment_add' %}?assigned_object_type={{ content_type.pk }}&assigned_object_id={{ object.pk }}" class="btn btn-sm btn-primary"><i class="mdi mdi-link-plus"></i> Zuordnen</a></div>{% endif %}
{% if perms.netbox_documentation.add_documentassignment %}<div class="card-footer"><a href="{% url 'plugins:netbox_documentation:documentassignment_add' %}?assigned_object_type={{ content_type.pk }}&assigned_object={{ object.pk }}" class="btn btn-sm btn-primary"><i class="mdi mdi-link-plus"></i> Zuordnen</a></div>{% endif %}
</div>