Refactor detailview modes

This commit is contained in:
arttu
2024-05-29 13:50:46 +03:00
parent 47d33761f3
commit a8eff5bb34
28 changed files with 110 additions and 70 deletions
@@ -1,14 +1,54 @@
import { ReactNode, createContext, useState, JSX, useEffect } from 'react'
/* eslint-disable react-refresh/only-export-components */
import { ReactNode, createContext, useState, JSX, useEffect, Context, useContext } from 'react'
import { DropdownOption } from '../common/editingComponents'
import { cloneDeep } from 'lodash-es'
// -ref modes mean that user is selecting reference for either adding or editing item
export type ModeType = 'read' | 'new' | 'edit' | 'new-ref' | 'edit-ref'
export type ModeOptions = 'read' | 'new' | 'edit' | 'staging-edit' | 'staging-new'
export type ModeType = {
read: boolean
staging: boolean
new: boolean
option: ModeOptions
}
export const modeOptionToMode: Record<ModeOptions, ModeType> = {
new: {
read: false,
staging: false,
new: true,
option: 'new',
},
read: {
read: true,
staging: false,
new: false,
option: 'read',
},
edit: {
read: false,
staging: false,
new: false,
option: 'edit',
},
'staging-edit': {
read: false,
staging: true,
new: false,
option: 'staging-edit',
},
'staging-new': {
read: false,
staging: true,
new: false,
option: 'staging-new',
},
}
export type DetailContextType<T> = {
data: T
mode: ModeType
setMode: (newMode: ModeType) => void
setMode: (newMode: ModeOptions) => void
editData: T
setEditData: (editData: T) => void
textField: (field: keyof T, type?: React.HTMLInputTypeAttribute) => JSX.Element
@@ -44,3 +84,9 @@ export const DetailContextProvider = <T extends object>({
</DetailContext.Provider>
)
}
export const useDetailContext = <T,>() => {
const detailContext = useContext<DetailContextType<T>>(DetailContext as unknown as Context<DetailContextType<T>>)
if (!detailContext) throw new Error('detailContext lacking provider')
return detailContext
}
@@ -1,7 +1,7 @@
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 { useDetailContext } from './Context/DetailContext'
import { useNavigate } from 'react-router-dom'
import { useContext } from 'react'
import { PageContext } from '../Page'
@@ -2,7 +2,7 @@ import { useSearchParams } from 'react-router-dom'
import { Box, Button, Paper, Stack, Tab, Tabs } from '@mui/material'
import EditIcon from '@mui/icons-material/Edit'
import { useEffect, useState, JSX } from 'react'
import { DetailContextProvider, ModeType } from './Context/DetailContext'
import { DetailContextProvider, ModeOptions, ModeType, modeOptionToMode } from './Context/DetailContext'
import { cloneDeep } from 'lodash-es'
import { DropdownOption, DropdownSelector, EditableTextField, RadioSelector } from './common/editingComponents'
import { DetailBrowser } from './DetailBrowser'
@@ -31,7 +31,8 @@ export const DetailView = <T extends object>({
if (typeof tabFromUrl !== 'string' || isNaN(parseInt(tabFromUrl))) return 0
return parseInt(tabFromUrl)
}
const [mode, setMode] = useState<ModeType>('read')
const [mode, setModeState] = useState<ModeType>(modeOptionToMode['read'])
const [tab, setTab] = useState(getUrl())
useEffect(() => {
@@ -44,6 +45,10 @@ export const DetailView = <T extends object>({
)
}, [tab, setSearchParams])
const setMode = (newMode: ModeOptions) => {
setModeState(modeOptionToMode[newMode])
}
const textField = (field: keyof T, type?: React.HTMLInputTypeAttribute) => (
<EditableTextField<T> field={field} type={type} />
)
@@ -79,8 +84,6 @@ export const DetailView = <T extends object>({
elevation: 5,
}
const refMode = ['new-ref', 'edit-ref'].includes(mode)
const refSelectionView = (
<Paper {...paperProps}>
<Box sx={{ display: 'flex', flexDirection: 'column', rowGap: '1em' }}>
@@ -110,26 +113,26 @@ export const DetailView = <T extends object>({
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignContent: 'center', marginTop: 'auto' }}>
<Box sx={{ display: 'flex' }} gap={10}>
<ReturnButton />
{!refMode && (
{!mode.staging && (
<Button
onClick={() => setMode(mode === 'edit' ? 'read' : 'edit')}
variant={mode === 'edit' ? 'contained' : 'outlined'}
onClick={() => setMode(!mode.read ? 'read' : 'edit')}
variant={mode.read ? 'contained' : 'outlined'}
style={{ width: '12em' }}
>
<EditIcon style={{ marginRight: '0.5em' }} /> {mode === 'read' ? 'Edit' : 'Stop editing'}
<EditIcon style={{ marginRight: '0.5em' }} /> {mode.read ? 'Edit' : 'Cancel edit'}
</Button>
)}
{mode !== 'read' && onWrite && (
<WriteButton onWrite={onWrite} text={refMode ? 'Complete and save' : 'Finalize entry'} />
{!mode.read && onWrite && (
<WriteButton onWrite={onWrite} text={mode.staging ? 'Complete and save' : 'Finalize entry'} />
)}
</Box>
{!refMode && (
{!mode.staging && (
<Box sx={{ marginRight: '3em' }}>
<DetailBrowser<T> />
</Box>
)}
</Box>
{refMode ? refSelectionView : tabView}
{mode.staging ? refSelectionView : tabView}
</DetailContextProvider>
</Stack>
)
@@ -1,14 +1,16 @@
import { LocalityDetails } from '@/backendTypes'
import { ArrayFrame } from './common/tabLayoutHelpers'
import { useDetailContext } from './hooks'
import { useDetailContext } from './Context/DetailContext'
export const StagingView = () => {
const { bigTextField } = useDetailContext<LocalityDetails>()
const array = [
['Date', new Date().toLocaleDateString('en-CA')],
['Editor', 'Users Name'],
['Coordinator', 'Indre Zliobaite'],
['Comment', bigTextField('update_comment')],
]
return <ArrayFrame array={array} title="Reference for the new data" />
}
@@ -1,7 +1,7 @@
import { Editable, RowState } from '@/backendTypes'
import { CircularProgress, Box, Button } from '@mui/material'
import { type MRT_ColumnDef, type MRT_RowData, MaterialReactTable, MRT_Row } from 'material-react-table'
import { useDetailContext } from '../hooks'
import { useDetailContext } from '../Context/DetailContext'
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'
@@ -56,17 +56,18 @@ export const EditableTable = <T extends Editable<MRT_RowData>, ParentType extend
)
}
const actionRowProps = editable && mode === 'edit' ? { enableRowActions: true, renderRowActions: actionRow } : {}
const actionRowProps =
editable && mode.option === 'edit' ? { enableRowActions: true, renderRowActions: actionRow } : {}
const rowStateToColor = (state: RowState | undefined) => {
if (mode === 'read') return null
if (mode.read) return null
if (state === 'new') return 'lightgreen'
else if (state === 'removed' || state === 'cancelled') return '#FFCCCB'
return null
}
const getData = () => {
if (editable && mode === 'edit') {
if (editable && !mode.read) {
if (!editTableData) return editData[field] as T[]
return editTableData
}
@@ -2,7 +2,7 @@ import { Box, TextField } from '@mui/material'
import { EditingModal } from './EditingModal'
import { useForm } from 'react-hook-form'
import { Editable } from '@/backendTypes'
import { useDetailContext } from '../hooks'
import { useDetailContext } from '../Context/DetailContext'
export type EditingFormField = { name: string; label: string; required?: boolean }
@@ -1,7 +1,7 @@
import { MRT_ColumnDef, MRT_RowData } from 'material-react-table'
import { EditingModal } from './EditingModal'
import { TableView } from '@/components/TableView/TableView'
import { useDetailContext } from '../hooks'
import { useDetailContext } from '../Context/DetailContext'
import { useMemo } from 'react'
import { CircularProgress } from '@mui/material'
@@ -11,7 +11,7 @@ import {
} from '@mui/material'
import { ChangeEvent, ReactNode } from 'react'
import { RegisterOptions, FieldValues, UseFormRegisterReturn, FieldErrors } from 'react-hook-form'
import { useDetailContext } from '../hooks'
import { useDetailContext } from '../Context/DetailContext'
import { DataValue } from './tabLayoutHelpers'
const fieldWidth = '16em'
@@ -1,6 +1,6 @@
import { Card, Typography, Box, Grid, Divider } from '@mui/material'
import { ReactNode } from 'react'
import { useDetailContext } from '../hooks'
import { useDetailContext } from '../Context/DetailContext'
export const ArrayToTable = ({ array, half }: { array: Array<Array<ReactNode>>; half?: boolean }) => {
const maxRowLength = Math.max(...array.map(row => row.length))
@@ -99,7 +99,7 @@ export const DataValue = <T extends object>({
displayValue?: ReactNode | null
}) => {
const { data, mode } = useDetailContext<T>()
if (mode === 'edit') {
if (!mode.read) {
return EditElement
}
return displayValue ?? data[field]
@@ -1,10 +1,10 @@
import { useNavigate } from 'react-router-dom'
import { useDetailContext } from './hooks'
import { Button } from '@mui/material'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import SaveIcon from '@mui/icons-material/Save'
import { useContext } from 'react'
import { PageContext } from '../Page'
import { useDetailContext } from './Context/DetailContext'
export const WriteButton = <T,>({ onWrite, text }: { onWrite: (editData: T) => void; text?: string }) => {
const { editData, mode, setMode } = useDetailContext<T>()
@@ -12,12 +12,8 @@ export const WriteButton = <T,>({ onWrite, text }: { onWrite: (editData: T) => v
<Button
sx={{ width: '20em' }}
onClick={() => {
if (mode === 'edit') {
setMode('edit-ref')
return
}
if (mode === 'new') {
setMode('new-ref')
if (!mode.staging) {
setMode(mode.new ? 'staging-edit' : 'staging-new')
return
}
onWrite(editData)
@@ -35,7 +31,7 @@ export const ReturnButton = () => {
const navigate = useNavigate()
const { tableUrl } = useContext(PageContext)
const { mode, setMode } = useDetailContext()
if (['new-ref', 'edit-ref'].includes(mode)) {
if (mode.staging) {
return (
<Button
onClick={() => {
@@ -1,8 +0,0 @@
import { useContext, Context } from 'react'
import { DetailContextType, DetailContext } from './Context/DetailContext'
export const useDetailContext = <T,>() => {
const detailContext = useContext<DetailContextType<T>>(DetailContext as unknown as Context<DetailContextType<T>>)
if (!detailContext) throw new Error('detailContext lacking provider')
return detailContext
}
@@ -1,6 +1,6 @@
import { LocalityDetails } from '@/backendTypes'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const AgeTab = () => {
const { textField, radioSelection, dropdown, bigTextField } = useDetailContext<LocalityDetails>()
@@ -1,6 +1,6 @@
import { LocalityDetails } from '@/backendTypes'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const ClimateTab = () => {
const { textField, dropdown, bigTextField } = useDetailContext<LocalityDetails>()
@@ -1,6 +1,6 @@
import { LocalityDetails } from '@/backendTypes'
import { ArrayFrame } from '../../DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const EcometricsTab = () => {
const { textField } = useDetailContext<LocalityDetails>()
@@ -2,7 +2,7 @@ import { Editable, LocalityDetails, SedimentaryStructure, SedimentaryStructureVa
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
import { ArrayFrame, HalfFrames, Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { useGetAllSedimentaryStructuresQuery } from '@/redux/sedimentaryStructureReducer'
import { MRT_ColumnDef } from 'material-react-table'
@@ -183,7 +183,7 @@ export const LithologyTab = () => {
</HalfFrames>
<HalfFrames>
<Grouped title="Sedimentary Structure & Taphonomic Detail">
{mode === 'edit' && (
{!mode.read && (
<>
<SelectingTable<SedimentaryStructureValues, LocalityDetails>
buttonText="Add existing sedimentary structure"
@@ -1,5 +1,5 @@
import { Editable, LocalityDetails, LocalitySynonym } from '@/backendTypes'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Grouped, ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
@@ -28,7 +28,7 @@ export const LocalityTab = () => {
['Name', textField('loc_name')],
[
'Visibility status',
mode === 'edit'
!mode.read
? radioSelection(
'loc_status',
[
@@ -93,7 +93,7 @@ export const LocalityTab = () => {
<ArrayFrame array={latlong} title="Latitude & Longitude" />
<Grouped title="Synonyms">
{mode === 'edit' && editingModal}
{!mode.read && editingModal}
<EditableTable<Editable<LocalitySynonym>, LocalityDetails> columns={columns} editable field="now_syn_loc" />
</Grouped>
</>
@@ -3,7 +3,7 @@ import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingForm } from '@/components/DetailView/common/EditingForm'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { useGetAllMuseumsQuery } from '@/redux/museumReducer'
import { Box, CircularProgress } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
@@ -46,7 +46,7 @@ export const MuseumTab = () => {
return (
<Grouped title="Museums">
{mode === 'edit' && (
{!mode.read && (
<Box display="flex" gap={1}>
<EditingForm<Museum, LocalityDetails>
buttonText="Add new museum"
@@ -2,7 +2,7 @@ import { Editable, LocalityDetails, LocalityProject } from '@/backendTypes'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
import { useForm } from 'react-hook-form'
@@ -57,7 +57,7 @@ export const ProjectTab = () => {
return (
<Grouped title="Projects">
{mode === 'edit' && editingModal}
{!mode.read && editingModal}
<EditableTable<Editable<LocalityProject>, LocalityDetails> columns={columns} editable field="now_plr" />
</Grouped>
)
@@ -2,7 +2,7 @@ import { Editable, LocalityDetails, LocalitySpecies } from '@/backendTypes'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
import { useForm } from 'react-hook-form'
@@ -78,7 +78,7 @@ export const SpeciesTab = () => {
return (
<Grouped title="Species">
{mode === 'edit' && editingModal}
{!mode.read && editingModal}
<EditableTable<Editable<LocalitySpecies>, LocalityDetails> columns={columns} editable field="now_ls" />
</Grouped>
)
@@ -1,5 +1,5 @@
import { Editable, LocalityDetails, CollectingMethod } from '@/backendTypes'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Grouped, ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
@@ -157,7 +157,7 @@ export const TaphonomyTab = () => {
<HalfFrames>
<Grouped title="Collecting Methods">
{mode === 'edit' && editingModal}
{!mode.read && editingModal}
<EditableTable<Editable<CollectingMethod>, LocalityDetails>
columns={columns}
editable
@@ -1,6 +1,6 @@
import { SpeciesDetails } from '@/backendTypes'
import { ArrayFrame } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const DietTab = () => {
const { dropdown } = useDetailContext<SpeciesDetails>()
@@ -2,7 +2,7 @@ import { Editable, SpeciesDetails, SpeciesLocality } from '@/backendTypes'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
import { useForm } from 'react-hook-form'
@@ -152,7 +152,7 @@ export const LocalitySpeciesTab = () => {
return (
<Grouped title="Locality-Species Information">
{mode === 'edit' && editingModal}
{!mode.read && editingModal}
<EditableTable<Editable<SpeciesLocality>, SpeciesDetails> columns={columns} editable field="now_ls" />
</Grouped>
)
@@ -1,7 +1,7 @@
import { Editable, Locality, SpeciesDetails } from '@/backendTypes'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { useGetAllLocalitiesQuery } from '@/redux/localityReducer'
import { Box, CircularProgress } from '@mui/material'
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
@@ -60,7 +60,7 @@ export const LocalityTab = () => {
return (
<Grouped title="Localities">
{mode === 'edit' && (
{!mode.read && (
<Box display="flex" gap={1}>
{selectingTable}
</Box>
@@ -1,6 +1,6 @@
import { SpeciesDetails } from '@/backendTypes'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const LocomotionTab = () => {
const { dropdown } = useDetailContext<SpeciesDetails>()
@@ -1,6 +1,6 @@
import { SpeciesDetails } from '@/backendTypes'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@components/DetailView/hooks'
import { useDetailContext } from '@components/DetailView/Context/DetailContext'
export const SizeTab = () => {
const { dropdown, textField } = useDetailContext<SpeciesDetails>()
@@ -1,5 +1,5 @@
import { Editable, SpeciesDetails, SpeciesSynonym } from '@/backendTypes'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
@@ -48,7 +48,7 @@ export const SynonymTab = () => {
return (
<>
<Grouped title="Synonyms">
{mode === 'edit' && editingModal}
{!mode.read && editingModal}
<EditableTable<Editable<SpeciesSynonym>, SpeciesDetails> columns={columns} editable field="com_taxa_synonym" />
</Grouped>
</>
@@ -1,6 +1,6 @@
import { SpeciesDetails } from '@/backendTypes'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const TaxonomyTab = () => {
const { textField, dropdown, bigTextField } = useDetailContext<SpeciesDetails>()
@@ -1,6 +1,6 @@
import { SpeciesDetails } from '@/backendTypes'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/hooks'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const TeethTab = () => {
const { dropdown, textField } = useDetailContext<SpeciesDetails>()