fix: Across Org Scenarios - Wrong links for event and team (#12358)

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
Hariom Balhara 2023-12-19 15:03:30 +05:30 committed by GitHub
parent f1caaad536
commit e5e0fa97eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 570 additions and 154 deletions

View File

@ -44,6 +44,7 @@ export const mapMemberToChildrenOption = (
username: member.username ?? "",
membership: member.membership,
eventTypeSlugs: member.eventTypes ?? [],
avatar: member.avatar,
},
value: `${member.id ?? ""}`,
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 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 Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
@ -67,6 +65,7 @@ type Props = {
isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
bookerUrl: string;
};
function getNavigation(props: {
@ -135,6 +134,7 @@ function EventTypeSingleLayout({
formMethods,
availability,
isUserOrganizationAdmin,
bookerUrl,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@ -235,10 +235,8 @@ function EventTypeSingleLayout({
formMethods,
]);
const orgBranding = useOrgBranding();
const isOrgEvent = orgBranding?.fullDomain;
const permalink = `${orgBranding?.fullDomain ?? CAL_URL}/${
team ? `${!isOrgEvent ? "team/" : ""}${team.slug}` : eventType.users[0].username
const permalink = `${bookerUrl}/${
team ? `${!team.parentId ? "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 MemberType = Pick<MembersType[number], "id" | "name" | "bio" | "username" | "organizationId"> & {
safeBio: string | null;
orgOrigin: string;
bookerUrl: string;
};
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
@ -26,7 +26,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
return (
<Link
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">
<UserAvatar size="md" user={member} />
<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={true}
currentUserMembership={currentUserMembership}
bookerUrl={eventType.bookerUrl}
isUserOrganizationAdmin={props.isUserOrganizationAdmin}>
<Form
form={formMethods}

View File

@ -1,7 +1,6 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client";
import { Trans } from "next-i18next";
import Link from "next/link";
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 { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import { ShellMain } from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
@ -33,7 +31,6 @@ import { trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Alert,
Avatar,
AvatarGroup,
Badge,
Button,
ButtonGroup,
@ -85,7 +82,7 @@ interface EventTypeListHeadingProps {
profile: EventTypeGroupProfile;
membershipCount: number;
teamId?: number | null;
orgSlug?: string;
bookerUrl: string;
}
type EventTypeGroup = EventTypeGroups[number];
@ -95,6 +92,7 @@ interface EventTypeListProps {
group: EventTypeGroup;
groupIndex: number;
readOnly: boolean;
bookerUrl: string | null;
types: EventType[];
}
@ -127,6 +125,7 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
types={events[0].eventTypes}
group={events[0]}
groupIndex={0}
bookerUrl={events[0].bookerUrl}
readOnly={events[0].metadata.readOnly}
/>
) : (
@ -207,12 +206,17 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
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 router = useRouter();
const pathname = usePathname();
const searchParams = useCompatSearchParams();
const orgBranding = useOrgBranding();
const [parent] = useAutoAnimate<HTMLUListElement>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
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">
{types.map((type, index) => {
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 isChildrenManagedEventType =
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 && (
<AvatarGroup
<UserAvatarGroup
className="relative right-3"
size="sm"
truncateAfter={4}
items={type?.children
.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 || "",
}))}
users={type?.children.flatMap((ch) => ch.users) ?? []}
/>
)}
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
@ -697,10 +695,10 @@ const EventTypeListHeading = ({
profile,
membershipCount,
teamId,
bookerUrl,
}: EventTypeListHeadingProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const orgBranding = useOrgBranding();
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
onSuccess(data) {
@ -710,18 +708,13 @@ const EventTypeListHeading = ({
showToast(error.message, "error");
},
});
const bookerUrl = useBookerUrl();
return (
<div className="mb-4 flex items-center space-x-2">
<Avatar
alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={
orgBranding?.fullDomain
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
: profile.image
}
imageSrc={`${bookerUrl}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`}
size="md"
className="mt-1 inline-flex justify-center"
/>
@ -742,9 +735,7 @@ const EventTypeListHeading = ({
</span>
)}
{profile?.slug && (
<Link
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${profile.slug}`}
className="text-subtle block text-xs">
<Link href={`${bookerUrl}/${profile.slug}`} className="text-subtle block text-xs">
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
</Link>
)}
@ -865,18 +856,22 @@ const Main = ({
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
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
profile={group.profile}
membershipCount={group.metadata.membershipCount}
teamId={group.teamId}
orgSlug={orgBranding?.slug}
bookerUrl={group.bookerUrl}
/>
{group.eventTypes.length ? (
<EventTypeList
types={group.eventTypes}
group={group}
bookerUrl={group.bookerUrl}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
@ -895,6 +890,7 @@ const Main = ({
types={data.eventTypeGroups[0].eventTypes}
group={data.eventTypeGroups[0]}
groupIndex={0}
bookerUrl={data.eventTypeGroups[0].bookerUrl}
readOnly={data.eventTypeGroups[0].metadata.readOnly}
/>
)

View File

@ -11,10 +11,11 @@ import { usePathname } from "next/navigation";
import { useEffect } from "react";
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 { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
@ -364,7 +365,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
accepted: member.accepted,
organizationId: member.organizationId,
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: {
team: {
include: {
children: true,
},
},
},
include: { team: { include: { children: true } } },
});
},
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.getFirstTeamMembership();
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.getFirstTeamMembership();
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

@ -39,7 +39,7 @@ type InputWebhook = {
/**
* Data to be mocked
*/
type ScenarioData = {
export type ScenarioData = {
// hosts: { id: number; eventTypeId?: number; userId?: number; isFixed?: boolean }[];
/**
* Prisma would return these eventTypes
@ -835,10 +835,12 @@ export function getOrganizer({
defaultScheduleId,
weekStart = "Sunday",
teams,
organizationId,
}: {
name: string;
email: string;
id: number;
organizationId?: number | null;
schedules: InputUser["schedules"];
credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[];
@ -849,7 +851,6 @@ export function getOrganizer({
}) {
return {
...TestData.users.example,
organizationId: null as null | number,
name,
email,
id,
@ -860,6 +861,7 @@ export function getOrganizer({
defaultScheduleId,
weekStart,
teams,
organizationId,
};
}
@ -904,6 +906,7 @@ export function getScenarioData(
eventTypes: eventTypes.map((eventType, index) => {
return {
...eventType,
teamId: eventType.teamId || null,
title: `Test Event Type - ${index + 1}`,
description: `It's a test event type - ${index + 1}`,
};
@ -911,6 +914,7 @@ export function getScenarioData(
users: users.map((user) => {
const newUser = {
...user,
organizationId: user.organizationId ?? null,
};
if (user.destinationCalendar) {
newUser.destinationCalendar = {
@ -924,7 +928,7 @@ export function getScenarioData(
apps: [...apps],
webhooks,
bookings: bookings || [],
};
} satisfies ScenarioData;
}
export function enableEmailFeature() {

View File

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

View File

@ -92,7 +92,9 @@ const 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 }[] = [];
eventTypesByGroup?.eventTypeGroups.forEach((group) => {

View File

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

View File

@ -8,6 +8,7 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
import { createOrganization } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import {
createBookingScenario,
getGoogleCalendarCredential,
@ -1085,6 +1086,219 @@ describe("handleNewBooking", () => {
},
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");

View File

@ -95,7 +95,7 @@ export function subdomainSuffix() {
}
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 = `${
options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""
}${slug}.${subdomainSuffix()}`;

View File

@ -1,11 +1,9 @@
import classNames from "classnames";
import TeamPill, { TeamRole } from "@calcom/ee/teams/components/TeamPill";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import {
Avatar,
Button,
ButtonGroup,
Dropdown,
@ -17,6 +15,8 @@ import {
} from "@calcom/ui";
import { ExternalLink, MoreHorizontal } from "@calcom/ui/components/icon";
import { UserAvatar } from "@components/ui/avatar/UserAvatar";
interface Props {
member: RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"]["rows"][number];
}
@ -26,22 +26,17 @@ export default function MemberListItem(props: Props) {
const { member } = props;
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 bookingLink = user.username && `${bookerUrlWithoutProtocol}/${user.username}`;
const name = user.name || user.username || user.email;
return (
<li className="divide-subtle divide-y px-5">
<div className="my-4 flex justify-between">
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
<div className="flex">
<Avatar
size="sm"
imageSrc={`${bookerUrl}/${user.username}/avatar.png`}
alt={name || ""}
className="h-10 w-10 rounded-full"
/>
<UserAvatar size="sm" user={user} className="h-10 w-10 rounded-full" />
<div className="ms-3 inline-block overflow-hidden">
<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 MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal";
import { classNames } from "@calcom/lib";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry";
import { MembershipRole } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import {
Avatar,
Badge,
Button,
showToast,
SkeletonButton,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { Badge, Button, showToast, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
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];
@ -219,7 +211,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
const { t } = useLocale();
const utils = trpc.useContext();
const session = useSession();
const bookerUrl = useBookerUrl();
const bookerUrl = member.bookerUrl;
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
enabled: !!session.data?.user?.org,
});
@ -247,7 +239,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
)}
data-testid="pending-member-item">
<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="flex space-x-1">
<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>}
</div>
{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>
)}

View File

@ -3,7 +3,6 @@ import { SendIcon } from "lucide-react";
import { signIn } from "next-auth/react";
import { useState } from "react";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@ -120,7 +119,7 @@ export default function MemberListItem(props: Props) {
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true";
const resendInvitation = editMode && !props.member.accepted;
const bookerUrl = useBookerUrl();
const bookerUrl = props.member.bookerUrl;
const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, "");
const bookingLink = !!props.member.username && `${bookerUrlWithoutProtocol}/${props.member.username}`;
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 classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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-muted block text-xs">
{team.slug ? (
orgBranding ? (
`${orgBranding.fullDomain}/${team.slug}`
) : (
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}`
)
`${getTeamUrlSync({ orgSlug: team.parent ? team.parent.slug : null, teamSlug: team.slug })}/${
team.slug
}`
) : (
<Badge>{t("upgrade")}</Badge>
)}

View File

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

View File

@ -1,8 +1,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
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 { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -15,6 +13,7 @@ export type ChildrenEventType = {
label: string;
created: boolean;
owner: {
avatar: string;
id: number;
email: string;
name: string;
@ -36,7 +35,6 @@ export const ChildrenEventTypeSelect = ({
onChange: (value: readonly ChildrenEventType[]) => void;
}) => {
const { t } = useLocale();
const orgBranding = useOrgBranding();
const [animationRef] = useAutoAnimate<HTMLUListElement>();
return (
@ -63,9 +61,7 @@ export const ChildrenEventTypeSelect = ({
<Avatar
size="mdLg"
className="overflow-visible"
imageSrc={`${orgBranding ? getOrgFullOrigin(orgBranding.slug) : CAL_URL}/${
children.owner.username
}/avatar.png`}
imageSrc={children.owner.avatar}
alt={children.owner.name || children.owner.email || ""}
/>
<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 = (
org: Pick<Team, "id" | "slug"> & {
org: Pick<Team, "slug"> & {
logoUrl?: 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 || "";
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 { WEBAPP_URL } from "./constants";
import { getBookerBaseUrl } from "./getBookerUrl/server";
interface getEventTypeByIdProps {
eventTypeId: number;
userId: number;
@ -106,6 +109,11 @@ export default async function getEventTypeById({
successRedirectUrl: true,
currency: true,
bookingFields: true,
owner: {
select: {
organizationId: true,
},
},
parent: {
select: {
teamId: true,
@ -167,6 +175,7 @@ export default async function getEventTypeById({
username: true,
email: true,
id: true,
organizationId: true,
},
},
hidden: true,
@ -257,12 +266,18 @@ export default async function getEventTypeById({
metadata: parsedMetaData,
customInputs: parsedCustomInputs,
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) =>
ch.owner !== null
? {
...ch,
owner: {
...ch.owner,
avatar: getUserAvatarUrl(ch.owner),
email: ch.owner.email,
name: ch.owner.name ?? "",
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 { getAppFromSlug } from "@calcom/app-store/utils";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import prisma, { baseEventTypeSelect } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { WEBAPP_URL } from "../../../constants";
import { getBookerBaseUrlSync } from "../../../getBookerUrl/client";
import { getTeam, getOrg } from "../../repository/team";
export type TeamWithMembers = Awaited<ReturnType<typeof getTeamWithMembers>>;
@ -158,7 +158,7 @@ export async function getTeamWithMembers(args: {
.map((membership) => membership.team.slug)
: null,
avatar: `${WEBAPP_URL}/${m.user.username}/avatar.png`,
orgOrigin: getOrgFullOrigin(m.user.organization?.slug || ""),
bookerUrl: getBookerBaseUrlSync(m.user.organization?.slug || ""),
connectedApps: !isTeamView
? credentials?.map((cred) => {
const appSlug = cred.app?.slug;

View File

@ -1,6 +1,7 @@
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 { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
@ -36,6 +37,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti
name: true,
slug: true,
metadata: true,
parentId: true,
members: {
select: {
userId: true,
@ -51,24 +53,34 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti
if (!user) {
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.filter((membership) => !isOrganization({ team: membership.team }));
const nonOrgTeams = user.teams
.filter((membership) => !isOrganization({ team: membership.team }))
.map((membership) => ({
...membership,
team: {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
},
}));
return [
{
teamId: null,
name: user.name,
slug: user.username,
image,
image: getUserAvatarUrl(user),
readOnly: false,
},
...nonOrgTeams.map((membership) => ({
teamId: membership.team.id,
name: membership.team.name,
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,
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 sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
import { prisma } from "@calcom/prisma";
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
@ -53,7 +53,15 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
startTime: true,
endTime: true,
eventTypeId: true,
eventType: true,
eventType: {
include: {
team: {
select: {
parentId: true,
},
},
},
},
location: true,
attendees: true,
references: true,
@ -175,9 +183,12 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
const builder = new CalendarEventBuilder();
const eventType = bookingToReschedule.eventType;
builder.init({
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,
startTime: bookingToReschedule.startTime.toISOString(),
endTime: bookingToReschedule.endTime.toISOString(),

View File

@ -4,9 +4,10 @@ import { orderBy } from "lodash";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
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 { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
import type { PrismaClient } from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
@ -116,6 +117,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
slug: true,
parentId: true,
metadata: true,
parent: true,
members: {
select: {
userId: true,
@ -160,6 +162,14 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
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 TeamEventTypeChildren = (typeof user.teams)[number]["team"]["eventTypes"][number];
@ -176,11 +186,12 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
type EventTypeGroup = {
teamId?: number | null;
parentId?: number | null;
bookerUrl: string;
membershipRole?: MembershipRole | null;
profile: {
slug: (typeof user)["username"];
name: (typeof user)["name"];
image?: string;
image: string;
};
metadata: {
membershipCount: number;
@ -195,16 +206,16 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
(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)) {
const bookerUrl = await getBookerBaseUrl(user);
eventTypeGroups.push({
teamId: null,
bookerUrl,
membershipRole: null,
profile: {
slug: user.username,
name: user.name,
image,
image: getUserAvatarUrl({ username: user.username, organizationId: user.organizationId }),
},
eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]),
metadata: {
@ -227,9 +238,9 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
};
eventTypeGroups = ([] as EventTypeGroup[]).concat(
eventTypeGroups,
user.teams
memberships
.filter((mmship) => {
const metadata = teamMetadataSchema.parse(mmship.team.metadata);
const metadata = mmship.team.metadata;
if (metadata?.isOrganization) {
return false;
} else {
@ -243,33 +254,50 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
const orgMembership = teamMemberships.find(
(teamM) => teamM.teamId === membership.team.parentId
)?.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 {
teamId: membership.team.id,
parentId: membership.team.parentId,
teamId: team.id,
parentId: team.parentId,
bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null),
membershipRole:
orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership
: membership.role,
profile: {
name: membership.team.name,
image: `${CAL_URL}/team/${membership.team.slug}/avatar.png`,
slug: membership.team.slug
? !membership.team.parentId
? `team/${membership.team.slug}`
: `${membership.team.slug}`
: null,
image: getTeamAvatarUrl({
slug: team.slug,
requestedSlug: team.metadata?.requestedSlug ?? null,
organizationId: team.parentId,
}),
name: team.name,
slug,
},
metadata: {
membershipCount: membership.team.members.length,
membershipCount: team.members.length,
readOnly:
membership.role ===
(membership.team.parentId
(team.parentId
? orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership
: MembershipRole.MEMBER
: MembershipRole.MEMBER),
},
eventTypes: membership.team.eventTypes
eventTypes: team.eventTypes
.map(mapEventType)
.filter(filterTeamsEventTypesBasedOnInput)
.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 {
eventTypeGroups,
// so we can show a dropdown when the user has teams
@ -291,7 +318,6 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
...group.metadata,
teamId: group.teamId,
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
.object({
filters: filterQuerySchemaStrict.optional(),
forRoutingForms: z.boolean().optional(),
})
.nullish();

View File

@ -1,6 +1,7 @@
import type { Prisma } from "@prisma/client";
import z from "zod";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
@ -70,6 +71,8 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
name: true,
email: true,
avatar: true,
organization: true,
organizationId: true,
},
},
},
@ -85,7 +88,12 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
}
return {
rows: members || [],
rows: members.map((m) => {
return {
...m,
bookerUrl: getBookerBaseUrlSync(m.user.organization?.slug || ""),
};
}),
nextCursor,
};
};

View File

@ -1,6 +1,5 @@
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import type { MembershipRole } from "@calcom/prisma/enums";
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);
if (!membership) {
throw new TRPCError({ code: "NOT_FOUND", message: "Not a member of this team." });
}
return {
...team,
safeBio: markdownToSafeHTML(team.bio),
membership: {
role: membership?.role as MembershipRole,
accepted: membership?.accepted,
role: membership.role,
accepted: membership.accepted,
},
};
};

View File

@ -22,6 +22,7 @@ export const listHandler = async ({ ctx }: ListOptions) => {
team: {
include: {
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 type { Webhook } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
@ -92,7 +92,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
let userWebhooks = user.webhooks;
userWebhooks = userWebhooks.filter(filterWebhooks);
let webhookGroups: WebhookGroup[] = [];
const bookerUrl = await getBookerUrl(user);
const bookerUrl = await getBookerBaseUrl(user);
const image = user?.username ? `${bookerUrl}/${user.username}/avatar.png` : undefined;
webhookGroups.push({

View File

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