feat: round robin: show avatar and name of person on success page (#14245)

* feat: round robin: show avatar and name of person on success page

* fix: add background border

* fix: e2e tests

* fix: remove avatar on select, use proper border variables

* fix: bookings-list e2e round robin title

* chore: change name

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
This commit is contained in:
Amit Sharma
2024-04-16 11:36:11 +01:00
committed by GitHub
co-authored by Peer Richelsen Udit Takkar Udit Takkar
parent 0eae0bb8fe
commit 68568e5a25
20 changed files with 82 additions and 59 deletions
+1
View File
@@ -72,6 +72,7 @@ export const getEventTypesFromDB = async (id: number) => {
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
schedulingType: true,
periodStartDate: true,
periodEndDate: true,
},
@@ -44,11 +44,12 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import { BookingStatus } from "@calcom/prisma/enums";
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
Avatar,
Badge,
Button,
EmailInput,
@@ -225,6 +226,7 @@ export default function Success(props: PageProps) {
const giphyAppData = getEventTypeAppData(eventType, "giphy");
const giphyImage = giphyAppData?.thankYouPage;
const isRoundRobin = eventType.schedulingType === SchedulingType.ROUND_ROBIN;
const eventName = getEventName(eventNameObject, true);
// Confirmation can be needed in two cases as of now
@@ -294,6 +296,7 @@ export default function Success(props: PageProps) {
function getTitle(): string {
const titleSuffix = props.recurringBookings ? "_recurring" : "";
const titlePrefix = isRoundRobin ? "round_robin_" : "";
if (isCancelled) {
return "";
}
@@ -305,6 +308,11 @@ export default function Success(props: PageProps) {
}
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
}
if (bookingInfo.user) {
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
user: bookingInfo.user.name || bookingInfo.user.email,
});
}
return t(`emailed_you_and_attendees${titleSuffix}`);
}
@@ -396,27 +404,36 @@ export default function Success(props: PageProps) {
{!isFeedbackMode && (
<>
<div
className={classNames(
"mx-auto flex items-center justify-center",
!giphyImage && !isCancelled && !needsConfirmation
? "bg-success h-12 w-12 rounded-full"
: "",
!giphyImage && !isCancelled && needsConfirmation
? "bg-subtle h-12 w-12 rounded-full"
: "",
isCancelled ? "bg-error h-12 w-12 rounded-full " : ""
)}>
{giphyImage && !needsConfirmation && !isCancelled && (
// eslint-disable-next-line @next/next/no-img-element
<img src={giphyImage} alt="Gif from Giphy" />
className={classNames(isRoundRobin && "min-w-32 min-h-24 relative mx-auto h-24 w-32")}>
{isRoundRobin && bookingInfo.user && (
<Avatar
className="mx-auto flex items-center justify-center"
alt={bookingInfo.user.name || bookingInfo.user.email}
size="xl"
imageSrc={`${bookingInfo.user.avatarUrl}`}
/>
)}
{!giphyImage && !needsConfirmation && !isCancelled && (
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
)}
{needsConfirmation && !isCancelled && (
<Icon name="calendar" className="text-emphasis h-5 w-5" />
)}
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
<div
className={classNames(
"mx-auto flex h-12 w-12 items-center justify-center rounded-full",
isRoundRobin &&
"border-cal-bg dark:border-cal-bg-muted absolute bottom-0 right-0 z-10 h-12 w-12 border-8",
!giphyImage && !isCancelled && !needsConfirmation ? "bg-success" : "",
!giphyImage && !isCancelled && needsConfirmation ? "bg-subtle" : "",
isCancelled ? "bg-error" : ""
)}>
{giphyImage && !needsConfirmation && !isCancelled && (
// eslint-disable-next-line @next/next/no-img-element
<img src={giphyImage} alt="Gif from Giphy" />
)}
{!giphyImage && !needsConfirmation && !isCancelled && (
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
)}
{needsConfirmation && !isCancelled && (
<Icon name="calendar" className="text-emphasis h-5 w-5" />
)}
{isCancelled && <Icon name="x" className="h-5 w-5 text-red-600 dark:text-red-200" />}
</div>
</div>
<div className="mb-8 mt-6 text-center last:mb-0">
<h3
@@ -463,7 +480,7 @@ export default function Success(props: PageProps) {
)}
<div className="font-medium">{t("what")}</div>
<div className="col-span-2 mb-6 last:mb-0" data-testid="booking-title">
{eventName}
{isRoundRobin ? bookingInfo.title : eventName}
</div>
<div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6 last:mb-0">
@@ -103,7 +103,7 @@ test.describe("Bookings", () => {
page,
},
async () => {
await bookTeamEvent({ page, team, event: teamEvent });
await bookTeamEvent({ page, team, event: teamEvent, teamMatesObj });
// Since all the users have the same leastRecentlyBooked value
// Anyone of the teammates could be the Host of the booking.
@@ -421,13 +421,15 @@ async function bookTeamEvent({
page,
team,
event,
teamMatesObj,
}: {
page: Page;
team: {
slug: string | null;
name: string | null;
};
event: { slug: string; title: string };
event: { slug: string; title: string; schedulingType: SchedulingType | null };
teamMatesObj?: { name: string }[];
}) {
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
// So, we are using /team in the URL to access the team booking
@@ -440,8 +442,19 @@ async function bookTeamEvent({
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
if (event.schedulingType === SchedulingType.ROUND_ROBIN) {
const bookingTitle = await page.getByTestId("booking-title").textContent();
expect(
teamMatesObj?.some((teamMate) => {
const BookingTitle = `${event.title} between ${teamMate.name} and ${testName}`;
return BookingTitle === bookingTitle;
})
).toBe(true);
} else {
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
}
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
+7 -2
View File
@@ -147,8 +147,13 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
// The title of the booking
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
await expect(page.locator("[data-testid=booking-title]")).toHaveText(BookingTitle);
const bookingTitle = await page.getByTestId("booking-title").textContent();
expect(
teamMatesObj?.some((teamMate) => {
const BookingTitle = `${teamEventTitle} between ${teamMate.name} and ${testName}`;
return BookingTitle === bookingTitle;
})
).toBe(true);
// Since all the users have the same leastRecentlyBooked value
// Anyone of the teammates could be the Host of the booking.
@@ -326,8 +326,10 @@
"add_another_calendar": "Add another calendar",
"other": "Other",
"email_sign_in_subject": "Your sign-in link for {{appName}}",
"round_robin_emailed_you_and_attendees": "You are meeting with {{user}}. We sent an email with a calendar invitation with the details to everyone.",
"emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.",
"emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
"round_robin_emailed_you_and_attendees_recurring": "You are meeting with {{user}}.We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
"emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.",
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
"needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.",
+2
View File
@@ -67,6 +67,8 @@ module.exports = {
booker: `var(--cal-border-booker, ${subtleColor})`,
error: "var(--cal-border-error, #AA2E26)",
focus: "var(--cal-border-focus, #1A1A1A)",
"cal-bg": "var(--cal-bg, white)",
"cal-bg-muted": "var(--cal-bg-muted)",
},
textColor: {
emphasis: "var(--cal-text-emphasis, #111827)",
+1 -1
View File
@@ -6,7 +6,7 @@ import z from "zod";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Icon } from "@calcom/ui";
import { Button } from "@calcom/ui";
interface Props {
samlTenantID: string;
@@ -9,7 +9,6 @@ import {
DialogClose,
DialogContent,
EmptyScreen,
Icon,
ListItem,
ListItemText,
ListItemTitle,
@@ -29,6 +29,7 @@ const getUserBooking = async (uid: string) => {
email: true,
username: true,
timeZone: true,
avatarUrl: true,
},
},
attendees: {
@@ -44,6 +45,7 @@ const getUserBooking = async (uid: string) => {
eventName: true,
slug: true,
timeZone: true,
schedulingType: true,
},
},
seatsReferences: {
@@ -1,5 +1,5 @@
import dayjs from "@calcom/dayjs";
import { Button, ButtonGroup, Icon } from "@calcom/ui";
import { Button, ButtonGroup } from "@calcom/ui";
import { useCalendarStore } from "../../state/store";
@@ -2,7 +2,6 @@ import type { Directory } from "@boxyhq/saml-jackson";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast, Label, Tooltip } from "@calcom/ui";
import { Icon } from "@calcom/ui";
const DirectoryInfo = ({ directory }: { directory: Directory }) => {
const { t } = useLocale();
@@ -11,7 +11,6 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Icon,
Tooltip,
UserAvatar,
} from "@calcom/ui";
@@ -82,12 +81,7 @@ export default function MemberListItem(props: Props) {
<div className="flex md:hidden">
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="icon"
color="minimal"
StartIcon="ellipsis"
/>
<Button type="button" variant="icon" color="minimal" StartIcon="ellipsis" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem className="outline-none">
@@ -5,7 +5,7 @@ import { classNames } from "@calcom/lib";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { Avatar, Badge, Button, ButtonGroup, Icon, Select, Switch, Tooltip } from "@calcom/ui";
import { Avatar, Badge, Button, ButtonGroup, Select, Switch, Tooltip } from "@calcom/ui";
export type ChildrenEventType = {
value: string;
@@ -2,7 +2,7 @@ import { useFilterContext } from "@calcom/features/insights/context/provider";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc";
import { trpc } from "@calcom/trpc";
import { Button, Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger, Icon } from "@calcom/ui";
import { Button, Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui";
const Download = () => {
const { filter } = useFilterContext();
@@ -2,7 +2,7 @@ import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { TimeRange, WorkingHours } from "@calcom/types/schedule";
import { Button, DialogTrigger, Icon, Tooltip } from "@calcom/ui";
import { Button, DialogTrigger, Tooltip } from "@calcom/ui";
import DateOverrideInputDialog from "./DateOverrideInputDialog";
@@ -71,11 +71,7 @@ const DateOverride = ({ workingHours, disabled }: { workingHours: WorkingHours[]
excludedDates={excludedDates}
onChange={(ranges) => ranges.forEach((range) => append({ ranges: [range] }))}
Trigger={
<Button
color="secondary"
StartIcon="plus"
data-testid="add-override"
disabled={disabled}>
<Button color="secondary" StartIcon="plus" data-testid="add-override" disabled={disabled}>
{t("add_an_override")}
</Button>
}
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import type { UserProfile } from "@calcom/types/UserProfile";
import { Button, ButtonGroup, DataTable, Icon, UserAvatar } from "@calcom/ui";
import { Button, ButtonGroup, DataTable, UserAvatar } from "@calcom/ui";
import { UpgradeTip } from "../../tips/UpgradeTip";
import { createTimezoneBuddyStore, TBContext } from "../store";
@@ -2,7 +2,7 @@ import { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, ButtonGroup, Icon } from "@calcom/ui";
import { Button, ButtonGroup } from "@calcom/ui";
import { useTroubleshooterStore } from "../store";
@@ -4,7 +4,7 @@ import { ZodError } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { ZTestTriggerInputSchema } from "@calcom/trpc/server/routers/viewer/webhook/testTrigger.schema";
import { Badge, Button, Icon, showToast } from "@calcom/ui";
import { Badge, Button, showToast } from "@calcom/ui";
export default function WebhookTestDisclosure() {
const [subscriberUrl, webhookSecret]: [string, string] = useWatch({ name: ["subscriberUrl", "secret"] });
@@ -183,10 +183,7 @@ const DateOverride = ({
onChange={(ranges) => ranges.forEach((range) => append({ ranges: [range] }))}
userTimeFormat={userTimeFormat}
Trigger={
<Button
color="secondary"
StartIcon="plus"
data-testid="add-override">
<Button color="secondary" StartIcon="plus" data-testid="add-override">
{t("add_an_override")}
</Button>
}
@@ -330,11 +327,7 @@ export function AvailabilitySettings({
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
)}>
<div className="flex flex-row items-center pt-5">
<Button
StartIcon="arrow-left"
color="minimal"
onClick={() => setOpenSidebar(false)}
/>
<Button StartIcon="arrow-left" color="minimal" onClick={() => setOpenSidebar(false)} />
<p className="-ml-2">{t("availability_settings")}</p>
<DeleteDialogButton
buttonClassName="ml-16 inline"