Event type dropdown (#2081)

This commit is contained in:
Leo Giovanetti 2022-03-16 16:55:18 -03:00 committed by GitHub
parent c9484172a4
commit 6e4f8e67b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1350 additions and 1062 deletions

View file

@ -59,12 +59,15 @@ type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>( export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
({ children, ...props }, forwardedRef) => ( ({ children, ...props }, forwardedRef) => (
<DialogPrimitive.Content <DialogPrimitive.Portal>
{...props} <DialogPrimitive.Overlay className="fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
className="fixed left-1/2 top-1/2 z-50 min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white p-6 text-left shadow-xl sm:w-full sm:max-w-lg sm:align-middle" <DialogPrimitive.Content
ref={forwardedRef}> {...props}
{children} className="fixed left-1/2 top-1/2 z-[9999999999] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white p-6 text-left shadow-xl sm:w-full sm:max-w-lg sm:align-middle"
</DialogPrimitive.Content> ref={forwardedRef}>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
) )
); );

View file

@ -17,6 +17,12 @@ import { useRouter } from "next/router";
import React, { ReactNode, useEffect, useState } from "react"; import React, { ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import LicenseBanner from "@ee/components/LicenseBanner"; import LicenseBanner from "@ee/components/LicenseBanner";
import TrialBanner from "@ee/components/TrialBanner"; import TrialBanner from "@ee/components/TrialBanner";
import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem"; import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem";
@ -32,12 +38,6 @@ import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding"; import CustomBranding from "@components/CustomBranding";
import Loader from "@components/Loader"; import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/ui/Dropdown";
import pkg from "../package.json"; import pkg from "../package.json";
import { useViewerI18n } from "./I18nLanguageHandler"; import { useViewerI18n } from "./I18nLanguageHandler";

View file

@ -7,6 +7,13 @@ import { useForm } from "react-hook-form";
import type { z } from "zod"; import type { z } from "zod";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype"; import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
@ -19,17 +26,10 @@ import { Form, InputLeading, TextAreaField, TextField } from "@components/form/f
import { Alert } from "@components/ui/Alert"; import { Alert } from "@components/ui/Alert";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import { Button } from "@components/ui/Button"; import { Button } from "@components/ui/Button";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/ui/Dropdown";
import * as RadioArea from "@components/ui/form/radio-area"; import * as RadioArea from "@components/ui/form/radio-area";
// this describes the uniform data needed to create a new event type on Profile or Team // this describes the uniform data needed to create a new event type on Profile or Team
interface EventTypeParent { export interface EventTypeParent {
teamId: number | null | undefined; // if undefined, then it's a profile teamId: number | null | undefined; // if undefined, then it's a profile
name?: string | null; name?: string | null;
slug?: string | null; slug?: string | null;
@ -56,12 +56,23 @@ export default function CreateEventTypeButton(props: Props) {
: undefined; : undefined;
const pageSlug = router.query.eventPage || props.options[0].slug; const pageSlug = router.query.eventPage || props.options[0].slug;
const hasTeams = !!props.options.find((option) => option.teamId); const hasTeams = !!props.options.find((option) => option.teamId);
const title: string =
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
const length: number =
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
const description: string =
typeof router.query.description === "string" && router.query.description ? router.query.description : "";
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
const form = useForm<z.infer<typeof createEventTypeInput>>({ const form = useForm<z.infer<typeof createEventTypeInput>>({
resolver: zodResolver(createEventTypeInput), resolver: zodResolver(createEventTypeInput),
defaultValues: { length: 15 },
}); });
const { setValue, watch, register } = form; const { setValue, watch, register } = form;
setValue("title", title);
setValue("length", length);
setValue("description", description);
setValue("slug", slug);
useEffect(() => { useEffect(() => {
const subscription = watch((value, { name, type }) => { const subscription = watch((value, { name, type }) => {
@ -113,7 +124,9 @@ export default function CreateEventTypeButton(props: Props) {
}; };
return ( return (
<Dialog name="new-eventtype" clearQueryParamsOnClose={["eventPage", "teamId"]}> <Dialog
name="new-eventtype"
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
{!hasTeams || props.isIndividualTeam ? ( {!hasTeams || props.isIndividualTeam ? (
<Button <Button
onClick={() => openModal(props.options[0])} onClick={() => openModal(props.options[0])}
@ -196,7 +209,6 @@ export default function CreateEventTypeButton(props: Props) {
required required
min="10" min="10"
placeholder="15" placeholder="15"
defaultValue={15}
label={t("length")} label={t("length")}
className="pr-20" className="pr-20"
{...register("length", { valueAsNumber: true })} {...register("length", { valueAsNumber: true })}
@ -222,11 +234,17 @@ export default function CreateEventTypeButton(props: Props) {
{...register("schedulingType")} {...register("schedulingType")}
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)} onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
className="relative mt-1 flex space-x-6 rounded-sm shadow-sm rtl:space-x-reverse"> className="relative mt-1 flex space-x-6 rounded-sm shadow-sm rtl:space-x-reverse">
<RadioArea.Item value={SchedulingType.COLLECTIVE} className="w-1/2 text-sm"> <RadioArea.Item
value={SchedulingType.COLLECTIVE}
defaultChecked={type === SchedulingType.COLLECTIVE}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("collective")}</strong> <strong className="mb-1 block">{t("collective")}</strong>
<p>{t("collective_description")}</p> <p>{t("collective_description")}</p>
</RadioArea.Item> </RadioArea.Item>
<RadioArea.Item value={SchedulingType.ROUND_ROBIN} className="w-1/2 text-sm"> <RadioArea.Item
value={SchedulingType.ROUND_ROBIN}
defaultChecked={type === SchedulingType.ROUND_ROBIN}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("round_robin")}</strong> <strong className="mb-1 block">{t("round_robin")}</strong>
<p>{t("round_robin_description")}</p> <p>{t("round_robin_description")}</p>
</RadioArea.Item> </RadioArea.Item>

View file

@ -3,6 +3,12 @@ import { ClockIcon, ExternalLinkIcon, DotsHorizontalIcon } from "@heroicons/reac
import Link from "next/link"; import Link from "next/link";
import React, { useState } from "react"; import React, { useState } from "react";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import TeamAvailabilityModal from "@ee/components/team/availability/TeamAvailabilityModal"; import TeamAvailabilityModal from "@ee/components/team/availability/TeamAvailabilityModal";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
@ -17,12 +23,6 @@ import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button"; import Button from "@components/ui/Button";
import ModalContainer from "@components/ui/ModalContainer"; import ModalContainer from "@components/ui/ModalContainer";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/Dropdown";
import MemberChangeRoleModal from "./MemberChangeRoleModal"; import MemberChangeRoleModal from "./MemberChangeRoleModal";
import TeamPill, { TeamRole } from "./TeamPill"; import TeamPill, { TeamRole } from "./TeamPill";
import { MembershipRole } from ".prisma/client"; import { MembershipRole } from ".prisma/client";

View file

@ -2,6 +2,13 @@ import { ExternalLinkIcon, TrashIcon, LogoutIcon, PencilIcon } from "@heroicons/
import { LinkIcon, DotsHorizontalIcon } from "@heroicons/react/solid"; import { LinkIcon, DotsHorizontalIcon } from "@heroicons/react/solid";
import Link from "next/link"; import Link from "next/link";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@calcom/ui/Dropdown";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
@ -13,12 +20,6 @@ import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button"; import Button from "@components/ui/Button";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@components/ui/Dropdown";
import { TeamRole } from "./TeamPill"; import { TeamRole } from "./TeamPill";
import { MembershipRole } from ".prisma/client"; import { MembershipRole } from ".prisma/client";
@ -126,8 +127,9 @@ export default function TeamListItem(props: Props) {
<a> <a>
<Button <Button
type="button" type="button"
size="lg"
color="minimal" color="minimal"
className="w-full font-normal" className="w-full rounded-none font-normal"
StartIcon={PencilIcon}> StartIcon={PencilIcon}>
{t("edit_team")} {t("edit_team")}
</Button> </Button>
@ -135,14 +137,14 @@ export default function TeamListItem(props: Props) {
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{isAdmin && <DropdownMenuSeparator className="h-px bg-gray-200" />}
<DropdownMenuItem> <DropdownMenuItem>
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${team.slug}`} passHref={true}> <Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${team.slug}`} passHref={true}>
<a target="_blank"> <a target="_blank">
<Button <Button
type="button" type="button"
size="lg"
color="minimal" color="minimal"
className="w-full font-normal" className="w-full rounded-none font-normal"
StartIcon={ExternalLinkIcon}> StartIcon={ExternalLinkIcon}>
{" "} {" "}
{t("preview_team")} {t("preview_team")}
@ -160,8 +162,9 @@ export default function TeamListItem(props: Props) {
e.stopPropagation(); e.stopPropagation();
}} }}
color="warn" color="warn"
size="lg"
StartIcon={TrashIcon} StartIcon={TrashIcon}
className="w-full font-normal"> className="w-full rounded-none font-normal">
{t("disband_team")} {t("disband_team")}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@ -183,8 +186,9 @@ export default function TeamListItem(props: Props) {
<Button <Button
type="button" type="button"
color="warn" color="warn"
size="lg"
StartIcon={LogoutIcon} StartIcon={LogoutIcon}
className="w-full" className="w-full rounded-none"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}}> }}>

View file

@ -1,15 +1,11 @@
import { DotsHorizontalIcon } from "@heroicons/react/solid"; import { DotsHorizontalIcon } from "@heroicons/react/solid";
import React, { FC } from "react"; import React, { FC } from "react";
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@calcom/ui/Dropdown";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { SVGComponent } from "@lib/types/SVGComponent"; import { SVGComponent } from "@lib/types/SVGComponent";
import Dropdown, {
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@components/ui/Dropdown";
import Button from "./Button"; import Button from "./Button";
export type ActionType = { export type ActionType = {
@ -52,8 +48,9 @@ const TableActions: FC<Props> = ({ actions }) => {
<DropdownMenuItem key={action.id}> <DropdownMenuItem key={action.id}>
<Button <Button
type="button" type="button"
size="lg"
color="minimal" color="minimal"
className="w-full font-normal" className="w-full rounded-none font-normal"
href={action.href} href={action.href}
StartIcon={action.icon} StartIcon={action.icon}
onClick={action.onClick}> onClick={action.onClick}>

View file

@ -1,11 +1,11 @@
import { ChatAltIcon } from "@heroicons/react/solid"; import { ChatAltIcon } from "@heroicons/react/solid";
import { useIntercom } from "react-use-intercom"; import { useIntercom } from "react-use-intercom";
import { DropdownMenuItem } from "@calcom/ui/Dropdown";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { DropdownMenuItem } from "@components/ui/Dropdown";
export default function IntercomMenuItem() { export default function IntercomMenuItem() {
const { t } = useLocale(); const { t } = useLocale();
const { boot, show } = useIntercom(); const { boot, show } = useIntercom();

View file

@ -2,11 +2,11 @@ import { ChatAltIcon } from "@heroicons/react/solid";
import Script from "next/script"; import Script from "next/script";
import { useState } from "react"; import { useState } from "react";
import { DropdownMenuItem } from "@calcom/ui/Dropdown";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { DropdownMenuItem } from "@components/ui/Dropdown";
const ZENDESK_KEY = process.env.NEXT_PUBLIC_ZENDESK_KEY; const ZENDESK_KEY = process.env.NEXT_PUBLIC_ZENDESK_KEY;
export default function ZendeskMenuItem() { export default function ZendeskMenuItem() {

View file

@ -3,72 +3,72 @@ import {
ArrowUpIcon, ArrowUpIcon,
DotsHorizontalIcon, DotsHorizontalIcon,
ExternalLinkIcon, ExternalLinkIcon,
DuplicateIcon,
LinkIcon, LinkIcon,
UsersIcon,
UploadIcon, UploadIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
TrashIcon,
PencilIcon,
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import { UsersIcon } from "@heroicons/react/solid";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react"; import React, { Fragment, useEffect, useState } from "react";
import { Button } from "@calcom/ui"; import { Button } from "@calcom/ui";
import Dropdown, {
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from "@calcom/ui/Dropdown";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification"; import showToast from "@lib/notification";
import { inferQueryOutput, trpc } from "@lib/trpc"; import { inferQueryOutput, trpc } from "@lib/trpc";
import { Dialog, DialogTrigger } from "@components/Dialog";
import Shell from "@components/Shell"; import Shell from "@components/Shell";
import { Tooltip } from "@components/Tooltip"; import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import CreateEventTypeButton from "@components/eventtype/CreateEventType"; import CreateEventTypeButton from "@components/eventtype/CreateEventType";
import EventTypeDescription from "@components/eventtype/EventTypeDescription"; import EventTypeDescription from "@components/eventtype/EventTypeDescription";
import { Alert } from "@components/ui/Alert"; import { Alert } from "@components/ui/Alert";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup"; import AvatarGroup from "@components/ui/AvatarGroup";
import Badge from "@components/ui/Badge"; import Badge from "@components/ui/Badge";
import Dropdown, {
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@components/ui/Dropdown";
import UserCalendarIllustration from "@components/ui/svg/UserCalendarIllustration"; import UserCalendarIllustration from "@components/ui/svg/UserCalendarIllustration";
type Profiles = inferQueryOutput<"viewer.eventTypes">["profiles"]; type Profiles = inferQueryOutput<"viewer.eventTypes">["profiles"];
type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
interface CreateEventTypeProps { interface CreateEventTypeProps {
canAddEvents: boolean; canAddEvents: boolean;
profiles: Profiles; profiles: Profiles;
} }
const CreateFirstEventTypeView = ({ canAddEvents, profiles }: CreateEventTypeProps) => { type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"];
const { t } = useLocale(); type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
interface EventTypeListHeadingProps {
return ( profile: EventTypeGroupProfile;
<div className="md:py-20"> membershipCount: number;
<UserCalendarIllustration /> }
<div className="mx-auto block text-center md:max-w-screen-sm">
<h3 className="mt-2 text-xl font-bold text-neutral-900">{t("new_event_type_heading")}</h3>
<p className="text-md mt-1 mb-2 text-neutral-600">{t("new_event_type_description")}</p>
<CreateEventTypeButton canAddEvents={canAddEvents} options={profiles} />
</div>
</div>
);
};
type EventTypeGroup = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"][number]; type EventTypeGroup = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"][number];
type EventType = EventTypeGroup["eventTypes"][number]; type EventType = EventTypeGroup["eventTypes"][number];
interface EventTypeListProps { interface EventTypeListProps {
profile: { slug: string | null }; group: EventTypeGroup;
readOnly: boolean; readOnly: boolean;
types: EventType[]; types: EventType[];
} }
const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.Element => {
export const EventTypeList = ({ group, readOnly, types }: EventTypeListProps): JSX.Element => {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter();
const utils = trpc.useContext(); const utils = trpc.useContext();
const mutation = trpc.useMutation("viewer.eventTypeOrder", { const mutation = trpc.useMutation("viewer.eventTypeOrder", {
@ -99,6 +99,50 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
}); });
} }
async function deleteEventTypeHandler(id: number) {
const payload = { id };
deleteMutation.mutate(payload);
}
// inject selection data into url for correct router history
const openModal = (group: EventTypeGroup, type: EventType) => {
const query = {
...router.query,
dialog: "new-eventtype",
eventPage: group.profile.slug,
title: type.title,
slug: type.slug,
description: type.description,
length: type.length,
type: type.schedulingType,
teamId: group.teamId,
};
if (!group.teamId) {
delete query.teamId;
}
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
};
const deleteMutation = trpc.useMutation("viewer.eventTypes.delete", {
onSuccess: async () => {
await utils.invalidateQueries(["viewer.eventTypes"]);
showToast(t("event_type_deleted_successfully"), "success");
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
},
});
const [isNativeShare, setNativeShare] = useState(true); const [isNativeShare, setNativeShare] = useState(true);
useEffect(() => { useEffect(() => {
@ -143,8 +187,16 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
className="flex-grow truncate text-sm" className="flex-grow truncate text-sm"
title={`${type.title} ${type.description ? ` ${type.description}` : ""}`}> title={`${type.title} ${type.description ? ` ${type.description}` : ""}`}>
<div> <div>
<span className="truncate font-medium text-neutral-900">{type.title} </span> <span
<small className="hidden text-neutral-500 sm:inline">{`/${profile.slug}/${type.slug}`}</small> className="truncate font-medium text-neutral-900"
data-testid={"event-type-title-" + type.id}>
{type.title}
</span>
<small
className="hidden text-neutral-500 sm:inline"
data-testid={
"event-type-slug-" + type.id
}>{`/${group.profile.slug}/${type.slug}`}</small>
{type.hidden && ( {type.hidden && (
<span className="rtl:mr-2inline items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ltr:ml-2"> <span className="rtl:mr-2inline items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ltr:ml-2">
{t("hidden")} {t("hidden")}
@ -161,7 +213,7 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
</Link> </Link>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex"> <div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
<div className="flex items-center space-x-2 overflow-hidden rtl:space-x-reverse"> <div className="flex justify-between rtl:space-x-reverse">
{type.users?.length > 1 && ( {type.users?.length > 1 && (
<AvatarGroup <AvatarGroup
border="border-2 border-white" border="border-2 border-white"
@ -175,7 +227,7 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
)} )}
<Tooltip content={t("preview")}> <Tooltip content={t("preview")}>
<a <a
href={`${process.env.NEXT_PUBLIC_APP_URL}/${profile.slug}/${type.slug}`} href={`${process.env.NEXT_PUBLIC_APP_URL}/${group.profile.slug}/${type.slug}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="btn-icon appearance-none"> className="btn-icon appearance-none">
@ -188,13 +240,74 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
onClick={() => { onClick={() => {
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${process.env.NEXT_PUBLIC_APP_URL}/${profile.slug}/${type.slug}` `${process.env.NEXT_PUBLIC_APP_URL}/${group.profile.slug}/${type.slug}`
); );
}} }}
className="btn-icon"> className="btn-icon">
<LinkIcon className="h-5 w-5 group-hover:text-black" /> <LinkIcon className="h-5 w-5 group-hover:text-black" />
</button> </button>
</Tooltip> </Tooltip>
<Dropdown>
<DropdownMenuTrigger
className="h-[38px] w-[38px] cursor-pointer rounded-sm border border-transparent text-neutral-500 hover:border-gray-300 hover:text-neutral-900"
data-testid={"event-type-options-" + type.id}>
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link href={"/event-types/" + type.id} passHref={true}>
<Button
type="button"
size="lg"
color="minimal"
className="w-full rounded-none font-normal"
StartIcon={PencilIcon}>
{" "}
{t("edit")}
</Button>
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Button
type="button"
color="minimal"
size="lg"
className="w-full rounded-none font-normal"
data-testid={"event-type-duplicate-" + type.id}
StartIcon={DuplicateIcon}
onClick={() => openModal(group, type)}>
{t("duplicate")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<Dialog>
<DialogTrigger asChild>
<Button
onClick={(e) => {
e.stopPropagation();
}}
color="warn"
size="lg"
StartIcon={TrashIcon}
className="w-full rounded-none font-normal">
{t("delete")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
onConfirm={(e) => {
e.preventDefault();
deleteEventTypeHandler(type.id);
}}>
{t("delete_event_type_description")}
</ConfirmationDialogContent>
</Dialog>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div> </div>
</div> </div>
</div> </div>
@ -205,9 +318,13 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent portalled> <DropdownMenuContent portalled>
<DropdownMenuItem> <DropdownMenuItem>
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/${profile.slug}/${type.slug}`}> <Link href={`${process.env.NEXT_PUBLIC_APP_URL}/${group.profile.slug}/${type.slug}`}>
<a target="_blank"> <a target="_blank">
<Button color="minimal" StartIcon={ExternalLinkIcon} className="w-full font-normal"> <Button
color="minimal"
size="lg"
StartIcon={ExternalLinkIcon}
className="w-full rounded-none font-normal">
{t("preview")} {t("preview")}
</Button> </Button>
</a> </a>
@ -217,12 +334,13 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
<Button <Button
type="button" type="button"
color="minimal" color="minimal"
className="w-full font-normal" size="lg"
className="w-full rounded-none text-left font-normal"
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={ClipboardCopyIcon} StartIcon={ClipboardCopyIcon}
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${process.env.NEXT_PUBLIC_APP_URL}/${profile.slug}/${type.slug}` `${process.env.NEXT_PUBLIC_APP_URL}/${group.profile.slug}/${type.slug}`
); );
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
}}> }}>
@ -234,7 +352,8 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
<Button <Button
type="button" type="button"
color="minimal" color="minimal"
className="w-full font-normal" size="lg"
className="w-full rounded-none font-normal"
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={UploadIcon} StartIcon={UploadIcon}
onClick={() => { onClick={() => {
@ -242,7 +361,7 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
.share({ .share({
title: t("share"), title: t("share"),
text: t("share_event"), text: t("share_event"),
url: `${process.env.NEXT_PUBLIC_APP_URL}/${profile.slug}/${type.slug}`, url: `${process.env.NEXT_PUBLIC_APP_URL}/${group.profile.slug}/${type.slug}`,
}) })
.then(() => showToast(t("link_shared"), "success")) .then(() => showToast(t("link_shared"), "success"))
.catch(() => showToast(t("failed"), "error")); .catch(() => showToast(t("failed"), "error"));
@ -251,6 +370,58 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
) : null} ) : null}
<DropdownMenuItem>
<Link href={"/event-types/" + type.id} passHref={true}>
<Button
type="button"
size="lg"
color="minimal"
className="w-full rounded-none font-normal"
StartIcon={PencilIcon}>
{" "}
{t("edit")}
</Button>
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Button
type="button"
color="minimal"
size="lg"
className="w-full rounded-none font-normal"
data-testid={"event-type-duplicate-" + type.id}
StartIcon={DuplicateIcon}
onClick={() => openModal(group, type)}>
{t("duplicate")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<Dialog>
<DialogTrigger asChild>
<Button
onClick={(e) => {
e.stopPropagation();
}}
color="warn"
size="lg"
StartIcon={TrashIcon}
className="w-full rounded-none font-normal">
{t("delete")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
onConfirm={(e) => {
e.preventDefault();
deleteEventTypeHandler(type.id);
}}>
{t("delete_event_type_description")}
</ConfirmationDialogContent>
</Dialog>
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</Dropdown> </Dropdown>
</div> </div>
@ -262,10 +433,6 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
); );
}; };
interface EventTypeListHeadingProps {
profile: EventTypeGroupProfile;
membershipCount: number;
}
const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => ( const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => (
<div className="mb-4 flex"> <div className="mb-4 flex">
<Link href="/settings/teams"> <Link href="/settings/teams">
@ -306,6 +473,21 @@ const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeading
</div> </div>
); );
const CreateFirstEventTypeView = ({ canAddEvents, profiles }: CreateEventTypeProps) => {
const { t } = useLocale();
return (
<div className="md:py-20">
<UserCalendarIllustration />
<div className="mx-auto block text-center md:max-w-screen-sm">
<h3 className="mt-2 text-xl font-bold text-neutral-900">{t("new_event_type_heading")}</h3>
<p className="text-md mt-1 mb-2 text-neutral-600">{t("new_event_type_description")}</p>
<CreateEventTypeButton canAddEvents={canAddEvents} options={profiles} />
</div>
</div>
);
};
const EventTypesPage = () => { const EventTypesPage = () => {
const { t } = useLocale(); const { t } = useLocale();
const query = trpc.useQuery(["viewer.eventTypes"]); const query = trpc.useQuery(["viewer.eventTypes"]);
@ -357,11 +539,7 @@ const EventTypesPage = () => {
membershipCount={group.metadata.membershipCount} membershipCount={group.metadata.membershipCount}
/> />
)} )}
<EventTypeList <EventTypeList types={group.eventTypes} group={group} readOnly={group.metadata.readOnly} />
types={group.eventTypes}
profile={group.profile}
readOnly={group.metadata.readOnly}
/>
</Fragment> </Fragment>
))} ))}

View file

@ -138,7 +138,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
value: locale, value: locale,
// FIXME // FIXME
// @ts-ignore // @ts-ignore
label: new Intl.DisplayNames(props.localeProp, { type: "language" }).of(locale), label: new Intl.DisplayNames(props.localeProp, { type: "language" }).of(locale) || "",
})); }));
}, [props.localeProp, router.locales]); }, [props.localeProp, router.locales]);

