Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: Hariom <hariombalhara@gmail.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
co-authored by
Amit Sharma
Hariom
Syed Ali Shahbaz
parent
b5c6d7af91
commit
f631abfdc4
@@ -5,6 +5,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
@@ -51,7 +52,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
select: {
|
||||
id: true,
|
||||
hideBranding: true,
|
||||
parent: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
name: true,
|
||||
bannerUrl: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
name: true,
|
||||
slug: true,
|
||||
eventTypes: {
|
||||
@@ -66,6 +78,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
length: true,
|
||||
},
|
||||
},
|
||||
isOrganization: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -98,6 +116,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
eventData,
|
||||
});
|
||||
|
||||
const organizationSettings = OrganizationRepository.utils.getOrganizationSEOSettings(team);
|
||||
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
@@ -124,6 +145,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
isSEOIndexable: allowSEOIndexing,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,10 +7,11 @@ import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
import type { Team, OrganizationSettings } from "@calcom/prisma/client";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -20,6 +21,30 @@ import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["team/[slug]"] });
|
||||
|
||||
function getOrgProfileRedirectToVerifiedDomain(
|
||||
team: {
|
||||
isOrganization: boolean;
|
||||
},
|
||||
settings: Pick<OrganizationSettings, "orgAutoAcceptEmail" | "orgProfileRedirectsToVerifiedDomain">
|
||||
) {
|
||||
if (!team.isOrganization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const verifiedDomain = OrganizationRepository.utils.getVerifiedDomain(settings);
|
||||
|
||||
if (!settings.orgProfileRedirectsToVerifiedDomain || !verifiedDomain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `https://${verifiedDomain}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const getTheLastArrayElement = (value: ReadonlyArray<string> | string | undefined): string | undefined => {
|
||||
if (value === undefined || typeof value === "string") {
|
||||
return value;
|
||||
@@ -120,6 +145,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
}
|
||||
|
||||
const organizationSettings = OrganizationRepository.utils.getOrganizationSettings(team);
|
||||
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
|
||||
|
||||
const redirectToVerifiedDomain = organizationSettings
|
||||
? getOrgProfileRedirectToVerifiedDomain(team, organizationSettings)
|
||||
: null;
|
||||
|
||||
if (redirectToVerifiedDomain) {
|
||||
return redirectToVerifiedDomain;
|
||||
}
|
||||
|
||||
const isTeamOrParentOrgPrivate = team.isPrivate || (team.parent?.isOrganization && team.parent?.isPrivate);
|
||||
|
||||
team.eventTypes =
|
||||
@@ -189,6 +225,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
markdownStrippedBio,
|
||||
isValidOrgDomain,
|
||||
currentOrgDomain,
|
||||
isSEOIndexable: allowSEOIndexing,
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -180,7 +180,11 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
const [isAllowDynamicBookingChecked, setIsAllowDynamicBookingChecked] = useState(
|
||||
!!user.allowDynamicBooking
|
||||
);
|
||||
const [isAllowSEOIndexingChecked, setIsAllowSEOIndexingChecked] = useState(!!user.allowSEOIndexing);
|
||||
const [isAllowSEOIndexingChecked, setIsAllowSEOIndexingChecked] = useState(
|
||||
user.organizationSettings?.allowSEOIndexing === false
|
||||
? !!user.organizationSettings?.allowSEOIndexing
|
||||
: !!user.allowSEOIndexing
|
||||
);
|
||||
const [isReceiveMonthlyDigestEmailChecked, setIsReceiveMonthlyDigestEmailChecked] = useState(
|
||||
!!user.receiveMonthlyDigestEmail
|
||||
);
|
||||
@@ -359,10 +363,11 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
data-testid="my-seo-indexing-switch"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("seo_indexing")}
|
||||
description={t("allow_seo_indexing")}
|
||||
disabled={mutation.isPending}
|
||||
disabled={mutation.isPending || user.organizationSettings?.allowSEOIndexing === false}
|
||||
checked={isAllowSEOIndexingChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsAllowSEOIndexingChecked(checked);
|
||||
|
||||
@@ -35,6 +35,7 @@ function TeamPage({
|
||||
markdownStrippedBio,
|
||||
isValidOrgDomain,
|
||||
currentOrgDomain,
|
||||
isSEOIndexable,
|
||||
}: PageProps) {
|
||||
useTheme(team.theme);
|
||||
const routerQuery = useRouterQuery();
|
||||
@@ -175,6 +176,10 @@ function TeamPage({
|
||||
image: profileImageSrc,
|
||||
},
|
||||
}}
|
||||
nextSeoProps={{
|
||||
nofollow: !isSEOIndexable,
|
||||
noindex: !isSEOIndexable,
|
||||
}}
|
||||
/>
|
||||
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
|
||||
<div className="mx-auto mb-8 max-w-3xl text-center">
|
||||
|
||||
@@ -34,6 +34,7 @@ function Type({
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
isSEOIndexable,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -47,6 +48,7 @@ function Type({
|
||||
isTeamEvent
|
||||
entity={eventData.entity}
|
||||
bookingData={booking}
|
||||
isSEOIndexable={isSEOIndexable}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
|
||||
@@ -47,6 +47,7 @@ function mockedUserPageComponentProps(props: Partial<React.ComponentProps<typeof
|
||||
...(props.entity ?? null),
|
||||
},
|
||||
eventTypes: [],
|
||||
isOrgSEOIndexable: false,
|
||||
} satisfies React.ComponentProps<typeof UserPage>;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
|
||||
|
||||
export type PageProps = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||
export function UserPage(props: PageProps) {
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity, isOrgSEOIndexable } = props;
|
||||
|
||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||
useTheme(profile.theme);
|
||||
@@ -59,6 +59,13 @@ export function UserPage(props: PageProps) {
|
||||
|
||||
const isEventListEmpty = eventTypes.length === 0;
|
||||
const isOrg = !!user?.profile?.organization;
|
||||
|
||||
const allowSEOIndexing = isOrg
|
||||
? isOrgSEOIndexable
|
||||
? profile.allowSEOIndexing
|
||||
: false
|
||||
: profile.allowSEOIndexing;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
@@ -71,8 +78,8 @@ export function UserPage(props: PageProps) {
|
||||
users: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||
}}
|
||||
nextSeoProps={{
|
||||
noindex: !profile.allowSEOIndexing,
|
||||
nofollow: !profile.allowSEOIndexing,
|
||||
noindex: !allowSEOIndexing,
|
||||
nofollow: !allowSEOIndexing,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { doOnOrgDomain } from "playwright/lib/testUtils";
|
||||
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.afterEach(({ users }) => {
|
||||
users.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Organization Settings", () => {
|
||||
test("Setting - 'Allow search engine indexing' inside Org profile settings", async ({ page, users }) => {
|
||||
const orgMember = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
hasSubteam: true,
|
||||
isOrgVerified: true,
|
||||
isDnsSetup: true,
|
||||
orgRequestedSlug: "example",
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
});
|
||||
const { team: org } = await orgMember.getOrgMembership();
|
||||
const { team } = await orgMember.getFirstTeamMembership();
|
||||
const teamEvent = await orgMember.getFirstTeamEvent(team.id);
|
||||
const userEvent = orgMember.eventTypes[0];
|
||||
|
||||
await orgMember.apiLogin();
|
||||
|
||||
await test.step("Set to true, enables 'Allow search engine indexing' switch on my-account/general settings", async () => {
|
||||
await page.goto(`/settings/organizations/profile`);
|
||||
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
|
||||
await orgSeoIndexingSwitch.click();
|
||||
await expect(orgSeoIndexingSwitch).toBeChecked();
|
||||
|
||||
await page.goto(`/settings/my-account/general`);
|
||||
const mySeoIndexingSwitch = await page.getByTestId("my-seo-indexing-switch");
|
||||
await expect(mySeoIndexingSwitch).toBeEnabled();
|
||||
});
|
||||
|
||||
await test.step("Set to false, disables and sets to false 'Allow search engine indexing' switch on my-account/general settings", async () => {
|
||||
await page.goto(`/settings/organizations/profile`);
|
||||
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
|
||||
await orgSeoIndexingSwitch.click();
|
||||
await expect(orgSeoIndexingSwitch).toBeChecked({ checked: false });
|
||||
|
||||
await page.goto(`/settings/my-account/general`);
|
||||
const mySeoIndexingSwitch = await page.getByTestId("my-seo-indexing-switch");
|
||||
await expect(mySeoIndexingSwitch).toBeChecked({ checked: false });
|
||||
await expect(mySeoIndexingSwitch).toBeDisabled();
|
||||
});
|
||||
|
||||
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for team page", async () => {
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/team/${team.slug}`);
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for team event page", async () => {
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/team/${team.slug}/${teamEvent.slug}`);
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for user page", async () => {
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/${orgMember.username}`);
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for user eventtype page", async () => {
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/${orgMember.username}/${userEvent.slug}`);
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to true, sets 'robots' meta on 'head' element to 'index,follow', for team page", async () => {
|
||||
await page.goto(`/settings/organizations/profile`);
|
||||
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
|
||||
await orgSeoIndexingSwitch.click();
|
||||
await expect(orgSeoIndexingSwitch).toBeChecked();
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/team/${team.slug}`);
|
||||
await page.reload();
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to true, sets 'robots' meta on 'head' element to 'index,follow', for team event page", async () => {
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/team/${team.slug}/${teamEvent.slug}`);
|
||||
await page.reload();
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to true and user's 'Allow search engine indexing' setting also set to true, user's setting is respected, for user page", async () => {
|
||||
await page.goto(`/settings/my-account/general`);
|
||||
const mySeoIndexingSwitch = await page.getByTestId("my-seo-indexing-switch");
|
||||
await expect(mySeoIndexingSwitch).toBeEnabled();
|
||||
await expect(mySeoIndexingSwitch).toBeChecked();
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/${orgMember.username}`);
|
||||
await page.reload();
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to true and user's 'Allow search engine indexing' setting also set to true, user's setting is respected, for user eventtype page", async () => {
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/${orgMember.username}/${userEvent.slug}`);
|
||||
await page.reload();
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await test.step("Set to false and user's 'Allow search engine indexing' setting was true, org's setting is prioritized, for user eventtype page", async () => {
|
||||
await page.goto(`/settings/organizations/profile`);
|
||||
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
|
||||
await orgSeoIndexingSwitch.click();
|
||||
await expect(orgSeoIndexingSwitch).toBeChecked({ checked: false });
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/${orgMember.username}/${userEvent.slug}`);
|
||||
await page.reload();
|
||||
const metaTag = await page.locator('head > meta[name="robots"]');
|
||||
const metaTagValue = await metaTag.getAttribute("content");
|
||||
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
|
||||
return { url: result.url };
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2819,7 +2819,9 @@
|
||||
"rr_distribution_method_availability_description": "Allows bookers to book meetings whenever a host is available. Use this to maximize the number of potential meetings booked and when the even distribution of meetings across hosts is less important.",
|
||||
"rr_distribution_method_balanced_title": "Load balancing",
|
||||
"rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.",
|
||||
"exclude_emails_that_contain" : "Exclude emails that contain ...",
|
||||
"exclude_emails_match_found_error_message" : "Please enter a valid work email address",
|
||||
"exclude_emails_that_contain": "Exclude emails that contain ...",
|
||||
"exclude_emails_match_found_error_message": "Please enter a valid work email address",
|
||||
"disable_org_url_label": "Disable public organization profile and redirect",
|
||||
"disable_org_url_description": "Redirects {{orgSlug}}.cal.com to {{destination}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -219,6 +219,12 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
const allowSEOIndexing = org
|
||||
? user?.profile?.organization?.organizationSettings?.allowSEOIndexing
|
||||
? user?.allowSEOIndexing
|
||||
: false
|
||||
: user?.allowSEOIndexing;
|
||||
|
||||
const props: Props = {
|
||||
eventData: {
|
||||
id: eventData.id,
|
||||
@@ -230,7 +236,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
isSEOIndexable: allowSEOIndexing,
|
||||
themeBasis: username,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: null,
|
||||
|
||||
@@ -69,6 +69,7 @@ type UserPageProps = {
|
||||
| "currency"
|
||||
| "recurringEvent"
|
||||
>)[];
|
||||
isOrgSEOIndexable: boolean | undefined;
|
||||
} & EmbedProps;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
|
||||
@@ -194,6 +195,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
themeBasis: user.username,
|
||||
trpcState: ssr.dehydrate(),
|
||||
markdownStrippedBio,
|
||||
isOrgSEOIndexable: org?.organizationSettings?.allowSEOIndexing,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -43,6 +43,12 @@ const OrgAppearanceView = ({
|
||||
} = themeForm;
|
||||
|
||||
const [hideBrandingValue, setHideBrandingValue] = useState(currentOrg?.hideBranding ?? false);
|
||||
const [allowSEOIndexingValue, setAllowSEOIndexingValue] = useState(
|
||||
currentOrg?.organizationSettings?.allowSEOIndexing ?? false
|
||||
);
|
||||
const [orgProfileRedirectsToVerifiedDomainValue, setOrgProfileRedirectsToVerifiedDomainValue] = useState(
|
||||
currentOrg?.organizationSettings?.orgProfileRedirectsToVerifiedDomain ?? false
|
||||
);
|
||||
|
||||
const brandColorsFormMethods = useForm<BrandColorsFormValues>({
|
||||
defaultValues: {
|
||||
@@ -153,6 +159,36 @@ const OrgAppearanceView = ({
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
data-testid={`${currentOrg?.id}-seo-indexing-switch`}
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("seo_indexing")}
|
||||
description={t("allow_seo_indexing")}
|
||||
disabled={mutation.isPending}
|
||||
checked={allowSEOIndexingValue}
|
||||
onCheckedChange={(checked) => {
|
||||
setAllowSEOIndexingValue(checked);
|
||||
mutation.mutate({ allowSEOIndexing: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("disable_org_url_label")}
|
||||
description={t("disable_org_url_description", {
|
||||
orgSlug: currentOrg?.slug,
|
||||
destination: currentOrg?.organizationSettings?.orgAutoAcceptEmail,
|
||||
})}
|
||||
disabled={mutation.isPending}
|
||||
checked={orgProfileRedirectsToVerifiedDomainValue}
|
||||
onCheckedChange={(checked) => {
|
||||
setOrgProfileRedirectsToVerifiedDomainValue(checked);
|
||||
mutation.mutate({ orgProfileRedirectsToVerifiedDomain: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -101,6 +101,13 @@ export async function getTeamWithMembers(args: {
|
||||
isOrganization: true,
|
||||
logoUrl: true,
|
||||
metadata: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
allowSEOIndexing: true,
|
||||
orgProfileRedirectsToVerifiedDomain: true,
|
||||
orgAutoAcceptEmail: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
parentId: true,
|
||||
@@ -158,6 +165,13 @@ export async function getTeamWithMembers(args: {
|
||||
identifier: true,
|
||||
},
|
||||
},
|
||||
organizationSettings: {
|
||||
select: {
|
||||
allowSEOIndexing: true,
|
||||
orgProfileRedirectsToVerifiedDomain: true,
|
||||
orgAutoAcceptEmail: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { OrganizationSettings } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -10,6 +11,11 @@ import { createAProfileForAnExistingUser } from "../../createAProfileForAnExisti
|
||||
import { getParsedTeam } from "./teamUtils";
|
||||
import { UserRepository } from "./user";
|
||||
|
||||
type MinimumOrganizationSettings = Pick<
|
||||
OrganizationSettings,
|
||||
"orgAutoAcceptEmail" | "orgProfileRedirectsToVerifiedDomain" | "allowSEOIndexing"
|
||||
>;
|
||||
type SEOOrganizationSettings = Pick<OrganizationSettings, "allowSEOIndexing">;
|
||||
const orgSelect = {
|
||||
id: true,
|
||||
name: true,
|
||||
@@ -215,6 +221,9 @@ export class OrganizationRepository {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
adminGetsNoSlotsNotification: true,
|
||||
isAdminReviewed: true,
|
||||
allowSEOIndexing: true,
|
||||
orgProfileRedirectsToVerifiedDomain: true,
|
||||
orgAutoAcceptEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -229,6 +238,9 @@ export class OrganizationRepository {
|
||||
organizationSettings: {
|
||||
lockEventTypeCreationForUsers: organizationSettings?.lockEventTypeCreationForUsers,
|
||||
adminGetsNoSlotsNotification: organizationSettings?.adminGetsNoSlotsNotification,
|
||||
allowSEOIndexing: organizationSettings?.allowSEOIndexing,
|
||||
orgProfileRedirectsToVerifiedDomain: organizationSettings?.orgProfileRedirectsToVerifiedDomain,
|
||||
orgAutoAcceptEmail: organizationSettings?.orgAutoAcceptEmail,
|
||||
},
|
||||
user: {
|
||||
role: membership?.role,
|
||||
@@ -311,4 +323,38 @@ export class OrganizationRepository {
|
||||
|
||||
return org?.calVideoLogo;
|
||||
}
|
||||
|
||||
static utils = {
|
||||
/**
|
||||
* Gets the organization setting if the team is an organization.
|
||||
* If not, it gets the organization setting of the parent organization.
|
||||
*/
|
||||
getOrganizationSettings: (team: {
|
||||
isOrganization: boolean;
|
||||
organizationSettings: MinimumOrganizationSettings | null;
|
||||
parent: {
|
||||
organizationSettings: MinimumOrganizationSettings | null;
|
||||
} | null;
|
||||
}) => {
|
||||
if (!team) return null;
|
||||
if (team.isOrganization) return team.organizationSettings ?? null;
|
||||
if (!team.parent) return null;
|
||||
return team.parent.organizationSettings ?? null;
|
||||
},
|
||||
getOrganizationSEOSettings: (team: {
|
||||
isOrganization: boolean;
|
||||
organizationSettings: SEOOrganizationSettings | null;
|
||||
parent: {
|
||||
organizationSettings: SEOOrganizationSettings | null;
|
||||
} | null;
|
||||
}) => {
|
||||
if (!team) return null;
|
||||
if (team.isOrganization) return team.organizationSettings ?? null;
|
||||
if (!team.parent) return null;
|
||||
return team.parent.organizationSettings ?? null;
|
||||
},
|
||||
getVerifiedDomain(settings: Pick<OrganizationSettings, "orgAutoAcceptEmail">) {
|
||||
return settings.orgAutoAcceptEmail;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ const membershipSelect = Prisma.validator<Prisma.MembershipSelect>()({
|
||||
});
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["repository/profile"] });
|
||||
const organizationSettingsSelect = Prisma.validator<Prisma.OrganizationSettingsSelect>()({
|
||||
allowSEOIndexing: true,
|
||||
orgProfileRedirectsToVerifiedDomain: true,
|
||||
});
|
||||
const organizationSelect = {
|
||||
id: true,
|
||||
slug: true,
|
||||
@@ -45,6 +49,12 @@ const organizationSelect = {
|
||||
bannerUrl: true,
|
||||
isPlatform: true,
|
||||
};
|
||||
const organizationWithSettingsSelect = {
|
||||
...organizationSelect,
|
||||
organizationSettings: {
|
||||
select: organizationSettingsSelect,
|
||||
},
|
||||
};
|
||||
|
||||
export enum LookupTarget {
|
||||
User,
|
||||
@@ -373,6 +383,7 @@ export class ProfileRepository {
|
||||
organizationSettings: {
|
||||
select: {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
@@ -416,7 +427,7 @@ export class ProfileRepository {
|
||||
select: userSelect,
|
||||
},
|
||||
organization: {
|
||||
select: organizationSelect,
|
||||
select: organizationWithSettingsSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrganizationSettings" ADD COLUMN "allowSEOIndexing" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "orgProfileRedirectsToVerifiedDomain" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -469,20 +469,24 @@ model Team {
|
||||
}
|
||||
|
||||
model OrganizationSettings {
|
||||
id Int @id @default(autoincrement())
|
||||
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId Int @unique
|
||||
isOrganizationConfigured Boolean @default(false)
|
||||
id Int @id @default(autoincrement())
|
||||
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId Int @unique
|
||||
isOrganizationConfigured Boolean @default(false)
|
||||
// It decides if new organization members can be auto-accepted or not
|
||||
isOrganizationVerified Boolean @default(false)
|
||||
orgAutoAcceptEmail String
|
||||
lockEventTypeCreationForUsers Boolean @default(false)
|
||||
adminGetsNoSlotsNotification Boolean @default(false)
|
||||
isOrganizationVerified Boolean @default(false)
|
||||
// It is a domain e.g "acme.com". Any email with this domain might be auto-accepted
|
||||
// Also, it is the domain to which the organization profile is redirected.
|
||||
orgAutoAcceptEmail String
|
||||
lockEventTypeCreationForUsers Boolean @default(false)
|
||||
adminGetsNoSlotsNotification Boolean @default(false)
|
||||
// It decides if instance ADMIN has reviewed the organization or not.
|
||||
// It is used to allow super sensitive operations like 'impersonation of Org members by Org admin'
|
||||
isAdminReviewed Boolean @default(false)
|
||||
dSyncData DSyncData?
|
||||
isAdminAPIEnabled Boolean @default(false)
|
||||
isAdminReviewed Boolean @default(false)
|
||||
dSyncData DSyncData?
|
||||
isAdminAPIEnabled Boolean @default(false)
|
||||
allowSEOIndexing Boolean @default(false)
|
||||
orgProfileRedirectsToVerifiedDomain Boolean @default(false)
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
|
||||
@@ -91,6 +91,7 @@ export const meHandler = async ({ ctx, input }: MeOptions) => {
|
||||
username: user.profile?.username ?? user.username ?? null,
|
||||
profile: user.profile ?? null,
|
||||
profiles: allUserEnrichedProfiles,
|
||||
organizationSettings: user?.profile?.organization?.organizationSettings,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,6 +41,14 @@ const updateOrganizationSettings = async ({
|
||||
data.adminGetsNoSlotsNotification = input.adminGetsNoSlotsNotification;
|
||||
}
|
||||
|
||||
if (input.hasOwnProperty("allowSEOIndexing")) {
|
||||
data.allowSEOIndexing = input.allowSEOIndexing;
|
||||
}
|
||||
|
||||
if (input.hasOwnProperty("orgProfileRedirectsToVerifiedDomain")) {
|
||||
data.orgProfileRedirectsToVerifiedDomain = input.orgProfileRedirectsToVerifiedDomain;
|
||||
}
|
||||
|
||||
// If no settings values have changed lets skip this update
|
||||
if (Object.keys(data).length === 0) return;
|
||||
|
||||
@@ -131,7 +139,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
|
||||
if (!prevOrganisation) throw new TRPCError({ code: "NOT_FOUND", message: "Organisation not found." });
|
||||
|
||||
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata);
|
||||
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata ?? {});
|
||||
|
||||
const data: Prisma.TeamUpdateArgs["data"] = {
|
||||
logoUrl: input.logoUrl,
|
||||
|
||||
@@ -31,6 +31,8 @@ export const ZUpdateInputSchema = z.object({
|
||||
lockEventTypeCreation: z.boolean().optional(),
|
||||
lockEventTypeCreationOptions: z.enum(["DELETE", "HIDE"]).optional(),
|
||||
adminGetsNoSlotsNotification: z.boolean().optional(),
|
||||
allowSEOIndexing: z.boolean().optional(),
|
||||
orgProfileRedirectsToVerifiedDomain: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user