39948-vm/frontend/src/factories/createFormPage.tsx

332 lines
8.7 KiB
TypeScript

/**
* Form Page Factory
*/
import { mdiChartTimelineVariant } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import BaseButton from '../components/BaseButton';
import { getPageTitle } from '../config';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { SelectField } from '../components/SelectField';
import { SelectFieldMany } from '../components/SelectFieldMany';
import { SwitchField } from '../components/SwitchField';
import FormImagePicker from '../components/FormImagePicker';
import type { RootState } from '../stores/store';
import type { AsyncThunk } from '@reduxjs/toolkit';
// Field types supported by the factory
export type FormFieldType =
| 'text'
| 'email'
| 'number'
| 'textarea'
| 'select'
| 'selectMany'
| 'enumSelect'
| 'switch'
| 'image'
| 'date'
| 'datetime'
| 'password'
| 'custom';
// Field configuration for dynamic form rendering
export interface FormFieldConfig {
name: string;
label: string;
type: FormFieldType;
placeholder?: string;
itemRef?: string; // For select fields - entity to fetch from
showField?: string; // For select fields - field to display
options?: Array<{ value: string; label: string }>; // For enum selects
path?: string; // For image fields
schema?: { size?: number; formats?: string[] }; // For image fields
component?: React.ComponentType<unknown>; // For custom field types
props?: Record<string, unknown>; // Additional props for custom components
}
interface FormPageConfig<T> {
entityName: string;
entityTitle: string;
singularTitle: string;
mode: 'create' | 'edit';
sliceSelector: (state: RootState) => { [key: string]: T | T[] };
fetchAction?: AsyncThunk<unknown, { id: string }, object>;
createAction?: AsyncThunk<unknown, T, object>;
updateAction?: AsyncThunk<unknown, { id: string; data: T }, object>;
permission: string;
initialValues: T;
fields: FormFieldConfig[];
validate?: (values: T) => Record<string, string>;
}
export function createFormPage<T extends Record<string, unknown>>(
config: FormPageConfig<T>,
) {
const {
entityName,
entityTitle,
singularTitle,
mode,
sliceSelector,
fetchAction,
createAction,
updateAction,
permission,
initialValues,
fields,
validate,
} = config;
const FormPage = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const entityState = useAppSelector(sliceSelector);
const entityData = entityState[entityName];
const { id } = router.query as { id?: string };
const [formValues, setFormValues] = useState<T>(initialValues);
// Fetch data for edit mode
useEffect(() => {
if (mode === 'edit' && id && fetchAction) {
dispatch(fetchAction({ id }));
}
}, [id, dispatch]);
// Sync form values with fetched data
useEffect(() => {
if (
mode === 'edit' &&
entityData &&
typeof entityData === 'object' &&
!Array.isArray(entityData)
) {
const newValues = { ...initialValues };
Object.keys(initialValues).forEach((key) => {
if (key in (entityData as Record<string, unknown>)) {
(newValues as Record<string, unknown>)[key] = (
entityData as Record<string, unknown>
)[key];
}
});
setFormValues(newValues);
}
}, [entityData]);
const handleSubmit = async (data: T) => {
if (mode === 'edit' && updateAction && id) {
await dispatch(updateAction({ id, data }));
} else if (mode === 'create' && createAction) {
await dispatch(createAction(data));
}
await router.push(`/${entityName}/${entityName}-list`);
};
const pageTitle =
mode === 'edit' ? `Edit ${singularTitle}` : `New ${singularTitle}`;
return (
<>
<Head>
<title>{getPageTitle(pageTitle)}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={pageTitle}
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
enableReinitialize
initialValues={formValues}
validate={validate}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
{fields.map((field) => (
<FormField
key={field.name}
label={field.label}
labelFor={field.name}
>
{renderField(field, formValues)}
</FormField>
))}
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() =>
router.push(`/${entityName}/${entityName}-list`)
}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
FormPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={permission}>{page}</LayoutAuthenticated>
);
};
return FormPage;
}
// Helper function to render form fields based on type
function renderField<T>(
field: FormFieldConfig,
formValues: T,
): React.ReactNode {
const {
name,
type,
placeholder,
itemRef,
showField,
options,
path,
schema,
component: CustomComponent,
props,
} = field;
switch (type) {
case 'text':
case 'email':
case 'password':
return (
<Field
name={name}
type={type}
placeholder={placeholder || field.label}
/>
);
case 'number':
return (
<Field
name={name}
type='number'
placeholder={placeholder || field.label}
/>
);
case 'textarea':
return (
<Field
as='textarea'
name={name}
placeholder={placeholder || field.label}
/>
);
case 'select':
return (
<Field
name={name}
id={name}
component={SelectField}
options={(formValues as Record<string, unknown>)[name]}
itemRef={itemRef}
showField={showField}
/>
);
case 'enumSelect':
return (
<Field name={name} id={name} component='select'>
{options?.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</Field>
);
case 'selectMany':
return (
<Field
name={name}
id={name}
component={SelectFieldMany}
options={(formValues as Record<string, unknown>)[name] || []}
itemRef={itemRef}
showField={showField}
/>
);
case 'switch':
return <Field name={name} id={name} component={SwitchField} />;
case 'image':
return (
<Field
label={field.label}
color='info'
path={path}
name={name}
id={name}
schema={schema || { size: undefined, formats: undefined }}
component={FormImagePicker}
/>
);
case 'date':
return (
<Field
name={name}
type='date'
placeholder={placeholder || field.label}
/>
);
case 'datetime':
return (
<Field
name={name}
type='datetime-local'
placeholder={placeholder || field.label}
/>
);
case 'custom':
if (CustomComponent) {
return (
<Field name={name} id={name} component={CustomComponent} {...props} />
);
}
return <Field name={name} placeholder={placeholder || field.label} />;
default:
return <Field name={name} placeholder={placeholder || field.label} />;
}
}
export default createFormPage;