advanced table

This commit is contained in:
estifanos
2026-07-25 08:41:27 +00:00
parent 4c412a9fba
commit af85fca02a

View File

@@ -1,16 +1,16 @@
import { ReactNode, useEffect, useState } from 'react';
import { ReactNode, useState } from 'react';
import {
Table,
TextInput,
ActionIcon,
Button,
Menu,
Checkbox,
Group,
Text,
Pagination,
Loader,
Center,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconSearch, IconRefresh } from '@tabler/icons-react';
import { IconRefresh, IconEye } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
export interface AdvancedColumn<T> {
@@ -20,6 +20,8 @@ export interface AdvancedColumn<T> {
cell?: (ctx: { row: { original: T }; value: unknown }) => ReactNode;
size?: number;
align?: 'left' | 'center' | 'right';
/** Whether column starts visible. Default true. */
enabled?: boolean;
}
interface AdvancedTableProps<T> {
@@ -56,24 +58,43 @@ export function AdvancedTable<T extends { id?: string | number }>({
onPageChange,
pageSize = 10,
refresh,
onSearchChange,
isLoading = false,
emptyText,
}: AdvancedTableProps<T>) {
const { t } = useTranslation();
const [visible, setVisible] = useState<boolean[]>(columns.map((c) => c.enabled ?? true));
const toggleColumn = (i: number) =>
setVisible((prev) => prev.map((v, idx) => (idx === i ? !v : v)));
const shownColumns = columns.filter((_, i) => visible[i] ?? true);
return (
<div>
<Group justify="space-between" mb="sm">
<Text fw={600}>{tableName}</Text>
<Group gap="xs">
{onSearchChange && (
<SearchBox tableName={tableName} onSearchChange={onSearchChange} />
)}
<Menu closeOnItemClick={false} shadow="md" position="bottom-end">
<Menu.Target>
<Button variant="subtle" size="sm" leftSection={<IconEye size={16} />}>
{t('common.view', 'View')}
</Button>
</Menu.Target>
<Menu.Dropdown>
{columns.map((col, i) => (
<Menu.Item key={i} onClick={() => toggleColumn(i)}>
<Checkbox
label={col.header}
checked={visible[i] ?? true}
onChange={() => toggleColumn(i)}
readOnly
/>
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
{refresh && (
<ActionIcon variant="subtle" onClick={refresh} title={t('common.refresh', 'Refresh')}>
<IconRefresh size={16} />
</ActionIcon>
<Button variant="subtle" size="sm" rightSection={<IconRefresh size={16} />} onClick={refresh}>
{t('common.refresh', 'Refresh')}
</Button>
)}
</Group>
</Group>
@@ -81,7 +102,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map((col, i) => (
{shownColumns.map((col, i) => (
<Table.Th key={i} style={{ width: col.size, textAlign: col.align }}>
{col.header}
</Table.Th>
@@ -91,7 +112,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
<Table.Tbody>
{isLoading ? (
<Table.Tr>
<Table.Td colSpan={columns.length}>
<Table.Td colSpan={shownColumns.length}>
<Center py="xl">
<Loader size="sm" />
</Center>
@@ -99,7 +120,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
</Table.Tr>
) : data.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={columns.length}>
<Table.Td colSpan={shownColumns.length}>
<Text c="dimmed" ta="center" py="xl">
{emptyText ?? t('common.noResult', 'No results')}
</Text>
@@ -108,7 +129,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
) : (
data.map((row, rowIndex) => (
<Table.Tr key={row.id ?? rowIndex}>
{columns.map((col, i) => {
{shownColumns.map((col, i) => {
const value = getByPath(row, col.accessorKey);
return (
<Table.Td key={i} style={{ textAlign: col.align }}>
@@ -135,29 +156,3 @@ export function AdvancedTable<T extends { id?: string | number }>({
</div>
);
}
function SearchBox({
tableName,
onSearchChange,
}: {
tableName: string;
onSearchChange: (q: string) => void;
}) {
const { t } = useTranslation();
const [value, setValue] = useState('');
const [debounced] = useDebouncedValue(value, 300);
useEffect(() => {
onSearchChange(debounced);
}, [debounced, onSearchChange]);
return (
<TextInput
placeholder={t('common.filterPlaceholder', { name: tableName, defaultValue: `Filter ${tableName}...` })}
leftSection={<IconSearch size={14} />}
size="sm"
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
);
}