View file

@ -39,6 +39,27 @@ test.describe("pro user", () => {
await expect(page.locator(`text='${eventTitle}'`)).toBeVisible(); await expect(page.locator(`text='${eventTitle}'`)).toBeVisible();
}); });
test("can duplicate an existing event type", async ({ page }) => {
const firstTitle = await page.locator("[data-testid=event-type-title-3]").innerText();
const firstFullSlug = await page.locator("[data-testid=event-type-slug-3]").innerText();
const firstSlug = firstFullSlug.split("/")[2];
await page.click("[data-testid=event-type-options-3]");
await page.click("[data-testid=event-type-duplicate-3]");
const url = await page.url();
const params = new URLSearchParams(url);
await expect(params.get("title")).toBe(firstTitle);
await expect(params.get("slug")).toBe(firstSlug);
const formTitle = await page.inputValue("[name=title]");
const formSlug = await page.inputValue("[name=slug]");
await expect(formTitle).toBe(firstTitle);
await expect(formSlug).toBe(firstSlug);
});
}); });
test.describe("free user", () => { test.describe("free user", () => {

View file

@ -449,7 +449,7 @@
"continue": "Continue", "continue": "Continue",
"confirm": "Confirm", "confirm": "Confirm",
"disband_team": "Disband Team", "disband_team": "Disband Team",
"disband_team_confirmation_message": "Are you sure you want to disband this team? Anyone who you&apos;ve shared this team link with will no longer be able to book using it.", "disband_team_confirmation_message": "Are you sure you want to disband this team? Anyone who you've shared this team link with will no longer be able to book using it.",
"remove_member_confirmation_message": "Are you sure you want to remove this member from the team?", "remove_member_confirmation_message": "Are you sure you want to remove this member from the team?",
"confirm_disband_team": "Yes, disband team", "confirm_disband_team": "Yes, disband team",
"confirm_remove_member": "Yes, remove member", "confirm_remove_member": "Yes, remove member",
@ -674,5 +674,6 @@
"example_name": "John Doe", "example_name": "John Doe",
"time_format": "Time format", "time_format": "Time format",
"12_hour": "12 hour", "12_hour": "12 hour",
"24_hour": "24 hour" "24_hour": "24 hour",
"duplicate": "Duplicate"
} }

View file

@ -12,7 +12,7 @@ export const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTri
className={ className={
props.asChild props.asChild
? className ? className
: `relative inline-flex items-center rounded-sm bg-transparent px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-1 group-hover:text-black ltr:ml-2 rtl:mr-2 ${className}` : `inline-flex items-center rounded-sm bg-transparent px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-1 group-hover:text-black ${className}`
} }
ref={forwardedRef} ref={forwardedRef}
/> />
@ -20,6 +20,8 @@ export const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTri
); );
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"; DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
export const DropdownMenuTriggerItem = DropdownMenuPrimitive.TriggerItem;
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuPrimitive["Content"]>; type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuPrimitive["Content"]>;
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>( export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
({ children, ...props }, forwardedRef) => { ({ children, ...props }, forwardedRef) => {
@ -27,9 +29,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
portalled={props.portalled} portalled={props.portalled}
{...props} {...props}
className={`${ className={`z-10 mt-1 -ml-0 w-48 origin-top-right rounded-sm bg-white text-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none`}
props.portalled ? `` : `md:-ml-[55px]`
} z-10 mt-1 -ml-0 w-full origin-top-right rounded-sm bg-white text-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none`}
ref={forwardedRef}> ref={forwardedRef}>
{children} {children}
</DropdownMenuPrimitive.Content> </DropdownMenuPrimitive.Content>

1982
yarn.lock

File diff suppressed because it is too large Load diff