Compare commits

...

1 Commits

Author SHA1 Message Date
Hariom dac9e17ccf Across org boundary fixes and tests 2023-12-05 16:54:37 +05:30
35 changed files with 570 additions and 154 deletions

View File

@ -44,6 +44,7 @@ export const mapMemberToChildrenOption = (
username: member.username ?? "", username: member.username ?? "",
membership: member.membership, membership: member.membership,
eventTypeSlugs: member.eventTypes ?? [], eventTypeSlugs: member.eventTypes ?? [],
avatar: member.avatar,
}, },
value: `${member.id ?? ""}`, value: `${member.id ?? ""}`,
label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`, label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,

View File

@ -7,11 +7,9 @@ import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form"; import type { UseFormReturn } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed"; import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums"; import { SchedulingType } from "@calcom/prisma/enums";
@ -67,6 +65,7 @@ type Props = {
isUpdateMutationLoading?: boolean; isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption; availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean; isUserOrganizationAdmin: boolean;
bookerUrl: string;
}; };
function getNavigation(props: { function getNavigation(props: {
@ -135,6 +134,7 @@ function EventTypeSingleLayout({
formMethods, formMethods,
availability, availability,
isUserOrganizationAdmin, isUserOrganizationAdmin,
bookerUrl,
}: Props) { }: Props) {
const utils = trpc.useContext(); const utils = trpc.useContext();
const { t } = useLocale(); const { t } = useLocale();
@ -235,10 +235,8 @@ function EventTypeSingleLayout({
formMethods, formMethods,
]); ]);
const orgBranding = useOrgBranding(); const permalink = `${bookerUrl}/${
const isOrgEvent = orgBranding?.fullDomain; team ? `${!team.parentId ? "team/" : ""}${team.slug}` : eventType.users[0].username
const permalink = `${orgBranding?.fullDomain ?? CAL_URL}/${
team ? `${!isOrgEvent ? "team/" : ""}${team.slug}` : eventType.users[0].username
}/${eventType.slug}`; }/${eventType.slug}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`; const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;

View File

@ -12,7 +12,7 @@ type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
type MembersType = TeamType["members"]; type MembersType = TeamType["members"];
type MemberType = Pick<MembersType[number], "id" | "name" | "bio" | "username" | "organizationId"> & { type MemberType = Pick<MembersType[number], "id" | "name" | "bio" | "username" | "organizationId"> & {
safeBio: string | null; safeBio: string | null;
orgOrigin: string; bookerUrl: string;
}; };
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => { const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
@ -26,7 +26,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
return ( return (
<Link <Link
key={member.id} key={member.id}
href={{ pathname: `${member.orgOrigin}/${member.username}`, query: queryParamsToForward }}> href={{ pathname: `${member.bookerUrl}/${member.username}`, query: queryParamsToForward }}>
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer"> <div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
<UserAvatar size="md" user={member} /> <UserAvatar size="md" user={member} />
<section className="mt-2 line-clamp-4 w-full space-y-1"> <section className="mt-2 line-clamp-4 w-full space-y-1">

View File

@ -522,6 +522,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
// disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"} // disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
disableBorder={true} disableBorder={true}
currentUserMembership={currentUserMembership} currentUserMembership={currentUserMembership}
bookerUrl={eventType.bookerUrl}
isUserOrganizationAdmin={props.isUserOrganizationAdmin}> isUserOrganizationAdmin={props.isUserOrganizationAdmin}>
<Form <Form
form={formMethods} form={formMethods}

View File

@ -1,7 +1,6 @@
"use client"; "use client";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
@ -19,8 +18,7 @@ import { DuplicateDialog } from "@calcom/features/eventtypes/components/Duplicat
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter"; import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery"; import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import { ShellMain } from "@calcom/features/shell/Shell"; import { ShellMain } from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
@ -33,7 +31,6 @@ import { trpc, TRPCClientError } from "@calcom/trpc/react";
import { import {
Alert, Alert,
Avatar, Avatar,
AvatarGroup,
Badge, Badge,
Button, Button,
ButtonGroup, ButtonGroup,
@ -85,7 +82,7 @@ interface EventTypeListHeadingProps {
profile: EventTypeGroupProfile; profile: EventTypeGroupProfile;
membershipCount: number; membershipCount: number;
teamId?: number | null; teamId?: number | null;
orgSlug?: string; bookerUrl: string;
} }
type EventTypeGroup = EventTypeGroups[number]; type EventTypeGroup = EventTypeGroups[number];
@ -95,6 +92,7 @@ interface EventTypeListProps {
group: EventTypeGroup; group: EventTypeGroup;
groupIndex: number; groupIndex: number;
readOnly: boolean; readOnly: boolean;
bookerUrl: string | null;
types: EventType[]; types: EventType[];
} }
@ -127,6 +125,7 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
types={events[0].eventTypes} types={events[0].eventTypes}
group={events[0]} group={events[0]}
groupIndex={0} groupIndex={0}
bookerUrl={events[0].bookerUrl}
readOnly={events[0].metadata.readOnly} readOnly={events[0].metadata.readOnly}
/> />
) : ( ) : (
@ -207,12 +206,17 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
const MemoizedItem = memo(Item); const MemoizedItem = memo(Item);
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => { export const EventTypeList = ({
group,
groupIndex,
readOnly,
types,
bookerUrl,
}: EventTypeListProps): JSX.Element => {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useCompatSearchParams(); const searchParams = useCompatSearchParams();
const orgBranding = useOrgBranding();
const [parent] = useAutoAnimate<HTMLUListElement>(); const [parent] = useAutoAnimate<HTMLUListElement>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0); const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
@ -383,7 +387,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types"> <ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types">
{types.map((type, index) => { {types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`; const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`; const calLink = `${bookerUrl}/${embedLink}`;
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED; const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType = const isChildrenManagedEventType =
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED; type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
@ -410,17 +414,11 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
/> />
)} )}
{isManagedEventType && type?.children && type.children?.length > 0 && ( {isManagedEventType && type?.children && type.children?.length > 0 && (
<AvatarGroup <UserAvatarGroup
className="relative right-3 top-1" className="relative right-3 top-1"
size="sm" size="sm"
truncateAfter={4} truncateAfter={4}
items={type?.children users={type?.children.flatMap((ch) => ch.users) ?? []}
.flatMap((ch) => ch.users)
.map((user: Pick<User, "name" | "username">) => ({
alt: user.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${user.username}/avatar.png`,
title: user.name || "",
}))}
/> />
)} )}
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse"> <div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
@ -696,10 +694,10 @@ const EventTypeListHeading = ({
profile, profile,
membershipCount, membershipCount,
teamId, teamId,
bookerUrl,
}: EventTypeListHeadingProps): JSX.Element => { }: EventTypeListHeadingProps): JSX.Element => {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const orgBranding = useOrgBranding();
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({ const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
onSuccess(data) { onSuccess(data) {
@ -709,18 +707,13 @@ const EventTypeListHeading = ({
showToast(error.message, "error"); showToast(error.message, "error");
}, },
}); });
const bookerUrl = useBookerUrl();
return ( return (
<div className="mb-4 flex items-center space-x-2"> <div className="mb-4 flex items-center space-x-2">
<Avatar <Avatar
alt={profile?.name || ""} alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"} href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={ imageSrc={`${bookerUrl}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`}
orgBranding?.fullDomain
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
: profile.image
}
size="md" size="md"
className="mt-1 inline-flex justify-center" className="mt-1 inline-flex justify-center"
/> />
@ -741,9 +734,7 @@ const EventTypeListHeading = ({
</span> </span>
)} )}
{profile?.slug && ( {profile?.slug && (
<Link <Link href={`${bookerUrl}/${profile.slug}`} className="text-subtle block text-xs">
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${profile.slug}`}
className="text-subtle block text-xs">
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`} {`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
</Link> </Link>
)} )}
@ -864,18 +855,22 @@ const Main = ({
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} /> <MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : ( ) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => ( data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="mt-4 flex flex-col" key={group.profile.slug}> <div
className="mt-4 flex flex-col"
data-testid={`slug-${group.profile.slug}`}
key={group.profile.slug}>
<EventTypeListHeading <EventTypeListHeading
profile={group.profile} profile={group.profile}
membershipCount={group.metadata.membershipCount} membershipCount={group.metadata.membershipCount}
teamId={group.teamId} teamId={group.teamId}
orgSlug={orgBranding?.slug} bookerUrl={group.bookerUrl}
/> />
{group.eventTypes.length ? ( {group.eventTypes.length ? (
<EventTypeList <EventTypeList
types={group.eventTypes} types={group.eventTypes}
group={group} group={group}
bookerUrl={group.bookerUrl}
groupIndex={index} groupIndex={index}
readOnly={group.metadata.readOnly} readOnly={group.metadata.readOnly}
/> />
@ -894,6 +889,7 @@ const Main = ({
types={data.eventTypeGroups[0].eventTypes} types={data.eventTypeGroups[0].eventTypes}
group={data.eventTypeGroups[0]} group={data.eventTypeGroups[0]}
groupIndex={0} groupIndex={0}
bookerUrl={data.eventTypeGroups[0].bookerUrl}
readOnly={data.eventTypeGroups[0].metadata.readOnly} readOnly={data.eventTypeGroups[0].metadata.readOnly}
/> />
) )

View File

@ -11,10 +11,11 @@ import { usePathname } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { orgDomainConfig, getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription"; import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
@ -364,7 +365,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
accepted: member.accepted, accepted: member.accepted,
organizationId: member.organizationId, organizationId: member.organizationId,
safeBio: markdownToSafeHTML(member.bio || ""), safeBio: markdownToSafeHTML(member.bio || ""),
orgOrigin: getOrgFullOrigin(member.organization?.slug || ""), bookerUrl: getBookerBaseUrlSync(member.organization?.slug || ""),
}; };
}) })
: []; : [];

View File

@ -491,13 +491,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
}, },
}, },
}, },
include: { include: { team: { include: { children: true } } },
team: {
include: {
children: true,
},
},
},
}); });
}, },
getFirstEventAsOwner: async () => getFirstEventAsOwner: async () =>

View File

@ -0,0 +1,101 @@
import { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { test } from "../../lib/fixtures";
test.afterAll(({ users, emails }) => {
users.deleteAll();
emails?.deleteAll();
});
test.describe("user1NotMemberOfOrg1 is part of team1MemberOfOrg1", () => {
test("Team1 profile should show correct domain if logged in as User1", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1NotMemberOfOrg1 = await users.create(undefined, {
hasTeam: true,
});
const { team: team1MemberOfOrg1 } = await user1NotMemberOfOrg1.getFirstTeam();
await moveTeamToOrg({ team: team1MemberOfOrg1, org });
await user1NotMemberOfOrg1.apiLogin();
await page.goto(`/settings/teams/${team1MemberOfOrg1.id}/profile`);
const domain = await page.locator(".testid-leading-text-team-url").textContent();
expect(domain).toContain(org.slug);
});
test("EventTypes listing should show correct link for user events and team1MemberOfOrg1's events", async ({
page,
users,
orgs,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1NotMemberOfOrg1 = await users.create(undefined, {
hasTeam: true,
});
const { team: team1MemberOfOrg1 } = await user1NotMemberOfOrg1.getFirstTeam();
await moveTeamToOrg({ team: team1MemberOfOrg1, org });
await user1NotMemberOfOrg1.apiLogin();
await page.goto("/event-types");
await page.waitForLoadState("networkidle");
const userEventLinksLocators = await page
.locator(`[data-testid=slug-${user1NotMemberOfOrg1.username}] [data-testid="preview-link-button"]`)
.all();
expect(userEventLinksLocators.length).toBeGreaterThan(0);
for (const userEventLinkLocator of userEventLinksLocators) {
const href = await userEventLinkLocator.getAttribute("href");
expect(href).toContain(WEBAPP_URL);
}
const teamEventLinksLocators = await page
.locator(`[data-testid=slug-${team1MemberOfOrg1.slug}] [data-testid="preview-link-button"]`)
.all();
expect(teamEventLinksLocators.length).toBeGreaterThan(0);
for (const teamEventLinksLocator of teamEventLinksLocators) {
const href = await teamEventLinksLocator.getAttribute("href");
expect(href).not.toContain(WEBAPP_URL);
expect(href).toContain(org.slug);
}
});
});
async function moveTeamToOrg({
team,
org,
}: {
team: {
id: number;
};
org: {
id: number;
};
}) {
await prisma.team.update({
where: {
id: team.id,
},
data: {
parent: {
connect: {
id: org.id,
},
},
},
});
}

View File

@ -38,7 +38,7 @@ type InputWebhook = {
/** /**
* Data to be mocked * Data to be mocked
*/ */
type ScenarioData = { export type ScenarioData = {
// hosts: { id: number; eventTypeId?: number; userId?: number; isFixed?: boolean }[]; // hosts: { id: number; eventTypeId?: number; userId?: number; isFixed?: boolean }[];
/** /**
* Prisma would return these eventTypes * Prisma would return these eventTypes
@ -789,10 +789,12 @@ export function getOrganizer({
destinationCalendar, destinationCalendar,
defaultScheduleId, defaultScheduleId,
teams, teams,
organizationId,
}: { }: {
name: string; name: string;
email: string; email: string;
id: number; id: number;
organizationId?: number | null;
schedules: InputUser["schedules"]; schedules: InputUser["schedules"];
credentials?: InputCredential[]; credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[]; selectedCalendars?: InputSelectedCalendar[];
@ -802,7 +804,6 @@ export function getOrganizer({
}) { }) {
return { return {
...TestData.users.example, ...TestData.users.example,
organizationId: null as null | number,
name, name,
email, email,
id, id,
@ -812,6 +813,7 @@ export function getOrganizer({
destinationCalendar, destinationCalendar,
defaultScheduleId, defaultScheduleId,
teams, teams,
organizationId,
}; };
} }
@ -856,6 +858,7 @@ export function getScenarioData(
eventTypes: eventTypes.map((eventType, index) => { eventTypes: eventTypes.map((eventType, index) => {
return { return {
...eventType, ...eventType,
teamId: eventType.teamId || null,
title: `Test Event Type - ${index + 1}`, title: `Test Event Type - ${index + 1}`,
description: `It's a test event type - ${index + 1}`, description: `It's a test event type - ${index + 1}`,
}; };
@ -863,6 +866,7 @@ export function getScenarioData(
users: users.map((user) => { users: users.map((user) => {
const newUser = { const newUser = {
...user, ...user,
organizationId: user.organizationId ?? null,
}; };
if (user.destinationCalendar) { if (user.destinationCalendar) {
newUser.destinationCalendar = { newUser.destinationCalendar = {
@ -876,7 +880,7 @@ export function getScenarioData(
apps: [...apps], apps: [...apps],
webhooks, webhooks,
bookings: bookings || [], bookings: bookings || [],
}; } satisfies ScenarioData;
} }
export function enableEmailFeature() { export function enableEmailFeature() {

View File

@ -135,7 +135,7 @@ expect.extend({
return { return {
pass: false, pass: false,
message: () => `Email content ${isNot ? "is" : "is not"} matching`, message: () => `Email content ${isNot ? "is" : "is not"} matching. ${JSON.stringify(emailsToLog)}`,
actual: actualEmailContent, actual: actualEmailContent,
expected: expectedEmailContent, expected: expectedEmailContent,
}; };

View File

@ -92,7 +92,9 @@ const Route = ({
}) => { }) => {
const index = routes.indexOf(route); const index = routes.indexOf(route);
const { data: eventTypesByGroup } = trpc.viewer.eventTypes.getByViewer.useQuery(); const { data: eventTypesByGroup } = trpc.viewer.eventTypes.getByViewer.useQuery({
forRoutingForms: true,
});
const eventOptions: { label: string; value: string }[] = []; const eventOptions: { label: string; value: string }[] = [];
eventTypesByGroup?.eventTypeGroups.forEach((group) => { eventTypesByGroup?.eventTypeGroups.forEach((group) => {

View File

@ -58,6 +58,7 @@ import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getErrorFromUnknown } from "@calcom/lib/errors";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
@ -67,7 +68,6 @@ import { handlePayment } from "@calcom/lib/payment/handlePayment";
import { getPiiFreeCalendarEvent, getPiiFreeEventType, getPiiFreeUser } from "@calcom/lib/piiFreeData"; import { getPiiFreeCalendarEvent, getPiiFreeEventType, getPiiFreeUser } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify"; import { safeStringify } from "@calcom/lib/safeStringify";
import { checkBookingLimits, checkDurationLimits, getLuckyUser } from "@calcom/lib/server"; import { checkBookingLimits, checkDurationLimits, getLuckyUser } from "@calcom/lib/server";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
import { getTranslation } from "@calcom/lib/server/i18n"; import { getTranslation } from "@calcom/lib/server/i18n";
import { slugify } from "@calcom/lib/slugify"; import { slugify } from "@calcom/lib/slugify";
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager"; import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
@ -272,6 +272,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
select: { select: {
id: true, id: true,
name: true, name: true,
parentId: true,
}, },
}, },
bookingFields: true, bookingFields: true,
@ -1247,7 +1248,9 @@ async function handler(
"calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null; "calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null;
let evt: CalendarEvent = { let evt: CalendarEvent = {
bookerUrl: await getBookerUrl(organizerUser), bookerUrl: eventType.team
? await getBookerBaseUrl({ organizationId: eventType.team.parentId })
: await getBookerBaseUrl(organizerUser),
type: eventType.slug, type: eventType.slug,
title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately
description: eventType.description, description: eventType.description,

View File

@ -8,6 +8,7 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
import { SchedulingType } from "@calcom/prisma/enums"; import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures"; import { test } from "@calcom/web/test/fixtures/fixtures";
import { createOrganization } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { import {
createBookingScenario, createBookingScenario,
getGoogleCalendarCredential, getGoogleCalendarCredential,
@ -1085,6 +1086,219 @@ describe("handleNewBooking", () => {
}, },
timeout timeout
); );
describe("Team(T1) not part of any org but the organizer is part of an organization(O1)", () => {
test(
`succesfully creates a booking when all the hosts are free as per their schedules
- Destination calendars for event-type and non-first hosts are used to create calendar events
- Reschedule and Cancel link in email are not of the org domain because the team is not part of any org
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const org = await createOrganization({
name: "Test Org",
slug: "testorg",
});
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const otherTeamMembers = [
{
name: "Other Team Member 1",
username: "other-team-member-1",
timeZone: Timezones["+5:30"],
// So, that it picks the first schedule from the list
defaultScheduleId: null,
email: "other-team-member-1@example.com",
id: 102,
// Has Evening shift
schedules: [TestData.schedules.IstEveningShift],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: TestData.apps["google-calendar"].type,
externalId: "other-team-member-1@google-calendar.com",
},
},
];
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
// So, that it picks the first schedule from the list
defaultScheduleId: null,
organizationId: org.id,
teams: [
{
membership: {
accepted: true,
},
team: {
id: 1,
name: "Team 1",
slug: "team-1",
},
},
],
// Has morning shift with some overlap with morning shift
schedules: [TestData.schedules.IstMorningShift],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: TestData.apps["google-calendar"].type,
externalId: "organizer@google-calendar.com",
},
});
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
schedulingType: SchedulingType.COLLECTIVE,
length: 45,
users: [
{
id: 101,
},
{
id: 102,
},
],
// It is a team event but that team isn't part of any org
teamId: 1,
destinationCalendar: {
integration: TestData.apps["google-calendar"].type,
externalId: "event-type-1@google-calendar.com",
},
},
],
organizer,
usersApartFromOrganizer: otherTeamMembers,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: appStoreMetadata.dailyvideo.dirName,
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
// Try booking the first available free timeslot in both the users' schedules
start: `${getDate({ dateIncrement: 1 }).dateString}T11:30:00.000Z`,
end: `${getDate({ dateIncrement: 1 }).dateString}T11:45:00.000Z`,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const createdBooking = await handleNewBooking(req);
await expectBookingToBeInDatabase({
description: "",
location: BookingLocations.CalVideo,
responses: expect.objectContaining({
email: booker.email,
name: booker.name,
}),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
references: [
{
type: appStoreMetadata.dailyvideo.type,
uid: "MOCK_ID",
meetingId: "MOCK_ID",
meetingPassword: "MOCK_PASS",
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
type: TestData.apps["google-calendar"].type,
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
},
],
});
expectWorkflowToBeTriggered();
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
destinationCalendars: [
{
integration: TestData.apps["google-calendar"].type,
externalId: "event-type-1@google-calendar.com",
},
{
integration: TestData.apps["google-calendar"].type,
externalId: "other-team-member-1@google-calendar.com",
},
],
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
});
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
// All booking links are of WEBAPP_URL and not of the org because the team isn't part of the org
urlOrigin: WEBAPP_URL,
},
booker,
organizer,
otherTeamMembers,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
});
},
timeout
);
});
}); });
test.todo("Round Robin booking"); test.todo("Round Robin booking");

