diff --git a/webview/src/plots/components/App.test.tsx b/webview/src/plots/components/App.test.tsx index 381a07a18f..8157ad06c6 100644 --- a/webview/src/plots/components/App.test.tsx +++ b/webview/src/plots/components/App.test.tsx @@ -784,6 +784,30 @@ describe('App', () => { ]) }) + it('should show a drop target at the end of the section when moving a plot from one section to another but not over any other plot', async () => { + renderAppWithOptionalData({ + template: complexTemplatePlotsFixture + }) + + const bottomSection = screen.getByTestId(NewSectionBlock.BOTTOM) + const aSingleViewPlot = screen.getByTestId(join('plot_other', 'plot.tsv')) + + dragAndDrop(aSingleViewPlot, bottomSection) + + await screen.findByTestId('plots-section_template-single_2') + const anotherSingleViewPlot = screen.getByTestId( + join('plot_logs', 'loss.tsv') + ) + + dragEnter( + anotherSingleViewPlot, + 'plots-section_template-single_0', + DragEnterDirection.RIGHT + ) + + expect(screen.getByTestId('plot_drop-target')).toBeInTheDocument() + }) + it('should show a drop zone when hovering a new section', () => { renderAppWithOptionalData({ template: complexTemplatePlotsFixture @@ -797,13 +821,7 @@ describe('App', () => { expect(topDropIcon).not.toBeInTheDocument() - act(() => { - multiViewPlot.dispatchEvent(createBubbledEvent('dragstart')) - }) - - act(() => { - topSection.dispatchEvent(createBubbledEvent('dragenter')) - }) + dragEnter(multiViewPlot, topSection.id, DragEnterDirection.LEFT) topDropIcon = screen.queryByTestId(`${NewSectionBlock.TOP}_drop-icon`) diff --git a/webview/src/plots/components/styles.module.scss b/webview/src/plots/components/styles.module.scss index 745b2033a8..4238db3f0b 100644 --- a/webview/src/plots/components/styles.module.scss +++ b/webview/src/plots/components/styles.module.scss @@ -262,6 +262,10 @@ $gap: 20px; } } +.dropSectionWrapper { + height: min-content; +} + .dropSection { height: 10px; } diff --git a/webview/src/plots/components/templatePlots/AddedSection.tsx b/webview/src/plots/components/templatePlots/AddedSection.tsx index cf904696f0..be7ab1c63b 100644 --- a/webview/src/plots/components/templatePlots/AddedSection.tsx +++ b/webview/src/plots/components/templatePlots/AddedSection.tsx @@ -43,14 +43,21 @@ export const AddedSection: React.FC = ({ const isHovered = hoveredSection === id return ( -
+
) => e.preventDefault()} + onDrop={onDrop} + draggable + className={cx( + styles.singleViewPlotsGrid, + styles.noBigGrid, + styles.dropSectionWrapper + )} + >
) => e.preventDefault()} - onDrop={onDrop} className={cx(styles.dropSection, { [styles.dropSectionMaximized]: isHovered, [styles.plot]: isHovered diff --git a/webview/src/plots/components/templatePlots/TemplatePlots.tsx b/webview/src/plots/components/templatePlots/TemplatePlots.tsx index 8fcb072e76..10d5368868 100644 --- a/webview/src/plots/components/templatePlots/TemplatePlots.tsx +++ b/webview/src/plots/components/templatePlots/TemplatePlots.tsx @@ -29,6 +29,12 @@ export const TemplatePlots: React.FC = () => { const { plotsSnapshot, size } = useSelector( (state: PlotsState) => state.template ) + const draggedOverGroup = useSelector( + (state: PlotsState) => state.dragAndDrop.draggedOverGroup + ) + const draggedRef = useSelector( + (state: PlotsState) => state.dragAndDrop.draggedRef + ) const [sections, setSections] = useState([]) const [hoveredSection, setHoveredSection] = useState('') const nbItemsPerRow = useNbItemsPerRow(size) @@ -76,8 +82,11 @@ export const TemplatePlots: React.FC = () => { } const handleDropInNewSection = (e: DragEvent) => { - const draggedSectionId = getIDIndex(e.dataTransfer.getData('group')) - const draggedId = e.dataTransfer.getData('itemId') + if (!draggedRef) { + return + } + const draggedSectionId = getIDIndex(draggedRef.group) + const draggedId = draggedRef.itemId const updatedSections = removeFromPreviousAndAddToNewSection( sections, @@ -184,6 +193,7 @@ export const TemplatePlots: React.FC = () => { setSectionEntries={setSectionEntries} useVirtualizedGrid={useVirtualizedGrid} nbItemsPerRow={nbItemsPerRow} + parentDraggedOver={draggedOverGroup === groupId} />
) diff --git a/webview/src/plots/components/templatePlots/TemplatePlotsGrid.tsx b/webview/src/plots/components/templatePlots/TemplatePlotsGrid.tsx index 6559e68b95..18f8a4fe32 100644 --- a/webview/src/plots/components/templatePlots/TemplatePlotsGrid.tsx +++ b/webview/src/plots/components/templatePlots/TemplatePlotsGrid.tsx @@ -23,6 +23,7 @@ interface TemplatePlotsGridProps { setSectionEntries: (groupIndex: number, entries: TemplatePlotEntry[]) => void useVirtualizedGrid?: boolean nbItemsPerRow: number + parentDraggedOver?: boolean } const autoSize = { @@ -38,7 +39,8 @@ export const TemplatePlotsGrid: React.FC = ({ multiView, setSectionEntries, useVirtualizedGrid, - nbItemsPerRow + nbItemsPerRow, + parentDraggedOver }) => { const [order, setOrder] = useState([]) @@ -101,6 +103,7 @@ export const TemplatePlotsGrid: React.FC = ({ } : undefined } + parentDraggedOver={parentDraggedOver} /> ) } diff --git a/webview/src/shared/components/dragDrop/DragDropContainer.tsx b/webview/src/shared/components/dragDrop/DragDropContainer.tsx index 8910f749f3..c170460c45 100644 --- a/webview/src/shared/components/dragDrop/DragDropContainer.tsx +++ b/webview/src/shared/components/dragDrop/DragDropContainer.tsx @@ -54,6 +54,7 @@ interface DragDropContainerProps { } shouldShowOnDrag?: boolean ghostElemStyle?: CSSProperties + parentDraggedOver?: boolean } export const DragDropContainer: React.FC = ({ @@ -66,7 +67,8 @@ export const DragDropContainer: React.FC = ({ dropTarget, wrapperComponent, shouldShowOnDrag, - ghostElemStyle + ghostElemStyle, + parentDraggedOver // eslint-disable-next-line sonarjs/cognitive-complexity }) => { const [draggedOverId, setDraggedOverId] = useState('') @@ -123,21 +125,18 @@ export const DragDropContainer: React.FC = ({ } } const itemIndex = idx.toString() - e.dataTransfer.setData('itemIndex', itemIndex) - e.dataTransfer.setData('itemId', id) - e.dataTransfer.setData('group', group) e.dataTransfer.effectAllowed = 'move' e.dataTransfer.dropEffect = 'move' - dispatch( - changeRef({ - group, - itemId: id, - itemIndex - }) - ) createGhostStyle(e) draggedOverIdTimeout.current = window.setTimeout(() => { + dispatch( + changeRef({ + group, + itemId: id, + itemIndex + }) + ) setDraggedId(id) setDraggedOverId(order[toIdx]) resetDraggedStyle(id) @@ -145,7 +144,6 @@ export const DragDropContainer: React.FC = ({ } const applyDrop = ( - e: DragEvent, droppedIndex: number, draggedIndex: number, newOrder: string[], @@ -159,34 +157,38 @@ export const DragDropContainer: React.FC = ({ setOrder(newOrder) dispatch(changeRef(undefined)) - onDrop?.(oldDraggedId, e.dataTransfer.getData('group'), group, droppedIndex) + onDrop?.(oldDraggedId, draggedRef?.group || '', group, droppedIndex) } const handleOnDrop = (e: DragEvent) => { + if (!draggedRef) { + return + } + const dragged = draggedRef.itemId + draggedOverIdTimeout.current = window.setTimeout(() => { setDraggedId('') }, 0) - if (e.dataTransfer.getData('itemId') === draggedOverId) { + if (dragged === draggedOverId) { dispatch(changeRef(undefined)) return } const newOrder = [...order] - const oldDraggedId = e.dataTransfer.getData('itemId') - const isNew = !order.includes(draggedId) + const isNew = !order.includes(dragged) if (isNew) { - newOrder.push(draggedId) + newOrder.push(dragged) } const draggedIndex = isNew ? newOrder.length - 1 - : getIDIndex(e.dataTransfer.getData('itemIndex')) + : getIDIndex(draggedRef.itemIndex) const droppedIndex = order.indexOf(e.currentTarget.id.split('__')[0]) const orderIdxChange = orderIdxTune(direction, droppedIndex > draggedIndex) const orderIdxChanged = droppedIndex + orderIdxChange const isEnabled = !disabledDropIds.includes(order[orderIdxChanged]) - if (isEnabled && isSameGroup(e.dataTransfer.getData('group'), group)) { - applyDrop(e, orderIdxChanged, draggedIndex, newOrder, oldDraggedId) + if (isEnabled && isSameGroup(draggedRef.group, group)) { + applyDrop(orderIdxChanged, draggedIndex, newOrder, dragged) } } @@ -245,20 +247,24 @@ export const DragDropContainer: React.FC = ({ }) : undefined + const getTarget = (id: string, isEnteringRight: boolean) => ( + + {dropTarget} + + ) + const createItemWithDropTarget = (id: string, item: JSX.Element) => { const isEnteringRight = direction === DragEnterDirection.RIGHT const target = - draggedOverGroup === group || draggedRef?.group === group ? ( - - {dropTarget} - - ) : null + draggedOverGroup === group || draggedRef?.group === group + ? getTarget(id, isEnteringRight) + : null const itemWithTag = shouldShowOnDrag ? (
@@ -285,6 +291,17 @@ export const DragDropContainer: React.FC = ({ return id === draggedOverId ? createItemWithDropTarget(id, item) : item }) + if ( + isSameGroup(draggedRef?.group, group) && + draggedRef?.itemId !== draggedId && + !draggedOverId && + parentDraggedOver && + wrappedItems.length > 0 + ) { + const lastId = wrappedItems[wrappedItems.length - 1].id + wrappedItems.push(getTarget(lastId, false)) + } + const Wrapper = wrapperComponent?.component return Wrapper ? ( diff --git a/webview/src/shared/components/dragDrop/dragDropSlice.ts b/webview/src/shared/components/dragDrop/dragDropSlice.ts index d9f40ac296..876d9fada5 100644 --- a/webview/src/shared/components/dragDrop/dragDropSlice.ts +++ b/webview/src/shared/components/dragDrop/dragDropSlice.ts @@ -4,7 +4,7 @@ export type DraggedInfo = | { itemIndex: string itemId: string - group?: string + group: string } | undefined export interface DragDropGroupState {