From 8918cca6e4d35f261df62179c2430e6dacaccf92 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 29 Dec 2025 16:34:04 +0000 Subject: [PATCH 1/4] feat: add shared data form and table components --- frontends/nextjs/src/data/form/FieldGroup.tsx | 50 ++++++++++++++++ .../src/data/form/ValidationSummary.tsx | 33 +++++++++++ frontends/nextjs/src/data/table/Body.tsx | 59 +++++++++++++++++++ .../nextjs/src/data/table/EmptyState.tsx | 21 +++++++ frontends/nextjs/src/data/table/Header.tsx | 52 ++++++++++++++++ frontends/nextjs/src/data/table/types.ts | 11 ++++ 6 files changed, 226 insertions(+) create mode 100644 frontends/nextjs/src/data/form/FieldGroup.tsx create mode 100644 frontends/nextjs/src/data/form/ValidationSummary.tsx create mode 100644 frontends/nextjs/src/data/table/Body.tsx create mode 100644 frontends/nextjs/src/data/table/EmptyState.tsx create mode 100644 frontends/nextjs/src/data/table/Header.tsx create mode 100644 frontends/nextjs/src/data/table/types.ts diff --git a/frontends/nextjs/src/data/form/FieldGroup.tsx b/frontends/nextjs/src/data/form/FieldGroup.tsx new file mode 100644 index 000000000..67ad752ca --- /dev/null +++ b/frontends/nextjs/src/data/form/FieldGroup.tsx @@ -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 ( + + + + + {title} + + {description ? ( + + {description} + + ) : null} + + + {actions ? ( + {actions} + ) : null} + + + {children} + + ) +} diff --git a/frontends/nextjs/src/data/form/ValidationSummary.tsx b/frontends/nextjs/src/data/form/ValidationSummary.tsx new file mode 100644 index 000000000..c73e6ced6 --- /dev/null +++ b/frontends/nextjs/src/data/form/ValidationSummary.tsx @@ -0,0 +1,33 @@ +import type { ReactNode } from 'react' +import { Alert, AlertTitle, List, ListItem, ListItemText } from '@mui/material' + +interface ValidationSummaryProps { + errors: Array + title?: string + showTitle?: boolean +} + +export function ValidationSummary({ + errors, + title = 'Please fix the following', + showTitle = true, +}: ValidationSummaryProps) { + if (!errors.length) return null + + return ( + + {showTitle ? {title} : null} + + {errors.map((error, index) => ( + + + + ))} + + + ) +} diff --git a/frontends/nextjs/src/data/table/Body.tsx b/frontends/nextjs/src/data/table/Body.tsx new file mode 100644 index 000000000..80225d390 --- /dev/null +++ b/frontends/nextjs/src/data/table/Body.tsx @@ -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 { + columns: Array> + rows: T[] + getRowId?: (row: T, rowIndex: number) => string | number + renderActions?: (row: T) => ReactNode + onRowClick?: (row: T) => void + emptyMessage?: string +} + +export function Body({ + columns, + rows, + getRowId, + renderActions, + onRowClick, + emptyMessage = 'No records found', +}: BodyProps) { + const colSpan = columns.length + (renderActions ? 1 : 0) + + return ( + + {rows.length === 0 ? ( + + ) : ( + rows.map((row, rowIndex) => { + const rowId = getRowId ? getRowId(row, rowIndex) : rowIndex + const handleClick = onRowClick ? () => onRowClick(row) : undefined + + return ( + + {columns.map((column) => { + const content = column.render ? column.render(row, rowIndex) : (row as Record)[column.key] + + return ( + + {content ?? '—'} + + ) + })} + + {renderActions ? {renderActions(row)} : null} + + ) + }) + )} + + ) +} diff --git a/frontends/nextjs/src/data/table/EmptyState.tsx b/frontends/nextjs/src/data/table/EmptyState.tsx new file mode 100644 index 000000000..c4db581ea --- /dev/null +++ b/frontends/nextjs/src/data/table/EmptyState.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react' +import { Stack, TableCell, TableRow, Typography } from '@mui/material' + +interface EmptyStateProps { + colSpan: number + message?: string + action?: ReactNode +} + +export function EmptyState({ colSpan, message = 'No data to display', action }: EmptyStateProps) { + return ( + + + + {message} + {action ? {action} : null} + + + + ) +} diff --git a/frontends/nextjs/src/data/table/Header.tsx b/frontends/nextjs/src/data/table/Header.tsx new file mode 100644 index 000000000..a7c9f6602 --- /dev/null +++ b/frontends/nextjs/src/data/table/Header.tsx @@ -0,0 +1,52 @@ +import { TableCell, TableHead, TableRow, Typography } from '@mui/material' + +import type { DataTableColumn } from './types' + +interface HeaderProps { + columns: Array> + actionsHeader?: string +} + +export function Header({ columns, actionsHeader }: HeaderProps) { + return ( + + + {columns.map((column) => ( + + + {column.label} + + + ))} + + {actionsHeader ? ( + + + {actionsHeader} + + + ) : null} + + + ) +} diff --git a/frontends/nextjs/src/data/table/types.ts b/frontends/nextjs/src/data/table/types.ts new file mode 100644 index 000000000..d0b94318f --- /dev/null +++ b/frontends/nextjs/src/data/table/types.ts @@ -0,0 +1,11 @@ +import type { ReactNode } from 'react' +import type { TableCellProps } from '@mui/material' + +export interface DataTableColumn { + key: string + label: string + align?: TableCellProps['align'] + width?: number | string + render?: (row: T, rowIndex: number) => ReactNode + sx?: TableCellProps['sx'] +} From 499c2775010bb3caabcc188b6077846e38264b13 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 29 Dec 2025 17:11:51 +0000 Subject: [PATCH 2/4] Update frontends/nextjs/src/data/form/ValidationSummary.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontends/nextjs/src/data/form/ValidationSummary.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontends/nextjs/src/data/form/ValidationSummary.tsx b/frontends/nextjs/src/data/form/ValidationSummary.tsx index c73e6ced6..5a8eded15 100644 --- a/frontends/nextjs/src/data/form/ValidationSummary.tsx +++ b/frontends/nextjs/src/data/form/ValidationSummary.tsx @@ -17,12 +17,13 @@ export function ValidationSummary({ return ( {showTitle ? {title} : null} - + {errors.map((error, index) => ( From 675c8d9b82711bb66ab2242d8b416f444c54fd4b Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 29 Dec 2025 17:12:03 +0000 Subject: [PATCH 3/4] Update frontends/nextjs/src/data/form/ValidationSummary.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/data/form/ValidationSummary.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/frontends/nextjs/src/data/form/ValidationSummary.tsx b/frontends/nextjs/src/data/form/ValidationSummary.tsx index 5a8eded15..57eb90278 100644 --- a/frontends/nextjs/src/data/form/ValidationSummary.tsx +++ b/frontends/nextjs/src/data/form/ValidationSummary.tsx @@ -1,12 +1,31 @@ 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 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', From fdb83483ebc2aad9f99d2b7436621182ae8d4482 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 29 Dec 2025 17:12:17 +0000 Subject: [PATCH 4/4] Update frontends/nextjs/src/data/table/EmptyState.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../nextjs/src/data/table/EmptyState.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/frontends/nextjs/src/data/table/EmptyState.tsx b/frontends/nextjs/src/data/table/EmptyState.tsx index c4db581ea..b79247aaa 100644 --- a/frontends/nextjs/src/data/table/EmptyState.tsx +++ b/frontends/nextjs/src/data/table/EmptyState.tsx @@ -1,12 +1,31 @@ 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 (