Event type dropdown (#2081)
This commit is contained in:
parent
c9484172a4
commit
6e4f8e67b6
14 changed files with 1350 additions and 1062 deletions
|
@ -59,12 +59,15 @@ type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]
|
|||
|
||||
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
|
||||
({ children, ...props }, forwardedRef) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
<DialogPrimitive.Content
|
||||
{...props}
|
||||
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"
|
||||
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"
|
||||
ref={forwardedRef}>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -17,6 +17,12 @@ import { useRouter } from "next/router";
|
|||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/Dropdown";
|
||||
import LicenseBanner from "@ee/components/LicenseBanner";
|
||||
import TrialBanner from "@ee/components/TrialBanner";
|
||||
import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem";
|
||||
|
@ -32,12 +38,6 @@ import { trpc } from "@lib/trpc";
|
|||
import CustomBranding from "@components/CustomBranding";
|
||||
import Loader from "@components/Loader";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/ui/Dropdown";
|
||||
|
||||
import pkg from "../package.json";
|
||||
import { useViewerI18n } from "./I18nLanguageHandler";
|
||||
|
|
|
@ -7,6 +7,13 @@ import { useForm } from "react-hook-form";
|
|||
import type { z } from "zod";
|
||||
|
||||
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 { 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 Avatar from "@components/ui/Avatar";
|
||||
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";
|
||||
|
||||
// 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
|
||||
name?: string | null;
|
||||
slug?: string | null;
|
||||
|
@ -56,12 +56,23 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
: undefined;
|
||||
const pageSlug = router.query.eventPage || props.options[0].slug;
|
||||
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>>({
|
||||
resolver: zodResolver(createEventTypeInput),
|
||||
defaultValues: { length: 15 },
|
||||
});
|
||||
const { setValue, watch, register } = form;
|
||||
setValue("title", title);
|
||||
setValue("length", length);
|
||||
setValue("description", description);
|
||||
setValue("slug", slug);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch((value, { name, type }) => {
|
||||
|
@ -113,7 +124,9 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog name="new-eventtype" clearQueryParamsOnClose={["eventPage", "teamId"]}>
|
||||
<Dialog
|
||||
name="new-eventtype"
|
||||
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
|
||||
{!hasTeams || props.isIndividualTeam ? (
|
||||
<Button
|
||||
onClick={() => openModal(props.options[0])}
|
||||
|
@ -196,7 +209,6 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
required
|
||||
min="10"
|
||||
placeholder="15"
|
||||
defaultValue={15}
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length", { valueAsNumber: true })}
|
||||
|
@ -222,11 +234,17 @@ export default function CreateEventTypeButton(props: Props) {
|
|||
{...register("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">
|
||||
<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>
|
||||
<p>{t("collective_description")}</p>
|
||||
</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>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
|
|
|
@ -3,6 +3,12 @@ import { ClockIcon, ExternalLinkIcon, DotsHorizontalIcon } from "@heroicons/reac
|
|||
import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/Dropdown";
|
||||
import TeamAvailabilityModal from "@ee/components/team/availability/TeamAvailabilityModal";
|
||||
|
||||
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
||||
|
@ -17,12 +23,6 @@ import Avatar from "@components/ui/Avatar";
|
|||
import Button from "@components/ui/Button";
|
||||
import ModalContainer from "@components/ui/ModalContainer";
|
||||
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/Dropdown";
|
||||
import MemberChangeRoleModal from "./MemberChangeRoleModal";
|
||||
import TeamPill, { TeamRole } from "./TeamPill";
|
||||
import { MembershipRole } from ".prisma/client";
|
||||
|
|
|
@ -2,6 +2,13 @@ import { ExternalLinkIcon, TrashIcon, LogoutIcon, PencilIcon } from "@heroicons/
|
|||
import { LinkIcon, DotsHorizontalIcon } from "@heroicons/react/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@calcom/ui/Dropdown";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
@ -13,12 +20,6 @@ import { Tooltip } from "@components/Tooltip";
|
|||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import Button from "@components/ui/Button";
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@components/ui/Dropdown";
|
||||
|
||||
import { TeamRole } from "./TeamPill";
|
||||
import { MembershipRole } from ".prisma/client";
|
||||
|
@ -126,8 +127,9 @@ export default function TeamListItem(props: Props) {
|
|||
<a>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
color="minimal"
|
||||
className="w-full font-normal"
|
||||
className="w-full rounded-none font-normal"
|
||||
StartIcon={PencilIcon}>
|
||||
{t("edit_team")}
|
||||
</Button>
|
||||
|
@ -135,14 +137,14 @@ export default function TeamListItem(props: Props) {
|
|||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && <DropdownMenuSeparator className="h-px bg-gray-200" />}
|
||||
<DropdownMenuItem>
|
||||
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${team.slug}`} passHref={true}>
|
||||
<a target="_blank">
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
color="minimal"
|
||||
className="w-full font-normal"
|
||||
className="w-full rounded-none font-normal"
|
||||
StartIcon={ExternalLinkIcon}>
|
||||
{" "}
|
||||
{t("preview_team")}
|
||||
|
@ -160,8 +162,9 @@ export default function TeamListItem(props: Props) {
|
|||
e.stopPropagation();
|
||||
}}
|
||||
color="warn"
|
||||
size="lg"
|
||||
StartIcon={TrashIcon}
|
||||
className="w-full font-normal">
|
||||
className="w-full rounded-none font-normal">
|
||||
{t("disband_team")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
@ -183,8 +186,9 @@ export default function TeamListItem(props: Props) {
|
|||
<Button
|
||||
type="button"
|
||||
color="warn"
|
||||
size="lg"
|
||||
StartIcon={LogoutIcon}
|
||||
className="w-full"
|
||||
className="w-full rounded-none"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { DotsHorizontalIcon } from "@heroicons/react/solid";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@calcom/ui/Dropdown";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
||||
|
||||
import Dropdown, {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@components/ui/Dropdown";
|
||||
|
||||
import Button from "./Button";
|
||||
|
||||
export type ActionType = {
|
||||
|
@ -52,8 +48,9 @@ const TableActions: FC<Props> = ({ actions }) => {
|
|||
<DropdownMenuItem key={action.id}>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
color="minimal"
|
||||
className="w-full font-normal"
|
||||
className="w-full rounded-none font-normal"
|
||||
href={action.href}
|
||||
StartIcon={action.icon}
|
||||
onClick={action.onClick}>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { ChatAltIcon } from "@heroicons/react/solid";
|
||||
import { useIntercom } from "react-use-intercom";
|
||||
|
||||
import { DropdownMenuItem } from "@calcom/ui/Dropdown";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { DropdownMenuItem } from "@components/ui/Dropdown";
|
||||
|
||||
export default function IntercomMenuItem() {
|
||||
const { t } = useLocale();
|
||||
const { boot, show } = useIntercom();
|
||||
|
|
|
@ -2,11 +2,11 @@ import { ChatAltIcon } from "@heroicons/react/solid";
|
|||
import Script from "next/script";
|
||||
import { useState } from "react";
|
||||
|
||||
import { DropdownMenuItem } from "@calcom/ui/Dropdown";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { DropdownMenuItem } from "@components/ui/Dropdown";
|
||||
|
||||
const ZENDESK_KEY = process.env.NEXT_PUBLIC_ZENDESK_KEY;
|
||||
|
||||
export default function ZendeskMenuItem() {
|
||||
|
|
|
@ -3,72 +3,72 @@ import {
|
|||
ArrowUpIcon,
|
||||
DotsHorizontalIcon,
|
||||
ExternalLinkIcon,
|
||||
DuplicateIcon,
|
||||
LinkIcon,
|
||||
UsersIcon,
|
||||
UploadIcon,
|
||||
ClipboardCopyIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { UsersIcon } from "@heroicons/react/solid";
|
||||
import { Trans } from "next-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@calcom/ui";
|
||||
import Dropdown, {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
} from "@calcom/ui/Dropdown";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import classNames from "@lib/classNames";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import showToast from "@lib/notification";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||
import Shell from "@components/Shell";
|
||||
import { Tooltip } from "@components/Tooltip";
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
|
||||
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||
import Badge from "@components/ui/Badge";
|
||||
import Dropdown, {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@components/ui/Dropdown";
|
||||
import UserCalendarIllustration from "@components/ui/svg/UserCalendarIllustration";
|
||||
|
||||
type Profiles = inferQueryOutput<"viewer.eventTypes">["profiles"];
|
||||
type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"];
|
||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||
|
||||
interface CreateEventTypeProps {
|
||||
canAddEvents: boolean;
|
||||
profiles: Profiles;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"];
|
||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||
interface EventTypeListHeadingProps {
|
||||
profile: EventTypeGroupProfile;
|
||||
membershipCount: number;
|
||||
}
|
||||
|
||||
type EventTypeGroup = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"][number];
|
||||
type EventType = EventTypeGroup["eventTypes"][number];
|
||||
interface EventTypeListProps {
|
||||
profile: { slug: string | null };
|
||||
group: EventTypeGroup;
|
||||
readOnly: boolean;
|
||||
types: EventType[];
|
||||
}
|
||||
const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.Element => {
|
||||
|
||||
export const EventTypeList = ({ group, readOnly, types }: EventTypeListProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -143,8 +187,16 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
className="flex-grow truncate text-sm"
|
||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||
<div>
|
||||
<span className="truncate font-medium text-neutral-900">{type.title} </span>
|
||||
<small className="hidden text-neutral-500 sm:inline">{`/${profile.slug}/${type.slug}`}</small>
|
||||
<span
|
||||
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 && (
|
||||
<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")}
|
||||
|
@ -161,7 +213,7 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
</Link>
|
||||
|
||||
<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 && (
|
||||
<AvatarGroup
|
||||
border="border-2 border-white"
|
||||
|
@ -175,7 +227,7 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
)}
|
||||
<Tooltip content={t("preview")}>
|
||||
<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"
|
||||
rel="noreferrer"
|
||||
className="btn-icon appearance-none">
|
||||
|
@ -188,13 +240,74 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
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">
|
||||
<LinkIcon className="h-5 w-5 group-hover:text-black" />
|
||||
</button>
|
||||
</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>
|
||||
|
@ -205,9 +318,13 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent portalled>
|
||||
<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">
|
||||
<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")}
|
||||
</Button>
|
||||
</a>
|
||||
|
@ -217,12 +334,13 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
<Button
|
||||
type="button"
|
||||
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}
|
||||
StartIcon={ClipboardCopyIcon}
|
||||
onClick={() => {
|
||||
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");
|
||||
}}>
|
||||
|
@ -234,7 +352,8 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
className="w-full font-normal"
|
||||
size="lg"
|
||||
className="w-full rounded-none font-normal"
|
||||
data-testid={"event-type-duplicate-" + type.id}
|
||||
StartIcon={UploadIcon}
|
||||
onClick={() => {
|
||||
|
@ -242,7 +361,7 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
.share({
|
||||
title: t("share"),
|
||||
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"))
|
||||
.catch(() => showToast(t("failed"), "error"));
|
||||
|
@ -251,6 +370,58 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
</Button>
|
||||
</DropdownMenuItem>
|
||||
) : 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>
|
||||
</Dropdown>
|
||||
</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 => (
|
||||
<div className="mb-4 flex">
|
||||
<Link href="/settings/teams">
|
||||
|
@ -306,6 +473,21 @@ const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeading
|
|||
</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 { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.eventTypes"]);
|
||||
|
@ -357,11 +539,7 @@ const EventTypesPage = () => {
|
|||
membershipCount={group.metadata.membershipCount}
|
||||
/>
|
||||
)}
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
profile={group.profile}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
<EventTypeList types={group.eventTypes} group={group} readOnly={group.metadata.readOnly} />
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
value: locale,
|
||||
// FIXME
|
||||
// @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]);
|
||||
|
||||
|
|
|
@ -39,6 +39,27 @@ test.describe("pro user", () => {
|
|||
|
||||
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", () => {
|
||||
|
|
|
@ -449,7 +449,7 @@
|
|||
"continue": "Continue",
|
||||
"confirm": "Confirm",
|
||||
"disband_team": "Disband Team",
|
||||
"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.",
|
||||
"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?",
|
||||
"confirm_disband_team": "Yes, disband team",
|
||||
"confirm_remove_member": "Yes, remove member",
|
||||
|
@ -674,5 +674,6 @@
|
|||
"example_name": "John Doe",
|
||||
"time_format": "Time format",
|
||||
"12_hour": "12 hour",
|
||||
"24_hour": "24 hour"
|
||||
"24_hour": "24 hour",
|
||||
"duplicate": "Duplicate"
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ export const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTri
|
|||
className={
|
||||
props.asChild
|
||||
? 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}
|
||||
/>
|
||||
|
@ -20,6 +20,8 @@ export const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTri
|
|||
);
|
||||
DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
|
||||
|
||||
export const DropdownMenuTriggerItem = DropdownMenuPrimitive.TriggerItem;
|
||||
|
||||
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuPrimitive["Content"]>;
|
||||
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
||||
({ children, ...props }, forwardedRef) => {
|
||||
|
@ -27,9 +29,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
|||
<DropdownMenuPrimitive.Content
|
||||
portalled={props.portalled}
|
||||
{...props}
|
||||
className={`${
|
||||
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`}
|
||||
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`}
|
||||
ref={forwardedRef}>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.Content>
|
Loading…
Reference in a new issue