v0.2.3: visible cross-panel drag ghost, front-image fallback, shorter hint
- Fix invisible device while dragging from front to rear: the block being dragged stayed structurally inside its own source <svg>, so it rendered underneath the other panel once it visually overlapped it. Replaced the in-SVG transform with a free-floating HTML ghost fixed to <body> that tracks the pointer, unaffected by either SVG's own stacking - the original block just dims in place during the drag, as before. - Fix rear elevation showing nothing for a device type with no rear_image: only full-depth devices (which have a genuinely different back) are restricted to their own rear image now; a shallow device (e.g. a PDU) falls back to its front image on the rear view, since that's usually the only picture that exists and is still recognisable either way. - Shorten the drag-hint badge text; the German translation of the previous, longer wording didn't fit well next to the other controls above the elevation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<svg>`, 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 `<body>` 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
__version__ = '0.2.2'
|
||||
__version__ = '0.2.3'
|
||||
|
||||
|
||||
class RackConceptConfig(PluginConfig):
|
||||
|
||||
@@ -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:
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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 <body>, not
|
||||
scoped to either elevation's <svg>, 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;
|
||||
}
|
||||
|
||||
@@ -120,6 +120,31 @@
|
||||
return el ? el.closest('.concept-elevation') : null;
|
||||
}
|
||||
|
||||
/* A free-floating copy of the device block, fixed to <body> 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 <svg> - it would render underneath the
|
||||
// other panel's own markup once it overlapped it, effectively
|
||||
// vanishing. A free-floating HTML ghost fixed to <body> 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||
</select>
|
||||
{% if perms.netbox_rack_concept.change_conceptdevice %}
|
||||
<span class="badge text-bg-secondary">{% trans "Drag devices to move or flip them between front and rear" %}</span>
|
||||
<span class="badge text-bg-secondary">{% trans "Drag to move or flip sides" %}</span>
|
||||
{% endif %}
|
||||
<div class="dropdown">
|
||||
<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]
|
||||
name = "netbox-rack-concept"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
description = "Rack blueprints for NetBox: plan racks and their equipment without creating real racks or devices"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
Reference in New Issue
Block a user