Merge branch 'main' into chore-team-metadata-table

This commit is contained in:
Keith Williams 2024-01-08 20:07:29 -03:00 committed by GitHub
commit 1f685822c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 274 additions and 87 deletions

View File

@ -19,12 +19,12 @@ Fixes # (issue)
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Tests (Unit/Integration/E2E or any other test)
- [ ] This change requires a documentation update
- Bug fix (non-breaking change which fixes an issue)
- Chore (refactoring code, technical debt, workflow improvements)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Tests (Unit/Integration/E2E or any other test)
- This change requires a documentation update
## How should this be tested?

View File

@ -17,10 +17,11 @@ jobs:
steps:
- uses: actions/stale@v7
with:
days-before-close: -1
days-before-issue-stale: 60
days-before-issue-close: -1
days-before-pr-stale: 14
days-before-pr-close: 7
days-before-pr-close: -1
stale-pr-message: "This PR is being marked as stale due to inactivity."
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
operations-per-run: 100

View File

@ -56,6 +56,41 @@ jobs:
uses: ./.github/workflows/production-build.yml
secrets: inherit
build-without-database:
name: Production build (without database)
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
e2e:
name: E2E tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e.yml
secrets: inherit
e2e-app-store:
name: E2E App Store tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: E2E embeds tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
analyze:
name: Analyze Build
needs: [changes, build]
@ -64,7 +99,7 @@ jobs:
secrets: inherit
required:
needs: [changes, lint, type-check, test, build]
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
steps:

View File

@ -2,9 +2,6 @@ name: Pre-release checks
on:
workflow_dispatch:
push:
branches:
- main
jobs:
changes:

View File

