Merge branch 'main' into issue-5-1

This commit is contained in:
Riddhesh Mahajan 2024-01-10 21:04:45 +05:30 committed by GitHub
commit 0b55470ce6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 753 additions and 544 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

@ -2,6 +2,7 @@ name: "Next.js Bundle Analysis"
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
@ -34,7 +35,7 @@ jobs:
- name: Download base branch bundle stats
uses: dawidd6/action-download-artifact@v2
if: success() && github.event.number
if: success()
with:
workflow: nextjs-bundle-analysis.yml
branch: ${{ github.event.pull_request.base.ref }}
@ -54,7 +55,7 @@ jobs:
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
# entry in your package.json file.
- name: Compare with base branch bundle
if: success() && github.event.number
if: success()
run: |
cd apps/web
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
@ -68,10 +69,10 @@ jobs:
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
echo "{body}={$body}" >> $GITHUB_OUTPUT
- name: Find Comment
uses: peter-evans/find-comment@v1
uses: peter-evans/find-comment@v2
if: success() && github.event.number
id: fc
with:
@ -79,14 +80,14 @@ jobs:
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
- name: Create Comment
uses: peter-evans/create-or-update-comment@v1.4.4
uses: peter-evans/create-or-update-comment@v3
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
- name: Update Comment
uses: peter-evans/create-or-update-comment@v1.4.4
uses: peter-evans/create-or-update-comment@v3
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
with:
issue-number: ${{ github.event.number }}

View File

@ -12,6 +12,14 @@ concurrency:
cancel-in-progress: true
jobs:
login:
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
changes:
name: Detect changes
runs-on: buildjet-4vcpu-ubuntu-2204
@ -56,6 +64,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 +107,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

