mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat(vessel-registration): add vessel registration page with incident reporting and certificate download functionality
feat(waiver): implement waiver page with application tracking and letter download feature feat(ui): introduce AdvancedTable component for enhanced table functionality across the application chore: update package-lock.json to remove unnecessary dependencies
This commit is contained in:
@@ -13,3 +13,4 @@ export * from './lib/layout/BrandAvatar';
|
||||
export * from './lib/layout/ColorSchemeToggle';
|
||||
export * from './lib/layout/LanguageSwitcher';
|
||||
export * from './lib/layout/PageHeader';
|
||||
export * from './lib/table/AdvancedTable';
|
||||
|
||||
261
libs/ui/src/lib/table/AdvancedTable.tsx
Normal file
261
libs/ui/src/lib/table/AdvancedTable.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Checkbox,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Pagination,
|
||||
Table,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
type MantineSpacing,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconRefresh,
|
||||
IconSelector,
|
||||
} from '@tabler/icons-react';
|
||||
import { EmptyState } from '../feedback/EmptyState';
|
||||
|
||||
export interface AdvancedTableColumn<T> {
|
||||
/** Unique column id; doubles as the sort field sent to `sort.onSort`. */
|
||||
key: string;
|
||||
header: ReactNode;
|
||||
/** Cell renderer. Defaults to reading `row[key]`. */
|
||||
render?: (row: T) => ReactNode;
|
||||
sortable?: boolean;
|
||||
width?: number | string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
export interface AdvancedTableAction<T> {
|
||||
key: string;
|
||||
/** Shown as tooltip and aria-label. */
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
color?: string;
|
||||
hidden?: (row: T) => boolean;
|
||||
disabled?: (row: T) => boolean;
|
||||
onClick: (row: T) => void;
|
||||
}
|
||||
|
||||
export interface AdvancedTableSort {
|
||||
sortBy?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
onSort: (field: string) => void;
|
||||
}
|
||||
|
||||
export interface AdvancedTablePagination {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
export interface AdvancedTableSelection {
|
||||
selected: string[];
|
||||
onChange: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
export interface AdvancedTableProps<T> {
|
||||
columns: AdvancedTableColumn<T>[];
|
||||
data: T[];
|
||||
rowKey: (row: T) => string;
|
||||
actions?: AdvancedTableAction<T>[];
|
||||
sort?: AdvancedTableSort;
|
||||
pagination?: AdvancedTablePagination;
|
||||
/** Controlled row selection (checkbox column); ids come from `rowKey`. */
|
||||
selection?: AdvancedTableSelection;
|
||||
loading?: boolean;
|
||||
/** Min table width before horizontal scroll kicks in. */
|
||||
minWidth?: number;
|
||||
/** Row density, e.g. 4 (compact) or 'sm' (comfortable). */
|
||||
verticalSpacing?: MantineSpacing;
|
||||
/** Per-row style override (e.g. focused-row highlight). */
|
||||
rowStyle?: (row: T) => CSSProperties | undefined;
|
||||
/** Rendered above the table, left-aligned (filters, search, tabs…). */
|
||||
toolbar?: ReactNode;
|
||||
/** Shows a refresh button above the table, right-aligned. */
|
||||
onRefresh?: () => void;
|
||||
emptyTitle?: string;
|
||||
emptyDescription?: string;
|
||||
onRowClick?: (row: T) => void;
|
||||
}
|
||||
|
||||
function SortableHeader({
|
||||
column,
|
||||
sort,
|
||||
}: {
|
||||
column: AdvancedTableColumn<never>;
|
||||
sort: AdvancedTableSort;
|
||||
}) {
|
||||
const active = sort.sortBy === column.key;
|
||||
const Icon = active
|
||||
? sort.sortDir === 'desc'
|
||||
? IconChevronDown
|
||||
: IconChevronUp
|
||||
: IconSelector;
|
||||
return (
|
||||
<UnstyledButton onClick={() => sort.onSort(column.key)} fz="sm" fw={700}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{column.header}
|
||||
<Icon size={14} stroke={1.5} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic data table: column definitions and row actions come from the
|
||||
* consumer as config (typically `*Columns.tsx` + `*ColumnActions.tsx` files);
|
||||
* filters, search and refresh live in the parent component above the table.
|
||||
* Sorting and pagination are controlled — the parent owns the state (URL,
|
||||
* query params) and refetches.
|
||||
*/
|
||||
export function AdvancedTable<T>({
|
||||
columns,
|
||||
data,
|
||||
rowKey,
|
||||
actions,
|
||||
sort,
|
||||
pagination,
|
||||
selection,
|
||||
loading = false,
|
||||
minWidth = 640,
|
||||
verticalSpacing,
|
||||
rowStyle,
|
||||
toolbar,
|
||||
onRefresh,
|
||||
emptyTitle = 'Nothing here yet',
|
||||
emptyDescription,
|
||||
onRowClick,
|
||||
}: AdvancedTableProps<T>) {
|
||||
const allIds = data.map(rowKey);
|
||||
const allSelected = selection
|
||||
? allIds.length > 0 && allIds.every((id) => selection.selected.includes(id))
|
||||
: false;
|
||||
|
||||
const toolbarRow = (toolbar || onRefresh) && (
|
||||
<Group justify="space-between" mb="sm" wrap="nowrap" align="flex-end">
|
||||
<Box style={{ flex: 1 }}>{toolbar}</Box>
|
||||
{onRefresh && (
|
||||
<Tooltip label="Refresh">
|
||||
<ActionIcon variant="subtle" color="gray" onClick={onRefresh} aria-label="Refresh">
|
||||
<IconRefresh size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
|
||||
if (!loading && data.length === 0) {
|
||||
return (
|
||||
<Box>
|
||||
{toolbarRow}
|
||||
<EmptyState title={emptyTitle} description={emptyDescription} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
{toolbarRow}
|
||||
<LoadingOverlay visible={loading} zIndex={10} />
|
||||
<Table.ScrollContainer minWidth={minWidth}>
|
||||
<Table striped highlightOnHover verticalSpacing={verticalSpacing}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{selection && (
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
indeterminate={selection.selected.length > 0 && !allSelected}
|
||||
onChange={() => selection.onChange(allSelected ? [] : allIds)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</Table.Th>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
<Table.Th key={col.key} w={col.width} ta={col.align}>
|
||||
{col.sortable && sort ? (
|
||||
<SortableHeader column={col as AdvancedTableColumn<never>} sort={sort} />
|
||||
) : (
|
||||
col.header
|
||||
)}
|
||||
</Table.Th>
|
||||
))}
|
||||
{actions && <Table.Th w={1} />}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.map((row) => (
|
||||
<Table.Tr
|
||||
key={rowKey(row)}
|
||||
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
||||
style={{
|
||||
...(onRowClick ? { cursor: 'pointer' } : undefined),
|
||||
...rowStyle?.(row),
|
||||
}}
|
||||
>
|
||||
{selection && (
|
||||
<Table.Td onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={selection.selected.includes(rowKey(row))}
|
||||
onChange={(e) => {
|
||||
const id = rowKey(row);
|
||||
selection.onChange(
|
||||
e.currentTarget.checked
|
||||
? [...selection.selected, id]
|
||||
: selection.selected.filter((s) => s !== id),
|
||||
);
|
||||
}}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
</Table.Td>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
<Table.Td key={col.key} ta={col.align}>
|
||||
{col.render
|
||||
? col.render(row)
|
||||
: ((row as Record<string, unknown>)[col.key] as ReactNode)}
|
||||
</Table.Td>
|
||||
))}
|
||||
{actions && (
|
||||
<Table.Td onClick={(e) => e.stopPropagation()}>
|
||||
<Group gap="xs" wrap="nowrap" justify="flex-end">
|
||||
{actions
|
||||
.filter((a) => !a.hidden?.(row))
|
||||
.map((a) => (
|
||||
<Tooltip key={a.key} label={a.label}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color={a.color}
|
||||
disabled={a.disabled?.(row)}
|
||||
aria-label={a.label}
|
||||
onClick={() => a.onClick(row)}
|
||||
>
|
||||
{a.icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Pagination
|
||||
value={pagination.page}
|
||||
total={pagination.totalPages}
|
||||
onChange={pagination.onPageChange}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user