From b1a0cdc8e2ca21a57eb6b31832c8b240db9d71f6 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Sat, 16 Nov 2024 14:01:26 +0000 Subject: [PATCH] fix: large org routing insights UI issues (#17687) * fix overflow colour in darkmode * truncate/show overflow scroll indicator when overflowing on response values * remove total created count * overflow Y on failed response container * fix darkmode styles on barlist * radix hovercards to properly allow overflow in container * column visible state * remove constrained width on dropdowns to w-fit * export type --- .../insights/insights-routing-view.tsx | 8 +- .../components/FailedBookingsByField.tsx | 6 +- .../components/RoutingFormResponsesTable.tsx | 67 ++++--- .../insights/components/RoutingKPICards.tsx | 13 +- .../features/insights/components/index.ts | 2 +- .../insights/components/tremor/BarList.tsx | 165 ++++++++++++++++++ .../features/insights/filters/FilterType.tsx | 12 +- packages/features/insights/filters/index.tsx | 13 +- .../components/data-table/filters/index.tsx | 27 ++- packages/ui/components/dropdown/Dropdown.tsx | 2 +- packages/ui/components/hover-card/index.tsx | 29 +++ packages/ui/index.ts | 2 + 12 files changed, 296 insertions(+), 50 deletions(-) create mode 100644 packages/features/insights/components/tremor/BarList.tsx create mode 100644 packages/ui/components/hover-card/index.tsx diff --git a/apps/web/modules/insights/insights-routing-view.tsx b/apps/web/modules/insights/insights-routing-view.tsx index 24cd91896b..13aa5b35aa 100644 --- a/apps/web/modules/insights/insights-routing-view.tsx +++ b/apps/web/modules/insights/insights-routing-view.tsx @@ -1,9 +1,12 @@ "use client"; +import { useState } from "react"; + import { RoutingFormResponsesTable, RoutingKPICards, FailedBookingsByField, + type RoutingFormTableType, } from "@calcom/features/insights/components"; import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider"; import { Filters } from "@calcom/features/insights/filters"; @@ -13,16 +16,17 @@ import InsightsLayout from "./layout"; export default function InsightsPage() { const { t } = useLocale(); + const [routingTable, setRoutingTable] = useState(null); return ( - +
- + diff --git a/packages/features/insights/components/FailedBookingsByField.tsx b/packages/features/insights/components/FailedBookingsByField.tsx index 7a6b285f0c..a41fee435b 100644 --- a/packages/features/insights/components/FailedBookingsByField.tsx +++ b/packages/features/insights/components/FailedBookingsByField.tsx @@ -1,4 +1,3 @@ -import { BarList } from "@tremor/react"; import { useState } from "react"; import { classNames } from "@calcom/lib"; @@ -6,6 +5,7 @@ import { trpc } from "@calcom/trpc"; import { ToggleGroup } from "@calcom/ui"; import { useFilterContext } from "../context/provider"; +import { BarList } from "./tremor/BarList"; interface FormCardProps { formName: string; @@ -27,7 +27,7 @@ function FormCard({ formName, fields }: FormCardProps) { })); return ( -
+

{formName}

@@ -40,7 +40,7 @@ function FormCard({ formName, fields }: FormCardProps) { onValueChange={(value) => value && setSelectedField(value)} /> {selectedFieldData && ( -
+
value.toString()} diff --git a/packages/features/insights/components/RoutingFormResponsesTable.tsx b/packages/features/insights/components/RoutingFormResponsesTable.tsx index a326fd309c..1a9e14727a 100644 --- a/packages/features/insights/components/RoutingFormResponsesTable.tsx +++ b/packages/features/insights/components/RoutingFormResponsesTable.tsx @@ -9,7 +9,7 @@ import { createColumnHelper, } from "@tanstack/react-table"; import Link from "next/link"; -import { useRef, useMemo, useId } from "react"; +import { useRef, useMemo, useId, useEffect } from "react"; import dayjs from "@calcom/dayjs"; import classNames from "@calcom/lib/classNames"; @@ -17,7 +17,16 @@ import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BookingStatus } from "@calcom/prisma/enums"; import { trpc, type RouterOutputs } from "@calcom/trpc"; -import { DataTable, useFetchMoreOnBottomReached, Badge, Avatar, Icon } from "@calcom/ui"; +import { + DataTable, + useFetchMoreOnBottomReached, + Badge, + Avatar, + Icon, + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@calcom/ui"; import type { BadgeProps } from "@calcom/ui/components/badge/Badge"; import { useFilterContext } from "../context/provider"; @@ -36,7 +45,7 @@ function CellWithOverflowX({ children, className }: { children: React.ReactNode; return (
{ // Only add shadow if the scroll width is greater than 200px if (!el) return; @@ -51,7 +60,7 @@ function CellWithOverflowX({ children, className }: { children: React.ReactNode; }}> {children}
-
+
); } @@ -92,7 +101,7 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number }) if (value.length === 0) return
; return ( -
+ {value.length > 2 ? ( <> {value.slice(0, 2).map((v: string, i: number) => ( @@ -100,18 +109,20 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number }) {v} ))} -
- +{value.length - 2} -
+ + + +{value.length - 2} + +
{value.slice(2).map((v: string, i: number) => ( - + {v} ))}
-
-
+ + ) : ( value.map((v: string, i: number) => ( @@ -120,7 +131,7 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number }) )) )} -
+ ); } @@ -158,12 +169,16 @@ function BookingStatusCell({ } return ( -
- - - {dayjs(booking.createdAt).format("MMM D, YYYY HH:mm")} - -
+ + +
+ + + {dayjs(booking.createdAt).format("MMM D, YYYY HH:mm")} + +
+
+
@@ -188,12 +203,18 @@ function BookingStatusCell({ {bookingStatusToText(booking.status)}
-
-
+ + ); } -export function RoutingFormResponsesTable() { +export type RoutingFormTableType = ReturnType>; + +export function RoutingFormResponsesTable({ + onTableReady, +}: { + onTableReady?: (table: RoutingFormTableType) => void; +}) { const { t } = useLocale(); const { filter } = useFilterContext(); const tableContainerRef = useRef(null); @@ -345,6 +366,10 @@ export function RoutingFormResponsesTable() { }, }); + useEffect(() => { + onTableReady?.(table); + }, [table, onTableReady]); + const fetchMoreOnBottomReached = useFetchMoreOnBottomReached( tableContainerRef, fetchNextPage, diff --git a/packages/features/insights/components/RoutingKPICards.tsx b/packages/features/insights/components/RoutingKPICards.tsx index a4e866dc74..3734e57e77 100644 --- a/packages/features/insights/components/RoutingKPICards.tsx +++ b/packages/features/insights/components/RoutingKPICards.tsx @@ -51,17 +51,8 @@ export const RoutingKPICards = () => { const categories: { title: string; - index: - | "created" - | "active" - | "total_responses" - | "total_responses_without_booking" - | "total_responses_with_booking"; + index: "active" | "total_responses" | "total_responses_without_booking" | "total_responses_with_booking"; }[] = [ - { - title: t("routing_forms_created"), - index: "created", - }, { title: t("routing_forms_total_responses"), index: "total_responses", @@ -84,7 +75,7 @@ export const RoutingKPICards = () => { return ( <> - + {categories.map((item) => ( {item.title} diff --git a/packages/features/insights/components/index.ts b/packages/features/insights/components/index.ts index 86acb20258..3aaa85c3ac 100644 --- a/packages/features/insights/components/index.ts +++ b/packages/features/insights/components/index.ts @@ -9,5 +9,5 @@ export { RecentFeedbackTable } from "./RecentFeedbackTable"; export { HighestNoShowHostTable } from "./HighestNoShowHostTable"; export { HighestRatedMembersTable } from "./HighestRatedMembersTable"; export { LowestRatedMembersTable } from "./LowestRatedMembersTable"; -export { RoutingFormResponsesTable } from "./RoutingFormResponsesTable"; +export { RoutingFormResponsesTable, type RoutingFormTableType } from "./RoutingFormResponsesTable"; export { FailedBookingsByField } from "./FailedBookingsByField"; diff --git a/packages/features/insights/components/tremor/BarList.tsx b/packages/features/insights/components/tremor/BarList.tsx new file mode 100644 index 0000000000..ba50273b8c --- /dev/null +++ b/packages/features/insights/components/tremor/BarList.tsx @@ -0,0 +1,165 @@ +// Tremor BarList [v0.1.1] +import React from "react"; + +import { classNames } from "@calcom/lib"; + +type Bar = T & { + key?: string; + href?: string; + value: number; + name: string; +}; + +const focusRing = [ + // base + "outline outline-offset-2 outline-0 focus-visible:outline-2", + // outline color + "outline-brand", +]; + +interface BarListProps extends React.HTMLAttributes { + data: Bar[]; + valueFormatter?: (value: number) => string; + showAnimation?: boolean; + onValueChange?: (payload: Bar) => void; + sortOrder?: "ascending" | "descending" | "none"; +} + +function BarListInner( + { + data = [], + valueFormatter = (value) => value.toString(), + showAnimation = false, + onValueChange, + sortOrder = "descending", + className, + ...props + }: BarListProps, + forwardedRef: React.ForwardedRef +) { + const Component = onValueChange ? "button" : "div"; + const sortedData = React.useMemo(() => { + if (sortOrder === "none") { + return data; + } + return [...data].sort((a, b) => { + return sortOrder === "ascending" ? a.value - b.value : b.value - a.value; + }); + }, [data, sortOrder]); + + const widths = React.useMemo(() => { + const maxValue = Math.max(...sortedData.map((item) => item.value), 0); + return sortedData.map((item) => (item.value === 0 ? 0 : Math.max((item.value / maxValue) * 100, 2))); + }, [sortedData]); + + const rowHeight = "h-8"; + + return ( +
+
+ {sortedData.map((item, index) => ( + { + onValueChange?.(item); + }} + className={classNames( + // base + "group w-full rounded", + // focus + focusRing, + onValueChange + ? [ + "!-m-0 cursor-pointer", + // hover + "hover:bg-gray-50 hover:dark:bg-gray-900", + ] + : "" + )}> +
+
+ {item.href ? ( + event.stopPropagation()}> + {item.name} + + ) : ( +

+ {item.name} +

+ )} +
+
+
+ ))} +
+
+ {sortedData.map((item, index) => ( +
+

+ {valueFormatter(item.value)} +

+
+ ))} +
+
+ ); +} + +BarListInner.displayName = "BarList"; + +const BarList = React.forwardRef(BarListInner) as ( + p: BarListProps & { ref?: React.ForwardedRef } +) => ReturnType; + +export { BarList, type BarListProps }; diff --git a/packages/features/insights/filters/FilterType.tsx b/packages/features/insights/filters/FilterType.tsx index 206add99f4..e01a9b3086 100644 --- a/packages/features/insights/filters/FilterType.tsx +++ b/packages/features/insights/filters/FilterType.tsx @@ -108,9 +108,9 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
- + {filterOptions?.map((option) => ( - + -
- {t(option.label)} - {selectedFilter?.includes(option.value) && } +
+ {t(option.label)} + {selectedFilter?.includes(option.value) && ( + + )}
diff --git a/packages/features/insights/filters/index.tsx b/packages/features/insights/filters/index.tsx index 62b0d8ed40..3cacac91b4 100644 --- a/packages/features/insights/filters/index.tsx +++ b/packages/features/insights/filters/index.tsx @@ -1,6 +1,7 @@ +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 { Button, Icon, Tooltip, DataTableFilters } from "@calcom/ui"; import { BookingStatusFilter } from "./BookingStatusFilter"; import { DateSelect } from "./DateSelect"; @@ -37,7 +38,13 @@ const ClearFilters = () => { ); }; -export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: boolean }) => { +export const Filters = ({ + showRoutingFilters = false, + routingTable = null, +}: { + showRoutingFilters?: boolean; + routingTable?: RoutingFormTableType | null; +}) => { const { filter } = useFilterContext(); const { selectedFilter } = filter; @@ -98,6 +105,8 @@ export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: b
{showRoutingFilters ? : } + + {routingTable && }
); diff --git a/packages/ui/components/data-table/filters/index.tsx b/packages/ui/components/data-table/filters/index.tsx index 7aedf9d60d..5a386b8276 100644 --- a/packages/ui/components/data-table/filters/index.tsx +++ b/packages/ui/components/data-table/filters/index.tsx @@ -41,6 +41,9 @@ function ColumnVisibilityButtonComponent( 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 ( @@ -59,15 +62,28 @@ function ColumnVisibilityButtonComponent( const canHide = column.getCanHide(); if (!column.columnDef.header || typeof column.columnDef.header !== "string" || !canHide) return null; - const isVisible = column.getIsVisible(); + const isVisible = visibleColumns.has(column.id); return ( - column.toggleVisibility(!isVisible)}> + { + 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}
@@ -78,7 +94,10 @@ function ColumnVisibilityButtonComponent( allColumns.forEach((column) => column.toggleVisibility(true))} + onSelect={() => { + 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" }) diff --git a/packages/ui/components/dropdown/Dropdown.tsx b/packages/ui/components/dropdown/Dropdown.tsx index 0cf6aa4c63..b973b8bf25 100644 --- a/packages/ui/components/dropdown/Dropdown.tsx +++ b/packages/ui/components/dropdown/Dropdown.tsx @@ -39,7 +39,7 @@ export const DropdownMenuContent = forwardRef*:first-child]:mt-1 [&>*:last-child]:mb-1", props.className )} diff --git a/packages/ui/components/hover-card/index.tsx b/packages/ui/components/hover-card/index.tsx new file mode 100644 index 0000000000..6e9594016b --- /dev/null +++ b/packages/ui/components/hover-card/index.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; +import * as React from "react"; + +import { classNames } from "@calcom/lib"; + +const HoverCard = HoverCardPrimitive.Root; + +const HoverCardTrigger = HoverCardPrimitive.Trigger; + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)); +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/packages/ui/index.ts b/packages/ui/index.ts index 8c7c2ece4a..39ee5512aa 100644 --- a/packages/ui/index.ts +++ b/packages/ui/index.ts @@ -170,6 +170,8 @@ export { CommandShortcut, } from "./components/command"; +export { HoverCard, HoverCardTrigger, HoverCardContent } from "./components/hover-card"; + export { OrgBanner } from "./components/organization-banner"; export type { OrgBannerProps } from "./components/organization-banner";