@ -64,6 +64,25 @@ export async function patchHandler(req: NextApiRequest) {
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
});
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
// Check if parentId is related to this user
if (data.parentId && data.parentId === teamId) {
throw new HttpError({
statusCode: 400,
message: "Bad request: Parent id cannot be the same as the team id.",
});
}
if (data.parentId) {
const parentTeam = await prisma.team.findFirst({
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
});
if (!parentTeam)
throw new HttpError({
statusCode: 401,
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
});
}
let paymentUrl;
if (_team.slug === null && data.slug) {
data.metadata = {

View File

@ -71,6 +71,18 @@ async function postHandler(req: NextApiRequest) {
}
}
// Check if parentId is related to this user
if (data.parentId) {
const parentTeam = await prisma.team.findFirst({
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
});
if (!parentTeam)
throw new HttpError({
statusCode: 401,
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
});
}
// TODO: Perhaps there is a better fix for this?
const cloneData: typeof data & {
metadata: NonNullable<typeof data.metadata> | undefined;

View File

@ -1,212 +0,0 @@
// originally from in the "experimental playground for tRPC + next.js 13" repo owned by trpc team
// file link: https://github.com/trpc/next-13/blob/main/%40trpc/next-layout/createTRPCNextLayout.ts
// repo link: https://github.com/trpc/next-13
// code is / will continue to be adapted for our usage
import { dehydrate, QueryClient } from "@tanstack/query-core";
import type { DehydratedState, QueryKey } from "@tanstack/react-query";
import type { Maybe, TRPCClientError, TRPCClientErrorLike } from "@calcom/trpc";
import {
callProcedure,
type AnyProcedure,
type AnyQueryProcedure,
type AnyRouter,
type DataTransformer,
type inferProcedureInput,
type inferProcedureOutput,
type inferRouterContext,
type MaybePromise,
type ProcedureRouterRecord,
} from "@calcom/trpc/server";
import { createRecursiveProxy, createFlatProxy } from "@trpc/server/shared";
export function getArrayQueryKey(
queryKey: string | [string] | [string, ...unknown[]] | unknown[],
type: string
): QueryKey {
const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey];
const [arrayPath, input] = queryKeyArrayed;
if (!input && (!type || type === "any")) {
return Array.isArray(arrayPath) && arrayPath.length !== 0 ? [arrayPath] : ([] as unknown as QueryKey);
}
return [
arrayPath,
{
...(typeof input !== "undefined" && { input: input }),
...(type && type !== "any" && { type: type }),
},
];
}
// copy starts
// copied from trpc/trpc repo
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L37-#L58
function transformQueryOrMutationCacheErrors<
TState extends DehydratedState["queries"][0] | DehydratedState["mutations"][0]
>(result: TState): TState {
const error = result.state.error as Maybe<TRPCClientError<any>>;
if (error instanceof Error && error.name === "TRPCClientError") {
const newError: TRPCClientErrorLike<any> = {
message: error.message,
data: error.data,
shape: error.shape,
};
return {
...result,
state: {
...result.state,
error: newError,
},
};
}
return result;
}
// copy ends
interface CreateTRPCNextLayoutOptions<TRouter extends AnyRouter> {
router: TRouter;
createContext: () => MaybePromise<inferRouterContext<TRouter>>;
transformer?: DataTransformer;
}
/**
* @internal
*/
export type DecorateProcedure<TProcedure extends AnyProcedure> = TProcedure extends AnyQueryProcedure
? {
fetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
fetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
prefetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
prefetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
}
: never;
type OmitNever<TType> = Pick<
TType,
{
[K in keyof TType]: TType[K] extends never ? never : K;
}[keyof TType]
>;
/**
* @internal
*/
export type DecoratedProcedureRecord<
TProcedures extends ProcedureRouterRecord,
TPath extends string = ""
> = OmitNever<{
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<TProcedures[TKey]["_def"]["record"], `${TPath}${TKey & string}.`>
: TProcedures[TKey] extends AnyQueryProcedure
? DecorateProcedure<TProcedures[TKey]>
: never;
}>;
type CreateTRPCNextLayout<TRouter extends AnyRouter> = DecoratedProcedureRecord<TRouter["_def"]["record"]> & {
dehydrate(): Promise<DehydratedState>;
queryClient: QueryClient;
};
const getStateContainer = <TRouter extends AnyRouter>(opts: CreateTRPCNextLayoutOptions<TRouter>) => {
let _trpc: {
queryClient: QueryClient;
context: inferRouterContext<TRouter>;
} | null = null;
return () => {
if (_trpc === null) {
_trpc = {
context: opts.createContext(),
queryClient: new QueryClient(),
};
}
return _trpc;
};
};
export function createTRPCNextLayout<TRouter extends AnyRouter>(
opts: CreateTRPCNextLayoutOptions<TRouter>
): CreateTRPCNextLayout<TRouter> {
const getState = getStateContainer(opts);
const transformer = opts.transformer ?? {
serialize: (v) => v,
deserialize: (v) => v,
};
return createFlatProxy((key) => {
const state = getState();
const { queryClient } = state;
if (key === "queryClient") {
return queryClient;
}
if (key === "dehydrate") {
// copy starts
// copied from trpc/trpc repo
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L214-#L229
const dehydratedCache = dehydrate(queryClient, {
shouldDehydrateQuery() {
// makes sure errors are also dehydrated
return true;
},
});
// since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects
const dehydratedCacheWithErrors = {
...dehydratedCache,
queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors),
mutations: dehydratedCache.mutations.map(transformQueryOrMutationCacheErrors),
};
return () => transformer.serialize(dehydratedCacheWithErrors);
}
// copy ends
return createRecursiveProxy(async (callOpts) => {
const path = [key, ...callOpts.path];
const utilName = path.pop();
const ctx = await state.context;
const caller = opts.router.createCaller(ctx);
const pathStr = path.join(".");
const input = callOpts.args[0];
if (utilName === "fetchInfinite") {
return queryClient.fetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
caller.query(pathStr, input)
);
}
if (utilName === "prefetch") {
return queryClient.prefetchQuery({
queryKey: getArrayQueryKey([path, input], "query"),
queryFn: async () => {
const res = await callProcedure({
procedures: opts.router._def.procedures,
path: pathStr,
rawInput: input,
ctx,
type: "query",
});
return res;
},
});
}
if (utilName === "prefetchInfinite") {
return queryClient.prefetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
caller.query(pathStr, input)
);
}
return queryClient.fetchQuery(getArrayQueryKey([path, input], "query"), () =>
caller.query(pathStr, input)
);
}) as CreateTRPCNextLayout<TRouter>;
});
}

