848 lines
25 KiB
Markdown
848 lines
25 KiB
Markdown
# 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<unknown, File, object>; // 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 `<title>` 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** | |
|