869 lines
23 KiB
Markdown
869 lines
23 KiB
Markdown
# PageManager Usage Guide
|
|
|
|
A step-by-step guide to creating new pages using the PageManager system.
|
|
|
|
---
|
|
|
|
## Quick Start: Adding a New Page
|
|
|
|
### Step 1: Create Page Definition File
|
|
|
|
Create a new file in `src/core/PageManager/data/pages/` (e.g., `mypage.ts`):
|
|
|
|
```typescript
|
|
import { useCallback } from 'react';
|
|
import { GenericPageData } from '../../pageInterface';
|
|
import { FaIcon } from 'react-icons/fa';
|
|
import { privilegeCheckers } from '../../../../hooks/privilegeCheckers';
|
|
|
|
// 1. Import your custom hooks
|
|
import { useMyData } from '../../../../hooks/useMyData';
|
|
import { useMyOperations } from '../../../../hooks/useMyOperations';
|
|
|
|
// 2. Create Hook Factory
|
|
const createMyPageHook = () => {
|
|
return () => {
|
|
// Call your data hooks
|
|
const { data, loading, error, refetch } = useMyData();
|
|
const { handleCreate, handleUpdate, handleDelete,
|
|
creatingItems, updatingItems, deletingItems } = useMyOperations();
|
|
|
|
// Return unified interface
|
|
return {
|
|
data,
|
|
loading,
|
|
error,
|
|
refetch,
|
|
// Operations
|
|
handleCreate,
|
|
handleUpdate,
|
|
handleDelete,
|
|
// Loading states
|
|
creatingItems,
|
|
updatingItems,
|
|
deletingItems
|
|
};
|
|
};
|
|
};
|
|
|
|
// 3. Define Columns
|
|
const myPageColumns = [
|
|
{
|
|
key: 'name',
|
|
label: 'Name',
|
|
type: 'string',
|
|
width: 250,
|
|
sortable: true,
|
|
filterable: true,
|
|
searchable: true
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
type: 'enum',
|
|
width: 150,
|
|
sortable: true,
|
|
filterable: true,
|
|
filterOptions: ['Active', 'Inactive']
|
|
},
|
|
{
|
|
key: 'created_at',
|
|
label: 'Created',
|
|
type: 'date',
|
|
width: 200,
|
|
sortable: true,
|
|
filterable: true
|
|
}
|
|
];
|
|
|
|
// 4. Export Page Configuration
|
|
export const myPageData: GenericPageData = {
|
|
// Identification
|
|
id: 'my-page',
|
|
path: 'my-page',
|
|
name: 'My Page',
|
|
description: 'Description of my page',
|
|
|
|
// Visual
|
|
icon: FaIcon,
|
|
title: 'My Page Title',
|
|
subtitle: 'Subtitle text',
|
|
|
|
// Header buttons (optional)
|
|
headerButtons: [
|
|
{
|
|
id: 'create-item',
|
|
label: 'Create New',
|
|
icon: FaIcon,
|
|
variant: 'primary',
|
|
onClick: () => {} // Will be handled by PageRenderer
|
|
}
|
|
],
|
|
|
|
// Content
|
|
content: [
|
|
{
|
|
id: 'my-table',
|
|
type: 'table',
|
|
tableConfig: {
|
|
hookFactory: createMyPageHook,
|
|
columns: myPageColumns,
|
|
actionButtons: [
|
|
{
|
|
type: 'view',
|
|
title: 'View details',
|
|
idField: 'id',
|
|
nameField: 'name',
|
|
operationName: 'handleView',
|
|
loadingStateName: 'viewingItems'
|
|
},
|
|
{
|
|
type: 'edit',
|
|
title: 'Edit item',
|
|
idField: 'id',
|
|
nameField: 'name',
|
|
operationName: 'handleUpdate',
|
|
loadingStateName: 'updatingItems'
|
|
},
|
|
{
|
|
type: 'delete',
|
|
title: 'Delete item',
|
|
idField: 'id',
|
|
operationName: 'handleDelete',
|
|
loadingStateName: 'deletingItems'
|
|
}
|
|
],
|
|
searchable: true,
|
|
filterable: true,
|
|
sortable: true,
|
|
resizable: true,
|
|
pagination: true,
|
|
pageSize: 10
|
|
}
|
|
}
|
|
],
|
|
|
|
// Privilege check
|
|
privilegeChecker: privilegeCheckers.viewerRole,
|
|
|
|
// Page behavior
|
|
persistent: false, // false = unmount when navigating away
|
|
preload: false,
|
|
moduleEnabled: true,
|
|
showInSidebar: true,
|
|
order: 10
|
|
};
|
|
```
|
|
|
|
### Step 2: Register the Page
|
|
|
|
Add your page to `src/core/PageManager/data/index.ts`:
|
|
|
|
```typescript
|
|
import { myPageData } from './pages/mypage';
|
|
|
|
export const allPageData: GenericPageData[] = [
|
|
// ... existing pages
|
|
myPageData, // Add your page
|
|
];
|
|
|
|
// Export for direct access
|
|
export { myPageData } from './pages/mypage';
|
|
```
|
|
|
|
### Step 3: Create Your Custom Hooks
|
|
|
|
Create `src/hooks/useMyData.ts`:
|
|
|
|
```typescript
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { useApiRequest } from './useApi';
|
|
|
|
export interface MyDataItem {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export function useMyData() {
|
|
const [data, setData] = useState<MyDataItem[]>([]);
|
|
const [isRefetching, setIsRefetching] = useState(false);
|
|
const { request, isLoading: loading, error, clearCache } = useApiRequest<null, MyDataItem[]>();
|
|
|
|
const fetchData = useCallback(async () => {
|
|
try {
|
|
const result = await request({
|
|
url: '/api/mydata',
|
|
method: 'get'
|
|
});
|
|
setData(result || []);
|
|
} catch (error: any) {
|
|
console.error('Failed to fetch data:', error);
|
|
setData([]);
|
|
}
|
|
}, [request]);
|
|
|
|
const refetch = useCallback(async () => {
|
|
setIsRefetching(true);
|
|
try {
|
|
clearCache('/api/mydata', 'get');
|
|
await fetchData();
|
|
} finally {
|
|
setIsRefetching(false);
|
|
}
|
|
}, [clearCache, fetchData]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
return { data, loading, isRefetching, error, refetch };
|
|
}
|
|
|
|
export function useMyOperations() {
|
|
const [creatingItems, setCreatingItems] = useState<Set<string>>(new Set());
|
|
const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set());
|
|
const [deletingItems, setDeletingItems] = useState<Set<string>>(new Set());
|
|
const { request } = useApiRequest();
|
|
|
|
const handleCreate = async (itemData: Partial<MyDataItem>) => {
|
|
setCreatingItems(prev => new Set(prev).add('new'));
|
|
try {
|
|
await request({
|
|
url: '/api/mydata',
|
|
method: 'post',
|
|
data: itemData
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Create failed:', error);
|
|
return false;
|
|
} finally {
|
|
setCreatingItems(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete('new');
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleUpdate = async (itemId: string, updateData: Partial<MyDataItem>) => {
|
|
setUpdatingItems(prev => new Set(prev).add(itemId));
|
|
try {
|
|
await request({
|
|
url: `/api/mydata/${itemId}`,
|
|
method: 'put',
|
|
data: updateData
|
|
});
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Update failed:', error);
|
|
return { success: false };
|
|
} finally {
|
|
setUpdatingItems(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(itemId);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (itemId: string) => {
|
|
setDeletingItems(prev => new Set(prev).add(itemId));
|
|
try {
|
|
await request({
|
|
url: `/api/mydata/${itemId}`,
|
|
method: 'delete'
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Delete failed:', error);
|
|
return false;
|
|
} finally {
|
|
setDeletingItems(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(itemId);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
return {
|
|
handleCreate,
|
|
handleUpdate,
|
|
handleDelete,
|
|
creatingItems,
|
|
updatingItems,
|
|
deletingItems
|
|
};
|
|
}
|
|
```
|
|
|
|
### Step 4: Navigate to Your Page
|
|
|
|
The page is now available at `/my-page` and will appear in the sidebar if `showInSidebar: true`.
|
|
|
|
---
|
|
|
|
## Advanced Features
|
|
|
|
### Adding Subpages
|
|
|
|
```typescript
|
|
export const parentPageData: GenericPageData = {
|
|
id: 'parent',
|
|
path: 'parent',
|
|
name: 'Parent',
|
|
hasSubpages: true,
|
|
subpagePrivilegeChecker: privilegeCheckers.adminRole,
|
|
showInSidebar: true
|
|
};
|
|
|
|
export const subpageData: GenericPageData = {
|
|
id: 'parent-subpage',
|
|
path: 'parent/subpage',
|
|
name: 'Subpage',
|
|
parentPath: 'parent', // Links to parent
|
|
showInSidebar: false // Shown under parent in sidebar
|
|
};
|
|
```
|
|
|
|
### Custom Upload Handler
|
|
|
|
If your page needs file upload:
|
|
|
|
```typescript
|
|
const createMyPageHook = () => {
|
|
return () => {
|
|
const { data, refetch } = useMyData();
|
|
|
|
// Memoized upload function
|
|
const handleUpload = useCallback(async (file: File) => {
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const headers = addCSRFTokenToHeaders();
|
|
const response = await api.post('/api/mydata/upload', formData, {
|
|
headers: { ...headers }
|
|
});
|
|
|
|
refetch(); // Refresh data
|
|
return { success: true, data: response.data };
|
|
} catch (error: any) {
|
|
throw new Error(error.message);
|
|
}
|
|
}, [refetch]);
|
|
|
|
return {
|
|
data,
|
|
handleUpload, // Add to return object
|
|
// ... other operations
|
|
};
|
|
};
|
|
};
|
|
|
|
// In page config
|
|
headerButtons: [
|
|
{
|
|
id: 'upload-file',
|
|
label: 'Upload File',
|
|
icon: FaUpload,
|
|
variant: 'primary',
|
|
onClick: () => {} // PageRenderer will detect and render UploadComponent
|
|
}
|
|
]
|
|
```
|
|
|
|
### Custom Action Buttons
|
|
|
|
Add custom actions beyond the standard view/edit/delete:
|
|
|
|
```typescript
|
|
actionButtons: [
|
|
{
|
|
type: 'download', // Standard type
|
|
title: 'Download',
|
|
idField: 'id',
|
|
nameField: 'name',
|
|
operationName: 'handleDownload',
|
|
loadingStateName: 'downloadingItems'
|
|
}
|
|
]
|
|
|
|
// In your operations hook
|
|
const handleDownload = async (itemId: string, itemName: string) => {
|
|
setDownloadingItems(prev => new Set(prev).add(itemId));
|
|
try {
|
|
const blob = await request({
|
|
url: `/api/mydata/${itemId}/download`,
|
|
method: 'get',
|
|
additionalConfig: { responseType: 'blob' }
|
|
});
|
|
|
|
// Trigger download
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = itemName;
|
|
link.click();
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Download failed:', error);
|
|
return false;
|
|
} finally {
|
|
setDownloadingItems(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(itemId);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
### Optimistic Updates
|
|
|
|
Implement instant UI feedback:
|
|
|
|
```typescript
|
|
export function useMyData() {
|
|
const [data, setData] = useState<MyDataItem[]>([]);
|
|
|
|
// Optimistic removal
|
|
const removeOptimistically = (itemId: string) => {
|
|
setData(prevData => prevData.filter(item => item.id !== itemId));
|
|
};
|
|
|
|
// Optimistic addition
|
|
const addOptimistically = (newItem: MyDataItem) => {
|
|
setData(prevData => [newItem, ...prevData]);
|
|
};
|
|
|
|
return {
|
|
data,
|
|
removeOptimistically,
|
|
addOptimistically,
|
|
// ... other properties
|
|
};
|
|
}
|
|
|
|
// In hook factory
|
|
return {
|
|
data,
|
|
removeOptimistically,
|
|
addOptimistically,
|
|
// ... other properties
|
|
};
|
|
|
|
// In delete operation
|
|
const handleDelete = async (itemId: string, onOptimisticDelete?: () => void) => {
|
|
// Call optimistic removal immediately
|
|
if (onOptimisticDelete) {
|
|
onOptimisticDelete();
|
|
}
|
|
|
|
try {
|
|
await request({ url: `/api/mydata/${itemId}`, method: 'delete' });
|
|
return true;
|
|
} catch (error) {
|
|
// On failure, refetch to restore data
|
|
return false;
|
|
}
|
|
};
|
|
```
|
|
|
|
### Custom Page Component
|
|
|
|
For complex pages that need custom UI beyond tables:
|
|
|
|
```typescript
|
|
import React from 'react';
|
|
|
|
export const MyCustomPage: React.FC = () => {
|
|
return (
|
|
<div>
|
|
<h1>Custom Page Content</h1>
|
|
{/* Your custom UI here */}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// In page config
|
|
export const myPageData: GenericPageData = {
|
|
// ... other config
|
|
customComponent: MyCustomPage, // PageRenderer will render this instead
|
|
};
|
|
```
|
|
|
|
### Edit Field Configuration
|
|
|
|
Customize edit form fields:
|
|
|
|
```typescript
|
|
actionButtons: [
|
|
{
|
|
type: 'edit',
|
|
title: 'Edit item',
|
|
idField: 'id',
|
|
operationName: 'handleUpdate',
|
|
loadingStateName: 'updatingItems',
|
|
editFields: [
|
|
{
|
|
key: 'name',
|
|
label: 'Name',
|
|
type: 'string',
|
|
editable: true,
|
|
required: true,
|
|
validator: (value: string) => {
|
|
if (value.length < 3) return 'Name must be at least 3 characters';
|
|
return null;
|
|
}
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
type: 'enum',
|
|
editable: true,
|
|
required: true,
|
|
options: ['Active', 'Inactive']
|
|
},
|
|
{
|
|
key: 'description',
|
|
label: 'Description',
|
|
type: 'textarea',
|
|
editable: true,
|
|
minRows: 4,
|
|
maxRows: 8
|
|
},
|
|
{
|
|
key: 'created_at',
|
|
label: 'Created',
|
|
type: 'readonly',
|
|
editable: false,
|
|
formatter: (value) => new Date(value).toLocaleDateString()
|
|
}
|
|
]
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## Column Types & Configuration
|
|
|
|
### Available Column Types
|
|
|
|
```typescript
|
|
type: 'string' | 'number' | 'date' | 'boolean' | 'enum'
|
|
```
|
|
|
|
### Column Properties
|
|
|
|
```typescript
|
|
{
|
|
key: string; // Data field name
|
|
label: string; // Column header label
|
|
type?: string; // Data type (affects formatting & filtering)
|
|
width?: number; // Default width in pixels
|
|
minWidth?: number; // Minimum width when resizing
|
|
maxWidth?: number; // Maximum width when resizing
|
|
sortable?: boolean; // Enable sorting
|
|
filterable?: boolean; // Enable filtering
|
|
searchable?: boolean; // Include in global search
|
|
filterOptions?: string[]; // Options for enum filter dropdown
|
|
formatter?: (value: any, row: any) => React.ReactNode; // Custom display
|
|
cellClassName?: (value: any, row: any) => string; // Custom cell CSS
|
|
}
|
|
```
|
|
|
|
### Custom Formatters
|
|
|
|
```typescript
|
|
{
|
|
key: 'price',
|
|
label: 'Price',
|
|
type: 'number',
|
|
formatter: (value) => `$${value.toFixed(2)}`
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
type: 'string',
|
|
formatter: (value) => (
|
|
<span className={`badge badge-${value.toLowerCase()}`}>
|
|
{value}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
key: 'date',
|
|
label: 'Date',
|
|
type: 'date',
|
|
formatter: (value) => new Date(value).toLocaleDateString('de-DE')
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Action Button Types
|
|
|
|
### Built-in Action Types
|
|
|
|
| Type | Purpose | Required Props | Optional Props |
|
|
|------|---------|----------------|----------------|
|
|
| `view` | Preview/view item | `idField`, `operationName` | `nameField`, `typeField`, `loadingStateName` |
|
|
| `edit` | Edit item | `idField`, `operationName` | `editFields`, `loadingStateName` |
|
|
| `download` | Download item | `idField`, `operationName` | `nameField`, `loadingStateName` |
|
|
| `delete` | Delete item | `idField`, `operationName` | `loadingStateName` |
|
|
|
|
### Action Button Configuration
|
|
|
|
```typescript
|
|
{
|
|
type: 'view' | 'edit' | 'download' | 'delete';
|
|
title?: string; // Tooltip text
|
|
idField?: string; // Row field for ID (default: 'id')
|
|
nameField?: string; // Row field for name (default: 'name')
|
|
typeField?: string; // Row field for type (default: 'type')
|
|
operationName?: string; // hookData operation name
|
|
loadingStateName?: string; // hookData loading state name
|
|
onAction?: (row: any) => void; // Optional callback
|
|
disabled?: (row: any) => boolean | { disabled: boolean; message?: string }; // Conditional disable with tooltip
|
|
editFields?: EditFieldConfig[]; // For edit button
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### ✅ Do
|
|
|
|
1. **Memoize functions in hooks** using `useCallback([dependencies])`
|
|
2. **Use per-item loading states** with `Set<string>` for better UX
|
|
3. **Implement optimistic updates** for delete operations
|
|
4. **Validate hookData operations** in action buttons (throw if missing)
|
|
5. **Keep hook factory simple** - just call hooks and return data
|
|
6. **Use clear naming** - `handleXyz` for operations, `xyzingItems` for loading states
|
|
7. **Add proper TypeScript types** for your data interfaces
|
|
8. **Clear API cache** when refetching: `clearCache(url, method)`
|
|
9. **Use disabled buttons with tooltips** - provide helpful messages explaining why buttons are disabled
|
|
10. **Test disabled states** - ensure buttons are properly disabled and tooltips show correctly
|
|
|
|
### ❌ Don't
|
|
|
|
1. **Don't call hooks conditionally** or in loops
|
|
2. **Don't create fallback hooks** in action buttons (use hookData)
|
|
3. **Don't forget to add operations** to hook factory return statement
|
|
4. **Don't mutate hookData** - it's a shared reference
|
|
5. **Don't forget refetch** after create/update/delete operations
|
|
6. **Don't skip operationName/loadingStateName** in button config
|
|
7. **Don't make hookData optional** in action buttons (require it)
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern: Create New Item
|
|
|
|
```typescript
|
|
// Header button
|
|
headerButtons: [
|
|
{
|
|
id: 'create-new',
|
|
label: 'Create New',
|
|
icon: FaPlus,
|
|
variant: 'primary',
|
|
onClick: (hookData) => {
|
|
// Open create dialog
|
|
// Call hookData.handleCreate()
|
|
// Call hookData.refetch()
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
### Pattern: Bulk Operations
|
|
|
|
```typescript
|
|
// In FormGenerator props
|
|
onDeleteMultiple: (rows: MyDataItem[]) => {
|
|
// Delete multiple selected items
|
|
Promise.all(rows.map(row => hookData.handleDelete(row.id)))
|
|
.then(() => hookData.refetch());
|
|
}
|
|
```
|
|
|
|
### Pattern: Conditional Action Buttons
|
|
|
|
```typescript
|
|
actionButtons: [
|
|
{
|
|
type: 'delete',
|
|
disabled: (row) => row.status === 'Protected',
|
|
title: (row) => row.status === 'Protected'
|
|
? 'Cannot delete protected item'
|
|
: 'Delete item'
|
|
}
|
|
]
|
|
```
|
|
|
|
### Pattern: Disabled Buttons with Tooltips
|
|
|
|
```typescript
|
|
actionButtons: [
|
|
{
|
|
type: 'edit',
|
|
title: 'Edit file',
|
|
operationName: 'handleUpdate',
|
|
loadingStateName: 'updatingItems',
|
|
// Disable with custom tooltip message
|
|
disabled: (file) => {
|
|
if (file.file_name.startsWith('.')) {
|
|
return {
|
|
disabled: true,
|
|
message: 'Cannot edit system files'
|
|
};
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
{
|
|
type: 'download',
|
|
title: 'Download file',
|
|
operationName: 'handleDownload',
|
|
loadingStateName: 'downloadingItems',
|
|
// Disable for large files with size info
|
|
disabled: (file) => {
|
|
if (file.file_size > 100 * 1024 * 1024) { // 100MB
|
|
return {
|
|
disabled: true,
|
|
message: `File too large to download (${Math.round(file.file_size / 1024 / 1024)}MB)`
|
|
};
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
{
|
|
type: 'delete',
|
|
title: 'Delete file',
|
|
operationName: 'handleDelete',
|
|
loadingStateName: 'deletingItems',
|
|
// Simple boolean disable (no custom message)
|
|
disabled: (file) => file.is_protected
|
|
}
|
|
]
|
|
```
|
|
|
|
### Pattern: Custom Loading Indicator
|
|
|
|
```typescript
|
|
// In page content
|
|
{
|
|
type: 'custom',
|
|
customComponent: () => {
|
|
const hookData = useTableData(); // Access hook data
|
|
return (
|
|
<div>
|
|
{hookData.loading && <div>Loading...</div>}
|
|
{hookData.error && <div>Error: {hookData.error}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: "hookData.X is not defined"
|
|
|
|
**Solution**: Add the operation to your hook factory's return statement.
|
|
|
|
### Issue: Duplicate hook calls
|
|
|
|
**Solution**: Remove any fallback hooks in action buttons. Make hookData required.
|
|
|
|
### Issue: Table not updating after operation
|
|
|
|
**Solution**: Call `refetch()` after create/update/delete operations.
|
|
|
|
### Issue: Loading state not working
|
|
|
|
**Solution**:
|
|
1. Ensure loading state is returned from hook factory
|
|
2. Add `loadingStateName` to button config
|
|
3. Use `Set<string>` for per-item tracking
|
|
|
|
### Issue: Edit form not opening
|
|
|
|
**Solution**:
|
|
1. Add `handleFileUpdate` (or your operation) to hook factory
|
|
2. Add `operationName: 'handleFileUpdate'` to button config
|
|
3. Optionally add `editFields` for custom form fields
|
|
|
|
---
|
|
|
|
## Example: Complete Minimal Page
|
|
|
|
```typescript
|
|
// src/core/PageManager/data/pages/simple.ts
|
|
import { GenericPageData } from '../../pageInterface';
|
|
import { FaList } from 'react-icons/fa';
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { useApiRequest } from '../../../../hooks/useApi';
|
|
|
|
const createSimpleHook = () => {
|
|
return () => {
|
|
const [data, setData] = useState([]);
|
|
const { request, isLoading: loading, error } = useApiRequest();
|
|
|
|
const fetchData = useCallback(async () => {
|
|
const result = await request({ url: '/api/items', method: 'get' });
|
|
setData(result || []);
|
|
}, [request]);
|
|
|
|
useEffect(() => { fetchData(); }, [fetchData]);
|
|
|
|
return { data, loading, error, refetch: fetchData };
|
|
};
|
|
};
|
|
|
|
export const simplePageData: GenericPageData = {
|
|
id: 'simple',
|
|
path: 'simple',
|
|
name: 'Simple Page',
|
|
icon: FaList,
|
|
title: 'Simple Page',
|
|
content: [{
|
|
type: 'table',
|
|
tableConfig: {
|
|
hookFactory: createSimpleHook,
|
|
columns: [
|
|
{ key: 'name', label: 'Name', type: 'string', sortable: true }
|
|
],
|
|
actionButtons: []
|
|
}
|
|
}],
|
|
moduleEnabled: true
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Creating a new page requires:
|
|
|
|
1. ✅ Create page definition file with hook factory
|
|
2. ✅ Register page in `data/index.ts`
|
|
3. ✅ Create data hooks (useMyData, useMyOperations)
|
|
4. ✅ Define columns and action buttons
|
|
5. ✅ Navigate to `/your-page-path`
|
|
|
|
The system handles routing, rendering, state management, and action buttons automatically. Focus on your data hooks and page configuration! 🚀
|
|
|