diff --git a/public/r/data-table-action-bar.json b/public/r/data-table-action-bar.json index 398035d44..267dcdda7 100644 --- a/public/r/data-table-action-bar.json +++ b/public/r/data-table-action-bar.json @@ -15,9 +15,10 @@ ], "files": [ { - "path": "src/components/data-table-action-bar.tsx", - "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport type { Table } from \"@tanstack/react-table\";\nimport { Loader } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\ninterface DataTableActionBarProps\n extends React.ComponentProps {\n table: Table;\n visible?: boolean;\n container?: Element | DocumentFragment | null;\n}\n\nfunction DataTableActionBar({\n table,\n visible: visibleProp,\n container: containerProp,\n children,\n className,\n ...props\n}: DataTableActionBarProps) {\n const [mounted, setMounted] = React.useState(false);\n\n React.useLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n table.toggleAllRowsSelected(false);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [table]);\n\n const container =\n containerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!container) return null;\n\n const visible =\n visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0;\n\n return ReactDOM.createPortal(\n \n {visible && (\n \n {children}\n \n )}\n ,\n container,\n );\n}\n\ninterface DataTableActionBarActionProps\n extends React.ComponentProps {\n tooltip?: string;\n isPending?: boolean;\n}\n\nfunction DataTableActionBarAction({\n size = \"sm\",\n tooltip,\n isPending,\n disabled,\n className,\n children,\n ...props\n}: DataTableActionBarActionProps) {\n const trigger = (\n svg]:size-3.5\",\n size === \"icon\" ? \"size-7\" : \"h-7\",\n className,\n )}\n disabled={disabled || isPending}\n {...props}\n >\n {isPending ? : children}\n \n );\n\n if (!tooltip) return trigger;\n\n return (\n \n {trigger}\n span]:hidden\"\n >\n

{tooltip}

\n \n
\n );\n}\n\nexport { DataTableActionBar, DataTableActionBarAction };\n", - "type": "registry:component" + "path": "src/components/data-table/data-table-action-bar.tsx", + "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport type { Table } from \"@tanstack/react-table\";\nimport { Loader, X } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\ninterface DataTableActionBarProps\n extends React.ComponentProps {\n table: Table;\n visible?: boolean;\n container?: Element | DocumentFragment | null;\n}\n\nfunction DataTableActionBar({\n table,\n visible: visibleProp,\n container: containerProp,\n children,\n className,\n ...props\n}: DataTableActionBarProps) {\n const [mounted, setMounted] = React.useState(false);\n\n React.useLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n table.toggleAllRowsSelected(false);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [table]);\n\n const container =\n containerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!container) return null;\n\n const visible =\n visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0;\n\n return ReactDOM.createPortal(\n \n {visible && (\n \n {children}\n \n )}\n ,\n container,\n );\n}\n\ninterface DataTableActionBarActionProps\n extends React.ComponentProps {\n tooltip?: string;\n isPending?: boolean;\n}\n\nfunction DataTableActionBarAction({\n size = \"sm\",\n tooltip,\n isPending,\n disabled,\n className,\n children,\n ...props\n}: DataTableActionBarActionProps) {\n const trigger = (\n svg]:size-3.5\",\n size === \"icon\" ? \"size-7\" : \"h-7\",\n className,\n )}\n disabled={disabled || isPending}\n {...props}\n >\n {isPending ? : children}\n \n );\n\n if (!tooltip) return trigger;\n\n return (\n \n {trigger}\n span]:hidden\"\n >\n

{tooltip}

\n \n
\n );\n}\n\ninterface DataTableActionBarSelectionProps {\n table: Table;\n}\n\nfunction DataTableActionBarSelection({\n table,\n}: DataTableActionBarSelectionProps) {\n const onClearSelection = React.useCallback(() => {\n table.toggleAllRowsSelected(false);\n }, [table]);\n\n return (\n
\n \n {table.getFilteredSelectedRowModel().rows.length} selected\n \n \n \n \n \n \n \n \n span]:hidden\"\n >\n

Clear selection

\n \n \n Esc\n \n \n \n
\n
\n );\n}\n\nexport {\n DataTableActionBar,\n DataTableActionBarAction,\n DataTableActionBarSelection,\n};\n", + "type": "registry:component", + "target": "src/components/data-table/data-table-action-bar.tsx" } ] } \ No newline at end of file diff --git a/public/r/data-table-filter-list.json b/public/r/data-table-filter-list.json index a952d35b8..9c12b7cd4 100644 --- a/public/r/data-table-filter-list.json +++ b/public/r/data-table-filter-list.json @@ -21,29 +21,34 @@ ], "files": [ { - "path": "src/components/data-table-filter-list.tsx", - "content": "\"use client\";\n\nimport type { Column, ColumnMeta, Table } from \"@tanstack/react-table\";\nimport {\n CalendarIcon,\n Check,\n ChevronsUpDown,\n GripVertical,\n ListFilter,\n Trash2,\n} from \"lucide-react\";\nimport { parseAsStringEnum, useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport { DataTableRangeFilter } from \"@/components/data-table-range-filter\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Faceted,\n FacetedBadgeList,\n FacetedContent,\n FacetedEmpty,\n FacetedGroup,\n FacetedInput,\n FacetedItem,\n FacetedList,\n FacetedTrigger,\n} from \"@/components/ui/faceted\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n JoinOperator,\n} from \"@/types/data-table\";\n\nconst FILTERS_KEY = \"filters\";\nconst JOIN_OPERATOR_KEY = \"joinOperator\";\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst OPEN_MENU_SHORTCUT = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterListProps\n extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n}\n\nexport function DataTableFilterList({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n ...props\n}: DataTableFilterListProps) {\n const id = React.useId();\n const labelId = React.useId();\n const descriptionId = React.useId();\n const [open, setOpen] = React.useState(false);\n const addButtonRef = React.useRef(null);\n\n const columns = React.useMemo(() => {\n return table\n .getAllColumns()\n .filter((column) => column.columnDef.enableColumnFilter);\n }, [table]);\n\n const [filters, setFilters] = useQueryState(\n FILTERS_KEY,\n getFiltersStateParser(columns.map((field) => field.id))\n .withDefault([])\n .withOptions({\n clearOnDefault: true,\n shallow,\n throttleMs,\n }),\n );\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\n\n console.log({ filters });\n\n const [joinOperator, setJoinOperator] = useQueryState(\n JOIN_OPERATOR_KEY,\n parseAsStringEnum([\"and\", \"or\"]).withDefault(\"and\").withOptions({\n clearOnDefault: true,\n shallow,\n }),\n );\n\n const onFilterAdd = React.useCallback(() => {\n const column = columns[0];\n\n if (!column) return;\n\n debouncedSetFilters([\n ...filters,\n {\n id: column.id as Extract,\n value: \"\",\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n filterId: generateId({ length: 8 }),\n },\n ]);\n }, [columns, filters, debouncedSetFilters]);\n\n const onFilterUpdate = React.useCallback(\n (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => {\n debouncedSetFilters((prevFilters) => {\n const updatedFilters = prevFilters.map((filter) => {\n if (filter.filterId === filterId) {\n return { ...filter, ...updates } as ExtendedColumnFilter;\n }\n return filter;\n });\n return updatedFilters;\n });\n },\n [debouncedSetFilters],\n );\n\n const onFilterRemove = React.useCallback(\n (filterId: string) => {\n const updatedFilters = filters.filter(\n (filter) => filter.filterId !== filterId,\n );\n void setFilters(updatedFilters);\n requestAnimationFrame(() => {\n addButtonRef.current?.focus();\n });\n },\n [filters, setFilters],\n );\n\n const onFiltersReset = React.useCallback(() => {\n void setFilters(null);\n void setJoinOperator(\"and\");\n }, [setFilters, setJoinOperator]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.shiftKey\n ) {\n event.preventDefault();\n setOpen(true);\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n event.shiftKey &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [filters, onFilterRemove]);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n },\n [filters, onFilterRemove],\n );\n\n return (\n item.filterId}\n >\n \n \n \n \n \n
\n

