wiki/z-archive/implementation/implementation_ui_table_pagination.md

754 lines
25 KiB
Markdown

# UI Table Pagination Implementation Concept
## Overview
This document defines the architecture and implementation strategy for server-side pagination in the UI table rendering system. **All pagination logic is implemented in the backend** - the frontend only receives paginated data and metadata needed for rendering pagination controls.
## Core Principles
1. **Backend-Driven Pagination**: All data slicing, sorting, and filtering happens on the server
2. **Stateless Requests**: Each UI action (page change, sort, filter) triggers a new API request with complete pagination state
3. **Explicit Structure**: Clear, well-defined data structures for requests and responses
4. **No Fallbacks**: Clean implementation without backward compatibility workarounds
5. **Extensibility**: Designed to support future filtering capabilities
## Architecture
### Request Flow
**Data Loading:**
```
UI Action (initial load, next page, sort, filter)
formGeneric.js constructs PaginationRequest
API call with pagination parameters
Backend applies pagination, sorting, filtering
Response with PaginatedResponse (items + pagination metadata)
formGeneric.js renders table and pagination controls
```
**Data Modification (Add/Update/Delete):**
```
UI Action (add, update, delete)
API call for CUD operation
Backend executes operation
Success response
formGeneric.js triggers data refresh with current pagination state
API call with same pagination parameters as before
Response with fresh PaginatedResponse
formGeneric.js re-renders table with updated data
```
### Response Flow
```
Backend returns:
{
"items": [...], // Page of data items
"pagination": { // Pagination metadata (or null if not paginated)
"currentPage": 1,
"pageSize": 10,
"totalItems": 100,
"totalPages": 10,
"sort": [...], // Current sort configuration
"filters": {...} // Current filters (for future use)
}
}
```
## Data Models
### PaginationRequest (Query Parameters)
```python
class PaginationRequest(BaseModel):
"""
Pagination request parameters sent from frontend to backend.
All fields are optional. If pagination=None, no pagination is applied.
"""
pagination: Optional[PaginationParams] = None
class PaginationParams(BaseModel):
"""
Complete pagination state including page, sorting, and filters.
"""
page: int = Field(ge=1, description="Current page number (1-based)")
pageSize: int = Field(ge=1, le=1000, description="Number of items per page")
sort: List[SortField] = Field(default_factory=list, description="List of sort fields in priority order")
filters: Optional[Dict[str, Any]] = Field(default=None, description="Filter criteria (structure TBD for future implementation)")
### PaginatedResponse
```python
class PaginatedResponse(BaseModel):
"""
Response containing paginated data and metadata.
"""
items: List[Any] = Field(..., description="Array of items for current page")
pagination: Optional[PaginationMetadata] = Field(..., description="Pagination metadata (None if pagination not applied)")
class PaginationMetadata(BaseModel):
"""
Pagination metadata returned to frontend for rendering controls.
Contains all information needed to render pagination UI and handle user interactions.
"""
currentPage: int = Field(..., description="Current page number (1-based)")
pageSize: int = Field(..., description="Number of items per page")
totalItems: int = Field(..., description="Total number of items across all pages (after filters)")
totalPages: int = Field(..., description="Total number of pages (calculated from totalItems / pageSize)")
sort: List[SortField] = Field(..., description="Current sort configuration applied")
filters: Optional[Dict[str, Any]] = Field(default=None, description="Current filters applied (for future use)")
```
## Backend Implementation
### Route Structure
**Before (no pagination):**
```python
@router.get("", response_model=List[Prompt])
async def get_prompts(...) -> List[Prompt]:
prompts = managementInterface.getAllPrompts()
return prompts
```
**After (with pagination):**
```python
@router.get("", response_model=PaginatedResponse[Prompt])
async def get_prompts(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser)
) -> PaginatedResponse[Prompt]:
"""
Get prompts with optional pagination, sorting, and filtering.
Query Parameters:
- pagination: JSON-encoded PaginationParams object, or None for no pagination
Examples:
- GET /api/prompts (no pagination - returns all items)
- GET /api/prompts?pagination={"page":1,"pageSize":10,"sort":[]}
- GET /api/prompts?pagination={"page":2,"pageSize":20,"sort":[{"field":"name","direction":"asc"}]}
"""
import json
from modules.shared.paginationModels import PaginationParams
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
managementInterface = interfaceDbComponentObjects.getInterface(currentUser)
# Call interface method with optional pagination
result = managementInterface.getAllPrompts(pagination=paginationParams)
# If pagination was requested, result is PaginatedResult
# If no pagination, result is List[Prompt]
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
```
### Interface Method Signature (Generic Approach)
**All interface methods that return lists should support optional pagination:**
```python
# In interfaceDbComponentObjects.py, interfaceDbAppObjects.py, interfaceDbChatObjects.py
class ComponentObjects:
def getAllPrompts(self, pagination: Optional[PaginationParams] = None) -> Union[List[Prompt], PaginatedResult]:
"""
Returns prompts based on user access level.
Supports optional pagination, sorting, and filtering.
Args:
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[Prompt]
If pagination is provided: PaginatedResult with items and metadata
"""
# Get all records from database
allPrompts = self.db.getRecordset(Prompt)
filteredPrompts = self._uam(Prompt, allPrompts)
# If no pagination requested, return all items
if pagination is None:
return [Prompt(**prompt) for prompt in filteredPrompts]
# Apply filtering (if filters provided)
if pagination.filters:
filteredPrompts = self._applyFilters(filteredPrompts, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination.sort:
filteredPrompts = self._applySorting(filteredPrompts, pagination.sort)
# Count total items after filters
totalItems = len(filteredPrompts)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedPrompts = filteredPrompts[startIdx:endIdx]
# Convert to model objects
items = [Prompt(**prompt) for prompt in pagedPrompts]
return PaginatedResult(
items=items,
totalItems=totalItems,
totalPages=totalPages
)
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Apply filter criteria to records (implementation for future filtering)."""
# TODO: Implement filtering logic
return records
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[SortField]) -> List[Dict[str, Any]]:
"""Apply multi-level sorting to records."""
def sortKey(record):
key_values = []
for sortField in sortFields:
value = record.get(sortField.field)
# Handle None values
if value is None:
key_values.append(("", sortField.direction == "desc"))
else:
key_values.append((value, sortField.direction == "desc"))
return key_values
# Sort records using all sort fields in priority order
sortedRecords = sorted(records, key=sortKey)
# For descending fields, reverse those groups
# (This is a simplified approach - more complex sorting can be implemented)
for sortField in reversed(sortFields):
if sortField.direction == "desc":
# This needs more sophisticated implementation for multi-field desc sorting
pass
return sortedRecords
```
**All other `getAll*` methods follow the same pattern:**
- `getAllFiles(pagination: Optional[PaginationParams] = None)`
- `getAllUsers(pagination: Optional[PaginationParams] = None)`
- `getAllMandates(pagination: Optional[PaginationParams] = None)`
- `getWorkflows(pagination: Optional[PaginationParams] = None)`
- `getMessages(workflowId: str, pagination: Optional[PaginationParams] = None)` (for nested resources)
- etc.
**Implementation across all three interfaces:**
- `interfaceDbComponentObjects.py`: `getAllPrompts`, `getAllFiles`
- `interfaceDbAppObjects.py`: `getAllMandates`, `getUsersByMandate`, `getAllUsers` (if exists)
- `interfaceDbChatObjects.py`: `getWorkflows`, `getMessages`, `getLogs`, etc.
### PaginatedResult (Internal)
```python
class PaginatedResult(BaseModel):
"""
Internal result structure from interface layer.
Used when pagination is requested.
"""
items: List[Any]
totalItems: int
totalPages: int # Calculated as: math.ceil(totalItems / pageSize)
```
**Note:** Interface methods return `Union[List[Model], PaginatedResult]`:
- When `pagination=None`: Return `List[Model]` (backward compatible)
- When `pagination` is provided: Return `PaginatedResult`
## Frontend Implementation
### Pagination State Object
```javascript
// In formGeneric.js module state
const paginationState = {
currentPage: 1,
pageSize: 10, // Default configured per view
totalItems: null,
totalPages: null,
sort: [], // Array of {field: string, direction: "asc"|"desc"}
filters: null // Object (for future use)
};
```
### API Call Structure
```javascript
// In apiCalls.js
getPrompts: async function(pagination = null) {
let url = '/api/prompts';
if (pagination) {
// Encode pagination object as JSON query parameter
const paginationJson = JSON.stringify(pagination);
url += `?pagination=${encodeURIComponent(paginationJson)}`;
}
return await privateApi.get(url);
}
```
### formGeneric.js Integration
**Initial Load:**
```javascript
async function loadData(entityType) {
const module = moduleRegistry.get(entityType);
const { config, state } = module;
// Get default page size from config
const defaultPageSize = config.pagination?.defaultPageSize || 10;
// Construct pagination request
const paginationRequest = {
page: 1,
pageSize: defaultPageSize,
sort: [],
filters: null
};
// Call API
const response = await config.apiEndpoint.get(paginationRequest);
// Update state
state.items = response.items;
if (response.pagination) {
state.currentPage = response.pagination.currentPage;
state.pageSize = response.pagination.pageSize;
state.totalItems = response.pagination.totalItems;
state.totalPages = response.pagination.totalPages;
state.sort = response.pagination.sort;
state.filters = response.pagination.filters;
} else {
// No pagination - all items loaded
state.currentPage = 1;
state.pageSize = response.items.length;
state.totalItems = response.items.length;
state.totalPages = 1;
state.sort = [];
state.filters = null;
}
// Render
await renderItems(entityType);
}
```
**Page Change:**
```javascript
async function goToPage(entityType, pageNumber) {
const module = moduleRegistry.get(entityType);
const { config, state } = module;
// Construct pagination request with new page
const paginationRequest = {
page: pageNumber,
pageSize: state.pageSize,
sort: state.sort,
filters: state.filters
};
// Call API
const response = await config.apiEndpoint.get(paginationRequest);
// Update state
state.items = response.items;
state.currentPage = response.pagination.currentPage;
// ... update other pagination state
// Render
await renderItems(entityType);
}
```
**After Create/Update/Delete (Refresh Data):**
```javascript
// After successful create/update/delete operation
async function refreshAfterModification(entityType) {
const module = moduleRegistry.get(entityType);
const { config, state } = module;
// Construct pagination request with current state
const paginationRequest = {
page: state.currentPage,
pageSize: state.pageSize,
sort: state.sort,
filters: state.filters
};
// Call API to get fresh data
const response = await config.apiEndpoint.get(paginationRequest);
// Update state with fresh data
state.items = response.items;
if (response.pagination) {
state.currentPage = response.pagination.currentPage;
state.totalItems = response.pagination.totalItems;
state.totalPages = response.pagination.totalPages;
state.sort = response.pagination.sort;
state.filters = response.pagination.filters;
}
// Render updated table
await renderItems(entityType);
}
// Example: After creating an item
async function createItem(entityType, itemData) {
try {
// Create item via API
const newItem = await config.apiEndpoint.create(itemData);
// Refresh data after successful creation
await refreshAfterModification(entityType);
// Call onItemCreated event
if (config.events?.onItemCreated) {
config.events.onItemCreated(newItem);
}
} catch (error) {
ui.log.error(`Error creating ${entityType}:`, error);
showError(`Error creating ${entityType}`, error.message);
}
}
// Example: After updating an item
async function updateItem(entityType, itemId, updateData) {
try {
// Update item via API
const updatedItem = await config.apiEndpoint.update(itemId, updateData);
// Refresh data after successful update
await refreshAfterModification(entityType);
// Call onItemUpdated event
if (config.events?.onItemUpdated) {
config.events.onItemUpdated(updatedItem);
}
} catch (error) {
ui.log.error(`Error updating ${entityType}:`, error);
showError(`Error updating ${entityType}`, error.message);
}
}
// Example: After deleting an item
async function deleteItem(entityType, itemId) {
try {
// Delete item via API
await config.apiEndpoint.delete(itemId);
// Refresh data after successful deletion
await refreshAfterModification(entityType);
// Call onItemDeleted event
if (config.events?.onItemDeleted) {
config.events.onItemDeleted(itemId);
}
} catch (error) {
ui.log.error(`Error deleting ${entityType}:`, error);
showError(`Error deleting ${entityType}`, error.message);
}
}
```
**Sort Change:**
```javascript
async function applySort(entityType, fieldName, direction) {
const module = moduleRegistry.get(entityType);
const { config, state } = module;
// Update sort array (replace existing sort for this field, or add new)
const newSort = [...state.sort];
const existingIndex = newSort.findIndex(s => s.field === fieldName);
if (existingIndex >= 0) {
// Update existing sort
newSort[existingIndex].direction = direction;
} else {
// Add new sort field
newSort.push({ field: fieldName, direction: direction });
}
// Reset to page 1 when sorting changes
const paginationRequest = {
page: 1,
pageSize: state.pageSize,
sort: newSort,
filters: state.filters
};
// Call API
const response = await config.apiEndpoint.get(paginationRequest);
// Update state
state.items = response.items;
state.currentPage = 1;
state.sort = newSort;
// ... update other pagination state
// Render
await renderItems(entityType);
}
```
**Page Size Change:**
```javascript
async function changePageSize(entityType, newPageSize) {
const module = moduleRegistry.get(entityType);
const { config, state } = module;
// Reset to page 1 when page size changes
const paginationRequest = {
page: 1,
pageSize: newPageSize,
sort: state.sort,
filters: state.filters
};
// Call API
const response = await config.apiEndpoint.get(paginationRequest);
// Update state
state.items = response.items;
state.currentPage = 1;
state.pageSize = newPageSize;
// ... update other pagination state
// Render
await renderItems(entityType);
}
```
### Module Configuration
**Per-View Default Page Size:**
```javascript
// In formPrompts.js (or other module files)
formGeneric.initModule(globalStateObj, {
entityType: 'prompt',
apiEndpoint: {
get: api.getPrompts, // Must accept pagination parameter
create: api.createPrompt,
update: api.updatePrompt,
delete: api.deletePrompt
},
pagination: {
defaultPageSize: 20 // Configure default for this view
},
// ... other config
});
```
## UI Rendering Requirements
### Pagination Controls
The frontend must render pagination controls using only the information from `PaginationMetadata`:
1. **Page Numbers**: Render buttons for pages 1 through `totalPages`
2. **Current Page Indicator**: Highlight `currentPage`
3. **Previous/Next Buttons**:
- Previous disabled if `currentPage === 1`
- Next disabled if `currentPage === totalPages`
4. **Page Size Selector**: Dropdown to change `pageSize` (triggers new API call)
5. **Item Count Display**: Show "Showing X to Y of Z items"
### Sort Indicators
- Display sort indicators in table headers based on `sort` array
- Show direction (↑ for ASC, ↓ for DESC)
- Show priority order for multi-level sorting (1, 2, 3...)
- Clicking a column header toggles/changes its sort
### Empty State
- If `totalItems === 0`: Show "No items found" message
- If current page has no items but `totalItems > 0`: Redirect to page 1 (should not happen with proper backend implementation)
## Validation Rules
### Backend Validation
1. **Page Number**: Must be >= 1 and <= totalPages
2. **Page Size**: Must be >= 1 and <= 1000 (configurable max)
3. **Sort Fields**: Must be valid field names for the entity type
4. **Sort Direction**: Must be "asc" or "desc"
### Frontend Validation
1. **Page Navigation**: Disable buttons when at boundaries (page 1 or last page)
2. **Page Size**: Only allow predefined values (e.g., 10, 20, 50, 100)
3. **Sort Fields**: Only allow sorting on fields that exist in field definitions
## Error Handling
### Backend Errors
- **Invalid Page**: Return 400 Bad Request with error message
- **Invalid Sort Field**: Return 400 Bad Request with list of valid fields
- **Invalid Page Size**: Return 400 Bad Request with allowed range
### Frontend Errors
- **API Error**: Display error message, keep current pagination state
- **Network Error**: Show retry option, maintain pagination state
- **Invalid Response**: Fallback to error state, log error for debugging
## Migration Strategy
### Phase 1: Backend Data Models
1. Create `PaginationParams`, `SortField` models in `modules/shared/paginationModels.py`
2. Create `PaginatedResponse`, `PaginationMetadata` models
3. Create `PaginatedResult` internal model for interface layer
### Phase 2: Backend Interface Updates
1. **interfaceDbComponentObjects.py:**
- Update `getAllPrompts(pagination: Optional[PaginationParams] = None)`
- Update `getAllFiles(pagination: Optional[PaginationParams] = None)`
- Add `_applyFilters()` and `_applySorting()` helper methods
2. **interfaceDbAppObjects.py:**
- Update `getAllMandates(pagination: Optional[PaginationParams] = None)`
- Update `getUsersByMandate(mandateId: str, pagination: Optional[PaginationParams] = None)`
- Add `_applyFilters()` and `_applySorting()` helper methods
3. **interfaceDbChatObjects.py:**
- Update `getWorkflows(pagination: Optional[PaginationParams] = None)`
- Update `getMessages(workflowId: str, pagination: Optional[PaginationParams] = None)`
- Update `getLogs(workflowId: str, pagination: Optional[PaginationParams] = None)`
- Add `_applyFilters()` and `_applySorting()` helper methods
### Phase 3: Backend Route Updates
1. Update all list endpoints to accept pagination query parameter
2. Parse pagination JSON parameter
3. Call interface methods with pagination parameter
4. Return `PaginatedResponse` with items and metadata
### Phase 4: Frontend Implementation
1. Update `apiCalls.js` functions to accept pagination parameter (as JSON string in query)
2. Update `formGeneric.js` to construct and send pagination requests
3. Update `formGeneric.js` to handle paginated responses
4. Update pagination controls to trigger API calls
5. Update sort controls to trigger API calls
6. Add `refreshAfterModification()` function for CUD operations
7. Update create/update/delete handlers to refresh data after success
8. Add default page size configuration to each module
### Phase 5: Testing
1. Test pagination with various page sizes
2. Test sorting (single and multi-level)
3. Test filtering (when implemented)
4. Test CUD operations with data refresh
5. Test edge cases (empty results, single page, invalid page numbers, etc.)
6. Test error handling
## Future Enhancements
### Filtering (Prepared Structure)
The `filters` field is prepared for future filtering implementation:
```javascript
// Future filter structure (to be defined)
const filters = {
fieldName: {
operator: "equals" | "contains" | "greaterThan" | "lessThan" | ...,
value: "..."
},
// Multiple filters combined with AND/OR logic
};
```
### Server-Side Filtering
When filtering is implemented:
1. Filters are included in `PaginationRequest`
2. Backend applies filters before counting `totalItems`
3. `PaginationMetadata.filters` contains applied filters
4. Frontend displays active filters and allows removal
## Summary Checklist
### Backend Must Provide:
- ✅ Paginated data (page of items only)
- ✅ Current page number
- ✅ Page size
- ✅ Total items count (after filters)
- ✅ Total pages count
- ✅ Current sort configuration
- ✅ Current filter configuration (for future)
### Frontend Must Handle:
- ✅ Construct pagination requests
- ✅ Send pagination requests to API
- ✅ Receive and parse paginated responses
- ✅ Render pagination controls
- ✅ Handle page navigation
- ✅ Handle sort changes
- ✅ Handle page size changes
- ✅ Display empty states
- ✅ Handle errors
### Missing Information Check:
**For Rendering:**
- ✅ Items to display (from `response.items`)
- ✅ Current page (from `response.pagination.currentPage`)
- ✅ Total pages (from `response.pagination.totalPages`)
- ✅ Total items (from `response.pagination.totalItems`)
- ✅ Page size (from `response.pagination.pageSize`)
- ✅ Sort state (from `response.pagination.sort`)
- ✅ Filter state (from `response.pagination.filters`)
**Nothing missing** - all required information is provided in the response structure.
## Implementation Notes
1. **No Client-Side Pagination**: All data slicing happens on the server
2. **No Fallbacks**: Clean implementation - if pagination is requested, it must work
3. **Explicit State**: Every UI action results in an explicit API call with full pagination state
4. **Default Configuration**: Each view module configures its default page size
5. **Extensibility**: Structure supports future filtering without breaking changes