`
diff --git a/README.md b/README.md
index 44f321e..5b5131d 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,10 @@ Wenn die Planung steht, wird das Konzept per Knopfdruck in echte NetBox-Objekte
Dazu:
-* **Elevation mit Drag & Drop** – Geräte per Maus verschieben, serverseitig validiert.
- Ein ungültiger Zug wird abgelehnt und ändert nichts.
+* **Elevation mit Drag & Drop** – Geräte per Maus verschieben, auch von der
+ Front- in die Rear-Ansicht ziehen (und umgekehrt), um sie auf die andere Seite
+ umzuhängen. Alles serverseitig validiert; ein ungültiger Zug wird abgelehnt und
+ ändert nichts.
* **Geräte mit Bild und Text** – ist am Gerätetyp ein Front-/Rear-Bild hinterlegt, wird
es im Elevation-Block angezeigt; darüber liegt der Gerätename als weiße, schwarz
konturierte Schrift (kein dunkler Balken). Ein Dropdown über der Elevation schaltet
diff --git a/netbox_rack_concept/__init__.py b/netbox_rack_concept/__init__.py
index 8b76b6c..f2f5a9c 100644
--- a/netbox_rack_concept/__init__.py
+++ b/netbox_rack_concept/__init__.py
@@ -1,6 +1,6 @@
from netbox.plugins import PluginConfig
-__version__ = '0.2.1'
+__version__ = '0.2.2'
class RackConceptConfig(PluginConfig):
diff --git a/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo b/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo
index 1b59d06..7e2ce92 100644
Binary files a/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo and b/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo differ
diff --git a/netbox_rack_concept/locale/de/LC_MESSAGES/django.po b/netbox_rack_concept/locale/de/LC_MESSAGES/django.po
index 68aa609..2a52571 100644
--- a/netbox_rack_concept/locale/de/LC_MESSAGES/django.po
+++ b/netbox_rack_concept/locale/de/LC_MESSAGES/django.po
@@ -340,9 +340,6 @@ msgstr "Geräte"
msgid "Dimensions"
msgstr "Abmessungen"
-msgid "Drag devices to move them"
-msgstr "Geräte zum Verschieben ziehen"
-
msgid "Draw.io (.drawio)"
msgstr "Draw.io (.drawio)"
@@ -926,3 +923,8 @@ msgid "{{ counter }} planned device will be created per rack."
msgid_plural "{{ counter }} planned devices will be created per rack."
msgstr[0] "Pro Rack wird {{ counter }} geplantes Gerät angelegt."
msgstr[1] "Pro Rack werden {{ counter }} geplante Geräte angelegt."
+
+msgid "Drag devices to move or flip them between front and rear"
+msgstr ""
+"Geräte ziehen, um sie zu verschieben oder zwischen Front und Rückseite zu "
+"wechseln"
diff --git a/netbox_rack_concept/static/netbox_rack_concept/elevation.css b/netbox_rack_concept/static/netbox_rack_concept/elevation.css
index cf09968..0461105 100644
--- a/netbox_rack_concept/static/netbox_rack_concept/elevation.css
+++ b/netbox_rack_concept/static/netbox_rack_concept/elevation.css
@@ -10,6 +10,10 @@
height: auto;
user-select: none;
touch-action: none;
+ /* A device being dragged toward the other face's panel shouldn't just vanish the
+ moment it crosses this SVG's own edge - let it render past the boundary while
+ it's mid-drag, into whatever sits next to it (the other panel, side by side). */
+ overflow: visible;
}
.concept-elevation .unit-slot {
diff --git a/netbox_rack_concept/static/netbox_rack_concept/elevation.js b/netbox_rack_concept/static/netbox_rack_concept/elevation.js
index 58671db..1e991db 100644
--- a/netbox_rack_concept/static/netbox_rack_concept/elevation.js
+++ b/netbox_rack_concept/static/netbox_rack_concept/elevation.js
@@ -113,10 +113,18 @@
return Math.min(Math.max(value, min), max);
}
+ /* Which .concept-elevation panel (front or rear) the pointer is currently over -
+ used at drop time to tell whether a device was dragged onto the other face. */
+ function panelAt(clientX, clientY) {
+ const el = document.elementFromPoint(clientX, clientY);
+ return el ? el.closest('.concept-elevation') : null;
+ }
+
container.querySelectorAll('.concept-elevation').forEach(function (panel) {
const svg = panel.querySelector('svg');
const geo = geometry(svg);
if (!geo) return;
+ const sourceFace = panel.dataset.face;
panel.querySelectorAll('.device-block:not(.opposite)').forEach(function (block) {
block.addEventListener('pointerdown', onPointerDown);
@@ -139,6 +147,11 @@
let dy = 0;
block.classList.add('dragging');
+ // Without this, elementFromPoint() at drop time would always hit the
+ // dragged block itself (it's what's rendered under the cursor), never
+ // whatever panel is actually underneath it - pointer capture (below)
+ // keeps move/up events coming to `block` regardless, so this is safe.
+ block.style.pointerEvents = 'none';
block.setPointerCapture(event.pointerId);
function onMove(moveEvent) {
@@ -152,11 +165,12 @@
block.setAttribute('transform', 'translate(' + dx + ' ' + snappedY + ')');
}
- function onUp() {
+ function onUp(upEvent) {
block.removeEventListener('pointermove', onMove);
block.removeEventListener('pointerup', onUp);
block.removeEventListener('pointercancel', onUp);
block.classList.remove('dragging');
+ block.style.pointerEvents = '';
if (!moved) {
// Treat it as a click: follow the device link.
@@ -166,6 +180,14 @@
return;
}
+ // Front and rear share one coordinate system (same UNIT_HEIGHT,
+ // RACK_X, ...), just placed side by side on screen - the delta
+ // computed against the source panel is equally valid whichever
+ // panel the pointer ends up over, so dropping onto the other face
+ // only needs to swap which face gets submitted.
+ const targetPanel = panelAt(upEvent.clientX, upEvent.clientY);
+ const targetFace = (targetPanel && targetPanel.dataset.face) || sourceFace;
+
const unitDelta = Math.round(dy / geo.unitHeight) * (descUnits ? 1 : -1);
const lowest = startingUnit;
const highest = startingUnit + rackHeight - deviceUnits;
@@ -180,7 +202,8 @@
);
}
- submit(block, newPosition, fraction > 1 ? horizontal : null);
+ submit(block, newPosition, fraction > 1 ? horizontal : null,
+ targetFace !== sourceFace ? targetFace : null);
}
block.addEventListener('pointermove', onMove);
@@ -190,7 +213,7 @@
});
});
- function submit(block, position, horizontalPosition) {
+ function submit(block, position, horizontalPosition, face) {
const move = {
id: parseInt(block.dataset.deviceId, 10),
position: position
@@ -198,6 +221,9 @@
if (horizontalPosition !== null) {
move.horizontal_position = horizontalPosition;
}
+ if (face) {
+ move.face = face;
+ }
setStatus('Saving…', false);
diff --git a/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html b/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html
index bd8f85a..028aaf8 100644
--- a/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html
+++ b/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html
@@ -138,7 +138,7 @@
{% trans "Labels only" %}
{% if perms.netbox_rack_concept.change_conceptdevice %}
- {% trans "Drag devices to move them" %}
+ {% trans "Drag devices to move or flip them between front and rear" %}
{% endif %}
diff --git a/pyproject.toml b/pyproject.toml
index 4f411cb..80cfee9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "netbox-rack-concept"
-version = "0.2.1"
+version = "0.2.2"
description = "Rack blueprints for NetBox: plan racks and their equipment without creating real racks or devices"
readme = "README.md"
requires-python = ">=3.12"