//Third party bits
import React from 'react';
import PropTypes from 'prop-types';
import { useFormContext } from 'react-hook-form';
// Third party components
import { Card, CardContent, Container, Stack, Skeleton } from '@mui/material';
import Grid from '@mui/material/Unstable_Grid2';
// Internal bits
import Button from '../Button';
import DynamicField from '../DynamicField';
import SubHeader from '../SubHeader';
import LoadingSpinner from '../LoadingSpinner';
import { createRowFields, functionOrDefault } from '../../helpers';
import { FIELD_TYPES } from '../../constants';
import { SectionTop } from '../Section';
/** @module FormSections */
/**
* @typedef {object} FormSectionProps
* @property {string} [title] - the title to display in the section
* @property {string} [description] - the description to display in the section
* @property {renderSectionDescription} [renderFormDescription] - a function to render the form description (bypassed if renderFormInformation is provided)
* @property {renderSectionTitle} [renderFormTitle] - a function to render the form title (bypassed if renderFormInformation is provided)
* @property {renderSectionTop} [renderFormInformation] - a function to render the form information (title and description)
* @property {renderSectionTitle} [renderSectionTitle] - a function to render the section title
* @property {renderSectionDescription} [renderSectionDescription] - a function to render the section description
* @property {renderSectionTop} [renderSectionTop] - a function to render the section top
* @property {function} [renderLoading] - a function to render the loading indicator
* @property {number} [columnCount] - the number of columns to render
* @property {object} [fieldOptions] - the options to pass to the fields
* @property {boolean} [hideEmptySections] - whether or not to hide the section if it is empty
* @property {object} [children] - the children to render
*/
/**
* FormSections will loop through the sections and render them and their fields inside an html form
* @function FormSections
* @param {FormSectionProps} props - props object
* @returns {React.ReactElement} - the rendered form sections
*/
const FormSections = ({
children, formTitle, formDescription, renderFormDescription, renderFormTitle, columnCount = 1, fieldOptions, hideEmptySections = true,
renderLoading, renderFormInformation, renderSectionTitle, renderSectionDescription, renderSectionTop, ...props
}) => {
const { sections, formProcessing, useFormObject } = useFormContext();
const { control } = useFormObject;
const loadingIndicator = functionOrDefault(renderLoading, () => (
<div>
<h1>Processing</h1>
<Skeleton variant="rectangular" height={300} />
</div>
));
if (formProcessing) {
return loadingIndicator();
}
if (!sections) {
return (
<div>
<h1>Error</h1>
<p>No form sections were provided</p>
</div>
);
}
const theSection = renderColumnSection;
const sectOpts = {
columnCount,
fieldOptions,
hideEmptySections,
renderSectionTitle,
renderSectionDescription,
renderSectionTop
};
const hasTopRender = renderFormInformation && (typeof renderFormInformation === 'function');
const hasTopText = !hasTopRender && (formTitle || formDescription || renderFormDescription || renderFormTitle);
return (
<form data-src-form="genericForm">
{sections.map((section, index) => {
const sx = { position: 'relative' };
if (index) {
sx.marginTop = '16px';
}
return (
<Card key={index} sx={sx}>
{index === 0 && hasTopRender &&
renderFormInformation(formTitle, formDescription)
}
{index === 0 && hasTopText &&
<SectionTop title={formTitle} description={formDescription} renderDescription={renderFormDescription} renderTitle={renderFormTitle} />
}
{theSection(section, control, index, sectOpts)}
</Card>
);
})}
{children}
</form>
);
};
FormSections.propTypes = {
children: PropTypes.node,
formTitle: PropTypes.string,
formDescription: PropTypes.string,
renderFormDescription: PropTypes.func,
renderFormTitle: PropTypes.func,
renderFormInformation: PropTypes.func,
renderSectionTitle: PropTypes.func,
renderSectionDescription: PropTypes.func,
renderSectionTop: PropTypes.func,
fieldOptions: PropTypes.object,
hideEmptySections: PropTypes.bool,
renderLoading: PropTypes.func,
columnCount: PropTypes.number,
};
/** @module GenericConfigForm */
/**
* A Generic Form with a header and buttons to submit and cancel
* Must be wrapped in a FormProvider
* @function GenericConfigForm
* @param {object} props
* @param {string} [props.headerTitle] - the title to display in the header
* @param {string} [props.resetColor] - the color to use for the edit button
* @param {string} [props.cancelColor] - the color to use for the cancel button
* @param {string} [props.cancelUrl] - the url to redirect to when the cancel button is clicked
* @param {boolean} [props.isEdit] - whether or not the form is in edit mode
* @param {string} [props.submitColor] - the color to use for the submit button
* @param {FormSectionProps} [props.sectionProps] - the props to pass to the section (See FormSections Component)
* @param {string} [props.cancelLabel] - the label to use for the cancel button
* @param {string} [props.resetLabel] - the label to use for the reset button
* @param {string} [props.submitLabel] - the label to use for the submit button
* @param {function} [props.onSubmit] - the function to call when the form is submitted
* @param {boolean} [props.modifying] - whether or not the form is currently being modified
* @param {object} [props.children] - the children to render
* @param {function} [props.renderLoading] - the function to render while the form is loading
* @returns {React.ReactElement} - the rendered form
*/
const GenericConfigForm = ({
headerTitle, resetColor, cancelColor, cancelUrl, isEdit, submitColor, sectionProps, onSubmit, modifying,
cancelLabel, resetLabel, submitLabel, children, renderLoading
}) => {
// Get the form context
// useFormObject is all the properties from react-hook-form useForm object
// formProcessing is a boolean that is true when the form is processing
// forceReset is a function that will reset the form to the initial values
const { formProcessing, forceReset, useFormObject } = useFormContext();
const { handleSubmit } = useFormObject;
const submitForm = functionOrDefault(onSubmit, (data) => {
console.warn('no onSubmit provided. Data to submit: ', data);
});
// onSubmit is not called if the form is invalid
// so we need to manually check for this
const preSubmit = (evt) => {
// const themValues = getValues();
handleSubmit(submitForm)(evt);
};
const loadingIndicator = functionOrDefault(renderLoading, () => <LoadingSpinner isActive={true} />);
return (
<>
<SubHeader data-src-form-subheader="genericForm"
title={headerTitle}
rightRender={
() =>
<Stack spacing={2} direction="row" justifyContent="flex-end">
<Button data-src-form-button="cancel" color={cancelColor} href={cancelUrl} label={cancelLabel || 'Cancel'} />
{isEdit &&
<Button data-src-form-button="reset" color={resetColor} onClick={forceReset} label={resetLabel || 'Reset'} />
}
<Button data-src-form-button="submit" color={submitColor} onClick={preSubmit} label={submitLabel || 'Save'} />
</Stack>
}
/>
{(formProcessing || modifying) && loadingIndicator()}
<Container sx={{ position: 'relative', marginTop: '16px' }} maxWidth={false}>
<FormSections {...sectionProps}>
{children}
</FormSections>
</Container>
</>
);
};
GenericConfigForm.propTypes = {
headerTitle: PropTypes.string,
resetColor: PropTypes.string,
cancelColor: PropTypes.string,
cancelUrl: PropTypes.string,
cancelLabel: PropTypes.string,
resetLabel: PropTypes.string,
isEdit: PropTypes.bool,
submitLabel: PropTypes.string,
submitColor: PropTypes.string,
// Runtime error if this is not defined ABOVE
sectionProps: PropTypes.shape(FormSections.propTypes),
children: PropTypes.node,
onSubmit: PropTypes.func,
modifying: PropTypes.bool,
renderLoading: PropTypes.func,
};
/** @module SectionRow */
/**
* Callback for rendering a section description
* @callback renderSectionDescription
* @param {string} description - the description to render
* @param {number} index - the index of the section
* @returns {React.ReactElement} - the rendered description
*/
/**
* Callback for rendering a section title
* @callback renderSectionTitle
* @param {string} title - the description to render
* @param {number} index - the index of the section
* @returns {React.ReactElement} - the rendered description
*/
/**
* Callback for rendering a section title
* @callback renderSectionTop
* @param {object} section - the section properties (title, description, etc)
* @param {number} index - the index of the section
* @returns {React.ReactElement} - the rendered description
*/
/**
* Renderer for a section with
* @callback renderColumnSection
* @param {object} section - the section to render
* @param {array} section.fields - the fields to render in the section
* @param {string} [section.title] - the title of the section
* @param {string} [section.description] - the description of the section
* @param {object} control - the control object from react-hook-form
* @param {number} index - the index of the section
* @param {object} [options] - options object
* @param {number} [options.columnCount] - the number of columns to render
* @param {renderSectionDescription} [options.renderSectionDescription] - a function to render the section description
* @param {renderSectionTitle} [options.renderSectionTitle] - a function to render the section title
* @param {renderSectionTop} [options.renderSectionTop] - a function to render the section top
* @param {boolean} [options.hideEmptySections] - a flag to hide sections that are empty
* @returns {React.ReactElement} - the rendered column section
*/
const renderColumnSection = (section, control, index, options) => {
// create two columns of fields
if (section.visible === false && options?.hideEmptySections) {
return null;
}
const rows = createRowFields(section.fields, options?.columnCount);
const hasTopRender = options?.renderSectionTop && (typeof options?.renderSectionTop === 'function');
const hasTop = !hasTopRender && (section.name || section.description);
return (
<React.Fragment key={index}>
{hasTopRender && options.renderSectionTop(section, index)}
{hasTop &&
<SectionTop
title={section.name}
description={section.description}
renderDescription={options?.renderSectionDescription}
renderTitle={options?.renderSectionTitle}
/>
}
<CardContent>
{rows.map((rowItem, rIndex) => {
return (
<SectionRow row={rowItem} control={control} options={options} key={`${index}-row-${rIndex}`} />
);
})}
</CardContent>
</React.Fragment>
);
};
/**
* Component to render a row of fields inside a section
* @function SectionRow
* @param {object} props - props object
* @param {object} props.row - the row object
* @param {array} props.row.fields - the fields to render
* @param {boolean} [props.row.solitary] - whether or not the row is a solitary field
* @param {number} [props.row.size] - the size of the solitary field
* @param {number} [props.row.maxColumns] - the max number of columns to render
* @param {object} props.control - the control object from useForm
* @param {object} [props.options] - the options object
* @param {object} [props.options.fieldOptions] - the options to pass to the fields
* @returns {React.ReactElement} - the rendered row using Grid
* @example
* <CardContent>
{rows.map((rowItem, rIndex) => {
return (
<SectionRow row={rowItem} control={control} options={options} key={`${index}-row-${rIndex}`} />
);
})}
</CardContent>
*/
const SectionRow = ({ row, control, options }) => {
const { fields, solitary, size, maxColumns } = row;
const colsAllowed = maxColumns || 1;
let colSize = 12 / fields.length;
if (solitary && !isNaN(size)) {
colSize = parseInt(size);
}
const spacing = colsAllowed === 1 ? 2 : { xs: 1, sm: 2, md: 4 };
return (
<Grid container spacing={spacing}>
{fields.map((field) => {
const { render } = field;
const isCluster = render?.type === FIELD_TYPES.CLUSTER ? true : false;
return (
<Grid container={isCluster} xs={colSize} key={`grid-item-${field?.render?.name}`}>
<DynamicField
field={field}
control={control}
options={options?.fieldOptions || {}}
/>
</Grid>
);
})}
</Grid>
);
};
SectionRow.propTypes = {
row: PropTypes.shape({
fields: PropTypes.array.isRequired,
solitary: PropTypes.bool,
size: PropTypes.number,
maxColumns: PropTypes.number,
}),
control: PropTypes.object,
options: PropTypes.object,
};
export {
FormSections,
SectionRow,
GenericConfigForm
};
Source