Merge pull request #662 from nowcommunity/add-contact-form-feature

Add contact form feature
This commit is contained in:
Kari Lintulaakso
2025-02-11 12:15:32 +02:00
committed by GitHub
6 changed files with 207 additions and 1 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ app.use('/person', personRouter)
app.use('/project', projectRouter)
app.use('/museum', museumRouter)
app.use('/sedimentary-structure', sedimentaryStructureRouter)
app.use('/email', requireOneOf([Role.Admin]), emailRouter)
app.use('/email', emailRouter)
app.use('/version', versionRouter)
app.use('/geonames-api', geonamesRouter)
if (RUNNING_ENV === 'dev') app.use('/test', testRouter)
@@ -17,6 +17,7 @@ import { ErrorBox, ReturnButton, WriteButton } from './components'
import { ValidationObject } from '@/shared/validators/validator'
import { EditDataType } from '@/shared/types'
import { usePageContext } from '../Page'
import { ContactForm } from './common/ContactForm'
export type TabType = {
title: string
@@ -58,6 +59,7 @@ export const DetailView = <T extends object>({
validator,
isNew = false,
isUserPage = false,
isPersonPage = false,
hasStagingMode = false,
deleteFunction,
}: {
@@ -67,6 +69,7 @@ export const DetailView = <T extends object>({
validator: (editData: EditDataType<T>, field: keyof EditDataType<T>) => ValidationObject
isNew?: boolean
isUserPage?: boolean
isPersonPage?: boolean
hasStagingMode?: boolean
deleteFunction?: () => Promise<void>
}) => {
@@ -192,6 +195,7 @@ export const DetailView = <T extends object>({
{!isUserPage && <ReturnButton />}
</Box>
<Box sx={{ display: 'flex' }} gap={1}>
{!isPersonPage && !isNew && <ContactForm<T> buttonText="Contact" />}
{editRights.edit && !mode.staging && !initialState.mode.new && (
<Button
id="edit-button"
@@ -0,0 +1,154 @@
import { ContactModal } from './ContactModal'
import { useDetailContext } from '../Context/DetailContext'
import { usePageContext } from '@/components/Page'
import { useEmailMutation } from '@/redux/emailReducer'
import { useForm, Controller, UseFormSetValue } from 'react-hook-form'
import { Box, TextField, FormControlLabel, CircularProgress, Switch } from '@mui/material'
import { useUser } from '@/hooks/user'
import { useEffect } from 'react'
import { useNotify } from '@/hooks/notification'
import { useGetPersonDetailsQuery } from '@/redux/personReducer'
import { skipToken } from '@reduxjs/toolkit/query'
const formatContactEmail = (values: ContactFormValues) => {
return `
Subject: ${values.subject}\n
Name: ${values.full_name}\n
Email: ${values.email}\n
Fast Reply Requested: ${values.fast_reply_requested ? 'Yes' : 'No'} \n
Message: \n ${values.comments}
`
}
type ContactFormValues = {
subject: string
full_name: string
email: string
fast_reply_requested: boolean
comments: string
}
export const ContactForm = <T extends object>({
buttonText,
noContext,
}: {
buttonText: string
noContext?: boolean
}) => {
const [sendEmailMutation, { isSuccess, isError }] = useEmailMutation()
const { trigger, getValues, register, setValue, control, formState } = useForm<ContactFormValues>()
const { errors } = formState
const notify = useNotify()
const user = useUser()
const { data: personData, isLoading: personQueryloading } = useGetPersonDetailsQuery(user.initials ?? skipToken)
const onSend = async () => {
const result = await trigger()
if (!result) return false
const values = getValues()
const title = values.subject
const message = formatContactEmail(values)
const confirm = window.confirm(`Send email?`)
if (!confirm) return false
void sendEmailMutation({ title, message })
return true
}
useEffect(() => {
if (isSuccess) {
notify('Email sent.')
} else if (isError) {
notify('Could not send email. Error happened.', 'error')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess, isError])
if (personQueryloading) return <CircularProgress />
const form = (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
<TextField
sx={{ width: '25em' }}
key="subject"
label="Subject"
defaultValue=""
fullWidth
{...register('subject', { required: true, validate: value => !!value.trim() })}
disabled={!noContext}
error={!!errors['subject']}
/>
<br></br>
<TextField
sx={{ width: '40em' }}
key="comments"
label="Comments"
defaultValue=""
fullWidth
multiline
minRows={5}
{...register('comments', { required: true, validate: value => !!value.trim() })}
error={!!errors['comments']}
/>
<br></br>
Your contact information
<br></br>
<TextField
sx={{ width: '25em' }}
key="full_name"
label="Full Name"
defaultValue={personData?.full_name ?? ''}
fullWidth
{...register('full_name', { required: true, validate: value => !!value.trim() })}
error={!!errors['full_name']}
/>
<br></br>
<TextField
sx={{ width: '25em' }}
key="email"
label="Email"
defaultValue={personData?.email ?? ''}
{...register('email', { required: true, validate: value => !!value.trim() })}
error={!!errors['email']}
/>
<Controller
control={control}
name="fast_reply_requested"
defaultValue={false}
render={({ field: { onChange, value } }) => (
<FormControlLabel
control={<Switch checked={value} onChange={onChange} />}
label="Please contact me as soon as possible."
/>
)}
/>
</Box>
)
if (noContext) return <ContactModal {...{ buttonText, onSend }}>{form}</ContactModal>
return <ContactFormDetailView<T> buttonText={buttonText} form={form} setValue={setValue} onSend={onSend} />
}
const ContactFormDetailView = <T extends object>({
buttonText,
onSend,
form,
setValue,
}: {
buttonText: string
onSend: () => Promise<boolean>
form: JSX.Element
setValue: UseFormSetValue<ContactFormValues>
}) => {
const { data } = useDetailContext<T>()
const { createTitle } = usePageContext<T>()
useEffect(() => {
// autofills the subject field on first render
const subject = createTitle(data)
setValue('subject', subject)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return <ContactModal {...{ buttonText, onSend }}>{form}</ContactModal>
}
@@ -0,0 +1,43 @@
import { Box, Button, Modal } from '@mui/material'
import { ReactNode, useState } from 'react'
import { modalStyle } from './misc'
export const ContactModal = ({
buttonText,
onSend,
children,
}: {
buttonText: string
onSend: () => Promise<boolean>
children: ReactNode
}) => {
const [open, setOpen] = useState(false)
const closeWithSend = async () => {
const close = await onSend()
if (!close) return
setOpen(false)
}
return (
<Box>
<Button onClick={() => setOpen(true)} variant="contained">
{buttonText}
</Button>
<Modal open={open} aria-labelledby={`modal-${buttonText}`} aria-describedby={`modal-${buttonText}`}>
<Box sx={{ ...modalStyle }}>
<Box marginBottom="2em" marginTop="1em">
{' '}
{children}
</Box>
<Button sx={{ marginRight: '0.5em' }} variant="contained" onClick={() => void closeWithSend()}>
Send Email
</Button>
<Button variant="contained" onClick={() => setOpen(false)}>
{'Cancel'}
</Button>
</Box>
</Modal>
</Box>
)
}
@@ -41,6 +41,7 @@ export const PersonDetails = () => {
<DetailView
onWrite={onWrite}
isUserPage={isUserPage}
isPersonPage={true}
tabs={tabs}
data={data}
validator={() => ({ name: '', error: null })}
@@ -15,6 +15,7 @@ import { renderCustomToolbar, renderCustomToolbarModalVersion } from './helpers'
import { ActionComponent } from './ActionComponent'
import { usePageContext } from '../Page'
import { useUser } from '@/hooks/user'
import { ContactForm } from '../DetailView/common/ContactForm'
type TableStateInUrl = 'sorting' | 'columnfilters' | 'pagination'
@@ -186,6 +187,9 @@ export const TableView = <T extends MRT_RowData>({
<Divider sx={{ marginTop: '1rem' }} />
</>
)}
<Box sx={{ display: 'flex', gap: '0.4em', justifyContent: 'flex-end', margin: '0.5em', marginTop: '1em' }}>
<ContactForm<T> buttonText="Contact" noContext={true} />
</Box>
{editRights.new && title != 'Locality-Species-Cross-Search' && (
<Box sx={{ display: 'flex', gap: '0.4em', justifyContent: 'flex-end', margin: '0.5em', marginTop: '1em' }}>
<Button variant="contained" component={Link} to="new">