View File

@ -1,34 +0,0 @@
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers } from "next/headers";
import superjson from "superjson";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma, { readonlyPrisma } from "@calcom/prisma";
import { appRouter } from "@calcom/trpc/server/routers/_app";
import { createTRPCNextLayout } from "./createTRPCNextLayout";
export async function ssgInit() {
const locale = headers().get("x-locale") ?? "en";
const i18n = (await serverSideTranslations(locale, ["common"])) || "en";
const ssg = createTRPCNextLayout({
router: appRouter,
transformer: superjson,
createContext() {
return { prisma, insightsDb: readonlyPrisma, session: null, locale, i18n };
},
});
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
ssg.queryClient.setQueryData(queryKey, { i18n });
return ssg;
}

View File

@ -1,57 +0,0 @@
import { type GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers, cookies } from "next/headers";
import superjson from "superjson";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma, { readonlyPrisma } from "@calcom/prisma";
import { appRouter } from "@calcom/trpc/server/routers/_app";
import { createTRPCNextLayout } from "./createTRPCNextLayout";
export async function ssrInit(options?: { noI18nPreload: boolean }) {
const req = {
headers: headers(),
cookies: cookies(),
};
const locale = await getLocale(req);
const i18n = (await serverSideTranslations(locale, ["common", "vital"])) || "en";
const ssr = createTRPCNextLayout({
router: appRouter,
transformer: superjson,
createContext() {
return {
prisma,
insightsDb: readonlyPrisma,
session: null,
locale,
i18n,
req: req as unknown as GetServerSidePropsContext["req"],
};
},
});
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
if (!options?.noI18nPreload) {
ssr.queryClient.setQueryData(queryKey, { i18n });
}
await Promise.allSettled([
// So feature flags are available on first render
ssr.viewer.features.map.prefetch(),
// Provides a better UX to the users who have already upgraded.
ssr.viewer.teams.hasTeamPlan.prefetch(),
ssr.viewer.public.session.prefetch(),
]);
return ssr;
}

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

