frontend_nyla/docs/USAGE_GUIDE_PAGES.md

23 KiB

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):

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:

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:

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

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:

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:

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:

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:

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:

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

type: 'string' | 'number' | 'date' | 'boolean' | 'enum'

Column Properties

{
    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

{
    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

{
    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

// 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

// 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

actionButtons: [
    {
        type: 'delete',
        disabled: (row) => row.status === 'Protected',
        title: (row) => row.status === 'Protected' 
            ? 'Cannot delete protected item' 
            : 'Delete item'
    }
]

Pattern: Disabled Buttons with Tooltips

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

// 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

// 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! 🚀