mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
update ListConfigMenu to work with new lists
This commit is contained in:
@@ -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: (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('table.config.general.pagination_infinite', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: ListPaginationType.INFINITE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('table.config.general.pagination_paginate', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: ListPaginationType.PAGINATED,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
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: (
|
||||||
|
<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, { ...list, itemsPerPage: value })}
|
||||||
|
restrictToMarks
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'itemsPerPage',
|
||||||
|
label: (
|
||||||
|
<Group>
|
||||||
|
{t('table.config.general.pagination_itemsPerPage', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
<Badge>{list.itemsPerPage}</Badge>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Group gap="xs" grow w="100%">
|
||||||
|
<ActionIcon
|
||||||
|
disabled={grid.itemGap === 'xl'}
|
||||||
|
icon="arrowUp"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={grid.itemGap === 'xs'}
|
||||||
|
icon="arrowDown"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
id: 'itemGap',
|
||||||
|
label: (
|
||||||
|
<Group>
|
||||||
|
{t('table.config.general.gap', { postProcess: 'sentenceCase' })}
|
||||||
|
<Badge>{grid.itemGap}</Badge>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Slider
|
||||||
|
defaultValue={grid.itemsPerRow}
|
||||||
|
max={20}
|
||||||
|
min={2}
|
||||||
|
onChangeEnd={(value) => setList(listKey, { grid: { itemsPerRow: value } })}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'itemsPerRow',
|
||||||
|
label: (
|
||||||
|
<Group justify="space-between" w="100%" wrap="nowrap">
|
||||||
|
<Group>
|
||||||
|
{t('table.config.general.itemsPerRow', { postProcess: 'sentenceCase' })}
|
||||||
|
<Badge>{grid.itemsPerRow}</Badge>
|
||||||
|
</Group>
|
||||||
|
<Checkbox
|
||||||
|
checked={grid.itemsPerRowEnabled}
|
||||||
|
label={t('common.enable', { postProcess: 'titleCase' })}
|
||||||
|
onChange={(e) =>
|
||||||
|
setList(listKey, {
|
||||||
|
grid: { itemsPerRowEnabled: e.target.checked },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
...(extraOptions || []),
|
||||||
|
];
|
||||||
|
}, [list, t, grid, extraOptions, setList, listKey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListConfigTable options={options} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,91 +1,128 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { GridConfig } from '/@/renderer/features/shared/components/grid-config';
|
||||||
import { SettingsButton } from '/@/renderer/features/shared/components/settings-button';
|
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 { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Popover } from '/@/shared/components/popover/popover';
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Slider } from '/@/shared/components/slider/slider';
|
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
|
||||||
import { Table } from '/@/shared/components/table/table';
|
import { Table } from '/@/shared/components/table/table';
|
||||||
import { ListDisplayType } from '/@/shared/types/types';
|
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const DISPLAY_TYPES = [
|
const DISPLAY_TYPES = [
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Stack align="center" p="sm">
|
<Group align="center" justify="center" p="sm">
|
||||||
<Icon icon="layoutTable" size="lg" />
|
<Icon icon="layoutTable" size="lg" />
|
||||||
{i18n.t('table.config.view.table', { postProcess: 'sentenceCase' }) as string}
|
{i18n.t('table.config.view.table', { postProcess: 'sentenceCase' }) as string}
|
||||||
</Stack>
|
</Group>
|
||||||
),
|
),
|
||||||
value: ListDisplayType.TABLE,
|
value: ListDisplayType.TABLE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Stack align="center" p="sm">
|
<Group align="center" justify="center" p="sm">
|
||||||
<Icon icon="layoutGrid" size="lg" />
|
<Icon icon="layoutGrid" size="lg" />
|
||||||
{i18n.t('table.config.view.card', { postProcess: 'sentenceCase' }) as string}
|
{i18n.t('table.config.view.grid', { postProcess: 'sentenceCase' }) as string}
|
||||||
</Stack>
|
</Group>
|
||||||
),
|
),
|
||||||
value: ListDisplayType.GRID,
|
value: ListDisplayType.GRID,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
disabled: true,
|
// disabled: true,
|
||||||
label: (
|
// label: (
|
||||||
<Stack align="center" p="sm">
|
// <Stack align="center" p="sm">
|
||||||
<Icon icon="layoutList" size="lg" />
|
// <Icon icon="layoutList" size="lg" />
|
||||||
{i18n.t('table.config.view.list', { postProcess: 'sentenceCase' }) as string}
|
// {i18n.t('table.config.view.list', { postProcess: 'sentenceCase' }) as string}
|
||||||
</Stack>
|
// </Stack>
|
||||||
),
|
// ),
|
||||||
value: ListDisplayType.LIST,
|
// value: ListDisplayType.LIST,
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ListConfigBooleanControl = ({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
value: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: i18n.t('common.enable', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}) as string,
|
||||||
|
value: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t('common.disable', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}) as string,
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) => onChange(value === 'true' ? true : false)}
|
||||||
|
size="sm"
|
||||||
|
value={value ? 'true' : 'false'}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ListConfigMenuProps {
|
interface ListConfigMenuProps {
|
||||||
autoFitColumns?: boolean;
|
listKey: ItemListKey;
|
||||||
disabledViewTypes?: ListDisplayType[];
|
tableColumnsData: { label: string; value: string }[];
|
||||||
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 }[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListConfigMenu = (props: ListConfigMenuProps) => {
|
export const ListConfigMenu = (props: ListConfigMenuProps) => {
|
||||||
|
const displayType = useSettingsStore((state) => state.lists[props.listKey].display);
|
||||||
|
const { setList } = useSettingsStoreActions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover position="bottom-end" width={300}>
|
<Popover position="bottom-end" trapFocus width={640}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<SettingsButton />
|
<SettingsButton />
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown p="md">
|
<Popover.Dropdown>
|
||||||
<Stack>
|
<ScrollArea
|
||||||
<SegmentedControl
|
allowDragScroll
|
||||||
data={DISPLAY_TYPES.map((type) => ({
|
scrollX
|
||||||
...type,
|
style={{ height: 'auto', maxHeight: '70dvh', padding: '1rem' }}
|
||||||
disabled: props.disabledViewTypes?.includes(type.value),
|
>
|
||||||
}))}
|
<Stack>
|
||||||
onChange={(value) => props.onChangeDisplayType?.(value as ListDisplayType)}
|
<SegmentedControl
|
||||||
value={props.displayType}
|
data={DISPLAY_TYPES.map((type) => ({
|
||||||
w="100%"
|
...type,
|
||||||
withItemsBorders={false}
|
}))}
|
||||||
/>
|
fullWidth
|
||||||
<Config {...props} />
|
onChange={(value) => {
|
||||||
</Stack>
|
setList(props.listKey, {
|
||||||
|
display: value as ListDisplayType,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={displayType}
|
||||||
|
withItemsBorders={false}
|
||||||
|
/>
|
||||||
|
<Config displayType={displayType} {...props} />
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Config = (props: ListConfigMenuProps) => {
|
const Config = ({
|
||||||
switch (props.displayType) {
|
displayType,
|
||||||
|
...props
|
||||||
|
}: ListConfigMenuProps & { displayType: ListDisplayType }) => {
|
||||||
|
switch (displayType) {
|
||||||
case ListDisplayType.GRID:
|
case ListDisplayType.GRID:
|
||||||
return <GridConfig {...props} />;
|
return <GridConfig {...props} />;
|
||||||
|
|
||||||
@@ -97,141 +134,28 @@ const Config = (props: ListConfigMenuProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type TableConfigProps = Pick<
|
export const ListConfigTable = ({
|
||||||
ListConfigMenuProps,
|
options,
|
||||||
| 'autoFitColumns'
|
}: {
|
||||||
| 'itemSize'
|
options: { component: ReactNode; id: string; label: ReactNode | string }[];
|
||||||
| '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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Table
|
||||||
<Table variant="vertical" withColumnBorders withRowBorders withTableBorder>
|
style={{ borderRadius: '1rem' }}
|
||||||
<Table.Tbody>
|
styles={{ th: { backgroundColor: 'initial', padding: 'var(--theme-spacing-md) 0' } }}
|
||||||
<Table.Tr>
|
variant="vertical"
|
||||||
<Table.Th>
|
withColumnBorders={false}
|
||||||
{t('table.config.general.size', {
|
withRowBorders={false}
|
||||||
postProcess: 'sentenceCase',
|
withTableBorder={false}
|
||||||
})}
|
>
|
||||||
</Table.Th>
|
<Table.Tbody>
|
||||||
<Table.Td>
|
{options.map((option) => (
|
||||||
<Slider
|
<Table.Tr key={option.id}>
|
||||||
defaultValue={itemSize}
|
<Table.Th w="50%">{option.label}</Table.Th>
|
||||||
max={100}
|
<Table.Td>{option.component}</Table.Td>
|
||||||
min={30}
|
|
||||||
onChangeEnd={onChangeItemSize}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
<Table.Tr>
|
))}
|
||||||
<Table.Th w="50%">
|
</Table.Tbody>
|
||||||
{t('table.config.general.autoFitColumns', {
|
</Table>
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
})}
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Td style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<Switch
|
|
||||||
defaultChecked={autoFitColumns}
|
|
||||||
onChange={(e) => onChangeAutoFitColumns?.(e.target.checked)}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
<ScrollArea allowDragScroll style={{ maxHeight: '200px' }}>
|
|
||||||
<CheckboxSelect
|
|
||||||
data={tableColumnsData}
|
|
||||||
onChange={onChangeTableColumns}
|
|
||||||
value={tableColumns}
|
|
||||||
/>
|
|
||||||
</ScrollArea>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type GridConfigProps = Pick<
|
|
||||||
ListConfigMenuProps,
|
|
||||||
'itemGap' | 'itemSize' | 'onChangeItemGap' | 'onChangeItemSize'
|
|
||||||
>;
|
|
||||||
|
|
||||||
const GridConfig = ({ itemSize, onChangeItemGap, onChangeItemSize }: GridConfigProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (!onChangeItemGap || !onChangeItemSize || !itemSize) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Table variant="vertical" withColumnBorders withRowBorders withTableBorder>
|
|
||||||
<Table.Tbody>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th w="50%">
|
|
||||||
{t('table.config.general.gap', {
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
})}
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Td>
|
|
||||||
<Slider
|
|
||||||
defaultValue={itemSize}
|
|
||||||
max={30}
|
|
||||||
min={0}
|
|
||||||
onChangeEnd={onChangeItemGap}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th w="50%">
|
|
||||||
{t('table.config.general.size', {
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
})}
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Td>
|
|
||||||
<Slider
|
|
||||||
defaultValue={itemSize}
|
|
||||||
max={300}
|
|
||||||
min={135}
|
|
||||||
onChangeEnd={onChangeItemSize}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
}
|
||||||
@@ -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: (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('table.config.general.pagination_infinite', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: 'infinite',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('table.config.general.pagination_paginate', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: 'paginate',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
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: (
|
||||||
|
<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', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
<Badge>{list.itemsPerPage}</Badge>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('table.config.general.size_compact', {
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: 'compact',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('table.config.general.size_default', {
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('table.config.general.size_large', {
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: 'large',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
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: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(e) =>
|
||||||
|
setList(listKey, { table: { enableRowHoverHighlight: e } })
|
||||||
|
}
|
||||||
|
value={list.table.enableRowHoverHighlight}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'enableRowHoverHighlight',
|
||||||
|
label: t('table.config.general.rowHoverHighlight', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(e) =>
|
||||||
|
setList(listKey, { table: { enableAlternateRowColors: e } })
|
||||||
|
}
|
||||||
|
value={list.table.enableAlternateRowColors}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'enableAlternateRowColors',
|
||||||
|
label: t('table.config.general.alternateRowColors', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(e) =>
|
||||||
|
setList(listKey, { table: { enableHorizontalBorders: e } })
|
||||||
|
}
|
||||||
|
value={list.table.enableHorizontalBorders}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'enableHorizontalBorders',
|
||||||
|
label: t('table.config.general.horizontalBorders', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(e) => 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 (
|
||||||
|
<>
|
||||||
|
<ListConfigTable options={options} />
|
||||||
|
<TableColumnConfig
|
||||||
|
data={tableColumnsData}
|
||||||
|
listKey={listKey}
|
||||||
|
onChange={(columns) =>
|
||||||
|
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<string, string>,
|
||||||
|
);
|
||||||
|
}, [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 (
|
||||||
|
<Stack gap="xs">
|
||||||
|
{value.map((item) => (
|
||||||
|
<motion.div className={styles.item} key={item.id} layout>
|
||||||
|
<Group wrap="nowrap">
|
||||||
|
<Checkbox
|
||||||
|
checked={item.isEnabled}
|
||||||
|
id={item.id}
|
||||||
|
label={labelMap[item.id]}
|
||||||
|
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>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user