Compare commits
1 Commits
main
...
across-org
Author | SHA1 | Date | |
---|---|---|---|
dac9e17ccf |
|
@ -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})` : ""}`,
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 top-1"
|
||||
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">
|
||||
|
@ -696,10 +694,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) {
|
||||
|
@ -709,18 +707,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"
|
||||
/>
|
||||
|
@ -741,9 +734,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>
|
||||
)}
|
||||
|
@ -864,18 +855,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}
|
||||
/>
|
||||
|
@ -894,6 +889,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}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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 || ""),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
|
|
@ -491,13 +491,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { team: { include: { children: true } } },
|
||||
});
|
||||
},
|
||||
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
|
||||
*/
|
||||
type ScenarioData = {
|
||||
export type ScenarioData = {
|
||||
// hosts: { id: number; eventTypeId?: number; userId?: number; isFixed?: boolean }[];
|
||||
/**
|
||||
* Prisma would return these eventTypes
|
||||
|
@ -789,10 +789,12 @@ export function getOrganizer({
|
|||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
teams,
|
||||
organizationId,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
id: number;
|
||||
organizationId?: number | null;
|
||||
schedules: InputUser["schedules"];
|
||||
credentials?: InputCredential[];
|
||||
selectedCalendars?: InputSelectedCalendar[];
|
||||
|
@ -802,7 +804,6 @@ export function getOrganizer({
|
|||
}) {
|
||||
return {
|
||||
...TestData.users.example,
|
||||
organizationId: null as null | number,
|
||||
name,
|
||||
email,
|
||||
id,
|
||||
|
@ -812,6 +813,7 @@ export function getOrganizer({
|
|||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
teams,
|
||||
organizationId,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -856,6 +858,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}`,
|
||||
};
|
||||
|
@ -863,6 +866,7 @@ export function getScenarioData(
|
|||
users: users.map((user) => {
|
||||
const newUser = {
|
||||
...user,
|
||||
organizationId: user.organizationId ?? null,
|
||||
};
|
||||
if (user.destinationCalendar) {
|
||||
newUser.destinationCalendar = {
|
||||
|
@ -876,7 +880,7 @@ export function getScenarioData(
|
|||
apps: [...apps],
|
||||
webhooks,
|
||||
bookings: bookings || [],
|
||||
};
|
||||
} satisfies ScenarioData;
|
||||
}
|
||||
|
||||
export function enableEmailFeature() {
|
||||
|
|
|
@ -135,7 +135,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,
|
||||
};
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -58,6 +58,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";
|
||||
|
@ -67,7 +68,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";
|
||||
|
@ -272,6 +272,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
bookingFields: true,
|
||||
|
@ -1247,7 +1248,9 @@ async function handler(
|
|||
"calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null;
|
||||
|
||||
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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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()}`;
|
||||
|
|
|
@ -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"][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">
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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";
|
||||
|
@ -119,7 +118,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);
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 { 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 ?? "",
|
||||
|
|
|
@ -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 { 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;
|
||||
|
|
|
@ -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),
|
||||
})),
|
||||
|
|
|
@ -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,
|
||||
|
@ -174,9 +182,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(),
|
||||
|
|
|
@ -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`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ export const filterQuerySchemaStrict = z.object({
|
|||
export const ZEventTypeInputSchema = z
|
||||
.object({
|
||||
filters: filterQuerySchemaStrict.optional(),
|
||||
forRoutingForms: z.boolean().optional(),
|
||||
})
|
||||
.nullish();
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
@ -68,6 +69,8 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
|
|||
name: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
organization: true,
|
||||
organizationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -77,7 +80,12 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
|
|||
skip: offset,
|
||||
});
|
||||
|
||||
return members;
|
||||
return members.map((m) => {
|
||||
return {
|
||||
...m,
|
||||
bookerUrl: getBookerBaseUrlSync(m.user.organization?.slug || ""),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default listOtherTeamMembers;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ export const listHandler = async ({ ctx }: ListOptions) => {
|
|||
team: {
|
||||
include: {
|
||||
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 type { Webhook } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
@ -82,7 +82,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
|
|||
|
||||
const userWebhooks = user.webhooks;
|
||||
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({
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user