Merge branch 'main' into teste2e-availability
This commit is contained in:
commit
c8d18f9b25
11
.env.example
11
.env.example
|
@ -293,5 +293,12 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
|||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_ADMIN_ENABLED=0
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories from /apps/categories or not
|
||||
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E App-Store Apps
|
||||
name: E2E App-Store Apps Tests
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E Embed React tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed React tests and booking flow (for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E Embed Core tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed Core tests and booking flow (for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E test
|
||||
name: E2E tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
|
|
@ -15,35 +15,6 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
- 'playwright.config.ts'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
type-check:
|
||||
name: Type check
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
|
@ -51,7 +22,7 @@ jobs:
|
|||
|
||||
test:
|
||||
name: Unit tests
|
||||
uses: ./.github/workflows/test.yml
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
|
@ -64,42 +35,13 @@ jobs:
|
|||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [lint, type-check, test, build]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
name: Pre-release checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
- 'playwright.config.ts'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production build
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [e2e, e2e-app-store, e2e-embed, e2e-embed-react, build-without-database]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
|
@ -44,5 +44,5 @@ jobs:
|
|||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link to [collect XP and win prizes!](https://cal.com/blog/community-incentives)
|
||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
|||
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
||||
// for example making weekStart only accept weekdays as input
|
||||
const schemaUserEditParams = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.string().email().toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
|
@ -114,7 +114,7 @@ const schemaUserEditParams = z.object({
|
|||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
|
||||
const schemaUserCreateParams = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.string().email().toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
const { withAxiom } = require("next-axiom");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
module.exports = withAxiom({
|
||||
const plugins = [withAxiom];
|
||||
const nextConfig = {
|
||||
transpilePackages: [
|
||||
"@calcom/app-store",
|
||||
"@calcom/core",
|
||||
|
@ -66,4 +68,15 @@ module.exports = withAxiom({
|
|||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
nextConfig["sentry"] = {
|
||||
autoInstrumentServerFunctions: true,
|
||||
hideSourceMaps: true,
|
||||
};
|
||||
|
||||
plugins.push(withSentryConfig);
|
||||
}
|
||||
|
||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
|
|
@ -3,8 +3,10 @@ import type { NextApiRequest } from "next";
|
|||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
|
||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
||||
|
@ -316,7 +318,13 @@ async function checkPermissions(req: NextApiRequest) {
|
|||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
if (!isAdmin && body.teamId)
|
||||
if (
|
||||
body.teamId &&
|
||||
!isAdmin &&
|
||||
!(await canUserAccessTeamWithRole(req.prisma, req.userId, isAdmin, body.teamId, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
}))
|
||||
)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `teamId`",
|
||||
|
|
|
@ -27,6 +27,16 @@ export async function checkPermissions(
|
|||
version: req.query.version,
|
||||
apiKey: req.query.apiKey,
|
||||
});
|
||||
return canUserAccessTeamWithRole(prisma, userId, isAdmin, teamId, role);
|
||||
}
|
||||
|
||||
export async function canUserAccessTeamWithRole(
|
||||
prisma: NextApiRequest["prisma"],
|
||||
userId: number,
|
||||
isAdmin: boolean,
|
||||
teamId: number,
|
||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||
) {
|
||||
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export {};
|
|
@ -0,0 +1,5 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
|
@ -197,88 +197,91 @@
|
|||
@layer {
|
||||
:root {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #e5e7eb;
|
||||
--cal-bg: white;
|
||||
--cal-bg-subtle: #f3f4f6;
|
||||
--cal-bg-muted: #f9fafb;
|
||||
--cal-bg-inverted: #111827;
|
||||
|
||||
|
||||
--cal-bg-emphasis: hsla(220,13%,91%,1);
|
||||
--cal-bg: hsla(0,0%,100%,1);
|
||||
--cal-bg-subtle: hsla(220, 14%, 96%,1);
|
||||
--cal-bg-muted: hsla(210,20%,98%,1);
|
||||
--cal-bg-inverted: hsla(0,0%,6%,1);
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
--cal-bg-info: hsla(218,83%,98%,1);
|
||||
--cal-bg-success: hsla(134,76%,94%,1);
|
||||
--cal-bg-attention: hsla(37, 86%, 92%, 1);
|
||||
--cal-bg-error: hsla(3,66,93,1);
|
||||
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis: #9ca3af;
|
||||
--cal-border: #d1d5db;
|
||||
--cal-border-subtle: #e5e7eb;
|
||||
--cal-border-muted: #f3f4f6;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
--cal-border-emphasis: hsla(218, 11%, 65%, 1);
|
||||
--cal-border: hsla(216, 12%, 84%, 1);
|
||||
--cal-border-subtle: hsla(220, 13%, 91%, 1);
|
||||
--cal-border-booker: #e5e7eb;
|
||||
--cal-border-muted: hsla(220, 14%, 96%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #111827;
|
||||
--cal-text: #374151;
|
||||
--cal-text-subtle: #6b7280;
|
||||
--cal-text-muted: #9ca3af;
|
||||
--cal-text-inverted: white;
|
||||
|
||||
--cal-text-emphasis: hsla(217, 19%, 27%, 1);
|
||||
--cal-text: hsla(217, 19%, 27%, 1);
|
||||
--cal-text-subtle: hsla(220, 9%, 46%, 1);
|
||||
--cal-text-muted: hsla(218, 11%, 65%, 1);
|
||||
--cal-text-inverted: hsla(0, 0%, 100%, 1);
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
|
||||
--cal-text-info: hsla(228, 56%, 33%, 1);
|
||||
--cal-text-success: hsla(133, 34%, 24%, 1);
|
||||
--cal-text-attention: hsla(16, 62%, 28%, 1);
|
||||
--cal-text-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Brand shinanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: #111827;
|
||||
--cal-brand-emphasis: #101010;
|
||||
--cal-brand-text: white;
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: hsla(221, 39%, 11%, 1);
|
||||
--cal-brand-emphasis: hsla(0, 0%, 6%, 1);
|
||||
--cal-brand-text: hsla(0, 0%, 100%, 1);
|
||||
}
|
||||
.dark {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #2b2b2b;
|
||||
--cal-bg: #101010;
|
||||
--cal-bg-subtle: #2b2b2b;
|
||||
--cal-bg-muted: #1c1c1c;
|
||||
--cal-bg-inverted: #f3f4f6;
|
||||
|
||||
|
||||
--cal-bg-emphasis: hsla(0, 0%, 32%, 1);
|
||||
--cal-bg: hsla(0, 0%, 10%, 1);
|
||||
--cal-bg-subtle: hsla(0, 0%, 18%, 1);
|
||||
--cal-bg-muted: hsla(0, 0%, 12%, 1);
|
||||
--cal-bg-inverted: hsla(220, 14%, 96%, 1);
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #263fa9;
|
||||
--cal-bg-success: #306339;
|
||||
--cal-bg-attention: #8e3b1f;
|
||||
--cal-bg-error: #8c2822;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
--cal-bg-info: hsla(228, 56%, 33%, 1);
|
||||
--cal-bg-success: hsla(133, 34%, 24%, 1);
|
||||
--cal-bg-attention: hsla(16, 62%, 28%, 1);
|
||||
--cal-bg-error: hsla(2, 55%, 30%, 1);
|
||||
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis: #575757;
|
||||
--cal-border: #444444;
|
||||
--cal-border-subtle: #2b2b2b;
|
||||
--cal-border-muted: #1c1c1c;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
--cal-border-emphasis: hsla(0, 0%, 46%, 1);
|
||||
--cal-border: hsla(0, 0%, 34%, 1);
|
||||
--cal-border-subtle: hsla(0, 0%, 22%, 1);
|
||||
--cal-border-booker: hsla(0, 0%, 22%, 1);
|
||||
--cal-border-muted: hsla(0, 0%, 18%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #f3f4f6;
|
||||
--cal-text: #d6d6d6;
|
||||
--cal-text-subtle: #757575;
|
||||
--cal-text-muted: #575757;
|
||||
--cal-text-inverted: #101010;
|
||||
|
||||
--cal-text-emphasis: hsla(240, 20%, 99%, 1);
|
||||
--cal-text: hsla(0, 0%, 84%, 1);
|
||||
--cal-text-subtle: hsla(0, 0%, 65%, 1);
|
||||
--cal-text-muted: hsla(0, 0%, 34%, 1);
|
||||
--cal-text-inverted: hsla(0, 0%, 10%, 1);
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #dee9fc;
|
||||
--cal-text-success: #e2fbe8;
|
||||
--cal-text-attention: #fceed8;
|
||||
--cal-text-error: #f9e3e2;
|
||||
|
||||
--cal-text-info: hsla(218, 83%, 93%, 1);
|
||||
--cal-text-success: hsla(134, 76%, 94%, 1);
|
||||
--cal-text-attention: hsla(37, 86%, 92%, 1);
|
||||
--cal-text-error: hsla(3, 66%, 93%, 1);
|
||||
|
||||
/* Brand shenanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: white;
|
||||
--cal-brand-emphasis: #e1e1e1;
|
||||
--cal-brand-text: black;
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: hsla(0, 0%, 100%, 1);
|
||||
--cal-brand-emphasis: hsla(218, 11%, 65%, 1);
|
||||
--cal-brand-text: hsla(0, 0%, 0%,1);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { createHydrateClient } from "app/_trpc/createHydrateClient";
|
||||
import { createHydrateClient } from "_app/_trpc/createHydrateClient";
|
||||
import superjson from "superjson";
|
||||
|
||||
export const HydrateClient = createHydrateClient({
|
|
@ -1,6 +1,6 @@
|
|||
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { HydrateClient } from "app/_trpc/HydrateClient";
|
||||
import { trpc } from "app/_trpc/client";
|
||||
import { HydrateClient } from "_app/_trpc/HydrateClient";
|
||||
import { trpc } from "_app/_trpc/client";
|
||||
import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { type ReactElement } from "react";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type EventTypesLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: EventTypesLayoutProps) {
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import AppPage from "@pages/apps/[slug]/index";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import { notFound } from "next/navigation";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
|
||||
import { APP_NAME, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const sourceSchema = z.object({
|
||||
content: z.string(),
|
||||
data: z.object({
|
||||
description: z.string().optional(),
|
||||
items: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
iframe: z.object({ src: z.string() }),
|
||||
}),
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const { data } = await getPageProps({ params });
|
||||
|
||||
return await _generateMetadata(
|
||||
() => `${data.name} | ${APP_NAME}`,
|
||||
() => data.description
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
try {
|
||||
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||
return appStore.map(({ slug }) => ({ slug }));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
if (typeof params?.slug !== "string") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const appMeta = await getAppWithMetadata({
|
||||
slug: params?.slug,
|
||||
});
|
||||
|
||||
const appFromDb = await prisma.app.findUnique({
|
||||
where: { slug: params.slug.toLowerCase() },
|
||||
});
|
||||
|
||||
const isAppAvailableInFileSystem = appMeta;
|
||||
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||
|
||||
if (!IS_PRODUCTION && isAppDisabled) {
|
||||
return {
|
||||
isAppDisabled: true as const,
|
||||
data: {
|
||||
...appMeta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!appFromDb || !appMeta || isAppDisabled) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isTemplate = appMeta.isTemplate;
|
||||
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||
const postFilePath = path.join(README_PATH);
|
||||
let source = "";
|
||||
|
||||
try {
|
||||
source = fs.readFileSync(postFilePath).toString();
|
||||
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||
} catch (error) {
|
||||
/* If the app doesn't have a README we fallback to the package description */
|
||||
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||
source = appMeta.description;
|
||||
}
|
||||
|
||||
const result = matter(source);
|
||||
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||
if (data.items) {
|
||||
data.items = data.items.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return getAppAssetFullPath(item, {
|
||||
dirName: appMeta.dirName,
|
||||
isTemplate: appMeta.isTemplate,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return {
|
||||
isAppDisabled: false as const,
|
||||
source: { content, data },
|
||||
data: appMeta,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
|
||||
return <AppPage {...pageProps} />;
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
|
@ -0,0 +1,36 @@
|
|||
import SetupPage from "@pages/apps/[slug]/setup";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
return await _generateMetadata(
|
||||
() => `${params.slug} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
const result = await getServerSideProps({ params, req } as unknown as GetServerSidePropsContext);
|
||||
|
||||
if (!result || "notFound" in result) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if ("redirect" in result) {
|
||||
redirect(result.redirect.destination);
|
||||
}
|
||||
|
||||
return result.props;
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
return <SetupPage {...pageProps} />;
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import CategoryPage from "@pages/apps/categories/[category]";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `${APP_NAME} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = Object.keys(AppCategories);
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time. Make sure we fall back to building these pages on demand
|
||||
return [];
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return paths.map((category) => ({ category }));
|
||||
};
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const appQuery = await prisma.app.findMany({
|
||||
where: {
|
||||
categories: {
|
||||
has: p.data.category,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dbAppsSlugs = appQuery.map((category) => category.slug);
|
||||
|
||||
const appStore = await getAppRegistry();
|
||||
|
||||
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
|
||||
return {
|
||||
apps,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const { apps } = await getPageProps({ params });
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||
<CategoryPage apps={apps} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
|
@ -0,0 +1,56 @@
|
|||
import LegacyPage from "@pages/apps/categories/index";
|
||||
import { ssrInit } from "_app/_trpc/ssrInit";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
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 PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `Categories | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
async function getPageProps() {
|
||||
const ssr = await ssrInit();
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage
|
||||
const session = await getServerSession({ req });
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id);
|
||||
} else {
|
||||
appStore = await getAppRegistry();
|
||||
}
|
||||
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
|
||||
dehydratedState: await ssr.dehydrate(),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const props = await getPageProps();
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
|
||||
<LegacyPage {...props} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { type ReactElement } from "react";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type EventTypesLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: EventTypesLayoutProps) {
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import LegacyPage from "@pages/apps/installed/[category]";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${t("installed_apps")} | ${APP_NAME}`,
|
||||
(t) => t("manage_your_connected_apps")
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
category: p.data.category,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const { category } = await getPageProps({ params });
|
||||
|
||||
return <LegacyPage />;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/apps/[category]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/apps/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/flags";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/impersonation";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/oAuth/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import EventTypes from "@pages/event-types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/oAuth/oAuthView";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,4 +1,4 @@
|
|||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { getServerCaller } from "app/_trpc/serverClient";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getServerCaller } from "_app/_trpc/serverClient";
|
||||
import { type Params } from "_app/_types";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/users/pages/users-add-view";
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { _generateMetadata } from "app/_utils";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/users/pages/users-listing-view";
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { dir } from "i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import React from "react";
|
||||
|
@ -10,6 +12,14 @@ import { prepareRootMetadata } from "@lib/metadata";
|
|||
|
||||
import "../styles/globals.css";
|
||||
|
||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||
const calFont = localFont({
|
||||
src: "../fonts/CalSans-SemiBold.woff2",
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "block",
|
||||
});
|
||||
|
||||
export const generateMetadata = () =>
|
||||
prepareRootMetadata({
|
||||
twitterCreator: "@calcom",
|
||||
|
@ -66,6 +76,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
|
||||
/>
|
||||
)}
|
||||
<style>{`
|
||||
:root {
|
||||
--font-inter: ${interFont.style.fontFamily.replace(/\'/g, "")};
|
||||
--font-cal: ${calFont.style.fontFamily.replace(/\'/g, "")};
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body
|
||||
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
|
|
@ -6,6 +6,11 @@ import z from "zod";
|
|||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
||||
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
import { type DehydratedState } from "@tanstack/react-query";
|
||||
import type { SSRConfig } from "next-i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
// import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Script from "next/script";
|
||||
|
@ -20,14 +18,6 @@ export interface CalPageWrapper {
|
|||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
}
|
||||
|
||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||
const calFont = localFont({
|
||||
src: "../fonts/CalSans-SemiBold.woff2",
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export type PageWrapperProps = Readonly<{
|
||||
getLayout: ((page: React.ReactElement) => ReactNode) | null;
|
||||
children: React.ReactElement;
|
||||
|
@ -71,13 +61,6 @@ function PageWrapper(props: PageWrapperProps) {
|
|||
id="page-status"
|
||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||
/>
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-inter: ${interFont.style.fontFamily};
|
||||
--font-cal: ${calFont.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{getLayout(
|
||||
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : props.children
|
||||
)}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import type { EventLocationType, getEventLocationValue } from "@calcom/app-store/locations";
|
||||
import {
|
||||
getEventLocationType,
|
||||
getSuccessPageLocationMessage,
|
||||
guessEventLocationType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||
import "@calcom/dayjs/locales";
|
||||
|
@ -14,6 +18,7 @@ import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { ActionType } from "@calcom/ui";
|
||||
|
@ -93,6 +98,16 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
|
||||
const paymentAppData = getPaymentAppData(booking.eventType);
|
||||
|
||||
const location = booking.location as ReturnType<typeof getEventLocationValue>;
|
||||
const locationVideoCallUrl = bookingMetadataSchema.parse(booking?.metadata || {})?.videoCallUrl;
|
||||
|
||||
const locationToDisplay = getSuccessPageLocationMessage(
|
||||
locationVideoCallUrl ? locationVideoCallUrl : location,
|
||||
t,
|
||||
booking.status
|
||||
);
|
||||
const provider = guessEventLocationType(location);
|
||||
|
||||
const bookingConfirm = async (confirm: boolean) => {
|
||||
let body = {
|
||||
bookingId: booking.id,
|
||||
|
@ -359,6 +374,33 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
attendees={booking.attendees}
|
||||
/>
|
||||
</div>
|
||||
{!isPending && (
|
||||
<div>
|
||||
{(provider?.label || locationToDisplay?.startsWith("https://")) &&
|
||||
locationToDisplay.startsWith("http") && (
|
||||
<a
|
||||
href={locationToDisplay}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
target="_blank"
|
||||
title={locationToDisplay}
|
||||
rel="noreferrer"
|
||||
className="text-sm leading-6 text-blue-600 hover:underline">
|
||||
<div className="flex items-center gap-2">
|
||||
{provider?.iconUrl && (
|
||||
<img
|
||||
src={provider.iconUrl}
|
||||
className="h-4 w-4 rounded-sm"
|
||||
alt={`${provider?.label} logo`}
|
||||
/>
|
||||
)}
|
||||
{provider?.label
|
||||
? t("join_event_location", { eventLocationType: provider?.label })
|
||||
: t("join_meeting")}
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isPending && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||
{t("unconfirmed")}
|
||||
|
|
|
@ -322,6 +322,7 @@ function EventTypeSingleLayout({
|
|||
StartIcon={Code}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
namespace={eventType.slug}
|
||||
tooltip={t("embed")}
|
||||
tooltipSide="bottom"
|
||||
tooltipOffset={4}
|
||||
|
|
|
@ -152,9 +152,7 @@ const UserProfile = () => {
|
|||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
/>
|
||||
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
|
||||
{t("few_sentences_about_yourself")}
|
||||
</p>
|
||||
<p className="text-default mt-2 font-sans text-sm font-normal">{t("few_sentences_about_yourself")}</p>
|
||||
</fieldset>
|
||||
<Button EndIcon={ArrowRight} type="submit" className="mt-8 w-full items-center justify-center">
|
||||
{t("finish")}
|
||||
|
|
|
@ -49,7 +49,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||
{...rest}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded"
|
||||
className="text-emphasis focus:ring-emphasis dark:text-muted border-default bg-default h-4 w-4 rounded"
|
||||
/>
|
||||
</div>
|
||||
<span className="ms-2 text-sm">{description}</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { TrpcProvider } from "_app/_trpc/trpc-provider";
|
||||
import { dir } from "i18next";
|
||||
import type { Session } from "next-auth";
|
||||
import { SessionProvider, useSession } from "next-auth/react";
|
||||
|
@ -97,13 +97,9 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR
|
|||
const clientViewerI18n = useViewerI18n(locale);
|
||||
const i18n = clientViewerI18n.data?.i18n ?? props.i18n;
|
||||
|
||||
if (!i18n || !i18n._nextI18Next) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error AppWithTranslationHoc expects AppProps
|
||||
<AppWithTranslationHoc pageProps={{ _nextI18Next: i18n._nextI18Next }}>
|
||||
<AppWithTranslationHoc pageProps={{ _nextI18Next: i18n?._nextI18Next }}>
|
||||
{props.children}
|
||||
</AppWithTranslationHoc>
|
||||
);
|
||||
|
|
|
@ -64,6 +64,23 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
|||
requestHeaders.set("x-csp-enforce", "true");
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/future/apps/installed")) {
|
||||
const returnTo = req.cookies.get("return-to")?.value;
|
||||
if (returnTo !== undefined) {
|
||||
requestHeaders.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
||||
|
||||
let validPathname = returnTo;
|
||||
|
||||
try {
|
||||
validPathname = new URL(returnTo).pathname;
|
||||
} catch (e) {}
|
||||
|
||||
const nextUrl = url.clone();
|
||||
nextUrl.pathname = validPathname;
|
||||
return NextResponse.redirect(nextUrl, { headers: requestHeaders });
|
||||
}
|
||||
}
|
||||
|
||||
requestHeaders.set("x-pathname", url.pathname);
|
||||
|
||||
const locale = await getLocale(req);
|
||||
|
@ -103,6 +120,16 @@ export const config = {
|
|||
"/future/event-types/",
|
||||
"/settings/admin/:path*",
|
||||
"/future/settings/admin/:path*",
|
||||
"/apps/installed/:category/",
|
||||
"/future/apps/installed/:category/",
|
||||
"/apps/:slug/",
|
||||
"/future/apps/:slug/",
|
||||
"/apps/:slug/setup/",
|
||||
"/future/apps/:slug/setup/",
|
||||
"/apps/categories/",
|
||||
"/future/apps/categories/",
|
||||
"/apps/categories/:category/",
|
||||
"/future/apps/categories/:category/",
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -161,7 +161,7 @@ const nextConfig = {
|
|||
...i18n,
|
||||
localeDetection: false,
|
||||
},
|
||||
productionBrowserSourceMaps: true,
|
||||
productionBrowserSourceMaps: false,
|
||||
/* We already do type check on GH actions */
|
||||
typescript: {
|
||||
ignoreBuildErrors: !!process.env.CI,
|
||||
|
@ -516,6 +516,11 @@ const nextConfig = {
|
|||
destination: "/apps/installed/conferencing",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/apps/installed",
|
||||
destination: "/apps/installed/calendar",
|
||||
permanent: true,
|
||||
},
|
||||
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
|
||||
...(process.env.NODE_ENV === "development" &&
|
||||
// Safer to enable the redirect only when the user is opting to test out organizations
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.5.4",
|
||||
"version": "3.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import {
|
||||
getBookingForReschedule,
|
||||
getBookingForSeatedEvent,
|
||||
getMultipleDurationValue,
|
||||
} from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
|
@ -26,6 +23,16 @@ import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect";
|
|||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
export const getMultipleDurationValue = (
|
||||
multipleDurationConfig: number[] | undefined,
|
||||
queryDuration: string | string[] | null | undefined,
|
||||
defaultValue: number
|
||||
) => {
|
||||
if (!multipleDurationConfig) return null;
|
||||
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export default function Type({
|
||||
slug,
|
||||
user,
|
||||
|
@ -35,9 +42,10 @@ export default function Type({
|
|||
isBrandingHidden,
|
||||
isSEOIndexable,
|
||||
rescheduleUid,
|
||||
entity,
|
||||
duration,
|
||||
eventData,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
|
@ -46,7 +54,7 @@ export default function Type({
|
|||
rescheduleUid={rescheduleUid ?? undefined}
|
||||
hideBranding={isBrandingHidden}
|
||||
isSEOIndexable={isSEOIndexable ?? true}
|
||||
entity={entity}
|
||||
entity={eventData.entity}
|
||||
bookingData={booking}
|
||||
/>
|
||||
<Booker
|
||||
|
@ -55,8 +63,16 @@ export default function Type({
|
|||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={entity}
|
||||
duration={duration}
|
||||
entity={eventData.entity}
|
||||
durationConfig={eventData.metadata?.multipleDuration}
|
||||
/* TODO: Currently unused, evaluate it is needed-
|
||||
* Possible alternative approach is to have onDurationChange.
|
||||
*/
|
||||
duration={getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
searchParams?.get("duration"),
|
||||
eventData.length
|
||||
)}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
@ -68,7 +84,7 @@ Type.PageWrapper = PageWrapper;
|
|||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
|
@ -120,12 +136,14 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||
|
||||
return {
|
||||
props: {
|
||||
entity: eventData.entity,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: {
|
||||
...eventData.metadata,
|
||||
multipleDuration: [15, 30, 60],
|
||||
},
|
||||
},
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
|
@ -144,7 +162,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const username = usernames[0];
|
||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
|
@ -207,15 +225,14 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||
return {
|
||||
props: {
|
||||
booking,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
away: user?.away,
|
||||
user: username,
|
||||
slug,
|
||||
entity: eventData.entity,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
|
||||
import Link from "next/link";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
|
@ -13,7 +15,7 @@ import PageWrapper from "@components/PageWrapper";
|
|||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export default function Apps({ categories }: inferSSRProps<typeof getServerSideProps>) {
|
||||
export default function Apps({ categories }: Omit<inferSSRProps<typeof getServerSideProps>, "trpcState">) {
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useReducer } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
|
|||
: isSAMLLoginEnabled && !isLoading && data?.connectionExists;
|
||||
|
||||
return (
|
||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:#9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||
<AuthContainer
|
||||
title={t("login")}
|
||||
description={t("login")}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { motion } from "framer-motion";
|
||||
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { AlertTriangle, Check, MailOpen } from "@calcom/ui/components/icon";
|
||||
import { AlertTriangle, ExternalLink, MailOpen } from "@calcom/ui/components/icon";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
@ -54,7 +57,62 @@ const querySchema = z.object({
|
|||
t: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function Verify() {
|
||||
const PaymentFailedIcon = () => (
|
||||
<div className="rounded-full bg-orange-900 p-3">
|
||||
<AlertTriangle className="h-6 w-6 flex-shrink-0 p-0.5 font-extralight text-orange-100" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const PaymentSuccess = () => (
|
||||
<div
|
||||
className="rounded-full"
|
||||
style={{
|
||||
padding: "6px",
|
||||
border: "0.6px solid rgba(0, 0, 0, 0.02)",
|
||||
background: "rgba(123, 203, 197, 0.10)",
|
||||
}}>
|
||||
<motion.div
|
||||
className="rounded-full"
|
||||
style={{
|
||||
padding: "6px",
|
||||
border: "0.6px solid rgba(0, 0, 0, 0.04)",
|
||||
background: "rgba(123, 203, 197, 0.16)",
|
||||
}}
|
||||
animate={{ scale: [1, 1.1, 1] }} // Define the pulsing animation for the second ring
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
delay: 0.2, // Delay the start of animation for the second ring
|
||||
}}>
|
||||
<motion.div
|
||||
className="rounded-full p-3"
|
||||
style={{
|
||||
border: "1px solid rgba(255, 255, 255, 0.40)",
|
||||
background: "linear-gradient(180deg, #66C9CF 0%, #9CCCB2 100%)",
|
||||
}}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.69185 10.6919L2.9297 10.9297L2.69185 10.6919C1.96938 11.4143 1.96938 12.5857 2.69185 13.3081L7.69185 18.3081C8.41432 19.0306 9.58568 19.0306 10.3081 18.3081L21.3081 7.30815C22.0306 6.58568 22.0306 5.41432 21.3081 4.69185C20.5857 3.96938 19.4143 3.96938 18.6919 4.69185L9 14.3837L5.30815 10.6919C4.58568 9.96938 3.41432 9.96938 2.69185 10.6919Z"
|
||||
fill="white"
|
||||
stroke="#48BAAE"
|
||||
strokeWidth="0.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MailOpenIcon = () => (
|
||||
<div className="bg-default rounded-full p-3">
|
||||
<MailOpen className="text-emphasis h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function Verify(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
@ -112,7 +170,7 @@ export default function Verify() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="text-inverted bg-black bg-opacity-90 backdrop-blur-md backdrop-grayscale backdrop-filter">
|
||||
<div className="text-default bg-muted bg-opacity-90 backdrop-blur-md backdrop-grayscale backdrop-filter">
|
||||
<Head>
|
||||
<title>
|
||||
{/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth
|
||||
|
@ -125,17 +183,9 @@ export default function Verify() {
|
|||
</title>
|
||||
</Head>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
||||
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
|
||||
<div className="rounded-full border border-white p-3">
|
||||
{hasPaymentFailed ? (
|
||||
<AlertTriangle className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
) : sessionId ? (
|
||||
<Check className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight dark:text-white" />
|
||||
) : (
|
||||
<MailOpen className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-cal text-inverted my-6 text-3xl font-normal dark:text-white">
|
||||
<div className="border-subtle bg-default m-10 flex max-w-2xl flex-col items-center rounded-xl border px-8 py-14 text-left">
|
||||
{hasPaymentFailed ? <PaymentFailedIcon /> : sessionId ? <PaymentSuccess /> : <MailOpenIcon />}
|
||||
<h3 className="font-cal text-emphasis my-6 text-2xl font-normal leading-none">
|
||||
{hasPaymentFailed
|
||||
? "Your payment failed"
|
||||
: sessionId
|
||||
|
@ -145,41 +195,60 @@ export default function Verify() {
|
|||
{hasPaymentFailed && (
|
||||
<p className="my-6">Your account has been created, but your premium has not been reserved.</p>
|
||||
)}
|
||||
<p className="text-inverted dark:text-white">
|
||||
<p className="text-muted dark:text-subtle text-base font-normal">
|
||||
We have sent an email to <b>{customer?.email} </b>with a link to activate your account.{" "}
|
||||
{hasPaymentFailed &&
|
||||
"Once you activate your account you will be able to try purchase your premium username again or select a different one."}
|
||||
</p>
|
||||
<p className="text-muted mt-6">
|
||||
Don't see an email? Click the button below to send another email.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex space-x-5 text-center">
|
||||
<div className="mt-7">
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={secondsLeft > 0}
|
||||
onClick={async (e) => {
|
||||
if (!customer) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
setSecondsLeft(30);
|
||||
// Update query params with t:timestamp, shallow: true doesn't re-render the page
|
||||
const _searchParams = new URLSearchParams(searchParams?.toString());
|
||||
_searchParams.set("t", `${Date.now()}`);
|
||||
router.replace(`${pathname}?${_searchParams.toString()}`);
|
||||
return await sendVerificationLogin(customer.email, customer.username);
|
||||
}}>
|
||||
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Send another mail"}
|
||||
</Button>
|
||||
<Button color="primary" href={`${WEBAPP_URL || "https://app.cal.com"}/auth/login`}>
|
||||
Login using another method
|
||||
href={
|
||||
props.EMAIL_FROM
|
||||
? encodeURIComponent(`https://mail.google.com/mail/u/0/#search/from:${props.EMAIL_FROM}`)
|
||||
: "https://mail.google.com/mail/u/0/"
|
||||
}
|
||||
target="_blank"
|
||||
EndIcon={ExternalLink}>
|
||||
Open in Gmail
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-subtle text-base font-normal ">Don’t seen an email?</p>
|
||||
<button
|
||||
className={classNames(
|
||||
"font-light",
|
||||
secondsLeft > 0 ? "text-muted" : "underline underline-offset-2 hover:font-normal"
|
||||
)}
|
||||
disabled={secondsLeft > 0}
|
||||
onClick={async (e) => {
|
||||
if (!customer) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
setSecondsLeft(30);
|
||||
// Update query params with t:timestamp, shallow: true doesn't re-render the page
|
||||
const _searchParams = new URLSearchParams(searchParams?.toString());
|
||||
_searchParams.set("t", `${Date.now()}`);
|
||||
router.replace(`${pathname}?${_searchParams.toString()}`);
|
||||
return await sendVerificationLogin(customer.email, customer.username);
|
||||
}}>
|
||||
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Resend"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const EMAIL_FROM = process.env.EMAIL_FROM;
|
||||
|
||||
return {
|
||||
props: {
|
||||
EMAIL_FROM,
|
||||
},
|
||||
};
|
||||
};
|
||||
Verify.PageWrapper = PageWrapper;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle";
|
||||
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
|
||||
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
|
@ -81,6 +82,7 @@ export default function Bookings() {
|
|||
const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
|
||||
const { t } = useLocale();
|
||||
const user = useMeQuery().data;
|
||||
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
|
||||
|
||||
const query = trpc.viewer.bookings.get.useInfiniteQuery(
|
||||
{
|
||||
|
@ -151,12 +153,11 @@ export default function Bookings() {
|
|||
return (
|
||||
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col flex-wrap lg:flex-row">
|
||||
<div className="flex flex-row flex-wrap justify-between">
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
<div className="max-w-full overflow-x-auto xl:ml-auto">
|
||||
<FiltersContainer />
|
||||
</div>
|
||||
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
|
||||
</div>
|
||||
<FiltersContainer isFiltersVisible={isFiltersVisible} />
|
||||
<main className="w-full">
|
||||
<div className="flex w-full flex-col" ref={animationParentRef}>
|
||||
{query.status === "error" && (
|
||||
|
|
|
@ -403,7 +403,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.team && !isManagedEventType && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3 top-1"
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
users={type?.users ?? []}
|
||||
|
@ -411,7 +411,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
)}
|
||||
{isManagedEventType && type?.children && type.children?.length > 0 && (
|
||||
<AvatarGroup
|
||||
className="relative right-3 top-1"
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={type?.children
|
||||
|
@ -507,6 +507,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
{!isManagedEventType && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<EventTypeEmbedButton
|
||||
namespace={type.slug}
|
||||
as={DropdownItem}
|
||||
type="button"
|
||||
StartIcon={Code}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { z } from "zod";
|
|||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
|
@ -105,7 +106,12 @@ const OnboardingPage = () => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:9CA3AF] [--cal-brand:#111827] [--cal-brand-text:#FFFFFF] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand:white] dark:[--cal-brand-text:#000000]"
|
||||
className={classNames(
|
||||
"dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1]",
|
||||
"[--cal-brand-subtle:#9CA3AF]",
|
||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]"
|
||||
)}
|
||||
data-testid="onboarding"
|
||||
key={pathname}>
|
||||
<Head>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
@ -10,21 +12,29 @@ import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../[u
|
|||
import type { PageProps as TeamTypePageProps } from "../../../team/[slug]/[type]";
|
||||
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
orgSlug: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => slugify(s)),
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
});
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const { user: teamOrUserSlug, orgSlug, type } = paramsSchema.parse(ctx.params);
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: ctx.query.user as string,
|
||||
slug: teamOrUserSlug,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
parent: getSlugOrRequestedSlug(ctx.query.orgSlug as string),
|
||||
parent: getSlugOrRequestedSlug(orgSlug),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
const params = { slug: ctx.query.user, type: ctx.query.type };
|
||||
const params = { slug: teamOrUserSlug, type };
|
||||
return GSSTeamTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
@ -37,7 +47,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
},
|
||||
});
|
||||
}
|
||||
const params = { user: ctx.query.user, type: ctx.query.type };
|
||||
const params = { user: teamOrUserSlug, type };
|
||||
return GSSUserTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
|
|
@ -106,8 +106,17 @@ const ProfileView = () => {
|
|||
setConfirmAuthEmailChangeWarningDialogOpen(false);
|
||||
setTempFormValues(null);
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
onError: (e) => {
|
||||
switch (e.message) {
|
||||
// TODO: Add error codes.
|
||||
case "email_already_used":
|
||||
{
|
||||
showToast(t(e.message), "error");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -7,10 +7,12 @@ import { useRouter } from "next/navigation";
|
|||
import { useState, useEffect } from "react";
|
||||
import type { SubmitHandler } from "react-hook-form";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
|
@ -27,7 +29,7 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
|
|||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, HeadSeo, PasswordField, TextField, Form, Alert } from "@calcom/ui";
|
||||
import { Button, HeadSeo, PasswordField, TextField, Form, Alert, showToast } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
|
@ -66,16 +68,6 @@ const FEATURES = [
|
|||
},
|
||||
];
|
||||
|
||||
const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => {
|
||||
const [emailUser, emailDomain = ""] = email.split("@");
|
||||
const username =
|
||||
emailDomain === autoAcceptEmailDomain
|
||||
? slugify(emailUser)
|
||||
: slugify(`${emailUser}-${emailDomain.split(".")[0]}`);
|
||||
|
||||
return username;
|
||||
};
|
||||
|
||||
function UsernameField({
|
||||
username,
|
||||
setPremium,
|
||||
|
@ -237,7 +229,13 @@ export default function Signup({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand-emphasis:#101010] [--cal-brand:#111827] [--cal-brand-text:#FFFFFF] [--cal-brand-subtle:#9CA3AF] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand:white] dark:[--cal-brand-text:#000000]">
|
||||
<div
|
||||
className={classNames(
|
||||
"light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||
"[--cal-brand-subtle:#9CA3AF]",
|
||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
|
||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1] "
|
||||
)}>
|
||||
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||
{/* Left side */}
|
||||
|
@ -276,25 +274,29 @@ export default function Signup({
|
|||
await signUp(updatedValues);
|
||||
}}>
|
||||
{/* Username */}
|
||||
<UsernameField
|
||||
label={t("username")}
|
||||
username={watch("username")}
|
||||
premium={premiumUsername}
|
||||
usernameTaken={usernameTaken}
|
||||
setUsernameTaken={(value) => setUsernameTaken(value)}
|
||||
data-testid="signup-usernamefield"
|
||||
setPremium={(value) => setPremiumUsername(value)}
|
||||
addOnLeading={
|
||||
orgSlug
|
||||
? `${getOrgFullOrigin(orgSlug, { protocol: true })}/`
|
||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
|
||||
}
|
||||
/>
|
||||
{!isOrgInviteByLink ? (
|
||||
<UsernameField
|
||||
label={t("username")}
|
||||
username={watch("username") || ""}
|
||||
premium={premiumUsername}
|
||||
usernameTaken={usernameTaken}
|
||||
disabled={!!orgSlug}
|
||||
setUsernameTaken={(value) => setUsernameTaken(value)}
|
||||
data-testid="signup-usernamefield"
|
||||
setPremium={(value) => setPremiumUsername(value)}
|
||||
addOnLeading={
|
||||
orgSlug
|
||||
? `${getOrgFullOrigin(orgSlug, { protocol: true })}/`
|
||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{/* Email */}
|
||||
<TextField
|
||||
{...register("email")}
|
||||
label={t("email")}
|
||||
type="email"
|
||||
disabled={prepopulateFormValues?.email}
|
||||
data-testid="signup-emailfield"
|
||||
/>
|
||||
|
||||
|
@ -322,7 +324,7 @@ export default function Signup({
|
|||
: t("create_account")}
|
||||
</Button>
|
||||
</Form>
|
||||
{/* Continue with Social Logins */}
|
||||
{/* Continue with Social Logins - Only for non-invite links */}
|
||||
{token || (!isGoogleLoginEnabled && !isSAMLLoginEnabled) ? null : (
|
||||
<div className="mt-6">
|
||||
<div className="relative flex items-center">
|
||||
|
@ -334,7 +336,7 @@ export default function Signup({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Social Logins */}
|
||||
{/* Social Logins - Only for non-invite links*/}
|
||||
{!token && (
|
||||
<div className="mt-6 flex flex-col gap-2 md:flex-row">
|
||||
{isGoogleLoginEnabled ? (
|
||||
|
@ -366,7 +368,7 @@ export default function Signup({
|
|||
if (username) {
|
||||
// If username is present we save it in query params to check for premium
|
||||
const searchQueryParams = new URLSearchParams();
|
||||
searchQueryParams.set("username", formMethods.getValues("username"));
|
||||
searchQueryParams.set("username", username);
|
||||
localStorage.setItem("username", username);
|
||||
router.push(`${GOOGLE_AUTH_URL}?${searchQueryParams.toString()}`);
|
||||
return;
|
||||
|
@ -402,10 +404,14 @@ export default function Signup({
|
|||
return;
|
||||
}
|
||||
const username = formMethods.getValues("username");
|
||||
if (!username) {
|
||||
showToast("error", t("username_required"));
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("username", username);
|
||||
const sp = new URLSearchParams();
|
||||
// @NOTE: don't remove username query param as it's required right now for stripe payment page
|
||||
sp.set("username", formMethods.getValues("username"));
|
||||
sp.set("username", username);
|
||||
sp.set("email", formMethods.getValues("email"));
|
||||
router.push(
|
||||
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/sso/saml` + `?${sp.toString()}`
|
||||
|
@ -491,6 +497,7 @@ export default function Signup({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe.skip("apps/ A/B tests", () => {
|
||||
test("should point to the /future/apps/installed/[category]", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/apps/installed/messaging");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Messaging" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/[slug]", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/apps/telegram");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Telegram" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/[slug]/setup", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/apps/apple-calendar/setup");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Connect to Apple Server" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/categories", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/apps/categories");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByTestId("app-store-category-messaging");
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/categories/[category]", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/apps/categories/messaging");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByText(/messaging apps/i);
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
});
|
|
@ -365,7 +365,7 @@ test.describe("Booking round robin event", () => {
|
|||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
const team = await testUser.getFirstTeam();
|
||||
const team = await testUser.getFirstTeamMembership();
|
||||
await page.goto(`/team/${team.team.slug}`);
|
||||
});
|
||||
|
||||
|
@ -373,7 +373,7 @@ test.describe("Booking round robin event", () => {
|
|||
const [testUser] = users.get();
|
||||
testUser.apiLogin();
|
||||
|
||||
const team = await testUser.getFirstTeam();
|
||||
const team = await testUser.getFirstTeamMembership();
|
||||
|
||||
// Click first event type (round robin)
|
||||
await page.click('[data-testid="event-type-link"]');
|
||||
|
|
|
@ -11,7 +11,7 @@ import { bookTimeSlot, createNewEventType, selectFirstAvailableTimeSlotNextMonth
|
|||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Event Types A/B tests", () => {
|
||||
test("should point to the /future/event-types page", async ({ page, users, context }) => {
|
||||
test.skip("should point to the /future/event-types page", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
|
|
|
@ -62,6 +62,7 @@ export const createBookingsFixture = (page: Page) => {
|
|||
rescheduled,
|
||||
paid,
|
||||
status,
|
||||
iCalUID: `${uid}@cal.com`,
|
||||
},
|
||||
});
|
||||
const bookingFixture = createBookingFixture(booking, store.page);
|
||||
|
|
|
@ -458,7 +458,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
},
|
||||
getFirstTeam: async () => {
|
||||
getFirstTeamMembership: async () => {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: { userId: user.id },
|
||||
include: { team: true },
|
||||
|
|
|
@ -84,7 +84,7 @@ test.describe("Stripe integration", () => {
|
|||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
});
|
||||
await owner.apiLogin();
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
const teamEvent = await owner.getFirstTeamEvent(team.id);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { test } from "./fixtures";
|
|||
|
||||
export type RouteVariant = "future" | "legacy";
|
||||
|
||||
const routeVariants = ["future", "legacy"];
|
||||
const routeVariants = [/*"future",*/ "legacy"];
|
||||
|
||||
/**
|
||||
* Small wrapper around test.describe().
|
||||
|
|
|
@ -336,3 +336,7 @@ export async function fillStripeTestCheckout(page: Page) {
|
|||
await page.fill("[name=billingName]", "Stripe Stripeson");
|
||||
await page.click(".SubmitButton--complete-Shimmer");
|
||||
}
|
||||
|
||||
// When App directory is there, this is the 404 page text. It is commented till it's disabled
|
||||
// export const NotFoundPageText = "This page could not be found";
|
||||
export const NotFoundPageText = "ERROR 404";
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import type { Browser, Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { getInviteLink } from "../lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
@ -11,106 +14,435 @@ test.afterEach(async ({ users, emails }) => {
|
|||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test("Invitation (non verified)", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (external user)", async () => {
|
||||
const invitedUserEmail = `rick@domain-${Date.now()}.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!inviteLink) return null;
|
||||
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const newPage = await context.newPage();
|
||||
newPage.goto(inviteLink);
|
||||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
await context.close();
|
||||
await newPage.close();
|
||||
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.bringToFront();
|
||||
test.describe.serial("Organization", () => {
|
||||
test.describe("Email not matching orgAutoAcceptEmail", () => {
|
||||
test("Org Invitation", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
page.locator(`[data-testid="login-form"]`);
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("By email", async () => {
|
||||
const invitedUserEmail = `rick-${Date.now()}@domain.com`;
|
||||
// '-domain' because the email doesn't match orgAutoAcceptEmail
|
||||
const usernameDerivedFromEmail = `${invitedUserEmail.split("@")[0]}-domain`;
|
||||
await inviteAnEmail(page, invitedUserEmail);
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: false,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
assertInviteLink(inviteLink);
|
||||
await signupFromEmailInviteLink(browser, inviteLink);
|
||||
|
||||
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("By invite link", async () => {
|
||||
const inviteLink = await copyInviteLink(page);
|
||||
const email = `rick-${Date.now()}@domain.com`;
|
||||
// '-domain' because the email doesn't match orgAutoAcceptEmail
|
||||
const usernameDerivedFromEmail = `${email.split("@")[0]}-domain`;
|
||||
await signupFromInviteLink({ browser, inviteLink, email });
|
||||
const dbUser = await prisma.user.findUnique({ where: { email } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("To the organization by invite link", async () => {
|
||||
// Get the invite link
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
|
||||
test("Team invitation", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, hasSubteam: true });
|
||||
await orgOwner.apiLogin();
|
||||
const { team } = await orgOwner.getFirstTeamMembership();
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
|
||||
const inviteLink = await getInviteLink(page);
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const inviteLinkPage = await context.newPage();
|
||||
await inviteLinkPage.goto(inviteLink);
|
||||
await inviteLinkPage.waitForLoadState("networkidle");
|
||||
await test.step("By email", async () => {
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
const invitedUserEmail = `rick-${Date.now()}@domain.com`;
|
||||
// '-domain' because the email doesn't match orgAutoAcceptEmail
|
||||
const usernameDerivedFromEmail = `${invitedUserEmail.split("@")[0]}-domain`;
|
||||
await inviteAnEmail(page, invitedUserEmail);
|
||||
await expectUserToBeAMemberOfTeam({
|
||||
page,
|
||||
teamId: team.id,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: false,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
// Check required fields
|
||||
const button = inviteLinkPage.locator("button[type=submit][disabled]");
|
||||
await expect(button).toBeVisible(); // email + 3 password hints
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: false,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
// Happy path
|
||||
await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`);
|
||||
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await inviteLinkPage.waitForURL("/getting-started");
|
||||
await page.waitForLoadState("networkidle");
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${team.name}'s admin invited you to join the team ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
assertInviteLink(inviteLink);
|
||||
|
||||
await signupFromEmailInviteLink(browser, inviteLink);
|
||||
|
||||
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
|
||||
await expectUserToBeAMemberOfTeam({
|
||||
page,
|
||||
teamId: team.id,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("By invite link", async () => {
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
const inviteLink = await copyInviteLink(page);
|
||||
const email = `rick-${Date.now()}@domain.com`;
|
||||
// '-domain' because the email doesn't match orgAutoAcceptEmail
|
||||
const usernameDerivedFromEmail = `${email.split("@")[0]}-domain`;
|
||||
await signupFromInviteLink({ browser, inviteLink, email });
|
||||
|
||||
const dbUser = await prisma.user.findUnique({ where: { email } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
await expectUserToBeAMemberOfTeam({
|
||||
teamId: team.id,
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: email,
|
||||
});
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: email,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (internal user)", async () => {
|
||||
const invitedUserEmail = `rick-${Date.now()}@example.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
test.describe("Email matching orgAutoAcceptEmail and a Verified Organization", () => {
|
||||
test("Org Invitation", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is not pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
await test.step("By email", async () => {
|
||||
const invitedUserEmail = `rick-${Date.now()}@example.com`;
|
||||
const usernameDerivedFromEmail = invitedUserEmail.split("@")[0];
|
||||
await inviteAnEmail(page, invitedUserEmail);
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
assertInviteLink(inviteLink);
|
||||
await signupFromEmailInviteLink(browser, inviteLink);
|
||||
|
||||
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("By invite link", async () => {
|
||||
const inviteLink = await copyInviteLink(page);
|
||||
const email = `rick-${Date.now()}@example.com`;
|
||||
const usernameDerivedFromEmail = email.split("@")[0];
|
||||
await signupFromInviteLink({ browser, inviteLink, email });
|
||||
|
||||
const dbUser = await prisma.user.findUnique({ where: { email } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Team Invitation", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
hasSubteam: true,
|
||||
isOrgVerified: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
const { team } = await orgOwner.getFirstTeamMembership();
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
|
||||
await test.step("By email", async () => {
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
const invitedUserEmail = `rick-${Date.now()}@example.com`;
|
||||
const usernameDerivedFromEmail = invitedUserEmail.split("@")[0];
|
||||
await inviteAnEmail(page, invitedUserEmail);
|
||||
await expectUserToBeAMemberOfTeam({
|
||||
page,
|
||||
teamId: team.id,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${team.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
assertInviteLink(inviteLink);
|
||||
|
||||
await signupFromEmailInviteLink(browser, inviteLink);
|
||||
|
||||
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
|
||||
await expectUserToBeAMemberOfTeam({
|
||||
page,
|
||||
teamId: team.id,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("By invite link", async () => {
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
|
||||
const inviteLink = await copyInviteLink(page);
|
||||
const email = `rick-${Date.now()}@example.com`;
|
||||
// '-domain' because the email doesn't match orgAutoAcceptEmail
|
||||
const usernameDerivedFromEmail = `${email.split("@")[0]}`;
|
||||
|
||||
await signupFromInviteLink({ browser, inviteLink, email });
|
||||
|
||||
const dbUser = await prisma.user.findUnique({ where: { email } });
|
||||
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
|
||||
await expectUserToBeAMemberOfTeam({
|
||||
teamId: team.id,
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: email,
|
||||
});
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: true,
|
||||
email: email,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function signupFromInviteLink({
|
||||
browser,
|
||||
inviteLink,
|
||||
email,
|
||||
}: {
|
||||
browser: Browser;
|
||||
inviteLink: string;
|
||||
email: string;
|
||||
}) {
|
||||
const context = await browser.newContext();
|
||||
const inviteLinkPage = await context.newPage();
|
||||
await inviteLinkPage.goto(inviteLink);
|
||||
await inviteLinkPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
const button = inviteLinkPage.locator("button[type=submit][disabled]");
|
||||
await expect(button).toBeVisible(); // email + 3 password hints
|
||||
|
||||
await inviteLinkPage.locator("input[name=email]").fill(email);
|
||||
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await inviteLinkPage.waitForURL("/getting-started");
|
||||
return { email };
|
||||
}
|
||||
|
||||
async function signupFromEmailInviteLink(browser: Browser, inviteLink: string) {
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const signupPage = await context.newPage();
|
||||
|
||||
signupPage.goto(inviteLink);
|
||||
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toBeDisabled();
|
||||
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toBeDisabled();
|
||||
await signupPage.waitForLoadState("networkidle");
|
||||
// Check required fields
|
||||
await signupPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await signupPage.locator("button[type=submit]").click();
|
||||
await signupPage.waitForURL("/getting-started?from=signup");
|
||||
await context.close();
|
||||
await signupPage.close();
|
||||
}
|
||||
|
||||
async function inviteAnEmail(page: Page, invitedUserEmail: string) {
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
async function expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
username,
|
||||
email,
|
||||
role,
|
||||
isMemberShipAccepted,
|
||||
}: {
|
||||
page: Page;
|
||||
username: string;
|
||||
role: string;
|
||||
isMemberShipAccepted: boolean;
|
||||
email: string;
|
||||
}) {
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.goto("/settings/organizations/members");
|
||||
expect(await page.locator(`[data-testid="member-${username}-username"]`).textContent()).toBe(username);
|
||||
expect(await page.locator(`[data-testid="member-${username}-email"]`).textContent()).toBe(email);
|
||||
expect((await page.locator(`[data-testid="member-${username}-role"]`).textContent())?.toLowerCase()).toBe(
|
||||
role.toLowerCase()
|
||||
);
|
||||
if (isMemberShipAccepted) {
|
||||
await expect(page.locator(`[data-testid2="member-${username}-pending"]`)).toBeHidden();
|
||||
} else {
|
||||
await expect(page.locator(`[data-testid2="member-${username}-pending"]`)).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectUserToBeAMemberOfTeam({
|
||||
page,
|
||||
teamId,
|
||||
email,
|
||||
role,
|
||||
username,
|
||||
isMemberShipAccepted,
|
||||
}: {
|
||||
page: Page;
|
||||
username: string;
|
||||
role: string;
|
||||
teamId: number;
|
||||
isMemberShipAccepted: boolean;
|
||||
email: string;
|
||||
}) {
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
expect(
|
||||
(
|
||||
await page.locator(`[data-testid="member-${username}"] [data-testid=member-role]`).textContent()
|
||||
)?.toLowerCase()
|
||||
).toBe(role.toLowerCase());
|
||||
if (isMemberShipAccepted) {
|
||||
await expect(page.locator(`[data-testid="email-${email.replace("@", "")}-pending"]`)).toBeHidden();
|
||||
} else {
|
||||
await expect(page.locator(`[data-testid="email-${email.replace("@", "")}-pending"]`)).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
function assertInviteLink(inviteLink: string | null | undefined): asserts inviteLink is string {
|
||||
if (!inviteLink) throw new Error("Invite link not found");
|
||||
}
|
||||
|
||||
async function copyInviteLink(page: Page) {
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
|
||||
const inviteLink = await getInviteLink(page);
|
||||
return inviteLink;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { test } from "./lib/fixtures";
|
|||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Settings/admin A/B tests", () => {
|
||||
test("should point to the /future/settings/admin page", async ({ page, users, context }) => {
|
||||
test.skip("should point to the /future/settings/admin page", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { expect } from "@playwright/test";
|
|||
import { randomBytes } from "crypto";
|
||||
|
||||
import { APP_NAME, IS_PREMIUM_USERNAME_ENABLED, IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { getEmailsReceivedByUser, localize } from "./lib/testUtils";
|
||||
|
@ -103,6 +104,8 @@ test.describe("Signup Flow Test", async () => {
|
|||
const userToCreate = users.buildForSignup({
|
||||
username: "rick-jones",
|
||||
password: "Password99!",
|
||||
// Email intentonally kept as different from username
|
||||
email: `rickjones${Math.random()}-${Date.now()}@example.com`,
|
||||
});
|
||||
|
||||
await page.goto("/signup");
|
||||
|
@ -120,6 +123,9 @@ test.describe("Signup Flow Test", async () => {
|
|||
|
||||
// Check that the URL matches the expected URL
|
||||
expect(page.url()).toContain("/auth/verify-email");
|
||||
const dbUser = await prisma.user.findUnique({ where: { email: userToCreate.email } });
|
||||
// Verify that the username is the same as the one provided and isn't accidentally changed to email derived username - That happens only for organization member signup
|
||||
expect(dbUser?.username).toBe(userToCreate.username);
|
||||
});
|
||||
test("Signup fields prefilled with query params", async ({ page, users }) => {
|
||||
const signupUrlWithParams = "/signup?username=rick-jones&email=rick-jones%40example.com";
|
||||
|
@ -236,7 +242,7 @@ test.describe("Signup Flow Test", async () => {
|
|||
|
||||
const t = await localize("en");
|
||||
const teamOwner = await users.create(undefined, { hasTeam: true });
|
||||
const { team } = await teamOwner.getFirstTeam();
|
||||
const { team } = await teamOwner.getFirstTeamMembership();
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
|
|
@ -17,7 +17,7 @@ test.describe("Team", () => {
|
|||
test("Invitation (non verified)", async ({ browser, page, users, emails }) => {
|
||||
const t = await localize("en");
|
||||
const teamOwner = await users.create(undefined, { hasTeam: true });
|
||||
const { team } = await teamOwner.getFirstTeam();
|
||||
const { team } = await teamOwner.getFirstTeamMembership();
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
@ -98,7 +98,7 @@ test.describe("Team", () => {
|
|||
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
|
||||
const t = await localize("en");
|
||||
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
|
||||
const { team } = await teamOwner.getFirstTeam();
|
||||
const { team } = await teamOwner.getFirstTeamMembership();
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
|
|
@ -7,6 +7,7 @@ import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
|||
|
||||
import { test } from "./lib/fixtures";
|
||||
import {
|
||||
NotFoundPageText,
|
||||
bookTimeSlot,
|
||||
fillStripeTestCheckout,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
|
@ -21,7 +22,7 @@ test.describe("Teams - NonOrg", () => {
|
|||
|
||||
test("Team Onboarding Invite Members", async ({ page, users }) => {
|
||||
const user = await users.create(undefined, { hasTeam: true });
|
||||
const { team } = await user.getFirstTeam();
|
||||
const { team } = await user.getFirstTeamMembership();
|
||||
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||
|
||||
await user.apiLogin();
|
||||
|
@ -79,7 +80,7 @@ test.describe("Teams - NonOrg", () => {
|
|||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
}
|
||||
);
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
|
@ -117,7 +118,7 @@ test.describe("Teams - NonOrg", () => {
|
|||
}
|
||||
);
|
||||
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
|
@ -234,7 +235,7 @@ test.describe("Teams - NonOrg", () => {
|
|||
);
|
||||
|
||||
await owner.apiLogin();
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
|
||||
// Mark team as private
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
|
@ -347,12 +348,12 @@ test.describe("Teams - Org", () => {
|
|||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
}
|
||||
);
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
|
||||
await expect(page.locator("text=This page could not be found")).toBeVisible();
|
||||
await expect(page.locator(`text=${NotFoundPageText}`)).toBeVisible();
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
|
@ -396,7 +397,7 @@ test.describe("Teams - Org", () => {
|
|||
}
|
||||
);
|
||||
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
|
@ -418,6 +419,45 @@ test.describe("Teams - Org", () => {
|
|||
expect(teamMatesObj.concat([{ name: owner.name! }]).some(({ name }) => name === chosenUser)).toBe(true);
|
||||
// TODO: Assert whether the user received an email
|
||||
});
|
||||
|
||||
test("Can access booking page with event slug and team page in lowercase/uppercase/mixedcase", async ({
|
||||
page,
|
||||
orgs,
|
||||
users,
|
||||
}) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
}
|
||||
);
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
const teamSlugUpperCase = team.slug?.toUpperCase();
|
||||
const teamEventSlugUpperCase = teamEventSlug.toUpperCase();
|
||||
|
||||
// This is the most closest to the actual user flow as org1.cal.com maps to /org/orgSlug
|
||||
await page.goto(`/org/${org.slug}/${teamSlugUpperCase}/${teamEventSlugUpperCase}`);
|
||||
await page.waitForSelector("[data-testid=day]");
|
||||
});
|
||||
});
|
||||
|
||||
async function doOnOrgDomain(
|
||||
|
|
|
@ -18,7 +18,7 @@ test.afterAll(async ({ users }) => {
|
|||
test.describe("Unpublished", () => {
|
||||
test("Regular team profile", async ({ page, users }) => {
|
||||
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true });
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
||||
await page.goto(`/team/${requestedSlug}`);
|
||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||
|
@ -33,7 +33,7 @@ test.describe("Unpublished", () => {
|
|||
isUnpublished: true,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
});
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
||||
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
await page.goto(`/team/${requestedSlug}/${teamEventSlug}`);
|
||||
|
|
|
@ -1638,6 +1638,7 @@
|
|||
"individual": "Individual",
|
||||
"all_bookings_filter_label": "All Bookings",
|
||||
"all_users_filter_label": "All Users",
|
||||
"all_event_types_filter_label": "All Event Types",
|
||||
"your_bookings_filter_label": "Your Bookings",
|
||||
"meeting_url_variable": "Meeting url",
|
||||
"meeting_url_info": "The event meeting conference url",
|
||||
|
@ -1868,6 +1869,7 @@
|
|||
"review_event_type": "Review Event Type",
|
||||
"looking_for_more_analytics": "Looking for more analytics?",
|
||||
"looking_for_more_insights": "Looking for more Insights?",
|
||||
"filters": "Filters",
|
||||
"add_filter": "Add filter",
|
||||
"remove_filters": "Clear all filters",
|
||||
"select_user": "Select User",
|
||||
|
@ -2128,6 +2130,8 @@
|
|||
"overlay_my_calendar":"Overlay my calendar",
|
||||
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
|
||||
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
|
||||
"join_event_location":"Join {{eventLocationType}}",
|
||||
"join_meeting":"Join Meeting",
|
||||
"troubleshooting":"Troubleshooting",
|
||||
"calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
|
||||
"availabilty_schedules":"Availability schedules",
|
||||
|
|
|
@ -7,90 +7,90 @@
|
|||
:root {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #e5e7eb;
|
||||
--cal-bg: white;
|
||||
--cal-bg-subtle: #f3f4f6;
|
||||
--cal-bg-muted: #f9fafb;
|
||||
--cal-bg-inverted: #111827;
|
||||
--cal-bg-emphasis: hsla(220,13%,91%,1);
|
||||
--cal-bg: hsla(0,0%,100%,1);
|
||||
--cal-bg-subtle: hsla(220, 14%, 96%,1);
|
||||
--cal-bg-muted: hsla(210,20%,98%,1);
|
||||
--cal-bg-inverted: hsla(0,0%,6%,1);
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
--cal-bg-info: hsla(218,83%,98%,1);
|
||||
--cal-bg-success: hsla(134,76%,94%,1);
|
||||
--cal-bg-attention: hsla(37, 86%, 92%, 1);
|
||||
--cal-bg-error: hsla(3,66,93,1);
|
||||
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis: #9ca3af;
|
||||
--cal-border: #d1d5db;
|
||||
--cal-border-subtle: #e5e7eb;
|
||||
--cal-border-emphasis: hsla(218, 11%, 65%, 1);
|
||||
--cal-border: hsla(216, 12%, 84%, 1);
|
||||
--cal-border-subtle: hsla(220, 13%, 91%, 1);
|
||||
--cal-border-booker: #e5e7eb;
|
||||
--cal-border-muted: #f3f4f6;
|
||||
--cal-border-error: #aa2e26;
|
||||
--cal-border-muted: hsla(220, 14%, 96%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #111827;
|
||||
--cal-text: #374151;
|
||||
--cal-text-subtle: #6b7280;
|
||||
--cal-text-muted: #9ca3af;
|
||||
--cal-text-inverted: white;
|
||||
--cal-text-emphasis: hsla(217, 19%, 27%, 1);
|
||||
--cal-text: hsla(217, 19%, 27%, 1);
|
||||
--cal-text-subtle: hsla(220, 9%, 46%, 1);
|
||||
--cal-text-muted: hsla(218, 11%, 65%, 1);
|
||||
--cal-text-inverted: hsla(0, 0%, 100%, 1);
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
--cal-text-info: hsla(228, 56%, 33%, 1);
|
||||
--cal-text-success: hsla(133, 34%, 24%, 1);
|
||||
--cal-text-attention: hsla(16, 62%, 28%, 1);
|
||||
--cal-text-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Brand shinanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: #111827;
|
||||
--cal-brand-emphasis: #101010;
|
||||
--cal-brand-text: white;
|
||||
--cal-brand: hsla(221, 39%, 11%, 1);
|
||||
--cal-brand-emphasis: hsla(0, 0%, 6%, 1);
|
||||
--cal-brand-text: hsla(0, 0%, 100%, 1);
|
||||
}
|
||||
.dark {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #2b2b2b;
|
||||
--cal-bg: #101010;
|
||||
--cal-bg-subtle: #2b2b2b;
|
||||
--cal-bg-muted: #1c1c1c;
|
||||
--cal-bg-inverted: #f3f4f6;
|
||||
--cal-bg-emphasis: hsla(0, 0%, 32%, 1);
|
||||
--cal-bg: hsla(0, 0%, 10%, 1);
|
||||
--cal-bg-subtle: hsla(0, 0%, 18%, 1);
|
||||
--cal-bg-muted: hsla(0, 0%, 12%, 1);
|
||||
--cal-bg-inverted: hsla(220, 14%, 96%, 1);
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #263fa9;
|
||||
--cal-bg-success: #306339;
|
||||
--cal-bg-attention: #8e3b1f;
|
||||
--cal-bg-error: #8c2822;
|
||||
--cal-bg-dark-error: #752522;
|
||||
--cal-bg-info: hsla(228, 56%, 33%, 1);
|
||||
--cal-bg-success: hsla(133, 34%, 24%, 1);
|
||||
--cal-bg-attention: hsla(16, 62%, 28%, 1);
|
||||
--cal-bg-error: hsla(2, 55%, 30%, 1);
|
||||
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis: #575757;
|
||||
--cal-border: #444444;
|
||||
--cal-border-subtle: #2b2b2b;
|
||||
--cal-border-booker: #2b2b2b;
|
||||
--cal-border-muted: #1c1c1c;
|
||||
--cal-border-error: #aa2e26;
|
||||
--cal-border-emphasis: hsla(0, 0%, 46%, 1);
|
||||
--cal-border: hsla(0, 0%, 34%, 1);
|
||||
--cal-border-subtle: hsla(0, 0%, 22%, 1);
|
||||
--cal-border-booker: hsla(0, 0%, 22%, 1);
|
||||
--cal-border-muted: hsla(0, 0%, 18%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #f3f4f6;
|
||||
--cal-text: #d6d6d6;
|
||||
--cal-text-subtle: #a5a5a5;
|
||||
--cal-text-muted: #575757;
|
||||
--cal-text-inverted: #101010;
|
||||
--cal-text-emphasis: hsla(240, 20%, 99%, 1);
|
||||
--cal-text: hsla(0, 0%, 84%, 1);
|
||||
--cal-text-subtle: hsla(0, 0%, 65%, 1);
|
||||
--cal-text-muted: hsla(0, 0%, 34%, 1);
|
||||
--cal-text-inverted: hsla(0, 0%, 10%, 1);
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #dee9fc;
|
||||
--cal-text-success: #e2fbe8;
|
||||
--cal-text-attention: #fceed8;
|
||||
--cal-text-error: #f9e3e2;
|
||||
--cal-text-info: hsla(218, 83%, 93%, 1);
|
||||
--cal-text-success: hsla(134, 76%, 94%, 1);
|
||||
--cal-text-attention: hsla(37, 86%, 92%, 1);
|
||||
--cal-text-error: hsla(3, 66%, 93%, 1);
|
||||
|
||||
/* Brand shenanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: white;
|
||||
--cal-brand-emphasis: #e1e1e1;
|
||||
--cal-brand-text: black;
|
||||
--cal-brand: hsla(0, 0%, 100%, 1);
|
||||
--cal-brand-emphasis: hsla(218, 11%, 65%, 1);
|
||||
--cal-brand-text: hsla(0, 0%, 0%,1);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
|
@ -68,6 +68,7 @@ type ExpectedEmail = {
|
|||
filename: string;
|
||||
iCalUID?: string;
|
||||
recurrence?: Recurrence;
|
||||
method: string;
|
||||
};
|
||||
/**
|
||||
* Checks that there is no
|
||||
|
@ -162,9 +163,14 @@ expect.extend({
|
|||
}
|
||||
|
||||
if (!expectedEmail.noIcs && !isIcsUIDExpected) {
|
||||
const icsObjectKeys = icsObject ? Object.keys(icsObject) : [];
|
||||
const icsKey = icsObjectKeys.find((key) => key !== "vcalendar");
|
||||
if (!icsKey) throw new Error("icsKey not found");
|
||||
return {
|
||||
pass: false,
|
||||
actual: JSON.stringify(icsObject),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
actual: icsObject[icsKey].uid!,
|
||||
expected: expectedEmail.ics?.iCalUID,
|
||||
message: () => `Expected ICS UID ${isNot ? "is" : "isn't"} present in actual`,
|
||||
};
|
||||
|
@ -375,6 +381,7 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
filename: "event.ics",
|
||||
iCalUID: `${iCalUID}`,
|
||||
recurrence,
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${organizer.email}`
|
||||
|
@ -397,8 +404,9 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
to: `${booker.name} <${booker.email}>`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: iCalUID,
|
||||
iCalUID: `${iCalUID}`,
|
||||
recurrence,
|
||||
method: "REQUEST",
|
||||
},
|
||||
links: recurrence
|
||||
? [
|
||||
|
@ -436,7 +444,8 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
to: `${otherTeamMember.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: iCalUID,
|
||||
iCalUID: `${iCalUID}`,
|
||||
method: "REQUEST",
|
||||
},
|
||||
links: [
|
||||
{
|
||||
|
@ -472,7 +481,8 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
to: `${guest.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: iCalUID,
|
||||
iCalUID: `${iCalUID}`,
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${guest.name} <${guest.email}`
|
||||
|
@ -529,6 +539,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID,
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${organizer.email}`
|
||||
|
@ -541,6 +552,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID,
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${booker.name} <${booker.email}>`
|
||||
|
@ -612,6 +624,7 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID,
|
||||
method: "REQUEST",
|
||||
},
|
||||
appsStatus,
|
||||
},
|
||||
|
@ -625,6 +638,7 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID,
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${booker.name} <${booker.email}>`
|
||||
|
@ -711,6 +725,7 @@ export function expectBookingRequestRescheduledEmails({
|
|||
to: `${booker.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${booker.email}`
|
||||
|
@ -724,6 +739,7 @@ export function expectBookingRequestRescheduledEmails({
|
|||
to: `${loggedInUser.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${loggedInUser.email}`
|
||||
|
@ -1074,3 +1090,11 @@ export async function expectBookingInDBToBeRescheduledFromTo({ from, to }: { fro
|
|||
...to,
|
||||
});
|
||||
}
|
||||
|
||||
export function expectICalUIDAsString(iCalUID: string | undefined | null) {
|
||||
if (typeof iCalUID !== "string") {
|
||||
throw new Error("iCalUID is not a string");
|
||||
}
|
||||
|
||||
return iCalUID;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import plausible_config_json from "./plausible/config.json";
|
|||
import qr_code_config_json from "./qr_code/config.json";
|
||||
import raycast_config_json from "./raycast/config.json";
|
||||
import riverside_config_json from "./riverside/config.json";
|
||||
import roam_config_json from "./roam/config.json";
|
||||
import routing_forms_config_json from "./routing-forms/config.json";
|
||||
import salesforce_config_json from "./salesforce/config.json";
|
||||
import sendgrid_config_json from "./sendgrid/config.json";
|
||||
|
@ -123,6 +124,7 @@ export const appStoreMetadata = {
|
|||
qr_code: qr_code_config_json,
|
||||
raycast: raycast_config_json,
|
||||
riverside: riverside_config_json,
|
||||
roam: roam_config_json,
|
||||
"routing-forms": routing_forms_config_json,
|
||||
salesforce: salesforce_config_json,
|
||||
sendgrid: sendgrid_config_json,
|
||||
|
|
|
@ -46,6 +46,7 @@ export const apiHandlers = {
|
|||
qr_code: import("./qr_code/api"),
|
||||
raycast: import("./raycast/api"),
|
||||
riverside: import("./riverside/api"),
|
||||
roam: import("./roam/api"),
|
||||
"routing-forms": import("./routing-forms/api"),
|
||||
salesforce: import("./salesforce/api"),
|
||||
sendgrid: import("./sendgrid/api"),
|
||||
|
|
|
@ -22,6 +22,7 @@ import office365video_config_json from "./office365video/config.json";
|
|||
import ping_config_json from "./ping/config.json";
|
||||
import plausible_config_json from "./plausible/config.json";
|
||||
import riverside_config_json from "./riverside/config.json";
|
||||
import roam_config_json from "./roam/config.json";
|
||||
import shimmervideo_config_json from "./shimmervideo/config.json";
|
||||
import signal_config_json from "./signal/config.json";
|
||||
import sirius_video_config_json from "./sirius_video/config.json";
|
||||
|
@ -56,6 +57,7 @@ export const appStoreMetadata = {
|
|||
ping: ping_config_json,
|
||||
plausible: plausible_config_json,
|
||||
riverside: riverside_config_json,
|
||||
roam: roam_config_json,
|
||||
shimmervideo: shimmervideo_config_json,
|
||||
signal: signal_config_json,
|
||||
sirius_video: sirius_video_config_json,
|
||||
|
|
|
@ -202,6 +202,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||
useDefault: true,
|
||||
},
|
||||
guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true,
|
||||
iCalUID: calEventRaw.iCalUID,
|
||||
};
|
||||
|
||||
if (calEventRaw.location) {
|
||||
|
@ -250,7 +251,6 @@ export default class GoogleCalendarService implements Calendar {
|
|||
type: "google_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
iCalUID: event.data.iCalUID,
|
||||
};
|
||||
} catch (error) {
|
||||
this.log.error(
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"slug": "paypal",
|
||||
"type": "paypal_payment",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://example.com/link",
|
||||
"url": "https://paypal.com",
|
||||
"variant": "payment",
|
||||
"categories": ["payment"],
|
||||
"publisher": "Cal.com",
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
items:
|
||||
- 1.jpg
|
||||
- 2.jpg
|
||||
- 3.jpg
|
||||
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
|
||||
Roam makes companies:
|
||||
- more productive with shorter meetings
|
||||
- more connected with a map that gives a feeling of working together without meeting
|
||||
- & Roam saves companies money with our all-in-one bundle
|
||||
|
||||
When the whole company is in one HQ, productivity is high, people feel connected to the company. Calendars are emptied. Culture comes back. And companies using Roam benefit from our all-in-one approach to cut tools and save money.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user