Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

267 lines
8.6 KiB

import { Box, BoxProps, SxProps, Theme } from '@mui/material';
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
import INPUT_TYPES from '../lib/consts/INPUT_TYPES';
import ContainedButton from './ContainedButton';
import FlexBox from './FlexBox';
import Grid from './Grid';
import InputWithRef, { InputForwardedRefContent } from './InputWithRef';
import MessageGroup, { MessageGroupForwardedRefContent } from './MessageGroup';
import OutlinedInputWithLabel from './OutlinedInputWithLabel';
import Spinner from './Spinner';
import { buildPeacefulStringTestBatch } from '../lib/test_input';
import useFormUtils from '../hooks/useFormUtils';
import useProtectedState from '../hooks/useProtectedState';
const INPUT_ROOT_SX: SxProps<Theme> = { width: '100%' };
const INPUT_ID_PREFIX_GATE = 'gate-input';
const INPUT_ID_GATE_ID = `${INPUT_ID_PREFIX_GATE}-credential-id`;
const INPUT_ID_GATE_PASSPHRASE = `${INPUT_ID_PREFIX_GATE}-credential-passphrase`;
const MSG_ID_GATE_ACCESS = 'access';
const GateForm = forwardRef<GateFormForwardedRefContent, GateFormProps>(
(
{
formContainer: isFormContainer = true,
gridProps: {
columns: gridColumns = { xs: 1, sm: 2 },
layout,
spacing: gridSpacing = '1em',
...restGridProps
} = {},
identifierId = INPUT_ID_GATE_ID,
identifierInputTestBatchBuilder:
buildIdentifierInputTestBatch = buildPeacefulStringTestBatch,
identifierLabel,
identifierOutlinedInputWithLabelProps: {
formControlProps: identifierFormControlProps = {},
inputProps: identifierInputProps,
...restIdentifierOutlinedInputWithLabelProps
} = {},
onIdentifierBlurAppend,
onSubmit,
onSubmitAppend,
passphraseId = INPUT_ID_GATE_PASSPHRASE,
passphraseLabel,
passphraseOutlinedInputWithLabelProps: {
formControlProps: passphraseFormControlProps = {},
inputProps: passphraseInputProps,
...restPassphraseOutlinedInputWithLabelProps
} = {},
submitLabel,
// Props that depend on others.
allowSubmit: isAllowSubmit = isFormContainer,
},
ref,
) => {
const { sx: identifierSx, ...restIdentifierFormControlProps } =
identifierFormControlProps;
const { sx: passphraseSx, ...restPassphraseFormControlProps } =
passphraseFormControlProps;
const inputIdentifierRef = useRef<InputForwardedRefContent<'string'>>({});
const inputPassphraseRef = useRef<InputForwardedRefContent<'string'>>({});
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
const [isSubmitting, setIsSubmitting] = useProtectedState<boolean>(false);
const formUtils = useFormUtils(
[INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE],
messageGroupRef,
);
const {
buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction,
buildInputUnmountFunction,
isFormInvalid,
setMessage,
} = formUtils;
const submitHandler: DivFormEventHandler = useMemo(
() =>
onSubmit ??
((...args) => {
const { 0: event } = args;
event.preventDefault();
setMessage(MSG_ID_GATE_ACCESS);
setIsSubmitting(true);
const { target } = event;
const { elements } = target as HTMLFormElement;
const { value: identifierValue } = elements.namedItem(
INPUT_ID_GATE_ID,
) as HTMLInputElement;
const { value: passphraseValue } = elements.namedItem(
INPUT_ID_GATE_PASSPHRASE,
) as HTMLInputElement;
onSubmitAppend?.call(
null,
identifierValue,
passphraseValue,
(message?) => {
setMessage(MSG_ID_GATE_ACCESS, message);
},
setIsSubmitting,
...args,
);
}),
[onSubmit, onSubmitAppend, setIsSubmitting, setMessage],
);
const submitElement = useMemo(
() =>
isSubmitting ? (
<Spinner mt={0} />
) : (
<FlexBox row sx={{ justifyContent: 'flex-end' }}>
<ContainedButton disabled={isFormInvalid} type="submit">
{submitLabel}
</ContainedButton>
</FlexBox>
),
[isFormInvalid, isSubmitting, submitLabel],
);
const submitAreaGridLayout = useMemo(() => {
const result: GridLayout = {};
if (isAllowSubmit) {
result['gate-cell-message-group'] = {
children: <MessageGroup count={1} ref={messageGroupRef} />,
sm: 2,
};
result['gate-cell-submit'] = { children: submitElement, sm: 2 };
}
return result;
}, [isAllowSubmit, submitElement]);
const containerProps = useMemo(() => {
const result: BoxProps = {};
if (isFormContainer) {
result.component = 'form';
result.onSubmit = submitHandler;
}
return result;
}, [isFormContainer, submitHandler]);
useImperativeHandle(ref, () => ({
get: () => ({
identifier: inputIdentifierRef.current.getValue?.call(null) ?? '',
passphrase: inputPassphraseRef.current.getValue?.call(null) ?? '',
}),
messageGroup: {
...messageGroupRef.current,
},
setIsSubmitting: (value) => {
setIsSubmitting(value);
},
}));
return (
<Box {...containerProps}>
<Grid
columns={gridColumns}
layout={{
'gate-input-cell-credential-id': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
formControlProps={{
...restIdentifierFormControlProps,
sx: { ...INPUT_ROOT_SX, ...identifierSx },
}}
id={identifierId}
inputProps={identifierInputProps}
label={identifierLabel}
{...restIdentifierOutlinedInputWithLabelProps}
/>
}
inputTestBatch={buildIdentifierInputTestBatch(
identifierLabel,
() => {
setMessage(identifierId);
},
{
onFinishBatch:
buildFinishInputTestBatchFunction(identifierId),
},
(message) => {
setMessage(identifierId, { children: message });
},
)}
onBlurAppend={(...args) => {
onIdentifierBlurAppend?.call(null, ...args);
}}
onFirstRender={buildInputFirstRenderFunction(identifierId)}
onUnmount={buildInputUnmountFunction(identifierId)}
ref={inputIdentifierRef}
required
/>
),
},
'gate-input-cell-credential-passphrase': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
formControlProps={{
...restPassphraseFormControlProps,
sx: { ...INPUT_ROOT_SX, ...passphraseSx },
}}
id={passphraseId}
inputProps={passphraseInputProps}
label={passphraseLabel}
type={INPUT_TYPES.password}
{...restPassphraseOutlinedInputWithLabelProps}
/>
}
inputTestBatch={buildPeacefulStringTestBatch(
passphraseLabel,
() => {
setMessage(passphraseId);
},
{
onFinishBatch:
buildFinishInputTestBatchFunction(passphraseId),
},
(message) => {
setMessage(passphraseId, {
children: message,
});
},
)}
onFirstRender={buildInputFirstRenderFunction(passphraseId)}
onUnmount={buildInputUnmountFunction(passphraseId)}
ref={inputPassphraseRef}
required
/>
),
},
...submitAreaGridLayout,
}}
spacing={gridSpacing}
{...restGridProps}
/>
</Box>
);
},
);
GateForm.displayName = 'GateForm';
export { INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE };
export default GateForm;