From cefcb1eaf8e05ccad5239c6ec8bca595bff98ece Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:35:01 +0000 Subject: [PATCH] Feature/booking filters (#6099) * EventTypeFilter UI working * Tidy + fix avatar + fix overflow * Component store * WIP - switchin to query filtering instead of state * Push working * Checkbox approach - weird conditional toggle for all bookings * WIP * the master query hook :O * useTypedQuery * Working typed query * Typed query hook * Add optional keys * safeParse query params * Add use callback * Fix re-renders * Remove local version of local query and use util branch * Working team filters! * working people filter! * Tidy up * NIT Co-authored-by: zomars --- .../components/booking/BookingListItem.tsx | 2 +- apps/web/pages/bookings/[status].tsx | 19 +++-- apps/web/public/static/locales/en/common.json | 3 + .../bookings/components/EventTypeFilter.tsx | 77 +++++++++++++++++++ .../bookings/components/FiltersContainer.tsx | 43 +++++++++++ .../bookings/components/PeopleFilter.tsx | 74 ++++++++++++++++++ .../bookings/components/TeamFilter.tsx | 75 ++++++++++++++++++ packages/features/bookings/groupBy.ts | 18 +++++ .../bookings/layout}/BookingLayout.tsx | 22 ++---- .../features/bookings/lib/useFilterQuery.tsx | 15 ++++ packages/lib/hooks/useTypedQuery.ts | 6 +- .../trpc/server/routers/viewer/bookings.tsx | 52 ++++++++++++- .../trpc/server/routers/viewer/eventTypes.tsx | 29 +++++++ packages/trpc/server/routers/viewer/teams.tsx | 41 ++++++++++ packages/trpc/server/trpc.ts | 1 + packages/ui/components/avatar/Avatar.tsx | 54 +++++++------ packages/ui/components/index.ts | 1 + packages/ui/components/popover/index.tsx | 72 +++++++++++++++++ packages/ui/index.tsx | 2 +- packages/ui/package.json | 1 + 20 files changed, 556 insertions(+), 51 deletions(-) create mode 100644 packages/features/bookings/components/EventTypeFilter.tsx create mode 100644 packages/features/bookings/components/FiltersContainer.tsx create mode 100644 packages/features/bookings/components/PeopleFilter.tsx create mode 100644 packages/features/bookings/components/TeamFilter.tsx create mode 100644 packages/features/bookings/groupBy.ts rename packages/{ui/v2/core/layouts => features/bookings/layout}/BookingLayout.tsx (58%) create mode 100644 packages/features/bookings/lib/useFilterQuery.tsx create mode 100644 packages/ui/components/popover/index.tsx diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 0dc3a59d05..32a972429b 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -30,7 +30,7 @@ import useMeQuery from "@lib/hooks/useMeQuery"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; -type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"]; +type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"]; type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number]; diff --git a/apps/web/pages/bookings/[status].tsx b/apps/web/pages/bookings/[status].tsx index 388c33b9d8..59eddf97e9 100644 --- a/apps/web/pages/bookings/[status].tsx +++ b/apps/web/pages/bookings/[status].tsx @@ -5,9 +5,11 @@ import { Fragment } from "react"; import { z } from "zod"; import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components"; +import BookingLayout from "@calcom/features/bookings/layout/BookingLayout"; +import { filterQuerySchema, useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react"; -import { Alert, BookingLayout, Button, EmptyScreen, Icon } from "@calcom/ui"; +import { RouterOutputs, trpc } from "@calcom/trpc/react"; +import { Alert, Button, EmptyScreen, Icon } from "@calcom/ui"; import { useInViewObserver } from "@lib/hooks/useInViewObserver"; @@ -16,7 +18,7 @@ import SkeletonLoader from "@components/booking/SkeletonLoader"; import { ssgInit } from "@server/lib/ssg"; -type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"]; +type BookingListingStatus = z.infer["status"]; type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0]; type RecurringInfo = { @@ -41,12 +43,19 @@ const querySchema = z.object({ }); export default function Bookings() { + const { data: filterQuery } = useFilterQuery(); const router = useRouter(); const { status } = router.isReady ? querySchema.parse(router.query) : { status: "upcoming" as const }; const { t } = useLocale(); const query = trpc.viewer.bookings.get.useInfiniteQuery( - { status, limit: 10 }, + { + limit: 10, + filters: { + ...filterQuery, + status: filterQuery.status ?? status, + }, + }, { // first render has status `undefined` enabled: router.isReady, @@ -165,7 +174,7 @@ export default function Bookings() { )} {query.status === "success" && isEmpty && ( -
+
; + +export const EventTypeFilter = () => { + const { t } = useLocale(); + const { data: user } = useSession(); + const eventTypes = trpc.viewer.eventTypes.listWithTeam.useQuery(); + const [groupedEventTypes, setGroupedEventTypes] = useState(); + // Will be handled up the tree to redirect + useEffect(() => { + if (!eventTypes.data) return; + // Group event types by team + const grouped = groupBy( + eventTypes.data.filter((el) => el.team), + (item) => item?.team?.name || "" + ); // Add the team name + const individualEvents = eventTypes.data.filter((el) => !el.team); + // push indivdual events to the start of grouped array + setGroupedEventTypes({ user_own_event_types: individualEvents, ...grouped }); + }, [eventTypes.data, user]); + + if (!user) return null; + + return ( + +
+ {groupedEventTypes && + Object.keys(groupedEventTypes).map((teamName) => ( + +
+ {teamName === "user_own_event_types" ? t("individual") : teamName} +
+ {groupedEventTypes[teamName].map((eventType) => ( + +
+

+ {eventType.title} +

+
+ +
+
+
+ ))} +
+ ))} +
+
+ ); +}; diff --git a/packages/features/bookings/components/FiltersContainer.tsx b/packages/features/bookings/components/FiltersContainer.tsx new file mode 100644 index 0000000000..1d30b1bf7f --- /dev/null +++ b/packages/features/bookings/components/FiltersContainer.tsx @@ -0,0 +1,43 @@ +import { Fragment, ReactNode } from "react"; + +import { PeopleFilter } from "./PeopleFilter"; +import { TeamsMemberFilter } from "./TeamFilter"; + +type FilterTypes = "teams" | "people"; + +type Filter = { + name: FilterTypes; + controllingQueryParams?: string[]; // this is what the filter controls - but also we show the filter if any of these query params are present + component: ReactNode; + showByDefault?: boolean; +}; + +const filters: Filter[] = [ + { + name: "teams", + component: , + controllingQueryParams: ["teamId"], + showByDefault: true, + }, + { + name: "people", + component: , + controllingQueryParams: ["usersId"], + showByDefault: true, + }, +]; + +export function FiltersContainer() { + return ( +
+ {filters.map((filter) => { + if (!filter.showByDefault) { + // TODO: check if any of the controllingQueryParams are present in the query params and show the filter if so + // TODO: Also check state to see if the user has toggled the filter + return null; + } + return {filter.component}; + })} +
+ ); +} diff --git a/packages/features/bookings/components/PeopleFilter.tsx b/packages/features/bookings/components/PeopleFilter.tsx new file mode 100644 index 0000000000..91ec050db2 --- /dev/null +++ b/packages/features/bookings/components/PeopleFilter.tsx @@ -0,0 +1,74 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { AnimatedPopover, Avatar, Icon } from "@calcom/ui"; + +import { useFilterQuery } from "../lib/useFilterQuery"; + +export const PeopleFilter = () => { + const { t } = useLocale(); + const { data: query, pushItemToKey, removeItemByKeyAndValue, removeByKey } = useFilterQuery(); + const { data } = trpc.viewer.teams.listMembers.useQuery({}); + + if (!data || !data.length) return null; + + // Get user names from query + const userNames = data?.filter((user) => query.userIds?.includes(user.id)).map((user) => user.name); + + return ( + 0 ? `${userNames.join(", ")}` : t("all_users_filter_label")}> +
+
+ +
+ + + { + // Always clear userIds on toggle as this is the toggle box for all users. No params means we are currently selecting all users + removeByKey("userIds"); + }} + className="text-primary-600 focus:ring-primary-500 inline-flex h-4 w-4 place-self-center justify-self-end rounded border-gray-300 " + /> +
+ {data && + data.map((user) => ( +
+ + + + { + if (e.target.checked) { + pushItemToKey("userIds", user.id); + } else if (!e.target.checked) { + removeItemByKeyAndValue("userIds", user.id); + } + }} + className="text-primary-600 focus:ring-primary-500 inline-flex h-4 w-4 place-self-center justify-self-end rounded border-gray-300 " + /> +
+ ))} +
+ ); +}; diff --git a/packages/features/bookings/components/TeamFilter.tsx b/packages/features/bookings/components/TeamFilter.tsx new file mode 100644 index 0000000000..61804ec952 --- /dev/null +++ b/packages/features/bookings/components/TeamFilter.tsx @@ -0,0 +1,75 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { AnimatedPopover, Avatar, Icon } from "@calcom/ui"; + +import { useFilterQuery } from "../lib/useFilterQuery"; + +export const TeamsMemberFilter = () => { + const { t } = useLocale(); + const { data: query, pushItemToKey, removeItemByKeyAndValue, removeByKey } = useFilterQuery(); + const { data } = trpc.viewer.teams.list.useQuery(); + + if (!data || !data.length) return null; + + // get team names from query + const teamNames = data?.filter((team) => query.teamIds?.includes(team.id)).map((team) => team.name); + + return ( + 0 ? `${teamNames.join(", ")}` : t("all_bookings_filter_label")}> +
+
+ +
+ + + { + removeByKey("teamIds"); // Always clear on toggle or not toggle (seems weird but when you know the behviour it works well ) + }} + className="text-primary-600 focus:ring-primary-500 inline-flex h-4 w-4 place-self-center justify-self-end rounded border-gray-300 " + /> +
+ {data && + data.map((team) => ( +
+ + + + { + if (e.target.checked) { + pushItemToKey("teamIds", team.id); + } else if (!e.target.checked) { + removeItemByKeyAndValue("teamIds", team.id); + } + }} + className="text-primary-600 focus:ring-primary-500 inline-flex h-4 w-4 place-self-center justify-self-end rounded border-gray-300 " + /> +
+ ))} +
+ ); +}; diff --git a/packages/features/bookings/groupBy.ts b/packages/features/bookings/groupBy.ts new file mode 100644 index 0000000000..c0d337af29 --- /dev/null +++ b/packages/features/bookings/groupBy.ts @@ -0,0 +1,18 @@ +type KeySelector = (item: T) => string; + +export function groupBy(array: Iterable, keySelector: KeySelector): Record { + return Array.from(array).reduce( + (acc: Record, item: T) => { + const key = keySelector(item); + if (key in acc) { + // found key, push new item into existing array + acc[key].push(item); + } else { + // did not find key, create new array + acc[key] = [item]; + } + return acc; + }, + {} // start with empty object + ); +} diff --git a/packages/ui/v2/core/layouts/BookingLayout.tsx b/packages/features/bookings/layout/BookingLayout.tsx similarity index 58% rename from packages/ui/v2/core/layouts/BookingLayout.tsx rename to packages/features/bookings/layout/BookingLayout.tsx index bf7eecd3fc..0e337c0af9 100644 --- a/packages/ui/v2/core/layouts/BookingLayout.tsx +++ b/packages/features/bookings/layout/BookingLayout.tsx @@ -1,36 +1,30 @@ import React, { ComponentProps } from "react"; -import type { VerticalTabItemProps } from "../"; -import { HorizontalTabs, VerticalTabs } from "../"; -import { Icon } from "../../../Icon"; -import Shell from "../Shell"; -import type { HorizontalTabItemProps } from "../navigation/tabs/HorizontalTabItem"; +import { HorizontalTabs, Shell } from "@calcom/ui"; +import { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui/v2"; + +import { FiltersContainer } from "../components/FiltersContainer"; const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [ { name: "upcoming", href: "/bookings/upcoming", - icon: Icon.FiCalendar, }, { name: "unconfirmed", href: "/bookings/unconfirmed", - icon: Icon.FiInbox, }, { name: "recurring", href: "/bookings/recurring", - icon: Icon.FiRotateCcw, }, { name: "past", href: "/bookings/past", - icon: Icon.FiSunset, }, { name: "cancelled", href: "/bookings/cancelled", - icon: Icon.FiSlash, }, ]; @@ -40,12 +34,10 @@ export default function BookingLayout({ }: { children: React.ReactNode } & ComponentProps) { return ( -
-
- -
-
+
+
+
{children}
diff --git a/packages/features/bookings/lib/useFilterQuery.tsx b/packages/features/bookings/lib/useFilterQuery.tsx new file mode 100644 index 0000000000..457c86e51f --- /dev/null +++ b/packages/features/bookings/lib/useFilterQuery.tsx @@ -0,0 +1,15 @@ +import z from "zod"; + +import { queryNumberArray, useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; + +// TODO: Move this to zod utils +export const filterQuerySchema = z.object({ + teamIds: queryNumberArray.optional(), + userIds: queryNumberArray.optional(), + status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]), + eventTypeIds: queryNumberArray.optional(), +}); + +export function useFilterQuery() { + return useTypedQuery(filterQuerySchema); +} diff --git a/packages/lib/hooks/useTypedQuery.ts b/packages/lib/hooks/useTypedQuery.ts index 97246781fc..47f5d38e72 100644 --- a/packages/lib/hooks/useTypedQuery.ts +++ b/packages/lib/hooks/useTypedQuery.ts @@ -24,7 +24,7 @@ export const queryStringArray = z .preprocess((a) => z.string().parse(a).split(","), z.string().array()) .or(z.string().array()); -export function useTypedQuery(schema: T) { +export function useTypedQuery(schema: T) { type Output = z.infer; type FullOutput = Required; type OutputKeys = Required; @@ -65,8 +65,10 @@ export function useTypedQuery(schema: T) { const existingValue = parsedQuery[key]; if (Array.isArray(existingValue)) { if (existingValue.includes(value)) return; // prevent adding the same value to the array + // @ts-expect-error this is too much for TS it seems setQuery(key, [...existingValue, value]); } else { + // @ts-expect-error this is too much for TS it seems setQuery(key, [value]); } } @@ -74,7 +76,7 @@ export function useTypedQuery(schema: T) { // Remove item by key and value function removeItemByKeyAndValue(key: J, value: ArrayOutput[J][number]) { const existingValue = parsedQuery[key]; - if (Array.isArray(existingValue)) { + if (Array.isArray(existingValue) && existingValue.length > 1) { // @ts-expect-error this is too much for TS it seems const newValue = existingValue.filter((item) => item !== value); setQuery(key, newValue); diff --git a/packages/trpc/server/routers/viewer/bookings.tsx b/packages/trpc/server/routers/viewer/bookings.tsx index 42ba8a63d3..dbf9c6b16a 100644 --- a/packages/trpc/server/routers/viewer/bookings.tsx +++ b/packages/trpc/server/routers/viewer/bookings.tsx @@ -103,7 +103,12 @@ export const bookingsRouter = router({ get: authedProcedure .input( z.object({ - status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]), + filters: z.object({ + teamIds: z.number().array().optional(), + userIds: z.number().array().optional(), + status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]), + eventTypeIds: z.number().array().optional(), + }), limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type }) @@ -114,7 +119,7 @@ export const bookingsRouter = router({ const take = input.limit ?? 10; const skip = input.cursor ?? 0; const { prisma, user } = ctx; - const bookingListingByStatus = input.status; + const bookingListingByStatus = input.filters.status; const bookingListingFilters: Record = { upcoming: { endTime: { gte: new Date() }, @@ -175,7 +180,46 @@ export const bookingsRouter = router({ cancelled: { startTime: "desc" }, unconfirmed: { startTime: "asc" }, }; - const passedBookingsFilter = bookingListingFilters[bookingListingByStatus]; + + // TODO: Fix record typing + const bookingWhereInputFilters: Record = { + teamIds: { + AND: [ + { + eventType: { + team: { + id: { + in: input.filters?.teamIds, + }, + }, + }, + }, + ], + }, + userIds: { + AND: [ + { + eventType: { + users: { + some: { + id: { + in: input.filters?.userIds, + }, + }, + }, + }, + }, + ], + }, + }; + + const filtersCombined: Prisma.BookingWhereInput[] = + input.filters && + Object.keys(input.filters).map((key) => { + return bookingWhereInputFilters[key]; + }); + + const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus]; const orderBy = bookingListingOrderby[bookingListingByStatus]; const bookingsQuery = await prisma.booking.findMany({ @@ -204,7 +248,7 @@ export const bookingsRouter = router({ }, }, ], - AND: [passedBookingsFilter], + AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])], }, select: { ...bookingMinimalSelect, diff --git a/packages/trpc/server/routers/viewer/eventTypes.tsx b/packages/trpc/server/routers/viewer/eventTypes.tsx index 9f83d62e60..5bf4169b91 100644 --- a/packages/trpc/server/routers/viewer/eventTypes.tsx +++ b/packages/trpc/server/routers/viewer/eventTypes.tsx @@ -356,6 +356,35 @@ export const eventTypesRouter = router({ }, }); }), + listWithTeam: authedProcedure.query(async ({ ctx }) => { + return await ctx.prisma.eventType.findMany({ + where: { + OR: [ + { userId: ctx.user.id }, + { + team: { + members: { + some: { + userId: ctx.user.id, + }, + }, + }, + }, + ], + }, + select: { + id: true, + team: { + select: { + id: true, + name: true, + }, + }, + title: true, + slug: true, + }, + }); + }), create: authedProcedure.input(createEventTypeInput).mutation(async ({ ctx, input }) => { const { schedulingType, teamId, ...rest } = input; const userId = ctx.user.id; diff --git a/packages/trpc/server/routers/viewer/teams.tsx b/packages/trpc/server/routers/viewer/teams.tsx index 81be573198..223e1845c3 100644 --- a/packages/trpc/server/routers/viewer/teams.tsx +++ b/packages/trpc/server/routers/viewer/teams.tsx @@ -642,4 +642,45 @@ export const viewerTeamsRouter = router({ }); return teams; }), + listMembers: authedProcedure + .input( + z.object({ + teamIds: z.number().array().optional(), + }) + ) + .query(async ({ ctx, input }) => { + const teams = await ctx.prisma.team.findMany({ + where: { + id: { + in: input.teamIds, + }, + members: { + some: { + user: { + id: ctx.user.id, + }, + }, + }, + }, + select: { + members: { + select: { + user: { + select: { + id: true, + name: true, + avatar: true, + }, + }, + }, + }, + }, + }); + type UserMap = Record; + // flattern users to be unique by id + const users = teams + .flatMap((t) => t.members) + .reduce((acc, m) => (m.user.id in acc ? acc : { ...acc, [m.user.id]: m.user }), {} as UserMap); + return Object.values(users); + }), }); diff --git a/packages/trpc/server/trpc.ts b/packages/trpc/server/trpc.ts index cd2317c8b1..8d6352dec2 100644 --- a/packages/trpc/server/trpc.ts +++ b/packages/trpc/server/trpc.ts @@ -1,4 +1,5 @@ import superjson from "superjson"; +import { ZodError } from "zod"; import rateLimit from "@calcom/lib/rateLimit"; diff --git a/packages/ui/components/avatar/Avatar.tsx b/packages/ui/components/avatar/Avatar.tsx index a94d974dbf..4fddcc1a96 100644 --- a/packages/ui/components/avatar/Avatar.tsx +++ b/packages/ui/components/avatar/Avatar.tsx @@ -9,48 +9,56 @@ import { Maybe } from "@trpc/server"; export type AvatarProps = { className?: string; - size: "sm" | "md" | "mdLg" | "lg"; + size: "xs" | "sm" | "md" | "mdLg" | "lg"; imageSrc?: Maybe; title?: string; alt: string; gravatarFallbackMd5?: string; + fallback?: React.ReactNode; accepted?: boolean; + asChild?: boolean; // Added to ignore the outer span on the fallback component - messes up styling }; const sizesPropsBySize = { - sm: "w-6", // 24px - md: "w-8", // 32px - mdLg: "w-10", //40px - lg: "w-16", // 64px + xs: "w-4 h-4", // 16px + sm: "w-6 h-6", // 24px + md: "w-8 h-8", // 32px + mdLg: "w-10 h-10", //40px + lg: "w-16 h-16", // 64px } as const; export function Avatar(props: AvatarProps) { const { imageSrc, gravatarFallbackMd5, size, alt, title } = props; const sizeClassname = sizesPropsBySize[size]; - const rootClass = classNames("rounded-full aspect-square", sizeClassname, "h-auto"); + const rootClass = classNames("rounded-full aspect-square ", sizeClassname); const avatar = ( - - - {gravatarFallbackMd5 && ( - {alt} - )} - - {props.accepted && ( -
-
- {size === "lg" && } + <> + + + <> + {props.fallback && !gravatarFallbackMd5 && props.fallback} + {gravatarFallbackMd5 && ( + {alt} + )} + + + {props.accepted && ( +
+
+ {size === "lg" && } +
-
- )} + )} + ); diff --git a/packages/ui/components/index.ts b/packages/ui/components/index.ts index bb1c224de7..8f4052d423 100644 --- a/packages/ui/components/index.ts +++ b/packages/ui/components/index.ts @@ -29,5 +29,6 @@ export { } from "./form"; export { TopBanner } from "./top-banner"; export type { TopBannerProps } from "./top-banner"; +export { AnimatedPopover } from "./popover/index"; export { TableActions, DropdownActions } from "./table/TableActions"; export type { ActionType } from "./table/TableActions"; diff --git a/packages/ui/components/popover/index.tsx b/packages/ui/components/popover/index.tsx new file mode 100644 index 0000000000..0d5f4762d0 --- /dev/null +++ b/packages/ui/components/popover/index.tsx @@ -0,0 +1,72 @@ +import * as Popover from "@radix-ui/react-popover"; +import React from "react"; + +import { classNames } from "@calcom/lib"; +import { Icon } from "@calcom/ui"; + +export const AnimatedPopover = ({ + text, + count, + children, +}: { + text: string; + count?: number; + children: React.ReactNode; +}) => { + const [open, setOpen] = React.useState(false); + const ref = React.useRef(null); + // calculate which aligment to open the popover with based on which half of the screen it is on (left or right) + const [align, setAlign] = React.useState<"start" | "end">("start"); + React.useEffect(() => { + const handleResize = () => { + const halfWidth = window.innerWidth / 2; + const { x, y } = ref?.current?.getBoundingClientRect() || { + x: 0, + y: 0, + }; + if (x > halfWidth) { + setAlign("end"); + } else { + setAlign("start"); + } + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [setAlign]); + + return ( + + +
+
+
+ {text} + {count && count > 0 && ( +
{count}
+ )} +
+ +
+
+
+ +
+ {children} +
+
+
+ ); +}; diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index ad2e6d6861..67d1e67e47 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -24,6 +24,7 @@ export { TextAreaField, TextField, TopBanner, + AnimatedPopover, Select, SelectField, SelectWithValidation, @@ -100,7 +101,6 @@ export { ToggleGroup } from "./v2/core/form/ToggleGroup"; export { default as ImageUploader } from "./v2/core/ImageUploader"; export { default as AdminLayout, getLayout as getAdminLayout } from "./v2/core/layouts/AdminLayout"; export { default as AppsLayout } from "./v2/core/layouts/AppsLayout"; -export { default as BookingLayout } from "./v2/core/layouts/BookingLayout"; export { default as InstalledAppsLayout } from "./v2/core/layouts/InstalledAppsLayout"; export { default as SettingsLayout, getLayout as getSettingsLayout } from "./v2/core/layouts/SettingsLayout"; export { default as WizardLayout, getLayout as getWizardLayout } from "./v2/core/layouts/WizardLayout"; diff --git a/packages/ui/package.json b/packages/ui/package.json index 857821b0be..f78f64801c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -17,6 +17,7 @@ "@calcom/trpc": "*", "@formkit/auto-animate": "^1.0.0-beta.5", "@radix-ui/react-dialog": "^1.0.0", + "@radix-ui/react-popover": "^1.0.2", "@radix-ui/react-portal": "^1.0.0", "@radix-ui/react-select": "^0.1.1", "@tanstack/react-query": "^4.3.9",