Skip to content

Commit 088fd14

Browse files
committed
feat(react): add ui primitives and data table updates
1 parent b17766f commit 088fd14

7 files changed

Lines changed: 1035 additions & 77 deletions

File tree

packages/react/src/components/auth0/shared/data-pagination.tsx

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ import {
1010
Pagination,
1111
PaginationContent,
1212
PaginationItem,
13-
PaginationLink,
1413
PaginationPrevious,
1514
PaginationNext,
16-
PaginationEllipsis,
1715
} from '@/components/auth0/shared/pagination';
1816
import {
1917
Select,
@@ -44,12 +42,12 @@ export const defaultLabels: DataPaginationLabels = {
4442
showing: 'Showing',
4543
to: 'to',
4644
of: 'of',
47-
results: 'results',
45+
results: '',
4846
totalResults: 'total results',
49-
show: 'Show',
47+
show: '',
5048
perPage: 'per page',
51-
previous: 'Previous',
52-
next: 'Next',
49+
previous: '',
50+
next: '',
5351
goToPage: 'Go to page {page}',
5452
goToPrevious: 'Go to previous page',
5553
goToNext: 'Go to next page',
@@ -66,6 +64,7 @@ export interface RegularPaginationState {
6664

6765
export interface CheckpointPaginationState {
6866
pageSize: number;
67+
currentPage?: number;
6968
totalItems?: number;
7069
hasNextPage: boolean;
7170
hasPreviousPage: boolean;
@@ -96,32 +95,6 @@ const formatNumber = (num: number | undefined | null, locale?: string): string =
9695
return num.toLocaleString(resolvedLocale);
9796
};
9897

99-
const interpolate = (str: string, values: Record<string, string | number>): string =>
100-
str.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? ''));
101-
102-
const generatePageNumbers = (
103-
currentPage: number,
104-
totalPages: number,
105-
maxVisible: number = 5,
106-
): (number | 'ellipsis')[] => {
107-
if (totalPages <= maxVisible) return Array.from({ length: totalPages }, (_, i) => i + 1);
108-
109-
const pages: (number | 'ellipsis')[] = [1];
110-
const range = Math.floor((maxVisible - 3) / 2);
111-
let start = Math.max(2, currentPage - range);
112-
let end = Math.min(totalPages - 1, currentPage + range);
113-
114-
if (currentPage <= range + 2) end = maxVisible - 2;
115-
if (currentPage >= totalPages - range - 1) start = totalPages - (maxVisible - 2);
116-
117-
if (start > 2) pages.push('ellipsis');
118-
for (let i = start; i <= end; i++) pages.push(i);
119-
if (end < totalPages - 1) pages.push('ellipsis');
120-
pages.push(totalPages);
121-
122-
return pages;
123-
};
124-
12598
/**
12699
*
127100
* @param props - Component props.
@@ -170,18 +143,6 @@ export function DataPagination({
170143
return Array.from(uniqueOptions).sort((a, b) => a - b);
171144
}, [shouldShowPageSizeSelector, pageSizeOptions, currentPageSize]);
172145

173-
const pageNumbers = useMemo(
174-
() =>
175-
isRegular && regularState
176-
? generatePageNumbers(
177-
regularState.currentPage,
178-
regularState.totalPages,
179-
regularState.maxVisiblePages,
180-
)
181-
: [],
182-
[isRegular, regularState],
183-
);
184-
185146
useEffect(() => {
186147
if (ariaLiveRegionRef.current) {
187148
if (isRegular && regularState) {
@@ -196,7 +157,6 @@ export function DataPagination({
196157
return null;
197158
}
198159

199-
const shouldShowPageNumbers = isRegular;
200160
const safeCurrentPageSize = currentPageSize as number;
201161

202162
return (
@@ -223,9 +183,7 @@ export function DataPagination({
223183
: (regularState.currentPage - 1) * regularState.pageSize + 1,
224184
locale,
225185
)}
226-
</span>{' '}
227-
{labels.to}{' '}
228-
<span className="font-medium text-foreground">
186+
-
229187
{formatNumber(
230188
Math.min(
231189
regularState.currentPage * regularState.pageSize,
@@ -237,8 +195,34 @@ export function DataPagination({
237195
{labels.of}{' '}
238196
<span className="font-medium text-foreground">
239197
{formatNumber(regularState.totalItems, locale)}
198+
</span>
199+
{labels.results && <> {labels.results}</>}
200+
</>
201+
) : checkpointState?.totalItems !== undefined &&
202+
checkpointState?.currentPage !== undefined ? (
203+
<>
204+
{labels.showing}{' '}
205+
<span className="font-medium text-foreground">
206+
{formatNumber(
207+
checkpointState.totalItems === 0
208+
? 0
209+
: (checkpointState.currentPage - 1) * checkpointState.pageSize + 1,
210+
locale,
211+
)}
212+
-
213+
{formatNumber(
214+
Math.min(
215+
checkpointState.currentPage * checkpointState.pageSize,
216+
checkpointState.totalItems,
217+
),
218+
locale,
219+
)}
240220
</span>{' '}
241-
{labels.results}
221+
{labels.of}{' '}
222+
<span className="font-medium text-foreground">
223+
{formatNumber(checkpointState.totalItems, locale)}
224+
</span>
225+
{labels.results && <> {labels.results}</>}
242226
</>
243227
) : checkpointState?.totalItems !== undefined ? (
244228
<>
@@ -254,7 +238,7 @@ export function DataPagination({
254238
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
255239
{shouldShowPageSizeSelector && allPageSizeOptions.length > 0 && (
256240
<div className="flex items-center justify-center gap-2 whitespace-nowrap sm:justify-start">
257-
<span className="text-sm text-foreground">{labels.show}</span>
241+
{labels.show && <span className="text-sm text-foreground">{labels.show}</span>}
258242
<Select
259243
value={safeCurrentPageSize.toString()}
260244
onValueChange={(value) => onPageSizeChange?.(Number(value))}
@@ -291,28 +275,6 @@ export function DataPagination({
291275
/>
292276
</PaginationItem>
293277

294-
{shouldShowPageNumbers &&
295-
pageNumbers.map((page, idx) => (
296-
<PaginationItem key={`page-${idx}`}>
297-
{page === 'ellipsis' ? (
298-
<PaginationEllipsis>
299-
<span className="sr-only">{labels.morePages}</span>
300-
</PaginationEllipsis>
301-
) : (
302-
<PaginationLink
303-
onClick={() => onPageChange?.(page)}
304-
isActive={regularState.currentPage === page}
305-
aria-label={interpolate(labels.goToPage, {
306-
page: formatNumber(page, locale),
307-
})}
308-
aria-current={regularState.currentPage === page ? 'page' : undefined}
309-
>
310-
{formatNumber(page, locale)}
311-
</PaginationLink>
312-
)}
313-
</PaginationItem>
314-
))}
315-
316278
<PaginationItem>
317279
<PaginationNext
318280
label={labels.next}

packages/react/src/components/auth0/shared/data-table.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ export interface EmptyStateProps {
110110
action?: ActionButton;
111111
}
112112

113+
export interface DataTableSortConfig {
114+
key: string | null;
115+
direction: 'asc' | 'desc';
116+
}
117+
113118
export interface DataTableProps<Item> {
114119
data: Item[];
115120
columns: Column<Item>[];
@@ -119,6 +124,10 @@ export interface DataTableProps<Item> {
119124
onRowClick?: (rowData: Item) => void;
120125
className?: string;
121126
headerAlign?: AlignmentType;
127+
/** When provided, sorting is delegated to the parent (server-side). */
128+
onSortChange?: (sortConfig: DataTableSortConfig) => void;
129+
/** Controlled sort state. Used with onSortChange for server-side sorting. */
130+
sortConfig?: DataTableSortConfig;
122131
}
123132

