Compare commits

...
14 Commits
Author SHA1 Message Date
zomars 38d242add9 v2.4.2
release.yml / release (push) Canceled after 0s
2022-12-22 17:12:32 -07:00
zomars c272e281cf Submodule sync 2022-12-22 17:12:13 -07:00
zomars d4e19b0fa4 Revert "Matches postgres image across projects"
This reverts commit c12877a58f.
2022-12-22 17:11:31 -07:00
zomars c12877a58f Matches postgres image across projects 2022-12-22 16:52:44 -07:00
af83350d04 replaced webhook icon with actual webhook logo (#6161)
* replaced webhook icon with actual webhook logo

* Apply suggestions from code review

* Linting

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-22 12:57:44 -07:00
sean-brydonandPeer Richelsen 9d0cf6be0b No more modules folder 🔥 (#6158)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-22 19:06:26 +00:00
zomars 2fe9c782d4 Lowercase usename checks again. 2022-12-22 11:09:31 -07:00
zomars d0a3d8d144 Link fixes
commit 56c1608deaf1921a80dc71bcbd107e772dd656f4
Author: Omar López <zomars@me.com>
Date:   Wed Dec 21 19:26:04 2022 -0700

    Update link.ts

commit eab43d833515cabc7acf2f973c2d2837ac1a0332
Author: zomars <zomars@me.com>
Date:   Wed Dec 21 19:03:02 2022 -0700

    Prevents 500 on TRPC error

commit 3a6970d5e5fab54136b379e1b2ccd7acf515a809
Author: zomars <zomars@me.com>
Date:   Wed Dec 21 18:15:26 2022 -0700

    Encoding fixes
2022-12-22 10:52:50 -07:00
sean-brydonandzomars cefcb1eaf8 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 <zomars@me.com>
2022-12-22 12:35:01 +00:00
sean-brydonandPeer Richelsen 83b60de640 Chor/move table actions (#6157)
* Move table actions

* Move table actions

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-22 12:41:43 +01:00
Hariom BalharaandAlex van Andel a076418989 Hotfix/Event Type Save Error with Multiple Durations (#6154)
* Fix .length being undefined when form is submitted without any change to duration

* Fix type error

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-22 11:13:33 +00:00
zomars 64ecce1ccb Submodule sync 2022-12-21 17:49:10 -07:00
zomars cd56a5d4f0 Removes deprecated rejectOnNotFound 2022-12-21 17:48:15 -07:00
zomars d5ca121ac6 Revert "team banner (#5956)"
This reverts commit 463787b607.
2022-12-21 17:39:07 -07:00
59 changed files with 733 additions and 228 deletions
@@ -23,14 +23,14 @@ import {
TextArea,
Tooltip,
} from "@calcom/ui";
import { ActionType, TableActions } from "@calcom/ui";
import useMeQuery from "@lib/hooks/useMeQuery";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions";
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
@@ -17,6 +17,7 @@ import {
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import DatePicker from "@calcom/features/calendars/DatePicker";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import getStripeAppData from "@calcom/lib/getStripeAppData";
@@ -27,7 +28,7 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc/react";
import { Icon, DatePicker } from "@calcom/ui";
import { Icon } from "@calcom/ui";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -6,13 +6,13 @@ import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Checkbox,
CustomInputItem,
Dialog,
DialogClose,
DialogContent,
@@ -290,7 +290,7 @@ export const EventSetupTab = (
setDefaultDuration(
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
);
formMethods.setValue("length", Number(option?.value));
if (option) formMethods.setValue("length", option.value);
}}
/>
</div>
@@ -298,14 +298,11 @@ export const EventSetupTab = (
) : (
<TextField
required
name="length"
type="number"
label={t("duration")}
addOnSuffix={<>{t("minutes")}</>}
defaultValue={eventType.length ?? 15}
onChange={(e) => {
formMethods.setValue("length", Number(e.target.value));
}}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
@@ -3,9 +3,10 @@ import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useMemo } from "react";
import { Controller, useFormContext } from "react-hook-form";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar, Button, CheckedTeamSelect, Icon, Label, Select } from "@calcom/ui";
import { Avatar, Button, Icon, Label, Select } from "@calcom/ui";
interface IMemberToValue {
id: number | null;
@@ -1,5 +1,6 @@
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { TbWebhook } from "react-icons/tb";
import { WebhookForm } from "@calcom/features/webhooks/components";
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
@@ -117,7 +118,7 @@ export const EventTeamWebhooksTab = ({
</>
) : (
<EmptyScreen
Icon={Icon.FiLink}
Icon={TbWebhook}
headline={t("create_your_first_webhook")}
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
buttonRaw={<NewWebhookButton />}
@@ -3,6 +3,7 @@ import { useRouter } from "next/router";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useMemo, useState } from "react";
import { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -159,7 +160,7 @@ function EventTypeSingleLayout({
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
icon: Icon.FiLink,
icon: TbWebhook,
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.4.1",
"version": "2.4.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+2 -1
View File
@@ -12,6 +12,7 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import CustomBranding from "@calcom/lib/CustomBranding";
import defaultEvents, {
@@ -26,7 +27,7 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { BadgeCheckIcon, EventTypeDescriptionLazy as EventTypeDescription, Icon } from "@calcom/ui";
import { BadgeCheckIcon, Icon } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
+19 -11
View File
@@ -5,6 +5,7 @@ import { z } from "zod";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
@@ -27,7 +28,7 @@ const decryptedSchema = z.object({
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
const { action, token, reason } = querySchema.parse(req.query);
const { bookingUid, userId } = decryptedSchema.parse(
JSON.parse(symmetricDecrypt(token, process.env.CALENDSO_ENCRYPTION_KEY || ""))
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
);
/** We shape the session as required by tRPC router */
@@ -47,17 +48,24 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
where: { uid: bookingUid },
});
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = viewerRouter.createCaller(ctx);
await caller.bookings.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
});
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = viewerRouter.createCaller(ctx);
await caller.bookings.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = e.message;
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
return;
}
return res.redirect(`/booking/${bookingUid}`);
res.redirect(`/booking/${bookingUid}`);
}
export default defaultResponder(handler);
+2 -1
View File
@@ -7,12 +7,13 @@ import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Alert, Button, EmailField, Icon, PasswordField, SAMLLogin } from "@calcom/ui";
import { Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
import { ErrorCode, getSession } from "@lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
+14 -5
View File
@@ -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<typeof filterQuerySchema>["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 && (
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Icon.FiCalendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
@@ -204,6 +205,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
),
metadata,
},
resolver: zodResolver(
z
.object({
// Length if string, is converted to a number or it can be a number
// Make it optional because it's not submitted from all tabs of the page
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
})
// TODO: Add schema for other fields later.
.passthrough()
),
});
const appsMetadata = formMethods.getValues("metadata")?.apps;
+5 -5
View File
@@ -4,10 +4,12 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react";
import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
import {
CreateEventTypeButton,
EventTypeDescriptionLazy as EventTypeDescription,
} from "@calcom/features/eventtypes/components";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import isCalcom from "@calcom/lib/isCalcom";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Badge,
@@ -23,13 +25,11 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyScreen,
EventTypeDescription,
Icon,
Shell,
showToast,
Switch,
Tooltip,
TipBanner,
} from "@calcom/ui";
import { withQuery } from "@lib/QueryCell";
@@ -589,7 +589,6 @@ const EventTypesPage = () => {
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
{isCalcom && <TipBanner />}
{data.eventTypeGroups.map((group, index) => (
<Fragment key={group.profile.slug}>
{/* hide list heading when there is only one (current user) */}
@@ -608,6 +607,7 @@ const EventTypesPage = () => {
/>
</Fragment>
))}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
</>
+2 -1
View File
@@ -5,13 +5,14 @@ import { useRouter } from "next/router";
import { useEffect } from "react";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { Avatar, Button, EventTypeDescription, Icon } from "@calcom/ui";
import { Avatar, Button, Icon } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -1443,6 +1443,9 @@
"enter_option": "Enter Option {{index}}",
"add_an_option": "Add an option",
"radio": "Radio",
"individual":"Individual",
"all_bookings_filter_label":"All Bookings",
"all_users_filter_label":"All Users",
"meeting_url_workflow": "Meeting url",
"meeting_url_info": "The event meeting conference url",
"date_overrides": "Date overrides",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 KiB

@@ -7,7 +7,7 @@ import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => {
const seedData = { bookingUid: props.calEvent.uid, userId: props.calEvent.organizer.id };
const token = symmetricEncrypt(JSON.stringify(seedData), process.env.CALENDSO_ENCRYPTION_KEY || "");
const actionHref = `${WEBAPP_URL}/api/link/?token=${token}`;
const actionHref = `${WEBAPP_URL}/api/link/?token=${encodeURIComponent(token)}`;
return (
<OrganizerScheduledEmail
title={
@@ -7,8 +7,7 @@ import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
import { Button, Icon } from "../../..";
import { Button, Icon } from "@calcom/ui";
interface Props {
samlTenantID: string;
@@ -20,7 +19,7 @@ const schema = z.object({
email: z.string().email({ message: "Please enter a valid email" }),
});
export default function SAMLLogin({ samlTenantID, samlProductID, setErrorMessage }: Props) {
export function SAMLLogin({ samlTenantID, samlProductID, setErrorMessage }: Props) {
const { t } = useLocale();
const methods = useFormContext();
const telemetry = useTelemetry();
@@ -0,0 +1,77 @@
import { useSession } from "next-auth/react";
import { Fragment, useState, useEffect } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc, RouterOutputs } from "@calcom/trpc/react";
import { AnimatedPopover } from "@calcom/ui";
import { groupBy } from "../groupBy";
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
export type IEventTypeFilter = IEventTypesFilters[0];
type GroupedEventTypeState = Record<
string,
{
team: {
id: number;
name: string;
} | null;
id: number;
title: string;
slug: string;
}[]
>;
export const EventTypeFilter = () => {
const { t } = useLocale();
const { data: user } = useSession();
const eventTypes = trpc.viewer.eventTypes.listWithTeam.useQuery();
const [groupedEventTypes, setGroupedEventTypes] = useState<GroupedEventTypeState>();
// Will be handled up the tree to redirect
useEffect(() => {
if (!eventTypes.data) return;
// Group event types by team
const grouped = groupBy<IEventTypeFilter>(
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 (
<AnimatedPopover text={t("event_type")}>
<div className="">
{groupedEventTypes &&
Object.keys(groupedEventTypes).map((teamName) => (
<Fragment key={teamName}>
<div className="p-4 text-xs font-medium uppercase leading-none text-gray-500">
{teamName === "user_own_event_types" ? t("individual") : teamName}
</div>
{groupedEventTypes[teamName].map((eventType) => (
<Fragment key={eventType.id}>
<div className="item-center flex px-4 py-[6px]">
<p className="block self-center truncate text-sm font-medium text-gray-700">
{eventType.title}
</p>
<div className="ml-auto">
<input
type="checkbox"
name=""
id=""
className="text-primary-600 focus:ring-primary-500 mr-2 h-4 w-4 rounded border-gray-300 "
/>
</div>
</div>
</Fragment>
))}
</Fragment>
))}
</div>
</AnimatedPopover>
);
};
@@ -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: <TeamsMemberFilter />,
controllingQueryParams: ["teamId"],
showByDefault: true,
},
{
name: "people",
component: <PeopleFilter />,
controllingQueryParams: ["usersId"],
showByDefault: true,
},
];
export function FiltersContainer() {
return (
<div className="flex w-full space-x-2 overflow-x-scroll lg:justify-end ">
{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 <Fragment key={filter.name}>{filter.component}</Fragment>;
})}
</div>
);
}
@@ -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 (
<AnimatedPopover
text={userNames && userNames.length > 0 ? `${userNames.join(", ")}` : t("all_users_filter_label")}>
<div className="item-center flex px-4 py-[6px] focus-within:bg-gray-100">
<div className="mr-2 flex h-6 w-6 items-center justify-center">
<Icon.FiUser className="h-full w-full" />
</div>
<label htmlFor="allUsers" className="mr-auto self-center truncate text-sm font-medium text-gray-700">
{t("all_users_filter_label")}
</label>
<input
id="allUsers"
type="checkbox"
checked={!query.userIds}
onChange={() => {
// 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 "
/>
</div>
{data &&
data.map((user) => (
<div className="item-center flex px-4 py-[6px] focus-within:bg-gray-100" key={`${user.id}`}>
<Avatar
imageSrc={user.avatar}
size="sm"
alt={`${user.name} Avatar`}
gravatarFallbackMd5="fallback"
className="self-center"
asChild
/>
<label
htmlFor={user.name ?? "NamelessUser"}
className="ml-2 mr-auto self-center truncate text-sm font-medium text-gray-700">
{user.name}
</label>
<input
id={user.name ?? "NamelessUser"}
name={user.name ?? "NamelessUser"}
type="checkbox"
checked={query.userIds?.includes(user.id)}
onChange={(e) => {
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 "
/>
</div>
))}
</AnimatedPopover>
);
};
@@ -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 (
<AnimatedPopover
text={teamNames && teamNames.length > 0 ? `${teamNames.join(", ")}` : t("all_bookings_filter_label")}>
<div className="item-center flex px-4 py-[6px] focus-within:bg-gray-100">
<div className="mr-2 flex h-6 w-6 items-center justify-center">
<Icon.FiLayers className="h-full w-full" />
</div>
<label
htmlFor="allBookings"
className="mr-auto self-center truncate text-sm font-medium text-gray-700">
{t("all_bookings_filter_label")}
</label>
<input
id="allBookings"
type="checkbox"
checked={!query.teamIds}
onChange={() => {
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 "
/>
</div>
{data &&
data.map((team) => (
<div className="item-center flex px-4 py-[6px] focus-within:bg-gray-100" key={`${team.id}`}>
<Avatar
imageSrc={team.logo}
size="sm"
alt={`${team.name} Avatar`}
gravatarFallbackMd5="fallback"
className="self-center"
asChild
/>
<label
htmlFor={team.name}
className="ml-2 mr-auto self-center truncate text-sm font-medium text-gray-700">
{team.name}
</label>
<input
id={team.name}
name={team.name}
type="checkbox"
checked={query.teamIds?.includes(team.id)}
onChange={(e) => {
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 "
/>
</div>
))}
</AnimatedPopover>
);
};
+18
View File
@@ -0,0 +1,18 @@
type KeySelector<T> = (item: T) => string;
export function groupBy<T>(array: Iterable<T>, keySelector: KeySelector<T>): Record<string, T[]> {
return Array.from(array).reduce(
(acc: Record<string, T[]>, 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
);
}
@@ -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<typeof Shell>) {
return (
<Shell {...rest}>
<div className="flex flex-col sm:space-x-2 xl:flex-row">
<div className="hidden xl:block">
<VerticalTabs tabs={tabs} sticky />
</div>
<div className="block xl:hidden">
<div className="flex max-w-6xl flex-col sm:space-x-2">
<div className="flex flex-col lg:flex-row">
<HorizontalTabs tabs={tabs} />
<FiltersContainer />
</div>
<main className="w-full max-w-6xl">{children}</main>
</div>
@@ -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);
}
@@ -6,9 +6,7 @@ import classNames from "@calcom/lib/classNames";
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import { Button, Icon } from "@calcom/ui";
import { SkeletonText } from "../../..";
import { Button, Icon, SkeletonText } from "@calcom/ui";
export type DatePickerProps = {
/** which day of the week to render the calendar. Usually Sunday (=0) or Monday (=1) - default: Sunday */
@@ -3,8 +3,7 @@ import { Props } from "react-select";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar, Icon, Select } from "../../..";
import { Avatar, Icon, Select } from "@calcom/ui";
type CheckedSelectOption = {
avatar: string;
@@ -1,6 +1,5 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Button, ButtonGroup, Icon } from "../../..";
import { Badge, Button, ButtonGroup, Icon } from "@calcom/ui";
type Props = {
required?: boolean;
@@ -8,8 +8,7 @@ import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { baseEventTypeSelect } from "@calcom/prisma";
import { EventTypeModel } from "@calcom/prisma/zod";
import { Badge, Icon } from "../../..";
import { Badge, Icon } from "@calcom/ui";
export type EventTypeDescriptionProps = {
eventType: Pick<
@@ -2,4 +2,6 @@ import dynamic from "next/dynamic";
export { default as CheckedTeamSelect } from "./CheckedTeamSelect";
export { default as CustomInputItem } from "./CustomInputItem";
export { default as CreateEventTypeButton } from "./CreateEventTypeButton";
export { default as EventTypeDescription } from "./EventTypeDescription";
export const EventTypeDescriptionLazy = dynamic(() => import("./EventTypeDescription"));
@@ -13,11 +13,11 @@ import {
DialogHeader,
DialogClose,
Switch,
DatePicker,
Form,
Button,
} from "@calcom/ui";
import DatePicker from "../../calendars/DatePicker";
import { DayRanges, TimeRange } from "./Schedule";
const ALL_DAY_RANGE = {
@@ -1,12 +1,9 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useEffect, useState } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
import { Button, Icon } from "@calcom/ui";
import Card from "./Card";
import { Card } from "@calcom/ui";
export const tips = [
{
@@ -59,30 +56,20 @@ export const tips = [
},
];
function useLocalList<T extends { id: number }>(storageKey: string, initialValue: T[]) {
const [list, setList] = useState<T[]>(initialValue);
export default function Tips() {
const [animationRef] = useAutoAnimate<HTMLDivElement>();
/**
This use effect is used to filter out the items that have been removed from the list
we get this from local storage and then we reverse the list so that the newest items
are at the top. This is done so that the items are displayed in the order they were
added
**/
useEffect(() => {
const reversedList = initialValue.slice(0).reverse();
const removedListString = localStorage.getItem(storageKey) || "";
const removedListIds = removedListString.split(",").map((id) => parseInt(id, 10));
const filteredList = reversedList.filter((tip) => removedListIds.indexOf(tip.id) === -1);
setList(() => [...filteredList]);
}, [initialValue, storageKey]);
const { t } = useLocale();
const [list, setList] = useState<typeof tips>([]);
const handleRemoveItem = (id: number) => {
setList((currentItems) => {
const items = localStorage.getItem(storageKey) || "";
const items = localStorage.getItem("removedTipsIds") || "";
const itemToRemoveIndex = currentItems.findIndex((item) => item.id === id);
localStorage.setItem(
storageKey,
"removedTipsIds",
`${currentItems[itemToRemoveIndex].id.toString()}${items.length > 0 ? `,${items.split(",")}` : ""}`
);
currentItems.splice(itemToRemoveIndex, 1);
@@ -90,20 +77,14 @@ function useLocalList<T extends { id: number }>(storageKey: string, initialValue
});
};
return {
list,
handleRemoveItem,
};
}
export default function Tips() {
//const [animationRef] = useAutoAnimate<HTMLDivElement>();
const { t } = useLocale();
const { list, handleRemoveItem } = useLocalList<typeof tips[0]>("removedTipsIds", tips);
useEffect(() => {
const reversedTips = tips.slice(0).reverse();
const removedTipsString = localStorage.getItem("removedTipsIds") || "";
const removedTipsIds = removedTipsString.split(",").map((id) => parseInt(id, 10));
const filteredTips = reversedTips.filter((tip) => removedTipsIds.indexOf(tip.id) === -1);
setList(() => [...filteredTips]);
}, []);
const baseOriginalList = list.slice(0).reverse();
return (
<div
className="mb-4 hidden lg:grid"
@@ -143,59 +124,3 @@ export default function Tips() {
</div>
);
}
export function TipBanner() {
const { t } = useLocale();
const [animationRef] = useAutoAnimate<HTMLDivElement>();
const bannerTips = [
{
id: 1,
img: "/team-banner-background-small.jpg",
title: t("calcom_is_better_with_team"),
subtitle: t("add_your_team_members"),
href: `${WEBAPP_URL}/settings/teams/new`,
learnMoreHref: "https://go.cal.com/teams-video",
},
];
const { list, handleRemoveItem } = useLocalList<typeof bannerTips[0]>("removedBannerIds", bannerTips);
return (
<div>
{list.map((tip) => {
return (
<div
key={tip.id}
ref={animationRef}
className="relative -mt-6 mb-6 min-h-[186px] rounded-md"
style={{ backgroundImage: "url(" + tip.img + ")", backgroundSize: "cover" }}>
<div className="p-6 text-white">
<h1 className="font-cal text-lg">{tip.title}</h1>
<p className="my-4 max-w-xl text-sm">{tip.subtitle}</p>
<div className="space-x-2">
<Button className="bg-white" color="secondary" href={tip.href}>
{t("create_team")}
</Button>
{tip.learnMoreHref && (
<Button
className="!bg-transparent text-white"
color="minimal"
target="_blank"
href={tip.learnMoreHref}>
{t("learn_more")}
</Button>
)}
</div>
<button
onClick={() => handleRemoveItem(tip.id)}
className="!focus:border-transparent !focus:ring-0 absolute right-2 top-2 flex h-8 w-8 items-center justify-center rounded-md !border-transparent text-white hover:bg-white/10">
<span className="sr-only">Hide Banner</span>
<Icon.FiX className="h-4 w-4" />
</button>
</div>
</div>
);
})}
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { default as Tips } from "./Tips";
+1 -1
View File
@@ -162,7 +162,7 @@ export const getUsernameList = (users: string | string[] | undefined): string[]
const allUsers = users.map((user) =>
user
//.toLowerCase() // This was causing 404s for mixed-case usernames
.toLowerCase()
.replace(/( |%20)/g, "+")
.split("+")
);
+4 -2
View File
@@ -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<T extends z.ZodType>(schema: T) {
export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
type Output = z.infer<typeof schema>;
type FullOutput = Required<Output>;
type OutputKeys = Required<keyof FullOutput>;
@@ -65,8 +65,10 @@ export function useTypedQuery<T extends z.ZodType>(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<T extends z.ZodType>(schema: T) {
// Remove item by key and value
function removeItemByKeyAndValue<J extends ArrayOutputKeys>(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);
@@ -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<typeof bookingListingByStatus, Prisma.BookingWhereInput> = {
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<string, Prisma.BookingWhereInput> = {
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,
@@ -647,14 +691,10 @@ export const bookingsRouter = router({
const tOrganizer = await getTranslation(user.locale ?? "en", "common");
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUniqueOrThrow({
where: {
id: bookingId,
},
rejectOnNotFound() {
throw new TRPCError({ code: "NOT_FOUND", message: "Booking not found" });
},
// should trpc handle this error ?
select: {
title: true,
description: true,
@@ -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;
@@ -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<number, typeof teams[number]["members"][number]["user"]>;
// 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);
}),
});
+1
View File
@@ -1,4 +1,5 @@
import superjson from "superjson";
import { ZodError } from "zod";
import rateLimit from "@calcom/lib/rateLimit";
+31 -23
View File
@@ -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<string>;
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 = (
<AvatarPrimitive.Root
className={classNames(
sizeClassname,
"dark:bg-darkgray-300 relative inline-block aspect-square overflow-hidden rounded-full"
"dark:bg-darkgray-300 item-center relative inline-flex aspect-square justify-center overflow-hidden rounded-full"
)}>
<AvatarPrimitive.Image src={imageSrc ?? undefined} alt={alt} className={rootClass} />
<AvatarPrimitive.Fallback delayMs={600}>
{gravatarFallbackMd5 && (
<img src={defaultAvatarSrc({ md5: gravatarFallbackMd5 })} alt={alt} className={rootClass} />
)}
</AvatarPrimitive.Fallback>
{props.accepted && (
<div
className={classNames(
"absolute bottom-0 right-0 block rounded-full bg-green-400 text-white ring-2 ring-white",
size === "lg" ? "h-5 w-5" : "h-2 w-2"
)}>
<div className="flex h-full items-center justify-center p-[2px]">
{size === "lg" && <Icon.FiCheck className="" />}
<>
<AvatarPrimitive.Image src={imageSrc ?? undefined} alt={alt} className={rootClass} />
<AvatarPrimitive.Fallback delayMs={600} asChild={props.asChild}>
<>
{props.fallback && !gravatarFallbackMd5 && props.fallback}
{gravatarFallbackMd5 && (
<img src={defaultAvatarSrc({ md5: gravatarFallbackMd5 })} alt={alt} className={rootClass} />
)}
</>
</AvatarPrimitive.Fallback>
{props.accepted && (
<div
className={classNames(
"absolute bottom-0 right-0 block rounded-full bg-green-400 text-white ring-2 ring-white",
size === "lg" ? "h-5 w-5" : "h-2 w-2"
)}>
<div className="flex h-full items-center justify-center p-[2px]">
{size === "lg" && <Icon.FiCheck />}
</div>
</div>
</div>
)}
)}
</>
</AvatarPrimitive.Root>
);
+3
View File
@@ -29,3 +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";
+2
View File
@@ -0,0 +1,2 @@
export { List, ListItem, ListItemText, ListItemTitle } from "./List";
export type { ListItemProps, ListProps } from "./List";
+72
View File
@@ -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<HTMLDivElement>(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 (
<Popover.Root onOpenChange={setOpen} modal={true}>
<Popover.Trigger asChild>
<div
ref={ref}
className="item-center mb-2 flex h-9 justify-between whitespace-nowrap rounded-md border border-gray-300 py-2 px-3
text-sm placeholder:text-gray-400 hover:cursor-pointer hover:border-gray-400
focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1">
<div className="max-w-36 flex items-center">
<div className="truncate">
{text}
{count && count > 0 && (
<div className="flex h-4 w-4 items-center justify-center rounded-full">{count}</div>
)}
</div>
<Icon.FiChevronDown
className={classNames("mt-auto ml-2 transition-transform duration-150", open && "rotate-180")}
/>
</div>
</div>
</Popover.Trigger>
<Popover.Content side="bottom" align={align} asChild>
<div
className={classNames(
"absolute z-50 mt-2 w-56 rounded-md bg-white shadow-sm ring-1 ring-black ring-opacity-5 focus-within:outline-none",
align === "end" && "-translate-x-[228px]"
)}>
{children}
</div>
</Popover.Content>
</Popover.Root>
);
};
@@ -1,4 +1,5 @@
import React, { FC } from "react";
import { IconType } from "react-icons/lib";
import {
Button,
@@ -7,18 +8,17 @@ import {
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuTrigger,
ButtonBaseProps,
Icon,
} from "@calcom/ui";
import { SVGComponent } from "@lib/types/SVGComponent";
export type ActionType = {
id: string;
icon?: SVGComponent;
icon?: IconType;
iconClassName?: string;
label: string;
disabled?: boolean;
color?: "primary" | "secondary";
color?: ButtonBaseProps["color"];
} & (
| { href: string; onClick?: never; actions?: never }
| { href?: never; onClick: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void; actions?: never }
@@ -33,7 +33,7 @@ const defaultAction = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation();
};
const DropdownActions = ({
export const DropdownActions = ({
actions,
actionTrigger,
}: {
@@ -52,10 +52,10 @@ const DropdownActions = ({
<DropdownMenuPortal>
<DropdownMenuContent>
{actions.map((action) => (
<DropdownMenuItem key={action.id} className="focus-visible:outline-none">
<DropdownMenuItem key={action.id}>
<Button
type="button"
color="minimal"
color={action.color || "minimal"}
className="w-full rounded-none font-normal"
href={action.href}
StartIcon={action.icon}
@@ -71,7 +71,7 @@ const DropdownActions = ({
);
};
const TableActions: FC<Props> = ({ actions }) => {
export const TableActions: FC<Props> = ({ actions }) => {
return (
<>
<div className="flex space-x-2 rtl:space-x-reverse">
@@ -99,5 +99,3 @@ const TableActions: FC<Props> = ({ actions }) => {
</>
);
};
export default TableActions;
+5 -9
View File
@@ -24,10 +24,14 @@ export {
TextAreaField,
TextField,
TopBanner,
AnimatedPopover,
Select,
SelectField,
SelectWithValidation,
TableActions,
DropdownActions,
} from "./components";
export type { ActionType } from "./components";
export type { AvatarProps, BadgeProps, ButtonBaseProps, ButtonProps, TopBannerProps } from "./components";
export { default as CheckboxField } from "./components/form/checkbox/Checkbox";
/** ⬇️ TODO - Move these to components */
@@ -42,8 +46,6 @@ export { default as TimezoneChangeDialog } from "./TimezoneChangeDialog";
// export { default as Tooltip } from "./Tooltip";
export {
Alert,
CheckedTeamSelect,
CustomInputItem,
EmptyScreen,
HorizontalTabs,
SettingsToggle,
@@ -54,8 +56,8 @@ export {
SkeletonText,
Swatch,
Switch,
Card,
VerticalTabs,
TipBanner,
} from "./v2";
export type { AlertProps } from "./v2";
export { Segment, SegmentOption } from "./v2/core";
@@ -98,7 +100,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";
@@ -116,8 +117,3 @@ export type { ITimezone, ITimezoneOption } from "./v2/core/TimezoneSelect";
export { Tooltip } from "./v2/core/Tooltip";
export { default as VerticalDivider } from "./v2/core/VerticalDivider";
export { default as WizardForm } from "./v2/core/WizardForm";
export { default as SAMLLogin } from "./v2/modules/auth/SAMLLogin";
export { default as DatePicker } from "./v2/modules/booker/DatePicker";
export { EventTypeDescriptionLazy } from "./v2/modules/event-types";
export { EventTypeDescription } from "./v2/modules/event-types/EventTypeDescription";
export { default as Tips } from "./v2/core/Tips";
+1
View File
@@ -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",
+1 -1
View File
@@ -12,6 +12,7 @@ import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookin
import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner";
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem";
import { TeamsUpgradeBanner } from "@calcom/features/ee/teams/components";
import { Tips } from "@calcom/features/tips";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import {
@@ -40,7 +41,6 @@ import {
Icon,
showToast,
TimezoneChangeDialog,
Tips,
} from "../..";
/* TODO: Get this from endpoint */
-1
View File
@@ -46,4 +46,3 @@ export { default as Switch } from "./Switch";
export { default as Tooltip } from "./Tooltip";
export { default as VerticalDivider } from "./VerticalDivider";
export { default as WizardForm } from "./WizardForm";
export { TipBanner } from "./Tips";
-1
View File
@@ -1,3 +1,2 @@
/* TODO: Make this index explicit */
export * from "./core";
export * from "./modules";
@@ -1,7 +0,0 @@
# To all Cal.com contributors
All these components should live in `packages/features`
With love,
zomars 💕
-1
View File
@@ -1 +0,0 @@
export { default as SAMLLogin } from "./SAMLLogin";
-1
View File
@@ -1 +0,0 @@
export * from "./DatePicker";
-3
View File
@@ -1,3 +0,0 @@
export * from "./auth";
export * from "./booker";
export * from "./event-types";
+75
View File
@@ -5195,6 +5195,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-arrow@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz#5246adf79e97f89e819af68da51ddcf349ecf1c4"
integrity sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-avatar@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.0.tgz#74ff3c0b39a74e07a5b2f1a1718ee480f6ed4e2b"
@@ -5403,6 +5411,16 @@
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-focus-scope@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz#faea8c25f537c5a5c38c50914b63722db0e7f951"
integrity sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-id@0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-0.1.5.tgz#010d311bedd5a2884c1e9bb6aaaa4e6cc1d1d3b8"
@@ -5509,6 +5527,28 @@
aria-hidden "^1.1.1"
react-remove-scroll "2.5.4"
"@radix-ui/react-popover@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.2.tgz#f761c8aafb61519522b8e80501c589e619073a2c"
integrity sha512-4tqZEl9w95R5mlZ/sFdgBnfhCBOEPepLIurBA5kt/qaAhldJ1tNQd0ngr0ET0AHbPotT4mwxMPr7a+MA/wbK0g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-dismissable-layer" "1.0.2"
"@radix-ui/react-focus-guards" "1.0.0"
"@radix-ui/react-focus-scope" "1.0.1"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-popper" "1.0.1"
"@radix-ui/react-portal" "1.0.1"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popper@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.0.0.tgz#fb4f937864bf39c48f27f55beee61fa9f2bef93c"
@@ -5525,6 +5565,22 @@
"@radix-ui/react-use-size" "1.0.0"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-popper@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.0.1.tgz#9fa8a6a493404afa225866a5cd75af23d141baa0"
integrity sha512-J4Vj7k3k+EHNWgcKrE+BLlQfpewxA7Zd76h5I0bIa+/EqaIZ3DuwrbPj49O3wqN+STnXsBuxiHLiF0iU3yfovw==
dependencies:
"@babel/runtime" "^7.13.10"
"@floating-ui/react-dom" "0.7.2"
"@radix-ui/react-arrow" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-use-rect" "1.0.0"
"@radix-ui/react-use-size" "1.0.0"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-portal@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-0.1.4.tgz#17bdce3d7f1a9a0b35cb5e935ab8bc562441a7d2"
@@ -5542,6 +5598,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-portal@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.1.tgz#169c5a50719c2bb0079cf4c91a27aa6d37e5dd33"
integrity sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-presence@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a"
@@ -22513,6 +22577,17 @@ react-remove-scroll@2.5.4:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-remove-scroll@2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-remove-scroll@^2.4.0:
version "2.4.4"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.4.4.tgz#2dfff377cf17efc00de39dad51c143fc7a1b9e3e"