Skip to content

Commit

Permalink
feat: DataTable supports pagination
Browse files Browse the repository at this point in the history
- build in pagination controls to the DataTable component
- use pagination in the OrdersTable within the orders page
- update the pagination ui components to use a button rather than a link for a different UI. Update associated uses
  • Loading branch information
dylants committed Apr 24, 2024
1 parent 931f2d2 commit ca1ca7a
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 54 deletions.
47 changes: 38 additions & 9 deletions src/app/orders/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,32 @@ import {
} from '@/components/Breadcrumbs';
import OrdersTable from '@/components/order/OrdersTable';
import { getOrders } from '@/lib/actions/order';
import { DEFAULT_LIMIT } from '@/lib/pagination';
import OrderHydrated from '@/types/OrderHydrated';
import PageInfo from '@/types/PageInfo';
import { usePathname } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';

export default function OrdersPage() {
const [orders, setOrders] = useState<Array<OrderHydrated> | null>();
const [pageInfo, setPageInfo] = useState<PageInfo>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const pathname = usePathname();

const initialLoad = useCallback(async () => {
const { orders, pageInfo } = await getOrders({
paginationQuery: {
first: DEFAULT_LIMIT,
},
});
setOrders(orders);
setPageInfo(pageInfo);
}, []);

useEffect(() => {
initialLoad();
}, [initialLoad]);

// Delay the loading animation a tiny amount to avoid screen flicker for quick connections (localhost)
const setDelayedLoading = useCallback(() => {
const timeout = setTimeout(() => setIsLoading(true), 50);
Expand All @@ -26,21 +43,31 @@ export default function OrdersPage() {
};
}, []);

const loadOrders = useCallback(async () => {
const onNext = useCallback(async () => {
const doneLoading = setDelayedLoading();
// TODO handle pagination
const { orders } = await getOrders({
const { orders: newOrders, pageInfo: newPageInfo } = await getOrders({
paginationQuery: {
first: 100,
after: pageInfo?.endCursor,
first: DEFAULT_LIMIT,
},
});
setOrders(orders);
setOrders(newOrders);
setPageInfo(newPageInfo);
doneLoading();
}, [setDelayedLoading]);
}, [pageInfo, setDelayedLoading]);

useEffect(() => {
loadOrders();
}, [loadOrders]);
const onPrevious = useCallback(async () => {
const doneLoading = setDelayedLoading();
const { orders: newOrders, pageInfo: newPageInfo } = await getOrders({
paginationQuery: {
before: pageInfo?.startCursor,
last: DEFAULT_LIMIT,
},
});
setOrders(newOrders);
setPageInfo(newPageInfo);
doneLoading();
}, [pageInfo, setDelayedLoading]);

return (
<>
Expand All @@ -57,6 +84,8 @@ export default function OrdersPage() {
orders={orders || []}
isLoading={!orders || isLoading}
linkPathname={pathname}
onNext={pageInfo?.hasNextPage ? onNext : undefined}
onPrevious={pageInfo?.hasPreviousPage ? onPrevious : undefined}
/>
</div>
</>
Expand Down
2 changes: 0 additions & 2 deletions src/components/book/Books.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,12 @@ export default function Books({
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={onPrevious ? onPrevious : undefined}
isDisabled={!onPrevious || isLoading}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
href="#"
onClick={onNext ? onNext : undefined}
isDisabled={!onNext || isLoading}
/>
Expand Down
6 changes: 6 additions & 0 deletions src/components/order/OrdersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,14 @@ export default function OrdersTable({
orders,
isLoading,
linkPathname,
onNext,
onPrevious,
}: {
orders: OrderHydrated[];
isLoading?: boolean;
linkPathname: string;
onNext?: () => Promise<void>;
onPrevious?: () => Promise<void>;
}) {
return (
<DataTable
Expand All @@ -85,6 +89,8 @@ export default function OrdersTable({
isLoading={isLoading}
linkPathname={linkPathname}
noDataText="No Orders found"
onNext={onNext}
onPrevious={onPrevious}
/>
);
}
25 changes: 25 additions & 0 deletions src/components/stories/table/DataTable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,31 @@ export const ClickableWithIdFieldName: Story = {
};
ClickableWithIdFieldName.storyName = 'Clckable w/idFieldName';

export const WithPagination: Story = {
render: () => {
return (
<div>
<DataTable
columns={columns}
data={data}
onNext={async () => {}}
onPrevious={async () => {}}
/>
</div>
);
},
};

export const WithPaginationDisabled: Story = {
render: () => {
return (
<div>
<DataTable columns={columns} data={data} showPagination />
</div>
);
},
};

export const Loading: Story = {
render: () => {
return (
Expand Down
79 changes: 57 additions & 22 deletions src/components/table/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
'use client';

import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination';
import { Skeleton } from '@/components/ui/skeleton';
import {
Table,
Expand Down Expand Up @@ -28,6 +35,9 @@ export type DataTableProps<TData, TValue> = {
isLoading?: boolean;
linkPathname?: string;
noDataText?: string;
onNext?: () => Promise<void>;
onPrevious?: () => Promise<void>;
showPagination?: boolean;
};

export default function DataTable<TData, TValue>({
Expand All @@ -37,6 +47,9 @@ export default function DataTable<TData, TValue>({
isLoading,
linkPathname,
noDataText = 'No items',
onNext,
onPrevious,
showPagination = !!onNext || !!onPrevious,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
Expand Down Expand Up @@ -100,28 +113,50 @@ export default function DataTable<TData, TValue>({
);

return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>{tableBody}</TableBody>
</Table>
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>{tableBody}</TableBody>
</Table>
</div>
{!isLoading && showPagination && (
<div className="mt-2">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={onPrevious ? onPrevious : undefined}
isDisabled={!onPrevious || isLoading}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={onNext ? onNext : undefined}
isDisabled={!onNext || isLoading}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</div>
);
}
5 changes: 4 additions & 1 deletion src/components/table/SortableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export default function SortableHeader<T>({
return (
<Button
variant="ghost"
className={clsx('flex justify-end w-full px-0', className)}
className={clsx(
'flex justify-end w-full px-0 hover:bg-inherit',
className,
)}
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
{text}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const buttonVariants = cva(
'bg-customPalette-300 text-customPalette-100 shadow hover:bg-customPalette-400/90',
destructive:
'bg-red-500 text-customPalette-100 shadow-sm hover:bg-red-500/90',
ghost: 'hover:bg-customPalette-100/10',
ghost: 'hover:bg-customPalette-200/10',
link: 'underline-offset-4 hover:underline',
outline:
'border border-customPalette-300 bg-white shadow-sm hover:bg-white/50',
Expand Down
35 changes: 16 additions & 19 deletions src/components/ui/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {
} from '@radix-ui/react-icons';

import { cn } from 'lib/tailwind-utils';
import { ButtonProps, buttonVariants } from 'components/ui/button';
import Link from 'next/link';
import { Button, ButtonProps } from 'components/ui/button';

const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
Expand Down Expand Up @@ -43,61 +42,59 @@ type PaginationLinkProps = {
isActive?: boolean;
isDisabled?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<typeof Link>;
React.ComponentProps<typeof Button>;

const PaginationLink = ({
const PaginationButton = ({
className,
isActive,
isDisabled,
size = 'icon',
...props
}: PaginationLinkProps) => (
<Link
<Button
aria-current={isActive ? 'page' : undefined}
aria-disabled={isDisabled}
className={cn(
buttonVariants({
size,
variant: isActive ? 'outline' : 'ghost',
}),
isDisabled &&
'text-customPalette-500/50 hover:text-customPalette-500/50 cursor-not-allowed',
isDisabled && 'text-customPalette-500/50 hover:text-customPalette-500/50',
className,
)}
disabled={isDisabled}
size={size}
variant="ghost"
{...props}
/>
);
PaginationLink.displayName = 'PaginationLink';
PaginationButton.displayName = 'PaginationButton';

const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
}: React.ComponentProps<typeof PaginationButton>) => (
<PaginationButton
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeftIcon className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
</PaginationButton>
);
PaginationPrevious.displayName = 'PaginationPrevious';

const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
}: React.ComponentProps<typeof PaginationButton>) => (
<PaginationButton
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="h-4 w-4" />
</PaginationLink>
</PaginationButton>
);
PaginationNext.displayName = 'PaginationNext';

Expand All @@ -119,7 +116,7 @@ PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationButton,
PaginationItem,
PaginationPrevious,
PaginationNext,
Expand Down

0 comments on commit ca1ca7a

Please sign in to comment.