Refactor TableView to use hook; create new Page component, save search id's in list, add TopBar with previous/next buttons on DetailView

This commit is contained in:
ShootingStar91
2024-05-28 14:46:44 +03:00
parent 61e7c462bd
commit 3c11327bad
6 changed files with 213 additions and 68 deletions
+35 -8
View File
@@ -2,13 +2,14 @@ import { Container, Grid } from '@mui/material'
import { FrontPage } from './components/FrontPage'
import { NavBar } from './components/NavBar'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { LocalityTable } from './components/Locality/LocalityTable'
import { ReferenceTable } from './components/Reference/ReferenceTable'
import { SpeciesTable } from './components/Species/SpeciesTable'
import { TimeUnitTable } from './components/TimeUnit/TimeUnitTable'
import { Login } from './components/Login'
import { LocalityDetails } from './components/Locality/LocalityDetails'
import { SpeciesDetails } from './components/Species/SpeciesDetails'
import { Page } from './components/Page'
import { LocalityTable } from './components/Locality/LocalityTable'
import { LocalityDetails } from './components/Locality/LocalityDetails'
const App = () => {
return (
@@ -22,12 +23,38 @@ const App = () => {
<Container maxWidth="xl" fixed style={{ marginTop: '2em', marginBottom: '2em' }}>
<Grid item>
<Routes>
<Route element={<LocalityDetails />} path="/locality/:id" />
<Route element={<LocalityTable />} path="/locality/" />
<Route element={<ReferenceTable />} path="/reference/" />
<Route element={<SpeciesDetails />} path="/species/:id" />
<Route element={<SpeciesTable />} path="/species/" />
<Route element={<TimeUnitTable />} path="/time-unit/" />
<Route
element={
<Page
tableView={<LocalityTable />}
detailView={<LocalityDetails />}
viewName="locality"
idFieldName="lid"
/>
}
path="/locality/:id?"
/>
<Route
element={
<Page
tableView={<SpeciesTable />}
detailView={<SpeciesDetails />}
viewName="species"
idFieldName="species_id"
/>
}
path="/species/:id?"
/>
<Route
element={
<Page tableView={<ReferenceTable />} detailView={null} viewName="reference" idFieldName="lid" />
}
path="/reference/:id?"
/>
<Route
element={<Page tableView={<TimeUnitTable />} detailView={null} viewName="time-unit" idFieldName="" />}
path="/time-unit/:id?"
/>
<Route element={<FrontPage />} path="/" />
<Route element={<Login />} path="/login" />
<Route element={<div>Page not found.</div>} path="*" />
@@ -3,11 +3,13 @@ import { Box, Button, Paper, Stack, Tab, Tabs } from '@mui/material'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import EditIcon from '@mui/icons-material/Edit'
import SaveIcon from '@mui/icons-material/Save'
import { useEffect, useState, JSX } from 'react'
import { useEffect, useState, JSX, useContext } from 'react'
import { DetailContextProvider, ModeType } from './Context/DetailContext'
import { cloneDeep } from 'lodash-es'
import { useDetailContext } from './hooks'
import { DropdownOption, DropdownSelector, EditableTextField, RadioSelector } from './common/editingComponents'
import { TopBar } from './TopBar'
import { PageContext } from '../Page'
export type TabType = {
title: string
@@ -37,8 +39,9 @@ const WriteButton = <T,>({
const ReturnButton = () => {
const navigate = useNavigate()
const { tableUrl } = useContext(PageContext)
return (
<Button onClick={() => navigate(-1)}>
<Button onClick={() => navigate(tableUrl, { relative: 'path' })}>
<ArrowBackIcon color="primary" style={{ marginRight: '0.35em' }} />
Return to table
</Button>
@@ -103,16 +106,21 @@ export const DetailView = <T extends object>({
return (
<Stack rowGap={2}>
<DetailContextProvider contextState={initialState}>
<Box sx={{ display: 'flex' }} gap={10}>
<ReturnButton />
<Button
onClick={() => setMode(mode === 'edit' ? 'read' : 'edit')}
variant={mode === 'edit' ? 'contained' : 'outlined'}
style={{ width: '12em' }}
>
<EditIcon /> {mode === 'read' ? 'Edit' : 'Stop editing'}
</Button>
{mode === 'edit' && onWrite && <WriteButton onWrite={onWrite} setMode={setMode} />}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignContent: 'center', marginTop: 'auto' }}>
<Box sx={{ display: 'flex' }} gap={10}>
<ReturnButton />
<Button
onClick={() => setMode(mode === 'edit' ? 'read' : 'edit')}
variant={mode === 'edit' ? 'contained' : 'outlined'}
style={{ width: '12em' }}
>
<EditIcon /> {mode === 'read' ? 'Edit' : 'Stop editing'}
</Button>
{mode === 'edit' && onWrite && <WriteButton onWrite={onWrite} setMode={setMode} />}
</Box>
<Box sx={{ marginRight: '3em' }}>
<TopBar<T> />
</Box>
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_event, newValue) => setTab(newValue as number)}>
@@ -0,0 +1,47 @@
import { Box, Button } from '@mui/material'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'
import { useDetailContext } from './hooks'
import { useNavigate } from 'react-router-dom'
import { useContext } from 'react'
import { PageContext } from '../Page'
export const TopBar = <T,>() => {
const { data } = useDetailContext<T>()
const navigate = useNavigate()
const { idList, idFieldName, viewName } = useContext(PageContext)
const idListExists = idList?.length > 0
const currentIndex = idList.indexOf((data as { [key: string]: string })[idFieldName as string])
const nextIndex = currentIndex + 1
const previousIndex = currentIndex - 1
return (
<Box
sx={{
alignItems: 'center',
justifyContent: 'center',
height: '100%',
display: 'flex',
fontWeight: 'bold',
fontSize: '22px',
gap: '1em',
}}
>
<div> Viewing details of a {viewName}</div>
{idListExists && (
<div>
{previousIndex > 0 && (
<Button onClick={() => navigate(`/${viewName}/${idList[previousIndex]}`)}>
<ArrowBackIcon />
</Button>
)}
{`${currentIndex} of ${idList.length}`}
{nextIndex <= idList.length && (
<Button onClick={() => navigate(`/${viewName}/${idList[nextIndex]}`)}>
<ArrowForwardIcon />
</Button>
)}
</div>
)}
</Box>
)
}
@@ -19,7 +19,6 @@ export const LocalityDetails = () => {
const { id } = useParams()
const { isLoading, isError, data } = useGetLocalityDetailsQuery(id!)
const [editLocalityRequest] = useEditLocalityMutation()
if (isError) return <div>Error loading data</div>
if (isLoading || !data) return <CircularProgress />
+67
View File
@@ -0,0 +1,67 @@
import { useParams } from 'react-router-dom'
import { ReactNode, createContext, useState } from 'react'
export type PageContextType<T> = {
idList: string[]
setIdList: (ids: string[]) => void
idFieldName: keyof T
viewName: string
tableUrl: string
setTableUrl: (newUrl: string) => void
}
export const PageContext = createContext<PageContextType<{ [key: string | number | symbol]: unknown }>>({
idFieldName: null!,
idList: [],
setIdList: () => {},
viewName: null!,
tableUrl: '',
setTableUrl: () => {},
})
export const PageContextProvider = <T extends object>({
children,
idFieldName,
viewName,
}: {
children: ReactNode | ReactNode[]
idFieldName: keyof T
viewName: string
}) => {
const [idList, setIdList] = useState<string[]>([])
const [tableUrl, setTableUrl] = useState<string>(`/${viewName}`)
return (
<PageContext.Provider
value={{
idList,
idFieldName,
setIdList: (newIdList: string[]) => setIdList(newIdList),
viewName,
tableUrl,
setTableUrl: (newUrl: string) => setTableUrl(newUrl),
}}
>
{children}
</PageContext.Provider>
)
}
export const Page = <T extends object>({
tableView,
detailView,
idFieldName,
viewName,
}: {
tableView: ReactNode
detailView: ReactNode
idFieldName: keyof T
viewName: string
}) => {
const { id } = useParams()
return (
<PageContextProvider<T> idFieldName={idFieldName} viewName={viewName}>
{id ? detailView : tableView}
</PageContextProvider>
)
}
+44 -47
View File
@@ -1,16 +1,18 @@
import { useEffect, useState } from 'react'
import { useContext, useEffect, useState } from 'react'
import {
type MRT_ColumnFiltersState,
MaterialReactTable,
type MRT_ColumnDef,
type MRT_RowData,
MRT_SortingState,
MRT_PaginationState,
useMaterialReactTable,
MaterialReactTable,
} from 'material-react-table'
import { CircularProgress } from '@mui/material'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { renderCustomToolbar, renderCustomToolbarModalVersion } from './helpers'
import { ActionComponent } from './ActionComponent'
import { PageContext } from '../Page'
type TableStateInUrl = 'sorting' | 'columnfilters' | 'pagination'
@@ -39,12 +41,11 @@ export const TableView = <T extends MRT_RowData>({
selectorFn?: (id: T) => void
url?: string
}) => {
const navigate = useNavigate()
const location = useLocation()
const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>([])
const [sorting, setSorting] = useState<MRT_SortingState>([])
const [pagination, setPagination] = useState<MRT_PaginationState>(defaultPagination)
const { setIdList, setTableUrl } = useContext(PageContext)
const loadStateFromUrl = (state: TableStateInUrl, defaultState: [] | MRT_PaginationState) => {
const searchParams = new URLSearchParams(location.search)
const stateFromUrl = searchParams.get(state)
@@ -52,6 +53,40 @@ export const TableView = <T extends MRT_RowData>({
return JSON.parse(stateFromUrl) as object
}
const table = useMaterialReactTable({
columns: columns,
data: data || [],
state: {
columnFilters,
showColumnFilters: true,
sorting,
pagination,
density: 'compact',
},
initialState: {
columnVisibility: { id: false },
},
onColumnFiltersChange: setColumnFilters,
renderRowActions: ({ row }) => <ActionComponent {...{ selectorFn, url, checkRowRestriction, row, idFieldName }} />,
displayColumnDefOptions: { 'mrt-row-actions': { size: 50, header: '' } },
enableRowActions: true,
onSortingChange: setSorting,
onPaginationChange: setPagination,
autoResetPageIndex: false,
positionPagination: 'both',
paginationDisplayMode: 'pages',
enableDensityToggle: false,
enableGlobalFilter: false,
enableColumnActions: false,
enableHiding: true,
renderToolbarInternalActions:
/*
To know what components you can render here if necessary, see the source code:
https://github.com/KevinVandy/material-react-table/blob/85b98f9aaa038df48aa1dd35123560abce78ee58/packages/material-react-table/src/components/toolbar/MRT_ToolbarInternalButtons.tsx#L45
*/
selectorFn ? renderCustomToolbarModalVersion : renderCustomToolbar,
})
// Load state from url only on first render
useEffect(() => {
if (selectorFn) return
@@ -67,49 +102,11 @@ export const TableView = <T extends MRT_RowData>({
const columnFilterToUrl = `columnfilters=${JSON.stringify(columnFilters)}`
const sortingToUrl = `sorting=${JSON.stringify(sorting)}`
const paginationToUrl = `pagination=${JSON.stringify(pagination)}`
navigate(`${location.pathname}?&${columnFilterToUrl}&${sortingToUrl}&${paginationToUrl}`, {
replace: true,
})
}, [columnFilters, sorting, pagination, location.pathname, navigate, selectorFn])
setTableUrl(`${location.pathname}?&${columnFilterToUrl}&${sortingToUrl}&${paginationToUrl}`)
setIdList(table.getPrePaginationRowModel().rows.map(row => row.original[idFieldName] as string))
}, [columnFilters, sorting, pagination, location.pathname, selectorFn, setIdList, table, idFieldName, setTableUrl])
if (!data) return <CircularProgress />
return (
<MaterialReactTable
columns={columns}
data={data}
state={{
columnFilters,
showColumnFilters: true,
sorting,
pagination,
density: 'compact',
}}
initialState={{
columnVisibility: { id: false },
}}
onColumnFiltersChange={setColumnFilters}
renderRowActions={({ row }) => (
<ActionComponent {...{ selectorFn, url, checkRowRestriction, row, idFieldName }} />
)}
displayColumnDefOptions={{ 'mrt-row-actions': { size: 50, header: '' } }}
enableRowActions
onSortingChange={setSorting}
onPaginationChange={setPagination}
autoResetPageIndex={false}
positionPagination="both"
paginationDisplayMode="pages"
enableDensityToggle={false}
enableGlobalFilter={false}
enableColumnActions={false}
enableHiding={true}
renderToolbarInternalActions={
/*
To know what components you can render here if necessary, see the source code:
https://github.com/KevinVandy/material-react-table/blob/85b98f9aaa038df48aa1dd35123560abce78ee58/packages/material-react-table/src/components/toolbar/MRT_ToolbarInternalButtons.tsx#L45
*/
selectorFn ? renderCustomToolbarModalVersion : renderCustomToolbar
}
/>
)
return <MaterialReactTable table={table} />
}