update filter functionality
This commit is contained in:
parent
464c62d1e5
commit
9431a0502a
10 changed files with 2362 additions and 220 deletions
490
frontend/PAGINATION_USAGE.md
Normal file
490
frontend/PAGINATION_USAGE.md
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue