diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ebdb48..8cd98e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.2.3 + +- Fix: dragging a device from front to rear (or back) made it invisible mid-drag - it + stayed structurally inside its own source ``, so it rendered underneath the + other panel once it overlapped it instead of over it. Replaced the in-SVG transform + with a free-floating HTML "ghost" fixed to `` that tracks the pointer; the + original block just dims in place while dragging, exactly like before. +- Fix: the rear elevation only ever showed a device type's `rear_image`, leaving + shallow devices without one (e.g. PDUs) blank on the rear view even when a perfectly + usable `front_image` exists. Full-depth devices are unaffected (their rear view still + only shows their own rear image, or nothing - showing the front picture there would + be wrong); only non-full-depth devices now fall back to the front image on the rear + view. +- Shorten the drag-hint badge text above the elevation (the German translation in + particular had gotten too long for the badge). + ## 0.2.2 - Drag-and-drop now supports dragging a device from the front elevation onto the rear diff --git a/README.md b/README.md index 5b5131d..87ec17d 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,12 @@ Dazu: ä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 - zwischen „Images and labels", „Images only" und „Labels only" um — genau wie bei - echten Racks in NetBox, inklusive Merken der Wahl im Browser. + konturierte Schrift (kein dunkler Balken). Auf der Rear-Seite zeigt ein **volltiefes** + Gerät nur sein eigenes Rear-Bild (oder gar keins); ein **nicht volltiefes** Gerät ohne + eigenes Rear-Bild (z.B. eine Steckdosenleiste) zeigt dort stattdessen sein Front-Bild. + Ein Dropdown über der Elevation schaltet zwischen „Images and labels", „Images only" + und „Labels only" um — genau wie bei echten Racks in NetBox, inklusive Merken der + Wahl im Browser. * **Teilbreiten** (1/1, 1/2, 1/3, 1/4 plus horizontale Slot-Position), passend zum Teilbreiten-Feature von `netbox_utilities`. * **Mandant und Mandantengruppe** an Konzept und an jedem geplanten Gerät. diff --git a/netbox_rack_concept/__init__.py b/netbox_rack_concept/__init__.py index f2f5a9c..af2e599 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.2' +__version__ = '0.2.3' class RackConceptConfig(PluginConfig): diff --git a/netbox_rack_concept/elevation.py b/netbox_rack_concept/elevation.py index e28d53e..182f819 100644 --- a/netbox_rack_concept/elevation.py +++ b/netbox_rack_concept/elevation.py @@ -95,12 +95,22 @@ class ElevationBlock: The device type's image for the face this block is drawn on, i.e. what's physically visible from that side of the rack - not the device's own mounting face, which only matters for half-depth devices occupying one face at a time. + + A full-depth device genuinely has two different sides, so its rear view only + ever shows its own rear image (or nothing, if none is set) - showing the front + picture there would misrepresent what's actually visible from the back. A + shallow device (e.g. a PDU) has no real "back" of its own; for those, the rear + view falls back to the front image, since that's usually the only picture that + exists and is still perfectly recognisable from either side. """ device_type = self.device.device_type if device_type is None: return None - image = device_type.front_image if self.face == DeviceFaceChoices.FACE_FRONT else device_type.rear_image - return image if image else None + if self.face == DeviceFaceChoices.FACE_FRONT: + return device_type.front_image or None + if self.device.effective_full_depth: + return device_type.rear_image or None + return device_type.rear_image or device_type.front_image or None class Elevation: diff --git a/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo b/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo index 7e2ce92..d9cb44d 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 2a52571..2a1f564 100644 --- a/netbox_rack_concept/locale/de/LC_MESSAGES/django.po +++ b/netbox_rack_concept/locale/de/LC_MESSAGES/django.po @@ -924,7 +924,5 @@ 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" +msgid "Drag to move or flip sides" +msgstr "Ziehen zum Verschieben/Wechseln" diff --git a/netbox_rack_concept/static/netbox_rack_concept/elevation.css b/netbox_rack_concept/static/netbox_rack_concept/elevation.css index 0461105..660ace1 100644 --- a/netbox_rack_concept/static/netbox_rack_concept/elevation.css +++ b/netbox_rack_concept/static/netbox_rack_concept/elevation.css @@ -10,10 +10,6 @@ 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 { @@ -108,6 +104,26 @@ } .concept-elevation .device-block.dragging { - opacity: 0.6; + opacity: 0.3; + cursor: grabbing; +} + +/* Follows the pointer during a drag - a plain HTML element fixed to , not + scoped to either elevation's , so it stays visible the whole way from the + front panel to the rear one and back. */ +.concept-drag-ghost { + position: fixed; + z-index: 2000; + pointer-events: none; + display: flex; + align-items: center; + border-radius: 2px; + padding-left: 6px; + font-size: 11px; + color: #ffffff; + text-shadow: 0 0 2px #000000, 0 0 2px #000000, 0 0 2px #000000; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); + white-space: nowrap; + overflow: hidden; cursor: grabbing; } diff --git a/netbox_rack_concept/static/netbox_rack_concept/elevation.js b/netbox_rack_concept/static/netbox_rack_concept/elevation.js index 1e991db..cbff73f 100644 --- a/netbox_rack_concept/static/netbox_rack_concept/elevation.js +++ b/netbox_rack_concept/static/netbox_rack_concept/elevation.js @@ -120,6 +120,31 @@ return el ? el.closest('.concept-elevation') : null; } + /* A free-floating copy of the device block, fixed to and tracking the + pointer - not scoped to either SVG, so it stays visible while crossing from one + panel to the other. */ + function createGhost(block, rectEl, clientX, clientY) { + const screenRect = rectEl.getBoundingClientRect(); + const label = block.querySelector('.device-label'); + + const ghost = document.createElement('div'); + ghost.className = 'concept-drag-ghost'; + ghost.style.width = screenRect.width + 'px'; + ghost.style.height = screenRect.height + 'px'; + ghost.style.backgroundColor = rectEl.getAttribute('fill') || '#c0c0c0'; + ghost.textContent = label ? label.textContent.trim() : ''; + ghost.dataset.offsetX = clientX - screenRect.left; + ghost.dataset.offsetY = clientY - screenRect.top; + document.body.appendChild(ghost); + moveGhost(ghost, clientX, clientY); + return ghost; + } + + function moveGhost(ghost, clientX, clientY) { + ghost.style.left = (clientX - ghost.dataset.offsetX) + 'px'; + ghost.style.top = (clientY - ghost.dataset.offsetY) + 'px'; + } + container.querySelectorAll('.concept-elevation').forEach(function (panel) { const svg = panel.querySelector('svg'); const geo = geometry(svg); @@ -154,6 +179,13 @@ block.style.pointerEvents = 'none'; block.setPointerCapture(event.pointerId); + // A device dragged toward the other panel is still, structurally, + // content of its own source - it would render underneath the + // other panel's own markup once it overlapped it, effectively + // vanishing. A free-floating HTML ghost fixed to sidesteps that + // entirely: it's not inside either SVG, so it stays visible over both. + const ghost = createGhost(block, rect, event.clientX, event.clientY); + function onMove(moveEvent) { const now = svgPoint(svg, moveEvent); dx = now.x - start.x; @@ -161,8 +193,7 @@ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { moved = true; } - const snappedY = Math.round(dy / geo.unitHeight) * geo.unitHeight; - block.setAttribute('transform', 'translate(' + dx + ' ' + snappedY + ')'); + moveGhost(ghost, moveEvent.clientX, moveEvent.clientY); } function onUp(upEvent) { @@ -171,10 +202,10 @@ block.removeEventListener('pointercancel', onUp); block.classList.remove('dragging'); block.style.pointerEvents = ''; + ghost.remove(); if (!moved) { // Treat it as a click: follow the device link. - block.removeAttribute('transform'); const link = block.querySelector('a'); if (link) window.location = link.getAttribute('href'); return; @@ -245,12 +276,10 @@ if (result.ok) { window.location.reload(); } else { - block.removeAttribute('transform'); setStatus(result.body.error || 'The move was rejected.', true); } }) .catch(function () { - block.removeAttribute('transform'); setStatus('The move could not be saved.', true); }); } diff --git a/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html b/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html index 028aaf8..a8c83cf 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 @@ {% if perms.netbox_rack_concept.change_conceptdevice %} - {% trans "Drag devices to move or flip them between front and rear" %} + {% trans "Drag to move or flip sides" %} {% endif %}