import { useAutoAnimate } from "@formkit/auto-animate/react";
import { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react";
import {
CreateEventTypeButton,
EventTypeDescriptionLazy as EventTypeDescription,
} from "@calcom/features/eventtypes/components";
import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Badge,
Button,
ButtonGroup,
ConfirmationDialogContent,
Dialog,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyScreen,
showToast,
Switch,
Avatar,
AvatarGroup,
Tooltip,
} from "@calcom/ui";
import {
FiArrowDown,
FiArrowUp,
FiClipboard,
FiCode,
FiCopy,
FiEdit,
FiEdit2,
FiExternalLink,
FiLink,
FiMoreHorizontal,
FiTrash,
FiUpload,
FiUsers,
} from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import { ssrInit } from "@server/lib/ssr";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
interface EventTypeListHeadingProps {
profile: EventTypeGroupProfile;
membershipCount: number;
teamId?: number | null;
}
type EventTypeGroup = EventTypeGroups[number];
type EventType = EventTypeGroup["eventTypes"][number];
interface EventTypeListProps {
group: EventTypeGroup;
groupIndex: number;
readOnly: boolean;
types: EventType[];
}
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
const { t } = useLocale();
return (
{type.title}
{`/${group.profile.slug}/${type.slug}`}
{readOnly && (
{t("readonly")}
)}
);
};
const MemoizedItem = React.memo(Item);
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const [parent] = useAutoAnimate();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
const utils = trpc.useContext();
const mutation = trpc.viewer.eventTypeOrder.useMutation({
onError: async (err) => {
console.error(err.message);
await utils.viewer.eventTypes.getByViewer.cancel();
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
await utils.viewer.eventTypes.invalidate();
},
onSettled: () => {
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
utils.viewer.eventTypes.invalidate();
},
});
const setHiddenMutation = trpc.viewer.eventTypes.update.useMutation({
onMutate: async ({ id }) => {
await utils.viewer.eventTypes.getByViewer.cancel();
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
if (previousValue) {
const newList = [...types];
const itemIndex = newList.findIndex((item) => item.id === id);
if (itemIndex !== -1 && newList[itemIndex]) {
newList[itemIndex].hidden = !newList[itemIndex].hidden;
}
utils.viewer.eventTypes.getByViewer.setData(undefined, {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
});
}
return { previousValue };
},
onError: async (err, _, context) => {
if (context?.previousValue) {
utils.viewer.eventTypes.getByViewer.setData(undefined, context.previousValue);
}
console.error(err.message);
},
onSettled: () => {
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
utils.viewer.eventTypes.invalidate();
},
});
async function moveEventType(index: number, increment: 1 | -1) {
const newList = [...types];
const type = types[index];
const tmp = types[index + increment];
if (tmp) {
newList[index] = tmp;
newList[index + increment] = type;
}
await utils.viewer.eventTypes.getByViewer.cancel();
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
if (previousValue) {
utils.viewer.eventTypes.getByViewer.setData(undefined, {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
});
}
mutation.mutate({
ids: newList.map((type) => type.id),
});
}
async function deleteEventTypeHandler(id: number) {
const payload = { id };
deleteMutation.mutate(payload);
}
// inject selection data into url for correct router history
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
const query = {
...router.query,
dialog: "duplicate-event-type",
title: eventType.title,
description: eventType.description,
slug: eventType.slug,
id: eventType.id,
length: eventType.length,
pageSlug: group.profile.slug,
};
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
};
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: () => {
showToast(t("event_type_deleted_successfully"), "success");
setDeleteDialogOpen(false);
},
onMutate: async ({ id }) => {
await utils.viewer.eventTypes.getByViewer.cancel();
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
if (previousValue) {
const newList = types.filter((item) => item.id !== id);
utils.viewer.eventTypes.getByViewer.setData(undefined, {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
});
}
return { previousValue };
},
onError: (err, _, context) => {
if (context?.previousValue) {
utils.viewer.eventTypes.getByViewer.setData(undefined, context.previousValue);
}
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
setDeleteDialogOpen(false);
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
onSettled: () => {
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
utils.viewer.eventTypes.invalidate();
},
});
const [isNativeShare, setNativeShare] = useState(true);
useEffect(() => {
if (!navigator.share) {
setNativeShare(false);
}
}, []);
const firstItem = types[0];
const lastItem = types[types.length - 1];
return (
{types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${CAL_URL}/${embedLink}`;
return (
{!(firstItem && firstItem.id === type.id) && (
moveEventType(index, -1)}>
)}
{!(lastItem && lastItem.id === type.id) && (
moveEventType(index, 1)}>
)}
{type.users?.length > 1 && (
({
alt: organizer.name || "",
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
title: organizer.name || "",
}))}
/>
)}
{type.hidden && (
{t("hidden")}
)}
{
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
}}
/>
{
showToast(t("link_copied"), "success");
navigator.clipboard.writeText(calLink);
}}
/>
router.push("/event-types/" + type.id)}>
{t("edit")}
openDuplicateModal(type, group)}>
{t("duplicate")}
{t("embed")}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
{
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
}}
StartIcon={FiTrash}
className="w-full rounded-none">
{t("delete")}
)}
{t("preview")}
{
navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success");
}}>
{t("copy_link")}
{isNativeShare ? (
{
navigator
.share({
title: t("share"),
text: t("share_event", { appName: APP_NAME }),
url: calLink,
})
.then(() => showToast(t("link_shared"), "success"))
.catch(() => showToast(t("failed"), "error"));
}}>
{t("share")}
) : null}
router.push("/event-types/" + type.id)}
color="minimal"
className="w-full rounded-none"
StartIcon={FiEdit}>
{t("edit")}
openDuplicateModal(type, group)}>
{t("duplicate")}
{
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
}}
color="destructive"
StartIcon={FiTrash}
className="w-full rounded-none">
{t("delete")}
);
})}
{
e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId);
}}>
{t("delete_event_type_description")}
);
};
const EventTypeListHeading = ({
profile,
membershipCount,
teamId,
}: EventTypeListHeadingProps): JSX.Element => {
return (
{profile?.name || ""}
{membershipCount && teamId && (
{membershipCount}
)}
{profile?.slug && (
{`${CAL_URL?.replace("https://", "")}/${profile.slug}`}
)}
);
};
const CreateFirstEventTypeView = () => {
const { t } = useLocale();
return (
);
};
const CTA = () => {
const query = trpc.viewer.eventTypes.getByViewer.useQuery();
if (!query.data) return null;
return ;
};
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
const EventTypesPage = () => {
const { t } = useLocale();
return (
}>
}
success={({ data }) => (
<>
{data.eventTypeGroups.map((group, index) => (
{/* hide list heading when there is only one (current user) */}
{(data.eventTypeGroups.length !== 1 || group.teamId) && (
)}
))}
{data.eventTypeGroups.length === 0 && }
>
)}
/>
);
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default EventTypesPage;