feat: add server-side table sorting
This commit is contained in:
@@ -81,6 +81,7 @@ export function useClientColumns({
|
|||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <div className="text-right">Actions</div>,
|
header: () => <div className="text-right">Actions</div>,
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const client = row.original;
|
const client = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import type { Client } from '@/types';
|
import type { Client } from '@/types';
|
||||||
@@ -14,8 +14,10 @@ interface ClientTableProps {
|
|||||||
skip: number;
|
skip: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
sorting: SortingState;
|
||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClientTable({
|
export function ClientTable({
|
||||||
@@ -26,8 +28,10 @@ export function ClientTable({
|
|||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
sorting,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
|
onSortingChange,
|
||||||
}: ClientTableProps) {
|
}: ClientTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -44,6 +48,8 @@ export function ClientTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
}}
|
}}
|
||||||
|
sorting={sorting}
|
||||||
|
onSortingChange={onSortingChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { useDebounce } from '@/hooks/useDebounce';
|
import { useDebounce } from '@/hooks/useDebounce';
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ export type ClientStatusFilter = 'all' | 'active' | 'inactive';
|
|||||||
export function useClientFilters() {
|
export function useClientFilters() {
|
||||||
const [skip, setSkip] = useState(0);
|
const [skip, setSkip] = useState(0);
|
||||||
const [limit, setLimitValue] = useState(10);
|
const [limit, setLimitValue] = useState(10);
|
||||||
|
const [sorting, setSortingValue] = useState<SortingState>([]);
|
||||||
const [searchTerm, setSearchTermValue] = useState('');
|
const [searchTerm, setSearchTermValue] = useState('');
|
||||||
const [statusFilter, setStatusFilterValue] = useState<ClientStatusFilter>('all');
|
const [statusFilter, setStatusFilterValue] = useState<ClientStatusFilter>('all');
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400);
|
const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400);
|
||||||
@@ -28,11 +30,18 @@ export function useClientFilters() {
|
|||||||
setSkip(0);
|
setSkip(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setSorting = useCallback((value: SortingState) => {
|
||||||
|
setSortingValue(value);
|
||||||
|
setSkip(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { getServerSortParams } from '@/components/data-table/sorting';
|
||||||
import { clientService } from '@/services/api';
|
import { clientService } from '@/services/api';
|
||||||
import type { ClientListParams } from '@/types';
|
import type { ClientListParams } from '@/types';
|
||||||
import { clientKeys } from '../queries/clientKeys';
|
import { clientKeys } from '../queries/clientKeys';
|
||||||
@@ -13,6 +15,7 @@ interface UseClientsQueryParams {
|
|||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
statusFilter: ClientStatusFilter;
|
statusFilter: ClientStatusFilter;
|
||||||
|
sorting: SortingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildClientListParams({
|
function buildClientListParams({
|
||||||
@@ -20,22 +23,22 @@ function buildClientListParams({
|
|||||||
limit,
|
limit,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
}: UseClientsQueryParams): ClientListParams {
|
}: UseClientsQueryParams): ClientListParams {
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
search_term: searchTerm || undefined,
|
search_term: searchTerm || undefined,
|
||||||
is_active: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
is_active: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
||||||
sort_by: 'created_at',
|
...getServerSortParams(sorting),
|
||||||
sort_order: 'desc',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useClientsQuery(params: UseClientsQueryParams) {
|
export function useClientsQuery(params: UseClientsQueryParams) {
|
||||||
const { skip, limit, searchTerm, statusFilter } = params;
|
const { skip, limit, searchTerm, statusFilter, sorting } = params;
|
||||||
const listParams = useMemo(
|
const listParams = useMemo(
|
||||||
() => buildClientListParams({ skip, limit, searchTerm, statusFilter }),
|
() => buildClientListParams({ skip, limit, searchTerm, statusFilter, sorting }),
|
||||||
[limit, searchTerm, skip, statusFilter],
|
[limit, searchTerm, skip, sorting, statusFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export default function ClientsPage() {
|
|||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
@@ -38,6 +40,7 @@ export default function ClientsPage() {
|
|||||||
limit,
|
limit,
|
||||||
searchTerm: debouncedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
});
|
});
|
||||||
const clientForm = useClientForm({
|
const clientForm = useClientForm({
|
||||||
onSaved: () => setIsSheetOpen(false),
|
onSaved: () => setIsSheetOpen(false),
|
||||||
@@ -112,8 +115,10 @@ export default function ClientsPage() {
|
|||||||
skip={skip}
|
skip={skip}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
total={total}
|
total={total}
|
||||||
|
sorting={sorting}
|
||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
|
onSortingChange={setSorting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PoweredBy />
|
<PoweredBy />
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export function usePlanColumns({
|
|||||||
{
|
{
|
||||||
id: 'limits',
|
id: 'limits',
|
||||||
header: 'Limits',
|
header: 'Limits',
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
<p>{row.original.max_projects} projects</p>
|
<p>{row.original.max_projects} projects</p>
|
||||||
@@ -96,6 +97,7 @@ export function usePlanColumns({
|
|||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <div className="text-right">Actions</div>,
|
header: () => <div className="text-right">Actions</div>,
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const plan = row.original;
|
const plan = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import type { Plan } from '@/types';
|
import type { Plan } from '@/types';
|
||||||
@@ -14,8 +14,10 @@ interface PlanTableProps {
|
|||||||
skip: number;
|
skip: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
sorting: SortingState;
|
||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlanTable({
|
export function PlanTable({
|
||||||
@@ -26,8 +28,10 @@ export function PlanTable({
|
|||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
sorting,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
|
onSortingChange,
|
||||||
}: PlanTableProps) {
|
}: PlanTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -44,6 +48,8 @@ export function PlanTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
}}
|
}}
|
||||||
|
sorting={sorting}
|
||||||
|
onSortingChange={onSortingChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
import { useDebounce } from '@/hooks/useDebounce';
|
import { useDebounce } from '@/hooks/useDebounce';
|
||||||
|
|
||||||
export type PlanStatusFilter = 'all' | 'active' | 'inactive';
|
export type PlanStatusFilter = 'all' | 'active' | 'inactive';
|
||||||
@@ -8,6 +9,7 @@ export type PlanStatusFilter = 'all' | 'active' | 'inactive';
|
|||||||
export function usePlanFilters() {
|
export function usePlanFilters() {
|
||||||
const [skip, setSkip] = useState(0);
|
const [skip, setSkip] = useState(0);
|
||||||
const [limit, setLimitValue] = useState(10);
|
const [limit, setLimitValue] = useState(10);
|
||||||
|
const [sorting, setSortingValue] = useState<SortingState>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState<PlanStatusFilter>('all');
|
const [statusFilter, setStatusFilter] = useState<PlanStatusFilter>('all');
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm, 400);
|
const debouncedSearchTerm = useDebounce(searchTerm, 400);
|
||||||
@@ -27,11 +29,18 @@ export function usePlanFilters() {
|
|||||||
setSkip(0);
|
setSkip(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSorting = (value: SortingState) => {
|
||||||
|
setSortingValue(value);
|
||||||
|
setSkip(0);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm: updateSearchTerm,
|
setSearchTerm: updateSearchTerm,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { getServerSortParams } from '@/components/data-table/sorting';
|
||||||
import { mapPermissionTree } from '@/components/permission-tree';
|
import { mapPermissionTree } from '@/components/permission-tree';
|
||||||
import { permissionService, planService } from '@/services/api';
|
import { permissionService, planService } from '@/services/api';
|
||||||
import type { PlanListParams } from '@/types';
|
import type { PlanListParams } from '@/types';
|
||||||
@@ -15,6 +17,7 @@ interface UsePlansQueryParams {
|
|||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
statusFilter: PlanStatusFilter;
|
statusFilter: PlanStatusFilter;
|
||||||
|
sorting: SortingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPlanListParams({
|
function buildPlanListParams({
|
||||||
@@ -22,14 +25,14 @@ function buildPlanListParams({
|
|||||||
limit,
|
limit,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
}: UsePlansQueryParams): PlanListParams {
|
}: UsePlansQueryParams): PlanListParams {
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
search_term: searchTerm || undefined,
|
search_term: searchTerm || undefined,
|
||||||
is_active: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
is_active: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
||||||
sort_by: 'created_at',
|
...getServerSortParams(sorting),
|
||||||
sort_order: 'desc',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default function PlansPage() {
|
|||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
@@ -40,6 +42,7 @@ export default function PlansPage() {
|
|||||||
limit,
|
limit,
|
||||||
searchTerm: debouncedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
});
|
});
|
||||||
const permissionsQuery = useOrganizationPermissionTreeQuery(isSheetOpen);
|
const permissionsQuery = useOrganizationPermissionTreeQuery(isSheetOpen);
|
||||||
|
|
||||||
@@ -166,8 +169,10 @@ export default function PlansPage() {
|
|||||||
skip={skip}
|
skip={skip}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
total={total}
|
total={total}
|
||||||
|
sorting={sorting}
|
||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
|
onSortingChange={setSorting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PoweredBy />
|
<PoweredBy />
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function useRoleColumns({
|
|||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <div className="text-right">Actions</div>,
|
header: () => <div className="text-right">Actions</div>,
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const role = row.original;
|
const role = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
@@ -14,8 +14,10 @@ interface RoleTableProps {
|
|||||||
skip: number;
|
skip: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
sorting: SortingState;
|
||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoleTable({
|
export function RoleTable({
|
||||||
@@ -26,8 +28,10 @@ export function RoleTable({
|
|||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
sorting,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
|
onSortingChange,
|
||||||
}: RoleTableProps) {
|
}: RoleTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -44,6 +48,8 @@ export function RoleTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
}}
|
}}
|
||||||
|
sorting={sorting}
|
||||||
|
onSortingChange={onSortingChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
export type StatusFilter = 'all' | 'active' | 'inactive';
|
export type StatusFilter = 'all' | 'active' | 'inactive';
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ const SEARCH_DEBOUNCE_MS = 400;
|
|||||||
export function useRoleFilters() {
|
export function useRoleFilters() {
|
||||||
const [skip, setSkip] = useState(0);
|
const [skip, setSkip] = useState(0);
|
||||||
const [limit, setLimitValue] = useState(10);
|
const [limit, setLimitValue] = useState(10);
|
||||||
|
const [sorting, setSortingValue] = useState<SortingState>([]);
|
||||||
const [searchTerm, setSearchTermValue] = useState('');
|
const [searchTerm, setSearchTermValue] = useState('');
|
||||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilterValue] = useState<StatusFilter>('all');
|
const [statusFilter, setStatusFilterValue] = useState<StatusFilter>('all');
|
||||||
@@ -36,11 +38,18 @@ export function useRoleFilters() {
|
|||||||
setSkip(0);
|
setSkip(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSorting = (value: SortingState) => {
|
||||||
|
setSortingValue(value);
|
||||||
|
setSkip(0);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { getServerSortParams } from '@/components/data-table/sorting';
|
||||||
import { mapPermissionTree } from '@/components/permission-tree';
|
import { mapPermissionTree } from '@/components/permission-tree';
|
||||||
import { permissionService, roleService } from '@/services/api';
|
import { permissionService, roleService } from '@/services/api';
|
||||||
import type { Role, RoleListParams } from '@/types';
|
import type { Role, RoleListParams } from '@/types';
|
||||||
@@ -16,6 +18,7 @@ interface UseRolesQueryParams {
|
|||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
statusFilter: StatusFilter;
|
statusFilter: StatusFilter;
|
||||||
|
sorting: SortingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRoleListParams({
|
function buildRoleListParams({
|
||||||
@@ -23,22 +26,22 @@ function buildRoleListParams({
|
|||||||
limit,
|
limit,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
}: UseRolesQueryParams): RoleListParams {
|
}: UseRolesQueryParams): RoleListParams {
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
search_term: searchTerm || undefined,
|
search_term: searchTerm || undefined,
|
||||||
effective_status: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
effective_status: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
||||||
sort_by: 'created_at',
|
...getServerSortParams(sorting),
|
||||||
sort_order: 'desc',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRolesQuery(params: UseRolesQueryParams) {
|
export function useRolesQuery(params: UseRolesQueryParams) {
|
||||||
const { skip, limit, searchTerm, statusFilter } = params;
|
const { skip, limit, searchTerm, statusFilter, sorting } = params;
|
||||||
const listParams = useMemo(
|
const listParams = useMemo(
|
||||||
() => buildRoleListParams({ skip, limit, searchTerm, statusFilter }),
|
() => buildRoleListParams({ skip, limit, searchTerm, statusFilter, sorting }),
|
||||||
[limit, searchTerm, skip, statusFilter],
|
[limit, searchTerm, skip, sorting, statusFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export default function RolesPage() {
|
|||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
@@ -43,6 +45,7 @@ export default function RolesPage() {
|
|||||||
limit,
|
limit,
|
||||||
searchTerm: debouncedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
});
|
});
|
||||||
const permissionsQuery = usePermissionsTreeQuery(isSheetOpen);
|
const permissionsQuery = usePermissionsTreeQuery(isSheetOpen);
|
||||||
const roleForm = useRoleForm({
|
const roleForm = useRoleForm({
|
||||||
@@ -125,8 +128,10 @@ export default function RolesPage() {
|
|||||||
skip={skip}
|
skip={skip}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
total={total}
|
total={total}
|
||||||
|
sorting={sorting}
|
||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
|
onSortingChange={setSorting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PoweredBy />
|
<PoweredBy />
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export function useTenantColumns({
|
|||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <div className="text-right">Actions</div>,
|
header: () => <div className="text-right">Actions</div>,
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const tenant = row.original;
|
const tenant = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import type { Tenant } from '@/types';
|
import type { Tenant } from '@/types';
|
||||||
@@ -14,8 +14,10 @@ interface TenantTableProps {
|
|||||||
skip: number;
|
skip: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
sorting: SortingState;
|
||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TenantTable({
|
export function TenantTable({
|
||||||
@@ -26,8 +28,10 @@ export function TenantTable({
|
|||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
sorting,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
|
onSortingChange,
|
||||||
}: TenantTableProps) {
|
}: TenantTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -44,6 +48,8 @@ export function TenantTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
}}
|
}}
|
||||||
|
sorting={sorting}
|
||||||
|
onSortingChange={onSortingChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { useDebounce } from '@/hooks/useDebounce';
|
import { useDebounce } from '@/hooks/useDebounce';
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ export type TenantStatusFilter = 'all' | 'active' | 'inactive';
|
|||||||
export function useTenantFilters() {
|
export function useTenantFilters() {
|
||||||
const [skip, setSkip] = useState(0);
|
const [skip, setSkip] = useState(0);
|
||||||
const [limit, setLimitValue] = useState(10);
|
const [limit, setLimitValue] = useState(10);
|
||||||
|
const [sorting, setSortingValue] = useState<SortingState>([]);
|
||||||
const [searchTerm, setSearchTermValue] = useState('');
|
const [searchTerm, setSearchTermValue] = useState('');
|
||||||
const [statusFilter, setStatusFilterValue] = useState<TenantStatusFilter>('all');
|
const [statusFilter, setStatusFilterValue] = useState<TenantStatusFilter>('all');
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400);
|
const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400);
|
||||||
@@ -28,11 +30,18 @@ export function useTenantFilters() {
|
|||||||
setSkip(0);
|
setSkip(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setSorting = useCallback((value: SortingState) => {
|
||||||
|
setSortingValue(value);
|
||||||
|
setSkip(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { getServerSortParams } from '@/components/data-table/sorting';
|
||||||
import { clientService, planService, tenantService } from '@/services/api';
|
import { clientService, planService, tenantService } from '@/services/api';
|
||||||
import type { TenantListParams } from '@/types';
|
import type { TenantListParams } from '@/types';
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ interface UseTenantsQueryParams {
|
|||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
statusFilter: TenantStatusFilter;
|
statusFilter: TenantStatusFilter;
|
||||||
|
sorting: SortingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientLookupParams = {
|
const clientLookupParams = {
|
||||||
@@ -37,14 +40,14 @@ function buildTenantListParams({
|
|||||||
limit,
|
limit,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
}: UseTenantsQueryParams): TenantListParams {
|
}: UseTenantsQueryParams): TenantListParams {
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
search_term: searchTerm || undefined,
|
search_term: searchTerm || undefined,
|
||||||
is_active: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
is_active: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
||||||
sort_by: 'created_at',
|
...getServerSortParams(sorting),
|
||||||
sort_order: 'desc',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export default function TenantsPage() {
|
|||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
@@ -43,6 +45,7 @@ export default function TenantsPage() {
|
|||||||
limit,
|
limit,
|
||||||
searchTerm: debouncedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
});
|
});
|
||||||
const clientsLookupQuery = useClientLookupQuery(isSheetOpen);
|
const clientsLookupQuery = useClientLookupQuery(isSheetOpen);
|
||||||
const plansLookupQuery = usePlanLookupQuery(isSheetOpen);
|
const plansLookupQuery = usePlanLookupQuery(isSheetOpen);
|
||||||
@@ -163,8 +166,10 @@ export default function TenantsPage() {
|
|||||||
skip={skip}
|
skip={skip}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
total={total}
|
total={total}
|
||||||
|
sorting={sorting}
|
||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
|
onSortingChange={setSorting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PoweredBy />
|
<PoweredBy />
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function useUserColumns({
|
|||||||
{
|
{
|
||||||
accessorKey: 'roles',
|
accessorKey: 'roles',
|
||||||
header: 'Roles',
|
header: 'Roles',
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{row.original.roles?.length ? (
|
{row.original.roles?.length ? (
|
||||||
@@ -93,6 +94,7 @@ export function useUserColumns({
|
|||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <div className="text-right">Actions</div>,
|
header: () => <div className="text-right">Actions</div>,
|
||||||
|
enableSorting: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const user = row.original;
|
const user = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import type { AdministrationUser } from '@/types';
|
import type { AdministrationUser } from '@/types';
|
||||||
@@ -14,8 +14,10 @@ interface UserTableProps {
|
|||||||
skip: number;
|
skip: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
sorting: SortingState;
|
||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number) => void;
|
||||||
onLimitChange: (limit: number) => void;
|
onLimitChange: (limit: number) => void;
|
||||||
|
onSortingChange: (sorting: SortingState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserTable({
|
export function UserTable({
|
||||||
@@ -26,8 +28,10 @@ export function UserTable({
|
|||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
sorting,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
|
onSortingChange,
|
||||||
}: UserTableProps) {
|
}: UserTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -44,6 +48,8 @@ export function UserTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
}}
|
}}
|
||||||
|
sorting={sorting}
|
||||||
|
onSortingChange={onSortingChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useDebounce } from '@/hooks/useDebounce';
|
import { useDebounce } from '@/hooks/useDebounce';
|
||||||
import type { UserStatus } from '@/types';
|
import type { UserStatus } from '@/types';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
export type StatusFilter = 'all' | UserStatus;
|
export type StatusFilter = 'all' | UserStatus;
|
||||||
@@ -9,6 +10,7 @@ export type StatusFilter = 'all' | UserStatus;
|
|||||||
export function useUserFilters() {
|
export function useUserFilters() {
|
||||||
const [skip, setSkip] = useState(0);
|
const [skip, setSkip] = useState(0);
|
||||||
const [limit, setLimitValue] = useState(10);
|
const [limit, setLimitValue] = useState(10);
|
||||||
|
const [sorting, setSortingValue] = useState<SortingState>([]);
|
||||||
const [searchTerm, setSearchTermValue] = useState('');
|
const [searchTerm, setSearchTermValue] = useState('');
|
||||||
const [statusFilter, setStatusFilterValue] = useState<StatusFilter>('all');
|
const [statusFilter, setStatusFilterValue] = useState<StatusFilter>('all');
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400);
|
const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400);
|
||||||
@@ -28,11 +30,18 @@ export function useUserFilters() {
|
|||||||
setSkip(0);
|
setSkip(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setSorting = useCallback((value: SortingState) => {
|
||||||
|
setSortingValue(value);
|
||||||
|
setSkip(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { getServerSortParams } from '@/components/data-table/sorting';
|
||||||
import { userService } from '@/services/api';
|
import { userService } from '@/services/api';
|
||||||
import type { UserListParams } from '@/types';
|
import type { UserListParams } from '@/types';
|
||||||
import { userKeys } from '../queries/userKeys';
|
import { userKeys } from '../queries/userKeys';
|
||||||
@@ -13,6 +15,7 @@ interface UseUsersQueryParams {
|
|||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
statusFilter: StatusFilter;
|
statusFilter: StatusFilter;
|
||||||
|
sorting: SortingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUserListParams({
|
function buildUserListParams({
|
||||||
@@ -20,22 +23,22 @@ function buildUserListParams({
|
|||||||
limit,
|
limit,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
}: UseUsersQueryParams): UserListParams {
|
}: UseUsersQueryParams): UserListParams {
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
search_term: searchTerm || undefined,
|
search_term: searchTerm || undefined,
|
||||||
effective_status: statusFilter === 'all' ? undefined : statusFilter,
|
effective_status: statusFilter === 'all' ? undefined : statusFilter,
|
||||||
sort_by: 'created_at',
|
...getServerSortParams(sorting),
|
||||||
sort_order: 'desc',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUsersQuery(params: UseUsersQueryParams) {
|
export function useUsersQuery(params: UseUsersQueryParams) {
|
||||||
const { skip, limit, searchTerm, statusFilter } = params;
|
const { skip, limit, searchTerm, statusFilter, sorting } = params;
|
||||||
const listParams = useMemo(
|
const listParams = useMemo(
|
||||||
() => buildUserListParams({ skip, limit, searchTerm, statusFilter }),
|
() => buildUserListParams({ skip, limit, searchTerm, statusFilter, sorting }),
|
||||||
[limit, searchTerm, skip, statusFilter],
|
[limit, searchTerm, skip, sorting, statusFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export default function UsersPage() {
|
|||||||
setSkip,
|
setSkip,
|
||||||
limit,
|
limit,
|
||||||
setLimit,
|
setLimit,
|
||||||
|
sorting,
|
||||||
|
setSorting,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
debouncedSearchTerm,
|
debouncedSearchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
@@ -39,6 +41,7 @@ export default function UsersPage() {
|
|||||||
limit,
|
limit,
|
||||||
searchTerm: debouncedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
sorting,
|
||||||
});
|
});
|
||||||
const userForm = useUserForm({
|
const userForm = useUserForm({
|
||||||
onSaved: () => setIsSheetOpen(false),
|
onSaved: () => setIsSheetOpen(false),
|
||||||
@@ -128,8 +131,10 @@ export default function UsersPage() {
|
|||||||
skip={skip}
|
skip={skip}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
total={total}
|
total={total}
|
||||||
|
sorting={sorting}
|
||||||
onPageChange={setSkip}
|
onPageChange={setSkip}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={setLimit}
|
||||||
|
onSortingChange={setSorting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PoweredBy />
|
<PoweredBy />
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { flexRender } from '@tanstack/react-table';
|
import { flexRender } from '@tanstack/react-table';
|
||||||
import { TableHead, TableHeader as ShadTableHeader, TableRow } from '@/components/ui/table';
|
import { TableHead, TableHeader as ShadTableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Table } from '@tanstack/react-table';
|
import { Table } from '@tanstack/react-table';
|
||||||
|
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
||||||
return (
|
return (
|
||||||
@@ -9,14 +11,38 @@ const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
|||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
|
const canSort = header.column.getCanSort();
|
||||||
|
const sortDirection = header.column.getIsSorted();
|
||||||
|
const SortIcon =
|
||||||
|
sortDirection === 'asc'
|
||||||
|
? ArrowUpNarrowWide
|
||||||
|
: sortDirection === 'desc'
|
||||||
|
? ArrowDownWideNarrow
|
||||||
|
: ArrowUpDown;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className="h-10 bg-card/80 px-2 text-foreground backdrop-blur-xl"
|
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
||||||
|
className={cn(
|
||||||
|
'h-10 bg-card/80 px-2 text-foreground backdrop-blur-xl transition-colors',
|
||||||
|
canSort && 'cursor-pointer select-none hover:bg-muted/60',
|
||||||
|
sortDirection && 'bg-muted/70',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder ? null : (
|
{header.isPlaceholder ? null : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex min-h-8 w-full items-center justify-between gap-2">
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
<span className="truncate">
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</span>
|
||||||
|
{canSort ? (
|
||||||
|
<SortIcon
|
||||||
|
className={cn(
|
||||||
|
'size-3.5 shrink-0 transition-colors',
|
||||||
|
sortDirection ? 'text-foreground' : 'text-muted-foreground/60',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export interface DataTableProps<TData, TValue> {
|
|||||||
onPageChange: (newSkip: number) => void;
|
onPageChange: (newSkip: number) => void;
|
||||||
onLimitChange: (newLimit: number) => void;
|
onLimitChange: (newLimit: number) => void;
|
||||||
};
|
};
|
||||||
|
sorting?: SortingState;
|
||||||
|
onSortingChange?: (sorting: SortingState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
@@ -53,9 +55,24 @@ export function DataTable<TData, TValue>({
|
|||||||
emptyTitle = 'No results found.',
|
emptyTitle = 'No results found.',
|
||||||
emptyDescription = 'Try adjusting your filters or search terms.',
|
emptyDescription = 'Try adjusting your filters or search terms.',
|
||||||
pagination,
|
pagination,
|
||||||
|
sorting: controlledSorting,
|
||||||
|
onSortingChange,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [internalSorting, setInternalSorting] = React.useState<SortingState>([]);
|
||||||
|
const sorting = controlledSorting ?? internalSorting;
|
||||||
|
const isManualSorting = Boolean(onSortingChange);
|
||||||
|
const handleSortingChange = React.useCallback(
|
||||||
|
(updater: SortingState | ((old: SortingState) => SortingState)) => {
|
||||||
|
const nextSorting = typeof updater === 'function' ? updater(sorting) : updater;
|
||||||
|
if (onSortingChange) {
|
||||||
|
onSortingChange(nextSorting);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInternalSorting(nextSorting);
|
||||||
|
},
|
||||||
|
[onSortingChange, sorting],
|
||||||
|
);
|
||||||
|
|
||||||
const columns = React.useMemo(() => {
|
const columns = React.useMemo(() => {
|
||||||
const cols: ColumnDef<TData, TValue>[] = [...initialColumns];
|
const cols: ColumnDef<TData, TValue>[] = [...initialColumns];
|
||||||
@@ -105,10 +122,11 @@ export function DataTable<TData, TValue>({
|
|||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
onSortingChange: setSorting,
|
onSortingChange: handleSortingChange,
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
manualPagination: !!pagination,
|
manualPagination: !!pagination,
|
||||||
|
manualSorting: isManualSorting,
|
||||||
pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1,
|
pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1,
|
||||||
state: {
|
state: {
|
||||||
rowSelection,
|
rowSelection,
|
||||||
|
|||||||
14
src/components/data-table/sorting.ts
Normal file
14
src/components/data-table/sorting.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
export function getServerSortParams(sorting: SortingState) {
|
||||||
|
const activeSort = sorting[0];
|
||||||
|
|
||||||
|
if (!activeSort) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sort_by: activeSort.id,
|
||||||
|
sort_order: activeSort.desc ? 'desc' : 'asc',
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user