# 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([]); const [isRefetching, setIsRefetching] = useState(false); const { request, isLoading: loading, error, clearCache } = useApiRequest(); 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>(new Set()); const [updatingItems, setUpdatingItems] = useState>(new Set()); const [deletingItems, setDeletingItems] = useState>(new Set()); const { request } = useApiRequest(); const handleCreate = async (itemData: Partial) => { 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) => { 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([]); // 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 (

Custom Page Content

{/* Your custom UI here */}
); }; // 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) => ( {value} ) }, { 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` 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 (
{hookData.loading &&
Loading...
} {hookData.error &&
Error: {hookData.error}
}
); } } ``` --- ## 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` 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! 🚀