From 33f8ea5acbf44776f5a9ab1706d2ecd2121d3899 Mon Sep 17 00:00:00 2001 From: one Date: Sun, 7 Sep 2025 17:21:55 +0800 Subject: [PATCH] refactor(Sortable): improve sortable props, support custom modifiers (#9879) * refactor(Sortable): improve props, support modifiers * refactor: update id and index --- .../src/components/dnd/ItemRenderer.tsx | 21 ++++---- src/renderer/src/components/dnd/Sortable.tsx | 54 ++++++++++++++----- .../src/components/dnd/SortableItem.tsx | 13 ++--- src/renderer/src/components/dnd/types.ts | 1 + src/renderer/src/components/dnd/utils.ts | 16 +----- .../settings/MCPSettings/McpServersList.tsx | 3 +- 6 files changed, 64 insertions(+), 44 deletions(-) create mode 100644 src/renderer/src/components/dnd/types.ts diff --git a/src/renderer/src/components/dnd/ItemRenderer.tsx b/src/renderer/src/components/dnd/ItemRenderer.tsx index 825772a8dd..a33301df62 100644 --- a/src/renderer/src/components/dnd/ItemRenderer.tsx +++ b/src/renderer/src/components/dnd/ItemRenderer.tsx @@ -1,13 +1,16 @@ import { DraggableSyntheticListeners } from '@dnd-kit/core' -import { Transform } from '@dnd-kit/utilities' +import { CSS, Transform } from '@dnd-kit/utilities' import { classNames } from '@renderer/utils' import React, { useEffect } from 'react' import styled from 'styled-components' +import { RenderItemType } from './types' + interface ItemRendererProps { ref?: React.Ref + index?: number item: T - renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode + renderItem: RenderItemType dragging?: boolean dragOverlay?: boolean ghost?: boolean @@ -18,6 +21,7 @@ interface ItemRendererProps { export function ItemRenderer({ ref, + index, item, renderItem, dragging, @@ -42,14 +46,15 @@ export function ItemRenderer({ const wrapperStyle = { transition, - '--translate-x': transform ? `${Math.round(transform.x)}px` : undefined, - '--translate-y': transform ? `${Math.round(transform.y)}px` : undefined, - '--scale-x': transform?.scaleX ? `${transform.scaleX}` : undefined, - '--scale-y': transform?.scaleY ? `${transform.scaleY}` : undefined + transform: CSS.Transform.toString(transform ?? null) } as React.CSSProperties return ( - + ({ const ItemWrapper = styled.div` box-sizing: border-box; - transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0) scaleX(var(--scale-x, 1)) - scaleY(var(--scale-y, 1)); transform-origin: 0 0; touch-action: manipulation; diff --git a/src/renderer/src/components/dnd/Sortable.tsx b/src/renderer/src/components/dnd/Sortable.tsx index 3ef77acb31..733163ec6c 100644 --- a/src/renderer/src/components/dnd/Sortable.tsx +++ b/src/renderer/src/components/dnd/Sortable.tsx @@ -5,13 +5,19 @@ import { DragOverlay, DropAnimation, KeyboardSensor, + Modifier, Over, TouchSensor, UniqueIdentifier, useSensor, useSensors } from '@dnd-kit/core' -import { restrictToHorizontalAxis, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { + restrictToFirstScrollableAncestor, + restrictToHorizontalAxis, + restrictToVerticalAxis, + restrictToWindowEdges +} from '@dnd-kit/modifiers' import { horizontalListSortingStrategy, rectSortingStrategy, @@ -25,6 +31,7 @@ import styled from 'styled-components' import { ItemRenderer } from './ItemRenderer' import { SortableItem } from './SortableItem' +import { RenderItemType } from './types' import { PortalSafePointerSensor } from './utils' interface SortableProps { @@ -39,7 +46,7 @@ interface SortableProps { /** Callback when drag ends, will be passed to dnd-kit's onDragEnd */ onDragEnd?: (event: { over: Over }) => void /** Function to render individual item, receives item data and drag state */ - renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode + renderItem: RenderItemType /** Layout type - 'list' for vertical/horizontal list, 'grid' for grid layout */ layout?: 'list' | 'grid' /** Whether sorting is horizontal */ @@ -54,10 +61,17 @@ interface SortableProps { className?: string /** Item list style */ listStyle?: React.CSSProperties - /** Ghost item style */ - ghostItemStyle?: React.CSSProperties /** Item gap */ gap?: number | string + /** Restrictions, shortcuts for some modifiers */ + restrictions?: { + /** Add modifier restrictToWindowEdges */ + windowEdges?: boolean + /** Add modifier restrictToFirstScrollableAncestor */ + scrollableAncestor?: boolean + } + /** Additional modifiers */ + modifiers?: Modifier[] } function Sortable({ @@ -73,7 +87,9 @@ function Sortable({ showGhost = false, className, listStyle, - gap + gap, + restrictions, + modifiers: customModifiers }: SortableProps) { const sensors = useSensors( useSensor(PortalSafePointerSensor, { @@ -132,7 +148,18 @@ function Sortable({ const strategy = layout === 'list' ? (horizontal ? horizontalListSortingStrategy : verticalListSortingStrategy) : rectSortingStrategy - const modifiers = layout === 'list' ? (horizontal ? [restrictToHorizontalAxis] : [restrictToVerticalAxis]) : [] + + const { windowEdges = false, scrollableAncestor = false } = restrictions ?? {} + + const modifiers = useMemo( + () => [ + ...(layout === 'list' ? [horizontal ? restrictToHorizontalAxis : restrictToVerticalAxis] : []), + ...(windowEdges ? [restrictToWindowEdges] : []), + ...(scrollableAncestor ? [restrictToFirstScrollableAncestor] : []), + ...(customModifiers ?? []) + ], + [layout, horizontal, windowEdges, scrollableAncestor, customModifiers] + ) const dropAnimation: DropAnimation = useMemo( () => ({ @@ -162,8 +189,9 @@ function Sortable({ {items.map((item, index) => ( ` &[data-layout='list'] { display: flex; align-items: center; + } - [data-direction='horizontal'] { - flex-direction: row; - } + &[data-layout='list'][data-direction='horizontal'] { + flex-direction: row; + } - [data-direction='vertical'] { - flex-direction: column; - } + &[data-layout='list'][data-direction='vertical'] { + flex-direction: column; } ` diff --git a/src/renderer/src/components/dnd/SortableItem.tsx b/src/renderer/src/components/dnd/SortableItem.tsx index 3d83f58a1d..60901223a0 100644 --- a/src/renderer/src/components/dnd/SortableItem.tsx +++ b/src/renderer/src/components/dnd/SortableItem.tsx @@ -1,25 +1,25 @@ import { useSortable } from '@dnd-kit/sortable' -import React from 'react' import { ItemRenderer } from './ItemRenderer' +import { RenderItemType } from './types' interface SortableItemProps { item: T - getId: (item: T) => string | number - renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode + id: string | number + index: number + renderItem: RenderItemType useDragOverlay?: boolean showGhost?: boolean } export function SortableItem({ item, - getId, + id, + index, renderItem, useDragOverlay = true, showGhost = true }: SortableItemProps) { - const id = getId(item) - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }) @@ -28,6 +28,7 @@ export function SortableItem({ = (item: T, props: { dragging: boolean }) => React.ReactNode diff --git a/src/renderer/src/components/dnd/utils.ts b/src/renderer/src/components/dnd/utils.ts index 1435621efd..d2e73ede6e 100644 --- a/src/renderer/src/components/dnd/utils.ts +++ b/src/renderer/src/components/dnd/utils.ts @@ -1,4 +1,4 @@ -import { defaultDropAnimationSideEffects, type DropAnimation, PointerSensor } from '@dnd-kit/core' +import { PointerSensor } from '@dnd-kit/core' export const PORTAL_NO_DND_SELECTORS = [ '.ant-dropdown', @@ -8,20 +8,6 @@ export const PORTAL_NO_DND_SELECTORS = [ '.ant-modal' ].join(',') -/** - * Default drop animation config. - * The opacity is set so to match the drag overlay case. - */ -export const dropAnimationConfig: DropAnimation = { - sideEffects: defaultDropAnimationSideEffects({ - styles: { - active: { - opacity: '0.25' - } - } - }) -} - /** * Prevent drag on elements with specific classes or data-no-dnd attribute */ diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index 3bf7d619a1..f45eb85015 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -253,7 +253,8 @@ const McpServersList: FC = () => { itemKey="id" onSortEnd={onSortEnd} layout="grid" - gap={'12px'} + gap="12px" + restrictions={{ scrollableAncestor: true }} useDragOverlay showGhost renderItem={(server) => (