{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "plugin-table", "title": "Data Table (TanStack Table)", "description": "TanStack Table tabanlı veri tablosu. Sıralama, filtreleme, sayfalama, skeleton ve filtre section bileşenleri.", "dependencies": [ "@tanstack/react-table@8.21.3", "@tanstack/match-sorter-utils@8.19.4" ], "registryDependencies": [ "@tra-kit/skeleton", "@tra-kit/input", "@tra-kit/select", "@tra-kit/pagination" ], "files": [ { "path": "registry/tra-plugins/table/components/table/custom-table.tsx", "content": "import React, { useMemo, useState } from \"react\";\n\nimport {\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n getExpandedRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport type {\n ColumnDef,\n PaginationState,\n FilterFn,\n ExpandedState,\n ColumnFiltersState,\n VisibilityState,\n SortingState,\n RowData,\n} from \"@tanstack/react-table\";\nimport { rankItem } from \"@tanstack/match-sorter-utils\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nimport { useIsMobile } from \"@/hooks/use-mobile\";\nimport Pagination from \"@/components/ui/pagination\";\nimport CustomTableFilterSection from \"./custom-table-filter-section\";\n\ndeclare module \"@tanstack/react-table\" {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface ColumnMeta {\n headerClassName?: string;\n bodyClassName?: string;\n headerItemClassName?: string;\n }\n interface FilterFns {\n fuzzy: FilterFn;\n }\n}\n\nconst CustomTable = ({\n data,\n columns,\n sorting,\n setSorting,\n hidePagination = false,\n renderExpandedRow,\n tableClassName,\n tableWrapperClassName,\n searchText,\n setSearchText,\n rowClassName,\n bodyRowClassName,\n expandKey,\n containerClassName,\n headCellClassName,\n bodyCellClassName,\n expandRowContainerClassName,\n onlyExpanded = false,\n filterColumns,\n headClassName = \"\",\n onFilteredDataChange,\n defaultPageSize = { desktop: 10, mobile: 8 },\n}: {\n data: T[];\n columns: ColumnDef[];\n sorting?: SortingState;\n setSorting?: React.Dispatch>;\n hidePagination?: boolean;\n renderExpandedRow?: (row: import(\"@tanstack/react-table\").Row) => React.ReactNode;\n tableClassName?: string;\n tableWrapperClassName?: string;\n searchText?: string;\n setSearchText?: React.Dispatch>;\n rowClassName?: (row: T) => string;\n bodyRowClassName?: string;\n expandKey?: string;\n containerClassName?: string;\n headCellClassName?: string;\n bodyCellClassName?: string;\n expandRowContainerClassName?: string;\n onlyExpanded?: boolean;\n headClassName?: string;\n filterColumns?: Array<\n | string\n | {\n id: string;\n label?: string;\n placeholder?: string;\n columns?: string[];\n path?: string | string[];\n }\n >;\n onFilteredDataChange?: (filteredData: T[]) => void;\n defaultPageSize?: { desktop: number; mobile: number };\n}) => {\n const isMobile = useIsMobile();\n\n const [pagination, setPagination] = useState({\n pageIndex: 0,\n pageSize: undefined as unknown as number,\n });\n // const [searchText, setSearchText] = useState('')\n\n React.useEffect(() => {\n const newPageSize = isMobile ? defaultPageSize.mobile : defaultPageSize.desktop;\n setPagination((prev) => {\n if (prev.pageSize !== newPageSize) {\n return { ...prev, pageSize: newPageSize };\n }\n return prev;\n });\n }, [isMobile, defaultPageSize]);\n\n const [expanded, setExpanded] = useState(onlyExpanded ? true : {});\n const [columnFilters, setColumnFilters] = useState([]);\n const [columnVisibility, setColumnVisibility] = useState({});\n\n const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => {\n const itemRank = rankItem(row.getValue(columnId), value);\n addMeta({\n itemRank,\n });\n return itemRank.passed;\n };\n\n const normalizedFilterColumns = useMemo(\n () =>\n (filterColumns || []).map((fc) =>\n typeof fc === \"string\"\n ? {\n id: fc,\n label: undefined as string | undefined,\n placeholder: undefined as string | undefined,\n columns: undefined as string[] | undefined,\n path: undefined as string | string[] | undefined,\n }\n : fc\n ),\n [filterColumns]\n );\n\n const augmentedColumns: ColumnDef[] = useMemo(() => {\n const existingIds = new Set(\n (columns || []).map(\n (c: ColumnDef) =>\n ((c as { id?: string; accessorKey?: string }).id ??\n (c as { id?: string; accessorKey?: string }).accessorKey) as string\n )\n );\n const syntheticColumns: ColumnDef[] = [];\n\n normalizedFilterColumns.forEach((fc) => {\n if (fc?.columns && fc.columns.length > 0) {\n if (!existingIds.has(fc.id)) {\n const synthetic: ColumnDef = {\n id: fc.id,\n header: fc.label ?? fc.id,\n enableSorting: false,\n meta: { headerClassName: \"hidden\", bodyClassName: \"hidden\" } as Record,\n accessorFn: (row: T) => {\n try {\n if (fc.path) {\n const getNestedByPath = (obj: unknown, path: string | string[]): string => {\n if (obj === null || obj === undefined) return \"\";\n if (Array.isArray(path)) {\n return path\n .map((p) =>\n p\n .split(\".\")\n .reduce(\n (current: Record, key: string) =>\n current?.[key] as Record,\n obj as Record\n )\n )\n .filter((v) => v !== undefined && v !== null)\n .join(\" \");\n }\n return String(\n (path as string)\n .split(\".\")\n .reduce(\n (current: Record, key: string) =>\n current?.[key] as Record,\n obj as Record\n ) ?? \"\"\n );\n };\n\n return fc\n .columns!.map((k) => {\n const value = (row as Record)[k];\n if (Array.isArray(value)) {\n return value\n .map((item) => {\n const nested = getNestedByPath(item, fc.path!);\n return String(nested ?? \"\");\n })\n .join(\" \");\n }\n const nested = getNestedByPath(value, fc.path!);\n return String(nested ?? \"\");\n })\n .join(\" \")\n .trim();\n }\n return fc\n .columns!.map((k) => String((row as Record)[k] ?? \"\"))\n .join(\" \")\n .trim();\n } catch {\n return \"\";\n }\n },\n cell: () => null,\n };\n syntheticColumns.push(synthetic);\n }\n }\n });\n\n return [...columns, ...syntheticColumns];\n }, [columns, normalizedFilterColumns]);\n\n React.useEffect(() => {\n const hidden: VisibilityState = {};\n augmentedColumns.forEach((c: ColumnDef) => {\n const meta = c.meta as { bodyClassName?: string; headerClassName?: string } | undefined;\n const id =\n (c as { id?: string; accessorKey?: string }).id ??\n (c as { id?: string; accessorKey?: string }).accessorKey;\n if ((meta?.bodyClassName === \"hidden\" || meta?.headerClassName === \"hidden\") && id) {\n hidden[id] = false;\n }\n });\n setColumnVisibility((prev) => ({ ...prev, ...hidden }));\n }, [augmentedColumns]);\n\n const table = useReactTable({\n columns: augmentedColumns,\n data,\n debugTable: true,\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: !hidePagination ? getPaginationRowModel() : undefined,\n onPaginationChange: !hidePagination ? setPagination : undefined,\n onSortingChange: setSorting,\n state: {\n pagination,\n sorting,\n expanded,\n globalFilter: searchText,\n columnFilters,\n columnVisibility,\n },\n filterFns: {\n fuzzy: fuzzyFilter,\n },\n defaultColumn: {\n filterFn: \"fuzzy\",\n },\n onGlobalFilterChange: setSearchText,\n onColumnFiltersChange: setColumnFilters,\n onColumnVisibilityChange: setColumnVisibility,\n globalFilterFn: \"fuzzy\",\n onExpandedChange: setExpanded,\n getExpandedRowModel: getExpandedRowModel(),\n // autoResetPageIndex: false,\n });\n\n // Filtrelenmiş verileri parent component'e gönder\n React.useEffect(() => {\n if (onFilteredDataChange) {\n const filteredRows = table.getFilteredRowModel().rows.map((row) => row.original);\n onFilteredDataChange(filteredRows);\n }\n }, [searchText, columnFilters, data]);\n\n const hasExpandableContent = (original: T, key: string): boolean => {\n const record = original as Record;\n const val = record[key];\n if (Array.isArray(val)) return val.length > 0;\n return !!val;\n };\n\n return (\n
\n
\n \n \n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => (\n \n \n {flexRender(header.column.columnDef.header, header.getContext()) as string}\n {{\n asc: ,\n desc: ,\n }[header.column.getIsSorted() as string] ?? null}\n \n \n ))}\n \n ))}\n \n \n {table.getRowModel()?.rows?.length > 0 ? (\n table.getRowModel().rows.map((row) => (\n \n row.toggleExpanded()\n : undefined\n }\n style={\n expandKey && row.original && hasExpandableContent(row.original, expandKey)\n ? { cursor: \"pointer\" }\n : undefined\n }\n >\n {row.getVisibleCells().map((cell, idx) => (\n \n {idx === 0 && renderExpandedRow ? (\n \n {expandKey &&\n !onlyExpanded &&\n row.original &&\n hasExpandableContent(row.original, expandKey) ? (\n {\n e.stopPropagation();\n row.toggleExpanded();\n }}\n >\n \n \n ) : (\n \n )}\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ) : (\n flexRender(cell.column.columnDef.cell, cell.getContext())\n )}\n \n ))}\n \n {/* Expanded row içeriği */}\n {row.getIsExpanded() && renderExpandedRow && (\n \n \n {renderExpandedRow(row)}\n \n \n )}\n \n ))\n ) : (\n \n \n No data found.\n \n \n )}\n \n
\n
\n
\n {!hidePagination && (\n
\n table.setPageIndex(page - 1)}\n maxVisiblePages={6}\n />\n
\n )}\n \n );\n};\n\nexport default CustomTable;\n", "type": "registry:file", "target": "src/components/table/custom-table.tsx" }, { "path": "registry/tra-plugins/table/components/table/table-skeleton.tsx", "content": "import { Skeleton } from \"@/components/skeleton\";\n\nconst TableSkeleton = ({ hideHeader = false }: { hideHeader?: boolean }) => (\n <>\n {!hideHeader && (\n
\n \n \n \n \n
\n )}\n \n \n \n \n \n \n \n \n);\n\nexport default TableSkeleton;\n", "type": "registry:file", "target": "src/components/table/table-skeleton.tsx" }, { "path": "registry/tra-plugins/table/components/table/custom-table-filter-section.tsx", "content": "import { useMemo } from \"react\";\nimport { useIsMobile } from \"@/hooks/use-mobile\";\nimport type { Table } from \"@tanstack/react-table\";\nimport Select, { type ISelectOption } from \"@/components/select\";\n\nconst CustomTableFilterSection = ({\n table,\n normalizedFilterColumns,\n data,\n augmentedColumns,\n}: {\n table: Table;\n normalizedFilterColumns: {\n id: string;\n label?: string;\n placeholder?: string;\n columns?: string[];\n path?: string | string[];\n }[];\n data: T[];\n augmentedColumns: Array<{ header: string; accessorKey?: string }>;\n}) => {\n const isMobile = useIsMobile();\n\n const filterOptionsCache = useMemo(() => {\n const getNamedValue = (obj: unknown) => {\n if (!obj || typeof obj !== \"object\") return \"\";\n const o = obj as Record;\n const extracted = o[\"name\"] ?? o[\"label\"] ?? o[\"title\"];\n return extracted ? String(extracted).trim() : \"\";\n };\n\n const getNestedValue = (obj: unknown, path: string): unknown =>\n path.split(\".\").reduce((current, key) => {\n if (current && typeof current === \"object\") {\n return (current as Record)[key];\n }\n return undefined;\n }, obj as unknown);\n\n const cache: Record = {};\n\n normalizedFilterColumns.forEach((fc) => {\n const uniqueValues = new Set();\n const keysToProcess = fc.columns && fc.columns.length > 0 ? fc.columns : [fc.id];\n\n // Column tanımını bul (accessorFn için)\n const columnDef = augmentedColumns.find(\n (c) =>\n (c as { accessorKey?: string }).accessorKey === fc.id ||\n (c as { id?: string }).id === fc.id\n );\n\n data.forEach((row) => {\n // Eğer column'da accessorFn varsa, onu kullan\n if (\n columnDef &&\n (columnDef as { accessorFn?: (row: T) => unknown }).accessorFn &&\n keysToProcess.length === 1 &&\n !fc.path\n ) {\n try {\n const value = (columnDef as { accessorFn?: (row: T) => unknown }).accessorFn!(row);\n if (value && String(value).trim() && String(value).trim() !== \"-\") {\n uniqueValues.add(String(value).trim());\n }\n } catch {\n // Hata durumunda devam et\n }\n return;\n }\n\n // Eğer birden fazla column varsa, değerleri birleştir\n if (keysToProcess.length > 1) {\n const combinedValue = keysToProcess\n .map((key) => {\n const value = (row as Record)[key];\n if (value === null || value === undefined) return \"\";\n\n // Path varsa nested değere eriş\n if (fc.path) {\n const paths = Array.isArray(fc.path) ? fc.path : [fc.path];\n if (Array.isArray(value)) {\n return value\n .map((item) => {\n if (item && typeof item === \"object\") {\n // Her path için değer al ve birleştir\n return paths\n .map((p) => {\n const nested = getNestedValue(item, p);\n return nested !== null && nested !== undefined\n ? String(nested).trim()\n : \"\";\n })\n .filter(Boolean)\n .join(\" \");\n }\n return \"\";\n })\n .filter(Boolean)\n .join(\", \");\n }\n if (typeof value === \"object\") {\n // Tek obje için tüm path'leri birleştir\n return paths\n .map((p) => {\n const nested = getNestedValue(value, p);\n return nested !== null && nested !== undefined ? String(nested).trim() : \"\";\n })\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n // Path yoksa normal değer\n if (Array.isArray(value)) {\n return value\n .map((item) => {\n if (item && typeof item === \"object\") {\n const obj = item as Record;\n const nestedVal = obj[\"name\"] ?? obj[\"label\"] ?? obj[\"title\"];\n return nestedVal !== null && nestedVal !== undefined\n ? String(nestedVal).trim()\n : \"\";\n }\n return String(item).trim();\n })\n .filter(Boolean)\n .join(\" \");\n }\n if (typeof value === \"object\") {\n return getNamedValue(value);\n }\n return String(value).trim();\n })\n .filter(Boolean)\n .join(\" \")\n .trim();\n\n if (combinedValue) {\n uniqueValues.add(combinedValue);\n }\n } else {\n // Tek column için normal işlem\n keysToProcess.forEach((key) => {\n const value = (row as unknown as Record)[key];\n\n if (value === null || value === undefined) return;\n\n // Eğer path verilmişse, nested değerlere eriş\n if (fc.path) {\n const paths = Array.isArray(fc.path) ? fc.path : [fc.path];\n if (Array.isArray(value)) {\n value.forEach((item) => {\n if (item && typeof item === \"object\") {\n // Her path için değer al ve birleştir\n const combined = paths\n .map((p) => {\n const nestedValue = getNestedValue(item, p);\n return nestedValue !== null && nestedValue !== undefined\n ? String(nestedValue).trim()\n : \"\";\n })\n .filter(Boolean)\n .join(\" \");\n if (combined) {\n uniqueValues.add(combined);\n }\n }\n });\n } else if (typeof value === \"object\") {\n // Tek obje için tüm path'leri birleştir\n const combined = paths\n .map((p) => {\n const nestedValue = getNestedValue(value as Record, p);\n return nestedValue !== null && nestedValue !== undefined\n ? String(nestedValue).trim()\n : \"\";\n })\n .filter(Boolean)\n .join(\" \");\n if (combined) {\n uniqueValues.add(combined);\n }\n }\n } else {\n // Path yoksa, basit string çevirme\n if (Array.isArray(value)) {\n value.forEach((item) => {\n if (item && typeof item === \"object\") {\n const extracted = item.name || item.label || item.title;\n if (extracted) {\n uniqueValues.add(String(extracted).trim());\n }\n } else if (item) {\n uniqueValues.add(String(item).trim());\n }\n });\n } else if (typeof value === \"object\") {\n const extracted = getNamedValue(value);\n if (extracted) {\n uniqueValues.add(extracted);\n }\n } else {\n uniqueValues.add(String(value).trim());\n }\n }\n });\n }\n });\n\n cache[fc.id] = Array.from(uniqueValues)\n .filter((v) => v !== \"\")\n .sort()\n .map((value) => ({\n content: value,\n value,\n }));\n });\n\n return cache;\n }, [data, normalizedFilterColumns, augmentedColumns]);\n\n return (\n <>\n {normalizedFilterColumns?.length > 0 && (\n \n {normalizedFilterColumns.map((fc) => {\n const col = table.getColumn(fc.id as string);\n if (!col) return null;\n const value = (col.getFilterValue() as string) ?? \"\";\n const { header } = col.columnDef;\n const headerText = typeof header === \"string\" ? header : undefined;\n const options = filterOptionsCache[fc.id] || [];\n return (\n {\n if (v) {\n col.setFilterValue(v);\n } else {\n col.setFilterValue(\"\");\n }\n }}\n options={options}\n placeholder={`${fc.placeholder ?? headerText ?? fc.id} Search...`}\n isSearchable\n size=\"sm\"\n dropdownAlign=\"left\"\n />\n );\n })}\n \n )}\n \n );\n};\n\nexport default CustomTableFilterSection;\n", "type": "registry:file", "target": "src/components/table/custom-table-filter-section.tsx" }, { "path": "registry/tra-plugins/table/hooks/useTableState.ts", "content": "import { useState } from \"react\";\nimport type { SortingState, PaginationState } from \"@tanstack/react-table\";\n\ninterface UseTableStateOptions {\n initialPageSize?: number;\n}\n\n/**\n * TanStack Table için ortak state yönetimi.\n * Sayfalama, sıralama ve global filtre durumunu yönetir.\n */\nexport function useTableState({ initialPageSize = 10 }: UseTableStateOptions = {}) {\n const [pagination, setPagination] = useState({\n pageIndex: 0,\n pageSize: initialPageSize,\n });\n const [sorting, setSorting] = useState([]);\n const [globalFilter, setGlobalFilter] = useState(\"\");\n\n const resetPagination = () => setPagination((p) => ({ ...p, pageIndex: 0 }));\n\n const handleGlobalFilterChange = (value: string) => {\n setGlobalFilter(value);\n resetPagination();\n };\n\n return {\n pagination,\n setPagination,\n sorting,\n setSorting,\n globalFilter,\n setGlobalFilter: handleGlobalFilterChange,\n };\n}\n", "type": "registry:file", "target": "src/hooks/useTableState.ts" }, { "path": "registry/tra-plugins/table/types/table.types.ts", "content": "export interface TableColumn {\n /** TanStack accessorKey veya id */\n key: string;\n header: string;\n /** Hücre render fonksiyonu — belirtilmezse ham değer gösterilir */\n cell?: (value: unknown, row: TData) => React.ReactNode;\n /** Sıralama aktif mi? Varsayılan: true */\n sortable?: boolean;\n /** Minimum kolon genişliği (px) */\n minWidth?: number;\n}\n\nexport interface TableState {\n pageIndex: number;\n pageSize: number;\n sorting: { id: string; desc: boolean }[];\n globalFilter: string;\n}\n\nexport interface PaginationMeta {\n totalCount: number;\n pageCount: number;\n}\n", "type": "registry:file", "target": "src/types/table.types.ts" } ], "type": "registry:block" }