From 8be2ea81172e87b288d7896a3a0a9915bc4db819 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Mon, 27 Jan 2025 11:35:51 +0100 Subject: [PATCH] chore: split data-table/components/filters/index.tsx (#18679) * chore: split data-table/components/filters/index.tsx * fix type error * fix type error --- .../components/filters/ActiveFilters.tsx | 28 ++ .../components/filters/AddFilterButton.tsx | 81 ++++++ .../filters/ColumnVisibilityButton.tsx | 115 ++++++++ .../data-table/components/filters/index.ts | 17 ++ .../data-table/components/filters/index.tsx | 261 ------------------ packages/features/data-table/hooks/index.ts | 1 + .../data-table/hooks/useFilterableColumns.ts | 56 ++++ 7 files changed, 298 insertions(+), 261 deletions(-) create mode 100644 packages/features/data-table/components/filters/ActiveFilters.tsx create mode 100644 packages/features/data-table/components/filters/AddFilterButton.tsx create mode 100644 packages/features/data-table/components/filters/ColumnVisibilityButton.tsx create mode 100644 packages/features/data-table/components/filters/index.ts delete mode 100644 packages/features/data-table/components/filters/index.tsx create mode 100644 packages/features/data-table/hooks/useFilterableColumns.ts diff --git a/packages/features/data-table/components/filters/ActiveFilters.tsx b/packages/features/data-table/components/filters/ActiveFilters.tsx new file mode 100644 index 0000000000..988618f04a --- /dev/null +++ b/packages/features/data-table/components/filters/ActiveFilters.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { type Table } from "@tanstack/react-table"; +// eslint-disable-next-line no-restricted-imports +import { Fragment } from "react"; + +import { useDataTable, useFilterableColumns } from "../../hooks"; +import { FilterPopover } from "./FilterPopover"; + +// Add the new ActiveFilters component +interface ActiveFiltersProps { + table: Table; +} + +export function ActiveFilters({ table }: ActiveFiltersProps) { + const { activeFilters } = useDataTable(); + const filterableColumns = useFilterableColumns(table); + + return ( + <> + {activeFilters.map((filter) => { + const column = filterableColumns.find((col) => col.id === filter.f); + if (!column) return null; + return ; + })} + + ); +} diff --git a/packages/features/data-table/components/filters/AddFilterButton.tsx b/packages/features/data-table/components/filters/AddFilterButton.tsx new file mode 100644 index 0000000000..4caf473842 --- /dev/null +++ b/packages/features/data-table/components/filters/AddFilterButton.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { type Table } from "@tanstack/react-table"; +// eslint-disable-next-line no-restricted-imports +import startCase from "lodash/startCase"; +import { forwardRef, useCallback } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { + Button, + Popover, + PopoverTrigger, + PopoverContent, + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandItem, + Icon, +} from "@calcom/ui"; + +import { useDataTable, useFilterableColumns } from "../../hooks"; + +export interface AddFilterButtonProps { + table: Table; +} + +function AddFilterButtonComponent( + { table }: AddFilterButtonProps, + ref: React.Ref +) { + const { t } = useLocale(); + const { activeFilters, setActiveFilters } = useDataTable(); + + const filterableColumns = useFilterableColumns(table); + + const handleAddFilter = useCallback( + (columnId: string) => { + if (!activeFilters?.some((filter) => filter.f === columnId)) { + setActiveFilters([...activeFilters, { f: columnId, v: undefined }]); + } + }, + [activeFilters, setActiveFilters] + ); + + return ( +
+ + + + + + + + + {t("no_columns_found")} + {filterableColumns.map((column) => { + if (activeFilters?.some((filter) => filter.f === column.id)) return null; + return ( + handleAddFilter(column.id)} + className="px-4 py-2"> + {startCase(column.title)} + + ); + })} + + + + +
+ ); +} + +export const AddFilterButton = forwardRef(AddFilterButtonComponent) as ( + props: AddFilterButtonProps & { ref?: React.Ref } +) => ReturnType; diff --git a/packages/features/data-table/components/filters/ColumnVisibilityButton.tsx b/packages/features/data-table/components/filters/ColumnVisibilityButton.tsx new file mode 100644 index 0000000000..9d9613b7be --- /dev/null +++ b/packages/features/data-table/components/filters/ColumnVisibilityButton.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { type Table } from "@tanstack/react-table"; +// eslint-disable-next-line no-restricted-imports +import { forwardRef, useState } from "react"; + +import { classNames } from "@calcom/lib"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { ButtonProps } from "@calcom/ui"; +import { + Button, + buttonClasses, + Popover, + PopoverTrigger, + PopoverContent, + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandSeparator, + Icon, +} from "@calcom/ui"; + +export interface ColumnVisiblityProps { + table: Table; +} + +function ColumnVisibilityButtonComponent( + { + children, + color = "secondary", + EndIcon = "sliders-vertical", + table, + ...rest + }: ColumnVisiblityProps & ButtonProps, + ref: React.Ref +) { + const { t } = useLocale(); + const allColumns = table.getAllLeafColumns(); + const [open, setOpen] = useState(false); + const [visibleColumns, setVisibleColumns] = useState( + () => new Set(allColumns.filter((col) => col.getIsVisible()).map((col) => col.id)) + ); + + return ( + + + + + + + + + {t("no_columns_found")} + + {allColumns.map((column) => { + const canHide = column.getCanHide(); + if (!column.columnDef.header || typeof column.columnDef.header !== "string" || !canHide) + return null; + const isVisible = visibleColumns.has(column.id); + return ( + { + column.toggleVisibility(!isVisible); + setVisibleColumns((prev) => { + const next = new Set(prev); + if (isVisible) { + next.delete(column.id); + } else { + next.add(column.id); + } + return next; + }); + }}> +
+ +
+ {column.columnDef.header} +
+ ); + })} +
+
+ + + { + allColumns.forEach((column) => column.toggleVisibility(true)); + setVisibleColumns(new Set(allColumns.map((col) => col.id))); + }} + className={classNames( + "w-full justify-center text-center", + buttonClasses({ color: "secondary" }) + )}> + {t("show_all_columns")} + + +
+
+
+ ); +} + +export const ColumnVisibilityButton = forwardRef(ColumnVisibilityButtonComponent) as ( + props: ColumnVisiblityProps & ButtonProps & { ref?: React.Ref } +) => ReturnType; diff --git a/packages/features/data-table/components/filters/index.ts b/packages/features/data-table/components/filters/index.ts new file mode 100644 index 0000000000..4070e28bbb --- /dev/null +++ b/packages/features/data-table/components/filters/index.ts @@ -0,0 +1,17 @@ +"use client"; + +import { ActiveFilters } from "./ActiveFilters"; +import { AddFilterButton } from "./AddFilterButton"; +import { ClearFiltersButton } from "./ClearFiltersButton"; +import { ColumnVisibilityButton } from "./ColumnVisibilityButton"; +import { DateRangeFilter } from "./DateRangeFilter"; + +// Update the export to include ActiveFilters +export const DataTableFilters = { + ColumnVisibilityButton, + AddFilterButton, + ActiveFilters, + ClearFiltersButton, +}; + +export { DateRangeFilter }; diff --git a/packages/features/data-table/components/filters/index.tsx b/packages/features/data-table/components/filters/index.tsx deleted file mode 100644 index da543db3bf..0000000000 --- a/packages/features/data-table/components/filters/index.tsx +++ /dev/null @@ -1,261 +0,0 @@ -"use client"; - -import { type Table } from "@tanstack/react-table"; -// eslint-disable-next-line no-restricted-imports -import startCase from "lodash/startCase"; -import { forwardRef, useState, useMemo, useCallback, Fragment } from "react"; - -import { classNames } from "@calcom/lib"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { ButtonProps } from "@calcom/ui"; -import { - Button, - buttonClasses, - Popover, - PopoverTrigger, - PopoverContent, - Command, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandSeparator, - Icon, -} from "@calcom/ui"; - -import { useDataTable } from "../../hooks"; -import type { FilterableColumn } from "../../lib/types"; -import { ClearFiltersButton } from "./ClearFiltersButton"; -import { DateRangeFilter } from "./DateRangeFilter"; -import { FilterPopover } from "./FilterPopover"; - -interface ColumnVisiblityProps { - table: Table; -} - -function ColumnVisibilityButtonComponent( - { - children, - color = "secondary", - EndIcon = "sliders-vertical", - table, - ...rest - }: ColumnVisiblityProps & ButtonProps, - ref: React.Ref -) { - const { t } = useLocale(); - const allColumns = table.getAllLeafColumns(); - const [open, setOpen] = useState(false); - const [visibleColumns, setVisibleColumns] = useState( - () => new Set(allColumns.filter((col) => col.getIsVisible()).map((col) => col.id)) - ); - - return ( - - - - - - - - - {t("no_columns_found")} - - {allColumns.map((column) => { - const canHide = column.getCanHide(); - if (!column.columnDef.header || typeof column.columnDef.header !== "string" || !canHide) - return null; - const isVisible = visibleColumns.has(column.id); - return ( - { - column.toggleVisibility(!isVisible); - setVisibleColumns((prev) => { - const next = new Set(prev); - if (isVisible) { - next.delete(column.id); - } else { - next.add(column.id); - } - return next; - }); - }}> -
- -
- {column.columnDef.header} -
- ); - })} -
-
- - - { - allColumns.forEach((column) => column.toggleVisibility(true)); - setVisibleColumns(new Set(allColumns.map((col) => col.id))); - }} - className={classNames( - "w-full justify-center text-center", - buttonClasses({ color: "secondary" }) - )}> - {t("show_all_columns")} - - -
-
-
- ); -} - -const ColumnVisibilityButton = forwardRef(ColumnVisibilityButtonComponent) as ( - props: ColumnVisiblityProps & ButtonProps & { ref?: React.Ref } -) => ReturnType; - -// Filters -interface AddFilterButtonProps { - table: Table; -} - -function AddFilterButtonComponent( - { table }: AddFilterButtonProps, - ref: React.Ref -) { - const { t } = useLocale(); - const { activeFilters, setActiveFilters } = useDataTable(); - - const filterableColumns = useFilterableColumns(table); - - const handleAddFilter = useCallback( - (columnId: string) => { - if (!activeFilters?.some((filter) => filter.f === columnId)) { - setActiveFilters([...activeFilters, { f: columnId, v: undefined }]); - } - }, - [activeFilters, setActiveFilters] - ); - - return ( -
- - - - - - - - - {t("no_columns_found")} - {filterableColumns.map((column) => { - if (activeFilters?.some((filter) => filter.f === column.id)) return null; - return ( - handleAddFilter(column.id)} - className="px-4 py-2"> - {startCase(column.title)} - - ); - })} - - - - -
- ); -} - -function useFilterableColumns(table: Table) { - const columns = useMemo( - () => table.getAllColumns().filter((column) => column.getCanFilter()), - [table.getAllColumns()] - ); - - const filterableColumns = useMemo( - () => - columns - .map((column) => { - const type = column.columnDef.meta?.filter?.type || "multi_select"; - const base = { - id: column.id, - title: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id, - ...(column.columnDef.meta?.filter || {}), - type, - }; - if (type === "multi_select" || type === "single_select") { - const values = column.getFacetedUniqueValues(); - const options = Array.from(values.keys()).map((option) => { - if (typeof option === "string") { - return { - label: option, - value: option, - }; - } else { - return { - label: option.label as string, - value: option.value as string | number, - }; - } - }); - return { - ...base, - options, - }; - } else { - return { - ...base, - }; - } - }) - .filter((column): column is FilterableColumn => Boolean(column)), - [columns] - ); - - return filterableColumns; -} - -const AddFilterButton = forwardRef(AddFilterButtonComponent) as ( - props: AddFilterButtonProps & { ref?: React.Ref } -) => ReturnType; - -// Add the new ActiveFilters component -interface ActiveFiltersProps { - table: Table; -} - -function ActiveFilters({ table }: ActiveFiltersProps) { - const { activeFilters } = useDataTable(); - const filterableColumns = useFilterableColumns(table); - - return ( - <> - {activeFilters.map((filter) => { - const column = filterableColumns.find((col) => col.id === filter.f); - if (!column) return null; - return ; - })} - - ); -} - -// Update the export to include ActiveFilters -export const DataTableFilters = { - ColumnVisibilityButton, - AddFilterButton, - ActiveFilters, - ClearFiltersButton, -}; - -export { DateRangeFilter }; diff --git a/packages/features/data-table/hooks/index.ts b/packages/features/data-table/hooks/index.ts index d504af94ee..70dc32414d 100644 --- a/packages/features/data-table/hooks/index.ts +++ b/packages/features/data-table/hooks/index.ts @@ -4,3 +4,4 @@ export * from "./useColumnSizingVars"; export * from "./useFilterValue"; export * from "./useColumnFilters"; export * from "./useDataTable"; +export * from "./useFilterableColumns"; diff --git a/packages/features/data-table/hooks/useFilterableColumns.ts b/packages/features/data-table/hooks/useFilterableColumns.ts new file mode 100644 index 0000000000..56c0ee6222 --- /dev/null +++ b/packages/features/data-table/hooks/useFilterableColumns.ts @@ -0,0 +1,56 @@ +"use client"; + +import { type Table } from "@tanstack/react-table"; +// eslint-disable-next-line no-restricted-imports +import { useMemo } from "react"; + +import type { FilterableColumn } from "../lib/types"; + +export function useFilterableColumns(table: Table) { + const columns = useMemo( + () => table.getAllColumns().filter((column) => column.getCanFilter()), + [table.getAllColumns()] + ); + + const filterableColumns = useMemo( + () => + columns + .map((column) => { + const type = column.columnDef.meta?.filter?.type || "multi_select"; + const base = { + id: column.id, + title: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id, + ...(column.columnDef.meta?.filter || {}), + type, + }; + if (type === "multi_select" || type === "single_select") { + const values = column.getFacetedUniqueValues(); + const options = Array.from(values.keys()).map((option) => { + if (typeof option === "string") { + return { + label: option, + value: option, + }; + } else { + return { + label: option.label as string, + value: option.value as string | number, + }; + } + }); + return { + ...base, + options, + }; + } else { + return { + ...base, + }; + } + }) + .filter((column): column is FilterableColumn => Boolean(column)), + [columns] + ); + + return filterableColumns; +}