View File

@ -95,7 +95,7 @@ export function subdomainSuffix() {
} }
export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) { export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) {
if (!slug) return WEBAPP_URL; if (!slug) return WEBAPP_URL.replace("https://", "").replace("http://", "");
const orgFullOrigin = `${ const orgFullOrigin = `${
options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : "" options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""
}${slug}.${subdomainSuffix()}`; }${slug}.${subdomainSuffix()}`;

View File

@ -1,11 +1,9 @@
import classNames from "classnames"; import classNames from "classnames";
import TeamPill, { TeamRole } from "@calcom/ee/teams/components/TeamPill"; import TeamPill, { TeamRole } from "@calcom/ee/teams/components/TeamPill";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
import { import {
Avatar,
Button, Button,
ButtonGroup, ButtonGroup,
Dropdown, Dropdown,
@ -17,6 +15,8 @@ import {
} from "@calcom/ui"; } from "@calcom/ui";
import { ExternalLink, MoreHorizontal } from "@calcom/ui/components/icon"; import { ExternalLink, MoreHorizontal } from "@calcom/ui/components/icon";
import { UserAvatar } from "@components/ui/avatar/UserAvatar";
interface Props { interface Props {
member: RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"][number]; member: RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"][number];
} }
@ -26,22 +26,17 @@ export default function MemberListItem(props: Props) {
const { member } = props; const { member } = props;
const { user } = member; const { user } = member;
const bookerUrl = useBookerUrl(); const name = user.name || user.username || user.email;
const bookerUrl = props.member.bookerUrl;
const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, ""); const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, "");
const bookingLink = user.username && `${bookerUrlWithoutProtocol}/${user.username}`; const bookingLink = user.username && `${bookerUrlWithoutProtocol}/${user.username}`;
const name = user.name || user.username || user.email;
return ( return (
<li className="divide-subtle divide-y px-5"> <li className="divide-subtle divide-y px-5">
<div className="my-4 flex justify-between"> <div className="my-4 flex justify-between">
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row"> <div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
<div className="flex"> <div className="flex">
<Avatar <UserAvatar size="sm" user={user} className="h-10 w-10 rounded-full" />
size="sm"
imageSrc={`${bookerUrl}/${user.username}/avatar.png`}
alt={name || ""}
className="h-10 w-10 rounded-full"
/>
<div className="ms-3 inline-block overflow-hidden"> <div className="ms-3 inline-block overflow-hidden">
<div className="mb-1 flex"> <div className="mb-1 flex">

View File

@ -6,24 +6,16 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid
import InviteLinkSettingsModal from "@calcom/features/ee/teams/components/InviteLinkSettingsModal"; import InviteLinkSettingsModal from "@calcom/features/ee/teams/components/InviteLinkSettingsModal";
import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal"; import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry"; import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry";
import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import { Badge, Button, showToast, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
Avatar,
Badge,
Button,
showToast,
SkeletonButton,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { ArrowRight, Plus, Trash2 } from "@calcom/ui/components/icon"; import { ArrowRight, Plus, Trash2 } from "@calcom/ui/components/icon";
import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar";
type TeamMember = RouterOutputs["viewer"]["teams"]["get"]["members"][number]; type TeamMember = RouterOutputs["viewer"]["teams"]["get"]["members"][number];
@ -219,7 +211,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
const session = useSession(); const session = useSession();
const bookerUrl = useBookerUrl(); const bookerUrl = member.bookerUrl;
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
enabled: !!session.data?.user?.org, enabled: !!session.data?.user?.org,
}); });
@ -247,7 +239,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
)} )}
data-testid="pending-member-item"> data-testid="pending-member-item">
<div className="mr-4 flex max-w-full space-x-2 overflow-hidden rtl:space-x-reverse"> <div className="mr-4 flex max-w-full space-x-2 overflow-hidden rtl:space-x-reverse">
<Avatar size="mdLg" imageSrc={`${bookerUrl}/${member.username}/avatar.png`} alt="owner-avatar" /> <UserAvatar size="mdLg" user={member} />
<div className="max-w-full overflow-hidden"> <div className="max-w-full overflow-hidden">
<div className="flex space-x-1"> <div className="flex space-x-1">
<p>{member.name || member.email || t("team_member")}</p> <p>{member.name || member.email || t("team_member")}</p>
@ -258,7 +250,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
{member.role === "ADMIN" && <Badge variant="default">{t("admin")}</Badge>} {member.role === "ADMIN" && <Badge variant="default">{t("admin")}</Badge>}
</div> </div>
{member.username ? ( {member.username ? (
<p className="text-default truncate">{`${WEBAPP_URL}/${member.username}`}</p> <p className="text-default truncate">{`${bookerUrl}/${member.username}`}</p>
) : ( ) : (
<p className="text-default truncate">{t("not_on_cal", { appName: APP_NAME })}</p> <p className="text-default truncate">{t("not_on_cal", { appName: APP_NAME })}</p>
)} )}

