From 1793bf13e2eba6559115af333bad97047680dbbd Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 16 Sep 2026 14:01:42 +0200 Subject: [PATCH] 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 , 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 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 --- CHANGELOG.md | 16 +++++++ README.md | 9 ++-- netbox_rack_concept/__init__.py | 2 +- netbox_rack_concept/elevation.py | 14 ++++++- .../locale/de/LC_MESSAGES/django.mo | Bin 21286 -> 21203 bytes .../locale/de/LC_MESSAGES/django.po | 6 +-- .../static/netbox_rack_concept/elevation.css | 26 +++++++++--- .../static/netbox_rack_concept/elevation.js | 39 +++++++++++++++--- .../netbox_rack_concept/rackconcept.html | 2 +- pyproject.toml | 2 +- 10 files changed, 94 insertions(+), 22 deletions(-) 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 7e2ce92b03095868a732318699a30a74488dc2fd..d9cb44d13e4b93b3b5b8e7762ebcc1e76463c7e7 100644 GIT binary patch delta 3666 zcmXZd3rv<(9LMp4H{cboh=AN)l~(};L`+0Qyg`s41RG2)DcP%_0xBxuve%rKvY4~H zU`sdWg67nmn1Z?4noV6^a)fg;T}@|8XQoY;o5Q|8pR=*|bIy65bNThai&m8kv? z(S?4|#spwcwDG+zgwQaIhG-1MRMZ0Ip*j>{FjnFWT!p>xIBMbTsD=H6>USO0?~zLAbHjj>mt;GmtkLQLXGP~7xSCV6tvI%K`cpLAJ= zpau>@ZD|xLlj)d>Rk#Cpqqfq+Lx1KslPM^Y88{vbu@g6-B2FV6{V)ra@;r27g>?h^ zQ$K)uuN9Taqp1F!s6Z~D=J^#<@ILy^QwWPUrWkMINlf9W6QsG1T2Rn98woC@UWJnC z<+i;B)vv*}zmH1&R@>f;iu7}=!xrS`8UJJssviO31nP@@{FYGIg=P2PIXb{>rO>hIbx#l07i(ylo{*|aRuo??-HxAVO zzfVCKc!o-?KO4(u-GpKmrXxu)KI9ei9ddrm-`K=qie`V*zlHoNfWxT0?LYEFOhp<+L6OE`1g0V9&=jLmz8TeVr>(c4GI0`<@O#wld0_n)m6?~Q@dGlP zz=Ba*G#d4MTn71%p^!#{A}B?zxCS*r1L`cSM=f9@YQR0HLv;|9%HL4~rqC!eP?^fb zJ-7+gKb$L}=gFvfre%_UtvrhcMLr)RaT#jjZKw&GQ7LP|96XA>@HvL#3)FkTGo6J; zqv{^iR!*=M;XLYVP~-gIqYy?RaF%n=N1-MzLG57$>VDTFH_x0!eR!Ur7XBP{Mqc3v z%$n^?ya=^5%TZ_QON_$9sB3-^^}g@1ZE($TKD{ZZOjM#$)r6XG3u=qDqYuAAvT7nZ z;+miw2jg;E{{Z#gXQ(q2#41UW3CCPqfGK+A5Csj~jk-2}S_fr2--9$%h8E%L1)(O| zV%=kHvv!~k*(KB-Uq@x?7V37~Lnby)@%8`z*f*V&C8GwOiitQ6`(P6)f)7yxd}{0a zPz!HGeP}vR6JAE`{SDN_f1vt3z`ppOecmILqCif9It)X-5M%2e)Wj1|DbBzO%(v}_ z(VzNh>p9edFQ6MQqX!?M&Q36SKaDx4@yuJ~Ujq!LpoTEy|AL9b->?F;fOL**Kg>p* zjeJywYA^x!q7LhisPVc{{cfYaD-TfPzd+rhz&vLQqVw4QP8tenC_z^~gJ2cv^v2FN zW+E2jINXYjcmfAvLV3~0Dj(bSm$uzi#1krX#5t9CvI_l?aKN(Z{3>q zfFUXUL)O&0YwOo}-Sw;8OIFmZbT`zz=WS?T5&4%prmV(W?X7dKUt8-g@vd%ITwUXR a*ISod>Rnvj;9XJo#fr$-_A40+LjDK9xR8hd delta 3738 zcmXZe2~5{z9LMp81O-t<$ytK(^ME)M4-id7^WZR1LPIu#OyH-G@c%1^pk)6_JIo8S zgQgZ)(j6{L*CJb`)r>CbAlEfpbCeyXb2D}8{o(g)?EiY6=l7i7=g_W$L9MMphsO2^ zOf$x$#TYXft1t`$*cZ2AFgD>(d;-JqRZPR9n1kmq1gDJfzR$o^^4Yip8&U1UMjF!} zV=)wyMw&oxB9(&cDVT}FFdsF63RHu&7=?{E3wPml_&I9gU!f-UE2`b!sCJi-u}x5{ zF(gea-i)&_9UEc;UI%Ry#8L1*Y9(h;9sY^a@iHc1n&U-e1!@Ils1B=86Mqp0;Cq;e zov8jUp%!ozhhyX@V}@XIfJ8KjS;#lD+qFLEsK_)Uf94=Rj2kc~NoWOMqe6ZL6Ywt##$j<@!x&V(M4O+A+WYya z&@MwQy$Zx1|{>4ccoXDh(lSn7A6tCc6%;TuDNE4jwO(+SOiz&p_ z7{G8mi=lV{weqVt6@ysC9K0EGun|@NI4Y7~qHe*rwdsz#P<`)uL9g z+2(hnR?vdFK5tr2pw2)ys@*wMzgJM94o~qW5QDlM38?z1Da2nh&!#{t$;AO!fI0&u zHeZPY$#1ab52M=cw&hQvLjQs-e-*XTL+HaJ$jvix6O5S_!~$_5`TGO>E+g?KF2^u7 zrVz_;33i|k&!EZPi!~4P$ZteFJSQ<1gQ=vI=Hn17!P!`c>h~?w7DV!H>!FH4)eEGN z;Q2P$7>{MBm2a~?fqFQe#YK1;721R;UZkd>R+?#Dh#Dv#)lV_b!qun;s}&>g4Bp82 z<~#}QX*dru2h)r|b(n=wn2Xx8yHH!R#@c}D=t0zgkE3oui!FZ%)&8jc{wXq``3_w? zk72t1g&bMUbR}w!*PsU6fEr*6UXOcFd;AhAVr{5_J5l$&8#Ta1)NP62pVs-9j%weC zIs;938@6MF?td^FsR%@&LK}xX?`8^SVj&VFvma?=x{&i@2Cy$1I7~B9?T-*&E#MQ> z-hPW(SPyE#7jP&>&h#b}j{&W81_`Y+3!||BIge%?D&z-H4clz~1S%4zFa^Iy-JZ}4 z&%vn33`g}Jk6PFy)E3RL-xp*M|Jx}jpg=2l1vTRi)Bqo$&cbJ?33Q=4>_Hu>i>Oeh zlF@)$tWBs$?ZYSW8&vyBu7v76fEs7#Y~rt(H&dXMKZh~+E^6S@r~!XL?cpDogXR|R zKOhV6MzXo6daFJiX7gX5>ivv5Lo1ji!84V(0AIsNs&a{hI!?&)uFZ67 z2_{hfC@Mnj*gGNAK;70J>wnhpZ10f8p!RqyDpDz^+c6ax*koewzyI~U2@VCS;~h92 zpTS`4M6KW}R0luV{5jObFQXosa5hu}#-g?$5jAics$B+#;6l{*e5nu>*@D%!Lbc5| zpa$NI3ULz_VT&!lhW*I*C!TT$YQn?O!B~vLTTo|b6&}UCsQ#ATO8nKZl!OY_pblXp z{(x<$32f)c_Qj`BXQKrbp$<&K-%*EkXs$Pr1XR0;sCOj;)qgJP78RkkU|lZz|2B!| zDOiTZi#X|c7yN64Qgfgp(63H&2K|L`Nwb! zUbmE=xP(%F)mp#L_0_hA4xSXATIQ~=D0OR`TEA1}U++5pYG+kt#agGf+^up-+}e8A z^*O7m{k~dfrLWAXc2`!nFOKPP7G=BDAHGoQIvXq8a@RM`sjG5oD#+G3>)q;_((($o ogqHp?x7yiQUqJ!6tloa==x51?9i?k(+zNG2=hV07Wo#MxKP>XN5C8xG 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 %}