Genesis
This commit is contained in:
196
src/lists/RecordList.jsx
Normal file
196
src/lists/RecordList.jsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { Component } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import { PanelTile, PanelTileGrid } from '../panels';
|
||||
|
||||
/**
|
||||
* Generic data-list GUI component — pure presentation, no data layer.
|
||||
*
|
||||
* Renders a PanelTileGrid > PanelTile with an optional create button, an empty
|
||||
* state, and a table of rows with per-row edit/delete actions. Everything data
|
||||
* related is injected via props, so this component never touches a storage
|
||||
* manager, router, toaster, or backend error formatter.
|
||||
*
|
||||
* @component RecordList
|
||||
* @import dashgui/lists
|
||||
*
|
||||
* @prop {'loading' | 'ready' | 'failed'} status - Controls which state renders: a centered spinner, the table, or the `error` node.
|
||||
* @prop {object[]} rows - Row objects to render. Each row needs the `rowKey` field for the React key.
|
||||
* @prop {Array<{key: string, label: ReactNode, render?: (row) => ReactNode, align?: string, padding?: string}>} columns - Column definitions. `key` is the row field (or used with `render`); `render(row)` customizes the cell; `align`/`padding` map to TableCell.
|
||||
* @prop {ReactNode} [error] - Node rendered when `status === "failed"` (e.g. a formatted backend error).
|
||||
* @prop {string} [rowKey=id] - Field used as the row identity / React key.
|
||||
* @prop {string} [createLabel] - Label of the top-right create button. Omit to hide the button entirely.
|
||||
* @prop {() => void} [onCreate] - Called when the create button is clicked.
|
||||
* @prop {(row) => void} [onEdit] - Called when a row is clicked or its edit button is pressed. Omit to hide the edit button and row-click affordance.
|
||||
* @prop {(row) => void} [onDelete] - Called when a row's delete button is pressed. Omit to hide the delete button.
|
||||
* @prop {boolean} [deleteDisabled=false] - Disables the delete buttons — e.g. `busyId != null` while a delete is in flight.
|
||||
* @prop {ReactNode} [emptyMessage] - Shown instead of the table when `rows` is empty. Omit to render nothing.
|
||||
* @prop {string} [color] - Background color of the wrapping PanelTile.
|
||||
* @prop {number | string} [minHeight=240] - Minimum height of the wrapping PanelTile.
|
||||
* @prop {(row) => string} [getEditLabel] - aria-label for each edit button (defaults to `„<id>“ bearbeiten`).
|
||||
* @prop {(row) => string} [getDeleteLabel] - aria-label for each delete button (defaults to `„<id>“ löschen`).
|
||||
* @prop {object} [sx] - Extra styles passed to the wrapping PanelTile.
|
||||
*
|
||||
* @behaviour `status === "loading"` renders only a centered CircularProgress (no panel chrome).
|
||||
* @behaviour `status === "failed"` renders only the `error` node — or nothing if `error` is omitted.
|
||||
* @behaviour Clicking a row calls `onEdit(row)` (only when `onEdit` is provided); the action buttons stop propagation so they do not double-trigger the row click.
|
||||
* @behaviour The actions column appears only if `onEdit` or `onDelete` is provided.
|
||||
* @behaviour No data layer: this component never touches storage, routing, toasters, or backend error formatting.
|
||||
*/
|
||||
class RecordList extends Component {
|
||||
handleRowClick(row) {
|
||||
this.props.onEdit?.(row);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderCreateButton() {
|
||||
const { createLabel, onCreate } = this.props;
|
||||
if (!createLabel) return null;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={onCreate}
|
||||
>
|
||||
{createLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
const { emptyMessage } = this.props;
|
||||
if (!emptyMessage) return null;
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{emptyMessage}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
const {
|
||||
rows,
|
||||
columns,
|
||||
rowKey = 'id',
|
||||
onEdit,
|
||||
onDelete,
|
||||
deleteDisabled,
|
||||
getEditLabel,
|
||||
getDeleteLabel,
|
||||
} = this.props;
|
||||
|
||||
const hasActions = Boolean(onEdit || onDelete);
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col.key} align={col.align} padding={col.padding}>
|
||||
{col.label}
|
||||
</TableCell>
|
||||
))}
|
||||
{hasActions ? <TableCell padding="checkbox" /> : null}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row) => {
|
||||
const id = row[rowKey];
|
||||
return (
|
||||
<TableRow
|
||||
key={id}
|
||||
hover
|
||||
sx={{ cursor: onEdit ? 'pointer' : undefined }}
|
||||
onClick={() => this.handleRowClick(row)}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
key={col.key}
|
||||
align={col.align}
|
||||
padding={col.padding}
|
||||
>
|
||||
{col.render ? col.render(row) : row[col.key]}
|
||||
</TableCell>
|
||||
))}
|
||||
{hasActions ? (
|
||||
<TableCell
|
||||
padding="checkbox"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{onEdit ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={
|
||||
getEditLabel?.(row) ?? `„${id}“ bearbeiten`
|
||||
}
|
||||
onClick={() => onEdit(row)}
|
||||
>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
) : null}
|
||||
{onDelete ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={
|
||||
getDeleteLabel?.(row) ?? `„${id}“ löschen`
|
||||
}
|
||||
disabled={deleteDisabled}
|
||||
onClick={() => onDelete(row)}
|
||||
>
|
||||
<DeleteOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</TableCell>
|
||||
) : null}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, rows, color, minHeight = 240, sx } = this.props;
|
||||
|
||||
if (status === 'loading') return this.renderLoading();
|
||||
if (status === 'failed') return error ?? null;
|
||||
|
||||
return (
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={minHeight} color={color} sx={sx}>
|
||||
<Stack spacing={2}>
|
||||
{this.renderCreateButton()}
|
||||
{rows.length === 0 ? this.renderEmpty() : this.renderTable()}
|
||||
</Stack>
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RecordList;
|
||||
1
src/lists/index.js
Normal file
1
src/lists/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as RecordList } from './RecordList';
|
||||
Reference in New Issue
Block a user