@ -1,13 +1,15 @@
import LegacyPage from "@pages/apps/categories/index";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () => {
return await _generateMetadata(
() => `Categories | ${APP_NAME}`,
@ -15,12 +17,12 @@ export const generateMetadata = async () => {
);
};
async function getPageProps() {
const ssr = await ssrInit();
const req = { headers: headers(), cookies: cookies() };
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage
const session = await getServerSession({ req });
const session = await getServerSession({ req: ctx.req });
let appStore;
if (session?.user?.id) {
@ -38,8 +40,8 @@ async function getPageProps() {
return {
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
dehydratedState: await ssr.dehydrate(),
dehydratedState: ssr.dehydrate(),
};
}
};
export default WithLayout({ getData: getPageProps, Page: LegacyPage, getLayout: null })<"P">;
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;

View File

@ -1,7 +1,6 @@
import AppsPage from "@pages/apps";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { WithLayout } from "app/layoutHOC";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
@ -11,7 +10,9 @@ import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { APP_NAME } from "@calcom/lib/constants";
import type { AppCategories } from "@calcom/prisma/enums";
import PageWrapper from "@components/PageWrapperAppDir";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () => {
return await _generateMetadata(
@ -20,12 +21,12 @@ export const generateMetadata = async () => {
);
};
const getPageProps = async () => {
const ssr = await ssrInit();
const req = { headers: headers(), cookies: cookies() };
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
const session = await getServerSession({ req });
const session = await getServerSession({ req: ctx.req });
let appStore, userAdminTeams: UserAdminTeams;
if (session?.user?.id) {
@ -58,24 +59,8 @@ const getPageProps = async () => {
}),
appStore,
userAdminTeams,
dehydratedState: await ssr.dehydrate(),
dehydratedState: ssr.dehydrate(),
};
};
export default async function AppPageAppDir() {
const { categories, appStore, userAdminTeams, dehydratedState } = await getPageProps();
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper
getLayout={getLayout}
requiresLicense={false}
nonce={nonce}
themeBasis={null}
dehydratedState={dehydratedState}>
<AppsPage categories={categories} appStore={appStore} userAdminTeams={userAdminTeams} />
</PageWrapper>
);
}
export default WithLayout({ getLayout, getData, Page: AppsPage });

View File

@ -1,22 +1,21 @@
import { ssgInit } from "app/_trpc/ssgInit";
import type { Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import type { ReactElement } from "react";
import { z } from "zod";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssgInit } from "@server/lib/ssg";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const querySchema = z.object({
status: z.enum(validStatuses),
});
type Props = { params: Params; children: ReactElement };
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${APP_NAME} | ${t("bookings")}`,
@ -27,18 +26,18 @@ export const generateStaticParams = async () => {
return validStatuses.map((status) => ({ status }));
};
const getData = async ({ params }: { params: Params }) => {
const parsedParams = querySchema.safeParse(params);
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
const parsedParams = querySchema.safeParse(ctx.params);
if (!parsedParams.success) {
notFound();
}
const ssg = await ssgInit();
const ssg = await ssgInit(ctx);
return {
status: parsedParams.data.status,
dehydratedState: await ssg.dehydrate(),
dehydratedState: ssg.dehydrate(),
};
};

View File

@ -0,0 +1,63 @@
import LegacyPage from "@pages/getting-started/[[...step]]";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
const req = { headers: headers(), cookies: cookies() };
//@ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
const session = await getServerSession({ req });
if (!session?.user?.id) {
return redirect("/auth/login");
}
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
await ssr.viewer.me.prefetch();
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
completedOnboarding: true,
teams: {
select: {
accepted: true,
team: {
select: {
id: true,
name: true,
logo: true,
},
},
},
},
},
});
if (!user) {
throw new Error("User from session not found");
}
if (user.completedOnboarding) {
redirect("/event-types");
}
return {
dehydratedState: ssr.dehydrate(),
hasPendingInvites: user.teams.find((team: any) => team.accepted === false) ?? false,
requiresLicense: false,
themeBasis: null,
};
};
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });

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

@ -1,24 +1,29 @@
import OldPage from "@pages/teams/index";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const ssr = await ssrInit();
async function getData(context: ReturnType<typeof buildLegacyCtx>) {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | (IncomingMessage & { cookies: Partial<{ [key: string]: string; }>; })'.
req: context.req,
});
@ -29,8 +34,7 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
return redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
}
return { dehydratedState: await ssr.dehydrate() };
return { dehydratedState: ssr.dehydrate() };
}
// @ts-expect-error getData arg
export default WithLayout({ getData, getLayout, Page: OldPage })<"P">;

View File

@ -1,15 +1,17 @@
import OldPage from "@pages/video/[uid]";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import MarkdownIt from "markdown-it";
import { type GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
() => `${APP_NAME} Video`,
@ -18,8 +20,9 @@ export const generateMetadata = async () =>
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const ssr = await ssrInit();
async function getData(context: ReturnType<typeof buildLegacyCtx>) {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
const booking = await prisma.booking.findUnique({
where: {
@ -76,6 +79,7 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
endTime: booking.endTime.toString(),
});
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | (IncomingMessage & { cookies: Partial<{ [key: string]: string; }>; })'.
const session = await getServerSession({ req: context.req });
// set meetingPassword to null for guests
@ -94,9 +98,8 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
...bookingObj,
...(bookingObj.description && { description: md.render(bookingObj.description) }),
},
dehydratedState: await ssr.dehydrate(),
dehydratedState: ssr.dehydrate(),
};
}
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -1,19 +1,23 @@
import LegacyPage from "@pages/video/no-meeting-found";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("no_meeting_found"),
(t) => t("no_meeting_found")
);
const getData = async () => {
const ssr = await ssrInit();
const getData = async (context: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
return {
dehydratedState: await ssr.dehydrate(),
dehydratedState: ssr.dehydrate(),
};
};

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

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.6.1",
"version": "3.6.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",

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

@ -1,3 +1,5 @@
"use client";
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Head from "next/head";
@ -51,13 +53,18 @@ const stepRouteSchema = z.object({
const OnboardingPage = () => {
const pathname = usePathname();
const params = useParamsWithFallback();
const router = useRouter();
const [user] = trpc.viewer.me.useSuspenseQuery();
const { t } = useLocale();
const result = stepRouteSchema.safeParse(params);
const result = stepRouteSchema.safeParse({
...params,
step: Array.isArray(params.step) ? params.step : [params.step],
});
const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
const from = result.success ? result.data.from : "";
const headers = [
{
title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`,
@ -218,7 +225,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return { redirect: { permanent: false, destination: "/event-types" } };
}
const locale = await getLocale(context.req);
return {
props: {
...(await serverSideTranslations(locale, ["common"])),

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

@ -166,4 +166,31 @@ test.describe("apps/ A/B tests", () => {
await expect(locator).toHaveClass(/bg-emphasis/);
});
test("should point to the /future/getting-started", async ({ page, users, context }) => {
await context.addCookies([
{
name: "x-calcom-future-routes-override",
value: "1",
url: "http://localhost:3000",
},
]);
const user = await users.create({ completedOnboarding: false, name: null });
await user.apiLogin();
await page.goto("/getting-started/connected-calendar");
await page.waitForLoadState();
const dataNextJsRouter = await page.evaluate(() =>
window.document.documentElement.getAttribute("data-nextjs-router")
);
expect(dataNextJsRouter).toEqual("app");
const locator = page.getByText("Apple Calendar");
await expect(locator).toBeVisible();
});
});

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,5 +1,3 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
@ -19,6 +17,6 @@ test.describe("Teams", () => {
await page.goto("/settings/my-account/profile");
// check if user avatar is loaded
await expect(page.locator('[data-testid="organization-avatar"]')).toBeVisible();
await page.getByTestId("profile-upload-avatar").isVisible();
});
});

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

