Skip to content

Commit 5a29ea6

Browse files
authored
feat(dashboard): preserve terminal popup drops (#13025) (#13053)
1 parent a5c9b8f commit 5a29ea6

4 files changed

Lines changed: 170 additions & 5 deletions

File tree

src/dashboard/Container.mjs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,34 @@ class Container extends BaseContainer {
184184
// Reset the window dragging flag when any drag operation ends
185185
// This ensures proper cleanup even if the drag ends outside the viewport
186186
this.#isWindowDragging = false;
187-
187+
188188
// Fire the event for other listeners
189189
this.fire('dragEnd', data);
190190
}
191191

192+
/**
193+
* Marks a detached widget as intentionally left in its popup after a terminal window drop.
194+
* @param {Object} data
195+
* @param {Neo.component.Base} data.draggedItem
196+
* @param {Neo.draggable.dashboard.SortZone} data.sortZone
197+
*/
198+
onWindowDragTerminalDrop(data) {
199+
let me = this,
200+
{draggedItem} = data,
201+
widgetName = draggedItem?.reference || draggedItem?.id,
202+
detachedItem = widgetName ? me.detachedItems.get(widgetName) : null;
203+
204+
if (detachedItem) {
205+
detachedItem.terminalDrop = true
206+
}
207+
208+
me.fire('windowDragTerminalDrop', {
209+
detachedItem,
210+
draggedItem,
211+
sortZone: data.sortZone
212+
})
213+
}
214+
192215
/**
193216
* @param {Object} data
194217
*/

src/draggable/dashboard/SortZone.mjs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ class DashboardSortZone extends SortZone {
264264
let me = this;
265265

266266
if (!me.isRemoteDragging) {
267-
// Signal Coordinator about end of drag
268267
DragCoordinator.onDragEnd({
269268
draggedItem : me.dragComponent,
270269
sourceSortZone: me
@@ -274,6 +273,17 @@ class DashboardSortZone extends SortZone {
274273
await super.processDragEnd(data)
275274
}
276275

276+
/**
277+
* Finalizes a window drag released outside every registered dashboard target.
278+
* @param {Neo.component.Base} draggedItem
279+
*/
280+
onTerminalWindowDrop(draggedItem) {
281+
this.owner.onWindowDragTerminalDrop?.({
282+
draggedItem,
283+
sortZone: this
284+
})
285+
}
286+
277287
/**
278288
* Handles the drag move event. This is the core logic loop for the drag operation.
279289
*

src/manager/DragCoordinator.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ class DragCoordinator extends Manager {
115115
}
116116

117117
/**
118+
* Finalizes a dashboard window drag by either dropping into the active target
119+
* or leaving the source popup as the terminal drop state.
118120
* @param {Object} data
121+
* @param {Neo.component.Base} data.draggedItem
119122
* @param {Neo.draggable.container.SortZone} data.sourceSortZone
120123
*/
121124
onDragEnd(data) {
@@ -129,8 +132,8 @@ class DragCoordinator extends Manager {
129132
data.sourceSortZone.onRemoteDropOut(data.draggedItem);
130133

131134
me.activeTargetZone = null
132-
} else {
133-
// Drag ended in void or source window (handled locally by source)
135+
} else if (data.sourceSortZone.isWindowDragging) {
136+
data.sourceSortZone.onTerminalWindowDrop?.(data.draggedItem)
134137
}
135138
}
136139

test/playwright/unit/draggable/dashboard/SortZone.spec.mjs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@ import InstanceManager from '../../../../../src/manager/Instance.mjs';
1717
* @summary Tests for Neo.draggable.dashboard.SortZone directional thresholds
1818
*/
1919
test.describe.serial('Neo.draggable.dashboard.SortZone Directional Logic', () => {
20-
let DashboardSortZone, Rectangle, sortZone;
20+
let DashboardContainer, DashboardSortZone, Rectangle, realDragCoordinatorOnDragEnd, sortZone;
2121

2222
test.beforeAll(async () => {
23+
const containerModule = await import('../../../../../src/dashboard/Container.mjs');
24+
DashboardContainer = containerModule.default;
25+
2326
const sortZoneModule = await import('../../../../../src/draggable/dashboard/SortZone.mjs');
2427
DashboardSortZone = sortZoneModule.default;
2528

2629
const rectModule = await import('../../../../../src/util/Rectangle.mjs');
2730
Rectangle = rectModule.default;
31+
32+
realDragCoordinatorOnDragEnd = Object.getPrototypeOf(Neo.manager.DragCoordinator).onDragEnd;
2833
});
2934

3035
test.beforeEach(() => {
@@ -188,6 +193,130 @@ test.describe.serial('Neo.draggable.dashboard.SortZone Directional Logic', () =>
188193
expect(sortZone.isWindowDragging).toBe(false)
189194
});
190195

196+
test('keeps a terminal popup drop detached instead of running remote-drop cleanup (#13025)', async () => {
197+
const
198+
appliedDeltas = [],
199+
terminalDrops = [],
200+
DragCoordinator = Neo.manager.DragCoordinator,
201+
item = {
202+
id : 'item1',
203+
reference : 'item1',
204+
vdom : {cls: ['neo-draggable']},
205+
wrapperStyle: {}
206+
},
207+
placeholder = {
208+
id : 'placeholder',
209+
vdom : {cls: []},
210+
wrapperStyle: {},
211+
destroy : () => {}
212+
},
213+
detachedItems = new Map([['item1', {
214+
index : 0,
215+
widget : item,
216+
windowId: 'popup-item1'
217+
}]]),
218+
mockOwner = {
219+
id : 'mockOwner',
220+
cls : [],
221+
detachedItems,
222+
dragResortable : true,
223+
items : [item, placeholder],
224+
style : {},
225+
vdom : {},
226+
addDomListeners : () => {},
227+
getDomRect : () => Promise.resolve([{x:0, y:0, width:200, height:100}]),
228+
getVdomItemsRoot : () => ({id: 'mockOwner-items'}),
229+
on : () => {},
230+
onWindowDragTerminalDrop: data => terminalDrops.push(data)
231+
};
232+
233+
Neo.applyDeltas = (windowId, deltas) => {
234+
appliedDeltas.push(...deltas);
235+
return Promise.resolve()
236+
};
237+
238+
DragCoordinator.activeTargetZone = null;
239+
DragCoordinator.onDragEnd = data => realDragCoordinatorOnDragEnd.call(DragCoordinator, data);
240+
241+
sortZone = Neo.create(DashboardSortZone, {
242+
owner : mockOwner,
243+
timeout: () => Promise.resolve()
244+
});
245+
246+
Object.assign(sortZone, {
247+
currentIndex : 1,
248+
dragComponent : item,
249+
dragPlaceholder : placeholder,
250+
dragProxy : {id: 'proxy', cls: [], destroy: () => {}},
251+
isWindowDragging: true,
252+
itemRects : [
253+
{height: 100, left: 0, top: 0, width: 100},
254+
{height: 100, left: 100, top: 0, width: 100}
255+
],
256+
itemStyles: [
257+
{height: '100px', width: '100px'},
258+
{height: '100px', width: '100px'}
259+
],
260+
ownerStyle : {},
261+
sortableItems: [item, placeholder],
262+
startIndex : 0,
263+
windowId : 1
264+
});
265+
266+
await sortZone.processDragEnd({type: 'drag:end'});
267+
268+
expect(terminalDrops).toHaveLength(1);
269+
expect(terminalDrops[0].draggedItem).toBe(item);
270+
expect(terminalDrops[0].sortZone).toBe(sortZone);
271+
expect(detachedItems.has('item1')).toBe(true);
272+
expect(appliedDeltas.some(delta => delta.action === 'moveNode' && delta.id === item.id)).toBe(false);
273+
expect(appliedDeltas.some(delta => delta.action === 'removeNode' && delta.id === placeholder.id)).toBe(true);
274+
expect(sortZone.isWindowDragging).toBe(false)
275+
});
276+
277+
test('does not classify an ordinary source-window drag end as a terminal popup drop (#13025)', () => {
278+
const
279+
DragCoordinator = Neo.manager.DragCoordinator,
280+
sourceSortZone = {
281+
isWindowDragging : false,
282+
onTerminalWindowDrop: () => {
283+
throw new Error('ordinary drag-end should not terminal-drop')
284+
}
285+
};
286+
287+
DragCoordinator.activeTargetZone = null;
288+
289+
realDragCoordinatorOnDragEnd.call(DragCoordinator, {
290+
draggedItem: {id: 'item1'},
291+
sourceSortZone
292+
})
293+
});
294+
295+
test('marks dashboard detached items as terminal popup drops (#13025)', () => {
296+
const
297+
item = {id: 'item1', reference: 'item1'},
298+
detachedItem = {index: 0, widget: item, windowId: 'popup-item1'},
299+
detachedItems = new Map([['item1', detachedItem]]),
300+
events = [],
301+
container = Object.create(DashboardContainer.prototype);
302+
303+
Object.assign(container, {
304+
detachedItems,
305+
fire: (event, data) => events.push({data, event})
306+
});
307+
308+
container.onWindowDragTerminalDrop({
309+
draggedItem: item,
310+
sortZone : {id: 'source-zone'}
311+
});
312+
313+
expect(detachedItems.get('item1')).toBe(detachedItem);
314+
expect(detachedItem.terminalDrop).toBe(true);
315+
expect(events).toHaveLength(1);
316+
expect(events[0].event).toBe('windowDragTerminalDrop');
317+
expect(events[0].data.detachedItem).toBe(detachedItem)
318+
});
319+
191320
test('latches dashboard drag-end coordinator notification (#12895)', async () => {
192321
const
193322
dragEndCalls = [],

0 commit comments

Comments
 (0)