Merge commit 'fcc50c1d0f90d77253455e7caadab383a35ebefb' into testE2E-timezone

This commit is contained in:
gitstart-calcom 2024-01-08 22:35:07 +00:00
commit 612ef2678c
48 changed files with 483 additions and 134 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

@ -81,7 +81,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
const ActionButtons = () => {
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
<div className="me-2 ms-2 flex flex-row space-x-2">
<div className="relative bottom-[6px] me-2 ms-2 flex flex-row space-x-2">
<Button
type="button"
onClick={() => setOpenDialogSaveUsername(true)}
@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-6 flex flex-row">
<span className={classNames("mx-2 py-3.5")}>
{usernameIsAvailable ? <Check className="h-4 w-4" /> : <></>}
{usernameIsAvailable ? <Check className="relative bottom-[6px] h-4 w-4" /> : <></>}
</span>
</div>
)}

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", () => {
@ -317,11 +319,13 @@ describe("orgMigration", () => {
await expectTeamToBeAPartOfOrg({
teamId: team1.id,
orgId: dbOrg.id,
teamSlugInOrg: team1.slug,
});
await expectTeamToBeAPartOfOrg({
teamId: team2.id,
orgId: dbOrg.id,
teamSlugInOrg: team2.slug,
});
await expectUserToBeNotAPartOfTheOrg({
@ -873,6 +877,7 @@ describe("orgMigration", () => {
id: 1,
name: "Team 1",
slug: "team1",
newSlug: "team1-new-slug",
},
targetOrg: {
id: 2,
@ -902,19 +907,23 @@ describe("orgMigration", () => {
await moveTeamToOrg({
teamId: data.teamToMigrate.id,
targetOrgId: data.targetOrg.id,
targetOrg: {
id: data.targetOrg.id,
teamSlug: data.teamToMigrate.newSlug,
},
});
await expectTeamToBeAPartOfOrg({
teamId: data.teamToMigrate.id,
orgId: data.targetOrg.id,
teamSlugInOrg: data.teamToMigrate.newSlug,
});
expectTeamRedirectToBeEnabled({
from: {
teamSlug: data.teamToMigrate.slug,
},
to: data.teamToMigrate.slug,
to: data.teamToMigrate.newSlug,
orgSlug: data.targetOrg.slug,
});
});
@ -1198,7 +1207,15 @@ async function expectUserToBeNotAPartOfTheOrg({
expect(membership).toBeUndefined();
}
async function expectTeamToBeAPartOfOrg({ teamId, orgId }: { teamId: number; orgId: number }) {
async function expectTeamToBeAPartOfOrg({
teamId,
orgId,
teamSlugInOrg,
}: {
teamId: number;
orgId: number;
teamSlugInOrg: string | null;
}) {
const migratedTeam = await prismock.team.findUnique({
where: {
id: teamId,
@ -1208,7 +1225,11 @@ async function expectTeamToBeAPartOfOrg({ teamId, orgId }: { teamId: number; org
throw new Error(`Team with id ${teamId} does not exist`);
}
if (!teamSlugInOrg) {
throw new Error(`teamSlugInOrg should be defined`);
}
expect(migratedTeam.parentId).toBe(orgId);
expect(migratedTeam.slug).toBe(teamSlugInOrg);
}
async function expectTeamToBeNotPartOfAnyOrganization({ teamId }: { teamId: number }) {
@ -1347,7 +1368,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

@ -182,35 +182,39 @@ export async function removeUserFromOrg({ targetOrgId, userId }: { targetOrgId:
* Make sure that the migration is idempotent
*/
export async function moveTeamToOrg({
targetOrgId,
targetOrg,
teamId,
moveMembers,
}: {
targetOrgId: number;
targetOrg: { id: number; teamSlug: string };
teamId: number;
moveMembers?: boolean;
}) {
const possibleOrg = await getTeamOrThrowError(targetOrgId);
const movedTeam = await dbMoveTeamToOrg({ teamId, targetOrgId });
const possibleOrg = await getTeamOrThrowError(targetOrg.id);
const { oldTeamSlug, updatedTeam } = await dbMoveTeamToOrg({ teamId, targetOrg });
const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata);
if (!teamMetadata?.isOrganization) {
throw new Error(`${targetOrgId} is not an Org`);
throw new Error(`${targetOrg.id} is not an Org`);
}
const targetOrganization = possibleOrg;
const orgMetadata = teamMetadata;
await addTeamRedirect(movedTeam.slug, targetOrganization.slug || orgMetadata.requestedSlug || null);
await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrgId);
await addTeamRedirect({
oldTeamSlug,
teamSlug: updatedTeam.slug,
orgSlug: targetOrganization.slug || orgMetadata.requestedSlug || null,
});
await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id);
if (moveMembers) {
for (const membership of movedTeam.members) {
for (const membership of updatedTeam.members) {
await moveUserToOrg({
user: {
id: membership.userId,
},
targetOrg: {
id: targetOrgId,
id: targetOrg.id,
membership: {
role: membership.role,
accepted: membership.accepted,
@ -220,21 +224,30 @@ export async function moveTeamToOrg({
});
}
}
log.debug(`Successfully moved team ${teamId} to org ${targetOrgId}`);
log.debug(`Successfully moved team ${teamId} to org ${targetOrg.id}`);
}
/**
* Make sure that the migration is idempotent
*/
export async function removeTeamFromOrg({ targetOrgId, teamId }: { targetOrgId: number; teamId: number }) {
const removedTeam = await dbRemoveTeamFromOrg({ teamId, targetOrgId });
const removedTeam = await dbRemoveTeamFromOrg({ teamId });
await removeTeamRedirect(removedTeam.slug);
log.debug(`Successfully removed team ${teamId} from org ${targetOrgId}`);
}
async function dbMoveTeamToOrg({ teamId, targetOrgId }: { teamId: number; targetOrgId: number }) {
async function dbMoveTeamToOrg({
teamId,
targetOrg,
}: {
teamId: number;
targetOrg: {
id: number;
teamSlug: string;
};
}) {
const team = await prisma.team.findUnique({
where: {
id: teamId,
@ -251,21 +264,30 @@ async function dbMoveTeamToOrg({ teamId, targetOrgId }: { teamId: number; target
});
}
if (team.parentId === targetOrgId) {
log.warn(`Team ${teamId} is already in org ${targetOrgId}`);
return team;
}
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
const oldTeamSlug = teamMetadata?.migratedToOrgFrom?.teamSlug || team.slug;
await prisma.team.update({
const updatedTeam = await prisma.team.update({
where: {
id: teamId,
},
data: {
parentId: targetOrgId,
slug: targetOrg.teamSlug,
parentId: targetOrg.id,
metadata: {
...teamMetadata,
migratedToOrgFrom: {
teamSlug: team.slug,
lastMigrationTime: new Date().toISOString(),
},
},
},
include: {
members: true,
},
});
return team;
return { oldTeamSlug, updatedTeam };
}
async function getUniqueUserThatDoesntBelongToOrg(
@ -460,11 +482,25 @@ async function addRedirect({
}
}
async function addTeamRedirect(teamSlug: string | null, orgSlug: string | null) {
async function addTeamRedirect({
oldTeamSlug,
teamSlug,
orgSlug,
}: {
oldTeamSlug: string | null;
teamSlug: string | null;
orgSlug: string | null;
}) {
if (!oldTeamSlug) {
throw new HttpError({
statusCode: 400,
message: "No oldSlug for team. Not adding the redirect",
});
}
if (!teamSlug) {
throw new HttpError({
statusCode: 400,
message: "No slug for team. Not removing the redirect",
message: "No slug for team. Not adding the redirect",
});
}
if (!orgSlug) {
@ -477,13 +513,13 @@ async function addTeamRedirect(teamSlug: string | null, orgSlug: string | null)
where: {
from_type_fromOrgId: {
type: RedirectType.Team,
from: teamSlug,
from: oldTeamSlug,
fromOrgId: 0,
},
},
create: {
type: RedirectType.Team,
from: teamSlug,
from: oldTeamSlug,
fromOrgId: 0,
toUrl: `${orgUrlPrefix}/${teamSlug}`,
},
@ -678,7 +714,7 @@ async function removeUserAlongWithItsTeamsRedirects({
}
}
async function dbRemoveTeamFromOrg({ teamId, targetOrgId }: { teamId: number; targetOrgId: number }) {
async function dbRemoveTeamFromOrg({ teamId }: { teamId: number }) {
const team = await prisma.team.findUnique({
where: {
id: teamId,
@ -692,13 +728,7 @@ async function dbRemoveTeamFromOrg({ teamId, targetOrgId }: { teamId: number; ta
});
}
if (team.parentId !== targetOrgId) {
log.warn(`Team ${teamId} is not part of org ${targetOrgId}. Not updating`);
return {
slug: team.slug,
};
}
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
try {
return await prisma.team.update({
where: {
@ -706,6 +736,14 @@ async function dbRemoveTeamFromOrg({ teamId, targetOrgId }: { teamId: number; ta
},
data: {
parentId: null,
slug: teamMetadata?.migratedToOrgFrom?.teamSlug || team.slug,
metadata: {
...teamMetadata,
migratedToOrgFrom: {
reverted: true,
lastRevertTime: new Date().toISOString(),
},
},
},
select: {
slug: true,

View File

@ -3,9 +3,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const requriedScopes = ["READ_PROFILE"];
const requiredScopes = ["READ_PROFILE"];
const account = await isAuthorized(req, requriedScopes);
const account = await isAuthorized(req, requiredScopes);
if (!account) {
return res.status(401).json({ message: "Unauthorized" });

View File

@ -40,7 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: JSON.stringify(parsedBody.error) });
}
const { teamId, targetOrgId, moveMembers } = parsedBody.data;
const { teamId, targetOrgId, moveMembers, teamSlugInOrganization } = parsedBody.data;
const isAllowed = isAdmin;
if (!isAllowed) {
return res.status(403).json({ message: "Not Authorized" });
@ -48,7 +48,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
await moveTeamToOrg({
targetOrgId,
targetOrg: {
id: targetOrgId,
teamSlug: teamSlugInOrganization,
},
teamId,
moveMembers,
});

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

@ -21,6 +21,7 @@ export const getFormSchema = (t: TFunction) => {
teamId: z.number().or(getStringAsNumberRequiredSchema(t)),
targetOrgId: z.number().or(getStringAsNumberRequiredSchema(t)),
moveMembers: z.boolean(),
teamSlugInOrganization: z.string(),
});
};
@ -103,6 +104,12 @@ export default function MoveTeamToOrg() {
required
placeholder="Enter teamId to move to org"
/>
<TextField
{...register("teamSlugInOrganization")}
label="New Slug"
required
placeholder="Team slug in the Organization"
/>
<TextField
{...register("targetOrgId")}
label="Target Organization ID"

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

@ -56,6 +56,16 @@
"a_refund_failed": "ההחזר הכספי נכשל",
"awaiting_payment_subject": "ממתין לתשלום: {{title}} ב- {{date}}",
"meeting_awaiting_payment": "התשלום על הפגישה שלך טרם בוצע",
"dark_theme_contrast_error": "צבע ערכת עיצוב כהה ללא עובר בדיקת ניגודיות. אנו ממליצים לשנות את הצבע הזה כדי שהכפתורים שלך יהיו יותר בולטים.",
"light_theme_contrast_error": "צבע ערכת עיצוב בהירה ללא עובר בדיקת ניגודיות. אנו ממליצים לשנות את הצבע הזה כדי שהכפתורים שלך יהיו יותר בולטים.",
"payment_not_created_error": "אי אפשר ליצור תשלום",
"couldnt_charge_card_error": "לא ניתן לחייב את התשלום בכרטיס",
"no_available_users_found_error": "לא נמצאו משתמשים זמינים. אפשר לנסות ליצור חלון זמן נוסף?",
"request_body_end_time_internal_error": "שגיאה פנימית. גוף הבקשה לא מכיל זמן סיום",
"create_calendar_event_error": "לא ניתן ליצור אירוע לוח שנה בלוח השנה של הגוף המארגן",
"update_calendar_event_error": "לא ניתן לעדכן אירוע לוח שנה.",
"delete_calendar_event_error": "לא ניתן למחוק אירוע לוח שנה.",
"already_signed_up_for_this_booking_error": "כבר נרשמת להזמנה הזאת.",
"help": "עזרה",
"price": "מחיר",
"paid": "שולם",
@ -67,6 +77,7 @@
"cannot_repackage_codebase": "לא ניתן לארוז מחדש או למכור את בסיס הקוד",
"acquire_license": "כדי להסיר את התנאים האלה, ניתן לקנות רישיון מסחרי על ידי שליחת דוא\"ל",
"terms_summary": "סיכום התנאים",
"signing_up_terms": "הרשמה מהווה את הסכמתך ל<2>תנאים</2> ול<3>מדיניות הפרטיות</3> שלנו.",
"open_env": "יש לפתוח את .env ולאשר את הרישיון שלנו",
"env_changed": "שיניתי את ה-.env שלי",
"accept_license": "אישור הרישיון",
@ -101,6 +112,7 @@
"requested_to_reschedule_subject_attendee": "הפעולה חייבה קביעת מועד חדש: יש לקבוע מועד חדש עבור {{eventType}} עם {{name}}",
"hi_user_name": "שלום {{name}}",
"ics_event_title": "{{eventType}} עם {{name}}",
"please_book_a_time_sometime_later": "אף אחד לא זמין כרגע. נא לקבוע זימון למועד אחר",
"new_event_subject": "אירוע חדש: {{attendeeName}} - {{date}} - {{eventType}}",
"join_by_entrypoint": "ניתן להצטרף עד {{entryPoint}}",
"notes": "הערות",
@ -117,15 +129,20 @@
"meeting_id": "מזהה הפגישה",
"meeting_password": "סיסמת הפגישה",
"meeting_url": "כתובת ה-URL של הפגישה",
"meeting_url_not_found": "כתובת הפגישה לא נמצאה",
"token_not_found": "האסימון לא נמצא",
"some_other_host_already_accepted_the_meeting": "מארח אחר כבר קיבל את הפגישה. בכל זאת מעניין אותך להצטרף? <1>להמשיך לפגישה</1>",
"meeting_request_rejected": "בקשת הפגישה שלך נדחתה",
"rejected_event_type_with_organizer": "נדחה: {{eventType}} עם {{organizer}} בתאריך {{date}}",
"hi": "שלום",
"join_team": "להצטרף לצוות",
"manage_this_team": "לנהל את הצוות הנוכחי",
"team_info": "מידע על הצוות",
"join_meeting": "הצטרפות לפגישה",
"request_another_invitation_email": "אם אתה מעדיף לא להשתמש בכתובת {{toEmail}} ככתובת הדוא\"ל שלך עבור {{appName}} או שכבר יש לך חשבון {{appName}}, יש לבקש לקבל הזמנה נוספת לכתובת הדוא\"ל הרצויה.",
"you_have_been_invited": "הוזמנת להצטרף לצוות {{teamName}}",
"user_invited_you": "{{user}} הזמין/ה אותך להצטרף ל{{entity}} {{team}} ב-{{appName}}",
"user_invited_you_to_subteam": "הוזמנת על ידי {{user}} להצטרף לצוות {{team}} של הארגון {{parentTeamName}} אצל {{appName}}",
"hidden_team_member_title": "אתה מוסתר בצוות זה",
"hidden_team_member_message": "לא בוצע תשלום עבור המקום שלך. ניתן לשדרג ל-PRO או ליידע את הבעלים של הצוות שהוא או היא יכולים לשלם עבור המקום שלך.",
"hidden_team_owner_message": "נדרש חשבון Pro כדי להשתמש בתכונות הצוותים. תהיה מוסתר עד שתבצע שידרוג.",
@ -229,6 +246,7 @@
"reset_your_password": "הגדר/י את הסיסמה החדשה שלך לפי ההוראות שנשלחו אל כתובת הדוא\"ל שלך.",
"email_change": "התחבר/י שוב עם כתובת הדוא\"ל החדשה והסיסמה.",
"create_your_account": "צור את החשבון שלך",
"create_your_calcom_account": "יצירת החשבון שלך ב־Cal.com",
"sign_up": "הרשמה",
"youve_been_logged_out": "יצאת מהמערכת",
"hope_to_see_you_soon": "מקווים לראותך שוב בקרוב!",
@ -266,6 +284,9 @@
"nearly_there_instructions": "דבר אחרון: תיאור קצר אודותיך/ייך בתוספת תמונה עוזרים מאוד להשיג הזמנות ומאפשרים לאנשים לדעת עם מי הם עומדים להיפגש.",
"set_availability_instructions": "הגדר טווחי זמן שבהם אתה זמין באופן קבוע. ניתן יהיה ליצור טווחי זמן נוספים מאוחר יותר ולהקצות אותם ללוחות שנה אחרים.",
"set_availability": "ציין את הזמינות שלך",
"set_availbility_description": "הגדרת תזמונים למועדים שמתאים לך לקבוע בהם זימונים.",
"share_a_link_or_embed": "שיתוף קישור או הטמעה",
"share_a_link_or_embed_description": "שיתוף הקישור שלך אל {{appName}} באתר שלך.",
"availability_settings": "הגדרות זמינוּת",
"continue_without_calendar": "להמשיך בלי לוח שנה",
"continue_with": "להמשיך עם {{appName}}",
@ -419,6 +440,7 @@
"browse_api_documentation": "עיון במסמכי ממשק תכנות היישומים (API) שלנו",
"leverage_our_api": "מומלץ להיעזר בממשק תכנות היישומים (API) שלנו לקבלת שליטה מלאה ויכולת התאמה אישית.",
"create_webhook": "יצירת Webhook",
"instant_meeting_created": "נוצרה פגישה מיידית",
"booking_cancelled": "ההזמנה בוטלה",
"booking_rescheduled": "מועד ההזמנה השתנה",
"recording_ready": "הקישור להורדת ההקלטה מוכן",
@ -606,6 +628,7 @@
"hide_book_a_team_member_description": "הסתר/י את הלחצן לשריון זמן של חבר/ת צוות מהדפים הציבוריים שלך.",
"danger_zone": "אזור מסוכן",
"account_deletion_cannot_be_undone": "יש לנקוט זהירות. מחיקת חשבון היא פעולה בלתי הפיכה.",
"team_deletion_cannot_be_undone": "יש לנקוט במשנה זהירות. מחיקת צוות היא פעולה בלתי הפיכה",
"back": "הקודם",
"cancel": "ביטול",
"cancel_all_remaining": "לבטל את כל הנותרים",
@ -656,6 +679,7 @@
"default_duration": "משך הזמן המוגדר כברירת מחדל",
"default_duration_no_options": "ראשית, אנא בחר משך זמינות",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "דקות",
"round_robin": "לפי תורות",
"round_robin_description": "פגישות מחזוריות בין חברי צוות מרובים.",
@ -668,6 +692,7 @@
"add_members": "הוספת חברים...",
"no_assigned_members": "לא הוקצה אף חבר",
"assigned_to": "הוקצה ל",
"you_must_be_logged_in_to": "חובה להיכנס אל {{url}}",
"start_assigning_members_above": "התחל/י להקצות חברים למעלה",
"locked_fields_admin_description": "חברים לא יוכלו לערוך את זה",
"locked_fields_member_description": "מנהל הצוות נעל את האפשרות הזו",
@ -780,6 +805,9 @@
"requires_confirmation_description": "יש לאשר את ההזמנה באופן ידני כדי שניתן יהיה להעביר אותה אל השילובים ולשלוח הודעת אישור בדוא״ל.",
"recurring_event": "אירוע חוזר",
"recurring_event_description": "אנשים יכולים להירשם לאירועים חוזרים",
"cannot_be_used_with_paid_event_types": "אי אפשר להשתמש בזה עם סוגי פגישות בתשלום",
"warning_payment_instant_meeting_event": "אין תמיכה בפגישות מיידיות עם אירועים מחזוריים ויישומוני תשלום",
"warning_instant_meeting_experimental": "ניסיוני: פגישות מיידיות הן ניסיוניות כרגע.",
"starting": "מועד התחלה",
"disable_guests": "השבתת אורחים",
"disable_guests_description": "השבת את האפשרות להוסיף אורחים נוספים בעת ביצוע הזמנה.",
@ -847,6 +875,7 @@
"next_step": "לדלג על שלב זה",
"prev_step": "לשלב הקודם",
"install": "התקנה",
"start_paid_trial": "התחלת ניסיון בחינם",
"installed": "מותקן",
"active_install_one": "התקנה פעילה {{count}}",
"active_install_other": "{{count}} התקנות פעילות",
@ -1032,6 +1061,7 @@
"user_impersonation_heading": "התחזות למשתמשים",
"user_impersonation_description": "מצב זה מאפשר לצוות התמיכה שלנו להתחבר באופן זמני לחשבונך כדי שנוכל לפתור במהירות את הבעיות שתדווח/י לנו עליהם.",
"team_impersonation_description": "מאפשר לבעלים של הצוות/מנהלים להיכנס זמנית בשמך.",
"cal_signup_description": "בחינם למשתמשים פרטיים. התוכנית לצוותים מאפשרת יכולות לשיתופי פעולה.",
"make_team_private": "להגדיר את הצוות כפרטי",
"make_team_private_description": "כשההגדרה הזו מופעלת, חברי הצוות שלך לא יוכלו לראות חברי צוות אחרים.",
"you_cannot_see_team_members": "אין לך אפשרות לראות את כל חברי הצוות של צוות פרטי.",
@ -1091,6 +1121,7 @@
"developer_documentation": "מסמכי מפתחים",
"get_in_touch": "יצירת קשר",
"contact_support": "פנייה לתמיכה",
"premium_support": "תמיכת פרימיום",
"community_support": "תמיכת קהילה",
"feedback": "משוב",
"submitted_feedback": "תודה על המשוב!",
@ -1297,6 +1328,7 @@
"customize_your_brand_colors": "בצע התאמה אישית של דף ההזמנות שלך עם צבעי מותג משלך.",
"pro": "Pro",
"removes_cal_branding": "הסרת מיתוגים הקשורים ל-{{appName}}, כגון 'מופעל על ידי {{appName}}'",
"instant_meeting_with_title": "פגישה מיידית עם {{name}}",
"profile_picture": "תמונת פרופיל",
"upload": "העלאה",
"add_profile_photo": "הוספת תמונת פרופיל",
@ -1352,6 +1384,7 @@
"event_name_info": "שם סוג האירוע",
"event_date_info": "תאריך האירוע",
"event_time_info": "שעת ההתחלה של האירוע",
"event_type_not_found": "EventType לא נמצא",
"location_info": "מיקום האירוע",
"additional_notes_info": "הערות נוספות להזמנה",
"attendee_name_info": "שם האדם שביצע את ההזמנה",
@ -1392,6 +1425,7 @@
"slot_length": "אורך חלון הזמן",
"booking_appearance": "מראה ההזמנה",
"appearance_team_description": "ניהול ההגדרות של מראה הזמנות הצוות שלך",
"appearance_org_description": "ניהול ההגדרות למראה ההזמנות של הארגון שלך",
"only_owner_change": "רק הבעלים של הצוות יכולים לבצע שינויים בהזמנת הצוות ",
"team_disable_cal_branding_description": "הסרת מיתוגים הקשורים ל-{{appName}}, כגון 'מופעל על ידי {{appName}}'",
"invited_by_team": "הוזמנת על ידי {{teamName}} להצטרף לצוות בתפקיד {{role}}",
@ -1456,6 +1490,8 @@
"report_app": "דיווח על האפליקציה",
"limit_booking_frequency": "הגבלת תדירות ההזמנות",
"limit_booking_frequency_description": "הגבלת מספר הפעמים שבהן ניתן להזמין את האירוע הזה",
"limit_booking_only_first_slot": "להגביל את ההזמנה לחלון הפנוי הראשון בלבד",
"limit_booking_only_first_slot_description": "לאפשר להזמין רק את החלון הפנוי הראשון בכל יום",
"limit_total_booking_duration": "הגבל משך תזמון כולל",
"limit_total_booking_duration_description": "הגבלת משך הזמן הכולל שבו ניתן להזמין את האירוע הזה",
"add_limit": "הוספת הגבלה",
@ -1523,10 +1559,14 @@
"your_org_disbanded_successfully": "פירוק הארגון שלך בוצע בהצלחה",
"error_creating_team": "אירעה שגיאה במהלך יצירת הצוות",
"you": "את/ה",
"or_continue_with": "או להמשיך עם",
"resend_email": "לשלוח שוב את הדוא״ל",
"member_already_invited": "החבר כבר הוזמן",
"already_in_use_error": "שם המשתמש כבר קיים",
"enter_email_or_username": "יש להזין כתובת דוא\"ל או שם משתמש",
"enter_email": "נא למלא כתובת דוא״ל",
"enter_emails": "נא למלא כתובות דוא״ל",
"too_many_invites": "חלה מגבלה על הזמנת עד {{nbUsers}} משתמשים בבת אחת.",
"team_name_taken": "השם הזה כבר תפוס",
"must_enter_team_name": "יש להזין שם צוות",
"team_url_required": "יש להזין כתובת URL של הצוות",
@ -1606,6 +1646,7 @@
"individual": "משתמש בודד",
"all_bookings_filter_label": "כל ההזמנות",
"all_users_filter_label": "כל המשתמשים",
"all_event_types_filter_label": "כל סוגי האירועים",
"your_bookings_filter_label": "התזמונים שלך",
"meeting_url_variable": "כתובת ה-URL של הפגישה",
"meeting_url_info": "כתובת ה-URL של שיחת הוועידה באירוע",
@ -1702,6 +1743,7 @@
"organizer_timezone": "מארגן אזורי זמן",
"email_user_cta": "צפה בהזמנה",
"email_no_user_invite_heading_team": "הוזמנת להצטרף לצוות ב-{{appName}}",
"email_no_user_invite_heading_subteam": "הוזמנת להצטרף לצוות בארגון {{parentTeamName}}",
"email_no_user_invite_heading_org": "הוזמנת להצטרף לארגון ב-{{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} הזמין אותך להצטרף לצוות שלו ב- {{appName}}. {{appName}} הינה מתזמן זימונים שמאפשר לך ולצוות שלך לזמן פגישות בלי כל הפינג פונג במיילים.",
"email_user_invite_subheading_team": "{{invitedBy}} הזמין/ה אותך להצטרף לצוות שלו/ה בשם '{{teamName}}' באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ולצוות שלך לתזמן פגישות בלי כל הפינג פונג במיילים.",
@ -1836,7 +1878,9 @@
"review_event_type": "בדיקת סוג האירוע",
"looking_for_more_analytics": "מחפש עוד מידע אנליטי?",
"looking_for_more_insights": "רוצה עוד Insights?",
"filters": "מסננים",
"add_filter": "הוסף סנן",
"remove_filters": "ניקוי כל המסננים",
"select_user": "בחר משתמש",
"select_event_type": "בחר סוג ארוע",
"select_date_range": "בחר טווח תאריכים",
@ -1991,6 +2035,8 @@
"add_times_to_your_email": "בחר/י כמה מועדים פנויים והטבע/י אותם בדוא\"ל",
"select_time": "בחירת שעה",
"select_date": "בחירת תאריך",
"connecting_you_to_someone": "אנו מחברים אותך למישהו או מישהי.",
"please_do_not_close_this_tab": "נא לא לסגור את הלשונית הזאת",
"see_all_available_times": "לצפייה בכל המועדים הפנויים",
"org_team_names_example_1": "לדוגמה, מחלקת שיווק",
"org_team_names_example_2": "לדוגמה, מחלקת מכירות",
@ -2008,8 +2054,12 @@
"description_requires_booker_email_verification": "כדי להבטיח אימות של כתובת הדוא\"ל של המזמין לפני תזמון אירועים",
"requires_confirmation_mandatory": "ניתן לשלוח הודעות טקסט למשתתפים רק כאשר סוג האירוע מחייב אישור.",
"organizations": "ארגונים",
"upload_cal_video_logo": "העלאת סרטון לוגו ל־Cal",
"update_cal_video_logo": "עדכון סרטון לוגו ל־Cal",
"cal_video_logo_upload_instruction": "כדי לוודא שהלוגו שלך גלוי כנגד הרקע הכהה של הסרטון של Cal, נא להעלות תמונה בצבעים בהירים מהסוגים PNG או SVG כדי שהחלקים המתאימים יישארו שקופים.",
"org_admin_other_teams": "צוותים אחרים",
"org_admin_other_teams_description": "כאן תוכל/י לראות צוותים בארגון שאינך שייך/ת אליהם. יש לך אפשרות להוסיף את עצמך, במקרה הצורך.",
"not_part_of_org": "אינך חלק משום ארגון",
"no_other_teams_found": "לא נמצא אף צוות אחר",
"no_other_teams_found_description": "אין צוותים אחרים בארגון הזה.",
"attendee_first_name_variable": "השם הפרטי של המשתתף",
@ -2045,7 +2095,9 @@
"org_error_processing": "היתה שגיאה בעיבוד של ארגון זה",
"orgs_page_description": "רשימה של כל הארגונים. קבלת ארגון תאפשר לכל המשתמשים מאותו דומיין דוא\"ל להירשם בלי להצטרך לבצע אימות של כתובת הדוא\"ל.",
"unverified": "לא אומת",
"verified": "מאומת",
"dns_missing": "DNS חסר",
"dns_configured": "DNS מוגדר",
"mark_dns_configured": "סימון כי DNS הוגדר",
"value": "ערך",
"your_organization_updated_sucessfully": "עדכון הארגון שלך בוצע בהצלחה",
@ -2055,7 +2107,9 @@
"oAuth": "OAuth",
"recently_added": "נוספו לאחרונה",
"connect_all_calendars": "חבר את כל לוחות השנה שלך",
"connect_all_calendars_description": "{{appName}} קורא את הזמינות מכל לוחות השנה הקיימים שלך.",
"workflow_automation": "אוטומצית תהליך עבודה",
"workflow_automation_description": "אפשר לכוון את חוויית התזמון שלך עם תהליכי עבודה",
"scheduling_for_your_team": "אוטומצית תהליך עבודה",
"no_members_found": "לא נמצא אף חבר",
"event_setup_length_error": "הגדרת אירוע: משך הזמן חייב להיות לפחות דקה אחת.",
@ -2089,9 +2143,39 @@
"overlay_my_calendar": "הצג את לוח השנה שלי בשכבת-על",
"overlay_my_calendar_toc": "על ידי חיבור אל לוח השנה שלך, את/ה מקבל/ת את מדיניות הפרטיות ואת תנאי השימוש שלנו. אפשר לשלול את הגישה בכל שלב.",
"view_overlay_calendar_events": "ראה/י את האירועים שלך בלוח השנה כדי למנוע התנגשות בהזמנות.",
"join_event_location": "הצטרפות אל {{eventLocationType}}",
"troubleshooting": "פתרון בעיות",
"calendars_were_checking_for_conflicts": "לוחות השנה לא בודקים סתירות",
"manage_calendars": "ניהול לוחות שנה",
"lock_timezone_toggle_on_booking_page": "נעילת אזור הזמן בדף ההזמנות",
"description_lock_timezone_toggle_on_booking_page": "כדי לנעול את אזור הזמן בדף ההזמנות שימושי לאירועים אישיים.",
"install_calendar": "התקנת לוח שנה",
"branded_subdomain": "תת־תחום ממותג",
"branded_subdomain_description": "קבלת תת־תחום ממותג משלך, כגון acme.cal.com",
"org_insights": "תובנות כלל־ארגוניות",
"extensive_whitelabeling": "תהליך הטמעה והנדסת תמיכה אישי",
"unlimited_teams": "כמות בלתי מוגבלת של צוותים",
"unlimited_teams_description": "אפשר להוסיף כמה תת־צוותים שדרושים לארגון שלך",
"unified_billing": "חיוב מאוחד",
"unified_billing_description": "ניתן להוסיף כרטיס אשראי אחד כדי לשלם על כל המינויים של הצוות שלך",
"advanced_managed_events": "סוגי אירועים מנוהלים מתקדמים",
"advanced_managed_events_description": "אפשר להוסיף כרטיס אשראי יחיד כדי לשלם עבור כל המינויים של הצוות שלך",
"enterprise_description": "יש לשדרג לרישיון תאגידי כדי ליצור את הארגון שלך",
"create_your_org": "יצירת הארגון שלך",
"create_your_org_description": "אפשר לשדרג לרישיון תאגידי ולקבל תת־תחום, חיוב מאוחד, תובנות, שינוי מיתוג נרחב ועוד",
"other_payment_app_enabled": "אפשר להפעיל רק יישומון תשלום אחד לכל סוג אירוע",
"admin_delete_organization_title": "למחוק את הארגון?",
"published": "מפורסם",
"unpublished": "לא מפורסם",
"publish": "פרסום",
"org_publish_error": "אי אפשר לפרסם את הארגון",
"need_help": "צריך עזרה?",
"troubleshooter": "פותר בעיות",
"please_install_a_calendar": "נא להתקין לוח שנה",
"instant_tab_title": "הזמנה מיידית",
"instant_event_tab_description": "לאפשר לאנשים ליצור הזמנות מיידית",
"uprade_to_create_instant_bookings": "ניתן לשדג לרישיון התאגידי ולאפשר למשתמשים להצטרף לשיחה מיידית שמשתתפים יכולים לקפוץ ישירות אליה. זה מיועד רק לסוגי אירועים של צוותים",
"dont_want_to_wait": "לא רוצה להמתין?",
"meeting_started": "הפגישה החלה",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

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

@ -94,7 +94,6 @@ export default function AppCard({
{app?.isInstalled || app.credentialOwner ? (
<div className="ml-auto flex items-center">
<Switch
data-testid="app-switch"
disabled={!app.enabled || managedDisabled || disableSwitch}
onCheckedChange={(enabled) => {
if (switchOnClick) {
@ -104,7 +103,7 @@ export default function AppCard({
}}
checked={switchChecked}
LockedIcon={LockedIcon}
data-testId={`${app.slug}-app-switch`}
data-testid={`${app.slug}-app-switch`}
tooltip={switchTooltip}
/>
</div>

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

@ -334,6 +334,14 @@ export const teamMetadataSchema = z
isOrganizationVerified: z.boolean().nullable(),
isOrganizationConfigured: z.boolean().nullable(),
orgAutoAcceptEmail: z.string().nullable(),
migratedToOrgFrom: z
.object({
teamSlug: z.string().or(z.null()).optional(),
lastMigrationTime: z.string().optional(),
reverted: z.boolean().optional(),
lastRevertTime: z.string().optional(),
})
.optional(),
})
.partial()
.nullable();

View File

@ -43,17 +43,22 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
},
});
// An org doesn't have a parentId. A team that isn't part of an org also doesn't have a parentId.
// So, an org can't have the same slug as a non-org team.
// There is a unique index on [slug, parentId] in Team because we don't add the slug to the team always. We only add metadata.requestedSlug in some cases. So, DB won't prevent creation of such an organization.
const hasANonOrgTeamOrOrgWithSameSlug = await prisma.team.findFirst({
const hasAnOrgWithSameSlug = await prisma.team.findFirst({
where: {
slug: slug,
parentId: null,
metadata: {
path: ["isOrganization"],
equals: true,
},
},
});
if (hasANonOrgTeamOrOrgWithSameSlug || RESERVED_SUBDOMAINS.includes(slug))
// Allow creating an organization with same requestedSlug as a non-org Team's slug
// It is needed so that later we can migrate the non-org Team(with the conflicting slug) to the newly created org
// Publishing the organization would fail if the team with the same slug is not migrated first
if (hasAnOrgWithSameSlug || RESERVED_SUBDOMAINS.includes(slug))
throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" });
if (userCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "admin_email_taken" });

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",