@ -67,5 +67,5 @@ const getPageProps = async ({ params }: { params: Record<string, string | string
};
// @ts-expect-error getData arg
export default WithLayout({ getData: getPageProps, Page: CategoryPage })<P>;
export default WithLayout({ getData: getPageProps, Page: CategoryPage })<"P">;
export const dynamic = "force-static";

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-appearance-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("booking_appearance"),
(t) => t("appearance_team_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/billing/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("billing"),
(t) => t("team_billing_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-members-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("team_members"),
(t) => t("members_team_description")
);
export default Page;

View File

@ -0,0 +1,11 @@
import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/onboard-members";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("add_team_members"),
(t) => t("add_team_members_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-profile-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("profile"),
(t) => t("profile_team_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/sso/page/teams-sso-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("sso_configuration"),
(t) => t("sso_configuration_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import LegacyPage, { LayoutWrapper } from "@pages/settings/teams/new/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("create_new_team"),
(t) => t("create_new_team_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper })<"P">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-listing-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
export default Page;

View File

@ -3,12 +3,14 @@ import prismock from "../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import type { z } from "zod";
import { WEBSITE_URL } from "@calcom/lib/constants";
import type { MembershipRole, Prisma } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/enums";
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { moveTeamToOrg, moveUserToOrg, removeTeamFromOrg, removeUserFromOrg } from "./orgMigration";
const WEBSITE_PROTOCOL = new URL(WEBSITE_URL).protocol;
describe("orgMigration", () => {
describe("moveUserToOrg", () => {
describe("when user email does not match orgAutoAcceptEmail", () => {
@ -1372,7 +1374,7 @@ async function expectRedirectToBeEnabled({
}
expect(redirect).not.toBeNull();
expect(redirect?.toUrl).toBe(`http://${orgSlug}.cal.local:3000/${to}`);
expect(redirect?.toUrl).toBe(`${WEBSITE_PROTOCOL}//${orgSlug}.cal.local:3000/${to}`);
if (!redirect) {
throw new Error(`Redirect doesn't exist for ${JSON.stringify(tempOrgRedirectWhere)}`);
}

View File

@ -486,48 +486,24 @@ export default function Success(props: SuccessProps) {
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3" data-testid="where">
{!rescheduleLocation || locationToDisplay === rescheduleLocationToDisplay ? (
locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
target="_blank"
title={locationToDisplay}
className="text-default flex items-center gap-2"
rel="noreferrer">
{providerName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
locationToDisplay
)
<DisplayLocation
locationToDisplay={locationToDisplay}
providerName={providerName}
/>
) : (
<>
{!!formerTime &&
(locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
target="_blank"
title={locationToDisplay}
className="text-default flex items-center gap-2 line-through"
rel="noreferrer">
{providerName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
<p className="line-through">{locationToDisplay}</p>
))}
{rescheduleLocationToDisplay.startsWith("http") ? (
<a
href={rescheduleLocationToDisplay}
target="_blank"
title={rescheduleLocationToDisplay}
className="text-default flex items-center gap-2"
rel="noreferrer">
{rescheduleProviderName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
rescheduleLocationToDisplay
{!!formerTime && (
<DisplayLocation
locationToDisplay={locationToDisplay}
providerName={providerName}
className="line-through"
/>
)}
<DisplayLocation
locationToDisplay={rescheduleLocationToDisplay}
providerName={rescheduleProviderName}
/>
</>
)}
</div>
@ -830,6 +806,29 @@ export default function Success(props: SuccessProps) {
);
}
const DisplayLocation = ({
locationToDisplay,
providerName,
className,
}: {
locationToDisplay: string;
providerName?: string;
className?: string;
}) =>
locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
target="_blank"
title={locationToDisplay}
className={classNames("text-default flex items-center gap-2", className)}
rel="noreferrer">
{providerName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
<p className={className}>{locationToDisplay}</p>
);
Success.isBookingPage = true;
Success.PageWrapper = PageWrapper;

View File

@ -1,3 +1,5 @@
"use client";
import { usePathname } from "next/navigation";
import { useIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom";

View File

@ -1,3 +1,5 @@
"use client";
import Head from "next/head";
import AddNewTeamMembers from "@calcom/features/ee/teams/components/AddNewTeamMembers";
@ -19,12 +21,13 @@ const OnboardTeamMembersPage = () => {
);
};
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
export const GetLayout = (page: React.ReactElement) => (
<WizardLayout currentStep={2} maxSteps={2}>
{page}
</WizardLayout>
);
OnboardTeamMembersPage.getLayout = GetLayout;
OnboardTeamMembersPage.PageWrapper = PageWrapper;
export default OnboardTeamMembersPage;

View File

@ -1,3 +1,5 @@
"use client";
import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
@ -18,7 +20,7 @@ const CreateNewTeamPage = () => {
</>
);
};
const LayoutWrapper = (page: React.ReactElement) => {
export const LayoutWrapper = (page: React.ReactElement) => {
return (
<WizardLayout currentStep={1} maxSteps={2}>
{page}

View File

@ -333,11 +333,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});
}
const videoReferences = bookingObj.references.filter((reference) => reference.type.includes("_video"));
const latestVideoReference = videoReferences[videoReferences.length - 1];
return {
props: {
meetingUrl: bookingObj.references[0].meetingUrl ?? "",
...(typeof bookingObj.references[0].meetingPassword === "string" && {
meetingPassword: bookingObj.references[0].meetingPassword,
meetingUrl: latestVideoReference.meetingUrl ?? "",
...(typeof latestVideoReference.meetingPassword === "string" && {
meetingPassword: latestVideoReference.meetingPassword,
}),
booking: {
...bookingObj,

View File

@ -214,7 +214,7 @@ test.describe("Booking limits", () => {
await page.goto(slotUrl);
await bookTimeSlot(page);
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 5000 });
});
await test.step(`month after booking`, async () => {
@ -224,7 +224,9 @@ test.describe("Booking limits", () => {
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
// the month after we made bookings should have availability unless we hit a yearly limit
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
// TODO: Temporary fix for failing test. It passes locally but fails on CI.
// See #13097
// await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
});
// increment date by unit after hitting each limit

View File

@ -751,7 +751,7 @@ export async function makePaymentUsingStripe(page: Page) {
const stripeFrame = stripeElement.frameLocator("iframe").first();
await stripeFrame.locator('[name="number"]').fill("4242 4242 4242 4242");
const now = new Date();
await stripeFrame.locator('[name="expiry"]').fill(`${now.getMonth()} / ${now.getFullYear() + 1}`);
await stripeFrame.locator('[name="expiry"]').fill(`${now.getMonth() + 1} / ${now.getFullYear() + 1}`);
await stripeFrame.locator('[name="cvc"]').fill("111");
const postcalCodeIsVisible = await stripeFrame.locator('[name="postalCode"]').isVisible();
if (postcalCodeIsVisible) {

View File

@ -1,9 +1,11 @@
import type { TestFunction } from "vitest";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { test } from "@calcom/web/test/fixtures/fixtures";
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
import { createOrganization } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
const WEBSITE_PROTOCOL = new URL(WEBSITE_URL).protocol;
const _testWithAndWithoutOrg = (
description: Parameters<typeof testWithAndWithoutOrg>[0],
fn: Parameters<typeof testWithAndWithoutOrg>[1],
@ -28,7 +30,7 @@ const _testWithAndWithoutOrg = (
skip,
org: {
organization: org,
urlOrigin: `http://${org.slug}.cal.local:3000`,
urlOrigin: `${WEBSITE_PROTOCOL}//${org.slug}.cal.local:3000`,
},
});
},

View File

@ -553,11 +553,18 @@ export default function RouteBuilder({
<SingleForm
form={form}
appUrl={appUrl}
Page={({ hookForm, form }) => (
<div className="route-config">
<Routes hookForm={hookForm} appUrl={appUrl} form={form} />
</div>
)}
Page={({ hookForm, form }) => {
// If hookForm hasn't been initialized, don't render anything
// This is important here because some states get initialized which aren't reset when the hookForm is reset with the form values and they don't get the updated values
if (!hookForm.getValues().id) {
return null;
}
return (
<div className="route-config">
<Routes hookForm={hookForm} appUrl={appUrl} form={form} />
</div>
);
}}
/>
);
}

View File

@ -33,7 +33,9 @@ import { useBrandColors } from "./utils/use-brand-colors";
const loadFramerFeatures = () => import("./framer-features").then((res) => res.default);
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const UnpublishedEntity = dynamic(() => import("@calcom/ui").then((mod) => mod.UnpublishedEntity));
const UnpublishedEntity = dynamic(() =>
import("@calcom/ui/components/unpublished-entity/UnpublishedEntity").then((mod) => mod.UnpublishedEntity)
);
const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), {
ssr: false,
});

View File

@ -15,9 +15,12 @@ import { useBookerStore } from "../store";
import { FromToTime } from "../utils/dates";
import { useEvent } from "../utils/event";
const TimezoneSelect = dynamic(() => import("@calcom/ui").then((mod) => mod.TimezoneSelect), {
ssr: false,
});
const TimezoneSelect = dynamic(
() => import("@calcom/ui/components/form/timezone-select/TimezoneSelect").then((mod) => mod.TimezoneSelect),
{
ssr: false,
}
);
export const EventMeta = () => {
const { setTimezone, timeFormat, timezone } = useTimePreferences();

View File

@ -1,7 +1,7 @@
import type { Prisma } from "@prisma/client";
import type { IncomingMessage } from "http";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { IS_PRODUCTION, WEBSITE_URL } from "@calcom/lib/constants";
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import slugify from "@calcom/lib/slugify";
@ -100,9 +100,10 @@ export function subdomainSuffix() {
}
export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) {
if (!slug) return options.protocol ? WEBAPP_URL : WEBAPP_URL.replace("https://", "").replace("http://", "");
if (!slug)
return options.protocol ? WEBSITE_URL : WEBSITE_URL.replace("https://", "").replace("http://", "");
const orgFullOrigin = `${
options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""
options.protocol ? `${new URL(WEBSITE_URL).protocol}//` : ""
}${slug}.${subdomainSuffix()}`;
return orgFullOrigin;
}

View File

@ -1,3 +1,5 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

View File

@ -107,9 +107,7 @@ export default function TeamListItem(props: Props) {
<span className="text-default text-sm font-bold">{team.name}</span>
<span className="text-muted block text-xs">
{team.slug ? (
`${getTeamUrlSync({ orgSlug: team.parent ? team.parent.slug : null, teamSlug: team.slug })}/${
team.slug
}`
`${getTeamUrlSync({ orgSlug: team.parent ? team.parent.slug : null, teamSlug: team.slug })}`
) : (
<Badge>{t("upgrade")}</Badge>
)}
@ -245,11 +243,10 @@ export default function TeamListItem(props: Props) {
color="secondary"
onClick={() => {
navigator.clipboard.writeText(
`${
orgBranding
? `${orgBranding.fullDomain}`
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team`
}/${team.slug}`
`${getTeamUrlSync({
orgSlug: team.parent ? team.parent.slug : null,
teamSlug: team.slug,
})}`
);
showToast(t("link_copied"), "success");
}}
@ -285,11 +282,10 @@ export default function TeamListItem(props: Props) {
<DropdownItem
type="button"
target="_blank"
href={`${
orgBranding
? `${orgBranding.fullDomain}`
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team`
}/${team.slug}`}
href={`${getTeamUrlSync({
orgSlug: team.parent ? team.parent.slug : null,
teamSlug: team.slug,
})}`}
StartIcon={ExternalLink}>
{t("preview_team") as string}
</DropdownItem>

View File

@ -1,3 +1,5 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";

View File

@ -1,3 +1,5 @@
"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta } from "@calcom/ui";

View File

@ -1,3 +1,5 @@
"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState } from "react";

View File

@ -1,3 +1,5 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import type { Prisma } from "@prisma/client";
import { useSession } from "next-auth/react";

View File

@ -10,7 +10,9 @@
"./components/icon": "./components/icon/index.ts",
"./components/icon/Discord": "./components/icon/Discord.tsx",
"./components/icon/SatSymbol": "./components/icon/SatSymbol.tsx",
"./components/icon/Spinner": "./components/icon/Spinner.tsx"
"./components/icon/Spinner": "./components/icon/Spinner.tsx",
"./components/unpublished-entity/UnpublishedEntity": "./components/unpublished-entity/index.ts",
"./components/form/timezone-select/TimezoneSelect": "./components/form/timezone-select/index.ts"
},
"types": "./index.tsx",
"license": "MIT",

View File

@ -112,6 +112,7 @@
"cache": false
},
"dx": {
"dependsOn": ["//#env-check:common", "//#env-check:app-store"],
"cache": false
},
"lint": {