revamp template

This commit is contained in:
fro1991
2025-02-01 16:44:51 +07:00
parent c432df568a
commit 276a289580
1212 changed files with 112762 additions and 0 deletions

View File

@ -0,0 +1,67 @@
import { ReactNode } from 'react';
import { DataGridInner, DataGridProvider } from '.';
import {
ColumnFiltersState,
RowSelectionState,
SortingState,
Table,
TableOptions
} from '@tanstack/react-table';
export type TDataGridLayoutCellSpacing = 'xs' | 'md' | 'sm' | 'lg';
export type TDataGridSelectedRowIds = Set<string>;
export type TDataGridRequestParams = {
pageIndex: number;
pageSize: number;
sorting?: SortingState;
columnFilters?: ColumnFiltersState;
};
export interface TDataGridProps<TData extends object> {
columns: any[];
data?: TData[];
rowSelection?: boolean;
getRowId?: TableOptions<TData>['getRowId'];
onRowSelectionChange?: (state: RowSelectionState, table?: Table<TData>) => void;
messages?: {
loading?: ReactNode | string;
empty?: ReactNode | string;
};
layout?: {
cellSpacing?: TDataGridLayoutCellSpacing;
cellBorder?: boolean;
card?: boolean;
classes?: {
table?: '';
container?: '';
root?: '';
};
};
pagination?: {
page?: number;
size?: number;
sizes?: number[];
sizesInfo?: string;
sizesLabel?: string;
sizesDescription?: string;
more?: boolean;
moreLimit?: number;
info?: string;
};
sorting?: { id: string; desc?: boolean }[];
toolbar?: ReactNode;
filters?: { id: string; value: unknown }[];
serverSide?: boolean;
onFetchData?: (params: TDataGridRequestParams) => Promise<any>;
children?: ReactNode;
}
export const DataGrid = <TData extends object>(props: TDataGridProps<TData>) => {
return (
<DataGridProvider {...props}>
<DataGridInner />
</DataGridProvider>
);
};

View File

