754 lines
25 KiB
Markdown
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
|
|
|