mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-10 04:12:08 +08:00
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:
@@ -1,5 +1,3 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { DeleteForeverRounded, UndoRounded } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
@@ -19,22 +17,7 @@ interface Props {
|
||||
|
||||
export const GroupItem = (props: Props) => {
|
||||
const { type, group, onDelete } = props
|
||||
const sortable = type === 'prepend' || type === 'append'
|
||||
|
||||
const {
|
||||
attributes: sortableAttributes,
|
||||
listeners: sortableListeners,
|
||||
setNodeRef: sortableSetNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: group.name,
|
||||
disabled: !sortable,
|
||||
})
|
||||
const dragAttributes = sortable ? sortableAttributes : undefined
|
||||
const dragListeners = sortable ? sortableListeners : undefined
|
||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined
|
||||
const isSortable = type === 'prepend' || type === 'append'
|
||||
|
||||
const iconCachePath = useIconCache({
|
||||
icon: group.icon,
|
||||
@@ -55,15 +38,12 @@ export const GroupItem = (props: Props) => {
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: '100%',
|
||||
margin: '8px 0',
|
||||
borderRadius: '8px',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
})}
|
||||
>
|
||||
{group.icon && group.icon?.trim().startsWith('http') && (
|
||||
{group?.icon?.trim().startsWith('http') && (
|
||||
<img
|
||||
alt="group icon"
|
||||
src={iconCachePath === '' ? group.icon : iconCachePath}
|
||||
width="32px"
|
||||
style={{
|
||||
@@ -72,8 +52,9 @@ export const GroupItem = (props: Props) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{group.icon && group.icon?.trim().startsWith('data') && (
|
||||
{group?.icon?.trim().startsWith('data') && (
|
||||
<img
|
||||
alt="group icon"
|
||||
src={group.icon}
|
||||
width="32px"
|
||||
style={{
|
||||
@@ -82,8 +63,9 @@ export const GroupItem = (props: Props) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{group.icon && group.icon?.trim().startsWith('<svg') && (
|
||||
{group?.icon?.trim().startsWith('<svg') && (
|
||||
<img
|
||||
alt="group icon"
|
||||
src={`data:image/svg+xml;base64,${btoa(group.icon ?? '')}`}
|
||||
width="32px"
|
||||
style={{
|
||||
@@ -93,10 +75,8 @@ export const GroupItem = (props: Props) => {
|
||||
/>
|
||||
)}
|
||||
<ListItemText
|
||||
{...(dragAttributes ?? {})}
|
||||
{...(dragListeners ?? {})}
|
||||
ref={dragNodeRef}
|
||||
sx={{ cursor: sortable ? 'move' : '' }}
|
||||
data-sortable-handle
|
||||
sx={{ cursor: isSortable ? 'move' : undefined }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
sx={{ textDecoration: type === 'delete' ? 'line-through' : '' }}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -51,6 +46,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
@@ -141,6 +137,16 @@ const buildGroupsYaml = (
|
||||
)
|
||||
}
|
||||
|
||||
const findRealIndex = (
|
||||
list: IProxyGroupConfig[],
|
||||
filtered: IProxyGroupConfig[],
|
||||
filteredIndex: number,
|
||||
): number => {
|
||||
const item = filtered[filteredIndex]
|
||||
if (!item) return -1
|
||||
return list.findIndex((group) => group.name === item.name)
|
||||
}
|
||||
|
||||
export const GroupsEditorViewer = (props: Props) => {
|
||||
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
||||
props
|
||||
@@ -201,20 +207,17 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredPrependSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
key={item.name}
|
||||
type="prepend"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
@@ -223,109 +226,138 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredGroupList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<GroupItem
|
||||
key={filteredGroupList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
group={filteredGroupList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredGroupList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredGroupList[newIndex].name),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredGroupList[newIndex].name,
|
||||
])
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<GroupItem
|
||||
key={filteredGroupList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
group={filteredGroupList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredGroupList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredGroupList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredGroupList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredAppendSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
key={item.name}
|
||||
type="append"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
prependSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
appendSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
setAppendSeq(arrayMove(appendSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
|
||||
// prevent drag over if source and target are in different groups
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
@@ -756,7 +788,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -779,7 +811,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
@@ -807,7 +839,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
@@ -839,7 +871,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -882,7 +914,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -1120,16 +1152,22 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<VirtualList
|
||||
count={
|
||||
filteredGroupList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredGroupList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
DraggableAttributes,
|
||||
DraggableSyntheticListeners,
|
||||
} from '@dnd-kit/core'
|
||||
import { useSortable } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
@@ -58,6 +55,8 @@ const round = keyframes`
|
||||
`
|
||||
|
||||
export interface ProfileItemProps {
|
||||
id: string
|
||||
index: number
|
||||
selected: boolean
|
||||
activating: boolean
|
||||
itemData: IProfileItem
|
||||
@@ -71,13 +70,12 @@ export interface ProfileItemProps {
|
||||
onSelectionChange?: () => void
|
||||
timerUpdateRevision: number
|
||||
completedUpdateRevision: number
|
||||
dragHandleRef: (node: HTMLElement | null) => void
|
||||
dragHandleAttributes: DraggableAttributes
|
||||
dragHandleListeners: DraggableSyntheticListeners
|
||||
}
|
||||
|
||||
const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const {
|
||||
id,
|
||||
index,
|
||||
selected,
|
||||
activating,
|
||||
itemData,
|
||||
@@ -91,11 +89,17 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
onSelectionChange,
|
||||
timerUpdateRevision,
|
||||
completedUpdateRevision,
|
||||
dragHandleRef,
|
||||
dragHandleAttributes,
|
||||
dragHandleListeners,
|
||||
} = props
|
||||
|
||||
const [element, setElement] = useState<Element | null>(null)
|
||||
const handleRef = useRef<HTMLButtonElement | null>(null)
|
||||
useSortable({
|
||||
id,
|
||||
index,
|
||||
element,
|
||||
handle: handleRef,
|
||||
})
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||
@@ -616,7 +620,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Box ref={setElement} sx={{ position: 'relative', borderRadius: '8px' }}>
|
||||
<ProfileBox
|
||||
aria-selected={selected}
|
||||
onClick={(e) => {
|
||||
@@ -678,14 +682,12 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
</IconButton>
|
||||
)}
|
||||
<Box
|
||||
ref={dragHandleRef}
|
||||
ref={handleRef}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
margin: 'auto 0',
|
||||
...(batchMode && { marginLeft: '-4px' }),
|
||||
}}
|
||||
{...dragHandleAttributes}
|
||||
{...dragHandleListeners}
|
||||
>
|
||||
<DragIndicatorRounded
|
||||
sx={[
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -40,7 +35,12 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseSearchBox, MonacoEditor, VirtualList } from '@/components/base'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
@@ -58,6 +58,24 @@ interface Props {
|
||||
onSave?: (prev?: string, curr?: string) => void
|
||||
}
|
||||
|
||||
const findRealIndex = (
|
||||
list: IProxyConfig[],
|
||||
filtered: IProxyConfig[],
|
||||
filteredIndex: number,
|
||||
): number => {
|
||||
const item = filtered[filteredIndex]
|
||||
if (!item) return -1
|
||||
return list.findIndex((proxy) => proxy.name === item.name)
|
||||
}
|
||||
|
||||
// 节点的 name 会被用作 sortable item id、React key 以及拖拽排序的
|
||||
// 依据。当 name 为空/null(例如高级模式下粘贴了缺少 name 的节点)时,
|
||||
// 无效 name 不能作为 sortable item id,否则会导致拖拽注册失败。
|
||||
// 这里统一过滤掉没有有效 name 的节点,避免可视化编辑页崩溃;原始 YAML
|
||||
// 数据仍然保留,用户可在高级(文本)模式中查看并修正这些节点。
|
||||
const hasValidName = (proxy: IProxyConfig) =>
|
||||
typeof proxy?.name === 'string' && proxy.name.length > 0
|
||||
|
||||
export const ProxiesEditorViewer = (props: Props) => {
|
||||
const { profileUid, property, open, onClose, onSave } = props
|
||||
const { t } = useTranslation()
|
||||
@@ -75,14 +93,6 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const [deleteSeq, setDeleteSeq] = useState<string[]>([])
|
||||
const hasLoadedSeqConfigRef = useRef(false)
|
||||
|
||||
// 节点的 name 会被用作 SortableContext 的 item id、React key 以及拖拽排序的
|
||||
// 依据。当 name 为空/null(例如高级模式下粘贴了缺少 name 的节点)时,
|
||||
// @dnd-kit 的 SortableContext 会对 null 执行 `'id' in item` 从而崩溃
|
||||
// (Cannot use 'in' operator to search for 'id' in null)。
|
||||
// 这里统一过滤掉没有有效 name 的节点,避免可视化编辑页崩溃;原始 YAML
|
||||
// 数据仍然保留,用户可在高级(文本)模式中查看并修正这些节点。
|
||||
const hasValidName = (proxy: IProxyConfig) =>
|
||||
typeof proxy?.name === 'string' && proxy.name.length > 0
|
||||
const filteredPrependSeq = useMemo(
|
||||
() =>
|
||||
prependSeq.filter((proxy) => hasValidName(proxy) && match(proxy.name)),
|
||||
@@ -101,20 +111,17 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredPrependSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
key={item.name}
|
||||
type="prepend"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
@@ -123,109 +130,135 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredProxyList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<ProxyItem
|
||||
key={filteredProxyList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
proxy={filteredProxyList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredProxyList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredProxyList[newIndex].name),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredProxyList[newIndex].name,
|
||||
])
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<ProxyItem
|
||||
key={filteredProxyList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
proxy={filteredProxyList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredProxyList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredProxyList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredProxyList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredAppendSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
key={item.name}
|
||||
type="append"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
prependSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
appendSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
setAppendSeq(arrayMove(appendSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
// 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState
|
||||
@@ -489,16 +522,22 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<VirtualList
|
||||
count={
|
||||
filteredProxyList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredProxyList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { DeleteForeverRounded, UndoRounded } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
@@ -18,22 +16,7 @@ interface Props {
|
||||
|
||||
export const ProxyItem = (props: Props) => {
|
||||
const { type, proxy, onDelete } = props
|
||||
const sortable = type === 'prepend' || type === 'append'
|
||||
|
||||
const {
|
||||
attributes: sortableAttributes,
|
||||
listeners: sortableListeners,
|
||||
setNodeRef: sortableSetNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: proxy.name,
|
||||
disabled: !sortable,
|
||||
})
|
||||
const dragAttributes = sortable ? sortableAttributes : undefined
|
||||
const dragListeners = sortable ? sortableListeners : undefined
|
||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined
|
||||
const isSortable = type === 'prepend' || type === 'append'
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -49,18 +32,12 @@ export const ProxyItem = (props: Props) => {
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: '100%',
|
||||
margin: '8px 0',
|
||||
borderRadius: '8px',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
})}
|
||||
>
|
||||
<ListItemText
|
||||
{...(dragAttributes ?? {})}
|
||||
{...(dragListeners ?? {})}
|
||||
ref={dragNodeRef}
|
||||
sx={{ cursor: sortable ? 'move' : '' }}
|
||||
data-sortable-handle
|
||||
sx={{ cursor: isSortable ? 'move' : undefined }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
title={proxy.name}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
DeleteForeverRounded,
|
||||
UndoRounded,
|
||||
@@ -14,6 +12,7 @@ import {
|
||||
alpha,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
|
||||
interface Props {
|
||||
type: 'prepend' | 'original' | 'delete' | 'append'
|
||||
ruleRaw: string
|
||||
@@ -24,32 +23,13 @@ interface Props {
|
||||
|
||||
export const RuleItem = (props: Props) => {
|
||||
const { type, ruleRaw, onDelete, onPrepend, onAppend } = props
|
||||
const sortable = type === 'prepend' || type === 'append'
|
||||
const isSortable = type === 'prepend' || type === 'append'
|
||||
const rule = ruleRaw.replace(',no-resolve', '')
|
||||
|
||||
const ruleType = rule.match(/^[^,]+/)?.[0] ?? ''
|
||||
const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? ''
|
||||
const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1)
|
||||
|
||||
const $sortable = useSortable({ id: ruleRaw })
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = sortable
|
||||
? $sortable
|
||||
: {
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: null,
|
||||
transform: null,
|
||||
transition: null,
|
||||
isDragging: false,
|
||||
}
|
||||
return (
|
||||
<ListItem
|
||||
dense
|
||||
@@ -64,18 +44,12 @@ export const RuleItem = (props: Props) => {
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: '100%',
|
||||
margin: '8px 0',
|
||||
borderRadius: '8px',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
})}
|
||||
>
|
||||
<ListItemText
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
ref={setNodeRef}
|
||||
sx={{ cursor: sortable ? 'move' : '' }}
|
||||
data-sortable-handle
|
||||
sx={{ cursor: isSortable ? 'move' : undefined }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
title={ruleContent || '-'}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -41,6 +39,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
@@ -252,6 +251,16 @@ const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
|
||||
{} as Record<string, TranslationKey>,
|
||||
)
|
||||
|
||||
const findRealIndex = (
|
||||
list: string[],
|
||||
filtered: string[],
|
||||
filteredIndex: number,
|
||||
): number => {
|
||||
const item = filtered[filteredIndex]
|
||||
if (item === undefined) return -1
|
||||
return list.indexOf(item)
|
||||
}
|
||||
|
||||
export const RulesEditorViewer = (props: Props) => {
|
||||
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
|
||||
props
|
||||
@@ -296,20 +305,17 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredPrependSeq.map((x) => {
|
||||
return x
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`prepend:${item}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
key={item}
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
@@ -322,49 +328,48 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredRuleList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<RuleItem
|
||||
key={filteredRuleList[newIndex]}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
ruleRaw={filteredRuleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<RuleItem
|
||||
key={filteredRuleList[newIndex]}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
ruleRaw={filteredRuleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredAppendSeq.map((x) => {
|
||||
return x
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`append:${item}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
key={item}
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
@@ -377,22 +382,14 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
const reorder = (list: string[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list)
|
||||
const [removed] = result.splice(startIndex, 1)
|
||||
@@ -400,23 +397,73 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return result
|
||||
}
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
const activeIndex = prependSeq.indexOf(active.id.toString())
|
||||
const overIndex = prependSeq.indexOf(over.id.toString())
|
||||
setPrependSeq(reorder(prependSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(reorder(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
const activeIndex = appendSeq.indexOf(active.id.toString())
|
||||
const overIndex = appendSeq.indexOf(over.id.toString())
|
||||
setAppendSeq(reorder(appendSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(reorder(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
@@ -806,16 +853,22 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<VirtualList
|
||||
count={
|
||||
filteredRuleList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredRuleList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
import { ProfileItem, type ProfileItemProps } from './profile-item'
|
||||
|
||||
const SORT_TRANSITION = 'transform 160ms cubic-bezier(0.2, 0, 0, 1)'
|
||||
|
||||
type SortableProfileItemProps = Omit<
|
||||
ProfileItemProps,
|
||||
'dragHandleRef' | 'dragHandleAttributes' | 'dragHandleListeners'
|
||||
> & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const SortableProfileItem = ({
|
||||
id,
|
||||
...profileItemProps
|
||||
}: SortableProfileItemProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setActivatorNodeRef,
|
||||
setNodeRef,
|
||||
transform,
|
||||
isSorting,
|
||||
isDragging,
|
||||
isOver,
|
||||
} = useSortable({ id, transition: null })
|
||||
|
||||
const isDiagonalMove =
|
||||
transform !== null && transform.x !== 0 && transform.y !== 0
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition:
|
||||
isSorting && !isDragging && isOver && !isDiagonalMove
|
||||
? SORT_TRANSITION
|
||||
: undefined,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
}}
|
||||
>
|
||||
<ProfileItem
|
||||
{...profileItemProps}
|
||||
dragHandleRef={setActivatorNodeRef}
|
||||
dragHandleAttributes={attributes}
|
||||
dragHandleListeners={listeners}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user