@ -0,0 +1,132 @@
import * as React from 'react';
import { Check, CirclePlus } from 'lucide-react';
import { Column } from '@tanstack/react-table';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Separator } from '@/components/ui/separator';
interface IDataGridColumnFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
options: {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
}[];
}
export function DataGridColumnFilter<TData, TValue>({
column,
title,
options
}: IDataGridColumnFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues();
const selectedValues = new Set(column?.getFilterValue() as string[]);
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="light" size="sm">
<CirclePlus className="size-4" />
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge variant="secondary" className="rounded-sm px-1 font-normal lg:hidden">
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge variant="secondary" className="rounded-sm px-1 font-normal">
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValues.has(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.value);
} else {
selectedValues.add(option.value);
}
const filterValues = Array.from(selectedValues);
column?.setFilterValue(filterValues.length ? filterValues : undefined);
}}
>
<div
className={cn(
'me-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<Check className={cn('h-4 w-4')} />
</div>
{option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
<span>{option.label}</span>
{facets?.get(option.value) && (
<span className="ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{facets.get(option.value)}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,130 @@
import { HTMLAttributes, ReactNode } from 'react';
import { ChevronsUpDown, ArrowUp, ArrowDown, EyeOff, Check } from 'lucide-react';
import { Column } from '@tanstack/react-table';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem
} from '@/components/ui/dropdown-menu';
interface IDataGridColumnHeader<TData, TValue> extends HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
title?: string;
filter?: ReactNode;
show?: string;
}
export function DataGridColumnHeader<TData, TValue>({
column,
title = '',
className,
filter,
show = 'true'
}: IDataGridColumnHeader<TData, TValue>) {
if (show == 'true') {
if (!filter && !column.getCanSort() && !column.getCanHide()) {
return <div className={cn(className)}>{title}</div>;
}
if (!filter && !column.getCanHide() && column.getCanSort()) {
return (
<Button
variant="ghost"
size="sm"
className={cn('-ms-3 h-8 data-[state=open]:bg-accent !ring-0 !ring-offset-0', className)}
onClick={() => {
// Determine the current sorting state
const isSorted = column.getIsSorted();
if (isSorted === 'asc') {
column.toggleSorting(true); // Switch to desc
} else if (isSorted === 'desc') {
column.clearSorting(); // Clear to unsorted
} else {
column.toggleSorting(false); // Switch to asc
}
}}
>
<span>{title}</span>
{column.getIsSorted() === 'desc' ? (
<ArrowDown className="!size-[0.825rem]" />
) : column.getIsSorted() === 'asc' ? (
<ArrowUp className="!size-[0.825rem]" />
) : (
<ChevronsUpDown className="!size-[0.825rem]" />
)}
</Button>
);
}
return (
<div className={cn('flex items-center space-x-2', className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
'-ms-3 h-8 data-[state=open]:bg-accent !ring-0 !ring-offset-0',
className
)}
>
<span className="text-sm">{title}</span>
{column.getIsSorted() === 'desc' ? (
<ArrowDown className="!size-[0.825rem]" />
) : column.getIsSorted() === 'asc' ? (
<ArrowUp className="!size-[0.825rem]" />
) : (
<ChevronsUpDown className="!size-[0.825rem]" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{filter && (
<>
<DropdownMenuLabel>{filter}</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
{column.getCanSort() && (
<>
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUp className="!size-[0.825rem] text-muted-foreground/90" />
<span className="grow">Asc</span>
{column.getIsSorted() === 'asc' && (
<Check className="size-4 text-muted-foreground/90" />
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDown className="!size-[0.825rem] text-muted-foreground/90" />
<span className="grow">Desc</span>
{column.getIsSorted() === 'desc' && (
<Check className="size-4 text-muted-foreground/90" />
)}
</DropdownMenuItem>
</>
)}
{column.getCanHide() && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOff className="!size-[0.825rem] text-muted-foreground/90" />
Hide
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
} else {
return '';
}
}

View File

@ -0,0 +1,51 @@
import { Table } from '@tanstack/react-table';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator
} from '@/components/ui/dropdown-menu';
import { KeenIcon } from '@/components/keenicons';
interface IDataGridColumnVisibilityProps<TData> {
table: Table<TData>;
hideTitle?: boolean;
}
export function DataGridColumnVisibility<TData>({
table,
hideTitle = false
}: IDataGridColumnVisibilityProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="light" size="sm">
<KeenIcon icon="setting-4" />
{!hideTitle && 'Columns'}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel className="font-medium">Toggle Columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter((column) => typeof column.accessorFn !== 'undefined' && column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.columnDef.meta?.headerTitle || column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,182 @@
'use client';
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
useReactTable,
ColumnFiltersState,
VisibilityState,
getFacetedRowModel,
getFacetedUniqueValues,
RowSelectionState,
OnChangeFn,
Table,
SortingState
} from '@tanstack/react-table';
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { DataGridInner } from './DataGridInner';
import { TDataGridProps, TDataGridRequestParams } from './DataGrid';
import { deepMerge, debounce } from '@/lib/helpers';
export interface IDataGridContextProps<TData extends object> {
props: TDataGridProps<TData>;
table: Table<TData>;
totalRows: number;
loading: boolean;
setLoading: (state: boolean) => void;
reload: () => void;
children?: React.ReactNode;
}
const DataGridContext = createContext<IDataGridContextProps<any> | undefined>(undefined);
export const useDataGrid = () => {
const context = useContext(DataGridContext);
if (!context) {
throw new Error('useDataGrid must be used within a DataGridProvider');
}
return context;
};
export const DataGridProvider = <TData extends object>(props: TDataGridProps<TData>) => {
const defaultValues: Partial<TDataGridProps<TData>> = {
messages: {
empty: 'No data available',
loading: 'Loading...'
},
layout: {
cellSpacing: 'xs',
cellBorder: true,
card: false
},
pagination: {
info: '{from} - {to} of {count}',
sizes: [5, 10, 25, 50, 100],
sizesLabel: 'Show',
sizesDescription: 'per page',
size: 5,
page: 0,
moreLimit: 5,
more: false
},
rowSelection: false,
serverSide: false
};
const mergedProps = deepMerge(defaultValues, props);
const [data, setData] = useState<TData[]>(mergedProps.data || []);
const [loading, setLoading] = useState<boolean>(false);
const [totalRows, setTotalRows] = useState<number>(
mergedProps.data ? mergedProps.data.length : 0
);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: props.pagination?.page ?? 0,
pageSize: props.pagination?.size ?? 5
});
const [rowSelection, setRowSelection] = useState(mergedProps.rowSelection);
const [sorting, setSorting] = useState<SortingState>(mergedProps.sorting ?? []);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const fetchServerSideData = useCallback(async () => {
if (loading || !mergedProps.onFetchData) return;
setLoading(true);
try {
const requestParams: TDataGridRequestParams = {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
sorting,
columnFilters
};
const { data, totalCount } = await mergedProps.onFetchData(requestParams);
setData(data || []);
setTotalRows(totalCount || 0);
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
setLoading(false);
}
}, [loading, pagination, sorting, columnFilters, mergedProps.onFetchData]);
const debouncedFetchData = debounce(fetchServerSideData, 100);
const loadData = () => {
if (mergedProps.serverSide) {
debouncedFetchData();
} else {
setLoading(true); // Show loading bar for local data
setData(mergedProps.data || []);
setTotalRows(mergedProps.data ? mergedProps.data.length : 0);
setLoading(false); // Hide loading bar after data is set
}
};
// Trigger debounced fetch for server-side data; load local data if serverSide is false
useEffect(() => {
loadData();
}, [pagination, sorting, columnFilters, mergedProps.data, mergedProps.serverSide]);
const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (updaterOrValue) => {
setRowSelection((prev: RowSelectionState) =>
typeof updaterOrValue === 'function' ? updaterOrValue(prev) : updaterOrValue
);
if (mergedProps.onRowSelectionChange) {
const newSelection =
typeof updaterOrValue === 'function' ? updaterOrValue(rowSelection) : updaterOrValue;
mergedProps.onRowSelectionChange(newSelection, table);
}
};
const table = useReactTable({
data,
columns: mergedProps.columns,
pageCount: mergedProps.serverSide ? Math.ceil(totalRows / pagination.pageSize) : undefined,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination
},
getRowId: mergedProps.getRowId || ((row, index) => String(index)),
enableRowSelection: mergedProps.rowSelection,
onRowSelectionChange: handleRowSelectionChange,
onSortingChange: (newSorting) => !loading && setSorting(newSorting),
onColumnFiltersChange: (newFilters) => !loading && setColumnFilters(newFilters),
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: (newPagination) => !loading && setPagination(newPagination),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
manualPagination: mergedProps.serverSide,
manualSorting: mergedProps.serverSide,
manualFiltering: mergedProps.serverSide
});
return (
<DataGridContext.Provider
value={{
props: mergedProps,
table,
totalRows,
loading,
setLoading,
reload: loadData
}}
>
{props.children ? props.children : <DataGridInner />}
</DataGridContext.Provider>
);
};

View File

@ -0,0 +1,16 @@
import { useDataGrid } from '.';
const DataGridEmpty = () => {
const { table, props } = useDataGrid();
const totalColumns = table.getAllColumns().length + (props.rowSelection ? 1 : 0);
return (
<tr>
<td colSpan={totalColumns} className="text-center text-muted-foreground py-6">
{props.messages?.empty || 'No data available'}
</td>
</tr>
);
};
export { DataGridEmpty };

View File

@ -0,0 +1,41 @@
import { cn } from '@/lib/utils';
import { useDataGrid, DataGridLoader, DataGridTable, DataGridPagination } from '.';
const DataGridInner = () => {
const { props, table, loading } = useDataGrid();
return (
<div
className={cn(
'grid',
props.layout?.card &&
`
card
[&>[data-container]]:border-x-0
[&>[data-container]]:rounded-none
[&>[data-container]>[data-table]>thead>tr>th:first-child]:px-5
[&>[data-container]>[data-table]>tbody>tr>td:first-child]:px-5
[&>[data-toolbar]]:p-5
[&>[data-pagination]]:px-5
[&>[data-pagination]]:py-3
`,
props.layout?.classes?.root
)}
>
{props.toolbar && props.toolbar}
<div
className={cn(
'relative w-full scrollable-x-auto border rounded-md',
props.layout?.classes?.container
)}
data-container
>
<DataGridTable />
{loading && <DataGridLoader />}
</div>
{table.getRowModel().rows.length > 0 && <DataGridPagination />}
</div>
);
};
export { DataGridInner };

View File

@ -0,0 +1,41 @@
import { cn } from '@/lib/utils';
import { useDataGrid, DataGridLoader, DataGridTable, DataGridPagination } from '.';
const DataGridInnerCard = () => {
const { props, table, loading } = useDataGrid();
return (
<div
className={cn(
'grid',
props.layout?.card &&
`
card border-0 shadow-none
[&>[data-container]]:border-x-0
[&>[data-container]]:rounded-none
[&>[data-container]>[data-table]>thead>tr>th:first-child]:px-0
[&>[data-container]>[data-table]>tbody>tr>td:first-child]:px-0
[&>[data-toolbar]]:p-0
[&>[data-pagination]]:px-0
[&>[data-pagination]]:py-2
`,
props.layout?.classes?.root
)}
>
{props.toolbar && props.toolbar}
<div
className={cn(
'relative w-full scrollable-x-auto border rounded-md',
props.layout?.classes?.container
)}
data-container
>
<DataGridTable />
{loading && <DataGridLoader />}
</div>
{table.getRowModel().rows.length > 0 && <DataGridPagination />}
</div>
);
};
export { DataGridInnerCard };

View File

@ -0,0 +1,34 @@
import React from 'react';
import { useDataGrid } from '.';
export const DataGridLoader = () => {
const { props } = useDataGrid();
return (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="text-muted-foreground bg-card flex items-center gap-2 px-4 py-2 font-medium leading-none text-sm border shadow-sm rounded-md">
<svg
className="animate-spin -ml-1 h-5 w-5 text-muted-foreground"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="3"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{props.messages?.loading}
</div>
</div>
);
};

View File

@ -0,0 +1,150 @@
import React from 'react';
import { useDataGrid } from '.';
import { ChevronRightIcon, ChevronLeftIcon } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
const DataGridPagination = () => {
const { table, totalRows, props } = useDataGrid();
const btnBaseClasses = 'size-7 p-0 text-[13px]';
const btnArrowClasses = btnBaseClasses + ' rtl:transform rtl:rotate-180';
const pageIndex = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
const from = pageIndex * pageSize + 1;
const to = Math.min((pageIndex + 1) * pageSize, totalRows);
// Replace placeholders in paginationInfo
const paginationInfo = props.pagination?.info
? props.pagination.info
.replace('{from}', from.toString())
.replace('{to}', to.toString())
.replace('{count}', totalRows.toString())
: `${from} - ${to} of ${totalRows}`;
// Pagination limit logic
const pageCount = table.getPageCount();
const paginationMoreLimit = props.pagination?.moreLimit || 5;
// Determine the start and end of the pagination group
const currentGroupStart = Math.floor(pageIndex / paginationMoreLimit) * paginationMoreLimit;
const currentGroupEnd = Math.min(currentGroupStart + paginationMoreLimit, pageCount);
// Render page buttons based on the current group
const renderPageButtons = () => {
const buttons = [];
for (let i = currentGroupStart; i < currentGroupEnd; i++) {
buttons.push(
<Button
key={i}
variant="ghost"
className={cn(btnBaseClasses, 'text-muted-foreground', {
'bg-accent text-accent-foreground': pageIndex === i
})}
onClick={() => table.setPageIndex(i)}
>
{i + 1}
</Button>
);
}
return buttons;
};
// Render a "previous" ellipsis button if there are previous pages to show
const renderEllipsisPrevButton = () => {
if (currentGroupStart > 0) {
return (
<Button
className={btnBaseClasses}
variant="ghost"
onClick={() => table.setPageIndex(currentGroupStart - 1)}
>
...
</Button>
);
}
return null; // No ellipsis needed if we're in the first group
};
// Render a "next" ellipsis button if there are more pages to show after the current group
const renderEllipsisNextButton = () => {
if (currentGroupEnd < pageCount) {
return (
<Button
className={btnBaseClasses}
variant="ghost"
onClick={() => table.setPageIndex(currentGroupEnd)}
>
...
</Button>
);
}
return null;
};
return (
<div
className="flex flex-col md:flex-row justify-between items-center gap-5 md:gap-4"
data-pagination
>
<div className="flex items-center space-x-2 order-2 md:order-1 pb-2 md:pb-0">
<div className="text-sm text-muted-foreground">Rows per page</div>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="w-[70px]" size="sm">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{props.pagination?.sizes?.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 order-1 md:order-2 pt-2 md:pt-0">
<div className="text-sm text-muted-foreground">{paginationInfo}</div>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
className={btnArrowClasses}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon className="size-4" />
</Button>
{renderEllipsisPrevButton()}
<>{renderPageButtons()}</>
{renderEllipsisNextButton()}
<Button
variant="ghost"
className={btnArrowClasses}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon className="size-4" />
</Button>
</div>
</div>
</div>
);
};
export { DataGridPagination };

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Checkbox } from '@/components/ui/checkbox';
import { Row } from '@tanstack/react-table';
export interface IDataGridRowSelectProps<TData> {
row: Row<TData>;
}
const DataGridRowSelect = <TData,>({ row }: IDataGridRowSelectProps<TData>) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="align-[inherit]"
/>
);
};
export { DataGridRowSelect };

View File

@ -0,0 +1,20 @@
import React from 'react';
import { useDataGrid } from '.';
import { Checkbox } from '@/components/ui/checkbox';
const DataGridRowSelectAll = () => {
const { table } = useDataGrid();
return (
<Checkbox
checked={
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="align-[inherit]"
/>
);
};
export { DataGridRowSelectAll };

View File

@ -0,0 +1,109 @@
import React from 'react';
import { DataGridEmpty, TDataGridLayoutCellSpacing } from '.';
import { flexRender, HeaderGroup, Row, Cell } from '@tanstack/react-table';
import { useDataGrid } from '.';
import { cn } from '@/lib/utils';
interface DataGridTableProps {
show?: boolean; // Menambahkan properti 'show' untuk kontrol visibilitas header
}
const DataGridTable = <TData,>({ show = true }: DataGridTableProps) => {
const { table, props } = useDataGrid();
const headCellSpacingOptions: Record<TDataGridLayoutCellSpacing, string> = {
xs: 'px-2.5',
sm: 'px-3',
md: 'px-4',
lg: 'px-6'
};
const bodyCellSpacingOptions: Record<TDataGridLayoutCellSpacing, string> = {
xs: 'p-2.5',
sm: 'p-3',
md: 'p-4',
lg: 'p-6'
};
const headCellSpacing = props.layout?.cellSpacing
? headCellSpacingOptions[props.layout?.cellSpacing]
: headCellSpacingOptions['md'];
const bodyCellSpacing = props.layout?.cellSpacing
? bodyCellSpacingOptions[props.layout?.cellSpacing]
: bodyCellSpacingOptions['md'];
const cellBorder = props.layout?.cellBorder ?? false;
return (
<table
className={cn(
'w-full align-middle text-left rtl:text-right caption-bottom text-sm',
props.layout?.classes?.table
)}
data-table
>
{/* Header tabel hanya dirender jika show=true */}
{show && (
<thead className="[&_tr]:border-b">
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
<tr
key={headerGroup.id}
className={cn(
'border-b bg-muted/30 data-[state=selected]:bg-muted',
cellBorder && '[&_>:last-child]:border-e-0'
)}
>
{headerGroup.headers.map((header) => (
<th
key={header.id}
colSpan={header.colSpan}
className={cn(
headCellSpacing,
cellBorder && 'border-e',
'h-12 text-left rtl:text-right align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pe-0',
header.column.columnDef.meta?.headerClassName
)}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
)}
{/* Body tabel */}
<tbody className="[&_tr:last-child]:border-0">
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row: Row<TData>) => (
<tr
key={row.id}
data-state={row.getIsSelected() ? 'selected' : undefined}
className={cn(
'border-b hover:bg-muted/30 data-[state=selected]:bg-muted/50',
cellBorder && '[&_>:last-child]:border-e-0'
)}
>
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className={cn(
bodyCellSpacing,
cellBorder && 'border-e',
'align-middle [&:has([role=checkbox])]:pe-0',
cell.column.columnDef.meta?.cellClassName
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))
) : (
<DataGridEmpty />
)}
</tbody>
</table>
);
};
export { DataGridTable };

View File

@ -0,0 +1,17 @@
import React, { ReactNode } from 'react';
import { cn } from '@/lib/utils';
export interface IDataGridToolbarProps {
children: ReactNode;
className?: string;
}
const DataGridToolbar = ({ children, className }: IDataGridToolbarProps) => {
return (
<div data-toolbar className={cn('flex items-center gap-2 justify-between', className)}>
{children}
</div>
);
};
export { DataGridToolbar };

View File

@ -0,0 +1,14 @@
export * from './DataGrid';
export * from './DataGridContext';
export * from './DataGridInner';
export * from './DataGridInnerCard';
export * from './DataGridToolbar';
export * from './DataGridTable';
export * from './DataGridPagination';
export * from './DataGridLoader';
export * from './DataGridEmpty';
export * from './DataGridColumnHeader';
export * from './DataGridColumnFilter';
export * from './DataGridColumnVisibility';
export * from './DataGridRowSelectAll';
export * from './DataGridRowSelect';

10
src/components/data-grid/types.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import { RowData } from '@tanstack/react-table';
declare module '@tanstack/react-table' {
// eslint-disable-next-line no-unused-vars
interface ColumnMeta<TData extends RowData, TValue> {
headerTitle?: string;
headerClassName?: string;
cellClassName?: string;
}
}