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) && ( )} {!(lastItem && lastItem.id === type.id) && ( )}
    {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 }); }} />
    {isNativeShare ? ( ) : null}
  • ); })}
{ 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;