mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 06:44:58 +00:00
Merge pull request #383 from johndoe6345789/codex/create-fieldgroup-and-validationsummary-components
Add shared data form and table components
This commit is contained in:
50
frontends/nextjs/src/data/form/FieldGroup.tsx
Normal file
50
frontends/nextjs/src/data/form/FieldGroup.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
|
||||
interface FieldGroupProps {
|
||||
title: string
|
||||
description?: ReactNode
|
||||
actions?: ReactNode
|
||||
children: ReactNode
|
||||
spacing?: number
|
||||
}
|
||||
|
||||
export function FieldGroup({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
spacing = 2,
|
||||
}: FieldGroupProps) {
|
||||
return (
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 2,
|
||||
p: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" spacing={2} alignItems="flex-start">
|
||||
<Box>
|
||||
<Typography variant="subtitle1" fontWeight={600} sx={{ lineHeight: 1.3 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{description ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
{actions ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>{actions}</Box>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={spacing}>{children}</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
53
frontends/nextjs/src/data/form/ValidationSummary.tsx
Normal file
53
frontends/nextjs/src/data/form/ValidationSummary.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Alert, AlertTitle, List, ListItem, ListItemText } from '@mui/material'
|
||||
|
||||
/**
|
||||
* Props for {@link ValidationSummary}.
|
||||
*
|
||||
* @property errors List of validation errors to display. Each entry can be a plain
|
||||
* string message or a ReactNode for fully custom rendering (e.g. including links
|
||||
* or emphasized text). The list is rendered as items in a bulleted list.
|
||||
* @property title Optional title rendered inside the error alert header. This
|
||||
* is only shown when {@link ValidationSummaryProps.showTitle | showTitle} is
|
||||
* `true`. Defaults to `"Please fix the following"`.
|
||||
* @property showTitle When `true` (default), renders an {@link AlertTitle} using
|
||||
* the provided `title`. Set to `false` when you want to suppress the header and
|
||||
* only show the list of errors.
|
||||
*/
|
||||
interface ValidationSummaryProps {
|
||||
errors: Array<string | ReactNode>
|
||||
title?: string
|
||||
showTitle?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a standardized validation error summary as an error {@link Alert}
|
||||
* containing an optional title and a bulleted list of errors.
|
||||
*
|
||||
* Renders nothing when the `errors` array is empty.
|
||||
*/
|
||||
export function ValidationSummary({
|
||||
errors,
|
||||
title = 'Please fix the following',
|
||||
showTitle = true,
|
||||
}: ValidationSummaryProps) {
|
||||
if (!errors.length) return null
|
||||
|
||||
return (
|
||||
<Alert severity="error" variant="outlined" sx={{ alignItems: 'flex-start' }}>
|
||||
{showTitle ? <AlertTitle>{title}</AlertTitle> : null}
|
||||
<List dense disablePadding component="ul" sx={{ pl: 3 }}>
|
||||
{errors.map((error, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
disableGutters
|
||||
component="li"
|
||||
sx={{ py: 0.25, px: 0 }}
|
||||
>
|
||||
<ListItemText primaryTypographyProps={{ variant: 'body2' }} primary={error} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
59
frontends/nextjs/src/data/table/Body.tsx
Normal file
59
frontends/nextjs/src/data/table/Body.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { TableBody, TableCell, TableRow } from '@mui/material'
|
||||
|
||||
import { EmptyState } from './EmptyState'
|
||||
import type { DataTableColumn } from './types'
|
||||
|
||||
interface BodyProps<T> {
|
||||
columns: Array<DataTableColumn<T>>
|
||||
rows: T[]
|
||||
getRowId?: (row: T, rowIndex: number) => string | number
|
||||
renderActions?: (row: T) => ReactNode
|
||||
onRowClick?: (row: T) => void
|
||||
emptyMessage?: string
|
||||
}
|
||||
|
||||
export function Body<T>({
|
||||
columns,
|
||||
rows,
|
||||
getRowId,
|
||||
renderActions,
|
||||
onRowClick,
|
||||
emptyMessage = 'No records found',
|
||||
}: BodyProps<T>) {
|
||||
const colSpan = columns.length + (renderActions ? 1 : 0)
|
||||
|
||||
return (
|
||||
<TableBody>
|
||||
{rows.length === 0 ? (
|
||||
<EmptyState colSpan={colSpan} message={emptyMessage} />
|
||||
) : (
|
||||
rows.map((row, rowIndex) => {
|
||||
const rowId = getRowId ? getRowId(row, rowIndex) : rowIndex
|
||||
const handleClick = onRowClick ? () => onRowClick(row) : undefined
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={rowId}
|
||||
hover={Boolean(onRowClick)}
|
||||
onClick={handleClick}
|
||||
sx={onRowClick ? { cursor: 'pointer' } : undefined}
|
||||
>
|
||||
{columns.map((column) => {
|
||||
const content = column.render ? column.render(row, rowIndex) : (row as Record<string, unknown>)[column.key]
|
||||
|
||||
return (
|
||||
<TableCell key={column.key} align={column.align} sx={column.sx}>
|
||||
{content ?? '—'}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
|
||||
{renderActions ? <TableCell align="right">{renderActions(row)}</TableCell> : null}
|
||||
</TableRow>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
)
|
||||
}
|
||||
40
frontends/nextjs/src/data/table/EmptyState.tsx
Normal file
40
frontends/nextjs/src/data/table/EmptyState.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Stack, TableCell, TableRow, Typography } from '@mui/material'
|
||||
|
||||
/**
|
||||
* Props for the {@link EmptyState} table row.
|
||||
*
|
||||
* This is typically used when a data-driven table has no rows to display and
|
||||
* you want to show a friendly message, optionally with a follow-up action.
|
||||
*
|
||||
* @property colSpan - Number of table columns the empty-state cell should span.
|
||||
* @property message - Optional message shown to explain that there is no data.
|
||||
* @property action - Optional call-to-action content (for example, a "Create"
|
||||
* button or "Reload" button) rendered below the message.
|
||||
*/
|
||||
interface EmptyStateProps {
|
||||
colSpan: number
|
||||
message?: string
|
||||
action?: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a full-width table row indicating that there is currently no data
|
||||
* to display for the surrounding table.
|
||||
*
|
||||
* Use this component as the only row in a table body when the data source is
|
||||
* empty. You can pass an optional `action` to surface primary actions such as
|
||||
* creating a new item or retrying a failed load.
|
||||
*/
|
||||
export function EmptyState({ colSpan, message = 'No data to display', action }: EmptyStateProps) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={colSpan} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<Typography variant="subtitle1">{message}</Typography>
|
||||
{action ? <Stack direction="row" spacing={1}>{action}</Stack> : null}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
52
frontends/nextjs/src/data/table/Header.tsx
Normal file
52
frontends/nextjs/src/data/table/Header.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { TableCell, TableHead, TableRow, Typography } from '@mui/material'
|
||||
|
||||
import type { DataTableColumn } from './types'
|
||||
|
||||
interface HeaderProps<T> {
|
||||
columns: Array<DataTableColumn<T>>
|
||||
actionsHeader?: string
|
||||
}
|
||||
|
||||
export function Header<T>({ columns, actionsHeader }: HeaderProps<T>) {
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableCell
|
||||
key={column.key}
|
||||
align={column.align}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
...(column.width ? { width: column.width } : {}),
|
||||
...(column.sx ?? {}),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ textTransform: 'uppercase', letterSpacing: 0.2 }}
|
||||
>
|
||||
{column.label}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
{actionsHeader ? (
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{ borderBottom: 1, borderColor: 'divider', width: 1, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ textTransform: 'uppercase', letterSpacing: 0.2 }}
|
||||
>
|
||||
{actionsHeader}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
) : null}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
)
|
||||
}
|
||||
11
frontends/nextjs/src/data/table/types.ts
Normal file
11
frontends/nextjs/src/data/table/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { TableCellProps } from '@mui/material'
|
||||
|
||||
export interface DataTableColumn<T> {
|
||||
key: string
|
||||
label: string
|
||||
align?: TableCellProps['align']
|
||||
width?: number | string
|
||||
render?: (row: T, rowIndex: number) => ReactNode
|
||||
sx?: TableCellProps['sx']
|
||||
}
|
||||
Reference in New Issue
Block a user