refactor: migrate to @dnd-kit/react and simplify sortable components (#7819)

* refactor: migrate to @dnd-kit/react and simplify sortable components

* chore: enable css formatting in biome.json

* refactor(profile): simplify icon rendering logic and add alt text

* style(profiles): add overflow hidden to profile grid container

* refactor(profile): add radix for parseInt and improve type safety in profiles

* docs: update changelog with drag-and-drop styling improvements

* refactor(profile): move hasValidName function out of ProxiesEditorViewer

* refactor(proxy): refactor proxy chain into sortable components

* feat(proxy): add drop state styling and arrow animations to proxy chain

* style(profiles): reduce horizontal padding in profile grid

* feat(profile): add vertical margin to editor viewer items

* chore: refresh pnpm lockfile

* fix: preserve drag and drop behavior

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
This commit is contained in:
oomeow
2026-08-28 08:17:02 +00:00
committed by GitHub
co-authored by Tunglies
parent 181dfb87a4
commit b6de05ffe4
22 changed files with 1105 additions and 1115 deletions
+168 -124
View File
@@ -1,20 +1,11 @@
import { arrayMove } from '@dnd-kit/helpers'
import {
closestCenter,
DndContext,
type DragEndEvent,
DragDropProvider,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
type DragEndEvent,
} from '@dnd-kit/react'
import { isSortable, useSortable } from '@dnd-kit/react/sortable'
import {
ArrowDownward,
Delete as DeleteIcon,
@@ -31,10 +22,18 @@ import {
IconButton,
Paper,
Typography,
keyframes,
useTheme,
} from '@mui/material'
import * as yaml from 'js-yaml'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
type Ref,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
closeAllConnections,
@@ -54,6 +53,10 @@ import { debugLog } from '@/utils/debug'
import { rebindProxyChainItems, type ProxyChainItem } from './proxy-chain-model'
const chainPointerSensor = PointerSensor.configure({
activationConstraints: () => undefined,
})
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
interface ParsedChainConfig {
@@ -73,7 +76,8 @@ interface ProxyChainProps {
selectedGroup?: string | null
}
interface SortableItemProps {
interface ProxyChainItemProps {
id: string
proxy: ProxyChainItem
index: number
isFirst: boolean
@@ -81,6 +85,17 @@ interface SortableItemProps {
onRemove: (id: string) => void
}
const arrowFadeIn = keyframes`
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
`
const toChainItems = (
parsedConfig: ParsedChainConfig | null | undefined,
): ProxyChainItem[] => {
@@ -96,29 +111,29 @@ const toChainItems = (
)
}
const SortableItem = ({
interface ChainCardProps {
proxy: ProxyChainItem
index: number
isFirst: boolean
isLast: boolean
isDragging?: boolean
isDropping?: boolean
handleRef?: Ref<HTMLElement> | null
onRemove?: (id: string) => void
}
const ChainCard = ({
proxy,
index,
isFirst,
isLast,
isDragging,
isDropping,
handleRef,
onRemove,
}: SortableItemProps) => {
}: ChainCardProps) => {
const theme = useTheme()
const { t } = useTranslation()
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: proxy.id })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
const roleLabel = isFirst
? t('proxies.page.chain.entryNode')
@@ -134,37 +149,31 @@ const SortableItem = ({
return (
<Box
ref={setNodeRef}
style={style}
sx={{
mb: 0,
display: 'flex',
alignItems: 'center',
p: 1,
backgroundColor: isDragging
? theme.palette.action.selected
: theme.palette.background.default,
backgroundColor: theme.palette.background.default,
borderRadius: 1,
border: roleColor
? `1.5px solid ${roleColor}`
: `1px solid ${theme.palette.divider}`,
boxShadow: isDragging ? theme.shadows[4] : theme.shadows[1],
transition: 'box-shadow 0.2s, background-color 0.2s',
opacity: proxy.recordId === undefined ? 0.55 : undefined,
transition: 'box-shadow 0.2s, background-color 0.2s',
boxShadow: isDropping
? `0 0 0 2px ${theme.palette.primary.main}66`
: undefined,
}}
>
<Box
{...attributes}
{...listeners}
ref={handleRef}
sx={{
display: 'flex',
alignItems: 'center',
mr: 1,
color: theme.palette.text.secondary,
cursor: 'grab',
'&:active': {
cursor: 'grabbing',
},
cursor: isDragging ? 'grabbing' : 'grab',
}}
>
<DragIndicator />
@@ -229,18 +238,87 @@ const SortableItem = ({
/>
)}
<IconButton
size="small"
onClick={() => onRemove(proxy.id)}
sx={{
color: theme.palette.error.main,
'&:hover': {
backgroundColor: theme.palette.error.light + '20',
{onRemove && (
<IconButton
size="small"
onClick={() => onRemove(proxy.id)}
sx={{
color: theme.palette.error.main,
'&:hover': {
backgroundColor: theme.palette.error.light + '20',
},
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
)}
</Box>
)
}
const SortableProxyChainItem = ({
id,
proxy,
index,
isFirst,
isLast,
onRemove,
}: ProxyChainItemProps) => {
const theme = useTheme()
const [element, setElement] = useState<Element | null>(null)
const handleRef = useRef<HTMLElement | null>(null)
const { isDragging, isDropping } = useSortable({
id,
index,
element,
handle: handleRef,
})
return (
<Box
ref={setElement}
sx={{
width: '100%',
'&[data-dnd-dragging]': {
height: 'auto',
boxShadow: 'none !important',
'.proxy-chain-arrow': {
display: 'none',
},
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
},
'& .proxy-chain-arrow': {
animation: `${arrowFadeIn} 0.25s ease`,
},
}}
>
<ChainCard
proxy={proxy}
index={index}
isFirst={isFirst}
isLast={isLast}
isDragging={isDragging}
isDropping={isDropping}
handleRef={handleRef}
onRemove={onRemove}
/>
{!isLast && (
<Box
className="proxy-chain-arrow"
sx={{
display: 'flex',
justifyContent: 'center',
py: 0.25,
}}
>
<ArrowDownward
sx={{
fontSize: 20,
color: theme.palette.primary.main,
opacity: 0.7,
}}
/>
</Box>
)}
</Box>
)
}
@@ -330,30 +408,25 @@ export const ProxyChain = ({
chainLengthRef.current = currentProxyChain.length
}, [currentProxyChain.length, markUnsavedChanges])
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
)
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event
const { operation, canceled } = event
const { source, target } = operation
if (canceled || !target || !isSortable(source)) return
if (active.id !== over?.id) {
const oldIndex = currentProxyChain.findIndex(
(item) => item.id === active.id,
)
const newIndex = currentProxyChain.findIndex(
(item) => item.id === over?.id,
)
onUpdateChain(arrayMove(currentProxyChain, oldIndex, newIndex))
markUnsavedChanges()
const { index: newIndex, initialIndex: oldIndex } = source.sortable
if (
oldIndex < 0 ||
newIndex < 0 ||
oldIndex >= currentProxyChain.length ||
newIndex >= currentProxyChain.length ||
oldIndex === newIndex
) {
return
}
onUpdateChain(arrayMove(currentProxyChain, oldIndex, newIndex))
markUnsavedChanges()
},
[currentProxyChain, onUpdateChain, markUnsavedChanges],
)
@@ -594,56 +667,27 @@ export const ProxyChain = ({
<Typography>{t('proxies.page.chain.empty')}</Typography>
</Box>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
<DragDropProvider
sensors={[chainPointerSensor, KeyboardSensor]}
onDragEnd={handleDragEnd}
>
<SortableContext
items={currentProxyChain.map((proxy) => proxy.id)}
strategy={verticalListSortingStrategy}
>
<Box
sx={{
borderRadius: 1,
minHeight: 60,
p: 1,
}}
>
{currentProxyChain.map((proxy, index) => (
<Box key={proxy.id}>
<SortableItem
proxy={proxy}
index={index}
isFirst={index === 0}
isLast={
index === currentProxyChain.length - 1 &&
currentProxyChain.length > 1
}
onRemove={handleRemoveProxy}
/>
{index < currentProxyChain.length - 1 && (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
py: 0.25,
}}
>
<ArrowDownward
sx={{
fontSize: 20,
color: theme.palette.primary.main,
opacity: 0.7,
}}
/>
</Box>
)}
</Box>
))}
</Box>
</SortableContext>
</DndContext>
<Box sx={{ borderRadius: 1, minHeight: 60, p: 1 }}>
{currentProxyChain.map((proxy, index) => (
<SortableProxyChainItem
key={proxy.id}
id={proxy.id}
proxy={proxy}
index={index}
isFirst={index === 0}
isLast={
index === currentProxyChain.length - 1 &&
currentProxyChain.length > 1
}
onRemove={handleRemoveProxy}
/>
))}
</Box>
</DragDropProvider>
)}
</Box>
</Paper>