332 lines
8.7 KiB
TypeScript
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;
|