From f699fd1732409de93cfe16fa4cfaf47dd4b0711b Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Wed, 26 Mar 2025 10:52:40 +0100 Subject: [PATCH] fix: add UID filter to /insights/routing (#20356) --- apps/web/public/static/locales/en/common.json | 3 ++- .../components/filters/TextFilterOptions.tsx | 12 ++++++++--- .../data-table/components/filters/utils.ts | 11 ++++++++-- packages/features/data-table/lib/server.ts | 4 ++-- packages/features/data-table/lib/types.ts | 17 ++++++++++++--- .../insights/components/RoutingKPICards.tsx | 6 +++++- .../insights/hooks/useInsightsColumns.tsx | 18 +++++++++++++++- .../insights/server/routing-events.ts | 21 ++++++++++++++++++- 8 files changed, 78 insertions(+), 14 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 5fb157e96b..daa2e4be7d 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3037,5 +3037,6 @@ "no_team_members": "You don't have team members yet", "could_not_find_slug_to_publish_org": "Could not find slug to publish the organization", "picklist": "Picklist", + "name_or_email": "Name or Email", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" -} \ No newline at end of file +} diff --git a/packages/features/data-table/components/filters/TextFilterOptions.tsx b/packages/features/data-table/components/filters/TextFilterOptions.tsx index 9a0bc5140d..c05ffc83e6 100644 --- a/packages/features/data-table/components/filters/TextFilterOptions.tsx +++ b/packages/features/data-table/components/filters/TextFilterOptions.tsx @@ -3,8 +3,8 @@ import { useForm, Controller } from "react-hook-form"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Select } from "@calcom/ui/components/form"; import { Button } from "@calcom/ui/components/button"; +import { Select } from "@calcom/ui/components/form"; import { Form } from "@calcom/ui/components/form"; import { Input } from "@calcom/ui/components/form"; @@ -19,7 +19,7 @@ export type TextFilterOptionsProps = { export function TextFilterOptions({ column }: TextFilterOptionsProps) { const { t } = useLocale(); - const textFilterOperatorOptions = useTextFilterOperatorOptions(); + const textFilterOperatorOptions = useTextFilterOperatorOptions(column.textOptions?.allowedOperators); const filterValue = useFilterValue(column.id, ZTextFilterValue); const { updateFilter, removeFilter } = useDataTable(); @@ -63,7 +63,13 @@ export function TextFilterOptions({ column }: TextFilterOptionsProps) { } }} /> - {value?.requiresOperand && } + {value?.requiresOperand && ( + + )} )} /> diff --git a/packages/features/data-table/components/filters/utils.ts b/packages/features/data-table/components/filters/utils.ts index 00099c28f5..a9a6ca59d4 100644 --- a/packages/features/data-table/components/filters/utils.ts +++ b/packages/features/data-table/components/filters/utils.ts @@ -1,5 +1,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { type TextFilterOperator, textFilterOperators } from "../../lib/types"; import type { TextFilterOperatorOption, NumberFilterOperatorOption } from "./types"; export const numberFilterOperatorOptions: NumberFilterOperatorOption[] = [ @@ -11,9 +12,12 @@ export const numberFilterOperatorOptions: NumberFilterOperatorOption[] = [ { value: "lte", label: "≤" }, ]; -export const useTextFilterOperatorOptions = (): TextFilterOperatorOption[] => { +export const useTextFilterOperatorOptions = ( + allowedOperators?: TextFilterOperator[] +): TextFilterOperatorOption[] => { const { t } = useLocale(); - return [ + const operators = allowedOperators ?? textFilterOperators; + const options = [ { value: "equals", label: t("filter_operator_is"), requiresOperand: true }, { value: "notEquals", label: t("filter_operator_is_not"), requiresOperand: true }, { value: "contains", label: t("filter_operator_contains"), requiresOperand: true }, @@ -23,4 +27,7 @@ export const useTextFilterOperatorOptions = (): TextFilterOperatorOption[] => { { value: "isEmpty", label: t("filter_operator_is_empty"), requiresOperand: false }, { value: "isNotEmpty", label: t("filter_operator_not_empty"), requiresOperand: false }, ]; + return options.filter((opt): opt is TextFilterOperatorOption => + operators.includes(opt.value as TextFilterOperator) + ); }; diff --git a/packages/features/data-table/lib/server.ts b/packages/features/data-table/lib/server.ts index 4bee912079..1baa98979e 100644 --- a/packages/features/data-table/lib/server.ts +++ b/packages/features/data-table/lib/server.ts @@ -6,7 +6,7 @@ import { isNumberFilterValue, } from "./utils"; -type makeWhereClauseProps = { +type MakeWhereClauseProps = { columnName: string; filterValue: FilterValue; json?: true | { path: string[] }; @@ -20,7 +20,7 @@ export function makeOrderBy(sorting: SortingState) { })); } -export function makeWhereClause(props: makeWhereClauseProps) { +export function makeWhereClause(props: MakeWhereClauseProps) { const { columnName, filterValue } = props; const isJson = props.json === true || (typeof props.json === "object" && props.json.path?.length > 0); const jsonPath = isJson && typeof props.json === "object" ? props.json.path : undefined; diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts index 0d7811acb6..b611e9e8b3 100644 --- a/packages/features/data-table/lib/types.ts +++ b/packages/features/data-table/lib/types.ts @@ -10,7 +10,7 @@ export enum ColumnFilterType { DATE_RANGE = "dr", } -const textFilterOperators = [ +export const textFilterOperators = [ "equals", "notEquals", "contains", @@ -113,14 +113,24 @@ export type DateRangeFilterOptions = { range: "past" | "custom"; }; +export type TextFilterOptions = { + allowedOperators?: TextFilterOperator[]; + placeholder?: string; +}; + export type ColumnFilterMeta = | { type: ColumnFilterType.DATE_RANGE; icon?: IconName; - dateRangeOptions: DateRangeFilterOptions; + dateRangeOptions?: DateRangeFilterOptions; } | { - type?: Exclude; + type: ColumnFilterType.TEXT; + icon?: IconName; + textOptions?: TextFilterOptions; + } + | { + type?: Exclude; icon?: IconName; }; @@ -139,6 +149,7 @@ export type FilterableColumn = { } | { type: ColumnFilterType.TEXT; + textOptions?: TextFilterOptions; } | { type: ColumnFilterType.NUMBER; diff --git a/packages/features/insights/components/RoutingKPICards.tsx b/packages/features/insights/components/RoutingKPICards.tsx index 1d8bab4d58..a912dd3cb6 100644 --- a/packages/features/insights/components/RoutingKPICards.tsx +++ b/packages/features/insights/components/RoutingKPICards.tsx @@ -50,10 +50,14 @@ export const RoutingKPICards = () => { }, ]; - if (isPending || !data) { + if (isPending) { return ; } + if (!data) { + return null; + } + return ( <> diff --git a/packages/features/insights/hooks/useInsightsColumns.tsx b/packages/features/insights/hooks/useInsightsColumns.tsx index d663eb7153..6bfcc86753 100644 --- a/packages/features/insights/hooks/useInsightsColumns.tsx +++ b/packages/features/insights/hooks/useInsightsColumns.tsx @@ -67,8 +67,16 @@ export const useInsightsColumns = ({ id: "bookingUid", header: t("uid"), size: 100, - enableColumnFilter: false, + enableColumnFilter: true, enableSorting: false, + meta: { + filter: { + type: ColumnFilterType.TEXT, + textOptions: { + allowedOperators: ["equals"], + }, + }, + }, cell: (info) => { const bookingUid = info.getValue(); if (!bookingUid) return null; @@ -95,6 +103,14 @@ export const useInsightsColumns = ({ size: 200, enableColumnFilter: false, enableSorting: false, + meta: { + filter: { + type: ColumnFilterType.TEXT, + textOptions: { + placeholder: t("name_or_email"), + }, + }, + }, cell: (info) => { return ; }, diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts index 433916194d..a2b8693dab 100644 --- a/packages/features/insights/server/routing-events.ts +++ b/packages/features/insights/server/routing-events.ts @@ -126,6 +126,12 @@ class RoutingEventsInsights { sorting, }); + if (whereClause.bookingUid) { + // If bookingUid filter is applied, total count should be either 0 or 1. + // So this metrics doesn't provide any value. + return null; + } + const totalPromise = prisma.routingFormResponse.count({ where: whereClause, }); @@ -217,9 +223,15 @@ class RoutingEventsInsights { const assignmentReasonValue = bookingAssignmentReason ? getLowercasedFilterValue(bookingAssignmentReason) : undefined; + const bookingUid = columnFilters.find((filter) => filter.id === "bookingUid") as + | TypedColumnFilter + | undefined; const responseFilters = columnFilters.filter( - (filter) => filter.id !== "bookingStatusOrder" && filter.id !== "bookingAssignmentReason" + (filter) => + filter.id !== "bookingStatusOrder" && + filter.id !== "bookingAssignmentReason" && + filter.id !== "bookingUid" ); const whereClause: Prisma.RoutingFormResponseWhereInput = { @@ -256,6 +268,13 @@ class RoutingEventsInsights { }, }), + // bookingUid + ...(bookingUid && + makeWhereClause({ + columnName: "bookingUid", + filterValue: bookingUid.value, + })), + // AND clause ...(responseFilters.length > 0 && { AND: responseFilters.map((fieldFilter) => {