\n {filters.length > 0 ? \"Filters\" : \"No filters applied\"}\n

\n 0 && \"sr-only\",\n )}\n >\n {filters.length > 0\n ? \"Modify filters to refine your rows.\"\n : \"Add filters to refine your rows.\"}\n

\n
\n {filters.length > 0 ? (\n \n \n {filters.map((filter, index) => (\n \n key={filter.filterId}\n filter={filter}\n index={index}\n filterItemId={`${id}-filter-${filter.filterId}`}\n joinOperator={joinOperator}\n setJoinOperator={setJoinOperator}\n columns={columns}\n onFilterUpdate={onFilterUpdate}\n onFilterRemove={onFilterRemove}\n />\n ))}\n \n \n ) : null}\n
\n \n Add filter\n \n {filters.length > 0 ? (\n \n Reset filters\n \n ) : null}\n
\n \n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n );\n}\n\ninterface DataTableFilterItemProps {\n filter: ExtendedColumnFilter;\n index: number;\n filterItemId: string;\n joinOperator: JoinOperator;\n setJoinOperator: (value: JoinOperator) => void;\n columns: Column[];\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataTableFilterItem({\n filter,\n index,\n filterItemId,\n joinOperator,\n setJoinOperator,\n columns,\n onFilterUpdate,\n onFilterRemove,\n}: DataTableFilterItemProps) {\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showOperatorSelector, setShowOperatorSelector] = React.useState(false);\n const [showValueSelector, setShowValueSelector] = React.useState(false);\n\n const column = columns.find((column) => column.id === filter.id);\n if (!column) return null;\n\n const joinOperatorListboxId = `${filterItemId}-join-operator-listbox`;\n const fieldListboxId = `${filterItemId}-field-listbox`;\n const operatorListboxId = `${filterItemId}-operator-listbox`;\n const inputId = `${filterItemId}-input`;\n\n const columnMeta = column.columnDef.meta;\n const filterOperators = getFilterOperators(filter.variant);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\n return;\n }\n\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onFilterRemove(filter.filterId);\n }\n },\n [\n filter.filterId,\n showFieldSelector,\n showOperatorSelector,\n showValueSelector,\n onFilterRemove,\n ],\n );\n\n return (\n \n \n
\n {index === 0 ? (\n Where\n ) : index === 1 ? (\n setJoinOperator(value)}\n >\n \n \n \n \n {dataTableConfig.joinOperators.map((joinOperator) => (\n \n {joinOperator}\n \n ))}\n \n \n ) : (\n \n {joinOperator}\n \n )}\n
\n \n \n \n \n {columns.find((column) => column.id === filter.id)?.columnDef\n .meta?.label ?? \"Select field\"}\n \n \n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n {\n onFilterUpdate(filter.filterId, {\n id: value as Extract,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n value: \"\",\n });\n\n setShowFieldSelector(false);\n }}\n >\n \n {column.columnDef.meta?.label}\n \n \n \n ))}\n \n \n \n \n \n \n onFilterUpdate(filter.filterId, {\n operator: value,\n value:\n value === \"isEmpty\" || value === \"isNotEmpty\"\n ? \"\"\n : filter.value,\n })\n }\n >\n \n
\n \n
\n \n \n {filterOperators.map((operator) => (\n \n {operator.label}\n \n ))}\n \n \n
\n {onFilterInputRender({\n filter,\n inputId,\n column,\n columnMeta,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n })}\n
\n onFilterRemove(filter.filterId)}\n >\n \n \n \n \n \n
\n \n );\n}\n\nfunction onFilterInputRender({\n filter,\n inputId,\n column,\n columnMeta,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n}: {\n filter: ExtendedColumnFilter;\n inputId: string;\n column: Column;\n columnMeta?: ColumnMeta;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n showValueSelector: boolean;\n setShowValueSelector: (value: boolean) => void;\n}) {\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\n return (\n \n );\n }\n\n switch (filter.variant) {\n case \"text\":\n case \"number\":\n case \"range\": {\n if (\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\n filter.operator === \"isBetween\"\n ) {\n return (\n \n );\n }\n\n const isNumber =\n filter.variant === \"number\" || filter.variant === \"range\";\n\n return (\n \n onFilterUpdate(filter.filterId, {\n value: event.target.value,\n })\n }\n />\n );\n }\n\n case \"boolean\": {\n if (Array.isArray(filter.value)) return null;\n\n const inputListboxId = `${inputId}-listbox`;\n\n return (\n \n onFilterUpdate(filter.filterId, {\n value,\n })\n }\n >\n \n \n \n \n True\n False\n \n \n );\n }\n\n case \"select\":\n case \"multiSelect\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const multiple = filter.variant === \"multiSelect\";\n const selectedValues = multiple\n ? Array.isArray(filter.value)\n ? filter.value\n : []\n : typeof filter.value === \"string\"\n ? filter.value\n : undefined;\n\n return (\n {\n onFilterUpdate(filter.filterId, {\n value,\n });\n }}\n multiple={multiple}\n >\n \n \n \n \n \n \n \n \n No options found.\n \n {columnMeta?.options?.map((option) => (\n \n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n ))}\n \n \n \n \n );\n }\n\n case \"date\":\n case \"dateRange\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const dateValue = Array.isArray(filter.value)\n ? filter.value.filter(Boolean)\n : [filter.value, filter.value].filter(Boolean);\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\n new Date(Number(dateValue[1])),\n )}`\n : dateValue[0]\n ? formatDate(new Date(Number(dateValue[0])))\n : \"Pick a date\";\n\n return (\n \n \n \n \n {displayValue}\n \n \n \n {filter.operator === \"isBetween\" ? (\n {\n onFilterUpdate(filter.filterId, {\n value: date\n ? [\n (date.from?.getTime() ?? \"\").toString(),\n (date.to?.getTime() ?? \"\").toString(),\n ]\n : [],\n });\n }}\n />\n ) : (\n {\n onFilterUpdate(filter.filterId, {\n value: (date?.getTime() ?? \"\").toString(),\n });\n }}\n />\n )}\n \n \n );\n }\n\n default:\n return null;\n }\n}\n", - "type": "registry:component" + "path": "src/components/data-table/data-table-filter-list.tsx", + "content": "\"use client\";\n\nimport type { Column, ColumnMeta, Table } from \"@tanstack/react-table\";\nimport {\n CalendarIcon,\n Check,\n ChevronsUpDown,\n GripVertical,\n ListFilter,\n Trash2,\n} from \"lucide-react\";\nimport { parseAsStringEnum, useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport { DataTableRangeFilter } from \"@/components/data-table/data-table-range-filter\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Faceted,\n FacetedBadgeList,\n FacetedContent,\n FacetedEmpty,\n FacetedGroup,\n FacetedInput,\n FacetedItem,\n FacetedList,\n FacetedTrigger,\n} from \"@/components/ui/faceted\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n JoinOperator,\n} from \"@/types/data-table\";\n\nconst FILTERS_KEY = \"filters\";\nconst JOIN_OPERATOR_KEY = \"joinOperator\";\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst OPEN_MENU_SHORTCUT = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterListProps\n extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n}\n\nexport function DataTableFilterList({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n ...props\n}: DataTableFilterListProps) {\n const id = React.useId();\n const labelId = React.useId();\n const descriptionId = React.useId();\n const [open, setOpen] = React.useState(false);\n const addButtonRef = React.useRef(null);\n\n const columns = React.useMemo(() => {\n return table\n .getAllColumns()\n .filter((column) => column.columnDef.enableColumnFilter);\n }, [table]);\n\n const [filters, setFilters] = useQueryState(\n FILTERS_KEY,\n getFiltersStateParser(columns.map((field) => field.id))\n .withDefault([])\n .withOptions({\n clearOnDefault: true,\n shallow,\n throttleMs,\n }),\n );\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\n\n const [joinOperator, setJoinOperator] = useQueryState(\n JOIN_OPERATOR_KEY,\n parseAsStringEnum([\"and\", \"or\"]).withDefault(\"and\").withOptions({\n clearOnDefault: true,\n shallow,\n }),\n );\n\n const onFilterAdd = React.useCallback(() => {\n const column = columns[0];\n\n if (!column) return;\n\n debouncedSetFilters([\n ...filters,\n {\n id: column.id as Extract,\n value: \"\",\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n filterId: generateId({ length: 8 }),\n },\n ]);\n }, [columns, filters, debouncedSetFilters]);\n\n const onFilterUpdate = React.useCallback(\n (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => {\n debouncedSetFilters((prevFilters) => {\n const updatedFilters = prevFilters.map((filter) => {\n if (filter.filterId === filterId) {\n return { ...filter, ...updates } as ExtendedColumnFilter;\n }\n return filter;\n });\n return updatedFilters;\n });\n },\n [debouncedSetFilters],\n );\n\n const onFilterRemove = React.useCallback(\n (filterId: string) => {\n const updatedFilters = filters.filter(\n (filter) => filter.filterId !== filterId,\n );\n void setFilters(updatedFilters);\n requestAnimationFrame(() => {\n addButtonRef.current?.focus();\n });\n },\n [filters, setFilters],\n );\n\n const onFiltersReset = React.useCallback(() => {\n void setFilters(null);\n void setJoinOperator(\"and\");\n }, [setFilters, setJoinOperator]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.shiftKey\n ) {\n event.preventDefault();\n setOpen(true);\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n event.shiftKey &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [filters, onFilterRemove]);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n },\n [filters, onFilterRemove],\n );\n\n return (\n item.filterId}\n >\n \n \n \n \n \n
\n

\n {filters.length > 0 ? \"Filters\" : \"No filters applied\"}\n

\n 0 && \"sr-only\",\n )}\n >\n {filters.length > 0\n ? \"Modify filters to refine your rows.\"\n : \"Add filters to refine your rows.\"}\n

\n
\n {filters.length > 0 ? (\n \n \n {filters.map((filter, index) => (\n \n key={filter.filterId}\n filter={filter}\n index={index}\n filterItemId={`${id}-filter-${filter.filterId}`}\n joinOperator={joinOperator}\n setJoinOperator={setJoinOperator}\n columns={columns}\n onFilterUpdate={onFilterUpdate}\n onFilterRemove={onFilterRemove}\n />\n ))}\n
\n \n ) : null}\n
\n \n Add filter\n \n {filters.length > 0 ? (\n \n Reset filters\n \n ) : null}\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n );\n}\n\ninterface DataTableFilterItemProps {\n filter: ExtendedColumnFilter;\n index: number;\n filterItemId: string;\n joinOperator: JoinOperator;\n setJoinOperator: (value: JoinOperator) => void;\n columns: Column[];\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataTableFilterItem({\n filter,\n index,\n filterItemId,\n joinOperator,\n setJoinOperator,\n columns,\n onFilterUpdate,\n onFilterRemove,\n}: DataTableFilterItemProps) {\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showOperatorSelector, setShowOperatorSelector] = React.useState(false);\n const [showValueSelector, setShowValueSelector] = React.useState(false);\n\n const column = columns.find((column) => column.id === filter.id);\n if (!column) return null;\n\n const joinOperatorListboxId = `${filterItemId}-join-operator-listbox`;\n const fieldListboxId = `${filterItemId}-field-listbox`;\n const operatorListboxId = `${filterItemId}-operator-listbox`;\n const inputId = `${filterItemId}-input`;\n\n const columnMeta = column.columnDef.meta;\n const filterOperators = getFilterOperators(filter.variant);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\n return;\n }\n\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onFilterRemove(filter.filterId);\n }\n },\n [\n filter.filterId,\n showFieldSelector,\n showOperatorSelector,\n showValueSelector,\n onFilterRemove,\n ],\n );\n\n return (\n \n \n
\n {index === 0 ? (\n Where\n ) : index === 1 ? (\n setJoinOperator(value)}\n >\n \n \n \n \n {dataTableConfig.joinOperators.map((joinOperator) => (\n \n {joinOperator}\n \n ))}\n \n \n ) : (\n \n {joinOperator}\n \n )}\n
\n \n \n \n \n {columns.find((column) => column.id === filter.id)?.columnDef\n .meta?.label ?? \"Select field\"}\n \n \n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n {\n onFilterUpdate(filter.filterId, {\n id: value as Extract,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n value: \"\",\n });\n\n setShowFieldSelector(false);\n }}\n >\n \n {column.columnDef.meta?.label}\n \n \n \n ))}\n \n \n \n \n \n \n onFilterUpdate(filter.filterId, {\n operator: value,\n value:\n value === \"isEmpty\" || value === \"isNotEmpty\"\n ? \"\"\n : filter.value,\n })\n }\n >\n \n
\n \n
\n \n \n {filterOperators.map((operator) => (\n \n {operator.label}\n \n ))}\n \n \n
\n {onFilterInputRender({\n filter,\n inputId,\n column,\n columnMeta,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n })}\n
\n onFilterRemove(filter.filterId)}\n >\n \n \n \n \n \n
\n \n );\n}\n\nfunction onFilterInputRender({\n filter,\n inputId,\n column,\n columnMeta,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n}: {\n filter: ExtendedColumnFilter;\n inputId: string;\n column: Column;\n columnMeta?: ColumnMeta;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n showValueSelector: boolean;\n setShowValueSelector: (value: boolean) => void;\n}) {\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\n return (\n \n );\n }\n\n switch (filter.variant) {\n case \"text\":\n case \"number\":\n case \"range\": {\n if (\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\n filter.operator === \"isBetween\"\n ) {\n return (\n \n );\n }\n\n const isNumber =\n filter.variant === \"number\" || filter.variant === \"range\";\n\n return (\n \n onFilterUpdate(filter.filterId, {\n value: event.target.value,\n })\n }\n />\n );\n }\n\n case \"boolean\": {\n if (Array.isArray(filter.value)) return null;\n\n const inputListboxId = `${inputId}-listbox`;\n\n return (\n \n onFilterUpdate(filter.filterId, {\n value,\n })\n }\n >\n \n \n \n \n True\n False\n \n \n );\n }\n\n case \"select\":\n case \"multiSelect\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const multiple = filter.variant === \"multiSelect\";\n const selectedValues = multiple\n ? Array.isArray(filter.value)\n ? filter.value\n : []\n : typeof filter.value === \"string\"\n ? filter.value\n : undefined;\n\n return (\n {\n onFilterUpdate(filter.filterId, {\n value,\n });\n }}\n multiple={multiple}\n >\n \n \n \n \n \n \n \n \n No options found.\n \n {columnMeta?.options?.map((option) => (\n \n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n ))}\n \n \n \n \n );\n }\n\n case \"date\":\n case \"dateRange\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const dateValue = Array.isArray(filter.value)\n ? filter.value.filter(Boolean)\n : [filter.value, filter.value].filter(Boolean);\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\n new Date(Number(dateValue[1])),\n )}`\n : dateValue[0]\n ? formatDate(new Date(Number(dateValue[0])))\n : \"Pick a date\";\n\n return (\n \n \n \n \n {displayValue}\n \n \n \n {filter.operator === \"isBetween\" ? (\n {\n onFilterUpdate(filter.filterId, {\n value: date\n ? [\n (date.from?.getTime() ?? \"\").toString(),\n (date.to?.getTime() ?? \"\").toString(),\n ]\n : [],\n });\n }}\n />\n ) : (\n {\n onFilterUpdate(filter.filterId, {\n value: (date?.getTime() ?? \"\").toString(),\n });\n }}\n />\n )}\n \n \n );\n }\n\n default:\n return null;\n }\n}\n", + "type": "registry:component", + "target": "src/components/data-table/data-table-filter-list.tsx" }, { - "path": "src/components/data-table-range-filter.tsx", + "path": "src/components/data-table/data-table-range-filter.tsx", "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport type { ExtendedColumnFilter } from \"@/types/data-table\";\n\ninterface DataTableRangeFilterProps extends React.ComponentProps<\"div\"> {\n filter: ExtendedColumnFilter;\n column: Column;\n inputId: string;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n}\n\nexport function DataTableRangeFilter({\n filter,\n column,\n inputId,\n onFilterUpdate,\n className,\n ...props\n}: DataTableRangeFilterProps) {\n const meta = column.columnDef.meta;\n\n const [min, max] = React.useMemo(() => {\n const range = column.columnDef.meta?.range;\n if (range) return range;\n\n const values = column.getFacetedMinMaxValues();\n if (!values) return [0, 100];\n\n return [values[0], values[1]];\n }, [column]);\n\n const formatValue = React.useCallback(\n (value: string | number | undefined) => {\n if (value === undefined || value === \"\") return \"\";\n const numValue = Number(value);\n return Number.isNaN(numValue)\n ? \"\"\n : numValue.toLocaleString(undefined, {\n maximumFractionDigits: 0,\n });\n },\n [],\n );\n\n const value = React.useMemo(() => {\n if (Array.isArray(filter.value)) return filter.value.map(formatValue);\n return [formatValue(filter.value), \"\"];\n }, [filter.value, formatValue]);\n\n const onRangeValueChange = React.useCallback(\n (value: string, isMin?: boolean) => {\n const numValue = Number(value);\n const currentValues = Array.isArray(filter.value)\n ? filter.value\n : [\"\", \"\"];\n const otherValue = isMin\n ? (currentValues[1] ?? \"\")\n : (currentValues[0] ?? \"\");\n\n if (\n value === \"\" ||\n (!Number.isNaN(numValue) &&\n (isMin\n ? numValue >= min && numValue <= (Number(otherValue) || max)\n : numValue <= max && numValue >= (Number(otherValue) || min)))\n ) {\n onFilterUpdate(filter.filterId, {\n value: isMin ? [value, otherValue] : [otherValue, value],\n });\n }\n },\n [filter.filterId, filter.value, min, max, onFilterUpdate],\n );\n\n return (\n \n onRangeValueChange(event.target.value, true)}\n />\n to\n onRangeValueChange(event.target.value)}\n />\n
\n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-range-filter.tsx" }, { - "path": "src/components/data-table-advanced-toolbar.tsx", - "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTableViewOptions } from \"@/components/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps\n extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableAdvancedToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableAdvancedToolbarProps) {\n return (\n \n
{children}
\n
\n \n
\n
\n );\n}\n", - "type": "registry:component" + "path": "src/components/data-table/data-table-advanced-toolbar.tsx", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps\n extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableAdvancedToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableAdvancedToolbarProps) {\n return (\n \n
{children}
\n
\n \n
\n
\n );\n}\n", + "type": "registry:component", + "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, { "path": "src/components/ui/sortable.tsx", "content": "\"use client\";\n\nimport {\n type Announcements,\n DndContext,\n type DndContextProps,\n type DragEndEvent,\n DragOverlay,\n type DraggableSyntheticListeners,\n type DropAnimation,\n KeyboardSensor,\n MouseSensor,\n type ScreenReaderInstructions,\n TouchSensor,\n type UniqueIdentifier,\n closestCenter,\n closestCorners,\n defaultDropAnimationSideEffects,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport {\n restrictToHorizontalAxis,\n restrictToParentElement,\n restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n SortableContext,\n type SortableContextProps,\n arrayMove,\n horizontalListSortingStrategy,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\n\nimport { composeEventHandlers, useComposedRefs } from \"@/lib/composition\";\nimport { cn } from \"@/lib/utils\";\nimport * as ReactDOM from \"react-dom\";\n\nconst orientationConfig = {\n vertical: {\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\n strategy: verticalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n horizontal: {\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n strategy: horizontalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n mixed: {\n modifiers: [restrictToParentElement],\n strategy: undefined,\n collisionDetection: closestCorners,\n },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\nconst SORTABLE_ERRORS = {\n [ROOT_NAME]: `\\`${ROOT_NAME}\\` components must be within \\`${ROOT_NAME}\\``,\n [CONTENT_NAME]: `\\`${CONTENT_NAME}\\` must be within \\`${ROOT_NAME}\\``,\n [ITEM_NAME]: `\\`${ITEM_NAME}\\` must be within \\`${CONTENT_NAME}\\``,\n [ITEM_HANDLE_NAME]: `\\`${ITEM_HANDLE_NAME}\\` must be within \\`${ITEM_NAME}\\``,\n [OVERLAY_NAME]: `\\`${OVERLAY_NAME}\\` must be within \\`${ROOT_NAME}\\``,\n} as const;\n\ninterface SortableRootContextValue {\n id: string;\n items: UniqueIdentifier[];\n modifiers: DndContextProps[\"modifiers\"];\n strategy: SortableContextProps[\"strategy\"];\n activeId: UniqueIdentifier | null;\n setActiveId: (id: UniqueIdentifier | null) => void;\n getItemValue: (item: T) => UniqueIdentifier;\n flatCursor: boolean;\n}\n\nconst SortableRootContext =\n React.createContext | null>(null);\nSortableRootContext.displayName = ROOT_NAME;\n\nfunction useSortableContext(name: keyof typeof SORTABLE_ERRORS) {\n const context = React.useContext(SortableRootContext);\n if (!context) {\n throw new Error(SORTABLE_ERRORS[name]);\n }\n return context;\n}\n\ninterface GetItemValue {\n /**\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n * @example getItemValue={(item) => item.id}\n */\n getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableProps = DndContextProps & {\n value: T[];\n onValueChange?: (items: T[]) => void;\n onMove?: (\n event: DragEndEvent & { activeIndex: number; overIndex: number },\n ) => void;\n strategy?: SortableContextProps[\"strategy\"];\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n flatCursor?: boolean;\n} & (T extends object ? GetItemValue : Partial>);\n\nfunction Sortable(props: SortableProps) {\n const {\n value,\n onValueChange,\n collisionDetection,\n modifiers,\n strategy,\n onMove,\n orientation = \"vertical\",\n flatCursor = false,\n getItemValue: getItemValueProp,\n accessibility,\n ...sortableProps\n } = props;\n const id = React.useId();\n const [activeId, setActiveId] = React.useState(null);\n\n const sensors = useSensors(\n useSensor(MouseSensor),\n useSensor(TouchSensor),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n const config = React.useMemo(\n () => orientationConfig[orientation],\n [orientation],\n );\n\n const getItemValue = React.useCallback(\n (item: T): UniqueIdentifier => {\n if (typeof item === \"object\" && !getItemValueProp) {\n throw new Error(\n \"getItemValue is required when using array of objects.\",\n );\n }\n return getItemValueProp\n ? getItemValueProp(item)\n : (item as UniqueIdentifier);\n },\n [getItemValueProp],\n );\n\n const items = React.useMemo(() => {\n return value.map((item) => getItemValue(item));\n }, [value, getItemValue]);\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (over && active.id !== over?.id) {\n const activeIndex = value.findIndex(\n (item) => getItemValue(item) === active.id,\n );\n const overIndex = value.findIndex(\n (item) => getItemValue(item) === over.id,\n );\n\n if (onMove) {\n onMove({ ...event, activeIndex, overIndex });\n } else {\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\n }\n }\n setActiveId(null);\n },\n [value, onValueChange, onMove, getItemValue],\n );\n\n const announcements: Announcements = React.useMemo(\n () => ({\n onDragStart({ active }) {\n const activeValue = active.id.toString();\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\n },\n onDragOver({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n onDragEnd({ active, over }) {\n const activeValue = active.id.toString();\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\n }\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n },\n onDragCancel({ active }) {\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const activeValue = active.id.toString();\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\n },\n onDragMove({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n }),\n [value],\n );\n\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n () => ({\n draggable: `\n To pick up a sortable item, press space or enter.\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\n Press space or enter again to drop the item in its new position, or press escape to cancel.\n `,\n }),\n [orientation],\n );\n\n const contextValue = React.useMemo(\n () => ({\n id,\n items,\n modifiers: modifiers ?? config.modifiers,\n strategy: strategy ?? config.strategy,\n activeId,\n setActiveId,\n getItemValue,\n flatCursor,\n }),\n [\n id,\n items,\n modifiers,\n strategy,\n config.modifiers,\n config.strategy,\n activeId,\n getItemValue,\n flatCursor,\n ],\n );\n\n return (\n }\n >\n setActiveId(active.id),\n )}\n onDragEnd={composeEventHandlers(sortableProps.onDragEnd, onDragEnd)}\n onDragCancel={composeEventHandlers(sortableProps.onDragCancel, () =>\n setActiveId(null),\n )}\n accessibility={{\n announcements,\n screenReaderInstructions,\n ...accessibility,\n }}\n />\n \n );\n}\n\nconst SortableContentContext = React.createContext(false);\nSortableContentContext.displayName = CONTENT_NAME;\n\ninterface SortableContentProps extends React.ComponentPropsWithoutRef<\"div\"> {\n strategy?: SortableContextProps[\"strategy\"];\n children: React.ReactNode;\n asChild?: boolean;\n withoutSlot?: boolean;\n}\n\nconst SortableContent = React.forwardRef(\n (props, forwardedRef) => {\n const {\n strategy: strategyProp,\n asChild,\n withoutSlot,\n children,\n ...contentProps\n } = props;\n const context = useSortableContext(CONTENT_NAME);\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n {withoutSlot ? (\n children\n ) : (\n \n {children}\n \n )}\n \n \n );\n },\n);\nSortableContent.displayName = CONTENT_NAME;\n\ninterface SortableItemContextValue {\n id: string;\n attributes: React.HTMLAttributes;\n listeners: DraggableSyntheticListeners | undefined;\n setActivatorNodeRef: (node: HTMLElement | null) => void;\n isDragging?: boolean;\n disabled?: boolean;\n}\n\nconst SortableItemContext =\n React.createContext(null);\nSortableItemContext.displayName = ITEM_NAME;\n\ninterface SortableItemProps extends React.ComponentPropsWithoutRef<\"div\"> {\n value: UniqueIdentifier;\n asHandle?: boolean;\n asChild?: boolean;\n disabled?: boolean;\n}\n\nconst SortableItem = React.forwardRef(\n (props, forwardedRef) => {\n const {\n value,\n style,\n asHandle,\n asChild,\n disabled,\n className,\n ...itemProps\n } = props;\n const inSortableContent = React.useContext(SortableContentContext);\n const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n if (!inSortableContent && !inSortableOverlay) {\n throw new Error(SORTABLE_ERRORS[ITEM_NAME]);\n }\n\n if (value === \"\") {\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n }\n\n const context = useSortableContext(ITEM_NAME);\n const id = React.useId();\n const {\n attributes,\n listeners,\n setNodeRef,\n setActivatorNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: value, disabled });\n\n const composedRef = useComposedRefs(forwardedRef, (node) => {\n if (disabled) return;\n setNodeRef(node);\n if (asHandle) setActivatorNodeRef(node);\n });\n\n const composedStyle = React.useMemo(() => {\n return {\n transform: CSS.Translate.toString(transform),\n transition,\n ...style,\n };\n }, [transform, transition, style]);\n\n const itemContext = React.useMemo(\n () => ({\n id,\n attributes,\n listeners,\n setActivatorNodeRef,\n isDragging,\n disabled,\n }),\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n );\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n \n );\n },\n);\nSortableItem.displayName = ITEM_NAME;\n\ninterface SortableItemHandleProps\n extends React.ComponentPropsWithoutRef<\"button\"> {\n asChild?: boolean;\n}\n\nconst SortableItemHandle = React.forwardRef<\n HTMLButtonElement,\n SortableItemHandleProps\n>((props, forwardedRef) => {\n const { asChild, disabled, className, ...itemHandleProps } = props;\n const itemContext = React.useContext(SortableItemContext);\n if (!itemContext) {\n throw new Error(SORTABLE_ERRORS[ITEM_HANDLE_NAME]);\n }\n const context = useSortableContext(ITEM_HANDLE_NAME);\n\n const isDisabled = disabled ?? itemContext.disabled;\n\n const composedRef = useComposedRefs(forwardedRef, (node) => {\n if (!isDisabled) return;\n itemContext.setActivatorNodeRef(node);\n });\n\n const HandlePrimitive = asChild ? Slot : \"button\";\n\n return (\n \n );\n});\nSortableItemHandle.displayName = ITEM_HANDLE_NAME;\n\nconst SortableOverlayContext = React.createContext(false);\nSortableOverlayContext.displayName = OVERLAY_NAME;\n\nconst dropAnimation: DropAnimation = {\n sideEffects: defaultDropAnimationSideEffects({\n styles: {\n active: {\n opacity: \"0.4\",\n },\n },\n }),\n};\n\ninterface SortableOverlayProps\n extends Omit, \"children\"> {\n container?: Element | DocumentFragment | null;\n children?:\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n const { container: containerProp, children, ...overlayProps } = props;\n const context = useSortableContext(OVERLAY_NAME);\n\n const [mounted, setMounted] = React.useState(false);\n React.useLayoutEffect(() => setMounted(true), []);\n\n const container =\n containerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!container) return null;\n\n return ReactDOM.createPortal(\n \n \n {context.activeId\n ? typeof children === \"function\"\n ? children({ value: context.activeId })\n : children\n : null}\n \n ,\n container,\n );\n}\n\nconst Root = Sortable;\nconst Content = SortableContent;\nconst Item = SortableItem;\nconst ItemHandle = SortableItemHandle;\nconst Overlay = SortableOverlay;\n\nexport {\n Root,\n Content,\n Item,\n ItemHandle,\n Overlay,\n //\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n};\n", - "type": "registry:ui" + "type": "registry:ui", + "target": "src/components/ui/sortable.tsx" }, { "path": "src/components/ui/faceted.tsx", "content": "\"use client\";\n\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\n\ntype FacetedValue = Multiple extends true\n ? string[]\n : string;\n\ninterface FacetedContextValue {\n value?: FacetedValue;\n onItemSelect?: (value: string) => void;\n multiple?: Multiple;\n}\n\nconst FacetedContext = React.createContext | null>(\n null,\n);\n\nfunction useFacetedContext(name: string) {\n const context = React.useContext(FacetedContext);\n if (!context) {\n throw new Error(`\\`${name}\\` must be within Faceted`);\n }\n return context;\n}\n\ninterface FacetedProps\n extends React.ComponentProps {\n value?: FacetedValue;\n onValueChange?: (value: FacetedValue | undefined) => void;\n children?: React.ReactNode;\n multiple?: Multiple;\n}\n\nfunction Faceted(\n props: FacetedProps,\n) {\n const {\n open: openProp,\n onOpenChange: onOpenChangeProp,\n value,\n onValueChange,\n children,\n multiple = false,\n ...facetedProps\n } = props;\n\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);\n const isControlled = openProp !== undefined;\n const open = isControlled ? openProp : uncontrolledOpen;\n\n const onOpenChange = React.useCallback(\n (newOpen: boolean) => {\n if (!isControlled) {\n setUncontrolledOpen(newOpen);\n }\n onOpenChangeProp?.(newOpen);\n },\n [isControlled, onOpenChangeProp],\n );\n\n const onItemSelect = React.useCallback(\n (selectedValue: string) => {\n if (!onValueChange) return;\n\n if (multiple) {\n const currentValue = (Array.isArray(value) ? value : []) as string[];\n const newValue = currentValue.includes(selectedValue)\n ? currentValue.filter((v) => v !== selectedValue)\n : [...currentValue, selectedValue];\n onValueChange(newValue as FacetedValue);\n } else {\n if (value === selectedValue) {\n onValueChange(undefined);\n } else {\n onValueChange(selectedValue as FacetedValue);\n }\n\n requestAnimationFrame(() => onOpenChange(false));\n }\n },\n [multiple, value, onValueChange, onOpenChange],\n );\n\n const contextValue = React.useMemo>(\n () => ({ value, onItemSelect, multiple }),\n [value, onItemSelect, multiple],\n );\n\n return (\n \n \n {children}\n \n \n );\n}\n\nfunction FacetedTrigger(props: React.ComponentProps) {\n const { className, children, ...triggerProps } = props;\n\n return (\n \n {children}\n \n );\n}\n\ninterface FacetedBadgeListProps extends React.ComponentProps<\"div\"> {\n options?: { label: string; value: string }[];\n max?: number;\n badgeClassName?: string;\n placeholder?: string;\n}\n\nfunction FacetedBadgeList(props: FacetedBadgeListProps) {\n const {\n options = [],\n max = 2,\n placeholder = \"Select options...\",\n className,\n badgeClassName,\n ...badgeListProps\n } = props;\n\n const context = useFacetedContext(\"FacetedBadgeList\");\n const values = Array.isArray(context.value)\n ? context.value\n : ([context.value].filter(Boolean) as string[]);\n\n const getLabel = React.useCallback(\n (value: string) => {\n const option = options.find((opt) => opt.value === value);\n return option?.label ?? value;\n },\n [options],\n );\n\n if (!values || values.length === 0) {\n return (\n \n {placeholder}\n \n
\n );\n }\n\n return (\n \n {values.length > max ? (\n \n {values.length} selected\n \n ) : (\n values.map((value) => (\n \n {getLabel(value)}\n \n ))\n )}\n
\n );\n}\n\nfunction FacetedContent(props: React.ComponentProps) {\n const { className, children, ...contentProps } = props;\n\n return (\n \n {children}\n \n );\n}\n\nconst FacetedInput = CommandInput;\n\nconst FacetedList = CommandList;\n\nconst FacetedEmpty = CommandEmpty;\n\nconst FacetedGroup = CommandGroup;\n\ninterface FacetedItemProps extends React.ComponentProps {\n value: string;\n}\n\nfunction FacetedItem(props: FacetedItemProps) {\n const { value, onSelect, className, children, ...itemProps } = props;\n const context = useFacetedContext(\"FacetedItem\");\n\n const isSelected = context.multiple\n ? Array.isArray(context.value) && context.value.includes(value)\n : context.value === value;\n\n const onItemSelect = React.useCallback(\n (currentValue: string) => {\n if (onSelect) {\n onSelect(currentValue);\n } else if (context.onItemSelect) {\n context.onItemSelect(currentValue);\n }\n },\n [onSelect, context.onItemSelect],\n );\n\n return (\n onItemSelect(value)}\n {...itemProps}\n >\n \n \n \n {children}\n \n );\n}\n\nconst FacetedSeparator = CommandSeparator;\n\nexport {\n Faceted,\n FacetedBadgeList,\n FacetedContent,\n FacetedEmpty,\n FacetedGroup,\n FacetedInput,\n FacetedItem,\n FacetedList,\n FacetedSeparator,\n FacetedTrigger,\n};\n", - "type": "registry:ui" + "type": "registry:ui", + "target": "src/components/ui/faceted.tsx" }, { "path": "src/hooks/use-callback-ref.ts", diff --git a/public/r/data-table-filter-menu.json b/public/r/data-table-filter-menu.json index f1409b19f..94b664bf6 100644 --- a/public/r/data-table-filter-menu.json +++ b/public/r/data-table-filter-menu.json @@ -22,19 +22,22 @@ ], "files": [ { - "path": "src/components/data-table-filter-menu.tsx", - "content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport {\n BadgeCheck,\n CalendarIcon,\n Check,\n ListFilter,\n Text,\n X,\n} from \"lucide-react\";\nimport { useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport { DataTableRangeFilter } from \"@/components/data-table-range-filter\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\nimport type { ExtendedColumnFilter, FilterOperator } from \"@/types/data-table\";\n\nconst FILTERS_KEY = \"filters\";\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst OPEN_MENU_SHORTCUT = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterMenuProps\n extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n}\n\nexport function DataTableFilterMenu({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n align = \"start\",\n ...props\n}: DataTableFilterMenuProps) {\n const id = React.useId();\n\n const columns = React.useMemo(() => {\n return table\n .getAllColumns()\n .filter((column) => column.columnDef.enableColumnFilter);\n }, [table]);\n\n const [open, setOpen] = React.useState(false);\n const [selectedColumn, setSelectedColumn] =\n React.useState | null>(null);\n const [inputValue, setInputValue] = React.useState(\"\");\n const triggerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n\n const onOpenChange = React.useCallback((open: boolean) => {\n setOpen(open);\n\n if (!open) {\n setTimeout(() => {\n setSelectedColumn(null);\n setInputValue(\"\");\n }, 100);\n }\n }, []);\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n !inputValue &&\n selectedColumn\n ) {\n event.preventDefault();\n setSelectedColumn(null);\n }\n },\n [inputValue, selectedColumn],\n );\n\n const [filters, setFilters] = useQueryState(\n FILTERS_KEY,\n getFiltersStateParser(columns.map((field) => field.id))\n .withDefault([])\n .withOptions({\n clearOnDefault: true,\n shallow,\n throttleMs,\n }),\n );\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\n\n const onFilterAdd = React.useCallback(\n (column: Column, value: string) => {\n if (!value.trim() && column.columnDef.meta?.variant !== \"boolean\") {\n return;\n }\n\n const filterValue =\n column.columnDef.meta?.variant === \"multiSelect\" ? [value] : value;\n\n const newFilter: ExtendedColumnFilter = {\n id: column.id as Extract,\n value: filterValue,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n filterId: generateId({ length: 8 }),\n };\n\n debouncedSetFilters([...filters, newFilter]);\n setOpen(false);\n\n setTimeout(() => {\n setSelectedColumn(null);\n setInputValue(\"\");\n }, 100);\n },\n [filters, debouncedSetFilters],\n );\n\n const onFilterRemove = React.useCallback(\n (filterId: string) => {\n const updatedFilters = filters.filter(\n (filter) => filter.filterId !== filterId,\n );\n debouncedSetFilters(updatedFilters);\n requestAnimationFrame(() => {\n triggerRef.current?.focus();\n });\n },\n [filters, debouncedSetFilters],\n );\n\n const onFilterUpdate = React.useCallback(\n (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => {\n debouncedSetFilters((prevFilters) => {\n const updatedFilters = prevFilters.map((filter) => {\n if (filter.filterId === filterId) {\n return { ...filter, ...updates } as ExtendedColumnFilter;\n }\n return filter;\n });\n return updatedFilters;\n });\n },\n [debouncedSetFilters],\n );\n\n const onFiltersReset = React.useCallback(() => {\n debouncedSetFilters([]);\n }, [debouncedSetFilters]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.shiftKey\n ) {\n event.preventDefault();\n setOpen(true);\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n event.shiftKey &&\n !open &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [open, filters, onFilterRemove]);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n },\n [filters, onFilterRemove],\n );\n\n return (\n
\n {filters.map((filter) => (\n \n ))}\n {filters.length > 0 && (\n \n \n \n )}\n \n \n 0 ? \"icon\" : \"sm\"}\n className={cn(filters.length > 0 && \"size-8\", \"h-8\")}\n ref={triggerRef}\n onKeyDown={onTriggerKeyDown}\n >\n \n {filters.length > 0 ? null : \"Filter\"}\n \n \n \n \n \n \n {selectedColumn ? (\n <>\n {selectedColumn.columnDef.meta?.options && (\n No options found.\n )}\n onFilterAdd(selectedColumn, value)}\n />\n \n ) : (\n <>\n No fields found.\n \n {columns.map((column) => (\n {\n setSelectedColumn(column);\n setInputValue(\"\");\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n }}\n >\n {column.columnDef.meta?.icon && (\n \n )}\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n ))}\n \n \n )}\n \n \n \n \n
\n );\n}\n\ninterface DataTableFilterItemProps {\n filter: ExtendedColumnFilter;\n filterItemId: string;\n columns: Column[];\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataTableFilterItem({\n filter,\n filterItemId,\n columns,\n onFilterUpdate,\n onFilterRemove,\n}: DataTableFilterItemProps) {\n {\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showOperatorSelector, setShowOperatorSelector] =\n React.useState(false);\n const [showValueSelector, setShowValueSelector] = React.useState(false);\n\n const column = columns.find((column) => column.id === filter.id);\n if (!column) return null;\n\n const operatorListboxId = `${filterItemId}-operator-listbox`;\n const inputId = `${filterItemId}-input`;\n\n const columnMeta = column.columnDef.meta;\n const filterOperators = getFilterOperators(filter.variant);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\n return;\n }\n\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onFilterRemove(filter.filterId);\n }\n },\n [\n filter.filterId,\n showFieldSelector,\n showOperatorSelector,\n showValueSelector,\n onFilterRemove,\n ],\n );\n\n return (\n \n \n \n \n {columnMeta?.icon && (\n \n )}\n {columnMeta?.label ?? column.id}\n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n {\n onFilterUpdate(filter.filterId, {\n id: column.id as Extract,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n value: \"\",\n });\n\n setShowFieldSelector(false);\n }}\n >\n {column.columnDef.meta?.icon && (\n \n )}\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n \n ))}\n \n \n \n \n \n \n onFilterUpdate(filter.filterId, {\n operator: value,\n value:\n value === \"isEmpty\" || value === \"isNotEmpty\"\n ? \"\"\n : filter.value,\n })\n }\n >\n \n \n \n \n {filterOperators.map((operator) => (\n \n {operator.label}\n \n ))}\n \n \n {onFilterInputRender({\n filter,\n column,\n inputId,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n })}\n onFilterRemove(filter.filterId)}\n >\n \n \n
\n );\n }\n}\n\ninterface FilterValueSelectorProps {\n column: Column;\n value: string;\n onSelect: (value: string) => void;\n}\n\nfunction FilterValueSelector({\n column,\n value,\n onSelect,\n}: FilterValueSelectorProps) {\n const variant = column.columnDef.meta?.variant ?? \"text\";\n\n switch (variant) {\n case \"boolean\":\n return (\n \n onSelect(\"true\")}>\n True\n \n onSelect(\"false\")}>\n False\n \n \n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \n {column.columnDef.meta?.options?.map((option) => (\n onSelect(option.value)}\n >\n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n ))}\n \n );\n\n case \"date\":\n case \"dateRange\":\n return (\n onSelect(date?.getTime().toString() ?? \"\")}\n />\n );\n\n default: {\n const isEmpty = !value.trim();\n\n return (\n \n onSelect(value)}\n disabled={isEmpty}\n >\n {isEmpty ? (\n <>\n \n Type to add filter...\n \n ) : (\n <>\n \n Filter by "{value}"\n \n )}\n \n \n );\n }\n }\n}\n\nfunction onFilterInputRender({\n filter,\n column,\n inputId,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n}: {\n filter: ExtendedColumnFilter;\n column: Column;\n inputId: string;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n showValueSelector: boolean;\n setShowValueSelector: (value: boolean) => void;\n}) {\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\n return (\n \n );\n }\n\n switch (filter.variant) {\n case \"text\":\n case \"number\":\n case \"range\": {\n if (\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\n filter.operator === \"isBetween\"\n ) {\n return (\n \n );\n }\n\n const isNumber =\n filter.variant === \"number\" || filter.variant === \"range\";\n\n return (\n \n onFilterUpdate(filter.filterId, { value: event.target.value })\n }\n />\n );\n }\n\n case \"boolean\": {\n const inputListboxId = `${inputId}-listbox`;\n\n return (\n \n onFilterUpdate(filter.filterId, { value })\n }\n >\n \n \n \n \n True\n False\n \n \n );\n }\n\n case \"select\":\n case \"multiSelect\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const options = column.columnDef.meta?.options ?? [];\n const selectedValues = Array.isArray(filter.value)\n ? filter.value\n : [filter.value];\n\n const selectedOptions = options.filter((option) =>\n selectedValues.includes(option.value),\n );\n\n return (\n \n \n \n {selectedOptions.length === 0 ? (\n filter.variant === \"multiSelect\" ? (\n \"Select options...\"\n ) : (\n \"Select option...\"\n )\n ) : (\n <>\n
\n {selectedOptions.map((selectedOption) =>\n selectedOption.icon ? (\n \n \n
\n ) : null,\n )}\n
\n \n {selectedOptions.length > 1\n ? `${selectedOptions.length} selected`\n : selectedOptions[0]?.label}\n \n \n )}\n \n \n \n \n \n \n No options found.\n \n {options.map((option) => (\n {\n const value =\n filter.variant === \"multiSelect\"\n ? selectedValues.includes(option.value)\n ? selectedValues.filter((v) => v !== option.value)\n : [...selectedValues, option.value]\n : option.value;\n onFilterUpdate(filter.filterId, { value });\n }}\n >\n {option.icon && }\n {option.label}\n {filter.variant === \"multiSelect\" && (\n \n )}\n \n ))}\n \n \n \n \n \n );\n }\n\n case \"date\":\n case \"dateRange\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const dateValue = Array.isArray(filter.value)\n ? filter.value.filter(Boolean)\n : [filter.value, filter.value].filter(Boolean);\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\n new Date(Number(dateValue[1])),\n )}`\n : dateValue[0]\n ? formatDate(new Date(Number(dateValue[0])))\n : \"Pick date...\";\n\n return (\n \n \n \n \n {displayValue}\n \n \n \n {filter.operator === \"isBetween\" ? (\n {\n onFilterUpdate(filter.filterId, {\n value: date\n ? [\n (date.from?.getTime() ?? \"\").toString(),\n (date.to?.getTime() ?? \"\").toString(),\n ]\n : [],\n });\n }}\n />\n ) : (\n {\n onFilterUpdate(filter.filterId, {\n value: (date?.getTime() ?? \"\").toString(),\n });\n }}\n />\n )}\n \n \n );\n }\n\n default:\n return null;\n }\n}\n", - "type": "registry:component" + "path": "src/components/data-table/data-table-filter-menu.tsx", + "content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport {\n BadgeCheck,\n CalendarIcon,\n Check,\n ListFilter,\n Text,\n X,\n} from \"lucide-react\";\nimport { useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport { DataTableRangeFilter } from \"@/components/data-table/data-table-range-filter\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\nimport type { ExtendedColumnFilter, FilterOperator } from \"@/types/data-table\";\n\nconst FILTERS_KEY = \"filters\";\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst OPEN_MENU_SHORTCUT = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterMenuProps\n extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n}\n\nexport function DataTableFilterMenu({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n align = \"start\",\n ...props\n}: DataTableFilterMenuProps) {\n const id = React.useId();\n\n const columns = React.useMemo(() => {\n return table\n .getAllColumns()\n .filter((column) => column.columnDef.enableColumnFilter);\n }, [table]);\n\n const [open, setOpen] = React.useState(false);\n const [selectedColumn, setSelectedColumn] =\n React.useState | null>(null);\n const [inputValue, setInputValue] = React.useState(\"\");\n const triggerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n\n const onOpenChange = React.useCallback((open: boolean) => {\n setOpen(open);\n\n if (!open) {\n setTimeout(() => {\n setSelectedColumn(null);\n setInputValue(\"\");\n }, 100);\n }\n }, []);\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n !inputValue &&\n selectedColumn\n ) {\n event.preventDefault();\n setSelectedColumn(null);\n }\n },\n [inputValue, selectedColumn],\n );\n\n const [filters, setFilters] = useQueryState(\n FILTERS_KEY,\n getFiltersStateParser(columns.map((field) => field.id))\n .withDefault([])\n .withOptions({\n clearOnDefault: true,\n shallow,\n throttleMs,\n }),\n );\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\n\n const onFilterAdd = React.useCallback(\n (column: Column, value: string) => {\n if (!value.trim() && column.columnDef.meta?.variant !== \"boolean\") {\n return;\n }\n\n const filterValue =\n column.columnDef.meta?.variant === \"multiSelect\" ? [value] : value;\n\n const newFilter: ExtendedColumnFilter = {\n id: column.id as Extract,\n value: filterValue,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n filterId: generateId({ length: 8 }),\n };\n\n debouncedSetFilters([...filters, newFilter]);\n setOpen(false);\n\n setTimeout(() => {\n setSelectedColumn(null);\n setInputValue(\"\");\n }, 100);\n },\n [filters, debouncedSetFilters],\n );\n\n const onFilterRemove = React.useCallback(\n (filterId: string) => {\n const updatedFilters = filters.filter(\n (filter) => filter.filterId !== filterId,\n );\n debouncedSetFilters(updatedFilters);\n requestAnimationFrame(() => {\n triggerRef.current?.focus();\n });\n },\n [filters, debouncedSetFilters],\n );\n\n const onFilterUpdate = React.useCallback(\n (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => {\n debouncedSetFilters((prevFilters) => {\n const updatedFilters = prevFilters.map((filter) => {\n if (filter.filterId === filterId) {\n return { ...filter, ...updates } as ExtendedColumnFilter;\n }\n return filter;\n });\n return updatedFilters;\n });\n },\n [debouncedSetFilters],\n );\n\n const onFiltersReset = React.useCallback(() => {\n debouncedSetFilters([]);\n }, [debouncedSetFilters]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.shiftKey\n ) {\n event.preventDefault();\n setOpen(true);\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n event.shiftKey &&\n !open &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [open, filters, onFilterRemove]);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n },\n [filters, onFilterRemove],\n );\n\n return (\n
\n {filters.map((filter) => (\n \n ))}\n {filters.length > 0 && (\n \n \n \n )}\n \n \n 0 ? \"icon\" : \"sm\"}\n className={cn(filters.length > 0 && \"size-8\", \"h-8\")}\n ref={triggerRef}\n onKeyDown={onTriggerKeyDown}\n >\n \n {filters.length > 0 ? null : \"Filter\"}\n \n \n \n \n \n \n {selectedColumn ? (\n <>\n {selectedColumn.columnDef.meta?.options && (\n No options found.\n )}\n onFilterAdd(selectedColumn, value)}\n />\n \n ) : (\n <>\n No fields found.\n \n {columns.map((column) => (\n {\n setSelectedColumn(column);\n setInputValue(\"\");\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n }}\n >\n {column.columnDef.meta?.icon && (\n \n )}\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n ))}\n \n \n )}\n \n \n \n \n
\n );\n}\n\ninterface DataTableFilterItemProps {\n filter: ExtendedColumnFilter;\n filterItemId: string;\n columns: Column[];\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataTableFilterItem({\n filter,\n filterItemId,\n columns,\n onFilterUpdate,\n onFilterRemove,\n}: DataTableFilterItemProps) {\n {\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showOperatorSelector, setShowOperatorSelector] =\n React.useState(false);\n const [showValueSelector, setShowValueSelector] = React.useState(false);\n\n const column = columns.find((column) => column.id === filter.id);\n if (!column) return null;\n\n const operatorListboxId = `${filterItemId}-operator-listbox`;\n const inputId = `${filterItemId}-input`;\n\n const columnMeta = column.columnDef.meta;\n const filterOperators = getFilterOperators(filter.variant);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\n return;\n }\n\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onFilterRemove(filter.filterId);\n }\n },\n [\n filter.filterId,\n showFieldSelector,\n showOperatorSelector,\n showValueSelector,\n onFilterRemove,\n ],\n );\n\n return (\n \n \n \n \n {columnMeta?.icon && (\n \n )}\n {columnMeta?.label ?? column.id}\n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n {\n onFilterUpdate(filter.filterId, {\n id: column.id as Extract,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n value: \"\",\n });\n\n setShowFieldSelector(false);\n }}\n >\n {column.columnDef.meta?.icon && (\n \n )}\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n \n ))}\n \n \n \n \n \n \n onFilterUpdate(filter.filterId, {\n operator: value,\n value:\n value === \"isEmpty\" || value === \"isNotEmpty\"\n ? \"\"\n : filter.value,\n })\n }\n >\n \n \n \n \n {filterOperators.map((operator) => (\n \n {operator.label}\n \n ))}\n \n \n {onFilterInputRender({\n filter,\n column,\n inputId,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n })}\n onFilterRemove(filter.filterId)}\n >\n \n \n
\n );\n }\n}\n\ninterface FilterValueSelectorProps {\n column: Column;\n value: string;\n onSelect: (value: string) => void;\n}\n\nfunction FilterValueSelector({\n column,\n value,\n onSelect,\n}: FilterValueSelectorProps) {\n const variant = column.columnDef.meta?.variant ?? \"text\";\n\n switch (variant) {\n case \"boolean\":\n return (\n \n onSelect(\"true\")}>\n True\n \n onSelect(\"false\")}>\n False\n \n \n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \n {column.columnDef.meta?.options?.map((option) => (\n onSelect(option.value)}\n >\n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n ))}\n \n );\n\n case \"date\":\n case \"dateRange\":\n return (\n onSelect(date?.getTime().toString() ?? \"\")}\n />\n );\n\n default: {\n const isEmpty = !value.trim();\n\n return (\n \n onSelect(value)}\n disabled={isEmpty}\n >\n {isEmpty ? (\n <>\n \n Type to add filter...\n \n ) : (\n <>\n \n Filter by "{value}"\n \n )}\n \n \n );\n }\n }\n}\n\nfunction onFilterInputRender({\n filter,\n column,\n inputId,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n}: {\n filter: ExtendedColumnFilter;\n column: Column;\n inputId: string;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n showValueSelector: boolean;\n setShowValueSelector: (value: boolean) => void;\n}) {\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\n return (\n \n );\n }\n\n switch (filter.variant) {\n case \"text\":\n case \"number\":\n case \"range\": {\n if (\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\n filter.operator === \"isBetween\"\n ) {\n return (\n \n );\n }\n\n const isNumber =\n filter.variant === \"number\" || filter.variant === \"range\";\n\n return (\n \n onFilterUpdate(filter.filterId, { value: event.target.value })\n }\n />\n );\n }\n\n case \"boolean\": {\n const inputListboxId = `${inputId}-listbox`;\n\n return (\n \n onFilterUpdate(filter.filterId, { value })\n }\n >\n \n \n \n \n True\n False\n \n \n );\n }\n\n case \"select\":\n case \"multiSelect\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const options = column.columnDef.meta?.options ?? [];\n const selectedValues = Array.isArray(filter.value)\n ? filter.value\n : [filter.value];\n\n const selectedOptions = options.filter((option) =>\n selectedValues.includes(option.value),\n );\n\n return (\n \n \n \n {selectedOptions.length === 0 ? (\n filter.variant === \"multiSelect\" ? (\n \"Select options...\"\n ) : (\n \"Select option...\"\n )\n ) : (\n <>\n
\n {selectedOptions.map((selectedOption) =>\n selectedOption.icon ? (\n \n \n
\n ) : null,\n )}\n
\n \n {selectedOptions.length > 1\n ? `${selectedOptions.length} selected`\n : selectedOptions[0]?.label}\n \n \n )}\n \n \n \n \n \n \n No options found.\n \n {options.map((option) => (\n {\n const value =\n filter.variant === \"multiSelect\"\n ? selectedValues.includes(option.value)\n ? selectedValues.filter((v) => v !== option.value)\n : [...selectedValues, option.value]\n : option.value;\n onFilterUpdate(filter.filterId, { value });\n }}\n >\n {option.icon && }\n {option.label}\n {filter.variant === \"multiSelect\" && (\n \n )}\n \n ))}\n \n \n \n \n \n );\n }\n\n case \"date\":\n case \"dateRange\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const dateValue = Array.isArray(filter.value)\n ? filter.value.filter(Boolean)\n : [filter.value, filter.value].filter(Boolean);\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\n new Date(Number(dateValue[1])),\n )}`\n : dateValue[0]\n ? formatDate(new Date(Number(dateValue[0])))\n : \"Pick date...\";\n\n return (\n \n \n \n \n {displayValue}\n \n \n \n {filter.operator === \"isBetween\" ? (\n {\n onFilterUpdate(filter.filterId, {\n value: date\n ? [\n (date.from?.getTime() ?? \"\").toString(),\n (date.to?.getTime() ?? \"\").toString(),\n ]\n : [],\n });\n }}\n />\n ) : (\n {\n onFilterUpdate(filter.filterId, {\n value: (date?.getTime() ?? \"\").toString(),\n });\n }}\n />\n )}\n \n \n );\n }\n\n default:\n return null;\n }\n}\n", + "type": "registry:component", + "target": "src/components/data-table/data-table-filter-menu.tsx" }, { - "path": "src/components/data-table-range-filter.tsx", + "path": "src/components/data-table/data-table-range-filter.tsx", "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport type { ExtendedColumnFilter } from \"@/types/data-table\";\n\ninterface DataTableRangeFilterProps extends React.ComponentProps<\"div\"> {\n filter: ExtendedColumnFilter;\n column: Column;\n inputId: string;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n}\n\nexport function DataTableRangeFilter({\n filter,\n column,\n inputId,\n onFilterUpdate,\n className,\n ...props\n}: DataTableRangeFilterProps) {\n const meta = column.columnDef.meta;\n\n const [min, max] = React.useMemo(() => {\n const range = column.columnDef.meta?.range;\n if (range) return range;\n\n const values = column.getFacetedMinMaxValues();\n if (!values) return [0, 100];\n\n return [values[0], values[1]];\n }, [column]);\n\n const formatValue = React.useCallback(\n (value: string | number | undefined) => {\n if (value === undefined || value === \"\") return \"\";\n const numValue = Number(value);\n return Number.isNaN(numValue)\n ? \"\"\n : numValue.toLocaleString(undefined, {\n maximumFractionDigits: 0,\n });\n },\n [],\n );\n\n const value = React.useMemo(() => {\n if (Array.isArray(filter.value)) return filter.value.map(formatValue);\n return [formatValue(filter.value), \"\"];\n }, [filter.value, formatValue]);\n\n const onRangeValueChange = React.useCallback(\n (value: string, isMin?: boolean) => {\n const numValue = Number(value);\n const currentValues = Array.isArray(filter.value)\n ? filter.value\n : [\"\", \"\"];\n const otherValue = isMin\n ? (currentValues[1] ?? \"\")\n : (currentValues[0] ?? \"\");\n\n if (\n value === \"\" ||\n (!Number.isNaN(numValue) &&\n (isMin\n ? numValue >= min && numValue <= (Number(otherValue) || max)\n : numValue <= max && numValue >= (Number(otherValue) || min)))\n ) {\n onFilterUpdate(filter.filterId, {\n value: isMin ? [value, otherValue] : [otherValue, value],\n });\n }\n },\n [filter.filterId, filter.value, min, max, onFilterUpdate],\n );\n\n return (\n \n onRangeValueChange(event.target.value, true)}\n />\n to\n onRangeValueChange(event.target.value)}\n />\n \n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-range-filter.tsx" }, { - "path": "src/components/data-table-advanced-toolbar.tsx", - "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTableViewOptions } from \"@/components/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps\n extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableAdvancedToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableAdvancedToolbarProps) {\n return (\n \n
{children}
\n
\n \n
\n \n );\n}\n", - "type": "registry:component" + "path": "src/components/data-table/data-table-advanced-toolbar.tsx", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps\n extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableAdvancedToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableAdvancedToolbarProps) {\n return (\n \n
{children}
\n
\n \n
\n \n );\n}\n", + "type": "registry:component", + "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, { "path": "src/hooks/use-callback-ref.ts", diff --git a/public/r/data-table-sort-list.json b/public/r/data-table-sort-list.json index 9a4bd92f9..670aa8a0a 100644 --- a/public/r/data-table-sort-list.json +++ b/public/r/data-table-sort-list.json @@ -17,14 +17,16 @@ ], "files": [ { - "path": "src/components/data-table-sort-list.tsx", + "path": "src/components/data-table/data-table-sort-list.tsx", "content": "\"use client\";\n\nimport type { ColumnSort, SortDirection, Table } from \"@tanstack/react-table\";\nimport {\n ArrowDownUp,\n ChevronsUpDown,\n GripVertical,\n Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport { cn } from \"@/lib/utils\";\n\nconst OPEN_MENU_SHORTCUT = \"s\";\nconst REMOVE_SORT_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableSortListProps\n extends React.ComponentProps {\n table: Table;\n}\n\nexport function DataTableSortList({\n table,\n ...props\n}: DataTableSortListProps) {\n const id = React.useId();\n const labelId = React.useId();\n const descriptionId = React.useId();\n const [open, setOpen] = React.useState(false);\n const addButtonRef = React.useRef(null);\n\n const sorting = table.getState().sorting;\n const onSortingChange = table.setSorting;\n\n const { columnLabels, columns } = React.useMemo(() => {\n const labels = new Map();\n const sortingIds = new Set(sorting.map((s) => s.id));\n const availableColumns: { id: string; label: string }[] = [];\n\n for (const column of table.getAllColumns()) {\n if (!column.getCanSort()) continue;\n\n const label = column.columnDef.meta?.label ?? column.id;\n labels.set(column.id, label);\n\n if (!sortingIds.has(column.id)) {\n availableColumns.push({ id: column.id, label });\n }\n }\n\n return {\n columnLabels: labels,\n columns: availableColumns,\n };\n }, [sorting, table]);\n\n const onSortAdd = React.useCallback(() => {\n const firstColumn = columns[0];\n if (!firstColumn) return;\n\n onSortingChange((prevSorting) => [\n ...prevSorting,\n { id: firstColumn.id, desc: false },\n ]);\n }, [columns, onSortingChange]);\n\n const onSortUpdate = React.useCallback(\n (sortId: string, updates: Partial) => {\n onSortingChange((prevSorting) => {\n if (!prevSorting) return prevSorting;\n return prevSorting.map((sort) =>\n sort.id === sortId ? { ...sort, ...updates } : sort,\n );\n });\n },\n [onSortingChange],\n );\n\n const onSortRemove = React.useCallback(\n (sortId: string) => {\n onSortingChange((prevSorting) =>\n prevSorting.filter((item) => item.id !== sortId),\n );\n },\n [onSortingChange],\n );\n\n const onSortingReset = React.useCallback(\n () => onSortingChange(table.initialState.sorting),\n [onSortingChange, table.initialState.sorting],\n );\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.shiftKey\n ) {\n event.preventDefault();\n setOpen(true);\n }\n\n if (\n event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&\n event.shiftKey &&\n sorting.length > 0\n ) {\n event.preventDefault();\n onSortingReset();\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [sorting.length, onSortingReset]);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase()) &&\n sorting.length > 0\n ) {\n event.preventDefault();\n onSortingReset();\n }\n },\n [sorting.length, onSortingReset],\n );\n\n return (\n item.id}\n >\n \n \n \n \n \n
\n

\n {sorting.length > 0 ? \"Sort by\" : \"No sorting applied\"}\n

\n 0 && \"sr-only\",\n )}\n >\n {sorting.length > 0\n ? \"Modify sorting to organize your rows.\"\n : \"Add sorting to organize your rows.\"}\n

\n
\n {sorting.length > 0 && (\n \n \n {sorting.map((sort) => (\n \n ))}\n \n \n )}\n
\n \n Add sort\n \n {sorting.length > 0 && (\n \n Reset sorting\n \n )}\n
\n \n
\n \n
\n
\n
\n
\n
\n
\n \n \n );\n}\n\ninterface DataTableSortItemProps {\n sort: ColumnSort;\n sortItemId: string;\n columns: { id: string; label: string }[];\n columnLabels: Map;\n onSortUpdate: (sortId: string, updates: Partial) => void;\n onSortRemove: (sortId: string) => void;\n}\n\nfunction DataTableSortItem({\n sort,\n sortItemId,\n columns,\n columnLabels,\n onSortUpdate,\n onSortRemove,\n}: DataTableSortItemProps) {\n const fieldListboxId = `${sortItemId}-field-listbox`;\n const fieldTriggerId = `${sortItemId}-field-trigger`;\n const directionListboxId = `${sortItemId}-direction-listbox`;\n\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showDirectionSelector, setShowDirectionSelector] =\n React.useState(false);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showDirectionSelector) {\n return;\n }\n\n if (REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onSortRemove(sort.id);\n }\n },\n [sort.id, showFieldSelector, showDirectionSelector, onSortRemove],\n );\n\n return (\n \n \n \n \n \n {columnLabels.get(sort.id)}\n \n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n onSortUpdate(sort.id, { id: value })}\n >\n {column.label}\n \n ))}\n \n \n \n \n \n \n onSortUpdate(sort.id, { desc: value === \"desc\" })\n }\n >\n \n \n \n \n {dataTableConfig.sortOrders.map((order) => (\n \n {order.label}\n \n ))}\n \n \n onSortRemove(sort.id)}\n >\n \n \n \n \n \n \n \n
\n \n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-sort-list.tsx" }, { "path": "src/components/ui/sortable.tsx", "content": "\"use client\";\n\nimport {\n type Announcements,\n DndContext,\n type DndContextProps,\n type DragEndEvent,\n DragOverlay,\n type DraggableSyntheticListeners,\n type DropAnimation,\n KeyboardSensor,\n MouseSensor,\n type ScreenReaderInstructions,\n TouchSensor,\n type UniqueIdentifier,\n closestCenter,\n closestCorners,\n defaultDropAnimationSideEffects,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport {\n restrictToHorizontalAxis,\n restrictToParentElement,\n restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n SortableContext,\n type SortableContextProps,\n arrayMove,\n horizontalListSortingStrategy,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\n\nimport { composeEventHandlers, useComposedRefs } from \"@/lib/composition\";\nimport { cn } from \"@/lib/utils\";\nimport * as ReactDOM from \"react-dom\";\n\nconst orientationConfig = {\n vertical: {\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\n strategy: verticalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n horizontal: {\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n strategy: horizontalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n mixed: {\n modifiers: [restrictToParentElement],\n strategy: undefined,\n collisionDetection: closestCorners,\n },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\nconst SORTABLE_ERRORS = {\n [ROOT_NAME]: `\\`${ROOT_NAME}\\` components must be within \\`${ROOT_NAME}\\``,\n [CONTENT_NAME]: `\\`${CONTENT_NAME}\\` must be within \\`${ROOT_NAME}\\``,\n [ITEM_NAME]: `\\`${ITEM_NAME}\\` must be within \\`${CONTENT_NAME}\\``,\n [ITEM_HANDLE_NAME]: `\\`${ITEM_HANDLE_NAME}\\` must be within \\`${ITEM_NAME}\\``,\n [OVERLAY_NAME]: `\\`${OVERLAY_NAME}\\` must be within \\`${ROOT_NAME}\\``,\n} as const;\n\ninterface SortableRootContextValue {\n id: string;\n items: UniqueIdentifier[];\n modifiers: DndContextProps[\"modifiers\"];\n strategy: SortableContextProps[\"strategy\"];\n activeId: UniqueIdentifier | null;\n setActiveId: (id: UniqueIdentifier | null) => void;\n getItemValue: (item: T) => UniqueIdentifier;\n flatCursor: boolean;\n}\n\nconst SortableRootContext =\n React.createContext | null>(null);\nSortableRootContext.displayName = ROOT_NAME;\n\nfunction useSortableContext(name: keyof typeof SORTABLE_ERRORS) {\n const context = React.useContext(SortableRootContext);\n if (!context) {\n throw new Error(SORTABLE_ERRORS[name]);\n }\n return context;\n}\n\ninterface GetItemValue {\n /**\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n * @example getItemValue={(item) => item.id}\n */\n getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableProps = DndContextProps & {\n value: T[];\n onValueChange?: (items: T[]) => void;\n onMove?: (\n event: DragEndEvent & { activeIndex: number; overIndex: number },\n ) => void;\n strategy?: SortableContextProps[\"strategy\"];\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n flatCursor?: boolean;\n} & (T extends object ? GetItemValue : Partial>);\n\nfunction Sortable(props: SortableProps) {\n const {\n value,\n onValueChange,\n collisionDetection,\n modifiers,\n strategy,\n onMove,\n orientation = \"vertical\",\n flatCursor = false,\n getItemValue: getItemValueProp,\n accessibility,\n ...sortableProps\n } = props;\n const id = React.useId();\n const [activeId, setActiveId] = React.useState(null);\n\n const sensors = useSensors(\n useSensor(MouseSensor),\n useSensor(TouchSensor),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n const config = React.useMemo(\n () => orientationConfig[orientation],\n [orientation],\n );\n\n const getItemValue = React.useCallback(\n (item: T): UniqueIdentifier => {\n if (typeof item === \"object\" && !getItemValueProp) {\n throw new Error(\n \"getItemValue is required when using array of objects.\",\n );\n }\n return getItemValueProp\n ? getItemValueProp(item)\n : (item as UniqueIdentifier);\n },\n [getItemValueProp],\n );\n\n const items = React.useMemo(() => {\n return value.map((item) => getItemValue(item));\n }, [value, getItemValue]);\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (over && active.id !== over?.id) {\n const activeIndex = value.findIndex(\n (item) => getItemValue(item) === active.id,\n );\n const overIndex = value.findIndex(\n (item) => getItemValue(item) === over.id,\n );\n\n if (onMove) {\n onMove({ ...event, activeIndex, overIndex });\n } else {\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\n }\n }\n setActiveId(null);\n },\n [value, onValueChange, onMove, getItemValue],\n );\n\n const announcements: Announcements = React.useMemo(\n () => ({\n onDragStart({ active }) {\n const activeValue = active.id.toString();\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\n },\n onDragOver({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n onDragEnd({ active, over }) {\n const activeValue = active.id.toString();\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\n }\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n },\n onDragCancel({ active }) {\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const activeValue = active.id.toString();\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\n },\n onDragMove({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n }),\n [value],\n );\n\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n () => ({\n draggable: `\n To pick up a sortable item, press space or enter.\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\n Press space or enter again to drop the item in its new position, or press escape to cancel.\n `,\n }),\n [orientation],\n );\n\n const contextValue = React.useMemo(\n () => ({\n id,\n items,\n modifiers: modifiers ?? config.modifiers,\n strategy: strategy ?? config.strategy,\n activeId,\n setActiveId,\n getItemValue,\n flatCursor,\n }),\n [\n id,\n items,\n modifiers,\n strategy,\n config.modifiers,\n config.strategy,\n activeId,\n getItemValue,\n flatCursor,\n ],\n );\n\n return (\n }\n >\n setActiveId(active.id),\n )}\n onDragEnd={composeEventHandlers(sortableProps.onDragEnd, onDragEnd)}\n onDragCancel={composeEventHandlers(sortableProps.onDragCancel, () =>\n setActiveId(null),\n )}\n accessibility={{\n announcements,\n screenReaderInstructions,\n ...accessibility,\n }}\n />\n \n );\n}\n\nconst SortableContentContext = React.createContext(false);\nSortableContentContext.displayName = CONTENT_NAME;\n\ninterface SortableContentProps extends React.ComponentPropsWithoutRef<\"div\"> {\n strategy?: SortableContextProps[\"strategy\"];\n children: React.ReactNode;\n asChild?: boolean;\n withoutSlot?: boolean;\n}\n\nconst SortableContent = React.forwardRef(\n (props, forwardedRef) => {\n const {\n strategy: strategyProp,\n asChild,\n withoutSlot,\n children,\n ...contentProps\n } = props;\n const context = useSortableContext(CONTENT_NAME);\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n {withoutSlot ? (\n children\n ) : (\n \n {children}\n \n )}\n \n \n );\n },\n);\nSortableContent.displayName = CONTENT_NAME;\n\ninterface SortableItemContextValue {\n id: string;\n attributes: React.HTMLAttributes;\n listeners: DraggableSyntheticListeners | undefined;\n setActivatorNodeRef: (node: HTMLElement | null) => void;\n isDragging?: boolean;\n disabled?: boolean;\n}\n\nconst SortableItemContext =\n React.createContext(null);\nSortableItemContext.displayName = ITEM_NAME;\n\ninterface SortableItemProps extends React.ComponentPropsWithoutRef<\"div\"> {\n value: UniqueIdentifier;\n asHandle?: boolean;\n asChild?: boolean;\n disabled?: boolean;\n}\n\nconst SortableItem = React.forwardRef(\n (props, forwardedRef) => {\n const {\n value,\n style,\n asHandle,\n asChild,\n disabled,\n className,\n ...itemProps\n } = props;\n const inSortableContent = React.useContext(SortableContentContext);\n const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n if (!inSortableContent && !inSortableOverlay) {\n throw new Error(SORTABLE_ERRORS[ITEM_NAME]);\n }\n\n if (value === \"\") {\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n }\n\n const context = useSortableContext(ITEM_NAME);\n const id = React.useId();\n const {\n attributes,\n listeners,\n setNodeRef,\n setActivatorNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: value, disabled });\n\n const composedRef = useComposedRefs(forwardedRef, (node) => {\n if (disabled) return;\n setNodeRef(node);\n if (asHandle) setActivatorNodeRef(node);\n });\n\n const composedStyle = React.useMemo(() => {\n return {\n transform: CSS.Translate.toString(transform),\n transition,\n ...style,\n };\n }, [transform, transition, style]);\n\n const itemContext = React.useMemo(\n () => ({\n id,\n attributes,\n listeners,\n setActivatorNodeRef,\n isDragging,\n disabled,\n }),\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n );\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n \n );\n },\n);\nSortableItem.displayName = ITEM_NAME;\n\ninterface SortableItemHandleProps\n extends React.ComponentPropsWithoutRef<\"button\"> {\n asChild?: boolean;\n}\n\nconst SortableItemHandle = React.forwardRef<\n HTMLButtonElement,\n SortableItemHandleProps\n>((props, forwardedRef) => {\n const { asChild, disabled, className, ...itemHandleProps } = props;\n const itemContext = React.useContext(SortableItemContext);\n if (!itemContext) {\n throw new Error(SORTABLE_ERRORS[ITEM_HANDLE_NAME]);\n }\n const context = useSortableContext(ITEM_HANDLE_NAME);\n\n const isDisabled = disabled ?? itemContext.disabled;\n\n const composedRef = useComposedRefs(forwardedRef, (node) => {\n if (!isDisabled) return;\n itemContext.setActivatorNodeRef(node);\n });\n\n const HandlePrimitive = asChild ? Slot : \"button\";\n\n return (\n \n );\n});\nSortableItemHandle.displayName = ITEM_HANDLE_NAME;\n\nconst SortableOverlayContext = React.createContext(false);\nSortableOverlayContext.displayName = OVERLAY_NAME;\n\nconst dropAnimation: DropAnimation = {\n sideEffects: defaultDropAnimationSideEffects({\n styles: {\n active: {\n opacity: \"0.4\",\n },\n },\n }),\n};\n\ninterface SortableOverlayProps\n extends Omit, \"children\"> {\n container?: Element | DocumentFragment | null;\n children?:\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n const { container: containerProp, children, ...overlayProps } = props;\n const context = useSortableContext(OVERLAY_NAME);\n\n const [mounted, setMounted] = React.useState(false);\n React.useLayoutEffect(() => setMounted(true), []);\n\n const container =\n containerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!container) return null;\n\n return ReactDOM.createPortal(\n \n \n {context.activeId\n ? typeof children === \"function\"\n ? children({ value: context.activeId })\n : children\n : null}\n \n ,\n container,\n );\n}\n\nconst Root = Sortable;\nconst Content = SortableContent;\nconst Item = SortableItem;\nconst ItemHandle = SortableItemHandle;\nconst Overlay = SortableOverlay;\n\nexport {\n Root,\n Content,\n Item,\n ItemHandle,\n Overlay,\n //\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n};\n", - "type": "registry:ui" + "type": "registry:ui", + "target": "src/components/ui/sortable.tsx" }, { "path": "src/lib/composition.ts", diff --git a/public/r/data-table.json b/public/r/data-table.json index 64a449470..1334f2136 100644 --- a/public/r/data-table.json +++ b/public/r/data-table.json @@ -24,49 +24,58 @@ ], "files": [ { - "path": "src/components/data-table.tsx", - "content": "import { type Table as TanstackTable, flexRender } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTablePagination } from \"@/components/data-table-pagination\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableProps extends React.ComponentProps<\"div\"> {\n table: TanstackTable;\n actionBar?: React.ReactNode;\n}\n\nexport function DataTable({\n table,\n actionBar,\n children,\n className,\n ...props\n}: DataTableProps) {\n return (\n \n {children}\n
\n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n \n ))}\n \n ))}\n \n \n {table.getRowModel().rows?.length ? (\n table.getRowModel().rows.map((row) => (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n \n ))}\n \n ))\n ) : (\n \n \n No results.\n \n \n )}\n \n
\n
\n
\n \n {actionBar &&\n table.getFilteredSelectedRowModel().rows.length > 0 &&\n actionBar}\n
\n
\n );\n}\n", - "type": "registry:component" + "path": "src/components/data-table/data-table.tsx", + "content": "import { type Table as TanstackTable, flexRender } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTablePagination } from \"@/components/data-table/data-table-pagination\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableProps extends React.ComponentProps<\"div\"> {\n table: TanstackTable;\n actionBar?: React.ReactNode;\n}\n\nexport function DataTable({\n table,\n actionBar,\n children,\n className,\n ...props\n}: DataTableProps) {\n return (\n \n {children}\n
\n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n \n ))}\n \n ))}\n \n \n {table.getRowModel().rows?.length ? (\n table.getRowModel().rows.map((row) => (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n \n ))}\n \n ))\n ) : (\n \n \n No results.\n \n \n )}\n \n
\n
\n
\n \n {actionBar &&\n table.getFilteredSelectedRowModel().rows.length > 0 &&\n actionBar}\n
\n
\n );\n}\n", + "type": "registry:component", + "target": "src/components/data-table/data-table.tsx" }, { - "path": "src/components/data-table-column-header.tsx", + "path": "src/components/data-table/data-table-column-header.tsx", "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport {\n ChevronDown,\n ChevronUp,\n ChevronsUpDown,\n EyeOff,\n X,\n} from \"lucide-react\";\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableColumnHeaderProps\n extends React.ComponentProps {\n column: Column;\n title: string;\n}\n\nexport function DataTableColumnHeader({\n column,\n title,\n className,\n ...props\n}: DataTableColumnHeaderProps) {\n if (!column.getCanSort() && !column.getCanHide()) {\n return
{title}
;\n }\n\n return (\n \n \n {title}\n {column.getCanSort() &&\n (column.getIsSorted() === \"desc\" ? (\n \n ) : column.getIsSorted() === \"asc\" ? (\n \n ) : (\n \n ))}\n \n \n {column.getCanSort() && (\n <>\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"asc\"}\n onClick={() => column.toggleSorting(false)}\n >\n \n Asc\n \n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"desc\"}\n onClick={() => column.toggleSorting(true)}\n >\n \n Desc\n \n {column.getIsSorted() && (\n column.clearSorting()}\n >\n \n Reset\n \n )}\n \n )}\n {column.getCanHide() && (\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={!column.getIsVisible()}\n onClick={() => column.toggleVisibility(false)}\n >\n \n Hide\n \n )}\n \n \n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-column-header.tsx" }, { - "path": "src/components/data-table-pagination.tsx", + "path": "src/components/data-table/data-table-pagination.tsx", "content": "import type { Table } from \"@tanstack/react-table\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTablePaginationProps extends React.ComponentProps<\"div\"> {\n table: Table;\n pageSizeOptions?: number[];\n}\n\nexport function DataTablePagination({\n table,\n pageSizeOptions = [10, 20, 30, 40, 50],\n className,\n ...props\n}: DataTablePaginationProps) {\n return (\n \n
\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n
\n
\n
\n

Rows per page

\n {\n table.setPageSize(Number(value));\n }}\n >\n \n \n \n \n {pageSizeOptions.map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n
\n
\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n
\n
\n table.setPageIndex(0)}\n disabled={!table.getCanPreviousPage()}\n >\n \n \n table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n \n \n table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n \n \n table.setPageIndex(table.getPageCount() - 1)}\n disabled={!table.getCanNextPage()}\n >\n \n \n
\n
\n
\n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-pagination.tsx" }, { - "path": "src/components/data-table-view-options.tsx", + "path": "src/components/data-table/data-table-view-options.tsx", "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { Check, ChevronsUpDown, Settings2 } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface DataTableViewOptionsProps {\n table: Table;\n}\n\nexport function DataTableViewOptions({\n table,\n}: DataTableViewOptionsProps) {\n const columns = React.useMemo(\n () =>\n table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\n ),\n [table],\n );\n\n return (\n \n \n \n \n \n \n \n No columns found.\n \n {columns.map((column) => (\n \n column.toggleVisibility(!column.getIsVisible())\n }\n >\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n \n ))}\n \n \n \n \n \n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-view-options.tsx" }, { - "path": "src/components/data-table-faceted-filter.tsx", + "path": "src/components/data-table/data-table-faceted-filter.tsx", "content": "\"use client\";\n\nimport type { Option } from \"@/types/data-table\";\nimport type { Column } from \"@tanstack/react-table\";\nimport { Check, PlusCircle, XCircle } from \"lucide-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface DataTableFacetedFilterProps {\n column?: Column;\n title?: string;\n options: Option[];\n multiple?: boolean;\n}\n\nexport function DataTableFacetedFilter({\n column,\n title,\n options,\n multiple,\n}: DataTableFacetedFilterProps) {\n const [open, setOpen] = React.useState(false);\n\n const columnFilterValue = column?.getFilterValue();\n const selectedValues = new Set(\n Array.isArray(columnFilterValue) ? columnFilterValue : [],\n );\n\n const onItemSelect = React.useCallback(\n (option: Option, isSelected: boolean) => {\n if (!column) return;\n\n if (multiple) {\n const newSelectedValues = new Set(selectedValues);\n if (isSelected) {\n newSelectedValues.delete(option.value);\n } else {\n newSelectedValues.add(option.value);\n }\n const filterValues = Array.from(newSelectedValues);\n column.setFilterValue(filterValues.length ? filterValues : undefined);\n } else {\n column.setFilterValue(isSelected ? undefined : [option.value]);\n setOpen(false);\n }\n },\n [column, multiple, selectedValues],\n );\n\n const onReset = React.useCallback(\n (event?: React.MouseEvent) => {\n event?.stopPropagation();\n column?.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \n \n \n \n \n \n \n \n No results found.\n \n {options.map((option) => {\n const isSelected = selectedValues.has(option.value);\n\n return (\n onItemSelect(option, isSelected)}\n >\n \n \n \n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n );\n })}\n \n {selectedValues.size > 0 && (\n <>\n \n \n onReset()}\n className=\"justify-center text-center\"\n >\n Clear filters\n \n \n \n )}\n \n \n \n \n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-faceted-filter.tsx" }, { - "path": "src/components/data-table-toolbar.tsx", - "content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { DataTableDateFilter } from \"@/components/data-table-date-filter\";\nimport { DataTableFacetedFilter } from \"@/components/data-table-faceted-filter\";\nimport { DataTableSliderFilter } from \"@/components/data-table-slider-filter\";\nimport { DataTableViewOptions } from \"@/components/data-table-view-options\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableToolbarProps extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableToolbarProps) {\n const isFiltered = table.getState().columnFilters.length > 0;\n\n const columns = React.useMemo(\n () => table.getAllColumns().filter((column) => column.getCanFilter()),\n [table],\n );\n\n const onReset = React.useCallback(() => {\n table.resetColumnFilters();\n }, [table]);\n\n return (\n \n
\n {columns.map((column) => (\n \n ))}\n {isFiltered && (\n \n \n Reset\n \n )}\n
\n
\n {children}\n \n
\n \n );\n}\ninterface DataTableToolbarFilterProps {\n column: Column;\n}\n\nfunction DataTableToolbarFilter({\n column,\n}: DataTableToolbarFilterProps) {\n {\n const columnMeta = column.columnDef.meta;\n\n const onFilterRender = React.useCallback(() => {\n if (!columnMeta?.variant) return null;\n\n switch (columnMeta.variant) {\n case \"text\":\n return (\n column.setFilterValue(event.target.value)}\n className=\"h-8 w-40 lg:w-56\"\n />\n );\n\n case \"number\":\n return (\n
\n column.setFilterValue(event.target.value)}\n className={cn(\"h-8 w-[120px]\", columnMeta.unit && \"pr-8\")}\n />\n {columnMeta.unit && (\n \n {columnMeta.unit}\n \n )}\n
\n );\n\n case \"range\":\n return (\n \n );\n\n case \"date\":\n case \"dateRange\":\n return (\n \n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \n );\n\n default:\n return null;\n }\n }, [column, columnMeta]);\n\n return onFilterRender();\n }\n}\n", - "type": "registry:component" + "path": "src/components/data-table/data-table-toolbar.tsx", + "content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { DataTableDateFilter } from \"@/components/data-table/data-table-date-filter\";\nimport { DataTableFacetedFilter } from \"@/components/data-table/data-table-faceted-filter\";\nimport { DataTableSliderFilter } from \"@/components/data-table/data-table-slider-filter\";\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableToolbarProps extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableToolbarProps) {\n const isFiltered = table.getState().columnFilters.length > 0;\n\n const columns = React.useMemo(\n () => table.getAllColumns().filter((column) => column.getCanFilter()),\n [table],\n );\n\n const onReset = React.useCallback(() => {\n table.resetColumnFilters();\n }, [table]);\n\n return (\n \n
\n {columns.map((column) => (\n \n ))}\n {isFiltered && (\n \n \n Reset\n \n )}\n
\n
\n {children}\n \n
\n \n );\n}\ninterface DataTableToolbarFilterProps {\n column: Column;\n}\n\nfunction DataTableToolbarFilter({\n column,\n}: DataTableToolbarFilterProps) {\n {\n const columnMeta = column.columnDef.meta;\n\n const onFilterRender = React.useCallback(() => {\n if (!columnMeta?.variant) return null;\n\n switch (columnMeta.variant) {\n case \"text\":\n return (\n column.setFilterValue(event.target.value)}\n className=\"h-8 w-40 lg:w-56\"\n />\n );\n\n case \"number\":\n return (\n
\n column.setFilterValue(event.target.value)}\n className={cn(\"h-8 w-[120px]\", columnMeta.unit && \"pr-8\")}\n />\n {columnMeta.unit && (\n \n {columnMeta.unit}\n \n )}\n
\n );\n\n case \"range\":\n return (\n \n );\n\n case \"date\":\n case \"dateRange\":\n return (\n \n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \n );\n\n default:\n return null;\n }\n }, [column, columnMeta]);\n\n return onFilterRender();\n }\n}\n", + "type": "registry:component", + "target": "src/components/data-table/data-table-toolbar.tsx" }, { - "path": "src/components/data-table-slider-filter.tsx", + "path": "src/components/data-table/data-table-slider-filter.tsx", "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport { PlusCircle, XCircle } from \"lucide-react\";\n\ninterface Range {\n min: number;\n max: number;\n}\n\ntype RangeValue = [number, number];\n\nfunction getIsValidRange(value: unknown): value is RangeValue {\n return (\n Array.isArray(value) &&\n value.length === 2 &&\n typeof value[0] === \"number\" &&\n typeof value[1] === \"number\"\n );\n}\n\ninterface DataTableSliderFilterProps {\n column: Column;\n title?: string;\n}\n\nexport function DataTableSliderFilter({\n column,\n title,\n}: DataTableSliderFilterProps) {\n const id = React.useId();\n\n const columnFilterValue = getIsValidRange(column.getFilterValue())\n ? (column.getFilterValue() as RangeValue)\n : undefined;\n\n const defaultRange = column.columnDef.meta?.range;\n const unit = column.columnDef.meta?.unit;\n\n const { min, max, step } = React.useMemo(() => {\n let minValue = 0;\n let maxValue = 100;\n\n if (defaultRange && getIsValidRange(defaultRange)) {\n [minValue, maxValue] = defaultRange;\n } else {\n const values = column.getFacetedMinMaxValues();\n if (values && Array.isArray(values) && values.length === 2) {\n const [facetMinValue, facetMaxValue] = values;\n if (\n typeof facetMinValue === \"number\" &&\n typeof facetMaxValue === \"number\"\n ) {\n minValue = facetMinValue;\n maxValue = facetMaxValue;\n }\n }\n }\n\n const rangeSize = maxValue - minValue;\n const step =\n rangeSize <= 20\n ? 1\n : rangeSize <= 100\n ? Math.ceil(rangeSize / 20)\n : Math.ceil(rangeSize / 50);\n\n return { min: minValue, max: maxValue, step };\n }, [column, defaultRange]);\n\n const range = React.useMemo((): RangeValue => {\n return columnFilterValue ?? [min, max];\n }, [columnFilterValue, min, max]);\n\n const formatValue = React.useCallback((value: number) => {\n return value.toLocaleString(undefined, { maximumFractionDigits: 0 });\n }, []);\n\n const onFromInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue >= min && numValue <= range[1]) {\n column.setFilterValue([numValue, range[1]]);\n }\n },\n [column, min, range],\n );\n\n const onToInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue <= max && numValue >= range[0]) {\n column.setFilterValue([range[0], numValue]);\n }\n },\n [column, max, range],\n );\n\n const onSliderValueChange = React.useCallback(\n (value: RangeValue) => {\n if (Array.isArray(value) && value.length === 2) {\n column.setFilterValue(value);\n }\n },\n [column],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) => {\n if (event.target instanceof HTMLDivElement) {\n event.stopPropagation();\n }\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \n \n \n \n \n
\n

\n {title}\n

\n
\n \n
\n \n {unit && (\n \n {unit}\n \n )}\n
\n \n
\n \n {unit && (\n \n {unit}\n \n )}\n
\n
\n \n \n
\n \n Clear\n \n
\n
\n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-slider-filter.tsx" }, { - "path": "src/components/data-table-date-filter.tsx", + "path": "src/components/data-table/data-table-date-filter.tsx", "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport { CalendarIcon, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { formatDate } from \"@/lib/format\";\n\ntype DateSelection = Date[] | DateRange;\n\nfunction getIsDateRange(value: DateSelection): value is DateRange {\n return value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction parseAsDate(timestamp: number | string | undefined): Date | undefined {\n if (!timestamp) return undefined;\n const numericTimestamp =\n typeof timestamp === \"string\" ? Number(timestamp) : timestamp;\n const date = new Date(numericTimestamp);\n return !Number.isNaN(date.getTime()) ? date : undefined;\n}\n\nfunction parseColumnFilterValue(value: unknown) {\n if (value === null || value === undefined) {\n return [];\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => {\n if (typeof item === \"number\" || typeof item === \"string\") {\n return item;\n }\n return undefined;\n });\n }\n\n if (typeof value === \"string\" || typeof value === \"number\") {\n return [value];\n }\n\n return [];\n}\n\ninterface DataTableDateFilterProps {\n column: Column;\n title?: string;\n multiple?: boolean;\n}\n\nexport function DataTableDateFilter({\n column,\n title,\n multiple,\n}: DataTableDateFilterProps) {\n const columnFilterValue = column.getFilterValue();\n\n const selectedDates = React.useMemo(() => {\n if (!columnFilterValue) {\n return multiple ? { from: undefined, to: undefined } : [];\n }\n\n if (multiple) {\n const timestamps = parseColumnFilterValue(columnFilterValue);\n return {\n from: parseAsDate(timestamps[0]),\n to: parseAsDate(timestamps[1]),\n };\n }\n\n const timestamps = parseColumnFilterValue(columnFilterValue);\n const date = parseAsDate(timestamps[0]);\n return date ? [date] : [];\n }, [columnFilterValue, multiple]);\n\n const onSelect = React.useCallback(\n (date: Date | DateRange | undefined) => {\n if (!date) {\n column.setFilterValue(undefined);\n return;\n }\n\n if (multiple && !(\"getTime\" in date)) {\n const from = date.from?.getTime();\n const to = date.to?.getTime();\n column.setFilterValue(from || to ? [from, to] : undefined);\n } else if (!multiple && \"getTime\" in date) {\n column.setFilterValue(date.getTime());\n }\n },\n [column, multiple],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n const hasValue = React.useMemo(() => {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return false;\n return selectedDates.from || selectedDates.to;\n }\n if (!Array.isArray(selectedDates)) return false;\n return selectedDates.length > 0;\n }, [multiple, selectedDates]);\n\n const formatDateRange = React.useCallback((range: DateRange) => {\n if (!range.from && !range.to) return \"\";\n if (range.from && range.to) {\n return `${formatDate(range.from)} - ${formatDate(range.to)}`;\n }\n return formatDate(range.from ?? range.to);\n }, []);\n\n const label = React.useMemo(() => {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDates = selectedDates.from || selectedDates.to;\n const dateText = hasSelectedDates\n ? formatDateRange(selectedDates)\n : \"Select date range\";\n\n return (\n \n {title}\n {hasSelectedDates && (\n <>\n \n {dateText}\n \n )}\n \n );\n }\n\n if (getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDate = selectedDates.length > 0;\n const dateText = hasSelectedDate\n ? formatDate(selectedDates[0])\n : \"Select date\";\n\n return (\n \n {title}\n {hasSelectedDate && (\n <>\n \n {dateText}\n \n )}\n \n );\n }, [selectedDates, multiple, formatDateRange, title]);\n\n return (\n \n \n \n \n \n {multiple ? (\n \n ) : (\n \n )}\n \n \n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-date-filter.tsx" }, { - "path": "src/components/data-table-skeleton.tsx", + "path": "src/components/data-table/data-table-skeleton.tsx", "content": "import { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableSkeletonProps extends React.ComponentProps<\"div\"> {\n columnCount: number;\n rowCount?: number;\n filterCount?: number;\n cellWidths?: string[];\n withViewOptions?: boolean;\n withPagination?: boolean;\n shrinkZero?: boolean;\n}\n\nexport function DataTableSkeleton({\n columnCount,\n rowCount = 10,\n filterCount = 0,\n cellWidths = [\"auto\"],\n withViewOptions = true,\n withPagination = true,\n shrinkZero = false,\n className,\n ...props\n}: DataTableSkeletonProps) {\n const cozyCellWidths = Array.from(\n { length: columnCount },\n (_, index) => cellWidths[index % cellWidths.length] ?? \"auto\",\n );\n\n return (\n \n
\n
\n {filterCount > 0\n ? Array.from({ length: filterCount }).map((_, i) => (\n \n ))\n : null}\n
\n {withViewOptions ? (\n
\n
\n \n \n {Array.from({ length: 1 }).map((_, i) => (\n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n ))}\n \n \n {Array.from({ length: rowCount }).map((_, i) => (\n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n ))}\n \n
\n
\n {withPagination ? (\n
\n \n
\n
\n \n \n
\n
\n \n
\n
\n \n \n \n \n
\n
\n
\n ) : null}\n \n );\n}\n", - "type": "registry:component" + "type": "registry:component", + "target": "src/components/data-table/data-table-skeleton.tsx" }, { "path": "src/hooks/use-callback-ref.ts", diff --git a/registry.json b/registry.json index 053429716..4cb940306 100644 --- a/registry.json +++ b/registry.json @@ -1,7 +1,7 @@ { "$schema": "https://ui.shadcn.com/schema/registry.json", "name": "shadcn-table", - "homepage": "https://table.sadmn.com", + "homepage": "https://tablecn.com", "items": [ { "name": "data-table", @@ -24,40 +24,49 @@ "dependencies": ["@tanstack/react-table", "lucide-react", "nuqs"], "files": [ { - "path": "src/components/data-table.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table.tsx" }, { - "path": "src/components/data-table-column-header.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-column-header.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-column-header.tsx" }, { - "path": "src/components/data-table-pagination.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-pagination.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-pagination.tsx" }, { - "path": "src/components/data-table-view-options.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-view-options.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-view-options.tsx" }, { - "path": "src/components/data-table-faceted-filter.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-faceted-filter.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-faceted-filter.tsx" }, { - "path": "src/components/data-table-toolbar.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-toolbar.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-toolbar.tsx" }, { - "path": "src/components/data-table-slider-filter.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-slider-filter.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-slider-filter.tsx" }, { - "path": "src/components/data-table-date-filter.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-date-filter.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-date-filter.tsx" }, { - "path": "src/components/data-table-skeleton.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-skeleton.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-skeleton.tsx" }, { "path": "src/hooks/use-callback-ref.ts", @@ -104,8 +113,9 @@ "dependencies": ["@tanstack/react-table", "lucide-react", "motion"], "files": [ { - "path": "src/components/data-table-action-bar.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-action-bar.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-action-bar.tsx" } ] }, @@ -124,12 +134,14 @@ "dependencies": ["@tanstack/react-table", "lucide-react"], "files": [ { - "path": "src/components/data-table-sort-list.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-sort-list.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-sort-list.tsx" }, { "path": "src/components/ui/sortable.tsx", - "type": "registry:ui" + "type": "registry:ui", + "target": "src/components/ui/sortable.tsx" }, { "path": "src/lib/composition.ts", @@ -169,24 +181,29 @@ ], "files": [ { - "path": "src/components/data-table-filter-list.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-filter-list.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-filter-list.tsx" }, { - "path": "src/components/data-table-range-filter.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-range-filter.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-range-filter.tsx" }, { - "path": "src/components/data-table-advanced-toolbar.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-advanced-toolbar.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, { "path": "src/components/ui/sortable.tsx", - "type": "registry:ui" + "type": "registry:ui", + "target": "src/components/ui/sortable.tsx" }, { "path": "src/components/ui/faceted.tsx", - "type": "registry:ui" + "type": "registry:ui", + "target": "src/components/ui/faceted.tsx" }, { "path": "src/hooks/use-callback-ref.ts", @@ -251,16 +268,19 @@ ], "files": [ { - "path": "src/components/data-table-filter-menu.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-filter-menu.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-filter-menu.tsx" }, { - "path": "src/components/data-table-range-filter.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-range-filter.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-range-filter.tsx" }, { - "path": "src/components/data-table-advanced-toolbar.tsx", - "type": "registry:component" + "path": "src/components/data-table/data-table-advanced-toolbar.tsx", + "type": "registry:component", + "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, { "path": "src/hooks/use-callback-ref.ts", diff --git a/src/app/_components/tasks-table-action-bar.tsx b/src/app/_components/tasks-table-action-bar.tsx index e8f2d3136..94100ffc8 100644 --- a/src/app/_components/tasks-table-action-bar.tsx +++ b/src/app/_components/tasks-table-action-bar.tsx @@ -11,7 +11,7 @@ import { DataTableActionBar, DataTableActionBarAction, DataTableActionBarSelection, -} from "@/components/data-table-action-bar"; +} from "@/components/data-table/data-table-action-bar"; import { Select, SelectContent, diff --git a/src/app/_components/tasks-table-columns.tsx b/src/app/_components/tasks-table-columns.tsx index 77fd0f3e4..1634dc72f 100644 --- a/src/app/_components/tasks-table-columns.tsx +++ b/src/app/_components/tasks-table-columns.tsx @@ -14,7 +14,7 @@ import { import * as React from "react"; import { toast } from "sonner"; -import { DataTableColumnHeader } from "@/components/data-table-column-header"; +import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; diff --git a/src/app/_components/tasks-table.tsx b/src/app/_components/tasks-table.tsx index d7a0542a8..c6d196aef 100644 --- a/src/app/_components/tasks-table.tsx +++ b/src/app/_components/tasks-table.tsx @@ -4,14 +4,14 @@ import type { Task } from "@/db/schema"; import type { DataTableRowAction } from "@/types/data-table"; import * as React from "react"; -import { DataTable } from "@/components/data-table"; +import { DataTable } from "@/components/data-table/data-table"; import { useDataTable } from "@/hooks/use-data-table"; -import { DataTableAdvancedToolbar } from "@/components/data-table-advanced-toolbar"; -import { DataTableFilterList } from "@/components/data-table-filter-list"; -import { DataTableFilterMenu } from "@/components/data-table-filter-menu"; -import { DataTableSortList } from "@/components/data-table-sort-list"; -import { DataTableToolbar } from "@/components/data-table-toolbar"; +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import { DataTableFilterList } from "@/components/data-table/data-table-filter-list"; +import { DataTableFilterMenu } from "@/components/data-table/data-table-filter-menu"; +import { DataTableSortList } from "@/components/data-table/data-table-sort-list"; +import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; import type { getEstimatedHoursRange, getTaskPriorityCounts, diff --git a/src/app/page.tsx b/src/app/page.tsx index 728f6e363..b0bd70ce4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,7 @@ import type { SearchParams } from "@/types"; import * as React from "react"; -import { DataTableSkeleton } from "@/components/data-table-skeleton"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; import { Shell } from "@/components/shell"; import { getValidFilters } from "@/lib/data-table"; diff --git a/src/components/data-table-action-bar.tsx b/src/components/data-table/data-table-action-bar.tsx similarity index 100% rename from src/components/data-table-action-bar.tsx rename to src/components/data-table/data-table-action-bar.tsx diff --git a/src/components/data-table-advanced-toolbar.tsx b/src/components/data-table/data-table-advanced-toolbar.tsx similarity index 90% rename from src/components/data-table-advanced-toolbar.tsx rename to src/components/data-table/data-table-advanced-toolbar.tsx index 0a5cadf8c..21654974b 100644 --- a/src/components/data-table-advanced-toolbar.tsx +++ b/src/components/data-table/data-table-advanced-toolbar.tsx @@ -3,7 +3,7 @@ import type { Table } from "@tanstack/react-table"; import type * as React from "react"; -import { DataTableViewOptions } from "@/components/data-table-view-options"; +import { DataTableViewOptions } from "@/components/data-table/data-table-view-options"; import { cn } from "@/lib/utils"; interface DataTableAdvancedToolbarProps diff --git a/src/components/data-table-column-header.tsx b/src/components/data-table/data-table-column-header.tsx similarity index 100% rename from src/components/data-table-column-header.tsx rename to src/components/data-table/data-table-column-header.tsx diff --git a/src/components/data-table-date-filter.tsx b/src/components/data-table/data-table-date-filter.tsx similarity index 100% rename from src/components/data-table-date-filter.tsx rename to src/components/data-table/data-table-date-filter.tsx diff --git a/src/components/data-table-faceted-filter.tsx b/src/components/data-table/data-table-faceted-filter.tsx similarity index 100% rename from src/components/data-table-faceted-filter.tsx rename to src/components/data-table/data-table-faceted-filter.tsx diff --git a/src/components/data-table-filter-list.tsx b/src/components/data-table/data-table-filter-list.tsx similarity index 99% rename from src/components/data-table-filter-list.tsx rename to src/components/data-table/data-table-filter-list.tsx index 67cc3595b..901f8a8c8 100644 --- a/src/components/data-table-filter-list.tsx +++ b/src/components/data-table/data-table-filter-list.tsx @@ -12,7 +12,7 @@ import { import { parseAsStringEnum, useQueryState } from "nuqs"; import * as React from "react"; -import { DataTableRangeFilter } from "@/components/data-table-range-filter"; +import { DataTableRangeFilter } from "@/components/data-table/data-table-range-filter"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; diff --git a/src/components/data-table-filter-menu.tsx b/src/components/data-table/data-table-filter-menu.tsx similarity index 99% rename from src/components/data-table-filter-menu.tsx rename to src/components/data-table/data-table-filter-menu.tsx index 893a5e713..b37c77ff2 100644 --- a/src/components/data-table-filter-menu.tsx +++ b/src/components/data-table/data-table-filter-menu.tsx @@ -12,7 +12,7 @@ import { import { useQueryState } from "nuqs"; import * as React from "react"; -import { DataTableRangeFilter } from "@/components/data-table-range-filter"; +import { DataTableRangeFilter } from "@/components/data-table/data-table-range-filter"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { diff --git a/src/components/data-table-pagination.tsx b/src/components/data-table/data-table-pagination.tsx similarity index 100% rename from src/components/data-table-pagination.tsx rename to src/components/data-table/data-table-pagination.tsx diff --git a/src/components/data-table-range-filter.tsx b/src/components/data-table/data-table-range-filter.tsx similarity index 100% rename from src/components/data-table-range-filter.tsx rename to src/components/data-table/data-table-range-filter.tsx diff --git a/src/components/data-table-skeleton.tsx b/src/components/data-table/data-table-skeleton.tsx similarity index 100% rename from src/components/data-table-skeleton.tsx rename to src/components/data-table/data-table-skeleton.tsx diff --git a/src/components/data-table-slider-filter.tsx b/src/components/data-table/data-table-slider-filter.tsx similarity index 100% rename from src/components/data-table-slider-filter.tsx rename to src/components/data-table/data-table-slider-filter.tsx diff --git a/src/components/data-table-sort-list.tsx b/src/components/data-table/data-table-sort-list.tsx similarity index 100% rename from src/components/data-table-sort-list.tsx rename to src/components/data-table/data-table-sort-list.tsx diff --git a/src/components/data-table-toolbar.tsx b/src/components/data-table/data-table-toolbar.tsx similarity index 91% rename from src/components/data-table-toolbar.tsx rename to src/components/data-table/data-table-toolbar.tsx index 4af583025..fd49d8fa5 100644 --- a/src/components/data-table-toolbar.tsx +++ b/src/components/data-table/data-table-toolbar.tsx @@ -4,10 +4,10 @@ import type { Column, Table } from "@tanstack/react-table"; import { X } from "lucide-react"; import * as React from "react"; -import { DataTableDateFilter } from "@/components/data-table-date-filter"; -import { DataTableFacetedFilter } from "@/components/data-table-faceted-filter"; -import { DataTableSliderFilter } from "@/components/data-table-slider-filter"; -import { DataTableViewOptions } from "@/components/data-table-view-options"; +import { DataTableDateFilter } from "@/components/data-table/data-table-date-filter"; +import { DataTableFacetedFilter } from "@/components/data-table/data-table-faceted-filter"; +import { DataTableSliderFilter } from "@/components/data-table/data-table-slider-filter"; +import { DataTableViewOptions } from "@/components/data-table/data-table-view-options"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; diff --git a/src/components/data-table-view-options.tsx b/src/components/data-table/data-table-view-options.tsx similarity index 100% rename from src/components/data-table-view-options.tsx rename to src/components/data-table/data-table-view-options.tsx diff --git a/src/components/data-table.tsx b/src/components/data-table/data-table.tsx similarity index 97% rename from src/components/data-table.tsx rename to src/components/data-table/data-table.tsx index 5d2639c9c..2e911149f 100644 --- a/src/components/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -1,7 +1,7 @@ import { type Table as TanstackTable, flexRender } from "@tanstack/react-table"; import type * as React from "react"; -import { DataTablePagination } from "@/components/data-table-pagination"; +import { DataTablePagination } from "@/components/data-table/data-table-pagination"; import { Table, TableBody,