From c41ba9d9841b276372705a971790c608a56677bb Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 5 Dec 2024 18:29:09 +0100 Subject: [PATCH] revert: "feat: refactor filters on insights response table (#17796)" (#18024) * revert: "feat: refactor filters on insights response table (#17796)" This reverts commit 5d8885899562d577dace2839f6fb3d5453a2bea3. * Put back the search columns and options english terms --- .../insights/insights-routing-view.tsx | 21 ++- apps/web/public/static/locales/en/common.json | 1 - packages/features/apps/AdminAppsList.tsx | 2 +- .../components/DataTableToolbar.tsx | 5 +- .../components/filters/FilterOptions.tsx | 4 +- .../filters/MultiSelectFilterOptions.tsx | 19 +- .../components/filters/TextFilterOptions.tsx | 2 +- .../data-table/components/filters/index.tsx | 106 +++++------ packages/features/data-table/lib/server.ts | 37 ++-- packages/features/data-table/lib/types.ts | 28 +-- packages/features/data-table/lib/utils.ts | 95 ++++------ .../attributes/attributes-list-view.tsx | 1 + .../ee/teams/components/MemberList.tsx | 2 +- .../components/RoutingFormResponsesTable.tsx | 166 +++++------------- .../insights/filters/ClearFilters.tsx | 48 ----- .../features/insights/filters/DateSelect.tsx | 5 +- .../filters/RoutingFormFilterList.tsx | 54 +++--- .../insights/filters/TeamAndSelfList.tsx | 10 +- ...UserListInTeam.tsx => UsersListInTeam.tsx} | 14 +- packages/features/insights/filters/index.tsx | 4 +- .../insights/filters/routing/FilterBar.tsx | 77 ++++++++ .../insights/server/routing-events.ts | 50 +++--- .../features/insights/server/trpc-router.ts | 18 +- .../components/UserTable/UserListTable.tsx | 15 +- packages/prisma/package.json | 1 - packages/prisma/seed-utils.ts | 71 +------- .../organizations/listMembers.handler.ts | 2 +- packages/types/tanstack-table.d.ts | 4 +- yarn.lock | 8 - 29 files changed, 315 insertions(+), 555 deletions(-) delete mode 100644 packages/features/insights/filters/ClearFilters.tsx rename packages/features/insights/filters/{UserListInTeam.tsx => UsersListInTeam.tsx} (78%) create mode 100644 packages/features/insights/filters/routing/FilterBar.tsx diff --git a/apps/web/modules/insights/insights-routing-view.tsx b/apps/web/modules/insights/insights-routing-view.tsx index ed38a09de2..cddc6f97d9 100644 --- a/apps/web/modules/insights/insights-routing-view.tsx +++ b/apps/web/modules/insights/insights-routing-view.tsx @@ -1,19 +1,34 @@ "use client"; -import { FailedBookingsByField, RoutingFormResponsesTable } from "@calcom/features/insights/components"; +import { + FailedBookingsByField, + RoutingFormResponsesTable, + RoutingKPICards, +} from "@calcom/features/insights/components"; import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider"; +import { RoutingInsightsFilters } from "@calcom/features/insights/filters/routing/FilterBar"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import InsightsLayout from "./layout"; -export default function InsightsRoutingFormResponsesPage() { +export default function InsightsPage() { const { t } = useLocale(); return (
- + + {/* 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.*/} + {(table) => ( +
+
+ +
+ +
+ )} +
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 628d946e28..a8400f7bbc 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2832,4 +2832,3 @@ "salesforce_ignore_guests": "Do not create new records for guests added to the booking", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } - diff --git a/packages/features/apps/AdminAppsList.tsx b/packages/features/apps/AdminAppsList.tsx index 2a9b7f282a..211d15e985 100644 --- a/packages/features/apps/AdminAppsList.tsx +++ b/packages/features/apps/AdminAppsList.tsx @@ -158,7 +158,7 @@ const AdminAppsList = ({
{ e.preventDefault(); diff --git a/packages/features/data-table/components/DataTableToolbar.tsx b/packages/features/data-table/components/DataTableToolbar.tsx index c6080a85f9..1147dcd3c8 100644 --- a/packages/features/data-table/components/DataTableToolbar.tsx +++ b/packages/features/data-table/components/DataTableToolbar.tsx @@ -10,8 +10,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { ButtonProps } from "@calcom/ui"; import { Button, Input } from "@calcom/ui"; -import { useColumnFilters } from "../lib/utils"; - interface DataTableToolbarProps extends ComponentPropsWithoutRef<"div"> { children: React.ReactNode; } @@ -91,8 +89,7 @@ function ClearFiltersButtonComponent( ref: React.Ref ) { const { t } = useLocale(); - const columnFilters = useColumnFilters(); - const isFiltered = columnFilters.length > 0; + const isFiltered = table.getState().columnFilters.length > 0; if (!isFiltered) return null; return ( @@ -244,8 +222,8 @@ function ActiveFilters({ table }: ActiveFiltersProps) { @@ -257,4 +235,4 @@ function ActiveFilters({ table }: ActiveFiltersProps) { } // Update the export to include ActiveFilters -export const DataTableFilters = { ColumnVisibilityButton, AddFilterButton, ActiveFilters }; +export const DataTableFilters = { ColumnVisibilityButton, FilterButton, ActiveFilters }; diff --git a/packages/features/data-table/lib/server.ts b/packages/features/data-table/lib/server.ts index e5934aca51..acf22e0615 100644 --- a/packages/features/data-table/lib/server.ts +++ b/packages/features/data-table/lib/server.ts @@ -1,76 +1,62 @@ import type { FilterValue } from "./types"; import { isSelectFilterValue, isTextFilterValue } from "./utils"; -type makeWhereClauseProps = { - columnName: string; - filterValue: FilterValue; - json?: true | { path: string[] }; -}; - -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; - - const jsonPathObj = isJson && jsonPath ? { path: jsonPath } : {}; +export function makeWhereClause(columnName: string, filterValue: FilterValue) { if (isSelectFilterValue(filterValue)) { return { [columnName]: { - ...jsonPathObj, - ...(isJson ? { array_contains: filterValue } : { in: filterValue }), + in: filterValue, }, }; } else if (isTextFilterValue(filterValue)) { const { operator, operand } = filterValue.data; + switch (operator) { case "equals": return { [columnName]: { - ...jsonPathObj, equals: operand, }, }; case "notEquals": return { [columnName]: { - ...jsonPathObj, not: operand, }, }; case "contains": return { [columnName]: { - ...jsonPathObj, - ...(isJson ? { string_contains: operand } : { contains: operand, mode: "insensitive" }), + contains: operand, + mode: "insensitive", }, }; case "notContains": return { NOT: { [columnName]: { - ...jsonPathObj, - ...(isJson ? { string_contains: operand } : { contains: operand, mode: "insensitive" }), + contains: operand, + mode: "insensitive", }, }, }; case "startsWith": return { [columnName]: { - ...jsonPathObj, - ...(isJson ? { string_starts_with: operand } : { startsWith: operand, mode: "insensitive" }), + startsWith: operand, + mode: "insensitive", }, }; case "endsWith": return { [columnName]: { - ...jsonPathObj, - ...(isJson ? { string_ends_with: operand } : { endsWith: operand, mode: "insensitive" }), + endsWith: operand, + mode: "insensitive", }, }; case "isEmpty": return { [columnName]: { - ...jsonPathObj, equals: "", }, }; @@ -78,7 +64,6 @@ export function makeWhereClause(props: makeWhereClauseProps) { return { NOT: { [columnName]: { - ...jsonPathObj, equals: "", }, }, diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts index 205eac5158..9beb0d4026 100644 --- a/packages/features/data-table/lib/types.ts +++ b/packages/features/data-table/lib/types.ts @@ -1,7 +1,5 @@ import { z } from "zod"; -import type { IconName } from "@calcom/ui/components/icon/icon-names"; - export const ZTextFilterOperator = z.enum([ "equals", "notEquals", @@ -33,29 +31,9 @@ export const ZFilterValue = z.union([ZSelectFilterValue, ZTextFilterValue]); export type FilterValue = z.infer; -export type ColumnFilterMeta = { - type?: "select" | "text"; - icon?: IconName; -}; - export type FilterableColumn = { id: string; title: string; -} & ( - | { - type: "select"; - icon?: IconName; - options: Map; - } - | { - type: "text"; - icon?: IconName; - } -); - -export const ZColumnFilter = z.object({ - id: z.string(), - value: ZFilterValue, -}); - -export type ColumnFilter = z.infer; + filterType: "text" | "select"; + options: Map; +}; diff --git a/packages/features/data-table/lib/utils.ts b/packages/features/data-table/lib/utils.ts index e24a512476..f5a4e19105 100644 --- a/packages/features/data-table/lib/utils.ts +++ b/packages/features/data-table/lib/utils.ts @@ -1,60 +1,51 @@ "use client"; import { parseAsArrayOf, parseAsJson, useQueryStates } from "nuqs"; -import { useMemo, useCallback } from "react"; +import { useMemo } from "react"; import { z } from "zod"; -import type { SelectFilterValue, TextFilterValue, FilterValue } from "./types"; -import { ZFilterValue } from "./types"; +import type { SelectFilterValue, TextFilterValue } from "./types"; +import { ZSelectFilterValue, ZTextFilterValue } from "./types"; -export const dataTableFiltersSchema = z.object({ +const filterSchema = z.object({ f: z.string(), - v: ZFilterValue, + v: z.union([ZSelectFilterValue, ZTextFilterValue]), }); -export function useFiltersState() { - const [state, setState] = useQueryStates({ - activeFilters: parseAsArrayOf(parseAsJson(dataTableFiltersSchema.parse)).withDefault([]), - }); - const clear = useCallback(() => { - setState({ activeFilters: [] }); - }, [setState]); +export const filtersSearchParams = { + activeFilters: parseAsArrayOf(parseAsJson(filterSchema.parse)).withDefault([]), +}; - return { state, setState, clear }; +export function useFiltersSearchState() { + return useQueryStates(filtersSearchParams); } -export type FiltersSearchState = ReturnType["state"]; -export type SetFiltersSearchState = ReturnType["setState"]; -export type ActiveFilter = z.infer; - export function useColumnFilters() { - const { state } = useFiltersState(); - return useMemo( - () => - (state.activeFilters || []) - .filter((filter) => typeof filter === "object" && filter && "f" in filter && "v" in filter) - .map((filter) => ({ - id: filter.f, - value: filter.v, - })) - .filter((filter) => { - // The empty arrays in `filtersSearchState` keep the filter UI component, - // but we do not send them to the actual query. - // Otherwise, `{ my_column_name: { in: []} }` would result in nothing being returned. - if (Array.isArray(filter.value) && filter.value.length === 0) { - return false; - } - return true; - }), - [state] - ); + const [filtersSearchState] = useFiltersSearchState(); + const columnFilters = useMemo(() => { + return (filtersSearchState.activeFilters || []) + .map((filter) => ({ + id: filter.f, + value: filter.v, + })) + .filter((filter) => { + // The empty arrays in `filtersSearchState` keep the filter UI component, + // but we do not send them to the actual query. + // Otherwise, { value: [] } would result in nothing being returned. + if (Array.isArray(filter.value) && filter.value.length === 0) { + return false; + } + return true; + }); + }, [filtersSearchState.activeFilters]); + return columnFilters; } -export const textFilter = (cellValue: unknown, filterValue: TextFilterValue) => { - if (typeof cellValue !== "string") { - return false; - } +export type FiltersSearchState = ReturnType[0]; +export type SetFiltersSearchState = ReturnType[1]; +export type ActiveFilter = NonNullable[number]; +export const textFilter = (cellValue: string, filterValue: TextFilterValue) => { switch (filterValue.data.operator) { case "equals": return cellValue.toLowerCase() === (filterValue.data.operand || "").toLowerCase(); @@ -86,28 +77,6 @@ export const isTextFilterValue = (filterValue: unknown): filterValue is TextFilt ); }; -export const selectFilter = (cellValue: unknown | undefined, filterValue: SelectFilterValue) => { - const cellValueArray = Array.isArray(cellValue) ? cellValue : [cellValue]; - if (!cellValueArray.every((value) => typeof value === "string")) { - return false; - } - - return filterValue.length === 0 ? true : cellValueArray.some((v) => filterValue.includes(v)); -}; - export const isSelectFilterValue = (filterValue: unknown): filterValue is SelectFilterValue => { return Array.isArray(filterValue) && filterValue.every((item) => typeof item === "string"); }; - -export const dataTableFilter = (cellValue: unknown, filterValue: FilterValue) => { - if (isSelectFilterValue(filterValue)) { - return selectFilter(cellValue, filterValue); - } else if (isTextFilterValue(filterValue)) { - return textFilter(cellValue, filterValue); - } - return false; -}; - -export const convertToTitleCase = (str: string) => { - return str.replace(/\b\w/g, (char) => char.toUpperCase()); -}; diff --git a/packages/features/ee/organizations/pages/settings/attributes/attributes-list-view.tsx b/packages/features/ee/organizations/pages/settings/attributes/attributes-list-view.tsx index d75e0dbe2a..9a72871cb5 100644 --- a/packages/features/ee/organizations/pages/settings/attributes/attributes-list-view.tsx +++ b/packages/features/ee/organizations/pages/settings/attributes/attributes-list-view.tsx @@ -17,6 +17,7 @@ import { DropdownMenuItem, Icon, showToast, + useMeta, } from "@calcom/ui"; import { DeleteAttributeModal } from "./DeleteAttributeModal"; diff --git a/packages/features/ee/teams/components/MemberList.tsx b/packages/features/ee/teams/components/MemberList.tsx index ca0a8a1330..f5fdc0cca4 100644 --- a/packages/features/ee/teams/components/MemberList.tsx +++ b/packages/features/ee/teams/components/MemberList.tsx @@ -654,7 +654,7 @@ export default function MemberList(props: Props) {
setDebouncedSearchTerm(value)} /> - + {isAdminOrOwner && ( @@ -120,31 +102,29 @@ function BookedByCell({ ); } -type ResponseValue = { label: string; value: string }; - -function ResponseValueCell({ values, rowId }: { values: ResponseValue[]; rowId: number }) { +function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number }) { const cellId = useId(); - if (values.length === 0) return
; + if (value.length === 0) return
; return ( - {values.length > 2 ? ( + {value.length > 2 ? ( <> - {values.slice(0, 2).map((value: ResponseValue, i: number) => ( + {value.slice(0, 2).map((v: string, i: number) => ( - {value.label} + {v} ))} - +{values.length - 2} + +{value.length - 2}
- {values.slice(2).map((value: ResponseValue, i: number) => ( + {value.slice(2).map((v: string, i: number) => ( - {value.label} + {v} ))}
@@ -153,9 +133,9 @@ function ResponseValueCell({ values, rowId }: { values: ResponseValue[]; rowId:
) : ( - values.map((value: ResponseValue, i: number) => ( + value.map((v: string, i: number) => ( - {value.label} + {v} )) )} @@ -249,15 +229,21 @@ export function RoutingFormResponsesTable({ const { t } = useLocale(); const { filter } = useFilterContext(); const tableContainerRef = useRef(null); - const { copyToClipboard } = useCopy(); + const { copyToClipboard, isCopied } = useCopy(); - const { dateRange, selectedTeamId, isAll, initialConfig, selectedRoutingFormId, selectedMemberUserId } = - filter; + const { + dateRange, + selectedTeamId, + isAll, + initialConfig, + selectedRoutingFormId, + selectedMemberUserId, + selectedBookingStatus, + selectedRoutingFormFilter, + } = filter; const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll); const [startDate, endDate] = dateRange; - const columnFilters = useColumnFilters(); - const { data: headers, isLoading: isHeadersLoading } = trpc.viewer.insights.routingFormResponsesHeaders.useQuery( { @@ -279,7 +265,8 @@ export function RoutingFormResponsesTable({ userId: selectedMemberUserId ?? undefined, isAll: isAll ?? false, routingFormId: selectedRoutingFormId ?? undefined, - columnFilters, + bookingStatus: selectedBookingStatus ?? undefined, + fieldFilter: selectedRoutingFormFilter ?? undefined, limit: 30, }, { @@ -308,27 +295,20 @@ export function RoutingFormResponsesTable({ }; Object.entries(response.response).forEach(([fieldId, field]) => { - const fieldHeader = headers?.fields.find((h) => h.id === fieldId); + const header = headers?.find((h) => h.id === fieldId); - if (fieldHeader?.options) { + if (header?.options) { if (Array.isArray(field.value)) { // Map the IDs to their corresponding labels for array values - const labels = field.value - .map((id) => { - const option = fieldHeader.options?.find((opt) => opt.id === id); - if (!option) { - return undefined; - } - return { label: option.label, value: option.id }; - }) - .filter(Boolean); + const labels = field.value.map((id) => { + const option = header.options?.find((opt) => opt.id === id); + return option?.label ?? id; + }); row[fieldId] = labels; } else { // Handle single value case - const option = fieldHeader.options?.find((opt) => opt.id === field.value); - if (option) { - row[fieldId] = { label: option.label, value: option.id }; - } + const option = header.options?.find((opt) => opt.id === field.value); + row[fieldId] = option?.label ?? field.value; } } else { row[fieldId] = field.value; @@ -347,50 +327,26 @@ export function RoutingFormResponsesTable({ id: "bookedBy", header: t("routing_form_insights_booked_by"), size: 200, - enableColumnFilter: false, cell: (info) => { const row = info.row.original; return ; }, }), - ...((headers?.fields || []).map((fieldHeader) => { - const isTextOrEmail = fieldHeader.type === "text" || fieldHeader.type === "email"; - return columnHelper.accessor(fieldHeader.id, { - id: fieldHeader.id, - header: convertToTitleCase(fieldHeader.label), + ...(headers?.map((header) => { + return columnHelper.accessor(header.id, { + id: header.id, + header: header.label, size: 200, cell: (info) => { - const values = info.getValue(); + let value = info.getValue(); + value = Array.isArray(value) ? value : [value]; return (
- {isTextOrEmail ? ( - {values} - ) : ( - - )} +
); }, - meta: { - filter: isTextOrEmail ? { type: "text" } : { type: "select" }, - }, - filterFn: (row, id, filterValue: FilterValue) => { - let cellValue: unknown; - - if (Array.isArray(fieldHeader.options)) { - cellValue = Array.isArray(row.original[id]) - ? row.original[id].map((item: FieldCellValue) => item.value) - : row.original[id].value; - } else { - cellValue = row.original[id]; - } - - return dataTableFilter(cellValue, filterValue); - }, }); }) ?? []), columnHelper.accessor("routedToBooking", { @@ -402,18 +358,11 @@ export function RoutingFormResponsesTable({
), - meta: { - filter: { type: "select", icon: "circle" }, - }, - filterFn: (row, id, filterValue) => { - return selectFilter(row.original.routedToBooking?.status, filterValue); - }, }), columnHelper.accessor("routedToBooking", { id: "bookingAt", header: t("routing_form_insights_booking_at"), size: 250, - enableColumnFilter: false, cell: (info) => (
(
{dayjs(info.getValue()).format("MMM D, YYYY HH:mm")} @@ -462,27 +410,6 @@ export function RoutingFormResponsesTable({ defaultColumn: { size: 200, }, - state: { - columnFilters, - }, - getFacetedUniqueValues: (_, columnId) => () => { - if (!headers) { - return new Map(); - } - - const fieldHeader = headers.fields.find((h) => h.id === columnId); - if (fieldHeader?.options) { - return new Map(fieldHeader.options.map((option) => [{ label: option.label, value: option.id }, 1])); - } else if (columnId === "bookingStatus") { - return new Map( - Object.keys(BookingStatus).map((status) => [ - { value: status, label: bookingStatusToText(status as BookingStatus) }, - 1, - ]) - ); - } - return new Map(); - }, }); const fetchMoreOnBottomReached = useFetchMoreOnBottomReached( @@ -550,20 +477,7 @@ export function RoutingFormResponsesTable({ } }} isPending={isFetching && !data}> -
-
- - - - - - - - - -
- -
+ {typeof children === "function" ? children(table) : children}
); diff --git a/packages/features/insights/filters/ClearFilters.tsx b/packages/features/insights/filters/ClearFilters.tsx deleted file mode 100644 index 7e14809072..0000000000 --- a/packages/features/insights/filters/ClearFilters.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useFiltersState } from "@calcom/features/data-table"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Icon, Button, Tooltip } from "@calcom/ui"; - -import { useFilterContext } from "../context/provider"; - -// This component clears filters from both: -// - the filter from FilterContext -// - the data table filters state -export const ClearFilters = () => { - const { t } = useLocale(); - const { clearFilters, filter } = useFilterContext(); - const { state, clear: clearFiltersState } = useFiltersState(); - - const clear = () => { - // clear filters from the filter context - clearFilters(); - - // clear filters from the data table state - clearFiltersState(); - }; - - const { initialConfig, selectedTeamId, selectedMemberUserId } = filter; - - const isFilterSelected = - initialConfig?.teamId !== selectedTeamId || - initialConfig?.userId !== selectedMemberUserId || - state.activeFilters?.length > 0; - - if (!isFilterSelected) { - return null; - } - - return ( - - - - ); -}; diff --git a/packages/features/insights/filters/DateSelect.tsx b/packages/features/insights/filters/DateSelect.tsx index 1f3169c296..2432754b8d 100644 --- a/packages/features/insights/filters/DateSelect.tsx +++ b/packages/features/insights/filters/DateSelect.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; -import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { DateRangePicker } from "@calcom/ui"; import { Select } from "@calcom/ui"; @@ -10,7 +9,7 @@ import { Select } from "@calcom/ui"; import { useFilterContext } from "../context/provider"; import "./DateSelect.css"; -export const DateSelect = ({ className }: { className?: string }) => { +export const DateSelect = () => { const { t } = useLocale(); const presetOptions = [ { label: t("today"), value: "tdy" }, @@ -69,7 +68,7 @@ export const DateSelect = ({ className }: { className?: string }) => { }; return ( -
+
{ - const { t } = useLocale(); - const { filter, setConfigFilters } = useFilterContext(); - const { selectedTeamId, selectedRoutingFormId, isAll } = filter; - const { selectedFilter } = filter; +export const RoutingFormFilterList = memo(() => { + const { t } = useLocale(); + const { filter, setConfigFilters } = useFilterContext(); + const { selectedTeamId, selectedRoutingFormId, isAll } = filter; + const { selectedFilter } = filter; - const { data: allForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery( - { - teamId: selectedTeamId ?? undefined, - isAll: isAll ?? false, - }, - { - enabled: selectedFilter?.includes("routing_forms"), - } - ); + const { data: allForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery( + { + teamId: selectedTeamId ?? undefined, + isAll: isAll ?? false, + }, + { + enabled: selectedFilter?.includes("routing_forms"), + } + ); - if (showOnlyWhenSelectedInContext && !selectedFilter?.includes("routing_forms")) return null; + if (!selectedFilter?.includes("routing_forms")) return null; - const filterOptions = buildFilterOptions(allForms || []); + const filterOptions = buildFilterOptions(allForms || []); - return ( - setConfigFilters({ selectedRoutingFormId: value as string })} - buttonIcon={} - /> - ); - } -); + return ( + setConfigFilters({ selectedRoutingFormId: value as string })} + buttonIcon={} + /> + ); +}); RoutingFormFilterList.displayName = "RoutingFormFilterList"; diff --git a/packages/features/insights/filters/TeamAndSelfList.tsx b/packages/features/insights/filters/TeamAndSelfList.tsx index f0134205ed..ce302d2c12 100644 --- a/packages/features/insights/filters/TeamAndSelfList.tsx +++ b/packages/features/insights/filters/TeamAndSelfList.tsx @@ -12,13 +12,7 @@ import { AnimatedPopover, Avatar, Divider, Icon } from "@calcom/ui"; import { useFilterContext } from "../context/provider"; -export const TeamAndSelfList = ({ - omitOrg = false, - className = "", -}: { - omitOrg?: boolean; - className?: string; -}) => { +export const TeamAndSelfList = ({ omitOrg = false }: { omitOrg?: boolean }) => { const { t } = useLocale(); const session = useSession(); const currentOrgId = session.data?.user.org?.id; @@ -87,7 +81,7 @@ export const TeamAndSelfList = ({ const text = getTextPopover(); return ( - + {isSuccess && data?.length > 0 && data[0].isOrg && ( ({ icon: , }); -export const UserListInTeam = ({ - showOnlyWhenSelectedInContext = true, -}: { - showOnlyWhenSelectedInContext?: boolean; -}) => { +export const UserListInTeam = () => { const { t } = useLocale(); const { filter, setConfigFilters } = useFilterContext(); const { selectedFilter, selectedTeamId, selectedMemberUserId, isAll } = filter; @@ -27,11 +23,7 @@ export const UserListInTeam = ({ isAll: !!isAll, }); - if (showOnlyWhenSelectedInContext && !selectedFilter?.includes("user")) { - return null; - } - - if (!selectedTeamId || !isSuccess || data?.length === 0) { + if (!selectedFilter?.includes("user") || !selectedTeamId || !isSuccess || data?.length === 0) { return null; } @@ -42,7 +34,7 @@ export const UserListInTeam = ({ title={t("people")} options={userListOptions} selectedValue={selectedMemberUserId} - onChange={(value) => setConfigFilters({ selectedMemberUserId: value === null ? null : Number(value) })} + onChange={(value) => setConfigFilters({ selectedMemberUserId: Number(value) })} buttonIcon={} placeholder={t("search")} testId="people-filter" diff --git a/packages/features/insights/filters/index.tsx b/packages/features/insights/filters/index.tsx index 9d277e5b03..62b0d8ed40 100644 --- a/packages/features/insights/filters/index.tsx +++ b/packages/features/insights/filters/index.tsx @@ -10,7 +10,7 @@ import { FilterType } from "./FilterType"; import { RoutingFormFieldFilter } from "./RoutingFormFieldFilter"; import { RoutingFormFilterList } from "./RoutingFormFilterList"; import { TeamAndSelfList } from "./TeamAndSelfList"; -import { UserListInTeam } from "./UserListInTeam"; +import { UserListInTeam } from "./UsersListInTeam"; const ClearFilters = () => { const { t } = useLocale(); @@ -97,7 +97,7 @@ export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: b */}
{showRoutingFilters ? : } - +
); diff --git a/packages/features/insights/filters/routing/FilterBar.tsx b/packages/features/insights/filters/routing/FilterBar.tsx new file mode 100644 index 0000000000..8857501892 --- /dev/null +++ b/packages/features/insights/filters/routing/FilterBar.tsx @@ -0,0 +1,77 @@ +import { DataTableFilters } from "@calcom/features/data-table"; +import type { RoutingFormTableType } from "@calcom/features/insights/components/RoutingFormResponsesTable"; +import { useFilterContext } from "@calcom/features/insights/context/provider"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, Icon, Tooltip } from "@calcom/ui"; + +import { BookingStatusFilter } from "../BookingStatusFilter"; +import { DateSelect } from "../DateSelect"; +import { RoutingDownload } from "../Download"; +import { FilterType } from "../FilterType"; +import { RoutingFormFieldFilter } from "../RoutingFormFieldFilter"; +import { RoutingFormFilterList } from "../RoutingFormFilterList"; +import { TeamAndSelfList } from "../TeamAndSelfList"; +import { UserListInTeam } from "../UsersListInTeam"; + +const ClearFilters = () => { + const { t } = useLocale(); + const { filter, clearFilters } = useFilterContext(); + const { selectedFilter } = filter; + + if (!selectedFilter || selectedFilter?.length < 1) return null; + + return ( + + + + ); +}; + +export const RoutingInsightsFilters = ({ table }: { table: RoutingFormTableType }) => { + const { filter } = useFilterContext(); + const { selectedFilter } = filter; + + // Get all filters that relate to the routing form + const routingFormFieldIds = selectedFilter + ? selectedFilter.map((filter) => { + if (filter.startsWith("rf_")) return filter.substring(3); + }) + : []; + + return ( +
+
+ + + + + + + {routingFormFieldIds.map((fieldId) => { + if (fieldId) return ; + })} + + + + +
+ +
+ + + +
+
+ ); +}; diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts index abdd7873d7..c759fe60c3 100644 --- a/packages/features/insights/server/routing-events.ts +++ b/packages/features/insights/server/routing-events.ts @@ -5,8 +5,6 @@ import { routingFormResponseInDbSchema, } from "@calcom/app-store/routing-forms/zod"; import dayjs from "@calcom/dayjs"; -import { type ColumnFilter } from "@calcom/features/data-table"; -import { makeWhereClause } from "@calcom/features/data-table/lib/server"; import { readonlyPrisma as prisma } from "@calcom/prisma"; import type { BookingStatus } from "@calcom/prisma/enums"; @@ -26,7 +24,6 @@ type RoutingFormInsightsFilter = RoutingFormInsightsTeamFilter & { fieldId: string; optionId: string; } | null; - columnFilters: ColumnFilter[]; }; class RoutingEventsInsights { @@ -84,7 +81,7 @@ class RoutingEventsInsights { userId, bookingStatus, fieldFilter, - }: Omit) { + }: RoutingFormInsightsFilter) { // Get team IDs based on organization if applicable const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ teamId, @@ -190,8 +187,9 @@ class RoutingEventsInsights { cursor, limit, userId, - columnFilters, - }: Omit & { cursor?: number; limit?: number }) { + fieldFilter, + bookingStatus, + }: RoutingFormInsightsFilter & { cursor?: number; limit?: number }) { const formsTeamWhereCondition = await this.getWhereForTeamOrAllTeams({ teamId, isAll, @@ -199,9 +197,6 @@ class RoutingEventsInsights { routingFormId, }); - const bookingStatusFilter = columnFilters.find((filter) => filter.id === "bookingStatus"); - const fieldFilters = columnFilters.filter((filter) => filter.id !== "bookingStatus"); - const responsesWhereCondition: Prisma.App_RoutingForms_FormResponseWhereInput = { ...(startDate && endDate && { @@ -210,31 +205,27 @@ class RoutingEventsInsights { lte: dayjs(endDate).endOf("day").toDate(), }, }), - ...(userId || bookingStatusFilter + ...(userId || bookingStatus ? { - ...{ - routedToBooking: { - ...(userId && { userId }), - ...(bookingStatusFilter && - makeWhereClause({ columnName: "status", filterValue: bookingStatusFilter.value })), - }, - }, + ...(bookingStatus === "NO_BOOKING" + ? { routedToBooking: null } + : { + routedToBooking: { + ...(userId && { userId }), + ...(bookingStatus && { status: bookingStatus }), + }, + }), } : {}), - + ...(fieldFilter && { + response: { + path: [fieldFilter.fieldId, "value"], + array_contains: [fieldFilter.optionId], + }, + }), form: formsTeamWhereCondition, }; - if (fieldFilters.length > 0) { - responsesWhereCondition.AND = fieldFilters.map((fieldFilter) => { - return makeWhereClause({ - columnName: "response", - filterValue: fieldFilter.value, - json: { path: [fieldFilter.id, "value"] }, - }); - }); - } - const totalResponsePromise = prisma.app_RoutingForms_FormResponse.count({ where: responsesWhereCondition, }); @@ -477,7 +468,6 @@ class RoutingEventsInsights { return { id: f.id, label: f.label, - type: f.type, options: f.options, }; }); @@ -497,7 +487,7 @@ class RoutingEventsInsights { fieldFilter, take, skip, - }: Omit & { take?: number; skip?: number }) { + }: RoutingFormInsightsFilter & { take?: number; skip?: number }) { const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ teamId, isAll, diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index 2555dfc146..d9f0a36602 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -3,7 +3,6 @@ import md5 from "md5"; import { z } from "zod"; import dayjs from "@calcom/dayjs"; -import { ZColumnFilter } from "@calcom/features/data-table"; import { rawDataInputSchema } from "@calcom/features/insights/server/raw-data.schema"; import { randomString } from "@calcom/lib/random"; import type { readonlyPrisma } from "@calcom/prisma"; @@ -1597,7 +1596,13 @@ export const insightsRouter = router({ routingFormId: z.string().optional(), cursor: z.number().optional(), limit: z.number().optional(), - columnFilters: z.array(ZColumnFilter), + bookingStatus: bookingStatusSchema, + fieldFilter: z + .object({ + fieldId: z.string(), + optionId: z.string(), + }) + .optional(), }) ) .query(async ({ ctx, input }) => { @@ -1612,7 +1617,8 @@ export const insightsRouter = router({ cursor: input.cursor, userId: input.userId ?? null, limit: input.limit, - columnFilters: input.columnFilters, + bookingStatus: input.bookingStatus ?? null, + fieldFilter: input.fieldFilter ?? null, }); }), getRoutingFormFieldOptions: userBelongsToTeamProcedure @@ -1645,16 +1651,12 @@ export const insightsRouter = router({ }) ) .query(async ({ ctx, input }) => { - const fields = await RoutingEventsInsights.getRoutingFormHeaders({ + return await RoutingEventsInsights.getRoutingFormHeaders({ teamId: input.teamId ?? null, isAll: input.isAll, organizationId: ctx.user.organizationId ?? null, routingFormId: input.routingFormId ?? null, }); - - return { - fields: fields || [], - }; }), rawRoutingData: userBelongsToTeamProcedure .input( diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx index eb0e7217e0..3f139745aa 100644 --- a/packages/features/users/components/UserTable/UserListTable.tsx +++ b/packages/features/users/components/UserTable/UserListTable.tsx @@ -22,8 +22,6 @@ import { useFetchMoreOnBottomReached, textFilter, isTextFilterValue, - isSelectFilterValue, - selectFilter, } from "@calcom/features/data-table"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import classNames from "@calcom/lib/classNames"; @@ -176,7 +174,7 @@ export function UserListTable() { id: attribute.id, header: attribute.name, meta: { - filter: { type: attribute.type.toLowerCase() === "text" ? "text" : "select" }, + filterType: attribute.type.toLowerCase() === "text" ? "text" : "select", }, size: 120, accessorFn: (data) => data.attributes.find((attr) => attr.attributeId === attribute.id)?.value, @@ -200,13 +198,10 @@ export function UserListTable() { if (isTextFilterValue(filterValue)) { return attributeValues.some((attr) => textFilter(attr.value, filterValue)); - } else if (isSelectFilterValue(filterValue)) { - return selectFilter( - attributeValues.map((attr) => attr.value), - filterValue - ); } - return false; + + if (attributeValues.length === 0) return false; + return attributeValues.some((attr) => filterValue.includes(attr.value)); }, })) as ColumnDef[]) ?? [] ); @@ -521,7 +516,7 @@ export function UserListTable() { {t("download")} {/* We have to omit member because we don't want the filter to show but we can't disable filtering as we need that for the search bar */} - + {adminOrOwner && ( ({ @@ -557,24 +547,6 @@ export async function seedRoutingForms( })), required: true, }, - { - id: seededForm.formFieldEmail.id, - type: "email", - label: "Email", - required: true, - }, - { - id: seededForm.formFieldManager.id, - type: "text", - label: "Manager", - required: true, - }, - { - id: seededForm.formFieldRating.id, - type: "number", - label: "Rating", - required: true, - }, ], team: { connect: { @@ -599,16 +571,7 @@ type SeededForm = { id: string; value: string; }[]; - formFieldSkills: { - id: string; - }; - formFieldEmail: { - id: string; - }; - formFieldManager: { - id: string; - }; - formFieldRating: { + formFieldFilled: { id: string; }; }; @@ -653,22 +616,10 @@ export async function seedRoutingFormResponses( formId: seededForm.id, formFillerId: randomUUID(), response: { - [seededForm.formFieldSkills.id]: { + [seededForm.formFieldFilled.id]: { label: "skills", value: selectedSkills.map((opt) => opt.id), }, - [seededForm.formFieldEmail.id]: { - label: "Email", - value: faker.internet.email(), - }, - [seededForm.formFieldManager.id]: { - label: "Manager", - value: faker.person.fullName(), - }, - [seededForm.formFieldRating.id]: { - label: "Rating", - value: Math.floor(Math.random() * 5) + 1, - }, }, }, }); @@ -689,22 +640,10 @@ export async function seedRoutingFormResponses( formId: seededForm.id, formFillerId: randomUUID(), response: { - [seededForm.formFieldSkills.id]: { + [seededForm.formFieldFilled.id]: { label: "skills", value: selectedSkills.map((opt) => opt.id), }, - [seededForm.formFieldEmail.id]: { - label: "Email", - value: faker.internet.email(), - }, - [seededForm.formFieldManager.id]: { - label: "Manager", - value: faker.person.fullName(), - }, - [seededForm.formFieldRating.id]: { - label: "Rating", - value: Math.floor(Math.random() * 5) + 1, - }, }, }, }); diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts index 860ec95c70..7f6e18253e 100644 --- a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts @@ -82,7 +82,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { attribute: { id: filter.id, }, - ...makeWhereClause({ columnName: "value", filterValue: filter.value }), + ...makeWhereClause("value", filter.value), }, }, }; diff --git a/packages/types/tanstack-table.d.ts b/packages/types/tanstack-table.d.ts index ffe75143b1..59fbebc289 100644 --- a/packages/types/tanstack-table.d.ts +++ b/packages/types/tanstack-table.d.ts @@ -1,14 +1,12 @@ import "@tanstack/react-table"; -import type { ColumnFilterMeta } from "@calcom/features/data-table"; - declare module "@tanstack/table-core" { interface ColumnMeta { sticky?: { position: "left" | "right"; gap?: number; }; - filter?: ColumnFilterMeta; + filterType?: "select" | "text"; // `autoWidth` can make the column size dynamic, // allowing each row to have a different width based on its content. diff --git a/yarn.lock b/yarn.lock index 063c86b683..3649318cc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4566,7 +4566,6 @@ __metadata: resolution: "@calcom/prisma@workspace:packages/prisma" dependencies: "@calcom/lib": "*" - "@faker-js/faker": 9.2.0 "@prisma/client": ^5.4.2 "@prisma/extension-accelerate": ^0.6.2 "@prisma/generator-helper": ^5.4.2 @@ -6312,13 +6311,6 @@ __metadata: languageName: node linkType: hard -"@faker-js/faker@npm:9.2.0": - version: 9.2.0 - resolution: "@faker-js/faker@npm:9.2.0" - checksum: 7cbff7363f198c24aa1a25499f200a25506f80df577a419ae878b6bdd4aa901ecfe071ae3bbecf8afafa5542c825623d55f5884d44db7b12420c898241332cd1 - languageName: node - linkType: hard - "@faker-js/faker@npm:^7.3.0": version: 7.4.0 resolution: "@faker-js/faker@npm:7.4.0"