View File

@ -3,7 +3,6 @@ import { SendIcon } from "lucide-react";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useState } from "react"; import { useState } from "react";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@ -119,7 +118,7 @@ export default function MemberListItem(props: Props) {
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true"; process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true";
const resendInvitation = editMode && !props.member.accepted; const resendInvitation = editMode && !props.member.accepted;
const bookerUrl = useBookerUrl(); const bookerUrl = props.member.bookerUrl;
const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, ""); const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, "");
const bookingLink = !!props.member.username && `${bookerUrlWithoutProtocol}/${props.member.username}`; const bookingLink = !!props.member.username && `${bookerUrlWithoutProtocol}/${props.member.username}`;
const isAdmin = props.team && ["ADMIN", "OWNER"].includes(props.team.membership?.role); const isAdmin = props.team && ["ADMIN", "OWNER"].includes(props.team.membership?.role);

View File

@ -6,6 +6,7 @@ import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSetti
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal"; import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums";
@ -106,11 +107,9 @@ export default function TeamListItem(props: Props) {
<span className="text-default text-sm font-bold">{team.name}</span> <span className="text-default text-sm font-bold">{team.name}</span>
<span className="text-muted block text-xs"> <span className="text-muted block text-xs">
{team.slug ? ( {team.slug ? (
orgBranding ? ( `${getTeamUrlSync({ orgSlug: team.parent ? team.parent.slug : null, teamSlug: team.slug })}/${
`${orgBranding.fullDomain}/${team.slug}` team.slug
) : ( }`
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}`
)
) : ( ) : (
<Badge>{t("upgrade")}</Badge> <Badge>{t("upgrade")}</Badge>
)} )}

View File

@ -7,11 +7,10 @@ import { useLayoutEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { md } from "@calcom/lib/markdownIt"; import { md } from "@calcom/lib/markdownIt";
@ -269,7 +268,6 @@ const TeamProfileForm = ({ team }: TeamProfileFormProps) => {
}); });
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const orgBranding = useOrgBranding();
const { const {
formState: { isSubmitting, isDirty }, formState: { isSubmitting, isDirty },
@ -375,11 +373,14 @@ const TeamProfileForm = ({ team }: TeamProfileFormProps) => {
name="slug" name="slug"
label={t("team_url")} label={t("team_url")}
value={value} value={value}
addOnLeading={ data-testid="team-url"
team.parent && orgBranding addOnClassname="testid-leading-text-team-url"
? `${getOrgFullOrigin(orgBranding?.slug, { protocol: false })}/` addOnLeading={`${getTeamUrlSync(
: `${WEBAPP_URL}/team/` { orgSlug: team.parent ? team.parent.slug : null, teamSlug: null },
} {
protocol: false,
}
)}`}
onChange={(e) => { onChange={(e) => {
form.clearErrors("slug"); form.clearErrors("slug");
form.setValue("slug", slugify(e?.target.value, true), { shouldDirty: true }); form.setValue("slug", slugify(e?.target.value, true), { shouldDirty: true });

View File

@ -1,8 +1,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { Props } from "react-select"; import type { Props } from "react-select";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -15,6 +13,7 @@ export type ChildrenEventType = {
label: string; label: string;
created: boolean; created: boolean;
owner: { owner: {
avatar: string;
id: number; id: number;
email: string; email: string;
name: string; name: string;
@ -36,7 +35,6 @@ export const ChildrenEventTypeSelect = ({
onChange: (value: readonly ChildrenEventType[]) => void; onChange: (value: readonly ChildrenEventType[]) => void;
}) => { }) => {
const { t } = useLocale(); const { t } = useLocale();
const orgBranding = useOrgBranding();
const [animationRef] = useAutoAnimate<HTMLUListElement>(); const [animationRef] = useAutoAnimate<HTMLUListElement>();
return ( return (
@ -63,9 +61,7 @@ export const ChildrenEventTypeSelect = ({
<Avatar <Avatar
size="mdLg" size="mdLg"
className="overflow-visible" className="overflow-visible"
imageSrc={`${orgBranding ? getOrgFullOrigin(orgBranding.slug) : CAL_URL}/${ imageSrc={children.owner.avatar}
children.owner.username
}/avatar.png`}
alt={children.owner.name || children.owner.email || ""} alt={children.owner.name || children.owner.email || ""}
/> />
<div className="flex w-full flex-row justify-between"> <div className="flex w-full flex-row justify-between">

View File

@ -19,8 +19,22 @@ export const getUserAvatarUrl = (
}`; }`;
}; };
export function getTeamAvatarUrl(
team: Pick<Team, "slug"> & {
organizationId?: number | null;
logoUrl?: string | null;
requestedSlug: string | null;
}
) {
if (team.logoUrl) {
return team.logoUrl;
}
const slug = team.slug ?? team.requestedSlug;
return `${WEBAPP_URL}/team/${slug}/avatar.png${team.organizationId ? `?orgId=${team.organizationId}` : ""}`;
}
export const getOrgAvatarUrl = ( export const getOrgAvatarUrl = (
org: Pick<Team, "id" | "slug"> & { org: Pick<Team, "slug"> & {
logoUrl?: string | null; logoUrl?: string | null;
requestedSlug: string | null; requestedSlug: string | null;
} }

View File

@ -0,0 +1,30 @@
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
export const getBookerBaseUrlSync = (
orgSlug: string | null,
options?: {
protocol: boolean;
}
) => {
return getOrgFullOrigin(orgSlug ?? "", options);
};
export const getTeamUrlSync = (
{
orgSlug,
teamSlug,
}: {
orgSlug: string | null;
teamSlug: string | null;
},
options?: {
protocol: boolean;
}
) => {
const bookerUrl = getBookerBaseUrlSync(orgSlug, options);
teamSlug = teamSlug ? teamSlug : "";
if (orgSlug) {
return `${bookerUrl}/${teamSlug}`;
}
return `${bookerUrl}/team/${teamSlug}`;
};

View File

@ -0,0 +1,12 @@
import { WEBAPP_URL } from "../constants";
import { getBrand } from "../server/getBrand";
export const getBookerBaseUrl = async (user: { organizationId: number | null }) => {
const orgBrand = await getBrand(user.organizationId);
return orgBrand?.fullDomain ?? WEBAPP_URL;
};
export const getTeamBookerUrl = async (team: { organizationId: number | null }) => {
const orgBrand = await getBrand(team.organizationId);
return orgBrand?.fullDomain ?? WEBAPP_URL;
};

View File

@ -15,6 +15,9 @@ import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-u
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { WEBAPP_URL } from "./constants";
import { getBookerBaseUrl } from "./getBookerUrl/server";
interface getEventTypeByIdProps { interface getEventTypeByIdProps {
eventTypeId: number; eventTypeId: number;
userId: number; userId: number;
@ -106,6 +109,11 @@ export default async function getEventTypeById({
successRedirectUrl: true, successRedirectUrl: true,
currency: true, currency: true,
bookingFields: true, bookingFields: true,
owner: {
select: {
organizationId: true,
},
},
parent: { parent: {
select: { select: {
teamId: true, teamId: true,
@ -167,6 +175,7 @@ export default async function getEventTypeById({
username: true, username: true,
email: true, email: true,
id: true, id: true,
organizationId: true,
}, },
}, },
hidden: true, hidden: true,
@ -257,12 +266,18 @@ export default async function getEventTypeById({
metadata: parsedMetaData, metadata: parsedMetaData,
customInputs: parsedCustomInputs, customInputs: parsedCustomInputs,
users: rawEventType.users, users: rawEventType.users,
bookerUrl: restEventType.team
? await getBookerBaseUrl({ organizationId: restEventType.team.parentId })
: restEventType.owner
? await getBookerBaseUrl(restEventType.owner)
: WEBAPP_URL,
children: restEventType.children.flatMap((ch) => children: restEventType.children.flatMap((ch) =>
ch.owner !== null ch.owner !== null
? { ? {
...ch, ...ch,
owner: { owner: {
...ch.owner, ...ch.owner,
avatar: getUserAvatarUrl(ch.owner),
email: ch.owner.email, email: ch.owner.email,
name: ch.owner.name ?? "", name: ch.owner.name ?? "",
username: ch.owner.username ?? "", username: ch.owner.username ?? "",

View File

@ -1,7 +0,0 @@
import { WEBAPP_URL } from "../constants";
import { getBrand } from "./getBrand";
export const getBookerUrl = async (user: { organizationId: number | null }) => {
const orgBrand = await getBrand(user.organizationId);
return orgBrand?.fullDomain ?? WEBAPP_URL;
};

View File

@ -1,12 +1,12 @@
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { getAppFromSlug } from "@calcom/app-store/utils"; import { getAppFromSlug } from "@calcom/app-store/utils";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import prisma, { baseEventTypeSelect } from "@calcom/prisma"; import prisma, { baseEventTypeSelect } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums"; import { SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { WEBAPP_URL } from "../../../constants"; import { WEBAPP_URL } from "../../../constants";
import { getBookerBaseUrlSync } from "../../../getBookerUrl/client";
import { getTeam, getOrg } from "../../repository/team"; import { getTeam, getOrg } from "../../repository/team";
export type TeamWithMembers = Awaited<ReturnType<typeof getTeamWithMembers>>; export type TeamWithMembers = Awaited<ReturnType<typeof getTeamWithMembers>>;
@ -158,7 +158,7 @@ export async function getTeamWithMembers(args: {
.map((membership) => membership.team.slug) .map((membership) => membership.team.slug)
: null, : null,
avatar: `${WEBAPP_URL}/${m.user.username}/avatar.png`, avatar: `${WEBAPP_URL}/${m.user.username}/avatar.png`,
orgOrigin: getOrgFullOrigin(m.user.organization?.slug || ""), bookerUrl: getBookerBaseUrlSync(m.user.organization?.slug || ""),
connectedApps: !isTeamView connectedApps: !isTeamView
? credentials?.map((cred) => { ? credentials?.map((cred) => {
const appSlug = cred.app?.slug; const appSlug = cred.app?.slug;

View File

@ -1,6 +1,7 @@
import { isOrganization, withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils"; import { isOrganization, withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl"; import { getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { PrismaClient } from "@calcom/prisma"; import type { PrismaClient } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@ -36,6 +37,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti
name: true, name: true,
slug: true, slug: true,
metadata: true, metadata: true,
parentId: true,
members: { members: {
select: { select: {
userId: true, userId: true,
@ -51,24 +53,34 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti
if (!user) { if (!user) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
} }
const bookerUrl = await getBookerUrl(user);
const image = user?.username ? `${bookerUrl}/${user.username}/avatar.png` : undefined; const nonOrgTeams = user.teams
const nonOrgTeams = user.teams.filter((membership) => !isOrganization({ team: membership.team })); .filter((membership) => !isOrganization({ team: membership.team }))
.map((membership) => ({
...membership,
team: {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
},
}));
return [ return [
{ {
teamId: null, teamId: null,
name: user.name, name: user.name,
slug: user.username, slug: user.username,
image, image: getUserAvatarUrl(user),
readOnly: false, readOnly: false,
}, },
...nonOrgTeams.map((membership) => ({ ...nonOrgTeams.map((membership) => ({
teamId: membership.team.id, teamId: membership.team.id,
name: membership.team.name, name: membership.team.name,
slug: membership.team.slug ? `team/${membership.team.slug}` : null, slug: membership.team.slug ? `team/${membership.team.slug}` : null,
image: `${bookerUrl}${membership.team.slug ? "/team" : ""}/${membership.team.slug}/avatar.png`, image: getTeamAvatarUrl({
slug: membership.team.slug,
requestedSlug: membership.team.metadata?.requestedSlug ?? null,
organizationId: membership.team.parentId,
}),
role: membership.role, role: membership.role,
readOnly: !withRoleCanCreateEntity(membership.role), readOnly: !withRoleCanCreateEntity(membership.role),
})), })),

View File

@ -15,11 +15,11 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { cancelScheduledJobs } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { cancelScheduledJobs } from "@calcom/features/webhooks/lib/scheduleTrigger";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined } from "@calcom/lib"; import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify"; import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server"; import { getTranslation } from "@calcom/lib/server";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
import { prisma } from "@calcom/prisma"; import { prisma } from "@calcom/prisma";
import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
@ -53,7 +53,15 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
startTime: true, startTime: true,
endTime: true, endTime: true,
eventTypeId: true, eventTypeId: true,
eventType: true, eventType: {
include: {
team: {
select: {
parentId: true,
},
},
},
},
location: true, location: true,
attendees: true, attendees: true,
references: true, references: true,
@ -174,9 +182,12 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
const [userAsPeopleType] = usersToPeopleType([user], userTranslation); const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
const builder = new CalendarEventBuilder(); const builder = new CalendarEventBuilder();
const eventType = bookingToReschedule.eventType;
builder.init({ builder.init({
title: bookingToReschedule.title, title: bookingToReschedule.title,
bookerUrl: await getBookerUrl(user), bookerUrl: eventType?.team
? await getBookerBaseUrl({ organizationId: eventType.team.parentId })
: await getBookerBaseUrl(user),
type: event && event.slug ? event.slug : bookingToReschedule.title, type: event && event.slug ? event.slug : bookingToReschedule.title,
startTime: bookingToReschedule.startTime.toISOString(), startTime: bookingToReschedule.startTime.toISOString(),
endTime: bookingToReschedule.endTime.toISOString(), endTime: bookingToReschedule.endTime.toISOString(),

View File

@ -4,9 +4,10 @@ import { orderBy } from "lodash";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { CAL_URL } from "@calcom/lib/constants"; import { getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
import type { PrismaClient } from "@calcom/prisma"; import type { PrismaClient } from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma"; import { baseEventTypeSelect } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
@ -116,6 +117,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
slug: true, slug: true,
parentId: true, parentId: true,
metadata: true, metadata: true,
parent: true,
members: { members: {
select: { select: {
userId: true, userId: true,
@ -160,6 +162,14 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
} }
const memberships = user.teams.map((membership) => ({
...membership,
team: {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
},
}));
type UserEventTypes = (typeof user.eventTypes)[number]; type UserEventTypes = (typeof user.eventTypes)[number];
type TeamEventTypeChildren = (typeof user.teams)[number]["team"]["eventTypes"][number]; type TeamEventTypeChildren = (typeof user.teams)[number]["team"]["eventTypes"][number];
@ -176,11 +186,12 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
type EventTypeGroup = { type EventTypeGroup = {
teamId?: number | null; teamId?: number | null;
parentId?: number | null; parentId?: number | null;
bookerUrl: string;
membershipRole?: MembershipRole | null; membershipRole?: MembershipRole | null;
profile: { profile: {
slug: (typeof user)["username"]; slug: (typeof user)["username"];
name: (typeof user)["name"]; name: (typeof user)["name"];
image?: string; image: string;
}; };
metadata: { metadata: {
membershipCount: number; membershipCount: number;
@ -195,16 +206,16 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
(evType) => evType.schedulingType !== SchedulingType.MANAGED (evType) => evType.schedulingType !== SchedulingType.MANAGED
); );
const image = user?.username ? `${CAL_URL}/${user.username}/avatar.png` : undefined;
if (!input?.filters || !hasFilter(input?.filters) || input?.filters?.userIds?.includes(user.id)) { if (!input?.filters || !hasFilter(input?.filters) || input?.filters?.userIds?.includes(user.id)) {
const bookerUrl = await getBookerBaseUrl(user);
eventTypeGroups.push({ eventTypeGroups.push({
teamId: null, teamId: null,
bookerUrl,
membershipRole: null, membershipRole: null,
profile: { profile: {
slug: user.username, slug: user.username,
name: user.name, name: user.name,
image, image: getUserAvatarUrl({ username: user.username, organizationId: user.organizationId }),
}, },
eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]), eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]),
metadata: { metadata: {
@ -227,9 +238,9 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
}; };
eventTypeGroups = ([] as EventTypeGroup[]).concat( eventTypeGroups = ([] as EventTypeGroup[]).concat(
eventTypeGroups, eventTypeGroups,
user.teams memberships
.filter((mmship) => { .filter((mmship) => {
const metadata = teamMetadataSchema.parse(mmship.team.metadata); const metadata = mmship.team.metadata;
if (metadata?.isOrganization) { if (metadata?.isOrganization) {
return false; return false;
} else { } else {
@ -243,33 +254,50 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
const orgMembership = teamMemberships.find( const orgMembership = teamMemberships.find(
(teamM) => teamM.teamId === membership.team.parentId (teamM) => teamM.teamId === membership.team.parentId
)?.membershipRole; )?.membershipRole;
const team = {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
};
let slug;
if (input?.forRoutingForms) {
// For Routing form we want to ensure that after migration of team to an org, the URL remains same for the team
// Once we solve this https://github.com/calcom/cal.com/issues/12399, we can remove this conditional change in slug
slug = `team/${team.slug}`;
} else {
// In an Org, a team can be accessed without /team prefix as well as with /team prefix
slug = team.slug ? (!team.parentId ? `team/${team.slug}` : `${team.slug}`) : null;
}
return { return {
teamId: membership.team.id, teamId: team.id,
parentId: membership.team.parentId, parentId: team.parentId,
bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null),
membershipRole: membershipRole:
orgMembership && compareMembership(orgMembership, membership.role) orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership ? orgMembership
: membership.role, : membership.role,
profile: { profile: {
name: membership.team.name, image: getTeamAvatarUrl({
image: `${CAL_URL}/team/${membership.team.slug}/avatar.png`, slug: team.slug,
slug: membership.team.slug requestedSlug: team.metadata?.requestedSlug ?? null,
? !membership.team.parentId organizationId: team.parentId,
? `team/${membership.team.slug}` }),
: `${membership.team.slug}` name: team.name,
: null, slug,
}, },
metadata: { metadata: {
membershipCount: membership.team.members.length, membershipCount: team.members.length,
readOnly: readOnly:
membership.role === membership.role ===
(membership.team.parentId (team.parentId
? orgMembership && compareMembership(orgMembership, membership.role) ? orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership ? orgMembership
: MembershipRole.MEMBER : MembershipRole.MEMBER
: MembershipRole.MEMBER), : MembershipRole.MEMBER),
}, },
eventTypes: membership.team.eventTypes eventTypes: team.eventTypes
.map(mapEventType) .map(mapEventType)
.filter(filterTeamsEventTypesBasedOnInput) .filter(filterTeamsEventTypesBasedOnInput)
.filter((evType) => evType.userId === null || evType.userId === ctx.user.id) .filter((evType) => evType.userId === null || evType.userId === ctx.user.id)
@ -282,7 +310,6 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
}) })
); );
const bookerUrl = await getBookerUrl(user);
return { return {
eventTypeGroups, eventTypeGroups,
// so we can show a dropdown when the user has teams // so we can show a dropdown when the user has teams
@ -291,7 +318,6 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
...group.metadata, ...group.metadata,
teamId: group.teamId, teamId: group.teamId,
membershipRole: group.membershipRole, membershipRole: group.membershipRole,
image: `${bookerUrl}/${group.profile.slug}/avatar.png`,
})), })),
}; };
}; };

View File

@ -9,6 +9,7 @@ export const filterQuerySchemaStrict = z.object({
export const ZEventTypeInputSchema = z export const ZEventTypeInputSchema = z
.object({ .object({
filters: filterQuerySchemaStrict.optional(), filters: filterQuerySchemaStrict.optional(),
forRoutingForms: z.boolean().optional(),
}) })
.nullish(); .nullish();

View File

@ -1,6 +1,7 @@
import type { Prisma } from "@prisma/client"; import type { Prisma } from "@prisma/client";
import z from "zod"; import z from "zod";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import { prisma } from "@calcom/prisma"; import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc"; import type { TrpcSessionUser } from "../../../trpc";
@ -68,6 +69,8 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
name: true, name: true,
email: true, email: true,
avatar: true, avatar: true,
organization: true,
organizationId: true,
}, },
}, },
}, },
@ -77,7 +80,12 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
skip: offset, skip: offset,
}); });
return members; return members.map((m) => {
return {
...m,
bookerUrl: getBookerBaseUrlSync(m.user.organization?.slug || ""),
};
});
}; };
export default listOtherTeamMembers; export default listOtherTeamMembers;

View File

@ -1,6 +1,5 @@
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import type { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@ -27,12 +26,16 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
const membership = team?.members.find((membership) => membership.id === ctx.user.id); const membership = team?.members.find((membership) => membership.id === ctx.user.id);
if (!membership) {
throw new TRPCError({ code: "NOT_FOUND", message: "Not a member of this team." });
}
return { return {
...team, ...team,
safeBio: markdownToSafeHTML(team.bio), safeBio: markdownToSafeHTML(team.bio),
membership: { membership: {
role: membership?.role as MembershipRole, role: membership.role,
accepted: membership?.accepted, accepted: membership.accepted,
}, },
}; };
}; };

View File

@ -22,6 +22,7 @@ export const listHandler = async ({ ctx }: ListOptions) => {
team: { team: {
include: { include: {
inviteTokens: true, inviteTokens: true,
parent: true,
}, },
}, },
}, },

View File

@ -1,4 +1,4 @@
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import { prisma } from "@calcom/prisma"; import { prisma } from "@calcom/prisma";
import type { Webhook } from "@calcom/prisma/client"; import type { Webhook } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums";
@ -82,7 +82,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
const userWebhooks = user.webhooks; const userWebhooks = user.webhooks;
let webhookGroups: WebhookGroup[] = []; let webhookGroups: WebhookGroup[] = [];
const bookerUrl = await getBookerUrl(user); const bookerUrl = await getBookerBaseUrl(user);
const image = user?.username ? `${bookerUrl}/${user.username}/avatar.png` : undefined; const image = user?.username ? `${bookerUrl}/${user.username}/avatar.png` : undefined;
webhookGroups.push({ webhookGroups.push({

View File

@ -1,6 +1,5 @@
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { ButtonColor } from "@calcom/ui"; import type { ButtonColor } from "@calcom/ui";
@ -19,7 +18,7 @@ import { Plus } from "@calcom/ui/components/icon";
export interface Option { export interface Option {
teamId: number | null | undefined; // if undefined, then it's a profile teamId: number | null | undefined; // if undefined, then it's a profile
label: string | null; label: string | null;
image?: string | null; image: string | null;
slug: string | null; slug: string | null;
} }
@ -43,7 +42,6 @@ export function CreateButton(props: CreateBtnProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useCompatSearchParams(); const searchParams = useCompatSearchParams();
const pathname = usePathname(); const pathname = usePathname();
const bookerUrl = useBookerUrl();
const { const {
createDialog, createDialog,
@ -114,12 +112,7 @@ export function CreateButton(props: CreateBtnProps) {
type="button" type="button"
data-testid={`option${option.teamId ? "-team" : ""}-${idx}`} data-testid={`option${option.teamId ? "-team" : ""}-${idx}`}
StartIcon={(props) => ( StartIcon={(props) => (
<Avatar <Avatar alt={option.label || ""} imageSrc={option.image} size="sm" {...props} />
alt={option.label || ""}
imageSrc={option.image || `${bookerUrl}/${option.label}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)} )}
onClick={() => onClick={() =>
!!CreateDialog !!CreateDialog