mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
refactor: add shared detail tab table wrapper
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user