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, { 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 { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu'; import { DataGridProps, ItemGridListRowConfig, ItemListSettings, useSettingsStore, useSettingsStoreActions, } from '/@/renderer/store'; import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon'; import { Badge } from '/@/shared/components/badge/badge'; import { Checkbox } from '/@/shared/components/checkbox/checkbox'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; 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 { useDebouncedState } from '/@/shared/hooks/use-debounced-state'; import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; import { ItemListKey, ListPaginationType } from '/@/shared/types/types'; type GridConfigProps = { extraOptions?: { component: React.ReactNode; id: string; label: string; }[]; gridRowsData: { label: string; value: string }[]; listKey: ItemListKey; optionsConfig?: { [key: string]: { disabled?: boolean; hidden?: boolean; }; }; }; export const GridConfig = ({ extraOptions, gridRowsData, listKey, optionsConfig, }: GridConfigProps) => { const { t } = useTranslation(); const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings; const grid = list.grid as DataGridProps; const { setList } = useSettingsStoreActions(); const options = useMemo(() => { const allOptions = [ { component: ( setList(listKey, { ...list, pagination: value as ListPaginationType, }) } size="sm" value={list.pagination} w="100%" /> ), id: 'pagination', label: t('table.config.general.pagination', { postProcess: 'sentenceCase' }), size: 'sm', }, { component: ( setList(listKey, { ...list, itemsPerPage: value })} restrictToMarks w="100%" /> ), id: 'itemsPerPage', label: ( {t('table.config.general.pagination_itemsPerPage', { postProcess: 'sentenceCase', })} {list.itemsPerPage} ), }, { component: ( { if (grid.itemGap === 'xl') return; if (grid.itemGap === 'lg') { return setList(listKey, { grid: { itemGap: 'xl' } }); } if (grid.itemGap === 'md') { return setList(listKey, { grid: { itemGap: 'lg' } }); } if (grid.itemGap === 'sm') { return setList(listKey, { grid: { itemGap: 'md' } }); } return setList(listKey, { grid: { itemGap: 'sm' } }); }} size="xs" /> { if (grid.itemGap === 'xs') return; if (grid.itemGap === 'sm') { return setList(listKey, { grid: { itemGap: 'xs' } }); } if (grid.itemGap === 'md') { return setList(listKey, { grid: { itemGap: 'sm' } }); } if (grid.itemGap === 'lg') { return setList(listKey, { grid: { itemGap: 'md' } }); } return setList(listKey, { grid: { itemGap: 'lg' } }); }} size="xs" /> ), id: 'itemGap', label: ( {t('table.config.general.gap', { postProcess: 'sentenceCase' })} {grid.itemGap} ), }, { component: ( setList(listKey, { grid: { itemsPerRow: value } })} w="100%" /> ), id: 'itemsPerRow', label: ( {t('table.config.general.itemsPerRow', { postProcess: 'sentenceCase' })} {grid.itemsPerRow} setList(listKey, { grid: { itemsPerRowEnabled: e.target.checked }, }) } pr="md" size="xs" /> ), }, { component: ( setList(listKey, { grid: { size: value as 'compact' | 'default' | 'large' }, }) } size="sm" value={grid.size || 'default'} w="100%" /> ), id: 'size', label: t('table.config.general.size', { postProcess: 'sentenceCase' }), size: 'sm', }, ...(extraOptions || []), ]; // Filter and apply config (hidden/disabled) return allOptions .map((option) => { const config = optionsConfig?.[option.id]; if (config?.hidden) { return null; } return { ...option, disabled: config?.disabled || false, }; }) .filter( (option): option is (typeof allOptions)[0] & { disabled: boolean } => option !== null, ); }, [list, t, grid, extraOptions, optionsConfig, setList, listKey]); return ( <> setList(listKey, { grid: { rows } })} value={grid.rows} /> ); }; const GridRowConfig = ({ data, onChange, value, }: { data: { label: string; value: string }[]; onChange: (value: ItemGridListRowConfig[]) => void; value: ItemGridListRowConfig[]; }) => { 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, ); }, [data]); const handleChangeEnabled = useCallback((item: ItemGridListRowConfig, 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: ItemGridListRowConfig) => { 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: ItemGridListRowConfig) => { 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 handleAlignLeft = useCallback((item: ItemGridListRowConfig) => { 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: ItemGridListRowConfig) => { 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: ItemGridListRowConfig) => { 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 [searchRows, setSearchRows] = 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 filteredRows = useMemo(() => { if (!searchRows.trim()) { return value.map((item) => ({ item, matches: null })); } const results = fuse.search(searchRows); const resultMap = new Map(results.map((result) => [result.item.id, result.matches])); return value.map((item) => ({ item, matches: resultMap.get(item.id) || null, })); }, [value, searchRows, 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 ( {t('common.gridRows', { postProcess: 'sentenceCase' })} setSearchRows(e.currentTarget.value)} placeholder={t('common.search', { postProcess: 'sentenceCase', })} size="xs" />
{filteredRows.map(({ item, matches }) => ( ))}
); }; const DragHandle = ({ dragHandleRef, }: { dragHandleRef: React.RefObject; }) => { return ( ); }; const GridRowItem = memo( ({ handleAlignCenter, handleAlignLeft, handleAlignRight, handleChangeEnabled, handleMoveDown, handleMoveUp, handleReorder, item, label, matches, }: { handleAlignCenter: (item: ItemGridListRowConfig) => void; handleAlignLeft: (item: ItemGridListRowConfig) => void; handleAlignRight: (item: ItemGridListRowConfig) => void; handleChangeEnabled: (item: ItemGridListRowConfig, checked: boolean) => void; handleMoveDown: (item: ItemGridListRowConfig) => void; handleMoveUp: (item: ItemGridListRowConfig) => void; handleReorder: (idFrom: string, idTo: string, edge: Edge | null) => void; item: ItemGridListRowConfig; label: string; matches: null | readonly FuseResultMatch[]; }) => { const { t } = useTranslation(); const ref = useRef(null); const dragHandleRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [isDraggedOver, setIsDraggedOver] = useState(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.GRID_ROW, }); 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.GRID_ROW]) && !isSelf; }, element: ref.current, getData: ({ element, input }) => { const data = dndUtils.generateDragData({ id: [item.id], operation: [DragOperation.REORDER], type: DragTarget.GRID_ROW, }); 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 (
0, })} ref={ref} > handleChangeEnabled(item, e.currentTarget.checked)} size="sm" /> handleMoveUp(item)} size="xs" tooltip={{ label: t('table.config.general.moveUp', { postProcess: 'sentenceCase', }), }} variant="subtle" /> handleMoveDown(item)} size="xs" tooltip={{ label: t('table.config.general.moveDown', { postProcess: 'sentenceCase', }), }} variant="subtle" /> handleAlignLeft(item)} size="xs" tooltip={{ label: t('table.config.general.alignLeft', { postProcess: 'sentenceCase', }), }} variant={item.align === 'start' ? 'filled' : 'subtle'} /> handleAlignCenter(item)} size="xs" tooltip={{ label: t('table.config.general.alignCenter', { postProcess: 'sentenceCase', }), }} variant={item.align === 'center' ? 'filled' : 'subtle'} /> handleAlignRight(item)} size="xs" tooltip={{ label: t('table.config.general.alignRight', { postProcess: 'sentenceCase', }), }} variant={item.align === 'end' ? 'filled' : 'subtle'} />
); }, (prevProps, nextProps) => { return ( prevProps.item.id === nextProps.item.id && prevProps.item.isEnabled === nextProps.item.isEnabled && prevProps.item.align === nextProps.item.align && prevProps.label === nextProps.label && prevProps.matches === nextProps.matches ); }, );