mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -50,6 +50,7 @@ const baseQueryWithReauth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQue
|
||||
export const api = createApi({
|
||||
reducerPath: 'api',
|
||||
tagTypes: [
|
||||
'museum',
|
||||
'user',
|
||||
'crosssearch',
|
||||
'locality',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user