mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-02 08:39:55 +02:00
aa3c9251f5
* Created a new album group configuration which includes (for now) an option to set the image size of the album group artwork.
856 lines
34 KiB
TypeScript
856 lines
34 KiB
TypeScript
import {
|
|
attachClosestEdge,
|
|
type Edge,
|
|
extractClosestEdge,
|
|
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
|
import {
|
|
draggable,
|
|
dropTargetForElements,
|
|
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';
|
|
import clsx from 'clsx';
|
|
import Fuse, { type FuseResultMatch } from 'fuse.js';
|
|
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import styles from './table-config.module.css';
|
|
|
|
import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
|
|
import {
|
|
ListConfigBooleanControl,
|
|
ListConfigTable,
|
|
} from '/@/renderer/features/shared/components/list-config-menu';
|
|
import {
|
|
type DataTableProps,
|
|
ItemListSettings,
|
|
useSettingsStore,
|
|
useSettingsStoreActions,
|
|
} from '/@/renderer/store';
|
|
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
|
import { Badge } from '/@/shared/components/badge/badge';
|
|
import { Button } from '/@/shared/components/button/button';
|
|
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
|
import { Divider } from '/@/shared/components/divider/divider';
|
|
import { Group } from '/@/shared/components/group/group';
|
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
|
import { Slider } from '/@/shared/components/slider/slider';
|
|
import { Stack } from '/@/shared/components/stack/stack';
|
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
|
import { Text } from '/@/shared/components/text/text';
|
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
|
import { useDebouncedState } from '/@/shared/hooks/use-debounced-state';
|
|
import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
|
|
import { ItemListKey, ListPaginationType, TableColumn } from '/@/shared/types/types';
|
|
|
|
interface TableConfigProps {
|
|
enablePinColumnButtons?: boolean;
|
|
extraOptions?: {
|
|
component: React.ReactNode;
|
|
id: string;
|
|
label: string;
|
|
}[];
|
|
listKey: ItemListKey;
|
|
optionsConfig?: {
|
|
[key: string]: {
|
|
disabled?: boolean;
|
|
hidden?: boolean;
|
|
};
|
|
};
|
|
tableColumnsData: { label: string; value: string }[];
|
|
tableKey?: 'detail' | 'main';
|
|
}
|
|
|
|
export const TableConfig = ({
|
|
enablePinColumnButtons = true,
|
|
extraOptions,
|
|
listKey,
|
|
optionsConfig,
|
|
tableColumnsData,
|
|
tableKey = 'main',
|
|
}: TableConfigProps) => {
|
|
const { t } = useTranslation();
|
|
|
|
const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings;
|
|
const albumGroupImageSize = useSettingsStore((state) => state.general.albumGroupImageSize);
|
|
const imageResTable = useSettingsStore((state) => state.general.imageRes.table);
|
|
const { setList, setSettings } = useSettingsStoreActions();
|
|
const [albumGroupOpen, setAlbumGroupOpen] = useState(false);
|
|
|
|
const table = tableKey === 'detail' ? (list?.detail ?? list?.table) : list?.table;
|
|
|
|
const hasAlbumGroupColumn = useMemo(
|
|
() => table.columns.some((column) => column.id === TableColumn.ALBUM_GROUP),
|
|
[table.columns],
|
|
);
|
|
|
|
const setTableUpdate = useCallback(
|
|
(patch: Partial<DataTableProps>) => {
|
|
if (tableKey === 'detail') {
|
|
setList(listKey, { detail: patch } as Parameters<
|
|
ReturnType<typeof useSettingsStoreActions>['setList']
|
|
>[1]);
|
|
} else {
|
|
setList(listKey, { table: patch });
|
|
}
|
|
},
|
|
[listKey, setList, tableKey],
|
|
);
|
|
|
|
const advancedSettings = useMemo(() => {
|
|
const albumGroupOptions =
|
|
hasAlbumGroupColumn && tableKey === 'main'
|
|
? [
|
|
{
|
|
component: (
|
|
<Group justify="flex-end" w="100%">
|
|
<Button
|
|
onClick={() => setAlbumGroupOpen((prev) => !prev)}
|
|
size="compact-md"
|
|
variant={albumGroupOpen ? 'subtle' : 'filled'}
|
|
>
|
|
{t(albumGroupOpen ? 'common.close' : 'common.edit')}
|
|
</Button>
|
|
</Group>
|
|
),
|
|
id: 'albumGroupConfig',
|
|
label: t('table.config.general.albumGroupConfig'),
|
|
},
|
|
...(albumGroupOpen
|
|
? [
|
|
{
|
|
component: (
|
|
<Group justify="flex-end" w="100%">
|
|
<NumberInput
|
|
max={2000}
|
|
min={0}
|
|
onChange={(value) => {
|
|
const size = Math.max(
|
|
0,
|
|
Math.min(
|
|
2000,
|
|
typeof value === 'number' ? value : 0,
|
|
),
|
|
);
|
|
setSettings({
|
|
general: {
|
|
albumGroupImageSize: size,
|
|
// Source table art must be at least as
|
|
// large as the displayed album image.
|
|
...(size >= imageResTable
|
|
? { imageRes: { table: size } }
|
|
: {}),
|
|
},
|
|
});
|
|
}}
|
|
rightSection={
|
|
<Text isMuted isNoSelect pr="lg" size="sm">
|
|
px
|
|
</Text>
|
|
}
|
|
value={albumGroupImageSize}
|
|
width={90}
|
|
/>
|
|
</Group>
|
|
),
|
|
id: 'albumImageSize',
|
|
label: (
|
|
<Text pl="md">
|
|
{t('table.config.general.albumImageSize')}
|
|
</Text>
|
|
),
|
|
},
|
|
]
|
|
: []),
|
|
]
|
|
: [];
|
|
|
|
const allOptions = [
|
|
{
|
|
component: (
|
|
<SegmentedControl
|
|
data={[
|
|
{
|
|
label: t('table.config.general.pagination_infinite'),
|
|
value: ListPaginationType.INFINITE,
|
|
},
|
|
{
|
|
label: t('table.config.general.pagination_paginate'),
|
|
value: ListPaginationType.PAGINATED,
|
|
},
|
|
]}
|
|
onChange={(value) =>
|
|
setList(listKey, { pagination: value as ListPaginationType })
|
|
}
|
|
size="sm"
|
|
value={list.pagination}
|
|
w="100%"
|
|
/>
|
|
),
|
|
id: 'pagination',
|
|
label: t('table.config.general.pagination'),
|
|
size: 'sm',
|
|
},
|
|
{
|
|
component: (
|
|
<Slider
|
|
defaultValue={list.itemsPerPage}
|
|
marks={[
|
|
{ value: 25 },
|
|
{ value: 50 },
|
|
{ value: 100 },
|
|
{ value: 150 },
|
|
{ value: 200 },
|
|
{ value: 250 },
|
|
{ value: 300 },
|
|
{ value: 400 },
|
|
{ value: 500 },
|
|
]}
|
|
max={500}
|
|
min={25}
|
|
onChangeEnd={(value) => setList(listKey, { itemsPerPage: value })}
|
|
restrictToMarks
|
|
w="100%"
|
|
/>
|
|
),
|
|
id: 'itemsPerPage',
|
|
label: (
|
|
<Group>
|
|
{t('table.config.general.pagination_itemsPerPage')}
|
|
<Badge>{list.itemsPerPage}</Badge>
|
|
</Group>
|
|
),
|
|
},
|
|
{
|
|
component: (
|
|
<SegmentedControl
|
|
data={[
|
|
{
|
|
label: t('table.config.general.size_compact'),
|
|
value: 'compact',
|
|
},
|
|
{
|
|
label: t('table.config.general.size_default'),
|
|
value: 'default',
|
|
},
|
|
{
|
|
label: t('table.config.general.size_large'),
|
|
value: 'large',
|
|
},
|
|
]}
|
|
onChange={(value) =>
|
|
setTableUpdate({
|
|
size: value as 'compact' | 'default' | 'large',
|
|
})
|
|
}
|
|
size="sm"
|
|
value={table?.size ?? 'default'}
|
|
w="100%"
|
|
/>
|
|
),
|
|
id: 'size',
|
|
label: t('table.config.general.size'),
|
|
},
|
|
{
|
|
component: (
|
|
<ListConfigBooleanControl
|
|
onChange={(e) => setTableUpdate({ enableHeader: e })}
|
|
value={table.enableHeader}
|
|
/>
|
|
),
|
|
id: 'enableHeader',
|
|
label: t('table.config.general.showHeader'),
|
|
},
|
|
{
|
|
component: (
|
|
<ListConfigBooleanControl
|
|
onChange={(e) => setTableUpdate({ enableRowHoverHighlight: e })}
|
|
value={table.enableRowHoverHighlight}
|
|
/>
|
|
),
|
|
id: 'enableRowHoverHighlight',
|
|
label: t('table.config.general.rowHoverHighlight'),
|
|
},
|
|
{
|
|
component: (
|
|
<ListConfigBooleanControl
|
|
onChange={(e) => setTableUpdate({ enableAlternateRowColors: e })}
|
|
value={table.enableAlternateRowColors}
|
|
/>
|
|
),
|
|
id: 'enableAlternateRowColors',
|
|
label: t('table.config.general.alternateRowColors'),
|
|
},
|
|
{
|
|
component: (
|
|
<ListConfigBooleanControl
|
|
onChange={(e) => setTableUpdate({ enableHorizontalBorders: e })}
|
|
value={table.enableHorizontalBorders}
|
|
/>
|
|
),
|
|
id: 'enableHorizontalBorders',
|
|
label: t('table.config.general.horizontalBorders'),
|
|
},
|
|
{
|
|
component: (
|
|
<ListConfigBooleanControl
|
|
onChange={(e) => setTableUpdate({ enableVerticalBorders: e })}
|
|
value={table.enableVerticalBorders}
|
|
/>
|
|
),
|
|
id: 'enableVerticalBorders',
|
|
label: t('table.config.general.verticalBorders'),
|
|
},
|
|
{
|
|
component: (
|
|
<ListConfigBooleanControl
|
|
onChange={(e) => setTableUpdate({ autoFitColumns: e })}
|
|
value={
|
|
tableKey === 'main' ? (table as DataTableProps).autoFitColumns : false
|
|
}
|
|
/>
|
|
),
|
|
id: 'autoFitColumns',
|
|
label: t('table.config.general.autoFitColumns'),
|
|
},
|
|
...albumGroupOptions,
|
|
...(extraOptions || []),
|
|
];
|
|
|
|
// Filter and apply config (hidden/disabled)
|
|
return allOptions
|
|
.map((option) => {
|
|
const config = optionsConfig?.[option.id];
|
|
if (config?.hidden) {
|
|
return null;
|
|
}
|
|
return option;
|
|
})
|
|
.filter((option): option is NonNullable<typeof option> => option !== null);
|
|
}, [
|
|
t,
|
|
list.pagination,
|
|
list.itemsPerPage,
|
|
table,
|
|
tableKey,
|
|
extraOptions,
|
|
setList,
|
|
listKey,
|
|
setTableUpdate,
|
|
optionsConfig,
|
|
hasAlbumGroupColumn,
|
|
albumGroupOpen,
|
|
albumGroupImageSize,
|
|
imageResTable,
|
|
setSettings,
|
|
]);
|
|
|
|
return (
|
|
<>
|
|
<ListConfigTable options={advancedSettings} />
|
|
<Divider />
|
|
<TableColumnConfig
|
|
data={tableColumnsData}
|
|
enablePinColumnButtons={enablePinColumnButtons}
|
|
onChange={(columns) => setTableUpdate({ columns })}
|
|
value={table.columns}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const TableColumnConfig = ({
|
|
data,
|
|
enablePinColumnButtons,
|
|
onChange,
|
|
value,
|
|
}: {
|
|
data: { label: string; value: string }[];
|
|
enablePinColumnButtons: boolean;
|
|
onChange: (value: ItemTableListColumnConfig[]) => void;
|
|
value: ItemTableListColumnConfig[];
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
|
|
const valueRef = useRef(value);
|
|
const onChangeRef = useRef(onChange);
|
|
|
|
useLayoutEffect(() => {
|
|
valueRef.current = value;
|
|
onChangeRef.current = onChange;
|
|
});
|
|
|
|
const labelMap = useMemo(() => {
|
|
return data.reduce(
|
|
(acc, item) => {
|
|
acc[item.value] = item.label;
|
|
return acc;
|
|
},
|
|
{} as Record<string, string>,
|
|
);
|
|
}, [data]);
|
|
|
|
const handleChangeEnabled = useCallback((item: ItemTableListColumnConfig, checked: boolean) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
newValues[index] = { ...newValues[index], isEnabled: checked };
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handleMoveUp = useCallback((item: ItemTableListColumnConfig) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
if (index === 0) return;
|
|
const newValues = [...currentValue];
|
|
[newValues[index], newValues[index - 1]] = [newValues[index - 1], newValues[index]];
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handleMoveDown = useCallback((item: ItemTableListColumnConfig) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
if (index === currentValue.length - 1) return;
|
|
const newValues = [...currentValue];
|
|
[newValues[index], newValues[index + 1]] = [newValues[index + 1], newValues[index]];
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handlePinToLeft = useCallback((item: ItemTableListColumnConfig) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
|
|
const isPinned = newValues[index].pinned;
|
|
const isPinnedLeft = isPinned === 'left';
|
|
|
|
if (isPinnedLeft) {
|
|
newValues[index] = { ...newValues[index], pinned: null };
|
|
} else {
|
|
newValues[index] = { ...newValues[index], pinned: 'left' };
|
|
}
|
|
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handlePinToRight = useCallback((item: ItemTableListColumnConfig) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
|
|
const isPinned = newValues[index].pinned;
|
|
const isPinnedRight = isPinned === 'right';
|
|
|
|
if (isPinnedRight) {
|
|
newValues[index] = { ...newValues[index], pinned: null };
|
|
} else {
|
|
newValues[index] = { ...newValues[index], pinned: 'right' };
|
|
}
|
|
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handleAlignLeft = useCallback((item: ItemTableListColumnConfig) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
newValues[index] = { ...newValues[index], align: 'start' };
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handleAlignCenter = useCallback((item: ItemTableListColumnConfig) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
newValues[index] = { ...newValues[index], align: 'center' };
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handleAlignRight = useCallback((item: ItemTableListColumnConfig) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
newValues[index] = { ...newValues[index], align: 'end' };
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handleAutoSize = useCallback((item: ItemTableListColumnConfig, checked: boolean) => {
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
newValues[index] = { ...newValues[index], autoSize: checked };
|
|
onChangeRef.current(newValues);
|
|
}, []);
|
|
|
|
const handleRowWidth = useCallback(
|
|
(item: ItemTableListColumnConfig, number: number | string) => {
|
|
if (typeof number !== 'number') {
|
|
number = 0;
|
|
}
|
|
|
|
if (number < 0) {
|
|
number = 0;
|
|
}
|
|
|
|
if (number > 2000) {
|
|
number = 2000;
|
|
}
|
|
|
|
const currentValue = valueRef.current;
|
|
const index = currentValue.findIndex((v) => v.id === item.id);
|
|
const newValues = [...currentValue];
|
|
newValues[index] = { ...newValues[index], width: number };
|
|
onChangeRef.current(newValues);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const [searchColumns, setSearchColumns] = useDebouncedState('', 300);
|
|
|
|
const fuse = useMemo(() => {
|
|
return new Fuse(value, {
|
|
getFn: (obj) => {
|
|
return labelMap[obj.id] || '';
|
|
},
|
|
includeMatches: true,
|
|
includeScore: true,
|
|
keys: ['id', 'label'],
|
|
threshold: 0.3,
|
|
});
|
|
}, [value, labelMap]);
|
|
|
|
const filteredColumns = useMemo(() => {
|
|
if (!searchColumns.trim()) {
|
|
return value.map((item) => ({ item, matches: null }));
|
|
}
|
|
|
|
const results = fuse.search(searchColumns);
|
|
const resultMap = new Map(results.map((result) => [result.item.id, result.matches]));
|
|
|
|
return value.map((item) => ({
|
|
item,
|
|
matches: resultMap.get(item.id) || null,
|
|
}));
|
|
}, [value, searchColumns, fuse]);
|
|
|
|
const handleReorder = useCallback((idFrom: string, idTo: string, edge: Edge | null) => {
|
|
const currentValue = valueRef.current;
|
|
const idList = currentValue.map((item) => item.id);
|
|
const newIdOrder = dndUtils.reorderById({
|
|
edge,
|
|
idFrom,
|
|
idTo,
|
|
list: idList,
|
|
});
|
|
|
|
// Map the new ID order back to full items
|
|
const newOrder = newIdOrder.map((id) => currentValue.find((item) => item.id === id)!);
|
|
onChangeRef.current(newOrder);
|
|
}, []);
|
|
|
|
return (
|
|
<Stack gap="xs">
|
|
<Group justify="space-between" mb="md">
|
|
<Text size="sm">{t('common.tableColumns')}</Text>
|
|
<TextInput
|
|
onChange={(e) => setSearchColumns(e.currentTarget.value)}
|
|
placeholder={t('common.search')}
|
|
size="xs"
|
|
/>
|
|
</Group>
|
|
<div style={{ userSelect: 'none' }}>
|
|
{filteredColumns.map(({ item, matches }) => (
|
|
<TableColumnItem
|
|
enablePinColumnButtons={enablePinColumnButtons}
|
|
handleAlignCenter={handleAlignCenter}
|
|
handleAlignLeft={handleAlignLeft}
|
|
handleAlignRight={handleAlignRight}
|
|
handleAutoSize={handleAutoSize}
|
|
handleChangeEnabled={handleChangeEnabled}
|
|
handleMoveDown={handleMoveDown}
|
|
handleMoveUp={handleMoveUp}
|
|
handlePinToLeft={handlePinToLeft}
|
|
handlePinToRight={handlePinToRight}
|
|
handleReorder={handleReorder}
|
|
handleRowWidth={handleRowWidth}
|
|
item={item}
|
|
key={item.id}
|
|
label={labelMap[item.id]}
|
|
matches={matches}
|
|
/>
|
|
))}
|
|
</div>
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
const DragHandle = ({
|
|
dragHandleRef,
|
|
}: {
|
|
dragHandleRef: React.RefObject<HTMLButtonElement | null>;
|
|
}) => {
|
|
return (
|
|
<ActionIcon
|
|
icon="dragVertical"
|
|
iconProps={{
|
|
size: 'md',
|
|
}}
|
|
ref={dragHandleRef as React.RefObject<HTMLButtonElement>}
|
|
size="xs"
|
|
style={{ cursor: 'grab' }}
|
|
variant="default"
|
|
/>
|
|
);
|
|
};
|
|
|
|
const TableColumnItem = memo(
|
|
({
|
|
enablePinColumnButtons,
|
|
handleAlignCenter,
|
|
handleAlignLeft,
|
|
handleAlignRight,
|
|
handleAutoSize,
|
|
handleChangeEnabled,
|
|
handleMoveDown,
|
|
handleMoveUp,
|
|
handlePinToLeft,
|
|
handlePinToRight,
|
|
handleReorder,
|
|
handleRowWidth,
|
|
item,
|
|
label,
|
|
matches,
|
|
}: {
|
|
enablePinColumnButtons: boolean;
|
|
handleAlignCenter: (item: ItemTableListColumnConfig) => void;
|
|
handleAlignLeft: (item: ItemTableListColumnConfig) => void;
|
|
handleAlignRight: (item: ItemTableListColumnConfig) => void;
|
|
handleAutoSize: (item: ItemTableListColumnConfig, checked: boolean) => void;
|
|
handleChangeEnabled: (item: ItemTableListColumnConfig, checked: boolean) => void;
|
|
handleMoveDown: (item: ItemTableListColumnConfig) => void;
|
|
handleMoveUp: (item: ItemTableListColumnConfig) => void;
|
|
handlePinToLeft: (item: ItemTableListColumnConfig) => void;
|
|
handlePinToRight: (item: ItemTableListColumnConfig) => void;
|
|
handleReorder: (idFrom: string, idTo: string, edge: Edge | null) => void;
|
|
handleRowWidth: (item: ItemTableListColumnConfig, number: number | string) => void;
|
|
item: ItemTableListColumnConfig;
|
|
label: string;
|
|
matches: null | readonly FuseResultMatch[];
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const dragHandleRef = useRef<HTMLButtonElement>(null);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [isDraggedOver, setIsDraggedOver] = useState<Edge | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!ref.current || !dragHandleRef.current) {
|
|
return;
|
|
}
|
|
|
|
return combine(
|
|
draggable({
|
|
element: dragHandleRef.current,
|
|
getInitialData: () => {
|
|
const data = dndUtils.generateDragData({
|
|
id: [item.id],
|
|
operation: [DragOperation.REORDER],
|
|
type: DragTarget.TABLE_COLUMN,
|
|
});
|
|
return data;
|
|
},
|
|
onDragStart: () => {
|
|
setIsDragging(true);
|
|
},
|
|
onDrop: () => {
|
|
setIsDragging(false);
|
|
},
|
|
onGenerateDragPreview: (data) => {
|
|
disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage });
|
|
},
|
|
}),
|
|
dropTargetForElements({
|
|
canDrop: (args) => {
|
|
const data = args.source.data as unknown as DragData;
|
|
const isSelf = (args.source.data.id as string[])[0] === item.id;
|
|
return (
|
|
dndUtils.isDropTarget(data.type, [DragTarget.TABLE_COLUMN]) && !isSelf
|
|
);
|
|
},
|
|
element: ref.current,
|
|
getData: ({ element, input }) => {
|
|
const data = dndUtils.generateDragData({
|
|
id: [item.id],
|
|
operation: [DragOperation.REORDER],
|
|
type: DragTarget.TABLE_COLUMN,
|
|
});
|
|
|
|
return attachClosestEdge(data, {
|
|
allowedEdges: ['top', 'bottom'],
|
|
element,
|
|
input,
|
|
});
|
|
},
|
|
onDrag: (args) => {
|
|
const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
|
|
setIsDraggedOver(closestEdgeOfTarget);
|
|
},
|
|
onDragLeave: () => {
|
|
setIsDraggedOver(null);
|
|
},
|
|
onDrop: (args) => {
|
|
const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
|
|
|
|
const from = args.source.data.id as string[];
|
|
const to = args.self.data.id as string[];
|
|
|
|
handleReorder(from[0], to[0], closestEdgeOfTarget);
|
|
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'),
|
|
}}
|
|
variant="subtle"
|
|
/>
|
|
<ActionIcon
|
|
icon="arrowDown"
|
|
iconProps={{ size: 'md' }}
|
|
onClick={() => handleMoveDown(item)}
|
|
size="xs"
|
|
tooltip={{
|
|
label: t('table.config.general.moveDown'),
|
|
}}
|
|
variant="subtle"
|
|
/>
|
|
</ActionIconGroup>
|
|
{enablePinColumnButtons && (
|
|
<ActionIconGroup className={styles.group}>
|
|
<ActionIcon
|
|
icon="arrowLeftToLine"
|
|
iconProps={{ size: 'md' }}
|
|
onClick={() => handlePinToLeft(item)}
|
|
size="xs"
|
|
tooltip={{
|
|
label: t('table.config.general.pinToLeft'),
|
|
}}
|
|
variant={item.pinned === 'left' ? 'filled' : 'subtle'}
|
|
/>
|
|
<ActionIcon
|
|
icon="arrowRightToLine"
|
|
iconProps={{ size: 'md' }}
|
|
onClick={() => handlePinToRight(item)}
|
|
size="xs"
|
|
tooltip={{
|
|
label: t('table.config.general.pinToRight'),
|
|
}}
|
|
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'),
|
|
}}
|
|
variant={item.align === 'start' ? 'filled' : 'subtle'}
|
|
/>
|
|
<ActionIcon
|
|
icon="alignCenter"
|
|
iconProps={{ size: 'md' }}
|
|
onClick={() => handleAlignCenter(item)}
|
|
size="xs"
|
|
tooltip={{
|
|
label: t('table.config.general.alignCenter'),
|
|
}}
|
|
variant={item.align === 'center' ? 'filled' : 'subtle'}
|
|
/>
|
|
<ActionIcon
|
|
icon="alignRight"
|
|
iconProps={{ size: 'md' }}
|
|
onClick={() => handleAlignRight(item)}
|
|
size="xs"
|
|
tooltip={{
|
|
label: t('table.config.general.alignRight'),
|
|
}}
|
|
variant={item.align === 'end' ? 'filled' : 'subtle'}
|
|
/>
|
|
</ActionIconGroup>
|
|
<NumberInput
|
|
className={clsx(styles.group, styles.numberInput)}
|
|
hideControls={false}
|
|
leftSection={
|
|
<Tooltip label={t('table.config.general.autosize')}>
|
|
<Checkbox
|
|
checked={item.autoSize}
|
|
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>
|
|
);
|
|
},
|
|
(prevProps, nextProps) => {
|
|
// Custom comparison function for better memoization
|
|
return (
|
|
prevProps.enablePinColumnButtons === nextProps.enablePinColumnButtons &&
|
|
prevProps.item.id === nextProps.item.id &&
|
|
prevProps.item.isEnabled === nextProps.item.isEnabled &&
|
|
prevProps.item.autoSize === nextProps.item.autoSize &&
|
|
prevProps.item.width === nextProps.item.width &&
|
|
prevProps.item.pinned === nextProps.item.pinned &&
|
|
prevProps.item.align === nextProps.item.align &&
|
|
prevProps.label === nextProps.label &&
|
|
prevProps.matches === nextProps.matches
|
|
);
|
|
},
|
|
);
|