mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 10:42:09 +08:00
* feat(connections): enable persistent column visibility and ordering fix(connections): smooth column manager access in table view fix(connections): integrate column manager into column menu - Move the Connections column manager trigger into the DataGrid column menu, reusing existing i18n strings - Clean up the page toolbar by removing the standalone button and lifting dialog state to the parent page fix(connections): unify column order handling and resolve lint warnings fix(connections): unify column order handling and enforce 0 ESLint warnings - extract reconcileColumnOrder helper and reuse it across: - initial normalization (useEffect) - manager drag order handler (handleManagerOrderChange) - derive columnOptions directly from the computed, sorted columns to keep a single source of truth and prevent dialog/grid drift - connections.tsx: remove direct setState in useEffect; gate dialog open with `isTableLayout && isColumnManagerOpen`; clean up unused imports - connection-column-manager.tsx: replace deprecated `primaryTypographyProps` with `slotProps` - run full lint; project now has 0 warnings on main configuration * feat(connection-table): safeguard column visibility --------- Co-authored-by: Slinetrac <realakayuki@gmail.com>
192 lines
4.7 KiB
TypeScript
192 lines
4.7 KiB
TypeScript
import {
|
|
closestCenter,
|
|
DndContext,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
type DragEndEvent,
|
|
} from "@dnd-kit/core";
|
|
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
|
|
import { CSS } from "@dnd-kit/utilities";
|
|
import { DragIndicatorRounded } from "@mui/icons-material";
|
|
import {
|
|
Button,
|
|
Checkbox,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
IconButton,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
} from "@mui/material";
|
|
import { useCallback, useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
interface ColumnOption {
|
|
field: string;
|
|
label: string;
|
|
visible: boolean;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
columns: ColumnOption[];
|
|
onClose: () => void;
|
|
onToggle: (field: string, visible: boolean) => void;
|
|
onOrderChange: (order: string[]) => void;
|
|
onReset: () => void;
|
|
}
|
|
|
|
export const ConnectionColumnManager = ({
|
|
open,
|
|
columns,
|
|
onClose,
|
|
onToggle,
|
|
onOrderChange,
|
|
onReset,
|
|
}: Props) => {
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: { distance: 6 },
|
|
}),
|
|
);
|
|
const { t } = useTranslation();
|
|
|
|
const items = useMemo(() => columns.map((column) => column.field), [columns]);
|
|
const visibleCount = useMemo(
|
|
() => columns.filter((column) => column.visible).length,
|
|
[columns],
|
|
);
|
|
|
|
const handleDragEnd = useCallback(
|
|
(event: DragEndEvent) => {
|
|
const { active, over } = event;
|
|
if (!over || active.id === over.id) return;
|
|
|
|
const order = columns.map((column) => column.field);
|
|
const oldIndex = order.indexOf(active.id as string);
|
|
const newIndex = order.indexOf(over.id as string);
|
|
if (oldIndex === -1 || newIndex === -1) return;
|
|
|
|
onOrderChange(arrayMove(order, oldIndex, newIndex));
|
|
},
|
|
[columns, onOrderChange],
|
|
);
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
|
<DialogTitle>
|
|
{t("connections.components.columnManager.title")}
|
|
</DialogTitle>
|
|
<DialogContent sx={{ pt: 1 }}>
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<SortableContext items={items}>
|
|
<List
|
|
dense
|
|
disablePadding
|
|
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
|
|
>
|
|
{columns.map((column) => (
|
|
<SortableColumnItem
|
|
key={column.field}
|
|
column={column}
|
|
onToggle={onToggle}
|
|
dragHandleLabel={t(
|
|
"connections.components.columnManager.dragHandle",
|
|
)}
|
|
disableToggle={column.visible && visibleCount <= 1}
|
|
/>
|
|
))}
|
|
</List>
|
|
</SortableContext>
|
|
</DndContext>
|
|
</DialogContent>
|
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
|
<Button variant="text" onClick={onReset}>
|
|
{t("shared.actions.resetToDefault")}
|
|
</Button>
|
|
<Button variant="contained" onClick={onClose}>
|
|
{t("shared.actions.close")}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
interface SortableColumnItemProps {
|
|
column: ColumnOption;
|
|
onToggle: (field: string, visible: boolean) => void;
|
|
dragHandleLabel: string;
|
|
disableToggle?: boolean;
|
|
}
|
|
|
|
const SortableColumnItem = ({
|
|
column,
|
|
onToggle,
|
|
dragHandleLabel,
|
|
disableToggle = false,
|
|
}: SortableColumnItemProps) => {
|
|
const {
|
|
attributes,
|
|
listeners,
|
|
setNodeRef,
|
|
transform,
|
|
transition,
|
|
isDragging,
|
|
} = useSortable({ id: column.field });
|
|
|
|
const style = useMemo(
|
|
() => ({
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
}),
|
|
[transform, transition],
|
|
);
|
|
|
|
return (
|
|
<ListItem
|
|
ref={setNodeRef}
|
|
disableGutters
|
|
sx={{
|
|
px: 1,
|
|
py: 0.5,
|
|
borderRadius: 1,
|
|
border: (theme) => `1px solid ${theme.palette.divider}`,
|
|
backgroundColor: isDragging ? "action.hover" : "transparent",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 1,
|
|
}}
|
|
style={style}
|
|
>
|
|
<Checkbox
|
|
edge="start"
|
|
checked={column.visible}
|
|
disabled={disableToggle}
|
|
onChange={(event) => onToggle(column.field, event.target.checked)}
|
|
/>
|
|
<ListItemText
|
|
primary={column.label}
|
|
slotProps={{ primary: { variant: "body2" } }}
|
|
sx={{ mr: 1 }}
|
|
/>
|
|
<IconButton
|
|
edge="end"
|
|
size="small"
|
|
sx={{ cursor: isDragging ? "grabbing" : "grab" }}
|
|
aria-label={dragHandleLabel}
|
|
{...attributes}
|
|
{...listeners}
|
|
>
|
|
<DragIndicatorRounded fontSize="small" />
|
|
</IconButton>
|
|
</ListItem>
|
|
);
|
|
};
|