optimize table config

This commit is contained in:
jeffvli
2025-11-14 00:58:48 -08:00
parent e82c1d3a20
commit 6d6caa0406
@@ -12,7 +12,7 @@ import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/elem
import { useDebouncedState } from '@mantine/hooks'; import { useDebouncedState } from '@mantine/hooks';
import clsx from 'clsx'; import clsx from 'clsx';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import styles from './table-config.module.css'; import styles from './table-config.module.css';
@@ -498,274 +498,286 @@ const TableColumnConfig = ({
}; };
const DragHandle = ({ dragHandleRef }: { dragHandleRef: React.RefObject<HTMLButtonElement> }) => { const DragHandle = ({ dragHandleRef }: { dragHandleRef: React.RefObject<HTMLButtonElement> }) => {
const { t } = useTranslation();
return ( return (
<ActionIcon <ActionIcon
icon="dragVertical" icon="dragHorizontal"
iconProps={{ iconProps={{
size: 'md', size: 'md',
}} }}
ref={dragHandleRef} ref={dragHandleRef}
size="xs" size="xs"
style={{ cursor: 'grab' }} style={{ cursor: 'grab' }}
tooltip={{
label: t('table.config.general.dragToReorder', {
postProcess: 'sentenceCase',
}),
}}
variant="transparent" variant="transparent"
/> />
); );
}; };
const TableColumnItem = ({ const TableColumnItem = memo(
handleAlignCenter, ({
handleAlignLeft, handleAlignCenter,
handleAlignRight, handleAlignLeft,
handleAutoSize, handleAlignRight,
handleChangeEnabled, handleAutoSize,
handleMoveDown, handleChangeEnabled,
handleMoveUp, handleMoveDown,
handlePinToLeft, handleMoveUp,
handlePinToRight, handlePinToLeft,
handleReorder, handlePinToRight,
handleRowWidth, handleReorder,
item, handleRowWidth,
label, item,
matches, label,
}: { matches,
handleAlignCenter: (item: ItemTableListColumnConfig) => void; }: {
handleAlignLeft: (item: ItemTableListColumnConfig) => void; handleAlignCenter: (item: ItemTableListColumnConfig) => void;
handleAlignRight: (item: ItemTableListColumnConfig) => void; handleAlignLeft: (item: ItemTableListColumnConfig) => void;
handleAutoSize: (item: ItemTableListColumnConfig, checked: boolean) => void; handleAlignRight: (item: ItemTableListColumnConfig) => void;
handleChangeEnabled: (item: ItemTableListColumnConfig, checked: boolean) => void; handleAutoSize: (item: ItemTableListColumnConfig, checked: boolean) => void;
handleMoveDown: (item: ItemTableListColumnConfig) => void; handleChangeEnabled: (item: ItemTableListColumnConfig, checked: boolean) => void;
handleMoveUp: (item: ItemTableListColumnConfig) => void; handleMoveDown: (item: ItemTableListColumnConfig) => void;
handlePinToLeft: (item: ItemTableListColumnConfig) => void; handleMoveUp: (item: ItemTableListColumnConfig) => void;
handlePinToRight: (item: ItemTableListColumnConfig) => void; handlePinToLeft: (item: ItemTableListColumnConfig) => void;
handleReorder: (idFrom: string, idTo: string, edge: Edge | null) => void; handlePinToRight: (item: ItemTableListColumnConfig) => void;
handleRowWidth: (item: ItemTableListColumnConfig, number: number | string) => void; handleReorder: (idFrom: string, idTo: string, edge: Edge | null) => void;
item: ItemTableListColumnConfig; handleRowWidth: (item: ItemTableListColumnConfig, number: number | string) => void;
label: string; item: ItemTableListColumnConfig;
matches: null | readonly Fuse.FuseResultMatch[]; label: string;
}) => { matches: null | readonly Fuse.FuseResultMatch[];
const { t } = useTranslation(); }) => {
const ref = useRef<HTMLDivElement>(null); const { t } = useTranslation();
const dragHandleRef = useRef<HTMLButtonElement>(null); const ref = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false); const dragHandleRef = useRef<HTMLButtonElement>(null);
const [isDraggedOver, setIsDraggedOver] = useState<Edge | null>(null); const [isDragging, setIsDragging] = useState(false);
const [isDraggedOver, setIsDraggedOver] = useState<Edge | null>(null);
useEffect(() => { useEffect(() => {
if (!ref.current || !dragHandleRef.current) { if (!ref.current || !dragHandleRef.current) {
return; return;
} }
return combine( return combine(
draggable({ draggable({
element: dragHandleRef.current, element: dragHandleRef.current,
getInitialData: () => { getInitialData: () => {
const data = dndUtils.generateDragData({ const data = dndUtils.generateDragData({
id: [item.id], id: [item.id],
operation: [DragOperation.REORDER], operation: [DragOperation.REORDER],
type: DragTarget.TABLE_COLUMN, type: DragTarget.TABLE_COLUMN,
}); });
return data; return data;
}, },
onDragStart: () => { onDragStart: () => {
setIsDragging(true); setIsDragging(true);
}, },
onDrop: () => { onDrop: () => {
setIsDragging(false); setIsDragging(false);
}, },
onGenerateDragPreview: (data) => { onGenerateDragPreview: (data) => {
disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage }); disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage });
}, },
}), }),
dropTargetForElements({ dropTargetForElements({
canDrop: (args) => { canDrop: (args) => {
const data = args.source.data as unknown as DragData; const data = args.source.data as unknown as DragData;
const isSelf = (args.source.data.id as string[])[0] === item.id; const isSelf = (args.source.data.id as string[])[0] === item.id;
return dndUtils.isDropTarget(data.type, [DragTarget.TABLE_COLUMN]) && !isSelf; return (
}, dndUtils.isDropTarget(data.type, [DragTarget.TABLE_COLUMN]) && !isSelf
element: ref.current, );
getData: ({ element, input }) => { },
const data = dndUtils.generateDragData({ element: ref.current,
id: [item.id], getData: ({ element, input }) => {
operation: [DragOperation.REORDER], const data = dndUtils.generateDragData({
type: DragTarget.TABLE_COLUMN, id: [item.id],
}); operation: [DragOperation.REORDER],
type: DragTarget.TABLE_COLUMN,
});
return attachClosestEdge(data, { return attachClosestEdge(data, {
allowedEdges: ['top', 'bottom'], allowedEdges: ['top', 'bottom'],
element, element,
input, input,
}); });
}, },
onDrag: (args) => { onDrag: (args) => {
const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
setIsDraggedOver(closestEdgeOfTarget); setIsDraggedOver(closestEdgeOfTarget);
}, },
onDragLeave: () => { onDragLeave: () => {
setIsDraggedOver(null); setIsDraggedOver(null);
}, },
onDrop: (args) => { onDrop: (args) => {
const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
const from = args.source.data.id as string[]; const from = args.source.data.id as string[];
const to = args.self.data.id as string[]; const to = args.self.data.id as string[];
handleReorder(from[0], to[0], closestEdgeOfTarget); handleReorder(from[0], to[0], closestEdgeOfTarget);
setIsDraggedOver(null); setIsDraggedOver(null);
}, },
}), }),
);
}, [item.id, handleReorder]);
return (
<div
className={clsx(styles.item, {
[styles.draggedOverBottom]: isDraggedOver === 'bottom',
[styles.draggedOverTop]: isDraggedOver === 'top',
[styles.dragging]: isDragging,
[styles.matched]: matches && matches.length > 0,
})}
ref={ref}
>
<Group wrap="nowrap">
<DragHandle dragHandleRef={dragHandleRef} />
<Checkbox
checked={item.isEnabled}
id={item.id}
label={label}
onChange={(e) => handleChangeEnabled(item, e.currentTarget.checked)}
size="sm"
/>
</Group>
<Group wrap="nowrap">
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="arrowUp"
iconProps={{ size: 'md' }}
onClick={() => handleMoveUp(item)}
size="xs"
tooltip={{
label: t('table.config.general.moveUp', {
postProcess: 'sentenceCase',
}),
}}
variant="subtle"
/>
<ActionIcon
icon="arrowDown"
iconProps={{ size: 'md' }}
onClick={() => handleMoveDown(item)}
size="xs"
tooltip={{
label: t('table.config.general.moveDown', {
postProcess: 'sentenceCase',
}),
}}
variant="subtle"
/>
</ActionIconGroup>
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="arrowLeftToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToLeft(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToLeft', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'left' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="arrowRightToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToRight(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToRight', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'right' ? 'filled' : 'subtle'}
/>
</ActionIconGroup>
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="alignLeft"
iconProps={{ size: 'md' }}
onClick={() => handleAlignLeft(item)}
size="xs"
tooltip={{
label: t('table.config.general.alignLeft', {
postProcess: 'sentenceCase',
}),
}}
variant={item.align === 'start' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="alignCenter"
iconProps={{ size: 'md' }}
onClick={() => handleAlignCenter(item)}
size="xs"
tooltip={{
label: t('table.config.general.alignCenter', {
postProcess: 'sentenceCase',
}),
}}
variant={item.align === 'center' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="alignRight"
iconProps={{ size: 'md' }}
onClick={() => handleAlignRight(item)}
size="xs"
tooltip={{
label: t('table.config.general.alignRight', {
postProcess: 'sentenceCase',
}),
}}
variant={item.align === 'end' ? 'filled' : 'subtle'}
/>
</ActionIconGroup>
<NumberInput
className={clsx(styles.group, styles.numberInput)}
hideControls={false}
leftSection={
<>
{item.pinned === null && (
<Tooltip
label={t('table.config.general.autosize', {
postProcess: 'sentenceCase',
})}
>
<Checkbox
checked={item.autoSize}
disabled={item.pinned !== null}
id={item.id}
onChange={(e) =>
handleAutoSize(item, e.currentTarget.checked)
}
size="xs"
/>
</Tooltip>
)}
</>
}
max={2000}
min={0}
onChange={(value) => handleRowWidth(item, value)}
size="xs"
step={10}
stepHoldDelay={300}
stepHoldInterval={100}
value={item.width}
variant="subtle"
/>
</Group>
</div>
); );
}, [item.id, handleReorder]); },
(prevProps, nextProps) => {
return ( // Custom comparison function for better memoization
<div return (
className={clsx(styles.item, { prevProps.item.id === nextProps.item.id &&
[styles.draggedOverBottom]: isDraggedOver === 'bottom', prevProps.item.isEnabled === nextProps.item.isEnabled &&
[styles.draggedOverTop]: isDraggedOver === 'top', prevProps.item.autoSize === nextProps.item.autoSize &&
[styles.dragging]: isDragging, prevProps.item.width === nextProps.item.width &&
[styles.matched]: matches && matches.length > 0, prevProps.item.pinned === nextProps.item.pinned &&
})} prevProps.item.align === nextProps.item.align &&
ref={ref} prevProps.label === nextProps.label &&
> prevProps.matches === nextProps.matches
<Group wrap="nowrap"> );
<DragHandle dragHandleRef={dragHandleRef} /> },
<Checkbox );
checked={item.isEnabled}
id={item.id}
label={label}
onChange={(e) => handleChangeEnabled(item, e.currentTarget.checked)}
size="sm"
/>
</Group>
<Group wrap="nowrap">
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="arrowUp"
iconProps={{ size: 'md' }}
onClick={() => handleMoveUp(item)}
size="xs"
tooltip={{
label: t('table.config.general.moveUp', {
postProcess: 'sentenceCase',
}),
}}
variant="subtle"
/>
<ActionIcon
icon="arrowDown"
iconProps={{ size: 'md' }}
onClick={() => handleMoveDown(item)}
size="xs"
tooltip={{
label: t('table.config.general.moveDown', {
postProcess: 'sentenceCase',
}),
}}
variant="subtle"
/>
</ActionIconGroup>
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="arrowLeftToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToLeft(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToLeft', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'left' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="arrowRightToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToRight(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToRight', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'right' ? 'filled' : 'subtle'}
/>
</ActionIconGroup>
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="alignLeft"
iconProps={{ size: 'md' }}
onClick={() => handleAlignLeft(item)}
size="xs"
tooltip={{
label: t('table.config.general.alignLeft', {
postProcess: 'sentenceCase',
}),
}}
variant={item.align === 'start' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="alignCenter"
iconProps={{ size: 'md' }}
onClick={() => handleAlignCenter(item)}
size="xs"
tooltip={{
label: t('table.config.general.alignCenter', {
postProcess: 'sentenceCase',
}),
}}
variant={item.align === 'center' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="alignRight"
iconProps={{ size: 'md' }}
onClick={() => handleAlignRight(item)}
size="xs"
tooltip={{
label: t('table.config.general.alignRight', {
postProcess: 'sentenceCase',
}),
}}
variant={item.align === 'end' ? 'filled' : 'subtle'}
/>
</ActionIconGroup>
<NumberInput
className={clsx(styles.group, styles.numberInput)}
hideControls={false}
leftSection={
<>
{item.pinned === null && (
<Tooltip
label={t('table.config.general.autosize', {
postProcess: 'sentenceCase',
})}
>
<Checkbox
checked={item.autoSize}
disabled={item.pinned !== null}
id={item.id}
onChange={(e) =>
handleAutoSize(item, e.currentTarget.checked)
}
size="xs"
/>
</Tooltip>
)}
</>
}
max={2000}
min={0}
onChange={(value) => handleRowWidth(item, value)}
size="xs"
step={10}
value={item.width}
variant="subtle"
/>
</Group>
</div>
);
};