parent
ce2fad66c7
commit
c623170334
7 changed files with 542 additions and 0 deletions
@ -0,0 +1,270 @@ |
||||
import { Grid } from '@mui/material'; |
||||
import { FC, ReactNode, useCallback, useMemo, useRef } from 'react'; |
||||
import { v4 as uuidv4 } from 'uuid'; |
||||
|
||||
import ActionGroup from '../ActionGroup'; |
||||
import api from '../../lib/api'; |
||||
import handleAPIError from '../../lib/handleAPIError'; |
||||
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; |
||||
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; |
||||
import mailServerListSchema from './schema'; |
||||
import SelectWithLabel from '../SelectWithLabel'; |
||||
import useFormikUtils from '../../hooks/useFormikUtils'; |
||||
import UncontrolledInput from '../UncontrolledInput'; |
||||
|
||||
const AddMailServerForm: FC<AddMailServerFormProps> = (props) => { |
||||
const { |
||||
localhostDomain = '', |
||||
mailServerUuid, |
||||
onSubmit, |
||||
previousFormikValues, |
||||
} = props; |
||||
|
||||
const msUuid = useMemo<string>( |
||||
() => mailServerUuid ?? uuidv4(), |
||||
[mailServerUuid], |
||||
); |
||||
|
||||
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({}); |
||||
|
||||
const setApiMessage = useCallback( |
||||
(message?: Message) => |
||||
messageGroupRef.current.setMessage?.call(null, 'api', message), |
||||
[], |
||||
); |
||||
|
||||
const { |
||||
disableAutocomplete, |
||||
disabledSubmit, |
||||
formik, |
||||
formikErrors, |
||||
handleChange, |
||||
} = useFormikUtils<MailServerFormikValues>({ |
||||
initialValues: previousFormikValues ?? { |
||||
[msUuid]: { |
||||
address: '', |
||||
authentication: 'none', |
||||
heloDomain: localhostDomain, |
||||
port: 587, |
||||
security: 'none', |
||||
uuid: msUuid, |
||||
}, |
||||
}, |
||||
onSubmit: (...args) => { |
||||
onSubmit( |
||||
{ |
||||
mailServer: args[0][msUuid], |
||||
onConfirmCancel: (values, { setSubmitting }) => setSubmitting(false), |
||||
onConfirmProceed: (values, { setSubmitting }) => { |
||||
let errorMessage: ReactNode = <>Failed to add mail server.</>; |
||||
let method: 'post' | 'put' = 'post'; |
||||
let successMessage = <>Mail server added.</>; |
||||
let url = '/mail-server'; |
||||
|
||||
if (previousFormikValues) { |
||||
errorMessage = <>Failed to update mail server.</>; |
||||
method = 'put'; |
||||
successMessage = <>Mail server updated.</>; |
||||
url += `/${msUuid}`; |
||||
} |
||||
|
||||
api[method](url, values[msUuid]) |
||||
.then(() => { |
||||
setApiMessage({ children: successMessage }); |
||||
}) |
||||
.catch((error) => { |
||||
const emsg = handleAPIError(error); |
||||
|
||||
emsg.children = ( |
||||
<> |
||||
{errorMessage} {emsg.children} |
||||
</> |
||||
); |
||||
|
||||
setApiMessage(emsg); |
||||
}) |
||||
.finally(() => { |
||||
setSubmitting(false); |
||||
}); |
||||
}, |
||||
}, |
||||
...args, |
||||
); |
||||
}, |
||||
validationSchema: mailServerListSchema, |
||||
}); |
||||
|
||||
const addressChain = useMemo<string>(() => `${msUuid}.address`, [msUuid]); |
||||
const authenticationChain = useMemo<string>( |
||||
() => `${msUuid}.authentication`, |
||||
[msUuid], |
||||
); |
||||
const confirmPasswordChain = useMemo<string>( |
||||
() => `${msUuid}.confirmPassword`, |
||||
[msUuid], |
||||
); |
||||
const heloDomainChain = useMemo<string>( |
||||
() => `${msUuid}.heloDomain`, |
||||
[msUuid], |
||||
); |
||||
const passwordChain = useMemo<string>(() => `${msUuid}.password`, [msUuid]); |
||||
const portChain = useMemo<string>(() => `${msUuid}.port`, [msUuid]); |
||||
const securityChain = useMemo<string>(() => `${msUuid}.security`, [msUuid]); |
||||
const usernameChain = useMemo<string>(() => `${msUuid}.username`, [msUuid]); |
||||
|
||||
return ( |
||||
<Grid |
||||
component="form" |
||||
onSubmit={(event) => { |
||||
event.preventDefault(); |
||||
|
||||
formik.submitForm(); |
||||
}} |
||||
container |
||||
columns={{ xs: 1, sm: 2 }} |
||||
spacing="1em" |
||||
> |
||||
<Grid item xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={addressChain} |
||||
label="Server address" |
||||
name={addressChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
required |
||||
value={formik.values[msUuid].address} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={portChain} |
||||
label="Server port" |
||||
name={portChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
required |
||||
type="number" |
||||
value={formik.values[msUuid].port} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item sm={2} xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<SelectWithLabel |
||||
id={securityChain} |
||||
label="Server security type" |
||||
name={securityChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
required |
||||
selectItems={['none', 'starttls', 'tls-ssl']} |
||||
value={formik.values[msUuid].security} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item sm={2} xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<SelectWithLabel |
||||
id={authenticationChain} |
||||
label="Server authentication method" |
||||
name={authenticationChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
required |
||||
selectItems={['none', 'plain-text', 'encrypted']} |
||||
value={formik.values[msUuid].authentication} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item sm={2} xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={heloDomainChain} |
||||
label="HELO domain" |
||||
name={heloDomainChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
required |
||||
value={formik.values[msUuid].heloDomain} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={usernameChain} |
||||
inputProps={disableAutocomplete()} |
||||
label="Server username" |
||||
name={usernameChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
value={formik.values[msUuid].username} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={passwordChain} |
||||
label="Server password" |
||||
name={passwordChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
type="password" |
||||
value={formik.values[msUuid].password} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item xs={1} /> |
||||
<Grid item xs={1}> |
||||
<UncontrolledInput |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={confirmPasswordChain} |
||||
label="Confirm password" |
||||
name={confirmPasswordChain} |
||||
onBlur={formik.handleBlur} |
||||
onChange={handleChange} |
||||
type="password" |
||||
value={formik.values[msUuid].confirmPassword} |
||||
/> |
||||
} |
||||
/> |
||||
</Grid> |
||||
<Grid item width="100%"> |
||||
<MessageGroup count={1} messages={formikErrors} ref={messageGroupRef} /> |
||||
</Grid> |
||||
<Grid item width="100%"> |
||||
<ActionGroup |
||||
actions={[ |
||||
{ |
||||
background: 'blue', |
||||
children: previousFormikValues ? 'Update' : 'Add', |
||||
disabled: disabledSubmit, |
||||
type: 'submit', |
||||
}, |
||||
]} |
||||
/> |
||||
</Grid> |
||||
</Grid> |
||||
); |
||||
}; |
||||
|
||||
export default AddMailServerForm; |
@ -0,0 +1,9 @@ |
||||
import { FC } from 'react'; |
||||
|
||||
import AddMailServerForm from './AddMailServerForm'; |
||||
|
||||
const EditMailServerForm: FC<EditMailServerFormProps> = (props) => ( |
||||
<AddMailServerForm {...props} /> |
||||
); |
||||
|
||||
export default EditMailServerForm; |
@ -0,0 +1,179 @@ |
||||
import { FC, useRef, useState } from 'react'; |
||||
|
||||
import API_BASE_URL from '../../lib/consts/API_BASE_URL'; |
||||
|
||||
import AddMailServerForm from './AddMailServerForm'; |
||||
import api from '../../lib/api'; |
||||
import { DialogWithHeader } from '../Dialog'; |
||||
import EditMailServerForm from './EditMailServerForm'; |
||||
import FormSummary from '../FormSummary'; |
||||
import List from '../List'; |
||||
import { ExpandablePanel } from '../Panels'; |
||||
import periodicFetch from '../../lib/fetchers/periodicFetch'; |
||||
import { BodyText } from '../Text'; |
||||
import useActiveFetch from '../../hooks/useActiveFetch'; |
||||
import useChecklist from '../../hooks/useChecklist'; |
||||
import useConfirmDialog from '../../hooks/useConfirmDialog'; |
||||
import useFetch from '../../hooks/useFetch'; |
||||
|
||||
const ManageMailServer: FC = () => { |
||||
const addDialogRef = useRef<DialogForwardedRefContent>(null); |
||||
const editDialogRef = useRef<DialogForwardedRefContent>(null); |
||||
|
||||
const { confirmDialog, setConfirmDialogOpen, setConfirmDialogProps } = |
||||
useConfirmDialog({ initial: { closeOnProceed: true } }); |
||||
|
||||
const [edit, setEdit] = useState<boolean>(false); |
||||
const [mailServer, setMailServer] = useState< |
||||
APIMailServerDetail | undefined |
||||
>(); |
||||
const [mailServers, setMailServers] = useState< |
||||
APIMailServerOverviewList | undefined |
||||
>(); |
||||
|
||||
const { isLoading: loadingMailServersPeriodic } = |
||||
periodicFetch<APIMailServerOverviewList>(`${API_BASE_URL}/mail-server`, { |
||||
onSuccess: (data) => setMailServers(data), |
||||
}); |
||||
|
||||
const { fetch: getMailServers, loading: loadingMailServersActive } = |
||||
useActiveFetch<APIMailServerOverviewList>({ |
||||
onData: (data) => setMailServers(data), |
||||
url: '/mail-server', |
||||
}); |
||||
|
||||
const { fetch: getMailServer, loading: loadingMailServer } = |
||||
useActiveFetch<APIMailServerDetail>({ |
||||
onData: (data) => setMailServer(data), |
||||
url: '/mail-server', |
||||
}); |
||||
|
||||
const { data: host, loading: loadingHost } = |
||||
useFetch<APIHostDetail>('/host/local'); |
||||
|
||||
const { |
||||
buildDeleteDialogProps, |
||||
checks, |
||||
getCheck, |
||||
hasAllChecks, |
||||
hasChecks, |
||||
multipleItems, |
||||
resetChecks, |
||||
setAllChecks, |
||||
setCheck, |
||||
} = useChecklist({ list: mailServers }); |
||||
|
||||
return ( |
||||
<> |
||||
<ExpandablePanel expandInitially header="Manage mail servers"> |
||||
<List |
||||
allowCheckAll={multipleItems} |
||||
allowEdit |
||||
allowItemButton={edit} |
||||
disableDelete={!hasChecks} |
||||
edit={edit} |
||||
getListCheckboxProps={() => ({ |
||||
checked: hasAllChecks, |
||||
onChange: (event, checked) => setAllChecks(checked), |
||||
})} |
||||
getListItemCheckboxProps={(uuid) => ({ |
||||
checked: getCheck(uuid), |
||||
onChange: (event, checked) => setCheck(uuid, checked), |
||||
})} |
||||
header |
||||
listEmpty="No mail server(s) found." |
||||
listItems={mailServers} |
||||
loading={loadingMailServersPeriodic || loadingMailServersActive} |
||||
onAdd={() => addDialogRef?.current?.setOpen(true)} |
||||
onDelete={() => { |
||||
setConfirmDialogProps({ |
||||
...buildDeleteDialogProps({ |
||||
onProceedAppend: () => { |
||||
Promise.all( |
||||
checks.map((uuid) => api.delete(`/mail-server/${uuid}`)), |
||||
).then(() => getMailServers()); |
||||
|
||||
resetChecks(); |
||||
}, |
||||
getConfirmDialogTitle: (count) => |
||||
`Delete the following ${count} mail server(s)?`, |
||||
renderEntry: ({ key }) => { |
||||
const ms = mailServers?.[key]; |
||||
|
||||
return ( |
||||
<BodyText> |
||||
{ms?.address}:{ms?.port} |
||||
</BodyText> |
||||
); |
||||
}, |
||||
}), |
||||
}); |
||||
|
||||
setConfirmDialogOpen(true); |
||||
}} |
||||
onEdit={() => setEdit((previous) => !previous)} |
||||
onItemClick={(value, uuid) => { |
||||
editDialogRef?.current?.setOpen(true); |
||||
|
||||
getMailServer(`/${uuid}`); |
||||
}} |
||||
renderListItem={(uuid, { address, port }) => ( |
||||
<BodyText> |
||||
{address}:{port} |
||||
</BodyText> |
||||
)} |
||||
/> |
||||
</ExpandablePanel> |
||||
<DialogWithHeader |
||||
header="Add mail server" |
||||
loading={loadingMailServersPeriodic || loadingHost} |
||||
ref={addDialogRef} |
||||
showClose |
||||
> |
||||
{host && ( |
||||
<AddMailServerForm |
||||
localhostDomain={host.domain} |
||||
onSubmit={(tools, ...args) => { |
||||
setConfirmDialogProps({ |
||||
actionProceedText: 'Add', |
||||
content: <FormSummary entries={tools.mailServer} />, |
||||
onCancelAppend: () => tools.onConfirmCancel(...args), |
||||
onProceedAppend: () => tools.onConfirmProceed(...args), |
||||
titleText: 'Add mail server with the following?', |
||||
}); |
||||
|
||||
setConfirmDialogOpen(true); |
||||
}} |
||||
/> |
||||
)} |
||||
</DialogWithHeader> |
||||
<DialogWithHeader |
||||
header="Update mail server" |
||||
loading={loadingMailServersPeriodic || loadingMailServer} |
||||
ref={editDialogRef} |
||||
showClose |
||||
> |
||||
{mailServer && ( |
||||
<EditMailServerForm |
||||
mailServerUuid={mailServer.uuid} |
||||
onSubmit={(tools, ...args) => { |
||||
setConfirmDialogProps({ |
||||
actionProceedText: 'Update', |
||||
content: <FormSummary entries={tools.mailServer} />, |
||||
onCancelAppend: () => tools.onConfirmCancel(...args), |
||||
onProceedAppend: () => tools.onConfirmProceed(...args), |
||||
titleText: 'Update mail server with the following?', |
||||
}); |
||||
|
||||
setConfirmDialogOpen(true); |
||||
}} |
||||
previousFormikValues={{ [mailServer.uuid]: mailServer }} |
||||
/> |
||||
)} |
||||
</DialogWithHeader> |
||||
{confirmDialog} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ManageMailServer; |
@ -0,0 +1,4 @@ |
||||
import AddMailServerForm from './AddMailServerForm'; |
||||
import ManageMailServer from './ManageMailServer'; |
||||
|
||||
export { AddMailServerForm, ManageMailServer }; |
@ -0,0 +1,27 @@ |
||||
import * as yup from 'yup'; |
||||
|
||||
import buildYupDynamicObject from '../../lib/buildYupDynamicObject'; |
||||
|
||||
const mailServerSchema = yup.object({ |
||||
address: yup.string().required(), |
||||
authentication: yup.string().oneOf(['none', 'plain-text', 'encrypted']), |
||||
confirmPassword: yup |
||||
.string() |
||||
.when('password', (password, field) => |
||||
String(password).length > 0 |
||||
? field.required().oneOf([yup.ref('password')]) |
||||
: field.optional(), |
||||
), |
||||
heloDomain: yup.string().required(), |
||||
password: yup.string().optional(), |
||||
port: yup.number().required(), |
||||
security: yup.string().oneOf(['none', 'starttls', 'tls-ssl']), |
||||
username: yup.string().optional(), |
||||
uuid: yup.string().uuid().required(), |
||||
}); |
||||
|
||||
const mailServerListSchema = yup.lazy((mailServers) => |
||||
yup.object(buildYupDynamicObject(mailServers, mailServerSchema)), |
||||
); |
||||
|
||||
export default mailServerListSchema; |
@ -0,0 +1,10 @@ |
||||
type APIMailServerOverview = Pick< |
||||
MailServerFormikMailServer, |
||||
'address' | 'port' | 'uuid' |
||||
>; |
||||
|
||||
type APIMailServerOverviewList = { |
||||
[uuid: string]: APIMailServerOverview; |
||||
}; |
||||
|
||||
type APIMailServerDetail = MailServerFormikMailServer; |
@ -0,0 +1,43 @@ |
||||
type MailServerFormikMailServer = { |
||||
address: string; |
||||
authentication: 'none' | 'plain-text' | 'encrypted'; |
||||
confirmPassword?: string; |
||||
heloDomain: string; |
||||
password?: string; |
||||
port: number; |
||||
security: 'none' | 'starttls' | 'tls-ssl'; |
||||
username?: string; |
||||
uuid: string; |
||||
}; |
||||
|
||||
type MailServerFormikValues = { |
||||
[mailServerUuid: string]: MailServerFormikMailServer; |
||||
}; |
||||
|
||||
/** AddMailServerForm */ |
||||
|
||||
type FormikSubmitHandler = |
||||
import('formik').FormikConfig<MailServerFormikValues>['onSubmit']; |
||||
|
||||
type AddMailServerFormOptionalProps = { |
||||
localhostDomain?: string; |
||||
mailServerUuid?: string; |
||||
previousFormikValues?: MailServerFormikValues; |
||||
}; |
||||
|
||||
type AddMailServerFormProps = AddMailServerFormOptionalProps & { |
||||
onSubmit: ( |
||||
tools: { |
||||
mailServer: MailServerFormikMailServer; |
||||
onConfirmCancel: FormikSubmitHandler; |
||||
onConfirmProceed: FormikSubmitHandler; |
||||
}, |
||||
...args: Parameters<FormikSubmitHandler> |
||||
) => ReturnType<FormikSubmitHandler>; |
||||
}; |
||||
|
||||
/** EditMailServerForm */ |
||||
|
||||
type EditMailServerFormProps = Required< |
||||
Omit<AddMailServerFormProps, 'localhostDomain'> |
||||
>; |
Loading…
Reference in new issue