Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-r

import { Button } from '@/vdb/components/ui/button.js';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
import { ALLOWED_PER_PAGE_VALUES } from '@/vdb/constants.js';

interface DataTablePaginationProps<TData> {
table: Table<TData>;
Expand All @@ -27,11 +28,11 @@ export function DataTablePagination<TData>({ table }: DataTablePaginationProps<T
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectTrigger className="h-8 max-w-20">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map(pageSize => (
{ALLOWED_PER_PAGE_VALUES.map(pageSize => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
Expand All @@ -40,7 +41,7 @@ export function DataTablePagination<TData>({ table }: DataTablePaginationProps<T
</Select>
</div>
<div className=" flex items-center justify-center text-sm font-medium">
<span className="hidden md:block w-[100px] ">
<span className="hidden md:block max-w-28 ">
<Trans>
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}
</Trans>
Expand Down
25 changes: 23 additions & 2 deletions packages/dashboard/src/lib/components/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { DataTableBulkActions } from './data-table-bulk-actions.js';
import { DataTableProvider } from './data-table-context.js';
import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
import { DataTableFilterBadgeEditable } from './data-table-filter-badge-editable.js';
import { DEFAULT_PER_PAGE } from '@/vdb/constants.js';

export interface FacetedFilter {
title: string;
Expand Down Expand Up @@ -120,9 +121,16 @@ export function DataTable<TData>({
const savedViewsResult = useSavedViews();
const globalViews = pageId && onFilterChange ? savedViewsResult.globalViews : [];
const { t } = useLingui();

// Calculate safe page value with validation
const pageSize = itemsPerPage ?? DEFAULT_PER_PAGE;
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
const requestedPage = page ?? 1;
const safePage = Math.min(Math.max(requestedPage, 1), totalPages);

const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: (page ?? 1) - 1,
pageSize: itemsPerPage ?? 10,
pageIndex: safePage - 1,
pageSize: pageSize,
});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
defaultColumnVisibility ?? {},
Expand Down Expand Up @@ -173,6 +181,19 @@ export function DataTable<TData>({

const table = useReactTable(tableOptions);

// Sync pagination state when props change
useEffect(() => {
const newPageIndex = safePage - 1;
const newPageSize = pageSize;

if (pagination.pageIndex !== newPageIndex || pagination.pageSize !== newPageSize) {
setPagination({
pageIndex: newPageIndex,
pageSize: newPageSize,
});
}
}, [safePage, pageSize]);

useEffect(() => {
onPageChange?.(table, pagination.pageIndex + 1, pagination.pageSize);
}, [pagination]);
Expand Down
6 changes: 6 additions & 0 deletions packages/dashboard/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const LS_KEY_SELECTED_CHANNEL_TOKEN = 'vendure-selected-channel-token';
export const LS_KEY_SHIPPING_TEST_ORDER = 'vendure-shipping-test-order';
export const LS_KEY_SHIPPING_TEST_ADDRESS = 'vendure-shipping-test-address';

/**
* Pagination constants
*/
export const DEFAULT_PER_PAGE = 10;
export const ALLOWED_PER_PAGE_VALUES = [10, 25, 50, 100] as const;

/**
* This is copied from the generated types from @vendure/common/lib/generated-types.d.ts
* It is used to provide a list of available currency codes for the user to select from.
Expand Down
18 changes: 12 additions & 6 deletions packages/dashboard/src/lib/framework/page/list-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ColumnFiltersState, SortingState, Table } from '@tanstack/react-table';
import { TableOptions } from '@tanstack/table-core';

import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
import { validatePageValue, validatePerPageValue } from '@/vdb/utils/pagination.js';
import {
FullWidthPageBlock,
Page,
Expand Down Expand Up @@ -485,12 +486,19 @@ export function ListPage<
const route = typeof routeOrFn === 'function' ? routeOrFn() : routeOrFn;
const routeSearch = route.useSearch();
const navigate = useNavigate<AnyRouter>({ from: route.fullPath });
const { setTableSettings, settings } = useUserSettings();
const { setTableSettings, setItemsPerPage, settings } = useUserSettings();
const tableSettings = pageId ? settings.tableSettings?.[pageId] : undefined;

const itemsPerPage = routeSearch.perPage
? validatePerPageValue(Number.parseInt(routeSearch.perPage))
: validatePerPageValue(settings.itemsPerPage);

const page = routeSearch.page ? validatePageValue(Number.parseInt(routeSearch.page)) : 1;


const pagination = {
page: routeSearch.page ? parseInt(routeSearch.page) : 1,
itemsPerPage: routeSearch.perPage ? parseInt(routeSearch.perPage) : (tableSettings?.pageSize ?? 10),
page,
itemsPerPage,
};

const columnVisibility = pageId
Expand Down Expand Up @@ -557,9 +565,7 @@ export function ListPage<
columnFilters={columnFilters}
onPageChange={(table, page, perPage) => {
persistListStateToUrl(table, { page, perPage });
if (pageId) {
setTableSettings(pageId, 'pageSize', perPage);
}
setItemsPerPage(perPage);
}}
onSortChange={(table, sorting) => {
persistListStateToUrl(table, { sort: sorting });
Expand Down
25 changes: 20 additions & 5 deletions packages/dashboard/src/lib/providers/user-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LS_KEY_USER_SETTINGS } from '@/vdb/constants.js';
import { LS_KEY_USER_SETTINGS, DEFAULT_PER_PAGE } from '@/vdb/constants.js';
import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
import { ColumnFiltersState } from '@tanstack/react-table';
import React, { createContext, useEffect, useRef, useState } from 'react';
Expand All @@ -7,13 +7,13 @@ import {
getSettingsStoreValueDocument,
setSettingsStoreValueDocument,
} from '../graphql/settings-store-operations.js';
import { validatePerPageValue } from '../utils/pagination.js';
import { Theme } from './theme-provider.js';

export interface TableSettings {
columnVisibility?: Record<string, boolean>;
columnOrder?: string[];
columnFilters?: ColumnFiltersState;
pageSize?: number;
}

export interface UserSettings {
Expand All @@ -26,6 +26,7 @@ export interface UserSettings {
activeChannelId: string;
devMode: boolean;
hasSeenOnboarding: boolean;
itemsPerPage: number;
tableSettings?: Record<string, TableSettings>;
widgetLayout?: Record<string, { x: number; y: number; w: number; h: number }>;
}
Expand All @@ -40,6 +41,7 @@ const defaultSettings: UserSettings = {
activeChannelId: '',
devMode: false,
hasSeenOnboarding: false,
itemsPerPage: DEFAULT_PER_PAGE,
tableSettings: {},
};

Expand All @@ -60,6 +62,7 @@ export interface UserSettingsContextType {
setActiveChannelId: (channelId: string) => void;
setDevMode: (devMode: boolean) => void;
setHasSeenOnboarding: (hasSeen: boolean) => void;
setItemsPerPage: (itemsPerPage: number) => void;
setTableSettings: <K extends keyof TableSettings>(
tableId: string,
key: K,
Expand All @@ -83,7 +86,12 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
try {
const storedSettings = localStorage.getItem(LS_KEY_USER_SETTINGS);
if (storedSettings) {
return { ...defaultSettings, ...JSON.parse(storedSettings) };
const parsed = JSON.parse(storedSettings);
return {
...defaultSettings,
...parsed,
itemsPerPage: validatePerPageValue(parsed.itemsPerPage ?? defaultSettings.itemsPerPage),
};
}
} catch (e) {
console.error('Failed to load user settings from localStorage', e);
Expand Down Expand Up @@ -145,8 +153,14 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
const serverSettingsData =
serverSettingsResponse?.getSettingsStoreValue as UserSettings | null;
if (serverSettingsData) {
// Server has settings, use them
const mergedSettings = { ...defaultSettings, ...serverSettingsData };
// Server has settings, use them and validate itemsPerPage
const mergedSettings = {
...defaultSettings,
...serverSettingsData,
itemsPerPage: validatePerPageValue(
serverSettingsData.itemsPerPage ?? defaultSettings.itemsPerPage
),
};
setSettings(mergedSettings);
setServerSettings(mergedSettings);
setIsReady(true);
Expand Down Expand Up @@ -210,6 +224,7 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
setActiveChannelId: channelId => updateSetting('activeChannelId', channelId),
setDevMode: devMode => updateSetting('devMode', devMode),
setHasSeenOnboarding: hasSeen => updateSetting('hasSeenOnboarding', hasSeen),
setItemsPerPage: itemsPerPage => updateSetting('itemsPerPage', validatePerPageValue(itemsPerPage)),
setTableSettings: (tableId, key, value) => {
setSettings(prev => ({
...prev,
Expand Down
23 changes: 23 additions & 0 deletions packages/dashboard/src/lib/utils/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ALLOWED_PER_PAGE_VALUES, DEFAULT_PER_PAGE } from '../constants.js';

/**
* Validates and returns a valid per-page value.
* If the value is not in the allowed list, returns the default value.
*/
export function validatePerPageValue(value: number): number {
if (ALLOWED_PER_PAGE_VALUES.includes(value as (typeof ALLOWED_PER_PAGE_VALUES)[number])) {
return value;
}
return DEFAULT_PER_PAGE;
}

/**
* Validates and returns a valid page number.
* If the value is not a valid positive integer, returns 1.
*/
export function validatePageValue(value: number): number {
if (Number.isFinite(value) && !Number.isNaN(value) && value >= 1 && Number.isInteger(value)) {
return value;
}
return 1;
}
Loading