diff --git a/src/renderer/features/shared/components/table-config.module.css b/src/renderer/features/shared/components/table-config.module.css index fdae7c68c..6feb0ed8f 100644 --- a/src/renderer/features/shared/components/table-config.module.css +++ b/src/renderer/features/shared/components/table-config.module.css @@ -9,6 +9,7 @@ } .item { + position: relative; display: flex; flex-wrap: nowrap; gap: var(--theme-spacing-md); @@ -24,3 +25,29 @@ outline: 2px solid var(--theme-colors-primary); outline-offset: 2px; } + +.item.dragging { + opacity: 0.5; +} + +.item.dragged-over-top::before { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 1; + height: 2px; + content: ''; + background-color: var(--theme-colors-primary); +} + +.item.dragged-over-bottom::before { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + height: 2px; + content: ''; + background-color: var(--theme-colors-primary); +} diff --git a/src/renderer/features/shared/components/table-config.tsx b/src/renderer/features/shared/components/table-config.tsx index 9dc8bab17..612493446 100644 --- a/src/renderer/features/shared/components/table-config.tsx +++ b/src/renderer/features/shared/components/table-config.tsx @@ -1,8 +1,18 @@ +import { + attachClosestEdge, + type Edge, + extractClosestEdge, +} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'; +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { + draggable, + dropTargetForElements, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview'; import { useDebouncedState } from '@mantine/hooks'; import clsx from 'clsx'; import Fuse from 'fuse.js'; -import { motion } from 'motion/react'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styles from './table-config.module.css'; @@ -25,6 +35,7 @@ import { Stack } from '/@/shared/components/stack/stack'; import { TextInput } from '/@/shared/components/text-input/text-input'; import { Text } from '/@/shared/components/text/text'; import { Tooltip } from '/@/shared/components/tooltip/tooltip'; +import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; import { ItemListKey, ListPaginationType } from '/@/shared/types/types'; interface TableConfigProps { @@ -432,6 +443,23 @@ const TableColumnConfig = ({ })); }, [value, searchColumns, fuse]); + const handleReorder = useCallback( + (idFrom: string, idTo: string, edge: Edge | null) => { + const idList = value.map((item) => item.id); + const newIdOrder = dndUtils.reorderById({ + edge, + idFrom, + idTo, + list: idList, + }); + + // Map the new ID order back to full items + const newOrder = newIdOrder.map((id) => value.find((item) => item.id === id)!); + onChange(newOrder); + }, + [onChange, value], + ); + return ( @@ -444,149 +472,300 @@ const TableColumnConfig = ({ size="xs" /> - {filteredColumns.map(({ item, matches }) => ( - 0, - })} - key={item.id} - layout - > - - handleChangeEnabled(item, e.currentTarget.checked)} - size="sm" - /> - - - - handleMoveUp(item)} - size="xs" - tooltip={{ - label: t('table.config.general.moveUp', { - postProcess: 'sentenceCase', - }), - }} - variant="subtle" - /> - handleMoveDown(item)} - size="xs" - tooltip={{ - label: t('table.config.general.moveDown', { - postProcess: 'sentenceCase', - }), - }} - variant="subtle" - /> - - - handlePinToLeft(item)} - size="xs" - tooltip={{ - label: t('table.config.general.pinToLeft', { - postProcess: 'sentenceCase', - }), - }} - variant={item.pinned === 'left' ? 'filled' : 'subtle'} - /> - handlePinToRight(item)} - size="xs" - tooltip={{ - label: t('table.config.general.pinToRight', { - postProcess: 'sentenceCase', - }), - }} - variant={item.pinned === 'right' ? 'filled' : 'subtle'} - /> - - - handleAlignLeft(item)} - size="xs" - tooltip={{ - label: t('table.config.general.alignLeft', { - postProcess: 'sentenceCase', - }), - }} - variant={item.align === 'start' ? 'filled' : 'subtle'} - /> - handleAlignCenter(item)} - size="xs" - tooltip={{ - label: t('table.config.general.alignCenter', { - postProcess: 'sentenceCase', - }), - }} - variant={item.align === 'center' ? 'filled' : 'subtle'} - /> - handleAlignRight(item)} - size="xs" - tooltip={{ - label: t('table.config.general.alignRight', { - postProcess: 'sentenceCase', - }), - }} - variant={item.align === 'end' ? 'filled' : 'subtle'} - /> - - - {item.pinned === null && ( - - - handleAutoSize(item, e.currentTarget.checked) - } - size="xs" - /> - - )} - - } - max={2000} - min={0} - onChange={(value) => handleRowWidth(item, value)} - size="xs" - step={10} - value={item.width} - variant="subtle" - /> - - - ))} +
+ {filteredColumns.map(({ item, matches }) => ( + + ))} +
); }; + +const DragHandle = ({ dragHandleRef }: { dragHandleRef: React.RefObject }) => { + const { t } = useTranslation(); + + return ( + + ); +}; + +const TableColumnItem = ({ + handleAlignCenter, + handleAlignLeft, + handleAlignRight, + handleAutoSize, + handleChangeEnabled, + handleMoveDown, + handleMoveUp, + handlePinToLeft, + handlePinToRight, + handleReorder, + handleRowWidth, + item, + label, + matches, +}: { + handleAlignCenter: (item: ItemTableListColumnConfig) => void; + handleAlignLeft: (item: ItemTableListColumnConfig) => void; + handleAlignRight: (item: ItemTableListColumnConfig) => void; + handleAutoSize: (item: ItemTableListColumnConfig, checked: boolean) => void; + handleChangeEnabled: (item: ItemTableListColumnConfig, checked: boolean) => void; + handleMoveDown: (item: ItemTableListColumnConfig) => void; + handleMoveUp: (item: ItemTableListColumnConfig) => void; + handlePinToLeft: (item: ItemTableListColumnConfig) => void; + handlePinToRight: (item: ItemTableListColumnConfig) => void; + handleReorder: (idFrom: string, idTo: string, edge: Edge | null) => void; + handleRowWidth: (item: ItemTableListColumnConfig, number: number | string) => void; + item: ItemTableListColumnConfig; + label: string; + matches: null | readonly Fuse.FuseResultMatch[]; +}) => { + const { t } = useTranslation(); + const ref = useRef(null); + const dragHandleRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [isDraggedOver, setIsDraggedOver] = useState(null); + + useEffect(() => { + if (!ref.current || !dragHandleRef.current) { + return; + } + + return combine( + draggable({ + element: dragHandleRef.current, + getInitialData: () => { + const data = dndUtils.generateDragData({ + id: [item.id], + operation: [DragOperation.REORDER], + type: DragTarget.TABLE_COLUMN, + }); + return data; + }, + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + onGenerateDragPreview: (data) => { + disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage }); + }, + }), + dropTargetForElements({ + canDrop: (args) => { + const data = args.source.data as unknown as DragData; + const isSelf = (args.source.data.id as string[])[0] === item.id; + return dndUtils.isDropTarget(data.type, [DragTarget.TABLE_COLUMN]) && !isSelf; + }, + element: ref.current, + getData: ({ element, input }) => { + const data = dndUtils.generateDragData({ + id: [item.id], + operation: [DragOperation.REORDER], + type: DragTarget.TABLE_COLUMN, + }); + + return attachClosestEdge(data, { + allowedEdges: ['top', 'bottom'], + element, + input, + }); + }, + onDrag: (args) => { + const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); + setIsDraggedOver(closestEdgeOfTarget); + }, + onDragLeave: () => { + setIsDraggedOver(null); + }, + onDrop: (args) => { + const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); + + const from = args.source.data.id as string[]; + const to = args.self.data.id as string[]; + + handleReorder(from[0], to[0], closestEdgeOfTarget); + setIsDraggedOver(null); + }, + }), + ); + }, [item.id, handleReorder]); + + return ( +
0, + })} + ref={ref} + > + + + handleChangeEnabled(item, e.currentTarget.checked)} + size="sm" + /> + + + + handleMoveUp(item)} + size="xs" + tooltip={{ + label: t('table.config.general.moveUp', { + postProcess: 'sentenceCase', + }), + }} + variant="subtle" + /> + handleMoveDown(item)} + size="xs" + tooltip={{ + label: t('table.config.general.moveDown', { + postProcess: 'sentenceCase', + }), + }} + variant="subtle" + /> + + + handlePinToLeft(item)} + size="xs" + tooltip={{ + label: t('table.config.general.pinToLeft', { + postProcess: 'sentenceCase', + }), + }} + variant={item.pinned === 'left' ? 'filled' : 'subtle'} + /> + handlePinToRight(item)} + size="xs" + tooltip={{ + label: t('table.config.general.pinToRight', { + postProcess: 'sentenceCase', + }), + }} + variant={item.pinned === 'right' ? 'filled' : 'subtle'} + /> + + + handleAlignLeft(item)} + size="xs" + tooltip={{ + label: t('table.config.general.alignLeft', { + postProcess: 'sentenceCase', + }), + }} + variant={item.align === 'start' ? 'filled' : 'subtle'} + /> + handleAlignCenter(item)} + size="xs" + tooltip={{ + label: t('table.config.general.alignCenter', { + postProcess: 'sentenceCase', + }), + }} + variant={item.align === 'center' ? 'filled' : 'subtle'} + /> + handleAlignRight(item)} + size="xs" + tooltip={{ + label: t('table.config.general.alignRight', { + postProcess: 'sentenceCase', + }), + }} + variant={item.align === 'end' ? 'filled' : 'subtle'} + /> + + + {item.pinned === null && ( + + + handleAutoSize(item, e.currentTarget.checked) + } + size="xs" + /> + + )} + + } + max={2000} + min={0} + onChange={(value) => handleRowWidth(item, value)} + size="xs" + step={10} + value={item.width} + variant="subtle" + /> + +
+ ); +};