# Frontend Factories Module ## Overview The Factories module provides **code generation patterns** that eliminate massive boilerplate across the frontend application. These factories generate complete components, pages, Redux slices, and table configurations from declarative configurations. **Location:** Multiple directories (factory pattern is distributed) **Total Lines:** ~1,291 LOC across 6 factory files --- ## Architecture ``` frontend/src/ ├── factories/ # Page factories │ ├── createListPage.tsx (193 LOC) # List page generator │ ├── createFormPage.tsx (331 LOC) # Form page generator │ └── index.ts (6 LOC) # Exports ├── components/ │ ├── Factory/ │ │ └── createTableComponent.tsx (118 LOC) # Table component generator │ └── DataGrid/ │ └── configBuilderFactory.tsx (301 LOC) # Column config generator └── stores/ └── createEntitySlice.ts (342 LOC) # Redux slice generator ``` --- ## Factory Relationship Diagram ``` ┌────────────────────────────────────┐ │ createListPage │ │ (generates list pages) │ └────────────────┬───────────────────┘ │ uses ▼ ┌────────────────────────────────────┐ │ createTableComponent │ │ (generates table wrappers) │ └────────────────┬───────────────────┘ │ wraps ▼ ┌────────────────────────────────────┐ │ GenericTable │ │ (reusable data grid) │ └────────────────┬───────────────────┘ │ uses ┌────────────────┴───────────────────┐ ▼ ▼ ┌───────────────────────────────┐ ┌────────────────────────────────┐ │ createColumnLoader │ │ createEntitySlice │ │ (column config generator) │ │ (Redux slice generator) │ └───────────────────────────────┘ └────────────────────────────────┘ ``` --- ## Factory Files ### 1. createListPage (`factories/createListPage.tsx`) **Purpose:** Generate complete list pages with filtering, CSV import/export, and permissions. **Lines:** 193 LOC #### Configuration Interface ```typescript interface ListPageConfig { entityName: string; // URL path segment (e.g., 'roles') entityTitle: string; // Page title (e.g., 'Roles') TableComponent: React.ComponentType<{ filterItems: FilterItem[]; setFilterItems: (items: FilterItem[]) => void; filters: Filter[]; showGrid?: boolean; }>; filters: Filter[]; // Available filter fields readPermission: string; // Permission to view (e.g., 'READ_ROLES') createPermission: string; // Permission to create (e.g., 'CREATE_ROLES') uploadCsvAction: AsyncThunk; // Redux thunk for CSV upload setRefetchAction: (value: boolean) => { type: string; payload: boolean }; // Action to trigger data refresh newItemLabel?: string; // Custom "New Item" button text cardBoxId?: string; // Optional card container ID } ``` #### Generated Features | Feature | Description | |---------|-------------| | Page Title | HTML `` via Next.js Head | | Permission Guard | `LayoutAuthenticated` wrapper with `readPermission` | | New Item Button | Links to `-new` page (if `createPermission`) | | Filter Button | Adds dynamic filter rows | | Download CSV | Exports entity data as CSV | | Upload CSV | Modal with drag-drop file picker | | Table Display | Renders configured `TableComponent` | #### Usage Example ```typescript // pages/roles/roles-list.tsx import { createListPage } from '../../factories/createListPage'; import TableRoles from '../../components/Roles/TableRoles'; import { uploadCsv, setRefetch } from '../../stores/roles/rolesSlice'; const filters = [ { label: 'Name', title: 'name' }, { label: 'Permissions', title: 'permissions' }, ]; export default createListPage({ entityName: 'roles', entityTitle: 'Roles', TableComponent: TableRoles, filters, readPermission: 'READ_ROLES', createPermission: 'CREATE_ROLES', uploadCsvAction: uploadCsv, setRefetchAction: setRefetch, }); ``` **Result:** 24 lines of code generates a complete list page (~200 lines equivalent). --- ### 2. createFormPage (`factories/createFormPage.tsx`) **Purpose:** Generate form pages for creating and editing entities with Formik integration. **Lines:** 331 LOC #### Field Types ```typescript type FormFieldType = | 'text' // Text input | 'email' // Email input with validation | 'number' // Numeric input | 'textarea' // Multi-line text | 'select' // Single-select relation | 'selectMany' // Multi-select relation | 'enumSelect' // Select from predefined options | 'switch' // Boolean toggle | 'image' // Image picker | 'date' // Date picker | 'datetime' // DateTime picker | 'password' // Password input | 'custom'; // Custom component ``` #### Field Configuration ```typescript interface FormFieldConfig { name: string; // Form field name label: string; // Display label type: FormFieldType; // Field type placeholder?: string; // Input placeholder itemRef?: string; // Entity reference for select fields showField?: string; // Display field for relations options?: Array<{ // Options for enumSelect value: string; label: string; }>; path?: string; // Upload path for images schema?: { // Image constraints size?: number; formats?: string[]; }; component?: React.ComponentType; // Custom component props?: Record<string, unknown>; // Custom component props } ``` #### Page Configuration ```typescript interface FormPageConfig<T> { entityName: string; // Entity URL segment entityTitle: string; // Page title singularTitle: string; // Singular form (e.g., 'Role') mode: 'create' | 'edit'; // Form mode sliceSelector: Selector; // Redux state selector fetchAction?: AsyncThunk; // Fetch for edit mode createAction?: AsyncThunk; // Create thunk updateAction?: AsyncThunk; // Update thunk permission: string; // Required permission initialValues: T; // Default form values fields: FormFieldConfig[]; // Field definitions validate?: Validator; // Formik validation } ``` #### Usage Example ```typescript // pages/users/users-new.tsx import { createFormPage } from '../../factories/createFormPage'; import { create } from '../../stores/users/usersSlice'; export default createFormPage({ entityName: 'users', entityTitle: 'Users', singularTitle: 'User', mode: 'create', sliceSelector: (state) => state.users, createAction: create, permission: 'CREATE_USERS', initialValues: { firstName: '', lastName: '', email: '', role: null, }, fields: [ { name: 'firstName', label: 'First Name', type: 'text' }, { name: 'lastName', label: 'Last Name', type: 'text' }, { name: 'email', label: 'Email', type: 'email' }, { name: 'role', label: 'Role', type: 'select', itemRef: 'roles', showField: 'name' }, ], }); ``` #### Field Rendering Logic The `renderField` helper function handles all field types: ```typescript function renderField(field: FormFieldConfig, formValues: T): React.ReactNode { switch (field.type) { case 'text': case 'email': case 'password': return <Field name={name} type={type} placeholder={placeholder} />; case 'select': return <Field name={name} component={SelectField} options={...} />; case 'switch': return <Field name={name} component={SwitchField} />; case 'image': return <Field name={name} component={FormImagePicker} path={path} />; case 'custom': return <Field name={name} component={CustomComponent} {...props} />; // ... other types } } ``` --- ### 3. createTableComponent (`components/Factory/createTableComponent.tsx`) **Purpose:** Generate table wrapper components that connect GenericTable with entity-specific configuration. **Lines:** 118 LOC #### Entity Slice State Interface ```typescript interface EntitySliceState<T> { [key: string]: T[] | boolean | number | NotificationState | unknown[]; loading: boolean; count: number; refetch: boolean; notify: NotificationState; } ``` #### Configuration Interface ```typescript interface TableComponentConfig<T extends BaseEntity> { entityName: string; // Entity identifier sliceSelector: (state: RootState) => EntitySliceState<T>; // Redux slice selector fetchAction: AsyncThunk< T | { rows: T[]; count: number }, { id?: string; query?: string }, object >; updateAction: AsyncThunk<T, { id: string; data: Partial<T> }, object>; deleteAction: AsyncThunk<void, string, object>; deleteByIdsAction: AsyncThunk<void, string[], object>; setRefetchAction: (refetch: boolean) => { type: string; payload: boolean }; loadColumnsFunction: ( onDelete: (id: string) => void, entityName: string, user: unknown, ) => Promise<GridColDef[]>; } ``` #### Generated Props Interface ```typescript interface TableComponentProps { filterItems: FilterItem[]; setFilterItems: (items: FilterItem[]) => void; filters: Filter[]; showGrid?: boolean; } ``` #### Usage Example ```typescript // components/Roles/TableRoles.tsx import { createTableComponent } from '../Factory/createTableComponent'; import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/roles/rolesSlice'; import { loadColumns } from './configureRolesCols'; import type { Role } from '../../types/entities'; const TableRoles = createTableComponent<Role>({ entityName: 'roles', sliceSelector: (state) => state.roles, fetchAction: fetch, updateAction: update, deleteAction: deleteItem, deleteByIdsAction: deleteItemsByIds, setRefetchAction: setRefetch, loadColumnsFunction: loadColumns, }); export default TableRoles; ``` **Result:** 13 lines replaces ~100 lines of boilerplate. --- ### 4. configBuilderFactory (`components/DataGrid/configBuilderFactory.tsx`) **Purpose:** Generate MUI X DataGrid column configurations from declarative metadata. **Lines:** 301 LOC #### Default Column Properties ```typescript const DEFAULT_COLUMN_PROPS = { flex: 1, minWidth: 120, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', }; ``` #### Column Metadata Interface ```typescript interface ColumnMetadata { field: string; // Data field name headerName: string; // Column header type?: // Column type | 'text' | 'boolean' | 'date' | 'datetime' | 'number' | 'relation' | 'relationMany' | 'singleSelectRelation' | 'image' | 'actions'; editable?: boolean; // Allow inline editing sortable?: boolean; // Allow sorting width?: number; // Fixed width flex?: number; // Flex grow minWidth?: number; // Minimum width entityRef?: string; // Related entity for relations displayField?: string; // Field to display for relations renderCell?: (params: GridRenderCellParams) => React.ReactElement; // Custom cell renderer valueFormatter?: (value: unknown) => string; // Value transformation } ``` #### Column Builder Configuration ```typescript interface ColumnBuilderConfig { entityName: string; // Entity identifier entityPath?: string; // URL path (defaults to entityName) columns: ColumnMetadata[]; // Column definitions updatePermission?: string; // Override update permission check } ``` #### Key Functions | Function | Purpose | |----------|---------| | `buildColumns()` | Async function that builds complete column array | | `createColumnLoader()` | Factory that returns column loader function | | `fetchRelationOptions()` | Fetches options for singleSelectRelation columns | | `buildColumn()` | Builds individual column definition | | `buildActionsColumn()` | Builds actions column with edit/view/delete | | `getFormatter()` | Returns appropriate value formatter | #### Column Type Handling ```typescript // Type-specific column configuration switch (col.type) { case 'boolean': baseColumn.type = 'boolean'; baseColumn.valueFormatter = ({ value }) => dataFormatter.booleanFormatter(value); break; case 'datetime': baseColumn.type = 'dateTime'; baseColumn.valueGetter = (_value, row) => new Date(row[col.field]); break; case 'relation': case 'relationMany': baseColumn.type = 'singleSelect'; baseColumn.renderEditCell = (params) => ( <DataGridMultiSelect {...params} entityName={col.entityRef} /> ); break; case 'image': baseColumn.renderCell = (params) => ( <span style={{ backgroundImage: `url(${params.value[0]?.publicUrl})` }} /> ); break; } ``` #### Usage Example ```typescript // components/Roles/configureRolesCols.tsx import { createColumnLoader, ColumnMetadata } from '../DataGrid/configBuilderFactory'; const ROLES_COLUMNS: ColumnMetadata[] = [ { field: 'name', headerName: 'Name', type: 'text', editable: true }, { field: 'permissions', headerName: 'Permissions', type: 'relationMany', entityRef: 'permissions' }, { field: 'actions', headerName: '', type: 'actions' }, ]; export const loadColumns = createColumnLoader({ entityName: 'roles', columns: ROLES_COLUMNS, }); ``` **Result:** 12 lines replaces ~150 lines of column configuration. --- ### 5. createEntitySlice (`stores/createEntitySlice.ts`) **Purpose:** Generate complete Redux Toolkit slices with CRUD async thunks. **Lines:** 342 LOC #### Imported Types The factory imports types from dedicated type files: ```typescript import type { EntitySliceConfig, NotificationState, EntitySliceState, } from '../types/redux'; import type { PaginatedResponse, FetchParams, ApiError } from '../types/api'; import type { BaseEntity } from '../types/entities'; ``` #### Configuration Interface ```typescript interface EntitySliceConfig { name: string; // Slice name (e.g., 'roles') endpoint: string; // API endpoint (e.g., 'roles') singularName?: string; // Singular form for notifications (e.g., 'Role') } ``` #### Generated Actions | Action | Type | Description | |--------|------|-------------| | `fetch` | AsyncThunk | Fetch single item by ID or paginated list | | `create` | AsyncThunk | Create new entity | | `update` | AsyncThunk | Update existing entity | | `deleteItem` | AsyncThunk | Delete single item | | `deleteItemsByIds` | AsyncThunk | Bulk delete items | | `uploadCsv` | AsyncThunk | Import entities from CSV | | `setRefetch` | Action | Trigger data refresh | #### Generated State ```typescript interface InternalSliceState<T> { [entityName]: T[]; // Entity array loading: boolean; // Loading state count: number; // Total count for pagination refetch: boolean; // Refetch trigger flag rolesWidgets: unknown[]; // Legacy widgets support notify: NotificationState; // Notification state } ``` #### Notification Handling Each async action automatically handles notifications: ```typescript // Fulfilled state builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, `${displayName} has been created`); }); // Rejected state builder.addCase(create.rejected, (state, action) => { state.loading = false; rejectNotify(state, action); }); ``` #### Usage Example ```typescript // stores/roles/rolesSlice.ts import { createEntitySlice } from '../createEntitySlice'; import type { Role } from '../../types/entities'; const { slice, actions, reducer: baseReducer } = createEntitySlice<Role>({ name: 'roles', endpoint: 'roles', singularName: 'Role', }); // Export standard CRUD actions export const { fetch, create, update, deleteItem, deleteItemsByIds, uploadCsv, setRefetch } = actions; export const rolesSlice = slice; export default baseReducer; ``` **Result:** 10 lines generates complete Redux slice (~300 lines equivalent). #### Exported Type The factory exports a utility type for accessing actions: ```typescript export type EntityActions<T extends BaseEntity> = ReturnType< typeof createEntitySlice<T> >['actions']; ``` #### Helper Functions The factory includes internal helper functions: | Function | Purpose | |----------|---------| | `isPaginatedResponse<T>()` | Type guard to check if response has `rows` and `count` | | `isAxiosError()` | Type guard for Axios errors | | `getSingularName()` | Get singular form for notifications | | `capitalize()` | Capitalize first letter of string | #### Custom Thunks Extension Slices can add custom thunks alongside generated ones when an existing legacy entity flow still requires Redux-side server calls. New server-state flows should use TanStack Query instead of adding new entity CRUD thunks. --- ## GenericTable Component The `GenericTable` component (`components/Generic/GenericTable.tsx`, 429 LOC) is the foundation that table factories wrap. ### Features | Feature | Implementation | |---------|----------------| | Server-side pagination | `paginationMode='server'` | | Server-side sorting | `sortingMode='server'` | | Row selection | Checkbox selection with bulk actions | | Inline editing | Row edit mode with validation | | Dynamic filters | Formik-based filter panel | | Notifications | Toast notifications via react-toastify | | Permissions | Column editability based on user permissions | ### Props Interface ```typescript interface GenericTableProps<T extends BaseEntity> { entityName: string; sliceSelector: StateSelector<T>; fetchAction: AsyncThunk; updateAction: AsyncThunk; deleteAction: AsyncThunk; deleteByIdsAction?: AsyncThunk; setRefetchAction: ActionCreator; loadColumnsFunction: ColumnLoader; filters: Filter[]; filterItems: FilterItem[]; setFilterItems: Setter; extraQuery?: string; } ``` --- ## Entity Implementation Pattern Complete pattern for adding a new entity using all factories: ### Step 1: Create Entity Slice ```typescript // stores/widgets/widgetsSlice.ts import { createEntitySlice } from '../createEntitySlice'; import type { Widget } from '../../types/entities'; const { slice, actions, reducer } = createEntitySlice<Widget>({ name: 'widgets', endpoint: 'widgets', singularName: 'Widget', }); export const { fetch, create, update, deleteItem, deleteItemsByIds, uploadCsv, setRefetch } = actions; export default reducer; ``` ### Step 2: Create Column Configuration ```typescript // components/Widgets/configureWidgetsCols.tsx import { createColumnLoader, ColumnMetadata } from '../DataGrid/configBuilderFactory'; const WIDGETS_COLUMNS: ColumnMetadata[] = [ { field: 'name', headerName: 'Name', type: 'text', editable: true }, { field: 'status', headerName: 'Status', type: 'text' }, { field: 'createdAt', headerName: 'Created', type: 'datetime' }, { field: 'actions', headerName: '', type: 'actions' }, ]; export const loadColumns = createColumnLoader({ entityName: 'widgets', columns: WIDGETS_COLUMNS, }); ``` ### Step 3: Create Table Component ```typescript // components/Widgets/TableWidgets.tsx import { createTableComponent } from '../Factory/createTableComponent'; import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/widgets/widgetsSlice'; import { loadColumns } from './configureWidgetsCols'; import type { Widget } from '../../types/entities'; const TableWidgets = createTableComponent<Widget>({ entityName: 'widgets', sliceSelector: (state) => state.widgets, fetchAction: fetch, updateAction: update, deleteAction: deleteItem, deleteByIdsAction: deleteItemsByIds, setRefetchAction: setRefetch, loadColumnsFunction: loadColumns, }); export default TableWidgets; ``` ### Step 4: Create List Page ```typescript // pages/widgets/widgets-list.tsx import { createListPage } from '../../factories/createListPage'; import TableWidgets from '../../components/Widgets/TableWidgets'; import { uploadCsv, setRefetch } from '../../stores/widgets/widgetsSlice'; const filters = [ { label: 'Name', title: 'name' }, { label: 'Status', title: 'status' }, ]; export default createListPage({ entityName: 'widgets', entityTitle: 'Widgets', TableComponent: TableWidgets, filters, readPermission: 'READ_WIDGETS', createPermission: 'CREATE_WIDGETS', uploadCsvAction: uploadCsv, setRefetchAction: setRefetch, }); ``` ### Step 5: Create Form Pages ```typescript // pages/widgets/widgets-new.tsx import { createFormPage } from '../../factories/createFormPage'; import { create } from '../../stores/widgets/widgetsSlice'; export default createFormPage({ entityName: 'widgets', entityTitle: 'Widgets', singularTitle: 'Widget', mode: 'create', sliceSelector: (state) => state.widgets, createAction: create, permission: 'CREATE_WIDGETS', initialValues: { name: '', status: 'draft' }, fields: [ { name: 'name', label: 'Name', type: 'text' }, { name: 'status', label: 'Status', type: 'enumSelect', options: [ { value: 'draft', label: 'Draft' }, { value: 'active', label: 'Active' }, ]}, ], }); ``` **Total:** ~80 lines for complete CRUD implementation (~1,500 lines equivalent without factories). --- ## Boilerplate Reduction | Component | Without Factory | With Factory | Reduction | |-----------|-----------------|--------------|-----------| | Redux Slice | ~300 LOC | ~10 LOC | **97%** | | List Page | ~200 LOC | ~24 LOC | **88%** | | Form Page | ~250 LOC | ~30 LOC | **88%** | | Table Component | ~100 LOC | ~13 LOC | **87%** | | Column Config | ~150 LOC | ~12 LOC | **92%** | | **Total per Entity** | **~1,000 LOC** | **~89 LOC** | **91%** | With 13 entities in the application, factories eliminate approximately **11,843 lines** of boilerplate code. --- ## Type Safety All factories are fully typed with TypeScript generics: ```typescript // createEntitySlice with type parameter const { actions } = createEntitySlice<Role>({ ... }); // actions.fetch: AsyncThunk<Role | PaginatedResponse<Role>, FetchParams, ...> // createTableComponent with type parameter const TableRoles = createTableComponent<Role>({ ... }); // TableRoles: React.FC<TableComponentProps> // createFormPage with type parameter const RolesNew = createFormPage<RoleFormValues>({ ... }); // Type-safe form values throughout ``` --- ## Best Practices ### 1. Use Consistent Naming ```typescript // Entity name should match across all layers const entityName = 'widgets'; // Used in: // - Slice: stores/widgets/widgetsSlice.ts // - Table: components/Widgets/TableWidgets.tsx // - Pages: pages/widgets/widgets-list.tsx ``` ### 2. Permission Naming Convention ```typescript // Permissions follow pattern: ACTION_ENTITY readPermission: 'READ_WIDGETS', createPermission: 'CREATE_WIDGETS', updatePermission: 'UPDATE_WIDGETS', // Used by configBuilderFactory deletePermission: 'DELETE_WIDGETS', ``` ### 3. Type Definitions ```typescript // Always define entity type in types/entities.ts export interface Widget extends BaseEntity { name: string; status: 'draft' | 'active'; } // Use type parameter in all factories createEntitySlice<Widget>({ ... }); createTableComponent<Widget>({ ... }); ``` ### 4. Custom Extensions ```typescript // Add custom thunks after factory generation const { slice, actions, reducer } = createEntitySlice<Widget>({ ... }); // Custom widget-specific thunk export const archiveWidget = createAsyncThunk( 'widgets/archive', async (id: string) => { ... } ); ``` --- ## Related Documentation - [stores-module.md](./stores-module.md) - Redux state management - [components-module.md](./components-module.md) - Component organization - [pages-module.md](./pages-module.md) - Page structure - [types-module.md](./types-module.md) - TypeScript types - [hooks-module.md](./hooks-module.md) - Custom hooks --- ## File Inventory | File | Location | LOC | Purpose | |------|----------|-----|---------| | `createListPage.tsx` | `factories/` | 193 | List page generator | | `createFormPage.tsx` | `factories/` | 331 | Form page generator | | `index.ts` | `factories/` | 6 | Exports | | `createTableComponent.tsx` | `components/Factory/` | 118 | Table wrapper generator | | `configBuilderFactory.tsx` | `components/DataGrid/` | 301 | Column config generator | | `createEntitySlice.ts` | `stores/` | 342 | Redux slice generator | | **Total** | | **1,291** | |