Skip to content

Commit 5d88858

Browse files
feat: refactor filters on insights response table (#17796)
* feat: improve text filters (WIP) * move function to bottom * apply some styles * fix selection of TextFilterOptions * rename value to operand * remove unused file * merge filters/filters into filters/utils * fix regression of not putting url params correctly * move makeWhereClause to filters/utils * fix negative, empty, and not empty operators * fix initial filtering from search state (url) * fix type errors * do not send an empty array to query * update yarn.lock * i18n for text filter operators * extract logic as useColumnFilters() * add missing import * fix type error * revert yarn.lock * use i18n * insensitive text match * move data-table to @calcom/features * fix type errors * fix type errors * fix type errors * feat: support DataTable filters for Insights Routing WIP * remove unused filters * remove additionalFilters and fix types * clean up filter components * support icons for ActiveFilters * support filters on json * fix filter ui * fix type error and clean up * revert changes * revert change * clean up * revert change * fix compatibility with insights booking page * remove unused params * fix type errors * update yarn.lock * fix field filter and adjust ui * chore: update yarn.lock * fix text filter * add Clear Filters button * add more test data --------- Co-authored-by: Udit Takkar <udit222001@gmail.com>
1 parent 82a500a commit 5d88858

29 files changed

+562
-321
lines changed

apps/web/modules/insights/insights-routing-view.tsx

+3-18
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,19 @@
11
"use client";
22

3-
import {
4-
FailedBookingsByField,
5-
RoutingFormResponsesTable,
6-
RoutingKPICards,
7-
} from "@calcom/features/insights/components";
3+
import { FailedBookingsByField, RoutingFormResponsesTable } from "@calcom/features/insights/components";
84
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
9-
import { RoutingInsightsFilters } from "@calcom/features/insights/filters/routing/FilterBar";
105
import { useLocale } from "@calcom/lib/hooks/useLocale";
116

127
import InsightsLayout from "./layout";
138

14-
export default function InsightsPage() {
9+
export default function InsightsRoutingFormResponsesPage() {
1510
const { t } = useLocale();
1611

1712
return (
1813
<InsightsLayout>
1914
<FiltersProvider>
2015
<div className="mb-4 space-y-4">
21-
<RoutingFormResponsesTable>
22-
{/* We now render the "filters and KPI as a children of the table but we still need to pass the table instance to it so we can access column status in the toolbar.*/}
23-
{(table) => (
24-
<div className="header mb-4">
25-
<div className="flex items-center justify-between">
26-
<RoutingInsightsFilters table={table} />
27-
</div>
28-
<RoutingKPICards />
29-
</div>
30-
)}
31-
</RoutingFormResponsesTable>
16+
<RoutingFormResponsesTable />
3217

3318
<FailedBookingsByField />
3419

apps/web/public/static/locales/en/common.json

+2
Original file line numberDiff line numberDiff line change
@@ -2822,6 +2822,8 @@
28222822
"rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.",
28232823
"exclude_emails_that_contain": "Exclude emails that contain ...",
28242824
"exclude_emails_match_found_error_message": "Please enter a valid work email address",
2825+
"search_columns": "Search columns",
2826+
"search_options": "Search options",
28252827
"disable_org_url_label": "Disable public organization profile and redirect",
28262828
"disable_org_url_description": "Redirects {{orgSlug}}.cal.com to {{destination}}",
28272829
"single_select": "Single Select",

packages/features/apps/AdminAppsList.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ const AdminAppsList = ({
158158
<form
159159
{...rest}
160160
className={
161-
classNames?.form ?? "max-w-80 bg-default mb-4 rounded-md px-0 pt-0 md:max-w-full md:px-8 md:pt-10"
161+
classNames?.form ?? "bg-default mb-4 max-w-80 rounded-md px-0 pt-0 md:max-w-full md:px-8 md:pt-10"
162162
}
163163
onSubmit={(e) => {
164164
e.preventDefault();

packages/features/data-table/components/DataTableToolbar.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
1010
import type { ButtonProps } from "@calcom/ui";
1111
import { Button, Input } from "@calcom/ui";
1212

13+
import { useColumnFilters } from "../lib/utils";
14+
1315
interface DataTableToolbarProps extends ComponentPropsWithoutRef<"div"> {
1416
children: React.ReactNode;
1517
}
@@ -89,7 +91,8 @@ function ClearFiltersButtonComponent<TData>(
8991
ref: React.Ref<HTMLButtonElement>
9092
) {
9193
const { t } = useLocale();
92-
const isFiltered = table.getState().columnFilters.length > 0;
94+
const columnFilters = useColumnFilters();
95+
const isFiltered = columnFilters.length > 0;
9396
if (!isFiltered) return null;
9497
return (
9598
<Button

packages/features/data-table/components/filters/FilterOptions.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function FilterOptions<TData>({ column, filter, state, setState, table }:
3535
table.getColumn(columnId)?.setFilterValue(undefined);
3636
};
3737

38-
if (column.filterType === "text") {
38+
if (column.type === "text") {
3939
return (
4040
<TextFilterOptions
4141
column={column}
@@ -44,7 +44,7 @@ export function FilterOptions<TData>({ column, filter, state, setState, table }:
4444
removeFilter={removeFilter}
4545
/>
4646
);
47-
} else if (column.filterType === "select") {
47+
} else if (column.type === "select") {
4848
return (
4949
<MultiSelectFilterOptions
5050
column={column}

packages/features/data-table/components/filters/MultiSelectFilterOptions.tsx

+11-8
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import type { FilterableColumn, SelectFilterValue } from "../../lib/types";
1616

1717
export type MultiSelectFilterOptionsProps = {
18-
column: FilterableColumn;
18+
column: Extract<FilterableColumn, { type: "select" }>;
1919
filterValue?: SelectFilterValue;
2020
setFilterValue: (value: SelectFilterValue) => void;
2121
removeFilter: (columnId: string) => void;
@@ -36,27 +36,30 @@ export function MultiSelectFilterOptions({
3636
<CommandEmpty>{t("no_options_found")}</CommandEmpty>
3737
{Array.from(column.options.keys()).map((option) => {
3838
if (!option) return null;
39+
const { label: optionLabel, value: optionValue } =
40+
typeof option === "string" ? { label: option, value: option } : option;
41+
3942
return (
4043
<CommandItem
41-
key={option}
44+
key={optionValue}
4245
onSelect={() => {
43-
const newFilterValue = filterValue?.includes(option)
44-
? filterValue?.filter((value) => value !== option)
45-
: [...(filterValue || []), option];
46+
const newFilterValue = filterValue?.includes(optionValue)
47+
? filterValue?.filter((value) => value !== optionValue)
48+
: [...(filterValue || []), optionValue];
4649
setFilterValue(newFilterValue);
4750
}}>
4851
<div
4952
className={classNames(
5053
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
51-
Array.isArray(filterValue) && (filterValue as string[])?.includes(option)
54+
Array.isArray(filterValue) && (filterValue as string[])?.includes(optionValue)
5255
? "bg-primary"
5356
: "opacity-50"
5457
)}>
55-
{Array.isArray(filterValue) && (filterValue as string[])?.includes(option) && (
58+
{Array.isArray(filterValue) && (filterValue as string[])?.includes(optionValue) && (
5659
<Icon name="check" className="text-primary-foreground h-4 w-4" />
5760
)}
5861
</div>
59-
{option}
62+
{optionLabel}
6063
</CommandItem>
6164
);
6265
})}

packages/features/data-table/components/filters/TextFilterOptions.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const useTextFilterOperatorOptions = (): TextFilterOperatorOption[] => {
2626
};
2727

2828
export type TextFilterOptionsProps = {
29-
column: FilterableColumn;
29+
column: Extract<FilterableColumn, { type: "text" }>;
3030
filterValue?: TextFilterValue;
3131
setFilterValue: (value: TextFilterValue) => void;
3232
removeFilter: (columnId: string) => void;

packages/features/data-table/components/filters/index.tsx

+65-43
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from "@calcom/ui";
2424

2525
import type { FilterableColumn } from "../../lib/types";
26-
import { useFiltersSearchState } from "../../lib/utils";
26+
import { convertToTitleCase, useFiltersState } from "../../lib/utils";
2727
import { FilterOptions } from "./FilterOptions";
2828

2929
interface ColumnVisiblityProps<TData> {
@@ -118,35 +118,24 @@ const ColumnVisibilityButton = forwardRef(ColumnVisibilityButtonComponent) as <T
118118
) => ReturnType<typeof ColumnVisibilityButtonComponent>;
119119

120120
// Filters
121-
interface FilterButtonProps<TData> {
121+
interface AddFilterButtonProps<TData> {
122122
table: Table<TData>;
123123
omit?: string[];
124124
}
125125

126-
function FilterButtonComponent<TData>(
127-
{ table, omit }: FilterButtonProps<TData>,
126+
function AddFilterButtonComponent<TData>(
127+
{ table, omit }: AddFilterButtonProps<TData>,
128128
ref: React.Ref<HTMLButtonElement>
129129
) {
130130
const { t } = useLocale();
131-
const [_state, _setState] = useFiltersSearchState();
132-
133-
const activeFilters = _state.activeFilters;
134-
const columns = table
135-
.getAllColumns()
136-
.filter((column) => column.getCanFilter())
137-
.filter((column) => !omit?.includes(column.id));
138-
139-
const filterableColumns = useMemo(() => {
140-
return columns.map((column) => ({
141-
id: column.id,
142-
title: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
143-
options: column.getFacetedUniqueValues(),
144-
}));
145-
}, [columns]);
131+
const { state, setState } = useFiltersState();
132+
133+
const activeFilters = state.activeFilters;
134+
const filterableColumns = useFilterableColumns(table, omit);
146135

147136
const handleAddFilter = (columnId: string) => {
148137
if (!activeFilters?.some((filter) => filter.f === columnId)) {
149-
_setState({ activeFilters: [...activeFilters, { f: columnId, v: [] }] });
138+
setState({ activeFilters: [...activeFilters, { f: columnId, v: [] }] });
150139
}
151140
};
152141

@@ -167,8 +156,11 @@ function FilterButtonComponent<TData>(
167156
{filterableColumns.map((column) => {
168157
if (activeFilters?.some((filter) => filter.f === column.id)) return null;
169158
return (
170-
<CommandItem key={column.id} onSelect={() => handleAddFilter(column.id)}>
171-
{column.title}
159+
<CommandItem
160+
key={column.id}
161+
onSelect={() => handleAddFilter(column.id)}
162+
className="px-4 py-2">
163+
{convertToTitleCase(column.title)}
172164
</CommandItem>
173165
);
174166
})}
@@ -180,50 +172,80 @@ function FilterButtonComponent<TData>(
180172
);
181173
}
182174

183-
const FilterButton = forwardRef(FilterButtonComponent) as <TData>(
184-
props: FilterButtonProps<TData> & { ref?: React.Ref<HTMLButtonElement>; omit?: string[] }
185-
) => ReturnType<typeof FilterButtonComponent>;
175+
function useFilterableColumns<TData>(table: Table<TData>, omit?: string[]) {
176+
const columns = useMemo(
177+
() =>
178+
table
179+
.getAllColumns()
180+
.filter((column) => column.getCanFilter())
181+
.filter((column) => !omit?.includes(column.id)),
182+
[table.getAllColumns(), omit]
183+
);
184+
185+
const filterableColumns = useMemo<FilterableColumn[]>(
186+
() =>
187+
columns
188+
.map((column) => {
189+
const type = column.columnDef.meta?.filter?.type || "select";
190+
const base = {
191+
id: column.id,
192+
title: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
193+
...(column.columnDef.meta?.filter || {}),
194+
type,
195+
};
196+
if (type === "select") {
197+
return {
198+
...base,
199+
options: column.getFacetedUniqueValues(),
200+
};
201+
} else if (type === "text") {
202+
return {
203+
...base,
204+
};
205+
}
206+
})
207+
.filter((column): column is FilterableColumn => Boolean(column)),
208+
[columns]
209+
);
210+
211+
return filterableColumns;
212+
}
213+
214+
const AddFilterButton = forwardRef(AddFilterButtonComponent) as <TData>(
215+
props: AddFilterButtonProps<TData> & { ref?: React.Ref<HTMLButtonElement>; omit?: string[] }
216+
) => ReturnType<typeof AddFilterButtonComponent>;
186217

187218
// Add the new ActiveFilters component
188219
interface ActiveFiltersProps<TData> {
189220
table: Table<TData>;
190221
}
191222

192223
function ActiveFilters<TData>({ table }: ActiveFiltersProps<TData>) {
193-
const [_state, _setState] = useFiltersSearchState();
194-
195-
const columns = table.getAllColumns().filter((column) => column.getCanFilter());
224+
const { state, setState } = useFiltersState();
196225

197-
const filterableColumns = useMemo<FilterableColumn[]>(() => {
198-
return columns.map((column) => {
199-
return {
200-
id: column.id,
201-
title: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
202-
filterType: column.columnDef.meta?.filterType || "select",
203-
options: column.getFacetedUniqueValues(),
204-
};
205-
});
206-
}, [columns]);
226+
const filterableColumns = useFilterableColumns(table);
207227

208228
return (
209229
<>
210-
{(_state.activeFilters || []).map((filter) => {
230+
{state.activeFilters.map((filter) => {
211231
const column = filterableColumns.find((col) => col.id === filter.f);
212232
if (!column) return null;
233+
const icon = column.icon || (column.type === "text" ? "file-text" : "layers");
213234
return (
214235
<Popover key={column.id}>
215236
<PopoverTrigger asChild>
216237
<Button color="secondary">
217-
{column.title}
238+
<Icon name={icon} className="mr-2 h-4 w-4" />
239+
{convertToTitleCase(column.title)}
218240
<Icon name="chevron-down" className="ml-2 h-4 w-4" />
219241
</Button>
220242
</PopoverTrigger>
221243
<PopoverContent className="p-0" align="start">
222244
<FilterOptions
223245
column={column}
224246
filter={filter}
225-
state={_state}
226-
setState={_setState}
247+
state={state}
248+
setState={setState}
227249
table={table}
228250
/>
229251
</PopoverContent>
@@ -235,4 +257,4 @@ function ActiveFilters<TData>({ table }: ActiveFiltersProps<TData>) {
235257
}
236258

237259
// Update the export to include ActiveFilters
238-
export const DataTableFilters = { ColumnVisibilityButton, FilterButton, ActiveFilters };
260+
export const DataTableFilters = { ColumnVisibilityButton, AddFilterButton, ActiveFilters };

0 commit comments

Comments
 (0)