custom_ui/frontend/PAGINATION_USAGE.md

12 KiB

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

<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

// 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

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

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

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

// 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:

// 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

// 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
  • Small screens: 5-10 records
  • Desktop: 10-25 records
  • Large datasets: 25-50 records
  • Avoid: 100+ records per page

Error Handling

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)

// Old approach - loads all data
onMounted(async () => {
  const data = await Api.getAllClients(); // 5000+ records
  tableData.value = data;
});

After (Server-side)

// New approach - loads only current page
onMounted(async () => {
  paginationStore.initializeTablePagination("clients");

  await handleLazyLoad({
    page: 0,
    rows: 10,
    // ... other params
  });
});

Testing

Unit Tests

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

// 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

// 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.