Museum tab (#905)

* add museum page

* add museum info tab

* add locality tab to museum details

* fix lint error

* update museum page location on navigation bar
This commit is contained in:
lllIIlIIlll
2025-07-01 15:42:08 +03:00
committed by GitHub
parent 4c6fdcb7c6
commit 121e291652
14 changed files with 336 additions and 2 deletions
+9 -1
View File
@@ -1,5 +1,6 @@
import { Router } from 'express'
import { getAllMuseums } from '../services/museum'
import { getAllMuseums, getMuseumDetails } from '../services/museum'
import { fixBigInt } from '../utils/common'
const router = Router()
@@ -8,4 +9,11 @@ router.get('/all', async (_req, res) => {
return res.status(200).send(museums)
})
router.get('/:id', async (req, res) => {
const id = req.params.id
const museum = await getMuseumDetails(id)
if (!museum) return res.status(404).send()
return res.status(200).send(fixBigInt(museum))
})
export default router
+27
View File
@@ -1,6 +1,33 @@
import { Locality } from '../../../frontend/src/shared/types'
import { nowDb } from '../utils/db'
export const getAllMuseums = async () => {
const result = await nowDb.com_mlist.findMany({})
return result
}
export const getMuseumDetails = async (id: string) => {
const museum = await nowDb.com_mlist.findUnique({
where: { museum: id },
})
if (!museum) return null
const localityLinks = await nowDb.now_mus.findMany({
where: { museum: id },
select: { lid: true },
})
const localityIds = localityLinks.map(link => link.lid)
const localities = (await nowDb.now_loc.findMany({
where: { lid: { in: localityIds } },
})) as Locality[]
const combined = {
...museum,
localities,
}
return combined
}
+2
View File
@@ -6,6 +6,7 @@ import {
crossSearchPage,
frontPage,
localityPage,
museumPage,
personPage,
projectPage,
referencePage,
@@ -34,6 +35,7 @@ const App = () => {
<Route element={crossSearchPage} path="/crosssearch/:id?" />
<Route element={localityPage} path="/locality/:id?" />
<Route element={speciesPage} path="/species/:id?" />
<Route element={museumPage} path="/museum/:id?" />
<Route element={referencePage} path="/reference/:id?" />
<Route element={timeUnitPage} path="/time-unit/:id?" />
<Route element={timeBoundPage} path="/time-bound/:id?" />
@@ -1,5 +1,6 @@
import {
LocalityDetailsType,
Museum,
ReferenceDetailsType,
RegionDetails,
SpeciesDetailsType,
@@ -248,6 +249,19 @@ export const emptyRegion = {
now_reg_coord_country: [],
} as unknown as RegionDetails
export const emptyMuseum = {
country: '',
state: '',
used_morph: false,
used_now: false,
used_gene: false,
museum: '',
institution: '',
alt_int_name: '',
city: '',
state_code: '',
} as unknown as Museum
export const emptyPerson = {
initials: '',
first_name: '',
@@ -0,0 +1,43 @@
import { useGetMuseumDetailsQuery } from '../../redux/museumReducer'
import { DetailView, TabType } from '../DetailView/DetailView'
import { MuseumInfoTab } from './Tabs/MuseumInfoTab'
import { emptyMuseum } from '../DetailView/common/defaultValues'
import { validateMuseum } from '@/shared/validators/museum'
import { useParams } from 'react-router-dom'
import { CircularProgress } from '@mui/material'
import { Museum } from '@/shared/types'
import { LocalityTab } from './Tabs/LocalityTab'
export const MuseumDetails = () => {
const { id } = useParams()
const { isError, isFetching, data } = useGetMuseumDetailsQuery(id!)
const tabs: TabType[] = [
{
title: 'Museum',
content: <MuseumInfoTab />,
},
{
title: 'Localities',
content: <LocalityTab />,
},
]
if (isError) return <div>Error loading data</div>
if (isFetching || !data) return <CircularProgress />
if (data) {
document.title = `Museum - ${data.institution}`
}
return (
<DetailView<Museum>
tabs={tabs}
data={!data ? emptyMuseum : data}
onWrite={undefined}
isNew={false}
validator={validateMuseum}
deleteFunction={undefined}
hasStagingMode
/>
)
}
@@ -0,0 +1,90 @@
import { useMemo } from 'react'
import { type MRT_ColumnDef } from 'material-react-table'
import { useGetAllMuseumsQuery } from '../../redux/museumReducer'
import { TableView } from '../TableView/TableView'
import { Museum } from '@/shared/types'
export const MuseumTable = ({ selectorFn }: { selectorFn?: (museum: Museum) => void }) => {
const { data: museumQueryData, isFetching } = useGetAllMuseumsQuery()
/*
museum: string;
institution: string;
alt_int_name: string | null;
city: string | null;
state_code: string | null;
state: string | null;
country: string | null;
used_morph: boolean | null;
used_now: boolean | null;
used_gene: boolean | null;
*/
const columns = useMemo<MRT_ColumnDef<Museum>[]>(
() => [
{
accessorKey: 'museum',
header: 'Museum',
size: 20,
enableColumnFilterModes: false,
},
{
id: 'institution',
accessorFn: row => row.institution || '',
header: 'Institution',
size: 20,
filterFn: 'contains',
},
{
id: 'city',
accessorFn: row => row.city || '',
header: 'City',
size: 20,
filterFn: 'contains',
},
{
id: 'state',
accessorFn: row => row.state || '',
header: 'State',
filterFn: 'contains',
},
{
id: 'state_code',
accessorFn: row => row.state_code || '',
header: 'State code',
filterFn: 'contains',
},
{
id: 'country',
accessorFn: row => row.country || '',
header: 'Country',
filterFn: 'contains',
},
],
[]
)
const visibleColumns = {
museum: false,
institution: true,
city: true,
state: false,
state_code: false,
country: true,
}
const museums = museumQueryData?.filter(museum => museum.institution !== '[missing details]')
return (
<TableView<Museum>
title="Museum"
selectorFn={selectorFn}
idFieldName="museum"
columns={columns}
isFetching={isFetching}
visibleColumns={visibleColumns}
data={museums}
url="museum"
/>
)
}
@@ -0,0 +1,29 @@
import { SimpleTable } from '@/components/DetailView/common/SimpleTable'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { MuseumLocalities } from '@/shared/types'
export const LocalityTab = () => {
const { data } = useDetailContext<MuseumLocalities>()
const columns = [
{
accessorKey: 'loc_name',
header: 'Locality',
},
{
accessorKey: 'country',
header: 'Country',
},
{
accessorKey: 'max_age',
header: 'Max Age',
},
{
accessorKey: 'min_age',
header: 'Min Age',
},
]
if (!data) return <div>No localities</div>
return <SimpleTable columns={columns} data={data.localities} idFieldName="lid" url="locality" />
}
@@ -0,0 +1,55 @@
import { ArrayFrame } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Museum } from '@/shared/types'
export const MuseumInfoTab = () => {
const { textField, radioSelection } = useDetailContext<Museum>()
const museumInfo = [
['Institution', textField('institution')],
['Alt. name', textField('alt_int_name')],
['City', textField('city')],
['Country', textField('country')],
['State', textField('state')],
['State code', textField('state_code')],
[
'Used morph',
radioSelection(
'used_morph',
[
{ value: 'null', display: 'No' },
{ value: 'true', display: 'Yes' },
],
'used-morph'
),
],
[
'Used now',
radioSelection(
'used_now',
[
{ value: 'null', display: 'No' },
{ value: 'true', display: 'Yes' },
],
'used-now'
),
],
[
'Used gene',
radioSelection(
'used_gene',
[
{ value: 'null', display: 'No' },
{ value: 'true', display: 'Yes' },
],
'used-gene'
),
],
['Museum code', textField('museum', { type: 'text', readonly: true })],
]
return (
<>
<ArrayFrame array={museumInfo} title="Museum information" />
</>
)
}
+1
View File
@@ -24,6 +24,7 @@ export const NavBar = () => {
{ title: 'References', url: '/reference' },
{ title: 'Time Units', url: '/time-unit' },
{ title: 'Time Bounds', url: '/time-bound', allowedRoles: [Role.Admin, Role.EditUnrestricted] },
{ title: 'Museums', url: '/museum' },
{
title: 'Admin',
url: '/admin',
+19
View File
@@ -8,6 +8,7 @@ import {
ProjectDetailsType,
RegionDetails as RegionDetailsType,
Role,
Museum,
} from '@/shared/types'
import { CrossSearchTable } from './CrossSearch/CrossSearchTable'
import { LocalityDetails } from './Locality/LocalityDetails'
@@ -29,6 +30,8 @@ import { RegionTable } from './Region/RegionTable'
import { RegionDetails } from './Region/RegionDetails'
import { UserState } from '@/redux/userReducer'
import { FrontPage } from './FrontPage'
import { MuseumTable } from './Museum/MuseumTable'
import { MuseumDetails } from './Museum/MuseumDetails'
const noRights: EditRights = {}
const fullRights: EditRights = { new: true, edit: true, delete: true }
@@ -94,6 +97,22 @@ export const speciesPage = (
/>
)
export const museumPage = (
<Page
tableView={<MuseumTable />}
detailView={<MuseumDetails />}
viewName="museum"
idFieldName="museum"
createTitle={(museum: Museum) => `${museum.institution}`}
createSubtitle={(museum: Museum) => `${museum.city ? `${museum.city}, ` : ''}${museum.country}`}
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
}}
/>
)
// from UpdateTab
const makeNameList = (names: Array<string | null | undefined>) => {
if (names.length === 3) {
+1
View File
@@ -50,6 +50,7 @@ const baseQueryWithReauth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQue
export const api = createApi({
reducerPath: 'api',
tagTypes: [
'museum',
'user',
'crosssearch',
'locality',
+7 -1
View File
@@ -8,7 +8,13 @@ const museumsApi = api.injectEndpoints({
url: `/museum/all`,
}),
}),
getMuseumDetails: builder.query<Museum, string>({
query: id => ({
url: `museum/${id}`,
}),
providesTags: result => (result ? [{ type: 'museum', id: result.museum }] : []),
}),
}),
})
export const { useGetAllMuseumsQuery } = museumsApi
export const { useGetAllMuseumsQuery, useGetMuseumDetailsQuery } = museumsApi
+1
View File
@@ -24,6 +24,7 @@ export type SpeciesLocality = FixBigInt<Prisma.now_ls> & { now_loc: Prisma.now_l
export type LocalityUpdate = Prisma.now_lau & { now_lr: LocalityReference[] } & { updates: UpdateLog[] }
export type SpeciesUpdate = Prisma.now_sau & { now_sr: SpeciesReference[] } & { updates: UpdateLog[] }
export type Museum = Prisma.com_mlist
export type MuseumLocalities = Prisma.com_mlist & { localities: Prisma.now_loc[] }
export type PersonDetailsType = Prisma.com_people & { user: Omit<Prisma.com_users, 'password, newpassword'> | null } & {
now_user_group: string
}
+38
View File
@@ -0,0 +1,38 @@
import { EditDataType, Museum } from '../types'
import { Validators, validator } from './validator'
/*
museum: string;
institution: string;
alt_int_name: string | null;
city: string | null;
state_code: string | null;
state: string | null;
country: string | null;
used_morph: boolean | null;
used_now: boolean | null;
used_gene: boolean | null;
*/
export const validateMuseum = (editData: EditDataType<Museum>, fieldName: keyof EditDataType<Museum>) => {
const validators: Validators<Partial<EditDataType<Museum>>> = {
museum: {
name: 'Museum',
required: true,
},
institution: {
name: 'Institution',
required: true,
},
city: {
name: 'City',
required: true,
},
country: {
name: 'Country',
required: true,
},
}
return validator<EditDataType<Museum>>(validators, editData, fieldName)
}