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>pull/10177/head^2
parent
4435451e9b
commit
4a6dc50909
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 || {});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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." });
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { UnpublishedEntity } from "./UnpublishedEntity";
|
||||
export type { UnpublishedEntityProps } from "./UnpublishedEntity";
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue