refactor: add shared detail tab table wrapper

This commit is contained in:
Kari Lintulaakso
2026-02-17 14:20:48 +02:00
parent da61ce10cc
commit 69d2100bec
5 changed files with 262 additions and 80 deletions
@@ -0,0 +1,79 @@
import { describe, expect, it, jest, beforeEach } from '@jest/globals'
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { DetailTabTable } from './DetailTabTable'
const tableViewMock = jest.fn<(props: Record<string, unknown>) => JSX.Element>()
const useMaterialReactTableMock = jest.fn<(options: Record<string, unknown>) => Record<string, unknown>>()
jest.mock('@/components/TableView/TableView', () => ({
TableView: (props: Record<string, unknown>) => tableViewMock(props),
}))
jest.mock('material-react-table', () => ({
useMaterialReactTable: (options: Record<string, unknown>) => useMaterialReactTableMock(options),
MaterialReactTable: () => <div data-testid="material-react-table" />,
}))
type TestRow = {
id: number
name: string
}
describe('DetailTabTable', () => {
beforeEach(() => {
tableViewMock.mockReset()
useMaterialReactTableMock.mockReset()
tableViewMock.mockReturnValue(<div data-testid="table-view" />)
useMaterialReactTableMock.mockReturnValue({})
})
it('maps read mode props to TableView', () => {
render(
<DetailTabTable<TestRow>
mode="read"
title="Read table"
data={[{ id: 1, name: 'Alpha' }]}
columns={[
{ accessorKey: 'id', header: 'Id' },
{ accessorKey: 'name', header: 'Name' },
]}
idFieldName="id"
url="species"
isFetching={false}
enableColumnFilterModes={true}
/>
)
expect(screen.getByTestId('table-view')).toBeTruthy()
expect(tableViewMock).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Read table',
idFieldName: 'id',
url: 'species',
enableColumnFilterModes: true,
})
)
})
it('renders editable mode via MaterialReactTable setup', () => {
render(
<DetailTabTable<TestRow>
mode="edit"
data={[{ id: 1, name: 'Alpha' }]}
columns={[{ accessorKey: 'name', header: 'Name' }]}
enableSorting={false}
enableColumnActions={false}
/>
)
expect(screen.getByTestId('material-react-table')).toBeTruthy()
expect(useMaterialReactTableMock).toHaveBeenCalledWith(
expect.objectContaining({
enableSorting: false,
enableColumnActions: false,
enablePagination: true,
})
)
})
})
@@ -0,0 +1,145 @@
import { useState, type ReactNode } from 'react'
import {
MaterialReactTable,
MRT_PaginationState,
MRT_Row,
MRT_RowData,
MRT_TableOptions,
MRT_VisibilityState,
type MRT_ColumnDef,
useMaterialReactTable,
} from 'material-react-table'
import { TableView } from '@/components/TableView/TableView'
const defaultEditPagination: MRT_PaginationState = { pageIndex: 0, pageSize: 15 }
type ReadOrSelectMode = 'read' | 'select'
type DetailTabTableReadSelectProps<T extends MRT_RowData> = {
mode: ReadOrSelectMode
data?: T[] | null
columns: MRT_ColumnDef<T>[]
title: string
visibleColumns?: MRT_VisibilityState
idFieldName: keyof T
isFetching?: boolean
isError?: boolean
checkRowRestriction?: (row: T) => boolean
selectorFn?: (id: T) => void
tableRowAction?: (row: T) => void
url?: string
clickableRows?: boolean
enableColumnFilterModes?: boolean
paginationPlacement?: 'top' | 'bottom' | 'both'
}
type DetailTabTableEditProps<T extends MRT_RowData> = {
mode: 'edit'
data: T[]
columns: MRT_ColumnDef<T>[]
enableSorting?: boolean
enableColumnActions?: boolean
enableTopToolbar?: boolean
positionPagination?: 'top' | 'bottom' | 'both'
paginationState?: MRT_PaginationState
onPaginationChange?: MRT_TableOptions<T>['onPaginationChange']
enableRowActions?: boolean
renderRowActions?: MRT_TableOptions<T>['renderRowActions']
muiTableBodyRowProps?: MRT_TableOptions<T>['muiTableBodyRowProps']
}
type DetailTabTableProps<T extends MRT_RowData> = DetailTabTableReadSelectProps<T> | DetailTabTableEditProps<T>
export const DetailTabTable = <T extends MRT_RowData>(props: DetailTabTableProps<T>) => {
if (props.mode === 'edit') {
return <DetailTabEditableTable {...props} />
}
const {
data,
columns,
title,
visibleColumns,
idFieldName,
isFetching = false,
selectorFn,
tableRowAction,
url,
checkRowRestriction,
clickableRows,
enableColumnFilterModes,
paginationPlacement,
isError,
} = props
const resolvedVisibleColumns: MRT_VisibilityState = visibleColumns
? visibleColumns
: columns.reduce<MRT_VisibilityState>((acc, column) => {
if (column.accessorKey) {
acc[column.accessorKey.toString()] = true
}
return acc
}, {})
return (
<TableView<T>
title={title}
idFieldName={idFieldName}
columns={columns}
visibleColumns={resolvedVisibleColumns}
data={data ?? undefined}
isFetching={isFetching}
selectorFn={selectorFn}
tableRowAction={tableRowAction}
url={url}
checkRowRestriction={checkRowRestriction}
clickableRows={clickableRows}
enableColumnFilterModes={enableColumnFilterModes}
paginationPlacement={paginationPlacement}
isError={isError}
/>
)
}
const DetailTabEditableTable = <T extends MRT_RowData>({
data,
columns,
enableSorting = false,
enableColumnActions = false,
enableTopToolbar = false,
positionPagination = 'both',
paginationState,
onPaginationChange,
enableRowActions = false,
renderRowActions,
muiTableBodyRowProps,
}: DetailTabTableEditProps<T>) => {
const [pagination, setPagination] = useState<MRT_PaginationState>(paginationState ?? defaultEditPagination)
const resolvedPagination = paginationState ?? pagination
const handlePaginationChange: MRT_TableOptions<T>['onPaginationChange'] = onPaginationChange ?? setPagination
const table = useMaterialReactTable({
columns,
data,
enableTopToolbar,
enableColumnActions,
enableSorting,
enablePagination: true,
onPaginationChange: handlePaginationChange,
positionPagination,
paginationDisplayMode: 'pages',
state: { density: 'compact', pagination: resolvedPagination },
enableRowActions,
renderRowActions,
muiTableBodyRowProps,
})
return <MaterialReactTable table={table} />
}
export type { DetailTabTableProps, DetailTabTableReadSelectProps, DetailTabTableEditProps }
export type DetailTableEditRowRenderer<T extends MRT_RowData> = (props: {
row: MRT_Row<T>
staticRowIndex?: number
}) => ReactNode
@@ -1,20 +1,13 @@
import { EditDataType, RowState } from '@/shared/types'
import { CircularProgress, Box, Button } from '@mui/material'
import {
type MRT_ColumnDef,
type MRT_RowData,
MaterialReactTable,
MRT_Row,
MRT_PaginationState,
} from 'material-react-table'
import { type MRT_ColumnDef, type MRT_Row, type MRT_RowData } from 'material-react-table'
import { useDetailContext } from '../Context/DetailContext'
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'
import { useState, useEffect } from 'react'
import { useEffect } from 'react'
import { checkFieldErrors } from './checkFieldErrors'
import { ActionComponent } from '@/components/TableView/ActionComponent'
const defaultPagination: MRT_PaginationState = { pageIndex: 0, pageSize: 15 }
import { DetailTabTable } from './DetailTabTable'
const getNewState = (state: RowState): RowState => {
if (!state || state === 'clean') return 'removed'
@@ -47,7 +40,6 @@ export const EditableTable = <
idFieldName?: keyof T
url?: string
}) => {
const [pagination, setPagination] = useState<MRT_PaginationState>(defaultPagination)
const { editData, setEditData, mode, data, validator, fieldsWithErrors, setFieldsWithErrors } =
useDetailContext<ParentType>()
const errorObject = validator(editData, field)
@@ -100,14 +92,16 @@ export const EditableTable = <
return null // code shouldn't get here!
}
const actionRowProps = () => {
const resolveRenderRowActions = () => {
if (mode.read && (!idFieldName || !url)) {
return {}
} else if (mode.read && idFieldName && url) {
return { enableRowActions: true, renderRowActions: linkToDetails }
} else {
return { enableRowActions: true, renderRowActions: actionRow }
return undefined
}
if (mode.read && idFieldName && url) {
return linkToDetails
}
return actionRow
}
const rowStateToColor = (state: RowState | undefined) => {
@@ -132,18 +126,15 @@ export const EditableTable = <
}
return (
<MaterialReactTable
{...actionRowProps()}
<DetailTabTable<T>
mode="edit"
columns={columns}
data={getData()}
enableTopToolbar={false}
enableColumnActions={false}
enableSorting={false}
enablePagination={true}
onPaginationChange={setPagination}
positionPagination="both"
paginationDisplayMode="pages"
state={{ density: 'compact', pagination }}
enableRowActions={Boolean(resolveRenderRowActions())}
renderRowActions={resolveRenderRowActions()}
muiTableBodyRowProps={({ row }: { row: MRT_Row<T> }) => ({
sx: { backgroundColor: rowStateToColor(row.original.rowState) },
})}
@@ -1,6 +1,6 @@
import { MRT_ColumnDef, MRT_RowData } from 'material-react-table'
import { EditingModal } from './EditingModal'
import { TableView } from '@/components/TableView/TableView'
import { DetailTabTable } from './DetailTabTable'
import { useDetailContext } from '../Context/DetailContext'
import { useMemo } from 'react'
import { Box, CircularProgress } from '@mui/material'
@@ -77,7 +77,8 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
return (
<EditingModal buttonText={buttonText} dataCy={dataCy}>
{data ? (
<TableView<T>
<DetailTabTable<T>
mode="select"
data={filteredData}
columns={columns}
title={title}
@@ -1,18 +1,7 @@
import { CircularProgress } from '@mui/material'
import {
type MRT_ColumnDef,
type MRT_RowData,
type MRT_PaginationState,
MRT_Row,
MaterialReactTable,
useMaterialReactTable,
} from 'material-react-table'
import { type MRT_ColumnDef, type MRT_RowData } from 'material-react-table'
import { useDetailContext } from '../Context/DetailContext'
import { ActionComponent } from '@/components/TableView/ActionComponent'
import { defaultPaginationSmall } from './defaultValues'
import { useState } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { usePageContext } from '@/components/Page'
import { DetailTabTable } from './DetailTabTable'
export const SimpleTable = <T extends MRT_RowData, ParentType extends MRT_RowData>({
data,
@@ -26,48 +15,25 @@ export const SimpleTable = <T extends MRT_RowData, ParentType extends MRT_RowDat
url?: string
}) => {
const { mode } = useDetailContext<ParentType>()
const [pagination, setPagination] = useState<MRT_PaginationState>(defaultPaginationSmall)
const navigate = useNavigate()
const location = useLocation()
const { previousTableUrls, setPreviousTableUrls } = usePageContext()
const [searchParams] = useSearchParams()
const linkToDetails = ({ row }: { row: MRT_Row<T> }) => {
if (mode.read && idFieldName && url) return <ActionComponent {...{ row, idFieldName, url }} />
return null // code shouldn't get here!
}
const actionRowProps =
mode.read && idFieldName && url
? {
enableRowActions: true,
renderRowActions: linkToDetails,
muiTableBodyRowProps: ({ row }: { row: MRT_Row<T> }) => ({
onClick: () => {
setPreviousTableUrls([...previousTableUrls, `${location.pathname}?tab=${searchParams.get('tab')}`])
navigate(`/${url}/${row.original[idFieldName]}`)
},
sx: {
cursor: 'pointer',
},
}),
}
: {}
const table = useMaterialReactTable({
columns: columns,
data: data || [],
enableTopToolbar: false,
enableColumnActions: false,
positionPagination: 'bottom',
onPaginationChange: setPagination,
paginationDisplayMode: 'pages',
state: { density: 'compact', pagination },
...actionRowProps,
})
if (!data) return <CircularProgress />
return <MaterialReactTable table={table} />
if (!idFieldName) {
return <DetailTabTable<T> mode="edit" data={data} columns={columns} enableRowActions={false} />
}
return (
<DetailTabTable<T>
mode={mode.read ? 'read' : 'select'}
title="Detail List"
data={data}
columns={columns}
idFieldName={idFieldName}
url={url}
isFetching={false}
clickableRows={mode.read}
enableColumnFilterModes={true}
paginationPlacement="bottom"
/>
)
}