124133
const ALIGNMENT_CLASSES = {
@@ -387,13 +396,43 @@ export function DataTable<Item>({
387396
onRowClick,
388397
className,
389398
headerAlign = 'left',
399+
onSortChange,
400+
sortConfig,
390401
}: DataTableProps<Item>) {
391-
const [sorting, setSorting] = useState<SortingState>([]);
402+
const isServerSideSort = !!onSortChange;
403+
404+
const [internalSorting, setInternalSorting] = useState<SortingState>([]);
405+
406+
// Convert controlled sortConfig to TanStack SortingState for header display
407+
const sorting: SortingState = useMemo(() => {
408+
if (isServerSideSort && sortConfig?.key) {
409+
return [{ id: sortConfig.key, desc: sortConfig.direction === 'desc' }];
410+
}
411+
return internalSorting;
412+
}, [isServerSideSort, sortConfig, internalSorting]);
413+
414+
const handleSortingChange = React.useCallback(
415+
(updater: SortingState | ((old: SortingState) => SortingState)) => {
416+
const newSorting = typeof updater === 'function' ? updater(sorting) : updater;
417+
418+
if (isServerSideSort) {
419+
const sort = newSorting[0];
420+
if (sort) {
421+
onSortChange({ key: sort.id, direction: sort.desc ? 'desc' : 'asc' });
422+
} else {
423+
onSortChange({ key: null, direction: 'asc' });
424+
}
425+
} else {
426+
setInternalSorting(newSorting);
427+
}
428+
},
429+
[isServerSideSort, onSortChange, sorting],
430+
);
392431

393432
const tableColumns = useMemo<ColumnDef<Item>[]>(() => {
394433
return columns.map((column, index) => {
395434
return {
396-
id: `column-${index}`,
435+
id: column.accessorKey ? String(column.accessorKey) : `column-${index}`,
397436
accessorKey: column.accessorKey as string,
398437
header: column.title,
399438
size: column.width
@@ -454,9 +493,10 @@ export function DataTable<Item>({
454493
state: {
455494
sorting,
456495
},
457-
onSortingChange: setSorting,
496+
onSortingChange: handleSortingChange,
458497
getCoreRowModel: getCoreRowModel(),
459-
getSortedRowModel: getSortedRowModel(),
498+
getSortedRowModel: isServerSideSort ? undefined : getSortedRowModel(),
499+
manualSorting: isServerSideSort,
460500
manualPagination: true,
461501
});
462502

packages/react/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {
1212
} from './auth0/my-organization/sso-provider-create';
1313
export { SsoProviderTable, SsoProviderTableView } from './auth0/my-organization/sso-provider-table';
1414
export { DomainTable, DomainTableView } from './auth0/my-organization/domain-table';
15+
export { OrganizationMemberManagement } from './auth0/my-organization/organization-member-management';
1516
export {
1617
OrganizationDetailsEdit,
1718
OrganizationDetailsEditView,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { cva, type VariantProps } from 'class-variance-authority';
2+
import { X } from 'lucide-react';
3+
import * as React from 'react';
4+
5+
import { Button } from '@/components/ui/button';
6+
import { cn } from '@/lib/utils';
7+
8+
const chipVariants = cva(
9+
'theme-default:shadow-xs box-border inline-flex items-center gap-1 overflow-clip rounded-2xl border border-transparent font-medium',
10+
{
11+
variants: {
12+
variant: {
13+
default:
14+
'bg-primary text-primary-foreground theme-default:border-primary hover:bg-primary/90',
15+
secondary:
16+
'theme-default:bg-muted bg-accent/10 text-muted-foreground hover:bg-accent/20 theme-default:border-muted-foreground/25 theme-default:hover:bg-muted/90',
17+
outline: 'border-border hover:bg-muted',
18+
info: 'bg-info text-info-foreground theme-default:border-info-foreground/25 hover:bg-info/90',
19+
success:
20+
'bg-success theme-default:border-success-foreground/25 text-success-foreground hover:bg-success/90',
21+
warning:
22+
'bg-warning theme-default:border-warning-foreground/25 text-warning-foreground hover:bg-warning/90',
23+
destructive:
24+
'bg-destructive theme-default:border-destructive-foreground/25 text-destructive-foreground hover:bg-destructive/90',
25+
},
26+
size: {
27+
sm: 'rounded-lg py-0.5 pr-0.5 pl-1.5 text-xs',
28+
md: 'rounded-xl py-1 pr-1 pl-2 text-sm',
29+
lg: 'rounded-2xl py-1.5 pr-1.5 pl-2.5 text-sm',
30+
},
31+
disabled: {
32+
true: 'pointer-events-none opacity-50',
33+
false: '',
34+
},
35+
},
36+
defaultVariants: {
37+
variant: 'default',
38+
size: 'md',
39+
disabled: false,
40+
},
41+
},
42+
);
43+
44+
const iconSizeVariants = cva('', {
45+
variants: {
46+
size: {
47+
sm: 'h-5 w-5 rounded-md',
48+
md: 'h-6 w-6 rounded-lg',
49+
lg: 'h-7 w-7 rounded-xl',
50+
},
51+
},
52+
defaultVariants: {
53+
size: 'md',
54+
},
55+
});
56+
57+
interface ChipProps
58+
extends React.HTMLAttributes<HTMLDivElement>,
59+
VariantProps<typeof chipVariants> {
60+
onDelete?: () => void;
61+
icon?: React.ReactNode;
62+
}
63+
64+
function Chip({
65+
children,
66+
variant,
67+
size,
68+
disabled,
69+
onDelete,
70+
icon,
71+
className,
72+
...props
73+
}: ChipProps) {
74+
const handleChipClick = (e: React.MouseEvent) => {
75+
e.stopPropagation();
76+
e.preventDefault();
77+
};
78+
79+
const handleDeleteClick = (e: React.MouseEvent) => {
80+
e.stopPropagation();
81+
e.preventDefault();
82+
onDelete?.();
83+
};
84+
85+
return (
86+
<div
87+
className={cn(chipVariants({ variant, size, disabled }), className)}
88+
{...props}
89+
data-slot="chip"
90+
onClick={handleChipClick}
91+
>
92+
{icon && <span className="shrink-0">{icon}</span>}
93+
<span className="pointer-events-none">{children}</span>
94+
{onDelete && (
95+
<Button
96+
variant="ghost"
97+
size="icon"
98+
onClick={handleDeleteClick}
99+
className={cn(
100+
chipVariants({ variant, disabled }),
101+
iconSizeVariants({ size }),
102+
'theme-default:shadow-none border-none p-1',
103+
)}
104+
disabled={!!disabled}
105+
>
106+
<X className={iconSizeVariants({ size })} />
107+
</Button>
108+
)}
109+
</div>
110+
);
111+
}
112+
113+
export { Chip };

packages/react/src/components/ui/color-picker.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,9 +651,10 @@ export const ColorPickerInput = ({
651651
)}
652652
{showColorPicker && (
653653
<Popover open={open} onOpenChange={handlePickerOpenChange}>
654-
<PopoverTrigger ref={triggerRef} asChild>
654+
<PopoverTrigger ref={triggerRef}>
655655
<div className="sr-only" />
656656
</PopoverTrigger>
657+
657658
<PopoverContent
658659
className="w-full max-w-72 p-0"
659660
align="start"

0 commit comments

Comments
 (0)