implement datatable sorting on non-analytics tables. (website/board/link/pixel/team/admin)

This commit is contained in:
Francis Cao committed 2026-05-07 10:46:03 -07:00
1 parent b5c4dbfa56
commit 5ccfb6d0a5
39 files changed
+322 -100

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ export function DataGrid({
const { data, error, isLoading, isFetching } = query;
const { router, updateParams, query: queryParams } = useNavigation();
const [search, setSearch] = useState(queryParams?.search || data?.search || '');
const showPager = allowPaging && data && (data.count > data.pageSize || data.isCapped);
const showPager = allowPaging && data && data.count > 0;
const { isMobile } = useMobile();
const displayMode = isMobile ? 'cards' : undefined;
+39 -26
View File
@@ -11,11 +11,18 @@ export interface PagerProps {
className?: string;
}
export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerProps) {
export function Pager({
page,
pageSize,
count,
isCapped,
onPageChange,
}: PagerProps) {
const { t, labels } = useMessages();
const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0;
const lastPage = page === maxPage;
const firstPage = page === 1;
const showNavigation = maxPage > 1 || isCapped;
if (count === 0 || !maxPage) {
return null;
@@ -29,34 +36,40 @@ export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerPr
}
};
if (maxPage === 1 && !isCapped) {
return null;
}
const displayCount = isCapped ? `10,000+` : (+count).toLocaleString();
return (
<Row alignItems="center" justifyContent="space-between" gap="3" flexGrow={1}>
<Text>{t(labels.numberOfRecords, { x: displayCount })}</Text>
<Row alignItems="center" justifyContent="flex-end" gap="3">
<Text>
{t(labels.pageOf, {
current: page.toLocaleString(),
total: maxPage.toLocaleString(),
})}
</Text>
<Row gap="1">
<Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
<Icon size="sm" rotate={180}>
<ChevronRight />
</Icon>
</Button>
<Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}>
<Icon size="sm">
<ChevronRight />
</Icon>
</Button>
</Row>
<Row alignItems="center" justifyContent="space-between" gap="3" flexGrow={1} wrap="wrap">
<Text color="muted">{t(labels.numberOfRecords, { x: displayCount })}</Text>
<Row
alignItems="center"
justifyContent="flex-end"
gap="3"
wrap="nowrap"
style={{ whiteSpace: 'nowrap' }}
>
{showNavigation && (
<>
<Text color="muted">
{t(labels.pageOf, {
current: page.toLocaleString(),
total: maxPage.toLocaleString(),
})}
</Text>
<Row gap="1">
<Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
<Icon size="sm" rotate={180}>
<ChevronRight />
</Icon>
</Button>
<Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}>
<Icon size="sm">
<ChevronRight />
</Icon>
</Button>
</Row>
</>
)}
</Row>
</Row>
);
+94
View File
@@ -0,0 +1,94 @@
'use client';
import type { ReactNode } from 'react';
import { Icon } from '@umami/react-zen';
import { ChevronDown, ChevronUp } from '@/components/icons';
import { useNavigation } from '@/components/hooks';
type SortDirection = 'asc' | 'desc';
export interface SortableLabelProps {
label: ReactNode;
sortKey: string;
defaultDirection?: SortDirection;
}
export function SortableLabel({
label,
sortKey,
defaultDirection = 'asc',
}: SortableLabelProps) {
const { router, query, updateParams } = useNavigation();
const isActive = query.orderBy === sortKey;
const isDescending = query.sortDescending === 'true';
const direction = isActive ? (isDescending ? 'desc' : 'asc') : undefined;
const activeColor = 'var(--text-primary)';
const getNextDirection = (): SortDirection => {
if (!isActive) {
return defaultDirection;
}
return direction === 'desc' ? 'asc' : 'desc';
};
const handleSort = () => {
const nextDirection = getNextDirection();
router.push(
updateParams({
orderBy: sortKey,
sortDescending: nextDirection === 'desc' ? 'true' : undefined,
page: 1,
}),
);
};
return (
<button
type="button"
onClick={handleSort}
className="inline-flex appearance-none items-center gap-1 border-0 bg-transparent p-0 text-inherit"
aria-pressed={isActive}
>
<span>{label}</span>
<span
aria-hidden
style={{
display: 'inline-flex',
flexDirection: 'column',
gap: 0,
lineHeight: 0,
color: 'var(--text-muted)',
opacity: 0.8,
}}
>
<span
style={{
color: direction === 'asc' ? activeColor : undefined,
opacity: direction === 'asc' ? 1 : 0.55,
transform: 'scale(0.9)',
transformOrigin: 'center',
}}
>
<Icon size="sm" color={direction === 'asc' ? undefined : 'muted'}>
<ChevronUp />
</Icon>
</span>
<span
style={{
color: direction === 'desc' ? activeColor : undefined,
opacity: direction === 'desc' ? 1 : 0.55,
marginTop: '-6px',
transform: 'scale(0.9)',
transformOrigin: 'center',
}}
>
<Icon size="sm" color={direction === 'desc' ? undefined : 'muted'}>
<ChevronDown />
</Icon>
</span>
</span>
</button>
);
}
@@ -1,14 +1,15 @@
import { useApi } from '../useApi';
import { useModified } from '../useModified';
import { usePagedQuery } from '../usePagedQuery';
export function useUserTeamsQuery(userId: string) {
const { get, useQuery } = useApi();
const { get } = useApi();
const { modified } = useModified(`teams`);
return useQuery({
return usePagedQuery({
queryKey: ['teams', { userId, modified }],
queryFn: () => {
return get(`/users/${userId}/teams`);
queryFn: params => {
return get(`/users/${userId}/teams`, params);
},
enabled: !!userId,
});
+4 -2
View File
@@ -3,7 +3,7 @@ import { useNavigation } from './useNavigation';
export function usePageParameters() {
const {
query: { page, pageSize, search },
query: { page, pageSize, search, orderBy, sortDescending },
} = useNavigation();
return useMemo(() => {
@@ -11,6 +11,8 @@ export function usePageParameters() {
page,
pageSize,
search,
orderBy,
sortDescending,
};
}, [page, pageSize, search]);
}, [orderBy, page, pageSize, search, sortDescending]);
}
+3 -3
View File
@@ -15,13 +15,13 @@ export function usePagedQuery<TData = any, TError = Error>({
queryFn: (params?: object) => Promise<PageResult<TData>> | PageResult<TData>;
}): UseQueryResult<PageResult<TData>, TError> {
const {
query: { page, search },
query: { page, search, orderBy, sortDescending },
} = useNavigation();
const { useQuery } = useApi();
return useQuery<PageResult<TData>, TError>({
queryKey: [...queryKey, page, search] as const,
queryFn: () => queryFn({ page, search }),
queryKey: [...queryKey, page, search, orderBy, sortDescending] as const,
queryFn: () => queryFn({ page, search, orderBy, sortDescending }),
...options,
});
}