Files
clash-verge-rev/src/components/connection/connection-column-manager.tsx
T
XiaoBuHalyandSlinetrac af4463fef1 feat(connections): enable persistent column visibility and ordering #5235 (#5249)
* 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>
2025-11-09 00:15:01 +08:00

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>
);
};