mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Initial commit for locality-species-cross-search
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'express-async-errors'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import crossSearchRouter from './routes/crossSearch'
|
||||
import localityRouter from './routes/locality'
|
||||
import museumRouter from './routes/museum'
|
||||
import referenceRouter from './routes/reference'
|
||||
@@ -43,6 +44,7 @@ app.use(tokenExtractor)
|
||||
app.use(userExtractor)
|
||||
|
||||
app.use('/user', userRouter)
|
||||
app.use('/crosssearch', crossSearchRouter)
|
||||
app.use('/locality', localityRouter)
|
||||
app.use('/locality-species', localitySpeciesRouter)
|
||||
app.use('/species', speciesRouter)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Request, Router } from 'express'
|
||||
import { getAllLocalities, getLocalityDetails, validateEntireLocality } from '../services/locality'
|
||||
import { fixBigInt } from '../utils/common'
|
||||
import { EditDataType, EditMetaData, LocalityDetailsType } from '../../../frontend/src/backendTypes'
|
||||
import { requireOneOf } from '../middlewares/authorizer'
|
||||
import { Role } from '../../../frontend/src/types'
|
||||
import { deleteLocality, writeLocality } from '../services/write/locality'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/all', async (req, res) => {
|
||||
const localities = await getAllLocalities(req.user)
|
||||
return res.status(200).send(fixBigInt(localities))
|
||||
})
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
const id = parseInt(req.params.id)
|
||||
const locality = await getLocalityDetails(id, req.user)
|
||||
if (!locality) return res.status(404).send()
|
||||
return res.status(200).send(fixBigInt(locality))
|
||||
})
|
||||
|
||||
router.put(
|
||||
'/',
|
||||
requireOneOf([Role.Admin, Role.EditUnrestricted]),
|
||||
async (req: Request<object, object, { locality: EditDataType<LocalityDetailsType> & EditMetaData }>, res) => {
|
||||
const { comment, references, ...editedLocality } = req.body.locality
|
||||
const validationErrors = validateEntireLocality(editedLocality)
|
||||
if (validationErrors.length > 0) {
|
||||
return res.status(400).send({ validationErrors })
|
||||
}
|
||||
const result = await writeLocality(editedLocality, comment, references, req.user!.initials)
|
||||
return res.status(200).send({ id: result })
|
||||
}
|
||||
)
|
||||
|
||||
router.delete('/:id', requireOneOf([Role.Admin, Role.EditUnrestricted]), async (req, res) => {
|
||||
const id = parseInt(req.params.id)
|
||||
await deleteLocality(id, req.user!)
|
||||
res.status(200).send()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -4,6 +4,7 @@ import { NavBar } from './components/NavBar'
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import { Login } from './components/Login'
|
||||
import {
|
||||
crossSearchPage,
|
||||
localityPage,
|
||||
personPage,
|
||||
projectPage,
|
||||
@@ -30,6 +31,7 @@ const App = () => {
|
||||
<Container maxWidth="xl" fixed style={{ marginTop: '2em', marginBottom: '2em' }}>
|
||||
<Grid item>
|
||||
<Routes>
|
||||
<Route element={crossSearchPage} path="/crosssearch/:id?" />
|
||||
<Route element={localityPage} path="/locality/:id?" />
|
||||
<Route element={speciesPage} path="/species/:id?" />
|
||||
<Route element={referencePage} path="/reference/:id?" />
|
||||
|
||||
Vendored
+10
@@ -81,6 +81,16 @@ export type Locality = {
|
||||
country: string | null
|
||||
loc_status: boolean | null
|
||||
}
|
||||
export type CrossSearch = {
|
||||
lid: number
|
||||
loc_name: string
|
||||
bfa_max: string | null
|
||||
bfa_min: string | null
|
||||
max_age: number
|
||||
min_age: number
|
||||
country: string | null
|
||||
loc_status: boolean | null
|
||||
}
|
||||
|
||||
export type Sequence = Prisma.now_tu_sequence
|
||||
export type SequenceDetailsType = Prisma.now_tu_sequence
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
useDeleteLocalityMutation,
|
||||
useEditLocalityMutation,
|
||||
useGetLocalityDetailsQuery,
|
||||
} from '../../redux/localityReducer'
|
||||
import { CircularProgress } from '@mui/material'
|
||||
import { DetailView, TabType } from '../DetailView/DetailView'
|
||||
import { AgeTab } from './Tabs/AgeTab'
|
||||
import { ArchaeologyTab } from './Tabs/ArchaeologyTab'
|
||||
import { ClimateTab } from './Tabs/ClimateTab'
|
||||
import { EcometricsTab } from './Tabs/EcometricsTab'
|
||||
import { LithologyTab } from './Tabs/LithologyTab'
|
||||
import { LocalityTab } from './Tabs/LocalityTab'
|
||||
import { MuseumTab } from './Tabs/MuseumTab'
|
||||
import { ProjectTab } from './Tabs/ProjectTab'
|
||||
import { SpeciesTab } from './Tabs/SpeciesTab'
|
||||
import { TaphonomyTab } from './Tabs/TaphonomyTab'
|
||||
import { EditDataType, LocalityDetailsType } from '@/backendTypes'
|
||||
import { validateLocality } from '@/validators/locality'
|
||||
import { UpdateTab } from '../DetailView/common/UpdateTab'
|
||||
import { emptyLocality } from '../DetailView/common/defaultValues'
|
||||
import { useNotify } from '@/hooks/notification'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const LocalityDetails = () => {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const isNew = id === 'new'
|
||||
const [editLocalityRequest, { isLoading: mutationLoading }] = useEditLocalityMutation()
|
||||
const { isFetching, isError, data } = useGetLocalityDetailsQuery(id!, {
|
||||
skip: isNew,
|
||||
})
|
||||
const notify = useNotify()
|
||||
const [deleteMutation, { isSuccess: deleteSuccess, isError: deleteError }] = useDeleteLocalityMutation()
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteSuccess) {
|
||||
notify('Deleted item successfully.')
|
||||
navigate('/locality')
|
||||
} else if (deleteError) {
|
||||
notify('Could not delete item. Error happened.', 'error')
|
||||
}
|
||||
}, [deleteSuccess, deleteError, notify, navigate])
|
||||
|
||||
if (isError) return <div>Error loading data</div>
|
||||
if (isFetching || (!data && !isNew) || mutationLoading) return <CircularProgress />
|
||||
|
||||
const deleteFunction = async () => {
|
||||
await deleteMutation(parseInt(id!)).unwrap()
|
||||
}
|
||||
|
||||
const onWrite = async (editData: EditDataType<LocalityDetailsType>) => {
|
||||
const { id } = await editLocalityRequest(editData).unwrap()
|
||||
navigate(`/locality/${id}`)
|
||||
}
|
||||
|
||||
const tabs: TabType[] = [
|
||||
{
|
||||
title: 'Age',
|
||||
content: <AgeTab />,
|
||||
},
|
||||
{
|
||||
title: 'Locality',
|
||||
content: <LocalityTab />,
|
||||
},
|
||||
{
|
||||
title: 'Species',
|
||||
content: <SpeciesTab />,
|
||||
},
|
||||
{
|
||||
title: 'Lithology',
|
||||
content: <LithologyTab />,
|
||||
},
|
||||
{
|
||||
title: 'Taphonomy',
|
||||
content: <TaphonomyTab />,
|
||||
},
|
||||
{
|
||||
title: 'Climate',
|
||||
content: <ClimateTab />,
|
||||
},
|
||||
{
|
||||
title: 'Ecometrics',
|
||||
content: <EcometricsTab />,
|
||||
},
|
||||
{
|
||||
title: 'Archaeology',
|
||||
content: <ArchaeologyTab />,
|
||||
},
|
||||
{
|
||||
title: 'Museum',
|
||||
content: <MuseumTab />,
|
||||
},
|
||||
{
|
||||
title: 'Projects',
|
||||
content: <ProjectTab />,
|
||||
},
|
||||
{
|
||||
title: 'Updates',
|
||||
content: <UpdateTab refFieldName="now_lr" updatesFieldName="now_lau" prefix="lau" />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<DetailView<LocalityDetailsType>
|
||||
tabs={tabs}
|
||||
data={isNew ? emptyLocality : data!}
|
||||
isNew={isNew}
|
||||
onWrite={onWrite}
|
||||
validator={validateLocality}
|
||||
deleteFunction={deleteFunction}
|
||||
hasStagingMode
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useMemo } from 'react'
|
||||
import { type MRT_ColumnDef } from 'material-react-table'
|
||||
import { useGetAllCrossSearchQuery, useGetCrossSearchListMutation } from '../../redux/crossSearchReducer'
|
||||
import { CrossSearch } from '@/backendTypes'
|
||||
import { TableView } from '../TableView/TableView'
|
||||
import { useNotify } from '@/hooks/notification'
|
||||
|
||||
export const CrossSearchTable = ({ selectorFn }: { selectorFn?: (newObject: CrossSearch) => void }) => {
|
||||
const CrossSearchQuery = useGetAllCrossSearchQuery()
|
||||
const [getCrossSearchList, { isLoading }] = useGetCrossSearchListMutation()
|
||||
const notify = useNotify()
|
||||
const columns = useMemo<MRT_ColumnDef<CrossSearch>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'id',
|
||||
accessorKey: 'lid',
|
||||
header: 'Id',
|
||||
size: 20,
|
||||
},
|
||||
{
|
||||
accessorKey: 'loc_name',
|
||||
header: 'Name',
|
||||
},
|
||||
{
|
||||
accessorKey: 'country',
|
||||
header: 'Country',
|
||||
},
|
||||
{
|
||||
accessorKey: 'max_age',
|
||||
header: 'Max age',
|
||||
filterVariant: 'range',
|
||||
},
|
||||
{
|
||||
accessorKey: 'min_age',
|
||||
header: 'Min age',
|
||||
filterVariant: 'range',
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const combinedExport = async (lids: number[]) => {
|
||||
if (isLoading) {
|
||||
notify('Please wait for the last request to complete.', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
const limit = 99999999
|
||||
if (lids.length > limit) {
|
||||
notify(`Please filter the table more. Current rows: ${lids.length}. Limit: ${limit}`, 'error')
|
||||
return
|
||||
}
|
||||
const result = await getCrossSearchList(lids).unwrap()
|
||||
const dataString = result.map(row => row.join(',')).join('\n')
|
||||
const blob = new Blob([dataString], { type: 'text/csv' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'localitiesWithSpecies.csv'
|
||||
a.click()
|
||||
}
|
||||
|
||||
const checkRowRestriction = (row: CrossSearch) => {
|
||||
return !!row.loc_status
|
||||
}
|
||||
|
||||
return (
|
||||
<TableView<CrossSearch>
|
||||
title="Locality-Species-Cross-Search (UNDER CONSTRUCTION)"
|
||||
selectorFn={selectorFn}
|
||||
checkRowRestriction={checkRowRestriction}
|
||||
idFieldName="lid"
|
||||
columns={columns}
|
||||
data={CrossSearchQuery.data}
|
||||
url="crosssearch"
|
||||
combinedExport={combinedExport}
|
||||
exportIsLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { LocalityDetailsType, TimeUnitDetailsType } from '@/backendTypes'
|
||||
import { FieldWithTableSelection } from '@/components/DetailView/common/editingComponents'
|
||||
import { emptyOption } from '@/components/DetailView/common/misc'
|
||||
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { TimeUnitTable } from '@/components/TimeUnit/TimeUnitTable'
|
||||
|
||||
export const AgeTab = () => {
|
||||
const { textField, radioSelection, dropdown, bigTextField, editData } = useDetailContext<LocalityDetailsType>()
|
||||
|
||||
const fracOptions = [
|
||||
emptyOption,
|
||||
'Early half 1:2',
|
||||
'Late half 2:2',
|
||||
'Early third 1:3',
|
||||
'Middle third 2:3',
|
||||
'Late third 3:3',
|
||||
]
|
||||
|
||||
const bfa_abs_options = [
|
||||
'AAR',
|
||||
'Ar/Ar',
|
||||
'C14',
|
||||
'chemical',
|
||||
'Delta_018',
|
||||
'ESR',
|
||||
'fission_track',
|
||||
'K/Ar',
|
||||
'obsidian_hyd',
|
||||
'other_absolute',
|
||||
'other_radiometric',
|
||||
'Rb/Sr',
|
||||
'Sr/Sr',
|
||||
'TL',
|
||||
'tree_ring',
|
||||
'U-series',
|
||||
'U/Th',
|
||||
]
|
||||
|
||||
// TODO: The date_meth options should come from db: distinct(now_loc.date_meth)
|
||||
const age = [
|
||||
[
|
||||
'Dating method',
|
||||
radioSelection(
|
||||
'date_meth',
|
||||
[
|
||||
{ value: 'time_unit', display: 'Time unit' },
|
||||
{ value: 'absolute', display: 'Absolute' },
|
||||
{ value: 'composite', display: 'Composite' },
|
||||
],
|
||||
'dating-method'
|
||||
),
|
||||
],
|
||||
[''],
|
||||
['', 'Age (Ma)', 'Basis for age (Absolute)', 'Basis for age (Time Unit)', 'Basis for age (Fraction)'],
|
||||
[
|
||||
'Minimum age',
|
||||
textField('min_age', { type: 'number', round: 3 }),
|
||||
dropdown('bfa_min_abs', bfa_abs_options, 'Min Basis for age (absolute)', editData.date_meth === 'time_unit'),
|
||||
<FieldWithTableSelection<TimeUnitDetailsType, LocalityDetailsType>
|
||||
key="bfa_min"
|
||||
sourceField="tu_name"
|
||||
targetField="bfa_min"
|
||||
selectorTable={<TimeUnitTable />}
|
||||
disabled={editData.date_meth === 'absolute'}
|
||||
/>,
|
||||
dropdown('frac_min', fracOptions, 'Minimum fraction', editData.date_meth === 'absolute'),
|
||||
],
|
||||
[
|
||||
'Maximum age',
|
||||
textField('max_age', { type: 'number', round: 3 }),
|
||||
dropdown('bfa_max_abs', bfa_abs_options, 'Max Basis for age (absolute)', editData.date_meth === 'time_unit'),
|
||||
<FieldWithTableSelection<TimeUnitDetailsType, LocalityDetailsType>
|
||||
key="bfa_max"
|
||||
sourceField="tu_name"
|
||||
targetField="bfa_max"
|
||||
selectorTable={<TimeUnitTable />}
|
||||
disabled={editData.date_meth === 'absolute'}
|
||||
/>,
|
||||
dropdown('frac_max', fracOptions, 'Maximum fraction', editData.date_meth === 'absolute'),
|
||||
],
|
||||
[''],
|
||||
['Chronostratigraphic Age', textField('chron')],
|
||||
['Age Comment', bigTextField('age_comm')],
|
||||
]
|
||||
|
||||
const lithostratigraphy = [
|
||||
['Group', textField('lgroup')],
|
||||
['Formation', textField('formation')],
|
||||
['Member', textField('member')],
|
||||
['Bed', textField('bed')],
|
||||
]
|
||||
|
||||
const sampleUnit = [
|
||||
['Datum Plane', textField('datum_plane')],
|
||||
['Top of Sample Unit (m)', textField('tos')],
|
||||
['Bottom of Sample Unit (m)', textField('bos')],
|
||||
]
|
||||
|
||||
const basinInformation = [
|
||||
['Basin', textField('basin')],
|
||||
['Subbasin', textField('subbasin')],
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<ArrayFrame array={age} title="Age" />
|
||||
<ArrayFrame array={lithostratigraphy} title="Lithostratigraphy" />
|
||||
<HalfFrames>
|
||||
<ArrayFrame half array={sampleUnit} title="Sample Unit" />
|
||||
<ArrayFrame half array={basinInformation} title="Basin Information" />
|
||||
</HalfFrames>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { LocalityDetailsType } from '@/backendTypes'
|
||||
import { emptyOption } from '@/components/DetailView/common/misc'
|
||||
import { ArrayFrame } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { getHomininSkeletalRemains } from '@/types'
|
||||
|
||||
export const ArchaeologyTab = () => {
|
||||
const { dropdown, radioSelection, data } = useDetailContext<LocalityDetailsType>()
|
||||
|
||||
const technologicalModeOptions = [
|
||||
emptyOption,
|
||||
{ display: '1', value: '1' },
|
||||
{ display: '2', value: '2' },
|
||||
{ display: '3', value: '3' },
|
||||
{ display: '4', value: '4' },
|
||||
{ display: '5', value: '5' },
|
||||
]
|
||||
|
||||
const culturalStageOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Early Stone Age', value: 'early_stone_age' },
|
||||
{ display: 'Middle Stone Age', value: 'middle_stone_age' },
|
||||
{ display: 'Late Stone Age', value: 'late_stone_age' },
|
||||
{ display: 'Lower Paleolithic', value: 'lower_paleolithic' },
|
||||
{ display: 'Middle Paleolithic', value: 'middle_paleolithic' },
|
||||
{ display: 'Upper Paleolithic/Paleoindian', value: 'upper_paleolithic/paleoindian' },
|
||||
{ display: 'Mesolithic/Archaic', value: 'mesolithic/archaic' },
|
||||
]
|
||||
|
||||
const regionalCultureOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Acheulean', value: 'acheulean' },
|
||||
{ display: 'Aterian', value: 'aterian' },
|
||||
{ display: 'Aurignac', value: 'aurignac' },
|
||||
{ display: 'Early Acheulean', value: 'early_acheulean' },
|
||||
{ display: 'Large Flake Acheulean', value: 'large_flake_acheulean' },
|
||||
{ display: 'LCT Acheulean', value: 'lct_acheulean' },
|
||||
{ display: 'Levantine Acheulean', value: 'levantine_acheulean' },
|
||||
{ display: 'Mousterian', value: 'mousterian' },
|
||||
{ display: 'Oldowan', value: 'oldowan' },
|
||||
{ display: 'Stillbay', value: 'stillbay' },
|
||||
{ display: 'Tabun B', value: 'tabun_b' },
|
||||
]
|
||||
|
||||
const archaeology = [
|
||||
['Hominin skeletal remains', getHomininSkeletalRemains(data) ? 'Yes' : 'No'],
|
||||
[
|
||||
'Stone tool cut marks on bones',
|
||||
radioSelection(
|
||||
'stone_tool_cut_marks_on_bones',
|
||||
[
|
||||
{ value: '0', display: 'No' },
|
||||
{ value: '1', display: 'Yes' },
|
||||
],
|
||||
'cut-marks'
|
||||
),
|
||||
],
|
||||
[
|
||||
'Bipedal footprints',
|
||||
radioSelection(
|
||||
'bipedal_footprints',
|
||||
[
|
||||
{ value: '0', display: 'No' },
|
||||
{ value: '1', display: 'Yes' },
|
||||
],
|
||||
'bipedal-footprints'
|
||||
),
|
||||
],
|
||||
[
|
||||
'Stone tool technology',
|
||||
radioSelection(
|
||||
'stone_tool_technology',
|
||||
[
|
||||
{ value: '0', display: 'No' },
|
||||
{ value: '1', display: 'Yes' },
|
||||
],
|
||||
'stone-tool-technology'
|
||||
),
|
||||
],
|
||||
['Technological mode', dropdown('technological_mode_1', technologicalModeOptions, 'Technological mode 1')],
|
||||
['', dropdown('technological_mode_2', technologicalModeOptions, 'Technological mode 2')],
|
||||
['', dropdown('technological_mode_3', technologicalModeOptions, 'Technological mode 3')],
|
||||
['Cultural stage', dropdown('cultural_stage_1', culturalStageOptions, 'Cultural stage 1')],
|
||||
['', dropdown('cultural_stage_2', culturalStageOptions, 'Cultural stage 2')],
|
||||
['', dropdown('cultural_stage_3', culturalStageOptions, 'Cultural stage 3')],
|
||||
['Regional culture', dropdown('regional_culture_1', regionalCultureOptions, 'Regional culture 1')],
|
||||
['', dropdown('regional_culture_2', regionalCultureOptions, 'Regional culture 2')],
|
||||
['', dropdown('regional_culture_3', regionalCultureOptions, 'Regional culture 3')],
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<ArrayFrame half array={archaeology} title="Archaeology" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { LocalityDetailsType } from '@/backendTypes'
|
||||
import { emptyOption } from '@/components/DetailView/common/misc'
|
||||
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
|
||||
export const ClimateTab = () => {
|
||||
const { textField, dropdown, bigTextField } = useDetailContext<LocalityDetailsType>()
|
||||
|
||||
const climateTypeOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Cold temperate', value: 'cold_temperate' },
|
||||
{ display: 'Paratropical', value: 'paratropical' },
|
||||
{ display: 'Subtropical', value: 'subtropical' },
|
||||
{ display: 'Temperate', value: 'temperate' },
|
||||
{ display: 'Tropical', value: 'tropical' },
|
||||
]
|
||||
|
||||
const temperatureOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Cold', value: 'cold' },
|
||||
{ display: 'Hot', value: 'hot' },
|
||||
{ display: 'Moderate', value: 'mod' },
|
||||
]
|
||||
|
||||
const moistureOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Dry', value: 'dry' },
|
||||
{ display: 'Intermediate', value: 'int' },
|
||||
{ display: 'Wet', value: 'wet' },
|
||||
]
|
||||
|
||||
const agentOfDisturbanceOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Fire', value: 'fire' },
|
||||
{ display: 'Fire-Wind', value: 'fire-wind' },
|
||||
{ display: 'Water', value: 'water' },
|
||||
{ display: 'Water-Fire', value: 'water-fire' },
|
||||
{ display: 'Water-Wind', value: 'water-wind' },
|
||||
{ display: 'Water-Fire-Wind', value: 'water-fire-wind' },
|
||||
{ display: 'Wind', value: 'wind' },
|
||||
]
|
||||
|
||||
const seasonalityOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Light', value: 'light' },
|
||||
{ display: 'Light-Temperature', value: 'light-temp' },
|
||||
{ display: 'None', value: 'none' },
|
||||
{ display: 'Temperature', value: 'temp' },
|
||||
{ display: 'Water', value: 'water' },
|
||||
{ display: 'Water-Light', value: 'water-light' },
|
||||
{ display: 'Water-Light-Temperature', value: 'water-light-temp' },
|
||||
]
|
||||
|
||||
const biomeOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Broad-leaved evergreen forest', value: 'bl_evergreen_f' },
|
||||
{ display: '"Mediterranean" sclerophyll vegetation', value: 'chaparral' },
|
||||
{ display: 'Coniferous forest - including boreal and subalpine', value: 'coniferous_f' },
|
||||
{ display: 'Deciduous forest - e.g., temperate angiosperm forests', value: 'deciduous_f' },
|
||||
{ display: 'Arid, both hot and cold desert', value: 'desert' },
|
||||
{ display: 'Treeless grasslands, including prairie, steppe', value: 'grassland' },
|
||||
{ display: 'Savanna - including bush, etc., with trees widely spaced', value: 'savanna' },
|
||||
{ display: 'Semi-deciduous forest', value: 'semi-decid_f' },
|
||||
{ display: 'Tundra - including alpine tundra', value: 'tundra' },
|
||||
{ display: 'Woodland - non-closed canopy, trees closely spaced', value: 'woodland' },
|
||||
]
|
||||
|
||||
const vegetationHeightOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Less than 2 m', value: '<2m' },
|
||||
{ display: '2-5 m', value: '2-5m' },
|
||||
{ display: 'Greater 5m', value: '>5m' },
|
||||
]
|
||||
|
||||
const vegetationStructureOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Closed', value: 'closed' },
|
||||
{ display: 'Open', value: 'open' },
|
||||
{ display: 'Semi-open', value: 'semi-open' },
|
||||
]
|
||||
|
||||
const primaryProductivityLevelOptions = [
|
||||
emptyOption,
|
||||
{ display: 'High', value: 'high' },
|
||||
{ display: 'Low', value: 'low' },
|
||||
{ display: 'Medium', value: 'med' },
|
||||
]
|
||||
|
||||
const nutrientAvailabilityOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Low', value: 'low' },
|
||||
{ display: 'Not low', value: 'not_low' },
|
||||
]
|
||||
|
||||
const waterAvailabilityOptions = [
|
||||
emptyOption,
|
||||
{ display: 'Mesic', value: 'mesic' },
|
||||
{ display: 'Seasonal', value: 'seasonal' },
|
||||
{ display: 'Wet', value: 'wet' },
|
||||
{ display: 'Xeric', value: 'xeric' },
|
||||
]
|
||||
|
||||
const environment = [
|
||||
['Climate Type', dropdown('climate_type', climateTypeOptions, 'Climate Type')],
|
||||
['Temperature', dropdown('temperature', temperatureOptions, 'Temperature')],
|
||||
['Moisture', dropdown('moisture', moistureOptions, 'Moisture')],
|
||||
['Agent(s) of Disturbance', dropdown('disturb', agentOfDisturbanceOptions, 'Agent(s) of Disturbance')],
|
||||
]
|
||||
|
||||
const environmentAndVegetationDetail = [['Environment & Vegetation Detail', bigTextField('v_envi_det')]]
|
||||
|
||||
const season = [
|
||||
['Seasonality', dropdown('seasonality', seasonalityOptions, 'Seasonality')],
|
||||
['Seasonality Intensity', textField('seas_intens')],
|
||||
]
|
||||
|
||||
const vegetation = [
|
||||
['Biome', dropdown('biome', biomeOptions, 'Biome')],
|
||||
['Vegetation Height', dropdown('v_ht', vegetationHeightOptions, 'Vegetation Height')],
|
||||
['Vegetation Structure', dropdown('v_struct', vegetationStructureOptions, 'Vegetation Structure')],
|
||||
['Primary Productivity Level', dropdown('pri_prod', primaryProductivityLevelOptions, 'Primary Productivity Level')],
|
||||
]
|
||||
|
||||
const plantSites = [
|
||||
['Nutrient Availability', dropdown('nutrients', nutrientAvailabilityOptions, 'Nutrient Availability')],
|
||||
['Water Availability', dropdown('water', waterAvailabilityOptions, 'Water Availability')],
|
||||
]
|
||||
|
||||
const pollenRecord = [
|
||||
['Arboreal pollen (AP%)', textField('pers_pollen_ap')],
|
||||
['Non-arboreal pollen (NAP%)', textField('pers_pollen_nap')],
|
||||
['Other pollen (OP%)', textField('pers_pollen_other')],
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<HalfFrames>
|
||||
<ArrayFrame half array={environment} title="Environment" />
|
||||
<ArrayFrame half array={environmentAndVegetationDetail} title="Environment & Vegetation Detail" />
|
||||
</HalfFrames>
|
||||
<HalfFrames>
|
||||
<ArrayFrame half array={vegetation} title="Vegetation" />
|
||||
<ArrayFrame half array={season} title="Season" />
|
||||
</HalfFrames>
|
||||
<HalfFrames>
|
||||
<ArrayFrame half array={plantSites} title="Plant Sites" />
|
||||
<ArrayFrame half array={pollenRecord} title="Pollen Record" />
|
||||
</HalfFrames>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { LocalityDetailsType } from '@/backendTypes'
|
||||
import { ArrayFrame } from '../../DetailView/common/tabLayoutHelpers'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { calculateMeanHypsodonty } from '@/types'
|
||||
|
||||
export const EcometricsTab = () => {
|
||||
const { textField, data } = useDetailContext<LocalityDetailsType>()
|
||||
|
||||
const ecometrics = [
|
||||
['Mean hypsodonty', calculateMeanHypsodonty(data)],
|
||||
['Estimate of annual precipitation (mm)', textField('estimate_precip', { type: 'number' })],
|
||||
['Estimate of mean annual temperature (°C)', textField('estimate_temp', { type: 'number' })],
|
||||
['Estimate of net primary productivity (g/m2/yr)', textField('estimate_npp', { type: 'number' })],
|
||||
['Woody cover percentage', textField('pers_woody_cover', { type: 'number' })],
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<ArrayFrame array={ecometrics} title="Ecometrics" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Editable, LocalityDetailsType, SedimentaryStructure, SedimentaryStructureValues } from '@/backendTypes'
|
||||
import { EditableTable } from '@/components/DetailView/common/EditableTable'
|
||||
import { emptyOption } from '@/components/DetailView/common/misc'
|
||||
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
|
||||
import { ArrayFrame, HalfFrames, Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { useGetAllSedimentaryStructuresQuery } from '@/redux/sedimentaryStructureReducer'
|
||||
import { skipToken } from '@reduxjs/toolkit/query'
|
||||
import { MRT_ColumnDef } from 'material-react-table'
|
||||
|
||||
export const LithologyTab = () => {
|
||||
const { mode, textField, dropdown, editData, setEditData, bigTextField } = useDetailContext<LocalityDetailsType>()
|
||||
const { data: sedimentaryStructuresData, isError } = useGetAllSedimentaryStructuresQuery(
|
||||
mode.read ? skipToken : undefined
|
||||
)
|
||||
|
||||
const rockTypeOptions = [
|
||||
emptyOption,
|
||||
'chert',
|
||||
'claystone',
|
||||
'coal',
|
||||
'conglomerate',
|
||||
'gravel',
|
||||
'grit',
|
||||
'limestone',
|
||||
'mudstone',
|
||||
'sandstone',
|
||||
'shale',
|
||||
'siltstone',
|
||||
'tuff',
|
||||
]
|
||||
|
||||
const generalOptions = [
|
||||
emptyOption,
|
||||
'coastal',
|
||||
'deltaic',
|
||||
'eolian',
|
||||
'fluvial',
|
||||
'fluv_deltaic',
|
||||
'fluv_lacustr',
|
||||
'freshwater',
|
||||
'karst',
|
||||
'lacustrine',
|
||||
'lacustrine_lg',
|
||||
'lacustrine_sm',
|
||||
'marine',
|
||||
'trap',
|
||||
'volcanic',
|
||||
]
|
||||
|
||||
const specificOptions = [
|
||||
emptyOption,
|
||||
'ab_chan_fill',
|
||||
'alluvial_fan',
|
||||
'beach',
|
||||
'cave',
|
||||
'chan_bar',
|
||||
'channel',
|
||||
'crev_splay',
|
||||
'deep_lake',
|
||||
'dry_floodplain',
|
||||
'dune',
|
||||
'estuary',
|
||||
'fissure',
|
||||
'floodplain',
|
||||
'interdist_bay',
|
||||
'interdune',
|
||||
'interfluv',
|
||||
'lagoon',
|
||||
'lake_margin',
|
||||
'levee',
|
||||
'loess',
|
||||
'offshore',
|
||||
'overbank',
|
||||
'paleosol',
|
||||
'raised_bog',
|
||||
'shallow_lake',
|
||||
'sinkhole',
|
||||
'spring',
|
||||
'swamp',
|
||||
'wet_floodplain',
|
||||
]
|
||||
|
||||
const circumstanceOptions = [
|
||||
emptyOption,
|
||||
'airfall',
|
||||
'amber',
|
||||
'burrow',
|
||||
'coprolite',
|
||||
'explosion',
|
||||
'flood',
|
||||
'midden',
|
||||
'mudflow',
|
||||
'paleosol',
|
||||
'storm',
|
||||
'tar',
|
||||
'tree_trunk',
|
||||
]
|
||||
|
||||
const depoContext1Options = [
|
||||
emptyOption,
|
||||
{ display: 'FC: Floodplain Channel', value: 'FC' },
|
||||
{ display: 'FP: Floodplain', value: 'FP' },
|
||||
{ display: 'FL: Float', value: 'FL' },
|
||||
{ display: 'MC: Major Channel', value: 'MC' },
|
||||
{ display: 'SD: Sheet Deposit', value: 'SD' },
|
||||
'multiple',
|
||||
'unknown',
|
||||
]
|
||||
|
||||
const depoContext2Options = [
|
||||
emptyOption,
|
||||
{ display: 'FC-C: Complex Fill', value: 'FC-C' },
|
||||
{ display: 'FC-S: Simple Fill', value: 'FC-S' },
|
||||
{ display: 'FP-C: Continuous', value: 'FP-C' },
|
||||
{ display: 'FP-L: Laminated', value: 'FP-L' },
|
||||
{ display: 'FP-P: Patchy', value: 'FP-P' },
|
||||
{ display: 'MC-L: Lower', value: 'MC-L' },
|
||||
{ display: 'MC-U: Upper', value: 'MC-U' },
|
||||
'multiple',
|
||||
]
|
||||
|
||||
const depoContext3Options = [
|
||||
emptyOption,
|
||||
{ display: 'FC-C1: Basal Lag and Bar', value: 'FC-C1' },
|
||||
{ display: 'FC-C2: Cross-cutting Lenses', value: 'FC-C2' },
|
||||
{ display: 'FC-S1: Basal Lag and Bar', value: 'FC-S1' },
|
||||
{ display: 'FC-S2: Mixed Lithologies', value: 'FC-S2' },
|
||||
{ display: 'FC-S3: Fine-grained', value: 'FC-S3' },
|
||||
{ display: 'MC-U1: Channel Bar', value: 'MC-U1' },
|
||||
{ display: 'MC-U2: Large-scale Channel Fill', value: 'MC-U2' },
|
||||
{ display: 'MC-U3: Small-scale Channel Fill', value: 'MC-U3' },
|
||||
'multiple',
|
||||
]
|
||||
|
||||
const depoContext4Options = [
|
||||
emptyOption,
|
||||
{ display: 'MC-U2a: Coarse-grained', value: 'MC-U2a' },
|
||||
{ display: 'MC-U2b: Fine-grained', value: 'MC-U2b' },
|
||||
{ display: 'MC-U3a: Coarse-grained', value: 'MC-U3a' },
|
||||
{ display: 'MC-U3b: Fine-grained', value: 'MC-U3b' },
|
||||
'multiple',
|
||||
]
|
||||
|
||||
const lithology = [
|
||||
['Rock Type', dropdown('rock_type', rockTypeOptions, 'Rock Type')],
|
||||
['Short Descriptor', textField('rt_adj')],
|
||||
['Comments', bigTextField('lith_comm')],
|
||||
]
|
||||
|
||||
const sedimentryEnvironment = [
|
||||
['General', dropdown('sed_env_1', generalOptions, 'General')],
|
||||
['Specific', dropdown('sed_env_2', specificOptions, 'Specific')],
|
||||
['GeneralEvent / Circumstance', dropdown('event_circum', circumstanceOptions, 'Event / Circumstance')],
|
||||
['Comments', bigTextField('se_comm')],
|
||||
]
|
||||
|
||||
const depositionalContext = [
|
||||
['Depo Context 1', dropdown('depo_context1', depoContext1Options, 'Depo Context 1')],
|
||||
['Depo Context 2', dropdown('depo_context2', depoContext2Options, 'Depo Context 2')],
|
||||
['Depo Context 3', dropdown('depo_context3', depoContext3Options, 'Depo Context 3')],
|
||||
['Depo Context 4', dropdown('depo_context4', depoContext4Options, 'Depo Context 4')],
|
||||
['Comments', bigTextField('depo_comm')],
|
||||
]
|
||||
|
||||
const columns: MRT_ColumnDef<SedimentaryStructure>[] = [
|
||||
{
|
||||
accessorKey: 'sed_struct',
|
||||
header: 'Sedimentary Structure',
|
||||
},
|
||||
]
|
||||
|
||||
const selectingTableColumns: MRT_ColumnDef<SedimentaryStructureValues>[] = [
|
||||
{
|
||||
header: 'Sedimentary structure',
|
||||
accessorKey: 'ss_value',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<HalfFrames>
|
||||
<ArrayFrame half array={lithology} title="Lithology" />
|
||||
<ArrayFrame half array={sedimentryEnvironment} title="Sedimentry Environment" />
|
||||
</HalfFrames>
|
||||
<HalfFrames>
|
||||
<Grouped title="Sedimentary Structure & Taphonomic Detail">
|
||||
{!mode.read && (
|
||||
<>
|
||||
<SelectingTable<SedimentaryStructureValues, LocalityDetailsType>
|
||||
buttonText="Select Sedimentary structure"
|
||||
isError={isError}
|
||||
columns={selectingTableColumns}
|
||||
data={sedimentaryStructuresData}
|
||||
fieldName="now_ss"
|
||||
idFieldName="ss_value"
|
||||
editingAction={(newSed: SedimentaryStructureValues) => {
|
||||
setEditData({
|
||||
...editData,
|
||||
now_ss: [...editData.now_ss, { lid: editData.lid!, sed_struct: newSed.ss_value, rowState: 'new' }],
|
||||
})
|
||||
}}
|
||||
selectedValues={editData.now_ss.map(ss => ss.sed_struct ?? '')} // TODO ss.sed_struct may be null. is empty string ok default?
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<EditableTable<Editable<SedimentaryStructure>, LocalityDetailsType> columns={columns} field="now_ss" />
|
||||
</Grouped>
|
||||
<ArrayFrame half array={depositionalContext} title="Depositional Context" />
|
||||
</HalfFrames>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Editable, LocalityDetailsType, LocalitySynonym } from '@/backendTypes'
|
||||
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'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { EditableTable } from '@/components/DetailView/common/EditableTable'
|
||||
import { EditingModal } from '@/components/DetailView/common/EditingModal'
|
||||
import { emptyOption } from '@/components/DetailView/common/misc'
|
||||
|
||||
export const LocalityTab = () => {
|
||||
const { textField, radioSelection, dropdown, mode, bigTextField } = useDetailContext<LocalityDetailsType>()
|
||||
|
||||
const approximateCoordinatesOptions = [
|
||||
{ display: 'No', value: 'false' },
|
||||
{ display: 'Yes', value: 'true' },
|
||||
]
|
||||
const generalLocalityOptions = [emptyOption, { display: 'No', value: 'n' }, { display: 'Yes', value: 'y' }]
|
||||
|
||||
const siteAreaOptions = [
|
||||
'',
|
||||
{ display: '<10 m2', value: '<10m2' },
|
||||
{ display: '10-50 m2', value: '10-50m2' },
|
||||
{ display: '50-100 m2', value: '50-100m2' },
|
||||
{ display: '100-1000 m2', value: '100-1000m2' },
|
||||
{ display: '>1000 m2', value: '>1000m2' },
|
||||
]
|
||||
|
||||
const info = [
|
||||
['Name', textField('loc_name')],
|
||||
[
|
||||
'Visibility status',
|
||||
radioSelection(
|
||||
'loc_status',
|
||||
[
|
||||
{ value: 'false', display: 'Public' },
|
||||
{ value: 'true', display: 'Draft' },
|
||||
],
|
||||
'Status'
|
||||
),
|
||||
],
|
||||
]
|
||||
const country = [
|
||||
['Country', textField('country')],
|
||||
['State', textField('state')],
|
||||
['County', textField('county')],
|
||||
['Detail', bigTextField('loc_detail')],
|
||||
['Site Area', dropdown('site_area', siteAreaOptions, 'Site Area')],
|
||||
['General Locality', dropdown('gen_loc', generalLocalityOptions, 'General Locality')],
|
||||
['Plate', textField('plate')],
|
||||
]
|
||||
const latlong = [
|
||||
['', 'dms', 'dec'],
|
||||
['Latitude', textField('dms_lat'), textField('dec_lat')],
|
||||
['Longitude', textField('dms_long'), textField('dec_long')],
|
||||
['Approximate Coordinates', dropdown('approx_coord', approximateCoordinatesOptions, 'Approximate Coordinates')],
|
||||
['Altitude (m)', textField('altitude')],
|
||||
]
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useForm()
|
||||
|
||||
const columns: MRT_ColumnDef<LocalitySynonym>[] = [
|
||||
{
|
||||
accessorKey: 'synonym',
|
||||
header: 'Synonym',
|
||||
},
|
||||
]
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
const onSave = async () => {
|
||||
// TODO: Saving logic here (add Synonym to editData)
|
||||
return Object.keys(errors).length === 0
|
||||
}
|
||||
|
||||
const editingModal = (
|
||||
<EditingModal buttonText="Add new Synonym" onSave={onSave}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
|
||||
<TextField {...register('synonym', { required: true })} label="Synonym" required />
|
||||
</Box>
|
||||
</EditingModal>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<HalfFrames>
|
||||
<ArrayFrame half array={info} title="Info" />
|
||||
<ArrayFrame half array={country} title="Country" />
|
||||
</HalfFrames>
|
||||
|
||||
<ArrayFrame array={latlong} title="Latitude & Longitude" />
|
||||
|
||||
<Grouped title="Synonyms">
|
||||
{!mode.read && editingModal}
|
||||
<EditableTable<Editable<LocalitySynonym>, LocalityDetailsType> columns={columns} field="now_syn_loc" />
|
||||
</Grouped>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Editable, LocalityDetailsType, Museum } from '@/backendTypes'
|
||||
import { EditableTable } from '@/components/DetailView/common/EditableTable'
|
||||
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { useGetAllMuseumsQuery } from '@/redux/museumReducer'
|
||||
import { Box } from '@mui/material'
|
||||
import { MRT_ColumnDef } from 'material-react-table'
|
||||
import { skipToken } from '@reduxjs/toolkit/query'
|
||||
|
||||
export const MuseumTab = () => {
|
||||
const { mode, editData, setEditData } = useDetailContext<LocalityDetailsType>()
|
||||
const { data: museumData, isError } = useGetAllMuseumsQuery(mode.read ? skipToken : undefined)
|
||||
|
||||
const columns: MRT_ColumnDef<Museum>[] = [
|
||||
{
|
||||
accessorKey: 'museum',
|
||||
header: 'Museum code',
|
||||
},
|
||||
{
|
||||
accessorKey: 'institution',
|
||||
header: 'Institution',
|
||||
},
|
||||
{
|
||||
accessorKey: 'city',
|
||||
header: 'City',
|
||||
},
|
||||
{
|
||||
accessorKey: 'country',
|
||||
header: 'Country',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Grouped title="Museums">
|
||||
{!mode.read && (
|
||||
<Box display="flex" gap={1}>
|
||||
<SelectingTable<Museum, LocalityDetailsType>
|
||||
buttonText="Select Museum"
|
||||
data={museumData}
|
||||
isError={isError}
|
||||
columns={columns}
|
||||
fieldName="now_mus"
|
||||
idFieldName="museum"
|
||||
editingAction={(newMuseum: Museum) => {
|
||||
setEditData({
|
||||
...editData,
|
||||
now_mus: [
|
||||
...editData.now_mus,
|
||||
{ lid: editData.lid, museum: newMuseum.museum, com_mlist: newMuseum, rowState: 'new' },
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<EditableTable<Editable<Museum>, LocalityDetailsType>
|
||||
columns={columns.map(col => ({ ...col, accessorKey: `com_mlist.${col.accessorKey}` }))}
|
||||
field="now_mus"
|
||||
/>
|
||||
</Grouped>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Editable, LocalityDetailsType, Project } from '@/backendTypes'
|
||||
import { EditableTable } from '@/components/DetailView/common/EditableTable'
|
||||
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { useGetAllProjectsQuery } from '@/redux/projectReducer'
|
||||
import { Box } from '@mui/material'
|
||||
import { MRT_ColumnDef } from 'material-react-table'
|
||||
import { skipToken } from '@reduxjs/toolkit/query'
|
||||
|
||||
export const ProjectTab = () => {
|
||||
const { mode, editData, setEditData } = useDetailContext<LocalityDetailsType>()
|
||||
const { data: projectData, isError } = useGetAllProjectsQuery(mode.read ? skipToken : undefined)
|
||||
|
||||
const columns: MRT_ColumnDef<Project>[] = [
|
||||
{
|
||||
accessorKey: 'proj_code',
|
||||
header: 'Code',
|
||||
},
|
||||
{
|
||||
accessorKey: 'proj_name',
|
||||
header: 'Project',
|
||||
},
|
||||
{
|
||||
accessorKey: 'contact',
|
||||
header: 'Contact',
|
||||
},
|
||||
{
|
||||
accessorKey: 'proj_status',
|
||||
header: 'Status',
|
||||
},
|
||||
// TODO
|
||||
/* {
|
||||
accessorFn: ({ proj_records }) => (proj_records ? 'Public' : 'Private'),
|
||||
header: 'Records',
|
||||
}, */
|
||||
]
|
||||
|
||||
return (
|
||||
<Grouped title="Projects">
|
||||
{!mode.read && (
|
||||
<Box display="flex" gap={1}>
|
||||
<SelectingTable<Project, LocalityDetailsType>
|
||||
buttonText="Select Project"
|
||||
data={projectData}
|
||||
columns={columns}
|
||||
isError={isError}
|
||||
idFieldName="pid"
|
||||
fieldName="now_plr"
|
||||
editingAction={(newProject: Project) => {
|
||||
setEditData({
|
||||
...editData,
|
||||
now_plr: [
|
||||
...editData.now_plr,
|
||||
{ lid: editData.lid, pid: newProject.pid, now_proj: newProject, rowState: 'new' },
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<EditableTable<Editable<Project>, LocalityDetailsType>
|
||||
columns={columns.map(c => ({ ...c, accessorKey: `now_proj.${c.accessorKey}` }))}
|
||||
field="now_plr"
|
||||
/>
|
||||
</Grouped>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { LocalityDetailsType, LocalitySpecies, Species, SpeciesDetailsType } from '@/backendTypes'
|
||||
import { EditableTable } from '@/components/DetailView/common/EditableTable'
|
||||
import { EditingForm } from '@/components/DetailView/common/EditingForm'
|
||||
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
|
||||
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { useGetAllSpeciesQuery } from '@/redux/speciesReducer'
|
||||
import { Box } from '@mui/material'
|
||||
import { skipToken } from '@reduxjs/toolkit/query'
|
||||
import { MRT_ColumnDef } from 'material-react-table'
|
||||
|
||||
export const SpeciesTab = () => {
|
||||
const { mode, editData, setEditData } = useDetailContext<LocalityDetailsType>()
|
||||
const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined)
|
||||
|
||||
const speciesColumns: MRT_ColumnDef<Species>[] = [
|
||||
{
|
||||
accessorKey: 'order_name',
|
||||
header: 'Order',
|
||||
},
|
||||
{
|
||||
accessorKey: 'family_name',
|
||||
header: 'Family',
|
||||
},
|
||||
{
|
||||
accessorKey: 'genus_name',
|
||||
header: 'Genus',
|
||||
},
|
||||
{
|
||||
accessorKey: 'species_name',
|
||||
header: 'Species',
|
||||
},
|
||||
{
|
||||
accessorKey: 'subclass_or_superorder_name',
|
||||
header: 'Subclass or Superorder',
|
||||
},
|
||||
{
|
||||
accessorKey: 'suborder_or_superfamily_name',
|
||||
header: 'Suborder or Superfamily',
|
||||
},
|
||||
{
|
||||
accessorKey: 'unique_identifier',
|
||||
header: 'Unique Identifier',
|
||||
},
|
||||
{
|
||||
accessorKey: 'taxonomic_status',
|
||||
header: 'Taxon status',
|
||||
},
|
||||
]
|
||||
const localitySpeciesColumns: MRT_ColumnDef<LocalitySpecies>[] = [
|
||||
{
|
||||
accessorKey: 'com_species.order_name',
|
||||
header: 'Order',
|
||||
},
|
||||
{
|
||||
accessorKey: 'com_species.family_name',
|
||||
header: 'Family',
|
||||
},
|
||||
{
|
||||
accessorKey: 'com_species.genus_name',
|
||||
header: 'Genus',
|
||||
},
|
||||
{
|
||||
accessorKey: 'com_species.species_name',
|
||||
header: 'Species',
|
||||
},
|
||||
{
|
||||
accessorKey: 'com_species.subclass_or_superorder_name',
|
||||
header: 'Subclass or Superorder',
|
||||
},
|
||||
{
|
||||
accessorKey: 'com_species.suborder_or_superfamily_name',
|
||||
header: 'Suborder or Superfamily',
|
||||
},
|
||||
{
|
||||
accessorKey: 'com_species.unique_identifier',
|
||||
header: 'Unique Identifier',
|
||||
},
|
||||
{
|
||||
accessorKey: 'com_species.taxonomic_status',
|
||||
header: 'Taxon status',
|
||||
},
|
||||
]
|
||||
const formFields: { name: string; label: string; required?: boolean }[] = [
|
||||
{ name: 'order_name', label: 'Order', required: true },
|
||||
{ name: 'family_name', label: 'Family', required: true },
|
||||
{ name: 'genus_name', label: 'Genus', required: true },
|
||||
{ name: 'species_name', label: 'Species', required: true },
|
||||
{ name: 'subclass_or_superorder_name', label: 'Subclass or Superorder' },
|
||||
{ name: 'suborder_or_superfamily_name', label: 'Suborder or Superfamily' },
|
||||
{ name: 'unique_identifier', label: 'Unique Identifier', required: true },
|
||||
{ name: 'taxonomic_status', label: 'Taxon status' },
|
||||
]
|
||||
|
||||
return (
|
||||
<Grouped title="Species">
|
||||
{!mode.read && (
|
||||
<Box display="flex" gap={1}>
|
||||
<EditingForm<Species, LocalityDetailsType>
|
||||
buttonText="Add new Species"
|
||||
formFields={formFields}
|
||||
editAction={(newSpecies: Species) => {
|
||||
setEditData({
|
||||
...editData,
|
||||
now_ls: [
|
||||
...editData.now_ls,
|
||||
{
|
||||
lid: editData.lid,
|
||||
species_id: newSpecies.species_id,
|
||||
com_species: { ...(newSpecies as SpeciesDetailsType) },
|
||||
rowState: 'new',
|
||||
},
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<SelectingTable<Species, LocalityDetailsType>
|
||||
buttonText="Select Species"
|
||||
data={speciesData}
|
||||
isError={isError}
|
||||
columns={speciesColumns}
|
||||
fieldName="now_ls"
|
||||
idFieldName="species_id"
|
||||
editingAction={(newSpecies: Species) => {
|
||||
setEditData({
|
||||
...editData,
|
||||
now_ls: [
|
||||
...editData.now_ls,
|
||||
{
|
||||
lid: editData.lid,
|
||||
species_id: newSpecies.species_id,
|
||||
com_species: { ...(newSpecies as SpeciesDetailsType) },
|
||||
rowState: 'new',
|
||||
},
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<EditableTable<LocalitySpecies, LocalityDetailsType> columns={localitySpeciesColumns} field="now_ls" />
|
||||
</Grouped>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Editable, LocalityDetailsType, CollectingMethod } from '@/backendTypes'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { Grouped, ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
|
||||
import { MRT_ColumnDef } from 'material-react-table'
|
||||
import { EditableTable } from '@/components/DetailView/common/EditableTable'
|
||||
import { EditingForm } from '@/components/DetailView/common/EditingForm'
|
||||
|
||||
export const TaphonomyTab = () => {
|
||||
const { textField, dropdown } = useDetailContext<LocalityDetailsType>()
|
||||
const { mode } = useDetailContext<LocalityDetailsType>()
|
||||
|
||||
const assemblageFormationOptions = [
|
||||
'',
|
||||
{ display: 'Attritional', value: 'attritional' },
|
||||
{ display: 'Bone collecting vertebrate', value: 'bone_collctr' },
|
||||
{ display: 'Carnivore ingestion', value: 'carn_ingest' },
|
||||
{ display: 'Herbivore accumulation', value: 'herb_accum' },
|
||||
{ display: 'Isolated death', value: 'isol_death' },
|
||||
{ display: 'Mass death', value: 'mass_death' },
|
||||
]
|
||||
|
||||
const transportOptions = [
|
||||
'',
|
||||
{ display: 'Highly transported', value: 'high_trans' },
|
||||
{ display: 'All three', value: 'high_trans_winn' },
|
||||
{ display: 'Transported', value: 'trans' },
|
||||
{ display: 'Transported and winnowed', value: 'trans_winn' },
|
||||
{ display: 'Untransported', value: 'untrans' },
|
||||
{ display: 'Untransported and winnowed', value: 'untrans_winn' },
|
||||
]
|
||||
|
||||
const abrasionOptions = [
|
||||
'',
|
||||
{ display: 'Abraded', value: 'abraded' },
|
||||
{ display: 'Mixed', value: 'mixed_abr' },
|
||||
{ display: 'Unabraded', value: 'unabrade' },
|
||||
]
|
||||
|
||||
const weatheringTramplingOptions = [
|
||||
'',
|
||||
{ display: 'Mixed weathered and unweathered', value: 'mx' },
|
||||
{ display: 'Mixed weathered and trampled', value: 'mx_trmp' },
|
||||
{ display: 'Trampled', value: 'trmp' },
|
||||
{ display: 'Unweathered', value: 'unwe' },
|
||||
{ display: 'Unweathered and trampled', value: 'unwe_trmp' },
|
||||
{ display: 'Weathered', value: 'we' },
|
||||
{ display: 'Weathered and trampled', value: 'we_trmp' },
|
||||
]
|
||||
|
||||
const partConcentrationOptions = [
|
||||
'',
|
||||
'1-10/m2',
|
||||
'10-100/m2',
|
||||
'100-1000/m2',
|
||||
'>1000/m2',
|
||||
'<1/100m2',
|
||||
'1-10/100m2',
|
||||
'1-10/m3',
|
||||
'10-100/m3',
|
||||
'100-1000/m3',
|
||||
'1000-10000/m3',
|
||||
'>10000/m3',
|
||||
'<1/100m3',
|
||||
'1-10/100m3',
|
||||
'<10^2/cm3',
|
||||
'10^2-10^3/cm3',
|
||||
'10^3-10^4/cm3',
|
||||
'10^4-10^5/cm3',
|
||||
'10^5-10^6/cm3',
|
||||
'>10^6/cm3',
|
||||
{ display: 'Isolated occurrence', value: 'isol_occur' },
|
||||
]
|
||||
|
||||
const assemblageComponentSizeOptions = ['', 'both', 'macro', 'micro']
|
||||
|
||||
const timeRepresentedOptions = [
|
||||
'',
|
||||
'<1',
|
||||
'1-10^1',
|
||||
'10^1-10^2',
|
||||
'10^2-10^3',
|
||||
'10^3-10^4',
|
||||
'10^4-10^5',
|
||||
'10^5-10^6',
|
||||
'>10^6',
|
||||
]
|
||||
|
||||
const vertebratePreservationOptions = [
|
||||
'',
|
||||
'artic_pts',
|
||||
'assoc_pts',
|
||||
'compression',
|
||||
'delic_frags',
|
||||
'disartic_pts',
|
||||
'isol_teeth',
|
||||
'mixed_assem',
|
||||
'not_known',
|
||||
]
|
||||
|
||||
const unbiasedCollectingOptions = ['', { display: 'Yes', value: 'y' }, { display: 'No', value: 'n' }]
|
||||
|
||||
const speciesListCompleteOptions = ['', { display: 'Yes', value: 'y' }, { display: 'No', value: 'n' }]
|
||||
|
||||
const fossilAssemblage = [
|
||||
['Assemblage Formation', dropdown('assem_fm', assemblageFormationOptions, 'Assemblage Formation')],
|
||||
['Transport', dropdown('transport', transportOptions, 'Transport')],
|
||||
['Abrasion', dropdown('trans_mod', abrasionOptions, 'Abrasion')],
|
||||
['Weathering / Trampling', dropdown('weath_trmp', weatheringTramplingOptions, 'Weathering / Trampling')],
|
||||
['Part Concentration', dropdown('pt_conc', partConcentrationOptions, 'Part Concentration')],
|
||||
['Assemblage Component Size', dropdown('size_type', assemblageComponentSizeOptions, 'Assemblage Component Size')],
|
||||
]
|
||||
|
||||
const taphonomy = [
|
||||
['Time Represented (years)', dropdown('time_rep', timeRepresentedOptions, 'Time Represented (years)')],
|
||||
['Vertebrate Preservation', dropdown('vert_pres', vertebratePreservationOptions, 'Vertebrate Preservation')],
|
||||
['Approx. Number of Specimens', textField('appr_num_spm')],
|
||||
['Exact Number of Specimens', textField('num_spm')],
|
||||
['Number of Quadrats', textField('num_quad')],
|
||||
['Unbiased Collecting', dropdown('true_quant', unbiasedCollectingOptions, 'Unbiased Collecting')],
|
||||
['Species List Complete', dropdown('complete', speciesListCompleteOptions, 'Species List Complete')],
|
||||
]
|
||||
|
||||
const columns: MRT_ColumnDef<CollectingMethod>[] = [
|
||||
{
|
||||
accessorKey: 'coll_meth',
|
||||
header: 'Collecting Methods',
|
||||
},
|
||||
]
|
||||
|
||||
const editingForm = (
|
||||
<EditingForm
|
||||
formFields={[{ name: 'coll_meth', label: 'Collection method', required: true }]}
|
||||
buttonText="Add collecting method"
|
||||
arrayFieldName="now_coll_meth"
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<HalfFrames>
|
||||
<ArrayFrame half array={fossilAssemblage} title="Fossil Assemblage" />
|
||||
<ArrayFrame half array={taphonomy} title="Taphonomy" />
|
||||
</HalfFrames>
|
||||
|
||||
<HalfFrames>
|
||||
<Grouped title="Collecting Methods">
|
||||
{!mode.read && editingForm}
|
||||
<EditableTable<Editable<CollectingMethod>, LocalityDetailsType> columns={columns} field="now_coll_meth" />
|
||||
</Grouped>
|
||||
<></>
|
||||
</HalfFrames>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export const NavBar = () => {
|
||||
{ title: 'Front Page', url: '/' },
|
||||
{ title: 'Localities', url: '/locality' },
|
||||
{ title: 'Species', url: '/species' },
|
||||
{ title: 'Cross-Search', url: '/crosssearch' },
|
||||
{ title: 'References', url: '/reference' },
|
||||
{ title: 'Time Units', url: '/time-unit' },
|
||||
{ title: 'Time Bounds', url: '/time-bound', allowedRoles: [Role.Admin, Role.EditUnrestricted] },
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ProjectDetailsType,
|
||||
RegionDetails as RegionDetailsType,
|
||||
} from '@/backendTypes'
|
||||
import { CrossSearchTable } from './CrossSearch/CrossSearchTable'
|
||||
import { LocalityDetails } from './Locality/LocalityDetails'
|
||||
import { LocalityTable } from './Locality/LocalityTable'
|
||||
import { EditRights, Page } from './Page'
|
||||
@@ -47,6 +48,21 @@ export const localityPage = (
|
||||
/>
|
||||
)
|
||||
|
||||
export const crossSearchPage = (
|
||||
<Page
|
||||
tableView={<CrossSearchTable />}
|
||||
detailView={<LocalityDetails />}
|
||||
viewName="crosssearch"
|
||||
idFieldName="lid"
|
||||
createTitle={(loc: LocalityDetailsType) => `${loc.loc_name}`}
|
||||
getEditRights={(user: UserState, id: string | number) => {
|
||||
if ([Role.Admin, Role.EditUnrestricted].includes(user.role)) return fullRights
|
||||
if (user.role === Role.EditRestricted && user.localities.includes(id as number)) return limitedRights
|
||||
return noRights
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
export const speciesPage = (
|
||||
<Page
|
||||
tableView={<SpeciesTable />}
|
||||
|
||||
@@ -51,6 +51,7 @@ export const api = createApi({
|
||||
reducerPath: 'api',
|
||||
tagTypes: [
|
||||
'user',
|
||||
'crosssearch',
|
||||
'locality',
|
||||
'localities',
|
||||
'species',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { api } from './api'
|
||||
import { EditDataType, CrossSearch, LocalityDetailsType } from '@/backendTypes'
|
||||
|
||||
const crossSearchApi = api.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
getAllCrossSearch: builder.query<CrossSearch[], void>({
|
||||
query: () => ({
|
||||
url: `/crosssearch/all`,
|
||||
}),
|
||||
providesTags: result => (result ? [{ type: 'localities' }] : []),
|
||||
}),
|
||||
getCrossSearchDetails: builder.query<LocalityDetailsType, string>({
|
||||
query: id => ({
|
||||
url: `/locality/${id}`,
|
||||
}),
|
||||
providesTags: result => (result ? [{ type: 'locality', id: result.lid }] : []),
|
||||
}),
|
||||
getCrossSearchList: builder.mutation<string[][], number[]>({
|
||||
query: (lids: number[]) => ({
|
||||
url: `/locality-species`,
|
||||
body: { lids },
|
||||
method: 'POST',
|
||||
}),
|
||||
}),
|
||||
editLocality: builder.mutation<{ id: number }, EditDataType<LocalityDetailsType>>({
|
||||
query: locality => ({
|
||||
url: `/locality`,
|
||||
method: 'PUT',
|
||||
body: { locality },
|
||||
}),
|
||||
invalidatesTags: (result, _error, { lid }) => (result ? [{ type: 'locality', id: lid }, 'localities'] : []),
|
||||
}),
|
||||
deleteLocality: builder.mutation<void, number>({
|
||||
query: id => ({
|
||||
url: `/locality/${id}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const {
|
||||
useGetAllCrossSearchQuery,
|
||||
useGetCrossSearchDetailsQuery,
|
||||
useEditLocalityMutation,
|
||||
useGetCrossSearchListMutation,
|
||||
useDeleteLocalityMutation,
|
||||
} = crossSearchApi
|
||||
Reference in New Issue
Block a user