mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Merge pull request #662 from nowcommunity/add-contact-form-feature
Add contact form feature
This commit is contained in:
+1
-1
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user