v0.2.2: support dragging a device between front and rear elevations
Front and rear panels already share one coordinate system (same UNIT_HEIGHT, RACK_X, ...), just placed side by side on screen, so the existing delta-based drag math is equally valid for a drop on the other panel - only needed to detect which face the pointer ended up over (elementFromPoint at drop time) and include it as `face` in the submitted move; the reorder endpoint already supported changing it. Also fixed a bug this surfaced: without pointer-events: none on the dragged block during the drag, elementFromPoint() at drop time always hit the dragged block itself (it's what's rendered under the cursor), never the panel actually underneath - cross-face drops would never have been detected. Pointer capture keeps delivering move/up events to the block regardless of its own pointer-events value, so this is safe. Added overflow: visible on the elevation SVGs so a device being dragged toward the other panel doesn't just vanish at its own SVG's edge mid-drag. Updated the German translation for the changed drag-hint string. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.2
|
||||||
|
|
||||||
|
- Drag-and-drop now supports dragging a device from the front elevation onto the rear
|
||||||
|
one (and back), reassigning its face. Front and rear share one coordinate system
|
||||||
|
(same unit height, rack offset, ...), just placed side by side on screen, so the
|
||||||
|
existing delta-based drag math is already valid for a cross-panel drop - only the
|
||||||
|
target face needed detecting (via `elementFromPoint` at drop time) and sending along.
|
||||||
|
Fixed a related bug along the way: without `pointer-events: none` on the dragged
|
||||||
|
block during the drag, `elementFromPoint` would always hit the dragged block itself
|
||||||
|
rather than the panel underneath, so the cross-face drop would never have been
|
||||||
|
detected; pointer capture keeps delivering move/up events to it regardless.
|
||||||
|
|
||||||
## 0.2.1
|
## 0.2.1
|
||||||
|
|
||||||
- Rework PNG export: instead of rasterizing the SVG export via `<img>`/`<canvas>`
|
- Rework PNG export: instead of rasterizing the SVG export via `<img>`/`<canvas>`
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ Wenn die Planung steht, wird das Konzept per Knopfdruck in echte NetBox-Objekte
|
|||||||
|
|
||||||
Dazu:
|
Dazu:
|
||||||
|
|
||||||
* **Elevation mit Drag & Drop** – Geräte per Maus verschieben, serverseitig validiert.
|
* **Elevation mit Drag & Drop** – Geräte per Maus verschieben, auch von der
|
||||||
Ein ungültiger Zug wird abgelehnt und ändert nichts.
|
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
|
* **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
|
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
|
konturierte Schrift (kein dunkler Balken). Ein Dropdown über der Elevation schaltet
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
|
|
||||||
__version__ = '0.2.1'
|
__version__ = '0.2.2'
|
||||||
|
|
||||||
|
|
||||||
class RackConceptConfig(PluginConfig):
|
class RackConceptConfig(PluginConfig):
|
||||||
|
|||||||
Binary file not shown.
@@ -340,9 +340,6 @@ msgstr "Geräte"
|
|||||||
msgid "Dimensions"
|
msgid "Dimensions"
|
||||||
msgstr "Abmessungen"
|
msgstr "Abmessungen"
|
||||||
|
|
||||||
msgid "Drag devices to move them"
|
|
||||||
msgstr "Geräte zum Verschieben ziehen"
|
|
||||||
|
|
||||||
msgid "Draw.io (.drawio)"
|
msgid "Draw.io (.drawio)"
|
||||||
msgstr "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."
|
msgid_plural "{{ counter }} planned devices will be created per rack."
|
||||||
msgstr[0] "Pro Rack wird {{ counter }} geplantes Gerät angelegt."
|
msgstr[0] "Pro Rack wird {{ counter }} geplantes Gerät angelegt."
|
||||||
msgstr[1] "Pro Rack werden {{ counter }} geplante Geräte 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"
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: 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 {
|
.concept-elevation .unit-slot {
|
||||||
|
|||||||
@@ -113,10 +113,18 @@
|
|||||||
return Math.min(Math.max(value, min), max);
|
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) {
|
container.querySelectorAll('.concept-elevation').forEach(function (panel) {
|
||||||
const svg = panel.querySelector('svg');
|
const svg = panel.querySelector('svg');
|
||||||
const geo = geometry(svg);
|
const geo = geometry(svg);
|
||||||
if (!geo) return;
|
if (!geo) return;
|
||||||
|
const sourceFace = panel.dataset.face;
|
||||||
|
|
||||||
panel.querySelectorAll('.device-block:not(.opposite)').forEach(function (block) {
|
panel.querySelectorAll('.device-block:not(.opposite)').forEach(function (block) {
|
||||||
block.addEventListener('pointerdown', onPointerDown);
|
block.addEventListener('pointerdown', onPointerDown);
|
||||||
@@ -139,6 +147,11 @@
|
|||||||
let dy = 0;
|
let dy = 0;
|
||||||
|
|
||||||
block.classList.add('dragging');
|
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);
|
block.setPointerCapture(event.pointerId);
|
||||||
|
|
||||||
function onMove(moveEvent) {
|
function onMove(moveEvent) {
|
||||||
@@ -152,11 +165,12 @@
|
|||||||
block.setAttribute('transform', 'translate(' + dx + ' ' + snappedY + ')');
|
block.setAttribute('transform', 'translate(' + dx + ' ' + snappedY + ')');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUp() {
|
function onUp(upEvent) {
|
||||||
block.removeEventListener('pointermove', onMove);
|
block.removeEventListener('pointermove', onMove);
|
||||||
block.removeEventListener('pointerup', onUp);
|
block.removeEventListener('pointerup', onUp);
|
||||||
block.removeEventListener('pointercancel', onUp);
|
block.removeEventListener('pointercancel', onUp);
|
||||||
block.classList.remove('dragging');
|
block.classList.remove('dragging');
|
||||||
|
block.style.pointerEvents = '';
|
||||||
|
|
||||||
if (!moved) {
|
if (!moved) {
|
||||||
// Treat it as a click: follow the device link.
|
// Treat it as a click: follow the device link.
|
||||||
@@ -166,6 +180,14 @@
|
|||||||
return;
|
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 unitDelta = Math.round(dy / geo.unitHeight) * (descUnits ? 1 : -1);
|
||||||
const lowest = startingUnit;
|
const lowest = startingUnit;
|
||||||
const highest = startingUnit + rackHeight - deviceUnits;
|
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);
|
block.addEventListener('pointermove', onMove);
|
||||||
@@ -190,7 +213,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function submit(block, position, horizontalPosition) {
|
function submit(block, position, horizontalPosition, face) {
|
||||||
const move = {
|
const move = {
|
||||||
id: parseInt(block.dataset.deviceId, 10),
|
id: parseInt(block.dataset.deviceId, 10),
|
||||||
position: position
|
position: position
|
||||||
@@ -198,6 +221,9 @@
|
|||||||
if (horizontalPosition !== null) {
|
if (horizontalPosition !== null) {
|
||||||
move.horizontal_position = horizontalPosition;
|
move.horizontal_position = horizontalPosition;
|
||||||
}
|
}
|
||||||
|
if (face) {
|
||||||
|
move.face = face;
|
||||||
|
}
|
||||||
|
|
||||||
setStatus('Saving…', false);
|
setStatus('Saving…', false);
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||||
</select>
|
</select>
|
||||||
{% if perms.netbox_rack_concept.change_conceptdevice %}
|
{% if perms.netbox_rack_concept.change_conceptdevice %}
|
||||||
<span class="badge text-bg-secondary">{% trans "Drag devices to move them" %}</span>
|
<span class="badge text-bg-secondary">{% trans "Drag devices to move or flip them between front and rear" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-rack-concept"
|
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"
|
description = "Rack blueprints for NetBox: plan racks and their equipment without creating real racks or devices"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
Reference in New Issue
Block a user