25 KiB
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
- Backend-Driven Pagination: All data slicing, sorting, and filtering happens on the server
- Stateless Requests: Each UI action (page change, sort, filter) triggers a new API request with complete pagination state
- Explicit Structure: Clear, well-defined data structures for requests and responses
- No Fallbacks: Clean implementation without backward compatibility workarounds
- 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)
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):
@router.get("", response_model=List[Prompt])
async def get_prompts(...) -> List[Prompt]:
prompts = managementInterface.getAllPrompts()
return prompts
After (with pagination):
@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:
# 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,getAllFilesinterfaceDbAppObjects.py:getAllMandates,getUsersByMandate,getAllUsers(if exists)interfaceDbChatObjects.py:getWorkflows,getMessages,getLogs, etc.
PaginatedResult (Internal)
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: ReturnList[Model](backward compatible) - When
paginationis provided: ReturnPaginatedResult
Frontend Implementation
Pagination State Object
// 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
// 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:
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:
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):
// 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:
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:
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:
// 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:
- Page Numbers: Render buttons for pages 1 through
totalPages - Current Page Indicator: Highlight
currentPage - Previous/Next Buttons:
- Previous disabled if
currentPage === 1 - Next disabled if
currentPage === totalPages
- Previous disabled if
- Page Size Selector: Dropdown to change
pageSize(triggers new API call) - Item Count Display: Show "Showing X to Y of Z items"
Sort Indicators
- Display sort indicators in table headers based on
sortarray - 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
- Page Number: Must be >= 1 and <= totalPages
- Page Size: Must be >= 1 and <= 1000 (configurable max)
- Sort Fields: Must be valid field names for the entity type
- Sort Direction: Must be "asc" or "desc"
Frontend Validation
- Page Navigation: Disable buttons when at boundaries (page 1 or last page)
- Page Size: Only allow predefined values (e.g., 10, 20, 50, 100)
- 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
- Create
PaginationParams,SortFieldmodels inmodules/shared/paginationModels.py - Create
PaginatedResponse,PaginationMetadatamodels - Create
PaginatedResultinternal model for interface layer
Phase 2: Backend Interface Updates
-
interfaceDbComponentObjects.py:
- Update
getAllPrompts(pagination: Optional[PaginationParams] = None) - Update
getAllFiles(pagination: Optional[PaginationParams] = None) - Add
_applyFilters()and_applySorting()helper methods
- Update
-
interfaceDbAppObjects.py:
- Update
getAllMandates(pagination: Optional[PaginationParams] = None) - Update
getUsersByMandate(mandateId: str, pagination: Optional[PaginationParams] = None) - Add
_applyFilters()and_applySorting()helper methods
- Update
-
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
- Update
Phase 3: Backend Route Updates
- Update all list endpoints to accept pagination query parameter
- Parse pagination JSON parameter
- Call interface methods with pagination parameter
- Return
PaginatedResponsewith items and metadata
Phase 4: Frontend Implementation
- Update
apiCalls.jsfunctions to accept pagination parameter (as JSON string in query) - Update
formGeneric.jsto construct and send pagination requests - Update
formGeneric.jsto handle paginated responses - Update pagination controls to trigger API calls
- Update sort controls to trigger API calls
- Add
refreshAfterModification()function for CUD operations - Update create/update/delete handlers to refresh data after success
- Add default page size configuration to each module
Phase 5: Testing
- Test pagination with various page sizes
- Test sorting (single and multi-level)
- Test filtering (when implemented)
- Test CUD operations with data refresh
- Test edge cases (empty results, single page, invalid page numbers, etc.)
- Test error handling
Future Enhancements
Filtering (Prepared Structure)
The filters field is prepared for future filtering implementation:
// 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:
- Filters are included in
PaginationRequest - Backend applies filters before counting
totalItems PaginationMetadata.filterscontains applied filters- 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
- No Client-Side Pagination: All data slicing happens on the server
- No Fallbacks: Clean implementation - if pagination is requested, it must work
- Explicit State: Every UI action results in an explicit API call with full pagination state
- Default Configuration: Each view module configures its default page size
- Extensibility: Structure supports future filtering without breaking changes