Compare commits
1 Commits
main
...
across-org
Author | SHA1 | Date | |
---|---|---|---|
dac9e17ccf |
|
@ -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})` : ""}`,
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 || ""),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
|
@ -491,13 +491,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: { team: { include: { children: true } } },
|
||||||
team: {
|
|
||||||
include: {
|
|
||||||
children: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getFirstEventAsOwner: async () =>
|
getFirstEventAsOwner: async () =>
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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()}`;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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 ?? "",
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
||||||
|
|
|
@ -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),
|
||||||
})),
|
})),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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`,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ export const listHandler = async ({ ctx }: ListOptions) => {
|
||||||
team: {
|
team: {
|
||||||
include: {
|
include: {
|
||||||
inviteTokens: true,
|
inviteTokens: true,
|
||||||
|
parent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user