Skip to content

Commit 4c6fdcb

Browse files
authored
Merge pull request #904 from nowcommunity/admin-can-add-new-person
Admin can add new person
2 parents 7c0d49e + 8a69641 commit 4c6fdcb

8 files changed

Lines changed: 92 additions & 14 deletions

File tree

backend/src/routes/person.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,9 @@ router.put(
4141
'/',
4242
async (req: Request<object, object, { person: EditDataType<PersonDetailsType> & EditMetaData }>, res) => {
4343
const { ...editedPerson } = req.body.person
44-
4544
if (!editedPerson.initials) {
4645
return res.status(403).send({ error: 'Missing initials, creating new persons is not yet implemented' })
4746
}
48-
4947
/* Access checking happens differently for this route, since we want to allow users to modify their own data */
5048
if (!req.user)
5149
return res.status(401).send({

backend/src/services/write/person.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
import { EditDataType, PersonDetailsType } from '../../../../frontend/src/shared/types'
22
import Prisma from '../../../prisma/generated/now_test_client'
33
import { nowDb, getFieldsOfTables } from '../../utils/db'
4+
import { getPersonDetails } from '../person'
45
import { filterAllowedKeys } from './writeOperations/utils'
56

67
export const writePerson = async (person: EditDataType<PersonDetailsType>) => {
78
const allowedColumns = getFieldsOfTables(['com_people'])
89
const filteredPerson = filterAllowedKeys(person, allowedColumns) as Prisma.com_people
910
let personId: string
1011

11-
if (!filteredPerson.initials) {
12-
throw new Error('Missing initials, creating new persons is not yet implemented')
12+
const isPerson = await getPersonDetails(filteredPerson.initials)
13+
14+
if (!isPerson) {
15+
const newPerson = await nowDb.com_people.create({
16+
data: { ...filteredPerson, full_name: `${filteredPerson.first_name} ${filteredPerson.surname}` },
17+
})
18+
personId = newPerson.initials
1319
} else {
1420
await nowDb.com_people.update({
1521
where: { initials: person.initials },
16-
data: { ...filteredPerson, full_name: `${filteredPerson.first_name} ${filteredPerson.surname}` },
22+
data: {
23+
first_name: filteredPerson.first_name,
24+
surname: filteredPerson.surname,
25+
email: filteredPerson.email,
26+
organization: filteredPerson.organization,
27+
country: filteredPerson.country,
28+
full_name: `${filteredPerson.first_name} ${filteredPerson.surname}`,
29+
},
1730
})
1831
personId = filteredPerson.initials
1932
}

frontend/src/components/DetailView/common/defaultValues.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
SpeciesDetailsType,
66
TimeBoundDetailsType,
77
TimeUnitDetailsType,
8+
PersonDetailsType,
89
} from '@/shared/types'
10+
911
import { MRT_PaginationState } from 'material-react-table'
1012

1113
export const emptyLocality = {
@@ -246,5 +248,14 @@ export const emptyRegion = {
246248
now_reg_coord_country: [],
247249
} as unknown as RegionDetails
248250

251+
export const emptyPerson = {
252+
initials: '',
253+
first_name: '',
254+
surname: '',
255+
email: '',
256+
organization: '',
257+
country: '',
258+
} as unknown as PersonDetailsType
259+
249260
export const defaultPagination: MRT_PaginationState = { pageIndex: 0, pageSize: 15 }
250261
export const defaultPaginationSmall: MRT_PaginationState = { pageIndex: 0, pageSize: 10 }

frontend/src/components/Person/PersonDetails.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { useParams, useNavigate } from 'react-router-dom'
2-
import { useEditPersonMutation, useGetPersonDetailsQuery } from '../../redux/personReducer'
2+
import {
3+
useEditPersonMutation,
4+
useGetPersonDetailsIdMutation,
5+
useGetPersonDetailsQuery,
6+
} from '../../redux/personReducer'
37
import { CircularProgress } from '@mui/material'
48
import { DetailView, TabType } from '../DetailView/DetailView'
59
import { PersonTab } from './Tabs/PersonTab'
@@ -8,6 +12,7 @@ import { EditDataType, PersonDetailsType, Role, ValidationErrors } from '@/share
812
import { validatePerson } from '@/shared/validators/person'
913
import { useNotify } from '@/hooks/notification'
1014
import { useEffect } from 'react'
15+
import { emptyPerson } from '../DetailView/common/defaultValues'
1116

1217
export const PersonDetails = () => {
1318
const { id: idFromUrl } = useParams()
@@ -21,8 +26,10 @@ export const PersonDetails = () => {
2126
// We designate special id 'user-page' instead of normal initials to mean current user's own page.
2227
const isUserPage = idFromUrl === 'user-page'
2328
const id = isUserPage ? user.initials : idFromUrl
29+
const isNew = idFromUrl === 'new'
2430

25-
const { isLoading, isError, data } = useGetPersonDetailsQuery(id!)
31+
const { isLoading, isError, data } = useGetPersonDetailsQuery(id!, { skip: isNew })
32+
const [getPersonDetailsId] = useGetPersonDetailsIdMutation()
2633

2734
useEffect(() => {
2835
if (!isUserPage && user.role !== Role.Admin) {
@@ -32,8 +39,23 @@ export const PersonDetails = () => {
3239
// eslint-disable-next-line react-hooks/exhaustive-deps
3340
}, [])
3441

42+
const personExists = async (initials: string) => {
43+
try {
44+
const isPerson = await getPersonDetailsId(initials).unwrap()
45+
if (isPerson) return true
46+
return false
47+
} catch {
48+
return false
49+
}
50+
}
51+
3552
const onWrite = async (editData: EditDataType<PersonDetailsType>) => {
53+
if (!editData.initials) return
3654
try {
55+
if (isNew && (await personExists(editData.initials))) {
56+
notify('Initials already exists. Select Edit.', 'error')
57+
return
58+
}
3759
const { initials } = await editPersonRequest(editData).unwrap()
3860
notify('Saved person successfully.')
3961
if (isUserPage) {
@@ -48,9 +70,9 @@ export const PersonDetails = () => {
4870
}
4971

5072
if (isError) return <div>Error loading data</div>
51-
if (isLoading || !data || mutationLoading) return <CircularProgress />
73+
if (isLoading || (!data && !isNew) || mutationLoading) return <CircularProgress />
5274

53-
document.title = `User - ${data.user?.user_name}`
75+
document.title = isNew ? 'New person' : `User - ${data!.user?.user_name}`
5476

5577
const tabs: TabType[] = [
5678
{
@@ -62,10 +84,11 @@ export const PersonDetails = () => {
6284
return (
6385
<DetailView
6486
onWrite={onWrite}
87+
isNew={isNew}
6588
isUserPage={isUserPage}
6689
isPersonPage={true}
6790
tabs={tabs}
68-
data={data}
91+
data={isNew ? emptyPerson : data!}
6992
validator={validatePerson}
7093
/>
7194
)

frontend/src/components/Person/Tabs/PersonTab.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import { Box, Button } from '@mui/material'
88
import { useEffect, useState } from 'react'
99
import { useNotify } from '@/hooks/notification'
1010
import { validCountries } from '@/shared/validators/countryList'
11+
import { useParams } from 'react-router-dom'
1112
import PersonAddIcon from '@mui/icons-material/PersonAdd'
1213
import { AddUserModal } from './AddUserModal'
1314

1415
export const PersonTab = () => {
15-
const { textField, dropdownWithSearch, data } = useDetailContext<PersonDetailsType>()
16+
const { textField, dropdownWithSearch, data, mode } = useDetailContext<PersonDetailsType>()
1617
const currentUser = useUser()
1718
const notify = useNotify()
19+
const { id: idFromUrl } = useParams()
20+
const isNew = idFromUrl === 'new'
1821
const isAdmin = currentUser.role == Role.Admin
1922
const [isAddUserModalOpen, setIsAddUserModalOpen] = useState(false)
2023
const [disableAddUserButton, setDisableAddUserButton] = useState(false)
@@ -29,7 +32,7 @@ export const PersonTab = () => {
2932
const countryOptions = ['', ...validCountries]
3033

3134
const person = [
32-
['Initials', textField('initials', { type: 'text', disabled: true })],
35+
['Initials', textField('initials', { type: 'text', disabled: !isNew })],
3336
['First Name', textField('first_name')],
3437
['Surname', textField('surname')],
3538
['Email', textField('email')],
@@ -57,7 +60,7 @@ export const PersonTab = () => {
5760
<ArrayFrame array={person} title="Person" />
5861
{data.user && <ArrayFrame array={user} title="User" />}
5962

60-
{isAdmin && !data.user && !disableAddUserButton && (
63+
{isAdmin && !data.user && !disableAddUserButton && mode.option === 'read' && (
6164
<Button
6265
variant="contained"
6366
startIcon={<PersonAddIcon />}

frontend/src/redux/personReducer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const personsApi = api.injectEndpoints({
1515
}),
1616
providesTags: result => (result ? [{ type: 'person', id: result.initials }] : []),
1717
}),
18+
getPersonDetailsId: builder.mutation<PersonDetailsType, string>({
19+
query: id => ({
20+
url: `/person/${id}`,
21+
}),
22+
}),
1823
editPerson: builder.mutation<PersonDetailsType, EditDataType<PersonDetailsType>>({
1924
query: person => ({
2025
url: `/person`,
@@ -26,4 +31,5 @@ const personsApi = api.injectEndpoints({
2631
}),
2732
})
2833

29-
export const { useGetAllPersonsQuery, useGetPersonDetailsQuery, useEditPersonMutation } = personsApi
34+
export const { useGetAllPersonsQuery, useGetPersonDetailsQuery, useEditPersonMutation, useGetPersonDetailsIdMutation } =
35+
personsApi

frontend/src/shared/types/data.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,15 @@ export type RegionDetails = Prisma.now_reg_coord & { now_reg_coord_people: Array
337337
now_reg_coord_country: Array<RegionCountry>
338338
}
339339

340+
export type PersonDetails = {
341+
initials: string | null
342+
first_name: string | null
343+
surname: string | null
344+
email: string | null
345+
organization: string | null
346+
country: string | null
347+
}
348+
340349
// used in Region's CoordinatorTab to allow adding com_people field to the editData manually
341350
export type RegionDetailsWithComPeople = Prisma.now_reg_coord & {
342351
now_reg_coord_people: Array<RegionCoordinator & { com_people?: Prisma.com_people }>

frontend/src/shared/validators/person.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,37 @@ export const validatePerson = (
99
initials: {
1010
name: 'initials',
1111
required: true,
12+
asString: true,
13+
minLength: 2,
14+
maxLength: 8,
1215
},
1316
first_name: {
1417
name: 'First Name',
1518
required: true,
19+
asString: true,
20+
minLength: 2,
21+
maxLength: 20,
1622
},
1723
surname: {
1824
name: 'Surname',
1925
required: true,
26+
asString: true,
27+
minLength: 2,
28+
maxLength: 20,
2029
},
2130
email: {
2231
name: 'Email',
2332
required: true,
33+
asString: true,
34+
minLength: 5,
35+
maxLength: 30,
2436
},
2537
organization: {
2638
name: 'Organization',
2739
required: true,
40+
asString: true,
41+
minLength: 2,
42+
maxLength: 30,
2843
},
2944
country: {
3045
name: 'Country',

0 commit comments

Comments
 (0)