diff --git a/apps/web/modules/event-types/views/event-types-listing-view.tsx b/apps/web/modules/event-types/views/event-types-listing-view.tsx index 9260449bb7..67b32999ac 100644 --- a/apps/web/modules/event-types/views/event-types-listing-view.tsx +++ b/apps/web/modules/event-types/views/event-types-listing-view.tsx @@ -3,10 +3,7 @@ import { Dialog } from "@calcom/features/components/controlled-dialog"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants"; -import { - extractHostTimezone, - filterActiveLinks, -} from "@calcom/lib/hashedLinksUtils"; +import { extractHostTimezone, filterActiveLinks } from "@calcom/lib/hashedLinksUtils"; import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver"; @@ -63,17 +60,13 @@ import type { FC } from "react"; import { createContext, memo, useContext, useEffect, useState } from "react"; import { z } from "zod"; -type GetUserEventGroupsResponse = - RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]; -type GetEventTypesFromGroupsResponse = - RouterOutputs["viewer"]["eventTypes"]["getEventTypesFromGroup"]; +type GetUserEventGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]; +type GetEventTypesFromGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getEventTypesFromGroup"]; -type InfiniteEventTypeGroup = - GetUserEventGroupsResponse["eventTypeGroups"][number]; +type InfiniteEventTypeGroup = GetUserEventGroupsResponse["eventTypeGroups"][number]; type InfiniteEventType = GetEventTypesFromGroupsResponse["eventTypes"][number]; -type EventTypeGroups = - RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"]; +type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"]; type EventTypeGroup = EventTypeGroups[number]; type EventType = EventTypeGroup["eventTypes"][number]; @@ -86,8 +79,9 @@ interface SearchContextType { debouncedSearchTerm: string; } -const SearchContextInternal: React.Context = - createContext(undefined); +const SearchContextInternal: React.Context = createContext< + SearchContextType | undefined +>(undefined); const useSearchContext = (): SearchContextType => { const context = useContext(SearchContextInternal); @@ -120,9 +114,7 @@ const querySchema = z.object({ teamId: z.nullable(z.coerce.number()).optional().default(null), }); -const InfiniteTeamsTab: FC = ( - props: InfiniteTeamsTabProps -) => { +const InfiniteTeamsTab: FC = (props: InfiniteTeamsTabProps) => { const { activeEventTypeGroup } = props; const { debouncedSearchTerm } = useSearchContext(); const { t } = useLocale(); @@ -140,8 +132,7 @@ const InfiniteTeamsTab: FC = ( refetchOnWindowFocus: true, refetchOnMount: true, staleTime: 0, - getNextPageParam: (lastPage: { nextCursor: number | null | undefined }) => - lastPage.nextCursor, + getNextPageParam: (lastPage: { nextCursor: number | null | undefined }) => lastPage.nextCursor, } ); @@ -163,20 +154,18 @@ const InfiniteTeamsTab: FC = ( debouncedSearchTerm={debouncedSearchTerm} /> )} - {(query.data?.pages?.[0]?.eventTypes?.length ?? 0) > 0 && - query.hasNextPage && ( -
- -
- )} + {(query.data?.pages?.[0]?.eventTypes?.length ?? 0) > 0 && query.hasNextPage && ( +
+ +
+ )} ); }; @@ -195,53 +184,47 @@ const Item = ({ const hasDarkTheme = !forcedTheme && resolvedTheme === "dark"; const parsedeventTypeColor = parseEventTypeColor(type.eventTypeColor); const eventTypeColor = - parsedeventTypeColor && - parsedeventTypeColor[ - hasDarkTheme ? "darkEventTypeColor" : "lightEventTypeColor" - ]; + parsedeventTypeColor && parsedeventTypeColor[hasDarkTheme ? "darkEventTypeColor" : "lightEventTypeColor"]; const isManagedEventType = type.schedulingType === SchedulingType.MANAGED; + const isRoundRobinOrCollective = + type.schedulingType === SchedulingType.ROUND_ROBIN || type.schedulingType === SchedulingType.COLLECTIVE; + const isCurrentUserHost = "isCurrentUserHost" in type && type.isCurrentUserHost; + const showAssignedBadge = isRoundRobinOrCollective && isCurrentUserHost; const content = (): JSX.Element => (
+ data-testid={`event-type-title-${type.id}`}> {type.title} {group.profile.slug && type.schedulingType !== SchedulingType.MANAGED ? ( + data-testid={`event-type-slug-${type.id}`}> {`/${group.profile.slug}/${type.slug}`} ) : null} {!isManagedEventType && type.hidden && ( - - {t("hidden")} - + {t("hidden")} )} {readOnly && ( {t("readonly")} )} + {showAssignedBadge && ( + + {t("assigned")} + + )}
); return ( -
+
{eventTypeColor && ( -
+
)}
{readOnly ? ( @@ -250,40 +233,33 @@ const Item = ({
) : ( - +
+ data-testid={`event-type-title-${type.id}`}> {type.title} - {group.profile.slug && - type.schedulingType !== SchedulingType.MANAGED ? ( + {group.profile.slug && type.schedulingType !== SchedulingType.MANAGED ? ( + data-testid={`event-type-slug-${type.id}`}> {`/${group.profile.slug}/${type.slug}`} ) : null} {!isManagedEventType && type.hidden && ( - - {t("hidden")} - + {t("hidden")} )} {readOnly && ( - + {t("readonly")} )} + {showAssignedBadge && ( + + {t("assigned")} + + )}
(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0); - const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = - useState(null); - const [privateLinkCopyIndices, setPrivateLinkCopyIndices] = useState< - Record - >({}); + const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState( + null + ); + const [privateLinkCopyIndices, setPrivateLinkCopyIndices] = useState>({}); const utils = trpc.useUtils(); const mutation = trpc.viewer.loggedInViewerRouter.eventTypeOrder.useMutation({ @@ -336,12 +311,11 @@ export const InfiniteEventTypeList = ({ const setHiddenMutation = trpc.viewer.eventTypesHeavy.update.useMutation({ onMutate: async (data: { id: number; hidden?: boolean }) => { await utils.viewer.eventTypes.getEventTypesFromGroup.cancel(); - const previousValue = - utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ - limit: LIMIT, - searchQuery: debouncedSearchTerm, - group: { teamId: group?.teamId, parentId: group?.parentId }, - }); + const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ + limit: LIMIT, + searchQuery: debouncedSearchTerm, + group: { teamId: group?.teamId, parentId: group?.parentId }, + }); if (previousValue) { await utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData( @@ -362,9 +336,7 @@ export const InfiniteEventTypeList = ({ pages: oldData.pages.map((page) => ({ ...page, eventTypes: page.eventTypes.map((eventType) => - eventType.id === data.id - ? { ...eventType, hidden: !eventType.hidden } - : eventType + eventType.id === data.id ? { ...eventType, hidden: !eventType.hidden } : eventType ), })), }; @@ -389,10 +361,7 @@ export const InfiniteEventTypeList = ({ }, }); - async function moveEventType( - index: number, - increment: 1 | -1 - ): Promise { + async function moveEventType(index: number, increment: 1 | -1): Promise { if (!pages) return; const newOrder = pages; const pageNo = Math.floor(index / LIMIT); @@ -405,8 +374,8 @@ export const InfiniteEventTypeList = ({ ? pageNo - 1 : pageNo : index % LIMIT === LIMIT - 1 - ? pageNo + 1 - : pageNo; + ? pageNo + 1 + : pageNo; const newIdx = (index + increment) % LIMIT; const newPositionEventType = newOrder[newPageNo].eventTypes[newIdx]; @@ -415,12 +384,11 @@ export const InfiniteEventTypeList = ({ newOrder[newPageNo].eventTypes[newIdx] = currentPositionEventType; await utils.viewer.eventTypes.getEventTypesFromGroup.cancel(); - const previousValue = - utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ - limit: LIMIT, - searchQuery: debouncedSearchTerm, - group: { teamId: group?.teamId, parentId: group?.parentId }, - }); + const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ + limit: LIMIT, + searchQuery: debouncedSearchTerm, + group: { teamId: group?.teamId, parentId: group?.parentId }, + }); if (previousValue) { utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData( @@ -454,17 +422,9 @@ export const InfiniteEventTypeList = ({ } // inject selection data into url for correct router history - const openDuplicateModal = ( - eventType: InfiniteEventType, - group: InfiniteEventTypeGroup - ): void => { - const newSearchParams = new URLSearchParams( - searchParams?.toString() ?? undefined - ); - function setParamsIfDefined( - key: string, - value: string | number | boolean | null | undefined - ) { + const openDuplicateModal = (eventType: InfiniteEventType, group: InfiniteEventTypeGroup): void => { + const newSearchParams = new URLSearchParams(searchParams?.toString() ?? undefined); + function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) { if (value) newSearchParams.set(key, value.toString()); if (value === null) newSearchParams.delete(key); } @@ -485,12 +445,11 @@ export const InfiniteEventTypeList = ({ }, onMutate: async ({ id }) => { await utils.viewer.eventTypes.getEventTypesFromGroup.cancel(); - const previousValue = - utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ - limit: LIMIT, - searchQuery: debouncedSearchTerm, - group: { teamId: group?.teamId, parentId: group?.parentId }, - }); + const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ + limit: LIMIT, + searchQuery: debouncedSearchTerm, + group: { teamId: group?.teamId, parentId: group?.parentId }, + }); if (previousValue) { await utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData( @@ -554,24 +513,16 @@ export const InfiniteEventTypeList = ({ return group.teamId ? ( ) : !group.profile.eventTypesLockedByOrg ? ( - + ) : ( <> ); } const firstItem = pages?.[0]?.eventTypes[0]; - const lastItem = - pages?.[pages.length - 1]?.eventTypes[ - pages?.[pages.length - 1].eventTypes.length - 1 - ]; + const lastItem = pages?.[pages.length - 1]?.eventTypes[pages?.[pages.length - 1].eventTypes.length - 1]; const isManagedEventPrefix = () => { - return deleteDialogTypeSchedulingType === SchedulingType.MANAGED - ? "_managed" - : ""; + return deleteDialogTypeSchedulingType === SchedulingType.MANAGED ? "_managed" : ""; }; const userTimezone = extractHostTimezone({ @@ -584,35 +535,23 @@ export const InfiniteEventTypeList = ({ return (
-
    +
      {pages.map((page, pageIdx) => { return page?.eventTypes?.map((type, index) => { const embedLink = `${group.profile.slug}/${type.slug}`; const calLink = `${bookerUrl}/${embedLink}`; - const activeHashedLinks = type.hashedLink - ? filterActiveLinks(type.hashedLink, userTimezone) - : []; + const activeHashedLinks = type.hashedLink ? filterActiveLinks(type.hashedLink, userTimezone) : []; // Ensure index is within bounds for active links const currentIndex = privateLinkCopyIndices[type.slug] ?? 0; - const safeIndex = - activeHashedLinks.length > 0 - ? currentIndex % activeHashedLinks.length - : 0; + const safeIndex = activeHashedLinks.length > 0 ? currentIndex % activeHashedLinks.length : 0; const isPrivateURLEnabled = - activeHashedLinks.length > 0 - ? activeHashedLinks[safeIndex]?.link - : ""; + activeHashedLinks.length > 0 ? activeHashedLinks[safeIndex]?.link : ""; const placeholderHashedLink = `${bookerUrl}/d/${isPrivateURLEnabled}/${type.slug}`; - const isManagedEventType = - type.schedulingType === SchedulingType.MANAGED; + const isManagedEventType = type.schedulingType === SchedulingType.MANAGED; const isChildrenManagedEventType = type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED; @@ -622,26 +561,18 @@ export const InfiniteEventTypeList = ({
      {!(firstItem && firstItem.id === type.id) && ( - moveEventType(LIMIT * pageIdx + index, -1) - } + onClick={() => moveEventType(LIMIT * pageIdx + index, -1)} arrowDirection="up" /> )} {!(lastItem && lastItem.id === type.id) && ( - moveEventType(LIMIT * pageIdx + index, 1) - } + onClick={() => moveEventType(LIMIT * pageIdx + index, 1)} arrowDirection="down" /> )} - +
      {!!type.teamId && !isManagedEventType && ( @@ -653,34 +584,23 @@ export const InfiniteEventTypeList = ({ users={type?.users ?? []} /> )} - {isManagedEventType && - type?.children && - type.children?.length > 0 && ( - ch.users) ?? [] - } - /> - )} + {isManagedEventType && type?.children && type.children?.length > 0 && ( + ch.users) ?? []} + /> + )}
      {!isManagedEventType && ( <> - {type.hidden && ( - - {t("hidden")} - - )} + {type.hidden && {t("hidden")}} + type.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile") + }>
      {isPrivateURLEnabled && ( - +
      - -
      ); @@ -1101,18 +956,13 @@ const EmptyEventTypeList = ({ <> + variant="button"> {t("create")} } @@ -1141,8 +991,7 @@ const InfiniteScrollMain = ({ })); const activeEventTypeGroup = - eventTypeGroups.filter((item) => item.teamId === data.teamId) ?? - eventTypeGroups[0]; + eventTypeGroups.filter((item) => item.teamId === data.teamId) ?? eventTypeGroups[0]; const bookerUrl = orgBranding ? orgBranding?.fullDomain : WEBSITE_URL; @@ -1151,8 +1000,7 @@ const InfiniteScrollMain = ({ // This keeps the app working for personal event types that were not migrated to the org (rare) if ( orgBranding && - (activeEventTypeGroup[0].teamId === orgBranding.id || - activeEventTypeGroup[0].parentId === orgBranding.id) + (activeEventTypeGroup[0].teamId === orgBranding.id || activeEventTypeGroup[0].parentId === orgBranding.id) ) { activeEventTypeGroup[0].bookerUrl = bookerUrl; } @@ -1160,12 +1008,8 @@ const InfiniteScrollMain = ({ return ( <> {eventTypeGroups.length > 1 && } - {eventTypeGroups.length >= 1 && ( - - )} - {eventTypeGroups.length === 0 && ( - - )} + {eventTypeGroups.length >= 1 && } + {eventTypeGroups.length === 0 && } {searchParams?.get("dialog") === "duplicate" && } @@ -1200,8 +1044,7 @@ export const EventTypesCTA = ({ userEventGroupsData }: Omit) => { // Fallback: allow admin and owner roles return ( - profile.membershipRole === MembershipRole.ADMIN || - profile.membershipRole === MembershipRole.OWNER + profile.membershipRole === MembershipRole.ADMIN || profile.membershipRole === MembershipRole.OWNER ); }) ?.map((profile) => { @@ -1244,9 +1087,7 @@ const EventTypesPage = ({ userEventGroupsData, user }: Props) => { useEffect(() => { setShowProfileBanner( - !!orgBranding && - !document.cookie.includes("calcom-profile-banner=1") && - !user?.completedOnboarding + !!orgBranding && !document.cookie.includes("calcom-profile-banner=1") && !user?.completedOnboarding ); }, [orgBranding, user]); @@ -1258,7 +1099,6 @@ const EventTypesPage = ({ userEventGroupsData, user }: Props) => { ); }; -export const SearchContext: React.Context = - SearchContextInternal; +export const SearchContext: React.Context = SearchContextInternal; export default EventTypesPage; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index fcdc035e67..bb86202136 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -987,6 +987,7 @@ "url": "URL", "hidden": "Hidden", "readonly": "Read only", + "assigned": "Assigned", "one_time_link": "One-time link", "plan_description": "You're currently on the {{plan}} plan.", "plan_upgrade_invitation": "Upgrade your account to the PRO plan to unlock all of the features we have to offer.", diff --git a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts index d5a55ac4db..34b001830b 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts @@ -2,10 +2,9 @@ import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/ev import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import logger from "@calcom/lib/logger"; -import { prisma } from "@calcom/prisma"; import type { PrismaClient } from "@calcom/prisma"; +import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; - import type { TrpcSessionUser } from "../../../types"; import type { TGetEventTypesFromGroupSchema } from "./getByViewer.schema"; import { mapEventType } from "./util"; @@ -22,12 +21,13 @@ type GetByViewerOptions = { type EventType = Awaited>[number]; type MappedEventType = Awaited>; +type MappedEventTypeWithHostFlag = MappedEventType & { isCurrentUserHost: boolean }; export const getEventTypesFromGroup = async ({ ctx, input, }: GetByViewerOptions): Promise<{ - eventTypes: MappedEventType[]; + eventTypes: MappedEventTypeWithHostFlag[]; nextCursor: number | null | undefined; }> => { await checkRateLimitAndThrowError({ @@ -144,7 +144,7 @@ export const getEventTypesFromGroup = async ({ eventTypes.push(...teamEventTypes); } - let nextCursor: number | null | undefined = undefined; + let nextCursor: number | null | undefined; if (eventTypes.length > limit) { const nextItem = eventTypes.pop(); nextCursor = nextItem?.id; @@ -152,6 +152,12 @@ export const getEventTypesFromGroup = async ({ const mappedEventTypes: MappedEventType[] = await Promise.all(eventTypes.map(mapEventType)); + // Add isCurrentUserHost flag to each event type + const eventTypesWithHostFlag = mappedEventTypes.map((eventType) => ({ + ...eventType, + isCurrentUserHost: eventType.hosts?.some((host) => host.user.id === ctx.user.id) ?? false, + })); + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, @@ -169,11 +175,11 @@ export const getEventTypesFromGroup = async ({ }); if (membership && membership.team.isPrivate) - mappedEventTypes.forEach((evType) => { + eventTypesWithHostFlag.forEach((evType) => { evType.users = []; evType.hosts = []; evType.children = []; }); - return { eventTypes: mappedEventTypes, nextCursor: nextCursor ?? undefined }; + return { eventTypes: eventTypesWithHostFlag, nextCursor: nextCursor ?? undefined }; };