mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
advanced table
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user