From 1172152018ce59a102940309be9e1bcc7c91f480 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 9 Oct 2025 14:26:52 -0700 Subject: [PATCH] update ListConfigMenu to work with new lists --- .../shared/components/grid-config.tsx | 200 +++++++ .../shared/components/list-config-menu.tsx | 290 ++++------ .../shared/components/table-config.module.css | 18 + .../shared/components/table-config.tsx | 522 ++++++++++++++++++ 4 files changed, 847 insertions(+), 183 deletions(-) create mode 100644 src/renderer/features/shared/components/grid-config.tsx create mode 100644 src/renderer/features/shared/components/table-config.module.css create mode 100644 src/renderer/features/shared/components/table-config.tsx diff --git a/src/renderer/features/shared/components/grid-config.tsx b/src/renderer/features/shared/components/grid-config.tsx new file mode 100644 index 000000000..0ac0e8839 --- /dev/null +++ b/src/renderer/features/shared/components/grid-config.tsx @@ -0,0 +1,200 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu'; +import { + DataGridProps, + DataListProps, + useSettingsStore, + useSettingsStoreActions, +} from '/@/renderer/store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { Badge } from '/@/shared/components/badge/badge'; +import { Checkbox } from '/@/shared/components/checkbox/checkbox'; +import { Group } from '/@/shared/components/group/group'; +import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; +import { Slider } from '/@/shared/components/slider/slider'; +import { ItemListKey, ListPaginationType } from '/@/shared/types/types'; + +type GridConfigProps = { + extraOptions?: { + component: React.ReactNode; + id: string; + label: string; + }[]; + listKey: ItemListKey; +}; + +export const GridConfig = ({ extraOptions, listKey }: GridConfigProps) => { + const { t } = useTranslation(); + + const list = useSettingsStore((state) => state.lists[listKey]) as DataListProps; + const grid = useSettingsStore((state) => state.lists[listKey].grid) as DataGridProps; + const { setList } = useSettingsStoreActions(); + + const options = useMemo(() => { + return [ + { + 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 }, + }) + } + size="xs" + /> + + ), + }, + + ...(extraOptions || []), + ]; + }, [list, t, grid, extraOptions, setList, listKey]); + + return ( + <> + + + ); +}; diff --git a/src/renderer/features/shared/components/list-config-menu.tsx b/src/renderer/features/shared/components/list-config-menu.tsx index 463dc31ad..2264a501d 100644 --- a/src/renderer/features/shared/components/list-config-menu.tsx +++ b/src/renderer/features/shared/components/list-config-menu.tsx @@ -1,91 +1,128 @@ -import { useTranslation } from 'react-i18next'; +import { ReactNode } from 'react'; import i18n from '/@/i18n/i18n'; +import { GridConfig } from '/@/renderer/features/shared/components/grid-config'; import { SettingsButton } from '/@/renderer/features/shared/components/settings-button'; -import { CheckboxSelect } from '/@/shared/components/checkbox-select/checkbox-select'; +import { TableConfig } from '/@/renderer/features/shared/components/table-config'; +import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store'; +import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Popover } from '/@/shared/components/popover/popover'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; -import { Slider } from '/@/shared/components/slider/slider'; import { Stack } from '/@/shared/components/stack/stack'; -import { Switch } from '/@/shared/components/switch/switch'; import { Table } from '/@/shared/components/table/table'; -import { ListDisplayType } from '/@/shared/types/types'; +import { ItemListKey, ListDisplayType } from '/@/shared/types/types'; const DISPLAY_TYPES = [ { label: ( - + {i18n.t('table.config.view.table', { postProcess: 'sentenceCase' }) as string} - + ), value: ListDisplayType.TABLE, }, { label: ( - + - {i18n.t('table.config.view.card', { postProcess: 'sentenceCase' }) as string} - + {i18n.t('table.config.view.grid', { postProcess: 'sentenceCase' }) as string} + ), value: ListDisplayType.GRID, }, - { - disabled: true, - label: ( - - - {i18n.t('table.config.view.list', { postProcess: 'sentenceCase' }) as string} - - ), - value: ListDisplayType.LIST, - }, + // { + // disabled: true, + // label: ( + // + // + // {i18n.t('table.config.view.list', { postProcess: 'sentenceCase' }) as string} + // + // ), + // value: ListDisplayType.LIST, + // }, ]; +export const ListConfigBooleanControl = ({ + onChange, + value, +}: { + onChange: (value: boolean) => void; + value: boolean; +}) => { + return ( + onChange(value === 'true' ? true : false)} + size="sm" + value={value ? 'true' : 'false'} + w="100%" + /> + ); +}; + interface ListConfigMenuProps { - autoFitColumns?: boolean; - disabledViewTypes?: ListDisplayType[]; - displayType: ListDisplayType; - itemGap?: number; - itemSize?: number; - onChangeAutoFitColumns?: (autoFitColumns: boolean) => void; - onChangeDisplayType?: (displayType: ListDisplayType) => void; - onChangeItemGap?: (itemGap: number) => void; - onChangeItemSize?: (itemSize: number) => void; - onChangeTableColumns?: (tableColumns: string[]) => void; - tableColumns?: string[]; - tableColumnsData?: { label: string; value: string }[]; + listKey: ItemListKey; + tableColumnsData: { label: string; value: string }[]; } export const ListConfigMenu = (props: ListConfigMenuProps) => { + const displayType = useSettingsStore((state) => state.lists[props.listKey].display); + const { setList } = useSettingsStoreActions(); + return ( - + - - - ({ - ...type, - disabled: props.disabledViewTypes?.includes(type.value), - }))} - onChange={(value) => props.onChangeDisplayType?.(value as ListDisplayType)} - value={props.displayType} - w="100%" - withItemsBorders={false} - /> - - + + + + ({ + ...type, + }))} + fullWidth + onChange={(value) => { + setList(props.listKey, { + display: value as ListDisplayType, + }); + }} + value={displayType} + withItemsBorders={false} + /> + + + ); }; -const Config = (props: ListConfigMenuProps) => { - switch (props.displayType) { +const Config = ({ + displayType, + ...props +}: ListConfigMenuProps & { displayType: ListDisplayType }) => { + switch (displayType) { case ListDisplayType.GRID: return ; @@ -97,141 +134,28 @@ const Config = (props: ListConfigMenuProps) => { } }; -type TableConfigProps = Pick< - ListConfigMenuProps, - | 'autoFitColumns' - | 'itemSize' - | 'onChangeAutoFitColumns' - | 'onChangeItemSize' - | 'onChangeTableColumns' - | 'tableColumns' - | 'tableColumnsData' ->; - -const TableConfig = ({ - autoFitColumns, - itemSize, - onChangeAutoFitColumns, - onChangeItemSize, - onChangeTableColumns, - tableColumns, - tableColumnsData, -}: TableConfigProps) => { - const { t } = useTranslation(); - - if ( - !tableColumnsData || - !onChangeTableColumns || - !tableColumns || - !onChangeItemSize || - autoFitColumns === undefined || - !onChangeAutoFitColumns || - itemSize === undefined - ) { - console.error('TableConfig: Missing required props', { - itemSize, - onChangeItemSize, - onChangeTableColumns, - tableColumns, - tableColumnsData, - }); - return null; - } - +export const ListConfigTable = ({ + options, +}: { + options: { component: ReactNode; id: string; label: ReactNode | string }[]; +}) => { return ( - <> - - - - - {t('table.config.general.size', { - postProcess: 'sentenceCase', - })} - - - - +
+ + {options.map((option) => ( + + {option.label} + {option.component} - - - {t('table.config.general.autoFitColumns', { - postProcess: 'sentenceCase', - })} - - - onChangeAutoFitColumns?.(e.target.checked)} - size="xs" - /> - - - -
- - - - - ); -}; - -type GridConfigProps = Pick< - ListConfigMenuProps, - 'itemGap' | 'itemSize' | 'onChangeItemGap' | 'onChangeItemSize' ->; - -const GridConfig = ({ itemSize, onChangeItemGap, onChangeItemSize }: GridConfigProps) => { - const { t } = useTranslation(); - - if (!onChangeItemGap || !onChangeItemSize || !itemSize) { - return null; - } - - return ( - <> - - - - - {t('table.config.general.gap', { - postProcess: 'sentenceCase', - })} - - - - - - - - {t('table.config.general.size', { - postProcess: 'sentenceCase', - })} - - - - - - -
- + ))} + + ); }; diff --git a/src/renderer/features/shared/components/table-config.module.css b/src/renderer/features/shared/components/table-config.module.css new file mode 100644 index 000000000..cbb5e5552 --- /dev/null +++ b/src/renderer/features/shared/components/table-config.module.css @@ -0,0 +1,18 @@ +.group { + overflow: hidden; + border: 1px solid var(--theme-colors-border); + border-radius: var(--theme-radius-md); +} + +.number-input { + width: 140px; +} + +.item { + display: flex; + flex-wrap: nowrap; + gap: var(--theme-spacing-md); + align-items: center; + justify-content: space-between; + width: 100%; +} diff --git a/src/renderer/features/shared/components/table-config.tsx b/src/renderer/features/shared/components/table-config.tsx new file mode 100644 index 000000000..f3301751a --- /dev/null +++ b/src/renderer/features/shared/components/table-config.tsx @@ -0,0 +1,522 @@ +import clsx from 'clsx'; +import { motion } from 'motion/react'; +import { useCallback, useMemo } 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 { DataListProps, 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 { 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 { Tooltip } from '/@/shared/components/tooltip/tooltip'; +import { ItemListKey, ListPaginationType } from '/@/shared/types/types'; + +interface TableConfigProps { + extraOptions?: { + component: React.ReactNode; + id: string; + label: string; + }[]; + listKey: ItemListKey; + tableColumnsData: { label: string; value: string }[]; +} + +export const TableConfig = ({ extraOptions, listKey, tableColumnsData }: TableConfigProps) => { + const { t } = useTranslation(); + + const list = useSettingsStore((state) => state.lists[listKey]) as DataListProps; + const { setList } = useSettingsStoreActions(); + + const options = useMemo(() => { + return [ + { + component: ( + + setList(listKey, { 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, { itemsPerPage: value })} + restrictToMarks + w="100%" + /> + ), + id: 'itemsPerPage', + label: ( + + {t('table.config.general.pagination_itemsPerPage', { + postProcess: 'sentenceCase', + })} + {list.itemsPerPage} + + ), + }, + { + component: ( + + setList(listKey, { + table: { size: value as 'compact' | 'default' }, + }) + } + size="sm" + value={list.table.size} + w="100%" + /> + ), + id: 'size', + label: t('table.config.general.size', { + postProcess: 'titleCase', + }), + }, + { + component: ( + + setList(listKey, { table: { enableRowHoverHighlight: e } }) + } + value={list.table.enableRowHoverHighlight} + /> + ), + id: 'enableRowHoverHighlight', + label: t('table.config.general.rowHoverHighlight', { + postProcess: 'sentenceCase', + }), + }, + { + component: ( + + setList(listKey, { table: { enableAlternateRowColors: e } }) + } + value={list.table.enableAlternateRowColors} + /> + ), + id: 'enableAlternateRowColors', + label: t('table.config.general.alternateRowColors', { + postProcess: 'sentenceCase', + }), + }, + { + component: ( + + setList(listKey, { table: { enableHorizontalBorders: e } }) + } + value={list.table.enableHorizontalBorders} + /> + ), + id: 'enableHorizontalBorders', + label: t('table.config.general.horizontalBorders', { + postProcess: 'sentenceCase', + }), + }, + { + component: ( + setList(listKey, { table: { enableVerticalBorders: e } })} + value={list.table.enableVerticalBorders} + /> + ), + id: 'enableVerticalBorders', + label: t('table.config.general.verticalBorders', { + postProcess: 'sentenceCase', + }), + }, + + ...(extraOptions || []), + ]; + }, [extraOptions, listKey, setList, t, list]); + + return ( + <> + + + setList(listKey, { ...list, table: { ...list.table, columns } }) + } + value={list.table.columns} + /> + + ); +}; + +const TableColumnConfig = ({ + data, + listKey, + onChange, + value, +}: { + data: { label: string; value: string }[]; + listKey: ItemListKey; + onChange: (value: ItemTableListColumnConfig[]) => void; + value: ItemTableListColumnConfig[]; +}) => { + const { t } = useTranslation(); + + const labelMap = useMemo(() => { + return data.reduce( + (acc, item) => { + acc[item.value] = item.label; + return acc; + }, + {} as Record, + ); + }, [data]); + + const handleChangeEnabled = useCallback( + (item: ItemTableListColumnConfig, checked: boolean) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + newValues[index] = { ...newValues[index], isEnabled: checked }; + onChange(newValues); + }, + [listKey, onChange], + ); + + const handleMoveUp = useCallback( + (item: ItemTableListColumnConfig) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + if (index === 0) return; + const newValues = [...value]; + [newValues[index], newValues[index - 1]] = [newValues[index - 1], newValues[index]]; + onChange(newValues); + }, + [listKey, onChange], + ); + + const handleMoveDown = useCallback( + (item: ItemTableListColumnConfig) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + if (index === value.length - 1) return; + const newValues = [...value]; + [newValues[index], newValues[index + 1]] = [newValues[index + 1], newValues[index]]; + onChange(newValues); + }, + [listKey, onChange], + ); + + const handlePinToLeft = useCallback( + (item: ItemTableListColumnConfig) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + + const isPinned = newValues[index].pinned; + const isPinnedLeft = isPinned === 'left'; + + if (isPinnedLeft) { + newValues[index] = { ...newValues[index], pinned: null }; + } else { + newValues[index] = { ...newValues[index], pinned: 'left' }; + } + + onChange(newValues); + }, + [listKey, onChange], + ); + + const handlePinToRight = useCallback( + (item: ItemTableListColumnConfig) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + + const isPinned = newValues[index].pinned; + const isPinnedRight = isPinned === 'right'; + + if (isPinnedRight) { + newValues[index] = { ...newValues[index], pinned: null }; + } else { + newValues[index] = { ...newValues[index], pinned: 'right' }; + } + + onChange(newValues); + }, + [listKey, onChange], + ); + + const handleAlignLeft = useCallback( + (item: ItemTableListColumnConfig) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + newValues[index] = { ...newValues[index], align: 'start' }; + onChange(newValues); + }, + [listKey, onChange], + ); + + const handleAlignCenter = useCallback( + (item: ItemTableListColumnConfig) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + newValues[index] = { ...newValues[index], align: 'center' }; + onChange(newValues); + }, + [listKey, onChange], + ); + + const handleAlignRight = useCallback( + (item: ItemTableListColumnConfig) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + newValues[index] = { ...newValues[index], align: 'end' }; + onChange(newValues); + }, + [listKey, onChange], + ); + + const handleAutoSize = useCallback( + (item: ItemTableListColumnConfig, checked: boolean) => { + const value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + newValues[index] = { ...newValues[index], autoSize: checked }; + onChange(newValues); + }, + [listKey, onChange], + ); + + 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 value = useSettingsStore.getState().lists[listKey].table.columns; + const index = value.findIndex((v) => v.id === item.id); + const newValues = [...value]; + newValues[index] = { ...newValues[index], width: number }; + onChange(newValues); + }, + [listKey, onChange], + ); + + return ( + + {value.map((item) => ( + + + 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" + /> + + + handlePinToLeft(item)} + size="xs" + tooltip={{ + label: t('table.config.general.pinToLeft', { + postProcess: 'sentenceCase', + }), + }} + variant={item.pinned === 'left' ? 'filled' : 'subtle'} + /> + handlePinToRight(item)} + size="xs" + tooltip={{ + label: t('table.config.general.pinToRight', { + postProcess: 'sentenceCase', + }), + }} + variant={item.pinned === 'right' ? 'filled' : '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'} + /> + + + {item.pinned === null && ( + + + handleAutoSize(item, e.currentTarget.checked) + } + size="xs" + /> + + )} + + } + max={2000} + min={0} + onChange={(value) => handleRowWidth(item, value)} + size="xs" + step={10} + value={item.width} + variant="subtle" + /> + + + ))} + + ); +};