fix(plant-overview): real drop insertion indicator + small logo back
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) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Shop Floor',
|
'name': 'Fusion Plating — Shop Floor',
|
||||||
'version': '19.0.1.0.0',
|
'version': '19.0.2.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||||
'first-piece inspection gates.',
|
'first-piece inspection gates.',
|
||||||
|
|||||||
@@ -95,12 +95,32 @@ export class PlantOverview extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----- Drag & drop --------------------------------------------------------
|
// ----- 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) {
|
onCardDragStart(card, col, ev) {
|
||||||
this._draggedCard = {
|
this._draggedCard = {
|
||||||
id: card.id,
|
id: card.id,
|
||||||
source_model: card.source_model || "mrp.workorder",
|
source_model: card.source_model || "mrp.workorder",
|
||||||
source_wc_id: col.work_center_id,
|
source_wc_id: col.work_center_id,
|
||||||
|
el: ev.target,
|
||||||
};
|
};
|
||||||
ev.dataTransfer.effectAllowed = "move";
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
ev.dataTransfer.setData("text/plain", String(card.id));
|
ev.dataTransfer.setData("text/plain", String(card.id));
|
||||||
@@ -113,31 +133,55 @@ export class PlantOverview extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onCardDragEnd(ev) {
|
onCardDragEnd(ev) {
|
||||||
this._draggedCard = null;
|
|
||||||
if (ev.target && ev.target.classList) {
|
if (ev.target && ev.target.classList) {
|
||||||
ev.target.classList.remove("o_fp_dragging");
|
ev.target.classList.remove("o_fp_dragging");
|
||||||
}
|
}
|
||||||
// Remove any lingering drop-target highlights
|
|
||||||
document.querySelectorAll(".o_fp_drop_target").forEach((el) => {
|
document.querySelectorAll(".o_fp_drop_target").forEach((el) => {
|
||||||
el.classList.remove("o_fp_drop_target");
|
el.classList.remove("o_fp_drop_target");
|
||||||
});
|
});
|
||||||
|
this._removePlaceholder();
|
||||||
|
this._draggedCard = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onColDragOver(col, ev) {
|
onColDragOver(col, ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.dataTransfer.dropEffect = "move";
|
ev.dataTransfer.dropEffect = "move";
|
||||||
const body = ev.currentTarget;
|
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");
|
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) {
|
onColDragLeave(col, ev) {
|
||||||
// Only remove highlight if we actually left the column body
|
|
||||||
// (not just hovering over a child element)
|
|
||||||
const body = ev.currentTarget;
|
const body = ev.currentTarget;
|
||||||
if (body && !body.contains(ev.relatedTarget)) {
|
if (body && !body.contains(ev.relatedTarget)) {
|
||||||
body.classList.remove("o_fp_drop_target");
|
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) {
|
if (body) {
|
||||||
body.classList.remove("o_fp_drop_target");
|
body.classList.remove("o_fp_drop_target");
|
||||||
}
|
}
|
||||||
|
this._removePlaceholder();
|
||||||
|
|
||||||
const dragged = this._draggedCard;
|
const dragged = this._draggedCard;
|
||||||
if (!dragged) {
|
if (!dragged) {
|
||||||
|
|||||||
@@ -173,28 +173,35 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: $fp-space-3;
|
padding: $fp-space-3;
|
||||||
flex: 1;
|
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
|
// Drop-zone highlight: soft accent tint + inset outline. Shows the
|
||||||
// so the manager sees EXACTLY where the card will land.
|
// whole column is receptive. The actual insertion point is drawn
|
||||||
|
// by the real placeholder node (inserted by JS) between cards.
|
||||||
&.o_fp_drop_target {
|
&.o_fp_drop_target {
|
||||||
background-color: color-mix(in srgb, #{$fp-accent} 8%, transparent);
|
background-color: color-mix(in srgb, #{$fp-accent} 6%, transparent);
|
||||||
box-shadow: inset 0 0 0 2px color-mix(in srgb, #{$fp-accent} 45%, transparent);
|
box-shadow: inset 0 0 0 2px color-mix(in srgb, #{$fp-accent} 40%, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
.o_fp_po_no_cards {
|
||||||
padding: $fp-space-5 $fp-space-3;
|
padding: $fp-space-5 $fp-space-3;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -256,6 +263,21 @@
|
|||||||
display: flex; align-items: center; gap: $fp-space-2;
|
display: flex; align-items: center; gap: $fp-space-2;
|
||||||
margin-bottom: $fp-space-1;
|
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 {
|
.o_fp_po_card_title {
|
||||||
flex: 1; min-width: 0;
|
flex: 1; min-width: 0;
|
||||||
font-size: $fp-text-base;
|
font-size: $fp-text-base;
|
||||||
|
|||||||
@@ -92,9 +92,16 @@
|
|||||||
t-on-dragend="(ev) => this.onCardDragEnd(ev)"
|
t-on-dragend="(ev) => this.onCardDragEnd(ev)"
|
||||||
t-on-click="() => this.onCardClick(card)">
|
t-on-click="() => this.onCardClick(card)">
|
||||||
|
|
||||||
<!-- Top row: customer name + step badge
|
<!-- Top row: small customer avatar + name + step pill -->
|
||||||
(logo removed — it was too dominant) -->
|
|
||||||
<div class="o_fp_po_card_top">
|
<div class="o_fp_po_card_top">
|
||||||
|
<img t-if="card.customer_logo_url"
|
||||||
|
t-att-src="card.customer_logo_url"
|
||||||
|
class="o_fp_po_card_avatar"
|
||||||
|
alt=""/>
|
||||||
|
<div class="o_fp_po_card_avatar o_fp_po_card_avatar_blank"
|
||||||
|
t-else="">
|
||||||
|
<i class="fa fa-building"/>
|
||||||
|
</div>
|
||||||
<div class="o_fp_po_card_title">
|
<div class="o_fp_po_card_title">
|
||||||
<strong t-esc="card.customer_name || 'Walk-In'"/>
|
<strong t-esc="card.customer_name || 'Walk-In'"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user