@ -143,7 +143,8 @@ html.todesktop header {
-webkit-app-region: drag;
}
html.todesktop header button {
html.todesktop header button,
html.todesktop header a {
-webkit-app-region: no-drag;
}

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

@ -439,6 +439,10 @@ class CalApi {
elementOrSelector: string | HTMLElement;
config?: PrefillAndIframeAttrsConfig;
}) {
if (this.cal.inlineEl) {
console.warn("Inline embed already exists. Ignoring this call");
return;
}
// eslint-disable-next-line prefer-rest-params
validate(arguments[0], {
required: true,

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,
});
@ -186,12 +188,6 @@ const BookerComponent = ({
return setBookerState("booking");
}, [event, selectedDate, selectedTimeslot, setBookerState]);
useEffect(() => {
if (layout === "mobile") {
timeslotsRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [layout]);
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
if (entity.isUnpublished) {

View File

@ -1,4 +1,4 @@
import { useRef, useEffect } from "react";
import { useRef } from "react";
import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
@ -93,13 +93,6 @@ export const AvailableTimeSlots = ({
const slotsPerDay = useSlotsForAvailableDates(dates, schedule?.data?.slots);
useEffect(() => {
if (isEmbed) return;
if (containerRef.current && !schedule.isLoading && isMobile) {
containerRef.current.scrollIntoView({ behavior: "smooth", block: "center" });
}
}, [containerRef, schedule.isLoading, isEmbed, isMobile]);
return (
<>
<div className="flex">

View File

@ -200,7 +200,7 @@ export const BookEventFormChild = ({
const { uid, paymentUid } = responseData;
const fullName = getFullName(bookingForm.getValues("responses.name"));
if (paymentUid) {
return router.push(
router.push(
createPaymentLink({
paymentUid,
date: timeslot,
@ -209,6 +209,7 @@ export const BookEventFormChild = ({
absolute: false,
})
);
return;
}
if (!uid) {
@ -225,7 +226,7 @@ export const BookEventFormChild = ({
isRescheduling && bookingData?.startTime ? dayjs(bookingData.startTime).toString() : undefined,
};
return bookingSuccessRedirect({
bookingSuccessRedirect({
successRedirectUrl: eventType?.successRedirectUrl || "",
query,
booking: responseData,
@ -272,7 +273,7 @@ export const BookEventFormChild = ({
isRescheduling && bookingData?.startTime ? dayjs(bookingData.startTime).toString() : undefined,
};
return bookingSuccessRedirect({
bookingSuccessRedirect({
successRedirectUrl: eventType?.successRedirectUrl || "",
query,
booking,
@ -325,10 +326,6 @@ export const BookEventFormChild = ({
);
const bookEvent = (values: BookingFormValues) => {
// Clears form values stored in store, so old values won't stick around.
setFormValues({});
bookingForm.clearErrors();
// It shouldn't be possible that this method is fired without having eventQuery data,
// but since in theory (looking at the types) it is possible, we still handle that case.
if (!eventQuery?.data) {
@ -375,6 +372,9 @@ export const BookEventFormChild = ({
} else {
createBookingMutation.mutate(mapBookingToMutationInput(bookingInput));
}
// Clears form values stored in store, so old values won't stick around.
setFormValues({});
bookingForm.clearErrors();
};
if (!eventType) {
@ -442,7 +442,14 @@ export const BookEventFormChild = ({
<Button
type="submit"
color="primary"
loading={createBookingMutation.isLoading || createRecurringBookingMutation.isLoading}
loading={
bookingForm.formState.isSubmitting ||
createBookingMutation.isLoading ||
createRecurringBookingMutation.isLoading ||
// A redirect is triggered on mutation success, so keep the button disabled as this is happening.
createBookingMutation.isSuccess ||
createRecurringBookingMutation.isSuccess
}
data-testid={
rescheduleUid && bookingData ? "confirm-reschedule-button" : "confirm-book-button"
}>

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

@ -4,7 +4,16 @@ import useDigitInput from "react-digit-input";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, Label, Input } from "@calcom/ui";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
Label,
Input,
} from "@calcom/ui";
import { Info } from "@calcom/ui/components/icon";
export const VerifyCodeDialog = ({
@ -45,6 +54,7 @@ export const VerifyCodeDialog = ({
},
onError: (err) => {
setIsLoading(false);
setHasVerified(false);
if (err.message === "invalid_code") {
setError(t("code_provided_invalid"));
}
@ -58,6 +68,7 @@ export const VerifyCodeDialog = ({
},
onError: (err) => {
setIsLoading(false);
setHasVerified(false);
if (err.message === "invalid_code") {
setError(t("code_provided_invalid"));
}
@ -138,6 +149,9 @@ export const VerifyCodeDialog = ({
)}
<DialogFooter>
<DialogClose />
<Button type="submit" onClick={verifyCode} loading={isLoading}>
{t("submit")}
</Button>
</DialogFooter>
</div>
</div>

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

@ -271,7 +271,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
required
value={value}
onChange={(e) => {
const targetValues = e.target.value.split(",");
const targetValues = e.target.value.split(/[\n,]/);
const emails =
targetValues.length === 1
? targetValues[0].trim().toLocaleLowerCase()

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

@ -893,7 +893,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
<div className="flex h-full flex-col justify-between py-3 lg:pt-4">
<header className="todesktop:-mt-3 todesktop:flex-col-reverse todesktop:[-webkit-app-region:drag] items-center justify-between md:hidden lg:flex">
{orgBranding ? (
<Link href="/settings/organizations/profile" className="px-1.5">
<Link href="/settings/organizations/profile" className="mt-3 w-full px-1.5">
<div className="flex items-center gap-2 font-medium">
<Avatar
alt={`${orgBranding.name} logo`}

View File

@ -106,9 +106,9 @@ const commons = {
const dynamicEvent = {
length: 30,
slug: "dynamic",
title: "Dynamic",
eventName: "Dynamic Event",
description: "",
title: "Group Meeting",
eventName: "Group Meeting",
description: "Join us for a meeting with multiple people",
descriptionAsSafeHTML: "",
position: 0,
...commons,

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

View File

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

View File

@ -3745,6 +3745,7 @@ __metadata:
"@tailwindcss/forms": ^0.5.2
"@tailwindcss/line-clamp": ^0.4.0
"@tailwindcss/typography": ^0.5.4
"@todesktop/tailwind-variants": ^1.0.0
"@trivago/prettier-plugin-sort-imports": 4.1.1
"@typescript-eslint/eslint-plugin": ^5.52.0
"@typescript-eslint/parser": ^5.52.0
@ -4175,6 +4176,7 @@ __metadata:
"@calcom/tsconfig": "*"
"@calcom/types": "*"
"@faker-js/faker": ^7.3.0
"@formbricks/api": ^1.1.0
"@sendgrid/client": ^7.7.0
"@vercel/og": ^0.5.0
bcryptjs: ^2.4.3
@ -4590,7 +4592,7 @@ __metadata:
"@calcom/lib": "*"
"@calcom/trpc": "*"
"@calcom/tsconfig": "*"
"@formkit/auto-animate": ^1.0.0-beta.5
"@formkit/auto-animate": ^0.8.1
"@radix-ui/react-checkbox": ^1.0.4
"@radix-ui/react-dialog": ^1.0.4
"@radix-ui/react-popover": ^1.0.2
@ -4673,7 +4675,7 @@ __metadata:
"@calcom/types": "*"
"@calcom/ui": "*"
"@daily-co/daily-js": ^0.37.0
"@formkit/auto-animate": ^1.0.0-beta.5
"@formkit/auto-animate": ^0.8.1
"@glidejs/glide": ^3.5.2
"@hookform/error-message": ^2.0.0
"@hookform/resolvers": ^2.9.7
@ -6228,10 +6230,17 @@ __metadata:
languageName: node
linkType: hard
"@formkit/auto-animate@npm:^1.0.0-beta.5":
version: 1.0.0-beta.5
resolution: "@formkit/auto-animate@npm:1.0.0-beta.5"
checksum: 479a4f694d91e7272b2d8bdf2051fd1ae2dd5fbb522612406eaef55d88b7131402a7d5f683df8eb4e7c00810ebec8d3048483a774b0864c299d2d7af81262eb2
"@formbricks/api@npm:^1.1.0":
version: 1.4.0
resolution: "@formbricks/api@npm:1.4.0"
checksum: 2fabf27f7a1013d7e327e1c1d3d99d4860451a51ce7775968d62e9cd6149b62fce4e736b4a5c21800040b257bff82733a28c3409d8d0ae48cb59d61ca1738c6a
languageName: node
linkType: hard
"@formkit/auto-animate@npm:^0.8.1":
version: 0.8.1
resolution: "@formkit/auto-animate@npm:0.8.1"
checksum: ffc1a3432ce81bcf7abd4360fabf05b7aee01005e64ce519f57d134e68a5e15a81f295a7b2979678896bfd7d2048202a9fa2dbd09781267899e9056e918120c1
languageName: node
linkType: hard
@ -12255,6 +12264,15 @@ __metadata:
languageName: node
linkType: hard
"@todesktop/tailwind-variants@npm:^1.0.0":
version: 1.0.1
resolution: "@todesktop/tailwind-variants@npm:1.0.1"
peerDependencies:
tailwindcss: ^3.4.0
checksum: 53dffba36455b555553cf4d1d74554b54f4a586ef6dc8f7eb66d586b935765f56a0b806c2bbba0fddb435340744fe68bb9e1bbf66a4646fdc3b18af13521e4b6
languageName: node
linkType: hard
"@tootallnate/once@npm:1":
version: 1.1.2
resolution: "@tootallnate/once@npm:1.1.2"