491 lines
12 KiB
Markdown
491 lines
12 KiB
Markdown
# Server-Side Pagination Implementation Guide
|
|
|
|
## Overview
|
|
|
|
This implementation provides server-side pagination with persistent state management for large datasets (5000+ records). It combines PrimeVue's lazy loading capabilities with Pinia stores for state persistence.
|
|
|
|
## Architecture
|
|
|
|
### Stores
|
|
|
|
1. **`usePaginationStore`** - Manages pagination state (page, pageSize, totalRecords, sorting)
|
|
2. **`useFiltersStore`** - Manages filter state (existing, enhanced for pagination)
|
|
3. **`useLoadingStore`** - Manages loading states (existing, works with pagination)
|
|
|
|
### Components
|
|
|
|
1. **`DataTable`** - Enhanced with lazy loading support
|
|
2. **`Api`** - Updated with pagination and filtering parameters
|
|
|
|
## Key Features
|
|
|
|
✅ **Server-side pagination** - Only loads current page data
|
|
✅ **Persistent state** - Page and filter state survive navigation
|
|
✅ **Real-time filtering** - Filters reset to page 1 and re-query server
|
|
✅ **Sorting support** - Server-side sorting with state persistence
|
|
✅ **Loading states** - Integrated with existing loading system
|
|
✅ **Performance** - Handles 5000+ records efficiently
|
|
|
|
## Usage
|
|
|
|
### Basic Paginated DataTable
|
|
|
|
```vue
|
|
<template>
|
|
<DataTable
|
|
:data="tableData"
|
|
:columns="columns"
|
|
tableName="clients"
|
|
:lazy="true"
|
|
:totalRecords="totalRecords"
|
|
:loading="isLoading"
|
|
:onLazyLoad="handleLazyLoad"
|
|
@lazy-load="handleLazyLoad"
|
|
/>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from "vue";
|
|
import { usePaginationStore } from "@/stores/pagination";
|
|
import { useFiltersStore } from "@/stores/filters";
|
|
import Api from "@/api";
|
|
|
|
const paginationStore = usePaginationStore();
|
|
const filtersStore = useFiltersStore();
|
|
|
|
const tableData = ref([]);
|
|
const totalRecords = ref(0);
|
|
const isLoading = ref(false);
|
|
|
|
const handleLazyLoad = async (event) => {
|
|
try {
|
|
isLoading.value = true;
|
|
|
|
const paginationParams = {
|
|
page: event.page || 0,
|
|
pageSize: event.rows || 10,
|
|
sortField: event.sortField,
|
|
sortOrder: event.sortOrder,
|
|
};
|
|
|
|
const filters = {};
|
|
if (event.filters) {
|
|
Object.keys(event.filters).forEach((key) => {
|
|
if (key !== "global" && event.filters[key]?.value) {
|
|
filters[key] = event.filters[key];
|
|
}
|
|
});
|
|
}
|
|
|
|
const result = await Api.getPaginatedData(paginationParams, filters);
|
|
|
|
tableData.value = result.data;
|
|
totalRecords.value = result.totalRecords;
|
|
paginationStore.setTotalRecords("tableName", result.totalRecords);
|
|
} catch (error) {
|
|
console.error("Error loading data:", error);
|
|
tableData.value = [];
|
|
totalRecords.value = 0;
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
// Initialize stores
|
|
paginationStore.initializeTablePagination("tableName", { rows: 10 });
|
|
filtersStore.initializeTableFilters("tableName", columns);
|
|
|
|
// Load initial data
|
|
const pagination = paginationStore.getTablePagination("tableName");
|
|
const filters = filtersStore.getTableFilters("tableName");
|
|
|
|
await handleLazyLoad({
|
|
page: pagination.page,
|
|
rows: pagination.rows,
|
|
first: pagination.first,
|
|
sortField: pagination.sortField,
|
|
sortOrder: pagination.sortOrder,
|
|
filters: filters,
|
|
});
|
|
});
|
|
</script>
|
|
```
|
|
|
|
## API Implementation
|
|
|
|
### Required API Method Structure
|
|
|
|
```javascript
|
|
// In your API class
|
|
static async getPaginatedData(paginationParams = {}, filters = {}) {
|
|
const {
|
|
page = 0,
|
|
pageSize = 10,
|
|
sortField = null,
|
|
sortOrder = null
|
|
} = paginationParams;
|
|
|
|
// Build database query with pagination
|
|
const offset = page * pageSize;
|
|
const limit = pageSize;
|
|
|
|
// Apply filters to query
|
|
const whereClause = buildWhereClause(filters);
|
|
|
|
// Apply sorting
|
|
const orderBy = sortField ? `${sortField} ${sortOrder === -1 ? 'DESC' : 'ASC'}` : '';
|
|
|
|
// Execute queries
|
|
const [data, totalCount] = await Promise.all([
|
|
db.query(`SELECT * FROM table ${whereClause} ${orderBy} LIMIT ${limit} OFFSET ${offset}`),
|
|
db.query(`SELECT COUNT(*) FROM table ${whereClause}`)
|
|
]);
|
|
|
|
return {
|
|
data: data,
|
|
totalRecords: totalCount[0].count
|
|
};
|
|
}
|
|
```
|
|
|
|
### Frappe Framework Implementation
|
|
|
|
```javascript
|
|
static async getPaginatedClientDetails(paginationParams = {}, filters = {}) {
|
|
const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams;
|
|
|
|
// Build Frappe filters
|
|
let frappeFilters = {};
|
|
Object.keys(filters).forEach(key => {
|
|
if (filters[key] && filters[key].value) {
|
|
switch (key) {
|
|
case 'fullName':
|
|
frappeFilters.address_line1 = ['like', `%${filters[key].value}%`];
|
|
break;
|
|
// Add other filter mappings
|
|
}
|
|
}
|
|
});
|
|
|
|
// Get total count and paginated data
|
|
const [totalCount, records] = await Promise.all([
|
|
this.getDocCount("DocType", frappeFilters),
|
|
this.getDocsList("DocType", ["*"], frappeFilters, page, pageSize)
|
|
]);
|
|
|
|
// Process and return data
|
|
const processedData = records.map(record => ({
|
|
id: record.name,
|
|
// ... other fields
|
|
}));
|
|
|
|
return {
|
|
data: processedData,
|
|
totalRecords: totalCount
|
|
};
|
|
}
|
|
```
|
|
|
|
## DataTable Props
|
|
|
|
### New Props for Pagination
|
|
|
|
```javascript
|
|
const props = defineProps({
|
|
// Existing props...
|
|
|
|
// Server-side pagination
|
|
lazy: {
|
|
type: Boolean,
|
|
default: false, // Set to true for server-side pagination
|
|
},
|
|
totalRecords: {
|
|
type: Number,
|
|
default: 0, // Total records from server
|
|
},
|
|
onLazyLoad: {
|
|
type: Function,
|
|
default: null, // Lazy load handler function
|
|
},
|
|
});
|
|
```
|
|
|
|
### Events
|
|
|
|
- **`@lazy-load`** - Emitted when pagination/filtering/sorting changes
|
|
- **`@page-change`** - Emitted when page changes
|
|
- **`@sort-change`** - Emitted when sorting changes
|
|
- **`@filter-change`** - Emitted when filters change
|
|
|
|
## Pagination Store Methods
|
|
|
|
### Basic Usage
|
|
|
|
```javascript
|
|
const paginationStore = usePaginationStore();
|
|
|
|
// Initialize pagination for a table
|
|
paginationStore.initializeTablePagination("clients", {
|
|
rows: 10,
|
|
totalRecords: 0,
|
|
});
|
|
|
|
// Update pagination after API response
|
|
paginationStore.setTotalRecords("clients", 1250);
|
|
|
|
// Navigate pages
|
|
paginationStore.setPage("clients", 2);
|
|
paginationStore.nextPage("clients");
|
|
paginationStore.previousPage("clients");
|
|
|
|
// Get pagination parameters for API calls
|
|
const params = paginationStore.getPaginationParams("clients");
|
|
// Returns: { page: 2, pageSize: 10, offset: 20, limit: 10, sortField: null, sortOrder: null }
|
|
|
|
// Get page information for display
|
|
const info = paginationStore.getPageInfo("clients");
|
|
// Returns: { start: 21, end: 30, total: 1250 }
|
|
```
|
|
|
|
### Advanced Methods
|
|
|
|
```javascript
|
|
// Handle PrimeVue lazy load events
|
|
const params = paginationStore.handleLazyLoad("clients", primeVueEvent);
|
|
|
|
// Set sorting
|
|
paginationStore.setSorting("clients", "name", 1); // 1 for ASC, -1 for DESC
|
|
|
|
// Change rows per page
|
|
paginationStore.setRowsPerPage("clients", 25);
|
|
|
|
// Reset to first page (useful when filters change)
|
|
paginationStore.resetToFirstPage("clients");
|
|
|
|
// Get computed properties
|
|
const totalPages = paginationStore.getTotalPages("clients");
|
|
const hasNext = paginationStore.hasNextPage("clients");
|
|
const hasPrevious = paginationStore.hasPreviousPage("clients");
|
|
```
|
|
|
|
## Filter Integration
|
|
|
|
Filters work seamlessly with pagination:
|
|
|
|
```javascript
|
|
// When a filter changes, pagination automatically resets to page 1
|
|
const handleFilterChange = (fieldName, value) => {
|
|
// Update filter
|
|
filtersStore.updateTableFilter("clients", fieldName, value);
|
|
|
|
// Pagination automatically resets to page 1 in DataTable component
|
|
// New API call is triggered with updated filters
|
|
};
|
|
```
|
|
|
|
## State Persistence
|
|
|
|
Both pagination and filter states persist across:
|
|
|
|
- Component re-mounts
|
|
- Page navigation
|
|
- Browser refresh (if using localStorage)
|
|
|
|
### Persistence Configuration
|
|
|
|
```javascript
|
|
// In your store, you can add persistence
|
|
import { defineStore } from "pinia";
|
|
|
|
export const usePaginationStore = defineStore("pagination", {
|
|
// ... store definition
|
|
|
|
persist: {
|
|
enabled: true,
|
|
strategies: [
|
|
{
|
|
key: "pagination-state",
|
|
storage: localStorage, // or sessionStorage
|
|
paths: ["tablePagination"],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
### Database Optimization
|
|
|
|
1. **Indexes** - Ensure filtered and sorted columns are indexed
|
|
2. **Query Optimization** - Use efficient WHERE clauses
|
|
3. **Connection Pooling** - Handle concurrent requests efficiently
|
|
|
|
### Frontend Optimization
|
|
|
|
1. **Debounced Filtering** - Avoid excessive API calls
|
|
2. **Loading States** - Provide user feedback during requests
|
|
3. **Error Handling** - Gracefully handle API failures
|
|
4. **Memory Management** - Clear data when not needed
|
|
|
|
### Recommended Page Sizes
|
|
|
|
- **Small screens**: 5-10 records
|
|
- **Desktop**: 10-25 records
|
|
- **Large datasets**: 25-50 records
|
|
- **Avoid**: 100+ records per page
|
|
|
|
## Error Handling
|
|
|
|
```javascript
|
|
const handleLazyLoad = async (event) => {
|
|
try {
|
|
isLoading.value = true;
|
|
const result = await Api.getPaginatedData(params, filters);
|
|
|
|
// Success handling
|
|
tableData.value = result.data;
|
|
totalRecords.value = result.totalRecords;
|
|
} catch (error) {
|
|
console.error("Pagination error:", error);
|
|
|
|
// Reset to safe state
|
|
tableData.value = [];
|
|
totalRecords.value = 0;
|
|
|
|
// Show user-friendly error
|
|
showErrorToast("Failed to load data. Please try again.");
|
|
|
|
// Optionally retry with fallback parameters
|
|
if (event.page > 0) {
|
|
paginationStore.setPage(tableName, 0);
|
|
// Retry with page 0
|
|
}
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
```
|
|
|
|
## Migration from Client-Side
|
|
|
|
### Before (Client-side)
|
|
|
|
```javascript
|
|
// Old approach - loads all data
|
|
onMounted(async () => {
|
|
const data = await Api.getAllClients(); // 5000+ records
|
|
tableData.value = data;
|
|
});
|
|
```
|
|
|
|
### After (Server-side)
|
|
|
|
```javascript
|
|
// New approach - loads only current page
|
|
onMounted(async () => {
|
|
paginationStore.initializeTablePagination("clients");
|
|
|
|
await handleLazyLoad({
|
|
page: 0,
|
|
rows: 10,
|
|
// ... other params
|
|
});
|
|
});
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Unit Tests
|
|
|
|
```javascript
|
|
import { usePaginationStore } from "@/stores/pagination";
|
|
|
|
describe("Pagination Store", () => {
|
|
it("should initialize pagination correctly", () => {
|
|
const store = usePaginationStore();
|
|
store.initializeTablePagination("test", { rows: 20 });
|
|
|
|
const pagination = store.getTablePagination("test");
|
|
expect(pagination.rows).toBe(20);
|
|
expect(pagination.page).toBe(0);
|
|
});
|
|
|
|
it("should handle page navigation", () => {
|
|
const store = usePaginationStore();
|
|
store.setTotalRecords("test", 100);
|
|
store.setPage("test", 2);
|
|
|
|
expect(store.getTablePagination("test").page).toBe(2);
|
|
expect(store.hasNextPage("test")).toBe(true);
|
|
});
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
```javascript
|
|
// Test lazy loading with mock API
|
|
const mockLazyLoad = vi.fn().mockResolvedValue({
|
|
data: [{ id: 1, name: "Test" }],
|
|
totalRecords: 50,
|
|
});
|
|
|
|
// Test component with mocked API
|
|
const wrapper = mount(DataTableComponent, {
|
|
props: {
|
|
lazy: true,
|
|
onLazyLoad: mockLazyLoad,
|
|
},
|
|
});
|
|
|
|
// Verify API calls
|
|
expect(mockLazyLoad).toHaveBeenCalledWith({
|
|
page: 0,
|
|
rows: 10,
|
|
// ... expected parameters
|
|
});
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
1. **Infinite Loading**
|
|
- Check API endpoint returns correct totalRecords
|
|
- Verify pagination parameters are calculated correctly
|
|
|
|
2. **Filters Not Working**
|
|
- Ensure filter parameters are passed to API correctly
|
|
- Check database query includes WHERE clauses
|
|
|
|
3. **Page State Not Persisting**
|
|
- Verify store persistence is configured
|
|
- Check localStorage/sessionStorage permissions
|
|
|
|
4. **Performance Issues**
|
|
- Add database indexes for filtered/sorted columns
|
|
- Optimize API query efficiency
|
|
- Consider reducing page size
|
|
|
|
### Debug Information
|
|
|
|
```javascript
|
|
// Add debug logging to lazy load handler
|
|
const handleLazyLoad = async (event) => {
|
|
console.log("Lazy Load Event:", {
|
|
page: event.page,
|
|
rows: event.rows,
|
|
sortField: event.sortField,
|
|
sortOrder: event.sortOrder,
|
|
filters: event.filters,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
// ... rest of implementation
|
|
};
|
|
```
|
|
|
|
This implementation provides a robust, performant solution for handling large datasets with persistent pagination and filtering state.
|