fix: Unpublished screens (#10453)

* Implementation

* Changes and e2e

* Reverting launch.json

* Reverting org create handler

* Reverting yarn.lock

* DRYness and nitpicks

* Default org domain to undefined

* Applying zomars suggestion

* Suggestions

* Fixing seed and type in suggestion

* Fixing types

---------

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Leo Giovanetti 2023-07-31 17:27:22 -03:00 committed by GitHub
parent 4435451e9b
commit 4a6dc50909
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 503 additions and 161 deletions

View File

@ -32,6 +32,7 @@ jobs:
- name: Run Tests
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}

View File

@ -34,6 +34,7 @@ jobs:
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
yarn workspace @calcom/embed-react packaged:tests
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}

View File

@ -32,6 +32,7 @@ jobs:
- name: Run Tests
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}

View File

@ -31,6 +31,7 @@ jobs:
- name: Run Tests
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}

View File

@ -4,6 +4,7 @@ on:
workflow_call:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}

View File

@ -4,6 +4,7 @@ on:
workflow_call:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}

View File

@ -12,6 +12,7 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
@ -24,7 +25,7 @@ import prisma from "@calcom/prisma";
import type { EventType, User } from "@calcom/prisma/client";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, HeadSeo } from "@calcom/ui";
import { Avatar, HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { EmbedProps } from "@lib/withEmbedSsr";
@ -34,7 +35,7 @@ import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr";
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { users, profile, eventTypes, markdownStrippedBio } = props;
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(profile.theme);
const { t } = useLocale();
@ -58,6 +59,15 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
}
}, [telemetry, router.asPath]); */
if (entity?.isUnpublished) {
return (
<div className="flex h-full min-h-[100dvh] items-center justify-center">
<UnpublishedEntity {...entity} />
</div>
);
}
const isEventListEmpty = eventTypes.length === 0;
return (
<>
@ -206,6 +216,11 @@ export type UserPageProps = {
themeBasis: string | null;
markdownStrippedBio: string;
safeBio: string;
entity: {
isUnpublished?: boolean;
orgSlug?: string | null;
name?: string | null;
};
eventTypes: ({
descriptionAsSafeHTML: string;
metadata: z.infer<typeof EventTypeMetaDataSchema>;
@ -226,8 +241,10 @@ export type UserPageProps = {
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const usernameList = getUsernameList(context.query.user as string);
const dataFetchStart = Date.now();
const usersWithoutAvatar = await prisma.user.findMany({
@ -235,11 +252,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
username: {
in: usernameList,
},
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
id: true,
@ -250,6 +263,12 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
brandColor: true,
darkBrandColor: true,
organizationId: true,
organization: {
select: {
slug: true,
name: true,
},
},
theme: true,
away: true,
verified: true,
@ -311,6 +330,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
const safeBio = markdownToSafeHTML(user.bio) || "";
const markdownStrippedBio = stripMarkdown(user?.bio || "");
const org = usersWithoutAvatar[0].organization;
return {
props: {
@ -321,6 +341,11 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
away: user.away,
verified: user.verified,
})),
entity: {
isUnpublished: org?.slug === null,
orgSlug: currentOrgDomain,
name: org?.name ?? null,
},
eventTypes,
safeBio,
profile,

View File

@ -6,6 +6,7 @@ import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import slugify from "@calcom/lib/slugify";
@ -26,7 +27,7 @@ export default function Type({
away,
isBrandingHidden,
rescheduleUid,
org,
entity,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
@ -35,7 +36,7 @@ export default function Type({
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
/>
<Booker
username={user}
@ -43,7 +44,7 @@ export default function Type({
bookingData={booking}
isAway={away}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
/>
</main>
);
@ -58,7 +59,10 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const users = await prisma.user.findMany({
where: {
@ -106,7 +110,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
return {
props: {
org,
entity: eventData.entity,
booking,
user: usernames.join("+"),
slug,
@ -124,18 +128,17 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid, bookingUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
away: true,
@ -158,7 +161,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
const org = isValidOrgDomain ? currentOrgDomain : null;
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
// as well as to check if the event exist, so we can show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username,
eventSlug: slug,
@ -177,7 +180,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
away: user?.away,
user: username,
slug,
org,
entity: eventData.entity,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
themeBasis: username,

View File

@ -25,7 +25,7 @@ export default function Type({
away,
isBrandingHidden,
isTeamEvent,
org,
entity,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
@ -34,7 +34,7 @@ export default function Type({
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
org={org}
entity={entity}
/>
<Booker
username={user}
@ -43,7 +43,7 @@ export default function Type({
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
org={org}
entity={entity}
/>
</main>
);
@ -132,7 +132,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
return {
props: {
org,
entity: eventData.entity,
booking,
away: user?.away,
user: username,

View File

@ -1,13 +1,14 @@
import type { GetServerSidePropsContext } from "next";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import prisma from "@calcom/prisma";
import PageWrapper from "@components/PageWrapper";
import type { PageProps as UserTypePageProps } from "../../../[user]/[type]";
import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../[user]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
import type { PageProps as TeamTypePageProps } from "../../../team/[slug]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const team = await prisma.team.findFirst({
@ -16,9 +17,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
parentId: {
not: null,
},
parent: {
slug: ctx.query.orgSlug as string,
},
parent: getSlugOrRequestedSlug(ctx.query.orgSlug as string),
},
select: {
id: true,

View File

@ -1,5 +1,6 @@
import type { GetServerSidePropsContext } from "next";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import prisma from "@calcom/prisma";
import PageWrapper from "@components/PageWrapper";
@ -16,9 +17,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
parentId: {
not: null,
},
parent: {
slug: ctx.query.orgSlug as string,
},
parent: getSlugOrRequestedSlug(ctx.query.orgSlug as string),
},
select: {
id: true,

View File

@ -17,7 +17,7 @@ import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
import { Avatar, AvatarGroup, Button, HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
@ -49,16 +49,12 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
}, [telemetry, router.asPath]);
if (isUnpublished) {
const slug = team.slug || metadata?.requestedSlug;
return (
<div className="m-8 flex items-center justify-center">
<EmptyScreen
avatar={<Avatar alt={teamName} imageSrc={getPlaceholderAvatar(team.logo, team.name)} size="lg" />}
headline={t("team_is_unpublished", {
team: teamName,
})}
description={t("team_is_unpublished_description", {
entity: metadata?.isOrganization ? t("organization").toLowerCase() : t("team").toLowerCase(),
})}
<div className="flex h-full min-h-[100dvh] items-center justify-center">
<UnpublishedEntity
{...(metadata?.isOrganization || team.parentId ? { orgSlug: slug } : { teamSlug: slug })}
name={teamName}
/>
</div>
);
@ -249,15 +245,21 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
const { isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const flags = await getFeatureFlagMap(prisma);
const team = await getTeamWithMembers(undefined, slug);
const team = await getTeamWithMembers({ slug, orgSlug: currentOrgDomain });
const metadata = teamMetadataSchema.parse(team?.metadata ?? {});
console.warn("gSSP, team/[slug] - ", {
isValidOrgDomain,
currentOrgDomain,
ALLOWED_HOSTNAMES: process.env.ALLOWED_HOSTNAMES,
flags: JSON.stringify,
});
// Taking care of sub-teams and orgs
if (
(isValidOrgDomain && team?.parent && !!metadata?.isOrganization) ||
(!isValidOrgDomain && team?.parent) ||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
flags["organizations"] !== true
@ -265,13 +267,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return { notFound: true } as const;
}
if (!team) {
if (!team || (team.parent && !team.parent.slug)) {
const unpublishedTeam = await prisma.team.findFirst({
where: {
metadata: {
path: ["requestedSlug"],
equals: slug,
},
...(team?.parent
? { id: team.parent.id }
: {
metadata: {
path: ["requestedSlug"],
equals: slug,
},
}),
},
});
@ -307,7 +313,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
team: { ...serializableTeam, safeBio, members },
team: { ...serializableTeam, safeBio, members, metadata },
themeBasis: serializableTeam.slug,
trpcState: ssr.dehydrate(),
markdownStrippedBio,

View File

@ -6,6 +6,7 @@ import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
@ -17,7 +18,7 @@ import PageWrapper from "@components/PageWrapper";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type({ slug, user, booking, away, isEmbed, isBrandingHidden, org }: PageProps) {
export default function Type({ slug, user, booking, away, isEmbed, isBrandingHidden, entity }: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
@ -26,7 +27,7 @@ export default function Type({ slug, user, booking, away, isEmbed, isBrandingHid
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
org={org}
entity={entity}
/>
<Booker
username={user}
@ -35,7 +36,7 @@ export default function Type({ slug, user, booking, away, isEmbed, isBrandingHid
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent
org={org}
entity={entity}
/>
</main>
);
@ -57,16 +58,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { rescheduleUid } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
context.req.headers.host ?? "",
context.params?.orgSlug
);
const team = await prisma.team.findFirst({
where: {
slug: teamSlug,
parent: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
...getSlugOrRequestedSlug(teamSlug),
parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
id: true,
@ -103,7 +103,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
org,
entity: eventData.entity,
booking,
away: false,
user: teamSlug,

View File

@ -6,8 +6,7 @@ import { hashSync as hash } from "bcryptjs";
import dayjs from "@calcom/dayjs";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { prisma } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
import type { TimeZoneEnum } from "./types";
@ -37,15 +36,71 @@ const seededForm = {
type UserWithIncludes = PrismaType.UserGetPayload<typeof userWithEventTypes>;
const createTeamEventType = async (
user: { id: number },
team: { id: number },
scenario?: {
schedulingType?: SchedulingType;
teamEventTitle?: string;
teamEventSlug?: string;
}
) => {
return await prisma.eventType.create({
data: {
team: {
connect: {
id: team.id,
},
},
users: {
connect: {
id: user.id,
},
},
owner: {
connect: {
id: user.id,
},
},
schedulingType: scenario?.schedulingType ?? SchedulingType.COLLECTIVE,
title: scenario?.teamEventTitle ?? `${teamEventTitle}-team-id-${team.id}`,
slug: scenario?.teamEventSlug ?? `${teamEventSlug}-team-id-${team.id}`,
length: 30,
},
});
};
const createTeamAndAddUser = async (
{ user }: { user: { id: number; role?: MembershipRole } },
{
user,
isUnpublished,
isOrg,
hasSubteam,
}: {
user: { id: number; username: string | null; role?: MembershipRole };
isUnpublished?: boolean;
isOrg?: boolean;
hasSubteam?: true;
},
workerInfo: WorkerInfo
) => {
const slug = `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}`;
const data: PrismaType.TeamCreateInput = {
name: `user-id-${user.id}'s Team ${isOrg ? "Org" : "Team"}`,
};
data.metadata = {
...(isUnpublished ? { requestedSlug: slug } : {}),
...(isOrg ? { isOrganization: true } : {}),
};
data.slug = !isUnpublished ? slug : undefined;
if (isOrg && hasSubteam) {
const team = await createTeamAndAddUser({ user }, workerInfo);
await createTeamEventType(user, team);
data.children = { connect: [{ id: team.id }] };
}
data.orgUsers = isOrg ? { connect: [{ id: user.id }] } : undefined;
const team = await prisma.team.create({
data: {
name: `user-id-${user.id}'s Team`,
slug: `team-${workerInfo.workerIndex}-${Date.now()}`,
},
data,
});
const { role = MembershipRole.OWNER, id: userId } = user;
@ -73,6 +128,9 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
schedulingType?: SchedulingType;
teamEventTitle?: string;
teamEventSlug?: string;
isOrg?: boolean;
hasSubteam?: true;
isUnpublished?: true;
} = {}
) => {
const _user = await prisma.user.create({
@ -216,30 +274,16 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
include: userIncludes,
});
if (scenario.hasTeam) {
const team = await createTeamAndAddUser({ user: { id: user.id, role: "OWNER" } }, workerInfo);
const teamEvent = await prisma.eventType.create({
data: {
team: {
connect: {
id: team.id,
},
},
users: {
connect: {
id: _user.id,
},
},
owner: {
connect: {
id: _user.id,
},
},
schedulingType: scenario.schedulingType ?? SchedulingType.COLLECTIVE,
title: scenario.teamEventTitle ?? teamEventTitle,
slug: scenario.teamEventSlug ?? teamEventSlug,
length: 30,
const team = await createTeamAndAddUser(
{
user: { id: user.id, username: user.username, role: "OWNER" },
isUnpublished: scenario.isUnpublished,
isOrg: scenario.isOrg,
hasSubteam: scenario.hasSubteam,
},
});
workerInfo
);
const teamEvent = await createTeamEventType(user, team, scenario);
if (scenario.teammates) {
// Create Teammate users
for (const teammateObj of scenario.teammates) {
@ -328,6 +372,20 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
include: { team: true },
});
},
getOrg: async () => {
return prisma.membership.findFirstOrThrow({
where: {
userId: user.id,
team: {
metadata: {
path: ["isOrganization"],
equals: true,
},
},
},
include: { team: { select: { children: true, metadata: true, name: true } } },
});
},
getFirstTeamEvent: async (teamId: number) => {
return prisma.eventType.findFirstOrThrow({
where: {

View File

@ -0,0 +1,120 @@
import { expect } from "@playwright/test";
import { SchedulingType } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
const title = (name: string) => `${name} is unpublished`;
const description = (entity: string) =>
`This ${entity} link is currently not available. Please contact the ${entity} owner or ask them to publish it.`;
const avatar = (slug: string) => `/team/${slug}/avatar.png`;
test.afterAll(async ({ users }) => {
await users.deleteAll();
});
test.describe("Unpublished", () => {
test("Regular team profile", async ({ page, users }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true });
const { team } = await owner.getTeam();
const { requestedSlug } = team.metadata as { requestedSlug: string };
await page.goto(`/team/${requestedSlug}`);
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
expect(await page.locator(`h2:has-text("${title(team.name)}")`).count()).toBe(1);
expect(await page.locator(`div:text("${description("team")}")`).count()).toBe(1);
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
});
test("Regular team event type", async ({ page, users }) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
schedulingType: SchedulingType.COLLECTIVE,
});
const { team } = await owner.getTeam();
const { requestedSlug } = team.metadata as { requestedSlug: string };
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
await page.goto(`/team/${requestedSlug}/${teamEventSlug}`);
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
expect(await page.locator(`h2:has-text("${title(team.name)}")`).count()).toBe(1);
expect(await page.locator(`div:text("${description("team")}")`).count()).toBe(1);
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
});
test("Organization profile", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { requestedSlug } = org.metadata as { requestedSlug: string };
await page.goto(`/org/${requestedSlug}`);
await page.waitForLoadState("networkidle");
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
});
test("Organization sub-team", async ({ users, page }) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
const { team: org } = await owner.getOrg();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: subteamSlug }] = org.children as { slug: string }[];
await page.goto(`/org/${requestedSlug}/team/${subteamSlug}`);
await page.waitForLoadState("networkidle");
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
});
test("Organization sub-team event-type", async ({ users, page }) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
const { team: org } = await owner.getOrg();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: subteamSlug, id: subteamId }] = org.children as { slug: string; id: number }[];
const { slug: subteamEventSlug } = await owner.getFirstTeamEvent(subteamId);
await page.goto(`/org/${requestedSlug}/team/${subteamSlug}/${subteamEventSlug}`);
await page.waitForLoadState("networkidle");
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
});
test("Organization user", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { requestedSlug } = org.metadata as { requestedSlug: string };
await page.goto(`/org/${requestedSlug}/${owner.username}`);
await page.waitForLoadState("networkidle");
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
});
test("Organization user event-type", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: ownerEventType }] = owner.eventTypes;
await page.goto(`/org/${requestedSlug}/${owner.username}/${ownerEventType}`);
await page.waitForLoadState("networkidle");
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1);
expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1);
await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug));
});
});

View File

@ -1721,7 +1721,7 @@
"show_on_booking_page": "Show on booking page",
"get_started_zapier_templates": "Get started with Zapier templates",
"team_is_unpublished": "{{team}} is unpublished",
"team_is_unpublished_description": "This {{entity}} link is currently not available. Please contact the {{entity}} owner or ask them publish it.",
"team_is_unpublished_description": "This {{entity}} link is currently not available. Please contact the {{entity}} owner or ask them to publish it.",
"team_member": "Team member",
"a_routing_form": "A Routing Form",
"form_description_placeholder": "Form Description",

View File

@ -27,6 +27,7 @@ import { getQueryParam } from "./utils/query-param";
import { useBrandColors } from "./utils/use-brand-colors";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const UnpublishedEntity = dynamic(() => import("@calcom/ui").then((mod) => mod.UnpublishedEntity));
const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), {
ssr: false,
});
@ -38,7 +39,7 @@ const BookerComponent = ({
bookingData,
hideBranding = false,
isTeamEvent,
org,
entity,
}: BookerProps) => {
const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(max-width: 1024px)");
@ -98,7 +99,7 @@ const BookerComponent = ({
bookingData,
layout: defaultLayout,
isTeamEvent,
org,
org: entity.orgSlug,
});
useEffect(() => {
@ -139,6 +140,10 @@ const BookerComponent = ({
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
if (entity.isUnpublished) {
return <UnpublishedEntity {...entity} />;
}
if (event.isSuccess && !event.data) {
return <NotFound />;
}

View File

@ -5,8 +5,16 @@ import type { GetBookingType } from "../lib/get-booking";
export interface BookerProps {
eventSlug: string;
username: string;
// Make it optional later, once we figure out where we can possibly need to set org
org: string | null;
/**
* Whether is a team or org, we gather basic info from both
*/
entity: {
isUnpublished?: boolean;
orgSlug?: string | null;
teamSlug?: string | null;
name?: string | null;
};
/**
* If month is NOT set as a prop on the component, we expect a query parameter

View File

@ -8,14 +8,18 @@ interface BookerSeoProps {
rescheduleUid: string | undefined;
hideBranding?: boolean;
isTeamEvent?: boolean;
org: string | null;
entity: {
orgSlug?: string | null;
teamSlug?: string | null;
name?: string | null;
};
}
export const BookerSeo = (props: BookerSeoProps) => {
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, org } = props;
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, entity } = props;
const { t } = useLocale();
const { data: event } = trpc.viewer.public.event.useQuery(
{ username, eventSlug, isTeamEvent, org },
{ username, eventSlug, isTeamEvent, org: entity.orgSlug ?? null },
{ refetchOnWindowFocus: false }
);

View File

@ -23,11 +23,20 @@ export function getOrgSlug(hostname: string) {
return null;
}
export function orgDomainConfig(hostname: string) {
export function orgDomainConfig(hostname: string, fallback?: string | string[]) {
const currentOrgDomain = getOrgSlug(hostname);
const isValidOrgDomain = currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain);
if (isValidOrgDomain || !fallback) {
return {
currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null,
isValidOrgDomain,
};
}
const fallbackOrgSlug = fallback as string;
const isValidFallbackDomain = !RESERVED_SUBDOMAINS.includes(fallbackOrgSlug);
return {
currentOrgDomain,
isValidOrgDomain: currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain),
currentOrgDomain: isValidFallbackDomain ? fallbackOrgSlug : null,
isValidOrgDomain: isValidFallbackDomain,
};
}
@ -39,3 +48,17 @@ export function subdomainSuffix() {
export function getOrgFullDomain(slug: string, options: { protocol: boolean } = { protocol: true }) {
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}`;
}
export function getSlugOrRequestedSlug(slug: string) {
return {
OR: [
{ slug },
{
metadata: {
path: ["requestedSlug"],
equals: slug,
},
},
],
};
}

View File

@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
@ -17,6 +18,7 @@ import {
userMetadata as userMetadataSchema,
bookerLayouts as bookerLayoutsSchema,
BookerLayouts,
teamMetadataSchema,
} from "@calcom/prisma/zod-utils";
const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
@ -38,7 +40,24 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
currency: true,
seatsPerTimeSlot: true,
bookingFields: true,
team: true,
team: {
select: {
parentId: true,
metadata: true,
brandColor: true,
darkBrandColor: true,
slug: true,
name: true,
logo: true,
theme: true,
parent: {
select: {
slug: true,
name: true,
},
},
},
},
successRedirectUrl: true,
workflows: {
include: {
@ -73,6 +92,12 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
metadata: true,
brandColor: true,
darkBrandColor: true,
organization: {
select: {
name: true,
slug: true,
},
},
},
},
hidden: true,
@ -86,11 +111,7 @@ export const getPublicEvent = async (
prisma: PrismaClient
) => {
const usernameList = getUsernameList(username);
const orgQuery = org
? {
slug: org ?? null,
}
: null;
const orgQuery = org ? getSlugOrRequestedSlug(org) : null;
// In case of dynamic group event, we fetch user's data and use the default event.
if (usernameList.length > 1) {
const users = await prisma.user.findMany({
@ -108,6 +129,12 @@ export const getPublicEvent = async (
brandColor: true,
darkBrandColor: true,
theme: true,
organization: {
select: {
slug: true,
name: true,
},
},
},
});
@ -133,6 +160,8 @@ export const getPublicEvent = async (
defaultLayout: BookerLayouts.MONTH_VIEW,
} as BookerLayoutSettings;
const disableBookingTitle = !defaultEvent.isDynamic;
const unPublishedOrgUser = users.find((user) => user.organization?.slug === null);
return {
...defaultEvent,
bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }),
@ -151,13 +180,18 @@ export const getPublicEvent = async (
firstUsersMetadata?.defaultBookerLayouts || defaultEventBookerLayouts
),
},
entity: {
isUnpublished: unPublishedOrgUser !== undefined,
orgSlug: org,
name: unPublishedOrgUser?.organization?.name ?? null,
},
};
}
const usersOrTeamQuery = isTeamEvent
? {
team: {
slug: username,
...getSlugOrRequestedSlug(username),
parent: orgQuery,
},
}
@ -183,6 +217,7 @@ export const getPublicEvent = async (
if (!event) return null;
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});
const teamMetadata = teamMetadataSchema.parse(event.team?.metadata || {});
const users = getUsersFromEvent(event) || (await getOwnerFromUsersArray(prisma, event.id));
if (users === null) {
@ -201,7 +236,15 @@ export const getPublicEvent = async (
// Sets user data on profile object for easier access
profile: getProfileFromEvent(event),
users,
orgDomain: org,
entity: {
isUnpublished:
event.team?.slug === null ||
event.owner?.organization?.slug === null ||
event.team?.parent?.slug === null,
orgSlug: org,
teamSlug: (event.team?.slug || teamMetadata?.requestedSlug) ?? null,
name: (event.owner?.organization?.name || event.team?.parent?.name || event.team?.name) ?? null,
},
isDynamic: false,
};
};
@ -218,20 +261,6 @@ function getProfileFromEvent(event: Event) {
if (!profile) throw new Error("Event has no owner");
const username = "username" in profile ? profile.username : team?.slug;
if (!username) {
if (event.slug === "test") {
// @TODO: This is a temporary debug statement that should be removed asap.
throw new Error(
"Ciaran event error" +
JSON.stringify(team) +
" -- " +
JSON.stringify(hosts) +
" -- " +
JSON.stringify(owner)
);
}
throw new Error("Event has no username/team slug");
}
const weekStart = hosts?.[0]?.user?.weekStart || owner?.weekStart || "Monday";
const basePath = team ? `/team/${username}` : `/${username}`;
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});

View File

@ -26,7 +26,7 @@ describe("Org Domains Utils", () => {
it("should return a non valid org domain", () => {
setupEnvs();
expect(orgDomainConfig("app.cal.com")).toEqual({
currentOrgDomain: "app",
currentOrgDomain: null,
isValidOrgDomain: false,
});
});

View File

@ -1,5 +1,6 @@
import { Prisma } from "@prisma/client";
import { getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
import prisma, { baseEventTypeSelect } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
@ -8,7 +9,13 @@ import { WEBAPP_URL } from "../../../constants";
export type TeamWithMembers = Awaited<ReturnType<typeof getTeamWithMembers>>;
export async function getTeamWithMembers(id?: number, slug?: string, userId?: number) {
export async function getTeamWithMembers(args: {
id?: number;
slug?: string;
userId?: number;
orgSlug?: string | null;
}) {
const { id, slug, userId, orgSlug } = args;
const userSelect = Prisma.validator<Prisma.UserSelect>()({
username: true,
email: true,
@ -92,6 +99,11 @@ export async function getTeamWithMembers(id?: number, slug?: string, userId?: nu
const where: Prisma.TeamFindFirstArgs["where"] = {};
if (userId) where.members = { some: { userId } };
if (orgSlug) {
where.parent = getSlugOrRequestedSlug(orgSlug);
} else {
where.parentId = null;
}
if (id) where.id = id;
if (slug) where.slug = slug;

View File

@ -1,5 +1,6 @@
import type { Prisma, UserPermissionRole } from "@prisma/client";
import { uuid } from "short-uuid";
import type z from "zod";
import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
@ -11,8 +12,12 @@ import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import prisma from ".";
import mainAppStore from "./seed-app-store";
import type { teamMetadataSchema } from "./zod-utils";
async function createUserAndEventType(opts: {
async function createUserAndEventType({
user,
eventTypes = [],
}: {
user: {
email: string;
password: string;
@ -23,20 +28,20 @@ async function createUserAndEventType(opts: {
role?: UserPermissionRole;
theme?: "dark" | "light";
};
eventTypes: Array<
Prisma.EventTypeCreateInput & {
eventTypes?: Array<
Prisma.EventTypeUncheckedCreateInput & {
_bookings?: Prisma.BookingCreateInput[];
}
>;
}) {
const userData = {
...opts.user,
password: await hashPassword(opts.user.password),
...user,
password: await hashPassword(user.password),
emailVerified: new Date(),
completedOnboarding: opts.user.completedOnboarding ?? true,
completedOnboarding: user.completedOnboarding ?? true,
locale: "en",
schedules:
opts.user.completedOnboarding ?? true
user.completedOnboarding ?? true
? {
create: {
name: "Working Hours",
@ -50,20 +55,20 @@ async function createUserAndEventType(opts: {
: undefined,
};
const user = await prisma.user.upsert({
where: { email_username: { email: opts.user.email, username: opts.user.username } },
const theUser = await prisma.user.upsert({
where: { email_username: { email: user.email, username: user.username } },
update: userData,
create: userData,
});
console.log(
`👤 Upserted '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${opts.user.username}`
`👤 Upserted '${user.username}' with email "${user.email}" & password "${user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}`
);
for (const eventTypeInput of opts.eventTypes) {
for (const eventTypeInput of eventTypes) {
const { _bookings: bookingFields = [], ...eventTypeData } = eventTypeInput;
eventTypeData.userId = user.id;
eventTypeData.users = { connect: { id: user.id } };
eventTypeData.userId = theUser.id;
eventTypeData.users = { connect: { id: theUser.id } };
const eventType = await prisma.eventType.findFirst({
where: {
@ -98,13 +103,13 @@ async function createUserAndEventType(opts: {
...bookingInput,
user: {
connect: {
email: opts.user.email,
email: user.email,
},
},
attendees: {
create: {
email: opts.user.email,
name: opts.user.name,
email: user.email,
name: user.name,
timeZone: "Europe/London",
},
},
@ -124,15 +129,32 @@ async function createUserAndEventType(opts: {
}
}
return user;
return theUser;
}
async function createTeamAndAddUsers(
teamInput: Prisma.TeamCreateInput,
users: { id: number; username: string; role?: MembershipRole }[]
users: { id: number; username: string; role?: MembershipRole }[] = []
) {
const checkUnpublishedTeam = async (slug: string) => {
return await prisma.team.findFirst({
where: {
metadata: {
path: ["requestedSlug"],
equals: slug,
},
},
});
};
const createTeam = async (team: Prisma.TeamCreateInput) => {
try {
const requestedSlug = (team.metadata as z.infer<typeof teamMetadataSchema>)?.requestedSlug;
if (requestedSlug) {
const unpublishedTeam = await checkUnpublishedTeam(requestedSlug);
if (unpublishedTeam) {
throw Error("Unique constraint failed on the fields");
}
}
return await prisma.team.create({
data: {
...team,
@ -168,6 +190,8 @@ async function createTeamAndAddUsers(
});
console.log(`\t👤 Added '${teamInput.name}' membership for '${username}' with role '${role}'`);
}
return team;
}
async function main() {
@ -178,7 +202,6 @@ async function main() {
username: "delete-me",
name: "delete-me",
},
eventTypes: [],
});
await createUserAndEventType({
@ -189,7 +212,6 @@ async function main() {
name: "onboarding",
completedOnboarding: false,
},
eventTypes: [],
});
await createUserAndEventType({
@ -279,19 +301,19 @@ async function main() {
title: "Zoom Event",
slug: "zoom",
length: 60,
locations: [{ type: zoomMeta.appData?.location.type }],
locations: [{ type: zoomMeta.appData?.location?.type }],
},
{
title: "Daily Event",
slug: "daily",
length: 60,
locations: [{ type: dailyMeta.appData?.location.type }],
locations: [{ type: dailyMeta.appData?.location?.type }],
},
{
title: "Google Meet",
slug: "google-meet",
length: 60,
locations: [{ type: googleMeetMeta.appData?.location.type }],
locations: [{ type: googleMeetMeta.appData?.location?.type }],
},
{
title: "Yoga class",
@ -503,7 +525,6 @@ async function main() {
username: "teamfree",
name: "Team Free Example",
},
eventTypes: [],
});
const proUserTeam = await createUserAndEventType({
@ -513,7 +534,6 @@ async function main() {
username: "teampro",
name: "Team Pro Example",
},
eventTypes: [],
});
await createUserAndEventType({
@ -525,7 +545,6 @@ async function main() {
name: "Admin Example",
role: "ADMIN",
},
eventTypes: [],
});
const pro2UserTeam = await createUserAndEventType({
@ -535,7 +554,6 @@ async function main() {
username: "teampro2",
name: "Team Pro Example 2",
},
eventTypes: [],
});
const pro3UserTeam = await createUserAndEventType({
@ -545,7 +563,6 @@ async function main() {
username: "teampro3",
name: "Team Pro Example 3",
},
eventTypes: [],
});
const pro4UserTeam = await createUserAndEventType({
@ -555,7 +572,6 @@ async function main() {
username: "teampro4",
name: "Team Pro Example 4",
},
eventTypes: [],
});
await createTeamAndAddUsers(

View File

@ -28,7 +28,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
where: {
slug: slug,
// If this is under an org, check that the team doesn't already exist
...(isOrgChildTeam && { parentId: user.organizationId }),
parentId: isOrgChildTeam ? user.organizationId : null,
},
});

View File

@ -15,7 +15,7 @@ type GetOptions = {
};
export const getHandler = async ({ ctx, input }: GetOptions) => {
const team = await getTeamWithMembers(input.teamId, undefined, ctx.user.id);
const team = await getTeamWithMembers({ id: input.teamId, userId: ctx.user.id });
if (!team) {
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found." });

View File

@ -0,0 +1,26 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen, Avatar } from "@calcom/ui";
export type UnpublishedEntityProps = {
teamSlug?: string | null;
orgSlug?: string | null;
name?: string | null;
};
export function UnpublishedEntity(props: UnpublishedEntityProps) {
const { t } = useLocale();
const slug = props.orgSlug || props.teamSlug;
return (
<div className="m-8 flex items-center justify-center">
<EmptyScreen
avatar={<Avatar alt={slug ?? ""} imageSrc={`/team/${slug}/avatar.png`} size="lg" />}
headline={t("team_is_unpublished", {
team: props.name,
})}
description={t("team_is_unpublished_description", {
entity: props.orgSlug ? t("organization").toLowerCase() : t("team").toLowerCase(),
})}
/>
</div>
);
}

View File

@ -0,0 +1,2 @@
export { UnpublishedEntity } from "./UnpublishedEntity";
export type { UnpublishedEntityProps } from "./UnpublishedEntity";

View File

@ -83,6 +83,7 @@ export type { AlertProps } from "./components/alert";
export { Credits } from "./components/credits";
export { Divider, VerticalDivider } from "./components/divider";
export { EmptyScreen } from "./components/empty-screen";
export { UnpublishedEntity } from "./components/unpublished-entity";
export { List, ListItem, ListItemText, ListItemTitle, ListLinkItem } from "./components/list";
export type { ListItemProps, ListProps } from "./components/list";
export { HeadSeo } from "./components/head-seo";