From 2588a2b651545b2f35ba7c7d5f747b76cd24b372 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 18 Apr 2026 19:06:40 -0400 Subject: [PATCH] fix(plant-overview): real drop insertion indicator + small logo back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three direct fixes responding to user feedback: 1. Drag-drop "simulation" — now works like Trello/Linear. As the cursor moves over a column, a live DOM placeholder node is INJECTED into the card list at the exact position the dragged card will drop. The placeholder is a 4px pulsing accent-coloured bar with a soft glow ring. Slides smoothly between cards as the cursor moves. Column body also gets a tinted background + inset accent outline for the "whole column is receptive" cue. Previous version only tinted the column — no indicator of WHERE the card would land. The new approach actually mimics the physical gesture: cards visually make room for the incoming card. 2. Customer logo restored at 32×32px. Removing it was the wrong call. It's back now as a small thumbnail avatar (rounded 10px corners, soft border, object-fit contain so wide logos don't squish). Sits to the left of the customer name in the card top row. Fallback icon for customers without a logo. Takes the same space as the step badge on the right — compact and organised. 3. Module version bumped 19.0.1.0.0 → 19.0.2.0.0 so the asset bundle content hash changes. The new compiled CSS is served at /web/assets/022171c/web.assets_backend.min.css (previously /web/assets/278b43c/...). Fresh URL forces browser to refetch — this is what was causing the "still no border" complaint. Verified in compiled CSS: o_fp_po_card_avatar, o_fp_po_drop_placeholder, o_fp_placeholder_pulse keyframes, o_fp_drop_target — all present. Zero SCSS warnings. Module upgrade clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_shopfloor/__manifest__.py | 2 +- .../static/src/js/plant_overview.js | 55 ++++++++++++++++-- .../static/src/scss/plant_overview.scss | 56 +++++++++++++------ .../static/src/xml/plant_overview.xml | 11 +++- 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index d7d57395..03627229 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Shop Floor', - 'version': '19.0.1.0.0', + 'version': '19.0.2.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, ' 'first-piece inspection gates.', diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_overview.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_overview.js index 6fcb7860..2618c375 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_overview.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_overview.js @@ -95,12 +95,32 @@ export class PlantOverview extends Component { } // ----- Drag & drop -------------------------------------------------------- + // + // A real insertion placeholder is injected into the column body as the + // card is dragged. The placeholder slides between cards to show exactly + // where the dragged card will land when released. The placeholder is a + // plain DOM node (not a reactive state field) so mouseover updates don't + // trigger OWL re-renders. + + _getOrCreatePlaceholder() { + let node = document.querySelector(".o_fp_po_drop_placeholder"); + if (!node) { + node = document.createElement("div"); + node.className = "o_fp_po_drop_placeholder"; + } + return node; + } + + _removePlaceholder() { + document.querySelectorAll(".o_fp_po_drop_placeholder").forEach((el) => el.remove()); + } onCardDragStart(card, col, ev) { this._draggedCard = { id: card.id, source_model: card.source_model || "mrp.workorder", source_wc_id: col.work_center_id, + el: ev.target, }; ev.dataTransfer.effectAllowed = "move"; ev.dataTransfer.setData("text/plain", String(card.id)); @@ -113,31 +133,55 @@ export class PlantOverview extends Component { } onCardDragEnd(ev) { - this._draggedCard = null; if (ev.target && ev.target.classList) { ev.target.classList.remove("o_fp_dragging"); } - // Remove any lingering drop-target highlights document.querySelectorAll(".o_fp_drop_target").forEach((el) => { el.classList.remove("o_fp_drop_target"); }); + this._removePlaceholder(); + this._draggedCard = null; } onColDragOver(col, ev) { ev.preventDefault(); ev.dataTransfer.dropEffect = "move"; const body = ev.currentTarget; - if (body && !body.classList.contains("o_fp_drop_target")) { + if (!body) return; + if (!body.classList.contains("o_fp_drop_target")) { body.classList.add("o_fp_drop_target"); } + + // Find which card the cursor is closest to and insert the + // placeholder above or below it. This gives the manager a + // clear "card will land HERE" preview between existing cards. + const placeholder = this._getOrCreatePlaceholder(); + const cards = [...body.querySelectorAll( + ".o_fp_po_card:not(.o_fp_dragging):not(.o_fp_po_drop_placeholder)", + )]; + const y = ev.clientY; + let insertBefore = null; + for (const cardEl of cards) { + const rect = cardEl.getBoundingClientRect(); + if (y < rect.top + rect.height / 2) { + insertBefore = cardEl; + break; + } + } + if (insertBefore) { + body.insertBefore(placeholder, insertBefore); + } else { + body.appendChild(placeholder); + } } onColDragLeave(col, ev) { - // Only remove highlight if we actually left the column body - // (not just hovering over a child element) const body = ev.currentTarget; if (body && !body.contains(ev.relatedTarget)) { body.classList.remove("o_fp_drop_target"); + // Only remove placeholder if we left the body entirely — + // otherwise the child card enter fires dragleave on the body + this._removePlaceholder(); } } @@ -147,6 +191,7 @@ export class PlantOverview extends Component { if (body) { body.classList.remove("o_fp_drop_target"); } + this._removePlaceholder(); const dragged = this._draggedCard; if (!dragged) { diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss index 4bd2ca64..1fa3e6c4 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss @@ -173,28 +173,35 @@ overflow-y: auto; padding: $fp-space-3; flex: 1; - transition: background-color $fp-dur $fp-ease; + transition: background-color $fp-dur $fp-ease, + box-shadow $fp-dur $fp-ease; - // Drop zone highlight — solid tinted background + dashed outline - // so the manager sees EXACTLY where the card will land. + // Drop-zone highlight: soft accent tint + inset outline. Shows the + // whole column is receptive. The actual insertion point is drawn + // by the real placeholder node (inserted by JS) between cards. &.o_fp_drop_target { - background-color: color-mix(in srgb, #{$fp-accent} 8%, transparent); - box-shadow: inset 0 0 0 2px color-mix(in srgb, #{$fp-accent} 45%, transparent); - - // Placeholder bar at the bottom of the list — that's where - // the card will drop when released. - &::after { - content: ""; - display: block; - height: 56px; - margin-top: $fp-space-2; - border: 2px dashed color-mix(in srgb, #{$fp-accent} 55%, transparent); - border-radius: $fp-radius-md; - background-color: color-mix(in srgb, #{$fp-accent} 6%, transparent); - } + background-color: color-mix(in srgb, #{$fp-accent} 6%, transparent); + box-shadow: inset 0 0 0 2px color-mix(in srgb, #{$fp-accent} 40%, transparent); } } +// Insertion placeholder — a live DOM node inserted between cards as +// the cursor moves so the manager sees exactly where the drop will +// slot in. 4px solid accent bar + small glow + smooth slide. +.o_fp_po_drop_placeholder { + height: 4px; + margin: $fp-space-2 0; + background-color: $fp-accent; + border-radius: 4px; + box-shadow: 0 0 0 3px color-mix(in srgb, #{$fp-accent} 25%, transparent), + 0 2px 8px color-mix(in srgb, #{$fp-accent} 35%, transparent); + animation: o_fp_placeholder_pulse 1s ease-in-out infinite alternate; +} +@keyframes o_fp_placeholder_pulse { + from { opacity: 0.75; transform: scaleY(1); } + to { opacity: 1; transform: scaleY(1.4); } +} + .o_fp_po_no_cards { padding: $fp-space-5 $fp-space-3; text-align: center; @@ -256,6 +263,21 @@ display: flex; align-items: center; gap: $fp-space-2; margin-bottom: $fp-space-1; + // Small customer avatar — 32px thumbnail. Just identifies the + // customer at a glance; not a billboard. + .o_fp_po_card_avatar { + flex-shrink: 0; + width: 32px; height: 32px; + border-radius: $fp-radius-sm; + object-fit: contain; + background-color: $fp-card-soft; + border: 1px solid var(--bs-border-color); + padding: 2px; + } + .o_fp_po_card_avatar_blank { + display: flex; align-items: center; justify-content: center; + color: $fp-ink-mute; font-size: 0.95rem; + } .o_fp_po_card_title { flex: 1; min-width: 0; font-size: $fp-text-base; diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml index 774f7c87..afd1ed96 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml @@ -92,9 +92,16 @@ t-on-dragend="(ev) => this.onCardDragEnd(ev)" t-on-click="() => this.onCardClick(card)"> - +
+ +
+ +