From d7e2ec0860c026080113680503b870ef9b5298ce Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 9 Nov 2025 01:03:25 -0800 Subject: [PATCH] add drag state to item table --- package.json | 6 +- pnpm-lock.yaml | 35 ++---- .../item-table-list-column.module.css | 4 + .../item-table-list-column.tsx | 114 +++++++++++++++++- .../item-table-list/item-table-list.tsx | 22 +++- 5 files changed, 152 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index a5db7a701..81055a7cb 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,9 @@ "@ag-grid-community/infinite-row-model": "^28.2.1", "@ag-grid-community/react": "^28.2.1", "@ag-grid-community/styles": "^28.2.1", - "@atlaskit/pragmatic-drag-and-drop": "1.4.0", - "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0", - "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", + "@atlaskit/pragmatic-drag-and-drop": "1.7.7", + "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "@mantine/colors-generator": "^8.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a2059277..ef96a00c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,13 +24,13 @@ importers: specifier: ^28.2.1 version: 28.2.1 '@atlaskit/pragmatic-drag-and-drop': - specifier: 1.4.0 - version: 1.4.0 + specifier: 1.7.7 + version: 1.7.7 '@atlaskit/pragmatic-drag-and-drop-auto-scroll': - specifier: ^2.1.0 - version: 2.1.1 + specifier: ^2.1.2 + version: 2.1.2 '@atlaskit/pragmatic-drag-and-drop-hitbox': - specifier: ^1.0.3 + specifier: ^1.1.0 version: 1.1.0 '@electron-toolkit/preload': specifier: ^3.0.1 @@ -395,17 +395,14 @@ packages: peerDependencies: ajv: '>=8' - '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1': - resolution: {integrity: sha512-VAQEb3NVLY9Q5ZgC5Eiws9Uf6xOINY9/pAZMdbOVlF90uRXEkmpYqdTL+zeyZ8U8deuqYCmXr7oWIEnxpNQVzA==} + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2': + resolution: {integrity: sha512-6BgAUxSNbQFiG3uqNxf53cDQADn5mSeh/JsQzCHo46GPQnVWIJk77zWC8yZ++0Mfg1ECy02zNrbniF7SgHAhXQ==} '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': resolution: {integrity: sha512-JWt6eVp6Br2FPHRM8s0dUIHQk/jFInGP1f3ti5CdtM1Ji5/pt8Akm44wDC063Gv2i5RGseixtbW0z/t6RYtbdg==} - '@atlaskit/pragmatic-drag-and-drop@1.4.0': - resolution: {integrity: sha512-qRY3PTJIcxfl/QB8Gwswz+BRvlmgAC5pB+J2hL6dkIxgqAgVwOhAamMUKsrOcFU/axG2Q7RbNs1xfoLKDuhoPg==} - - '@atlaskit/pragmatic-drag-and-drop@1.7.4': - resolution: {integrity: sha512-lZHnO9BJdHPKnwB0uvVUCyDnIhL+WAHzXQ2EXX0qacogOsnvIUiCgY0BLKhBqTCWln3/f/Ox5jU54MKO6ayh9A==} + '@atlaskit/pragmatic-drag-and-drop@1.7.7': + resolution: {integrity: sha512-jX+68AoSTqO/fhCyJDTZ38Ey6/wyL2Iq+J/moanma0YyktpnoHxevjY1UNJHYp0NCburdQDZSL1ZFac1mO1osQ==} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} @@ -5857,23 +5854,17 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1': + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2': dependencies: - '@atlaskit/pragmatic-drag-and-drop': 1.7.4 + '@atlaskit/pragmatic-drag-and-drop': 1.7.7 '@babel/runtime': 7.27.1 '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': dependencies: - '@atlaskit/pragmatic-drag-and-drop': 1.7.4 + '@atlaskit/pragmatic-drag-and-drop': 1.7.7 '@babel/runtime': 7.27.1 - '@atlaskit/pragmatic-drag-and-drop@1.4.0': - dependencies: - '@babel/runtime': 7.27.1 - bind-event-listener: 3.0.0 - raf-schd: 4.0.3 - - '@atlaskit/pragmatic-drag-and-drop@1.7.4': + '@atlaskit/pragmatic-drag-and-drop@1.7.7': dependencies: '@babel/runtime': 7.27.1 bind-event-listener: 3.0.0 diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.module.css b/src/renderer/components/item-list/item-table-list/item-table-list-column.module.css index a23cc4c68..db5d59d37 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.module.css +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.module.css @@ -115,6 +115,10 @@ } } +.container.data-row.dragging { + opacity: 0.5; +} + .container.data-row > * { position: relative; z-index: 2; diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx index 479caa4d6..9b29317ff 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx @@ -1,3 +1,4 @@ +import { useMergedRef } from '@mantine/hooks'; import clsx from 'clsx'; import React, { CSSProperties, ReactNode, useEffect, useRef } from 'react'; import { CellComponentProps } from 'react-window-v2'; @@ -5,6 +6,7 @@ import { CellComponentProps } from 'react-window-v2'; import styles from './item-table-list-column.module.css'; import i18n from '/@/i18n/i18n'; +import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column'; import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column'; import { ArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/artists-column'; @@ -29,9 +31,11 @@ import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/co import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column'; import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types'; +import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { Icon } from '/@/shared/components/icon/icon'; import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Text } from '/@/shared/components/text/text'; +import { DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop'; import { TableColumn } from '/@/shared/types/types'; export interface ItemTableListColumn extends CellComponentProps {} @@ -145,6 +149,58 @@ export const TableColumnTextContainer = ( ? props.internalState.isSelected((item as any).id) : false; + const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item; + + const { isDragging: isDraggingLocal, ref: dragRef } = useDragDrop({ + drag: { + getId: () => { + if (!item || !isDataRow) { + return []; + } + + const draggedItems = getDraggedItems( + item as any, + props.itemType, + props.internalState, + ); + return draggedItems.map((draggedItem) => draggedItem.id); + }, + getItem: () => { + if (!item || !isDataRow) { + return []; + } + + return [item]; + }, + onDragStart: () => { + if (!item || !isDataRow || !props.internalState) { + return; + } + + const draggedItems = getDraggedItems( + item as any, + props.itemType, + props.internalState, + ); + props.internalState.setDragging(draggedItems); + }, + onDrop: () => { + if (props.internalState) { + props.internalState.setDragging([]); + } + }, + target: DragTargetMap[props.itemType] || DragTarget.GENERIC, + }, + isEnabled: shouldEnableDrag, + }); + + const isDragging = + item && typeof item === 'object' && 'id' in item && props.internalState + ? props.internalState.isDragging((item as any).id) + : isDraggingLocal; + + const mergedRef = useMergedRef(containerRef, shouldEnableDrag ? dragRef : null); + useEffect(() => { if (!isDataRow || !containerRef.current) return; @@ -203,6 +259,7 @@ export const TableColumnTextContainer = ( [styles.center]: props.columns[props.columnIndex].align === 'center', [styles.compact]: props.size === 'compact', [styles.dataRow]: isDataRow, + [styles.dragging]: isDataRow && isDragging, [styles.large]: props.size === 'large', [styles.left]: props.columns[props.columnIndex].align === 'start', [styles.paddingLg]: props.cellPadding === 'lg', @@ -219,7 +276,7 @@ export const TableColumnTextContainer = ( })} data-row-index={isDataRow ? props.rowIndex : undefined} onClick={handleCellClick} - ref={containerRef} + ref={mergedRef} style={props.style} > ({ + drag: { + getId: () => { + if (!item || !isDataRow) { + return []; + } + + const draggedItems = getDraggedItems( + item as any, + props.itemType, + props.internalState, + ); + return draggedItems.map((draggedItem) => draggedItem.id); + }, + getItem: () => { + if (!item || !isDataRow) { + return []; + } + + return [item]; + }, + onDragStart: () => { + if (!item || !isDataRow || !props.internalState) { + return; + } + + const draggedItems = getDraggedItems( + item as any, + props.itemType, + props.internalState, + ); + props.internalState.setDragging(draggedItems); + }, + onDrop: () => { + if (props.internalState) { + props.internalState.setDragging([]); + } + }, + target: DragTargetMap[props.itemType] || DragTarget.GENERIC, + }, + isEnabled: shouldEnableDrag, + }); + + const isDragging = + item && typeof item === 'object' && 'id' in item && props.internalState + ? props.internalState.isDragging((item as any).id) + : isDraggingLocal; + + const mergedRef = useMergedRef(containerRef, shouldEnableDrag ? dragRef : null); + useEffect(() => { if (!isDataRow || !containerRef.current) return; @@ -312,6 +421,7 @@ export const TableColumnContainer = ( [styles.center]: props.columns[props.columnIndex].align === 'center', [styles.compact]: props.size === 'compact', [styles.dataRow]: isDataRow, + [styles.dragging]: isDataRow && isDragging, [styles.large]: props.size === 'large', [styles.left]: props.columns[props.columnIndex].align === 'start', [styles.paddingLg]: props.cellPadding === 'lg', @@ -328,7 +438,7 @@ export const TableColumnContainer = ( })} data-row-index={isDataRow ? props.rowIndex : undefined} onClick={handleCellClick} - ref={containerRef} + ref={mergedRef} style={{ ...props.containerStyle, ...props.style }} > {props.children} diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index c550a5851..3994abd0d 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -1,5 +1,6 @@ // Component adapted from https://github.com/bvaughn/react-window/issues/826 +import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; import { useMergedRef } from '@mantine/hooks'; import clsx from 'clsx'; import debounce from 'lodash/debounce'; @@ -23,8 +24,8 @@ import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { - ItemListStateItem, ItemListStateActions, + ItemListStateItem, useItemListState, } from '/@/renderer/components/item-list/helpers/item-list-state'; import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns'; @@ -42,6 +43,7 @@ interface VirtualizedTableGridProps { controls: ItemControls; data: unknown[]; enableAlternateRowColors: boolean; + enableDrag?: boolean; enableExpansion: boolean; enableHeader: boolean; enableHorizontalBorders: boolean; @@ -77,6 +79,7 @@ const VirtualizedTableGrid = React.memo( controls, data, enableAlternateRowColors, + enableDrag, enableExpansion, enableHeader, enableHorizontalBorders, @@ -115,6 +118,7 @@ const VirtualizedTableGrid = React.memo( controls, data: enableHeader ? [null, ...data] : data, enableAlternateRowColors, + enableDrag, enableExpansion, enableHeader, enableHorizontalBorders, @@ -134,6 +138,7 @@ const VirtualizedTableGrid = React.memo( enableHeader, data, enableAlternateRowColors, + enableDrag, enableExpansion, enableHorizontalBorders, enableRowHoverHighlight, @@ -414,6 +419,7 @@ export interface TableItemProps { controls: ItemControls; data: ItemTableListProps['data']; enableAlternateRowColors?: ItemTableListProps['enableAlternateRowColors']; + enableDrag?: ItemTableListProps['enableDrag']; enableExpansion?: ItemTableListProps['enableExpansion']; enableHeader?: ItemTableListProps['enableHeader']; enableHorizontalBorders?: ItemTableListProps['enableHorizontalBorders']; @@ -434,6 +440,7 @@ interface ItemTableListProps { currentPage?: number; data: unknown[]; enableAlternateRowColors?: boolean; + enableDrag?: boolean; enableExpansion?: boolean; enableHeader?: boolean; enableHorizontalBorders?: boolean; @@ -461,6 +468,7 @@ export const ItemTableList = ({ currentPage, data, enableAlternateRowColors = false, + enableDrag = true, enableExpansion = true, enableHeader = true, enableHorizontalBorders = false, @@ -677,10 +685,19 @@ export const ItemTableList = ({ elements: { viewport: root.firstElementChild as HTMLElement }, target: root, }); + + if (enableDrag) { + autoScrollForElements({ + canScroll: () => true, + element: root.firstElementChild as HTMLElement, + getAllowedAxis: () => 'vertical', + getConfiguration: () => ({ maxScrollSpeed: 'fast' }), + }); + } } return undefined; - }, [initialize]); + }, [enableDrag, initialize]); useEffect(() => { const header = pinnedRowRef.current?.childNodes[0] as HTMLDivElement; @@ -1266,6 +1283,7 @@ export const ItemTableList = ({ controls={controls} data={data} enableAlternateRowColors={enableAlternateRowColors} + enableDrag={enableDrag} enableExpansion={enableExpansion} enableHeader={enableHeader} enableHorizontalBorders={enableHorizontalBorders}