Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95bf90402c | ||
|
|
f523ce7795 | ||
|
|
65b70d682d | ||
|
|
2d518f49dd | ||
|
|
7a9d291d1f | ||
|
|
1a1a506324 | ||
|
|
0f7ecb3345 | ||
|
|
4f1d71e660 | ||
|
|
30eb7ce846 | ||
|
|
3eb5490df9 | ||
|
|
235382213f | ||
|
|
01baf7d3c0 | ||
|
|
1a649158e1 | ||
|
|
d6306dcd22 | ||
|
|
7131a1f015 | ||
|
|
5357228f72 | ||
|
|
bfeafaa21f | ||
|
|
02ef6dc934 | ||
|
|
4424b2aea3 | ||
|
|
1f91fe888a | ||
|
|
b4a51d3560 | ||
|
|
4855b6b40f | ||
|
|
4b264a867b | ||
|
|
9caaa0af32 | ||
|
|
f411c6ed11 | ||
|
|
245196fbf3 | ||
|
|
6be69b820c | ||
|
|
b63c9791ec | ||
|
|
8cc61a1fe9 | ||
|
|
be2e1274ff | ||
|
|
fa3b1c17ea | ||
|
|
015edd3c9b | ||
|
|
de05e8e989 | ||
|
|
78a6963cd9 | ||
|
|
ea827ebbc6 | ||
|
|
08e58851e7 | ||
|
|
da1b8137c2 | ||
|
|
d182e5c99a | ||
|
|
42259a113b | ||
|
+8 |
6561b92f40 | ||
|
|
9e5665f2fb | ||
|
|
07778d22ed | ||
|
|
56df8bdf7c | ||
|
|
a518c7152a | ||
|
|
c37a8a587b | ||
|
|
b7f7c33afc | ||
|
|
1d449ecc6b | ||
|
|
f582faed2b | ||
|
|
79a8ab79f8 | ||
|
|
ce87de4fc7 | ||
|
|
b128579729 | ||
|
|
3cc5d9c065 | ||
|
|
7897e14273 | ||
|
|
c581456586 | ||
|
|
36dd3e9e6c | ||
|
|
f137aee436 | ||
|
|
4f494d5a32 | ||
|
|
cce6e83578 | ||
|
|
c21ba636d2 | ||
|
|
5ccf5e6be8 | ||
|
|
66b3e735d8 | ||
|
|
7a8b9ededb | ||
|
|
501c49f369 | ||
|
|
536604455b | ||
|
|
cfb76a9689 | ||
|
|
30180edc0b | ||
|
|
2aa3d1d4e0 | ||
|
|
21694c039a | ||
|
|
a8c29b5459 | ||
|
|
6cd7454f43 | ||
|
|
99b2419dd7 | ||
|
|
f6d0a2ca09 | ||
|
|
f5ab9bd40b | ||
|
|
07085b9ffb | ||
|
|
14151b827f | ||
|
|
28b757f19c | ||
|
|
68455db860 | ||
|
|
96ff88d266 | ||
|
|
f88a91dad0 | ||
|
|
35059d7727 | ||
|
|
68f76e2acc | ||
|
|
80aac06ff3 | ||
|
|
0f47528ba2 | ||
|
|
113dc5fca6 | ||
|
|
0befdde5d7 | ||
|
|
b3ed6e39e3 | ||
|
|
e07b1eda32 | ||
|
|
8262894bde | ||
|
|
a3d7bcd22e | ||
|
|
62b753afba | ||
|
|
bb40b2f6bf | ||
|
|
f628dc83a8 | ||
|
|
2fd2578504 | ||
|
|
a29407349d | ||
|
|
e7df1ef493 | ||
|
|
b652ae7714 | ||
|
|
cec1c9ee46 |
+1
-2
@@ -242,6 +242,7 @@ E2E_TEST_MAILHOG_ENABLED=
|
||||
|
||||
# Cloudflare Turnstile
|
||||
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
|
||||
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
|
||||
CLOUDFLARE_TURNSTILE_SECRET=
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
@@ -367,9 +368,7 @@ APP_ROUTER_TEAM_ENABLED=0
|
||||
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGIN_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
|
||||
APP_ROUTER_AUTH_NEW_ENABLED=0
|
||||
APP_ROUTER_AUTH_SAML_ENABLED=0
|
||||
APP_ROUTER_AUTH_ERROR_ENABLED=0
|
||||
APP_ROUTER_AUTH_PLATFORM_ENABLED=0
|
||||
APP_ROUTER_AUTH_OAUTH2_ENABLED=0
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# .github/workflows/chromatic.yml
|
||||
|
||||
# Workflow name
|
||||
name: "Chromatic"
|
||||
|
||||
# Event for the workflow
|
||||
on:
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- apps/storybook/**
|
||||
- packages/ui/**
|
||||
workflow_dispatch:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# List of jobs
|
||||
jobs:
|
||||
chromatic-deployment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
# 👇 Adds Chromatic as a step in the workflow
|
||||
- name: Publish to Chromatic
|
||||
uses: chromaui/action@v1
|
||||
# Options required to the GitHub Chromatic Action
|
||||
with:
|
||||
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
workingDir: "apps/storybook"
|
||||
buildScriptName: "build"
|
||||
@@ -12,16 +12,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Run Linting with Reports
|
||||
run: yarn lint:report
|
||||
- name: Run Lint
|
||||
run: yarn lint
|
||||
continue-on-error: true
|
||||
|
||||
- name: Merge lint reports
|
||||
run: jq -s '[.[]]|flatten' lint-results/*.json &> lint-results/eslint_report.json
|
||||
|
||||
- name: Upload ESLint report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lint-results
|
||||
path: lint-results
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
diff --git a/index.cjs b/index.cjs
|
||||
index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644
|
||||
index c83f700..da6fc7e 100644
|
||||
--- a/index.cjs
|
||||
+++ b/index.cjs
|
||||
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
|
||||
@@ -12,4 +12,4 @@ index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67
|
||||
+// exports['default'] = min.parsePhoneNumberFromString
|
||||
|
||||
// `parsePhoneNumberFromString()` named export is now considered legacy:
|
||||
// it has been promoted to a default export due to being too verbose.
|
||||
// it has been promoted to a default export due to being too verbose.
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
@@ -1,9 +1,12 @@
|
||||
import type { RatelimitResponse } from "@unkey/ratelimit";
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiResponse, NextApiRequest } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { handleAutoLock } from "@calcom/lib/autoLock";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
|
||||
|
||||
@@ -14,6 +17,10 @@ vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
|
||||
checkRateLimitAndThrowError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/autoLock", () => ({
|
||||
handleAutoLock: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("rateLimitApiKey middleware", () => {
|
||||
it("should return 401 if no apiKey is provided", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
@@ -55,17 +62,20 @@ describe("rateLimitApiKey middleware", () => {
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
const rateLimiterResponse = {
|
||||
const rateLimiterResponse: RatelimitResponse = {
|
||||
limit: 100,
|
||||
remaining: 99,
|
||||
reset: Date.now(),
|
||||
success: true,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
});
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
@@ -89,4 +99,135 @@ describe("rateLimitApiKey middleware", () => {
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
|
||||
});
|
||||
|
||||
it("should lock API key when rate limit is repeatedly exceeded", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
const rateLimiterResponse: RatelimitResponse = {
|
||||
success: false,
|
||||
remaining: 0,
|
||||
limit: 100,
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
}
|
||||
);
|
||||
|
||||
// Mock handleAutoLock to indicate the key was locked
|
||||
vi.mocked(handleAutoLock).mockResolvedValueOnce(true);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(handleAutoLock).toHaveBeenCalledWith({
|
||||
identifier: "test-key",
|
||||
identifierType: "apiKey",
|
||||
rateLimitResponse: rateLimiterResponse,
|
||||
});
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
expect(res._getJSONData()).toEqual({ message: "Too many requests" });
|
||||
});
|
||||
|
||||
it("should handle API key not found error during auto-lock", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
const rateLimiterResponse: RatelimitResponse = {
|
||||
success: false,
|
||||
remaining: 0,
|
||||
limit: 100,
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
}
|
||||
);
|
||||
|
||||
// Mock handleAutoLock to throw a "No user found" error
|
||||
vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key."));
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(handleAutoLock).toHaveBeenCalledWith({
|
||||
identifier: "test-key",
|
||||
identifierType: "apiKey",
|
||||
rateLimitResponse: rateLimiterResponse,
|
||||
});
|
||||
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
expect(res._getJSONData()).toEqual({ message: "No user found for this API key." });
|
||||
});
|
||||
|
||||
it("should continue if auto-lock returns false (not locked)", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
const rateLimiterResponse: RatelimitResponse = {
|
||||
success: false,
|
||||
remaining: 0,
|
||||
limit: 100,
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
}
|
||||
);
|
||||
|
||||
// Mock handleAutoLock to indicate the key was not locked
|
||||
vi.mocked(handleAutoLock).mockResolvedValueOnce(false);
|
||||
|
||||
const next = vi.fn();
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, next);
|
||||
|
||||
expect(handleAutoLock).toHaveBeenCalledWith({
|
||||
identifier: "test-key",
|
||||
identifierType: "apiKey",
|
||||
rateLimitResponse: rateLimiterResponse,
|
||||
});
|
||||
|
||||
// Verify headers were set but request continued
|
||||
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle HttpError during rate limiting", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
// Mock checkRateLimitAndThrowError to throw HttpError
|
||||
vi.mocked(checkRateLimitAndThrowError).mockRejectedValueOnce(
|
||||
new HttpError({
|
||||
statusCode: 429,
|
||||
message: "Custom rate limit error",
|
||||
})
|
||||
);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
expect(res._getJSONData()).toEqual({ message: "Custom rate limit error" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { handleAutoLock } from "@calcom/lib/autoLock";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
@@ -10,14 +12,34 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
await checkRateLimitAndThrowError({
|
||||
identifier: req.query.apiKey as string,
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: (response) => {
|
||||
onRateLimiterResponse: async (response) => {
|
||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||
|
||||
try {
|
||||
const didLock = await handleAutoLock({
|
||||
identifier: req.query.apiKey as string, // Casting as this is verified in another middleware
|
||||
identifierType: "apiKey",
|
||||
rateLimitResponse: response,
|
||||
});
|
||||
|
||||
if (didLock) {
|
||||
return res.status(429).json({ message: "Too many requests" });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "No user found for this API key.") {
|
||||
return res.status(401).json({ message: error.message });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(429).json({ message: "Rate limit exceeded" });
|
||||
if (error instanceof HttpError) {
|
||||
return res.status(error.statusCode).json({ message: error.message });
|
||||
}
|
||||
return res.status(429).json({ message: "Rate limit exceeded" });
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string;
|
||||
type Literal = boolean | number | string | null;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
const jsonObjectSchema = z.record(z.lazy(() => jsonSchema));
|
||||
const jsonArraySchema = z.array(z.lazy(() => jsonSchema));
|
||||
export const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
z.union([literalSchema, jsonObjectSchema, jsonArraySchema])
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { getDubCustomer } from "@calcom/features/auth/lib/dub";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -192,11 +193,26 @@ const generateTeamCheckoutSession = async ({
|
||||
pendingPaymentTeamId: number;
|
||||
ownerId: number;
|
||||
}) => {
|
||||
const customer = await getStripeCustomerIdFromUserId(ownerId);
|
||||
const [customer, dubCustomer] = await Promise.all([
|
||||
getStripeCustomerIdFromUserId(ownerId),
|
||||
getDubCustomer(ownerId.toString()),
|
||||
]);
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer,
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
...(dubCustomer?.discount?.couponId
|
||||
? {
|
||||
discounts: [
|
||||
{
|
||||
coupon:
|
||||
process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId
|
||||
? dubCustomer.discount.couponTestId
|
||||
: dubCustomer.discount.couponId,
|
||||
},
|
||||
],
|
||||
}
|
||||
: { allow_promotion_codes: true }),
|
||||
success_url: `${WEBAPP_URL}/api/teams/api/create?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${WEBAPP_URL}/settings/my-account/profile`,
|
||||
line_items: [
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.81",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.86",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -332,6 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
guests: ["bob@gmail.com"],
|
||||
};
|
||||
|
||||
const beforeCreate = new Date();
|
||||
@@ -376,7 +377,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
...body.bookingFieldsResponses,
|
||||
guests: body.guests,
|
||||
});
|
||||
expect(data.guests).toEqual(body.guests);
|
||||
|
||||
// Check createdAt date is between the time of the request and after the request
|
||||
const createdAtDate = new Date(data.createdAt);
|
||||
|
||||
@@ -146,6 +146,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
);
|
||||
const endTime = startTime.plus({ minutes: lengthInMinutes });
|
||||
|
||||
const guests = inputBooking.guests;
|
||||
|
||||
return {
|
||||
start: startTime.toISO(),
|
||||
end: endTime.toISO(),
|
||||
@@ -154,7 +156,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
language: inputBooking.attendee.language || "en",
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: inputBooking.guests,
|
||||
guests,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: inputBooking.bookingFieldsResponses
|
||||
? {
|
||||
@@ -162,11 +164,13 @@ export class InputBookingsService_2024_08_13 {
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
}
|
||||
: {
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -251,6 +255,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
inputBooking.attendee.timeZone
|
||||
);
|
||||
|
||||
const guests = inputBooking.guests;
|
||||
|
||||
for (let i = 0; i < repeatsTimes; i++) {
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
@@ -263,15 +269,16 @@ export class InputBookingsService_2024_08_13 {
|
||||
language: inputBooking.attendee.language || "en",
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: inputBooking.guests,
|
||||
guests,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: inputBooking.bookingFieldsResponses
|
||||
? {
|
||||
...inputBooking.bookingFieldsResponses,
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email,
|
||||
guests,
|
||||
}
|
||||
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email },
|
||||
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email, guests },
|
||||
schedulingType: eventType.schedulingType,
|
||||
});
|
||||
|
||||
|
||||
+22
-20
@@ -123,7 +123,6 @@ describe("Event types Endpoints", () => {
|
||||
slug: "location",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldTitle = {
|
||||
@@ -452,9 +451,6 @@ describe("Event types Endpoints", () => {
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
@@ -466,7 +462,10 @@ describe("Event types Endpoints", () => {
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldNotes },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
];
|
||||
|
||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -942,9 +941,6 @@ describe("Event types Endpoints", () => {
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
@@ -956,7 +952,10 @@ describe("Event types Endpoints", () => {
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldNotes, ...notesBookingField },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
];
|
||||
|
||||
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -1163,10 +1162,7 @@ describe("Event types Endpoints", () => {
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
label: "",
|
||||
placeholder: "",
|
||||
},
|
||||
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
|
||||
{
|
||||
@@ -1185,7 +1181,14 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
|
||||
{
|
||||
disableOnPrefill: false,
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
@@ -1610,7 +1613,6 @@ describe("Event types Endpoints", () => {
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
@@ -1621,6 +1623,14 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
@@ -1637,14 +1647,6 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-13
@@ -243,19 +243,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
}
|
||||
}
|
||||
|
||||
const fields = [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
|
||||
|
||||
// ensure additional notes are always at the end
|
||||
return fields.sort((a, b) => {
|
||||
if (!a?.slug || !b?.slug) return 0;
|
||||
if (a.slug === "notes" && b.slug !== "notes") {
|
||||
return 1;
|
||||
}
|
||||
if (b.slug === "notes" && a.slug !== "notes") {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
|
||||
}
|
||||
|
||||
getDefaultBookingFields(isOrgTeamEvent: boolean) {
|
||||
|
||||
+6
@@ -220,6 +220,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
await userConnectedToOAuth(responseBody.data.user.email);
|
||||
await userHasDefaultEventTypes(responseBody.data.user.id);
|
||||
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
|
||||
await userHasOnlyOneSchedule(responseBody.data.user.id);
|
||||
});
|
||||
|
||||
async function userConnectedToOAuth(userEmail: string) {
|
||||
@@ -260,6 +261,11 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(schedule?.userId).toEqual(userId);
|
||||
}
|
||||
|
||||
async function userHasOnlyOneSchedule(userId: number) {
|
||||
const schedules = await schedulesRepositoryFixture.getByUserId(userId);
|
||||
expect(schedules?.length).toEqual(1);
|
||||
}
|
||||
|
||||
it(`should fail /POST using already used managed user email`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: userEmail,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator";
|
||||
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
export enum OutOfOfficeReason {
|
||||
UNSPECIFIED = "unspecified",
|
||||
VACATION = "vacation",
|
||||
@@ -80,3 +82,47 @@ export class CreateOutOfOfficeEntryDto {
|
||||
}
|
||||
|
||||
export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {}
|
||||
|
||||
export enum SortOrder {
|
||||
asc = "asc",
|
||||
desc = "desc",
|
||||
}
|
||||
type SortOrderType = keyof typeof SortOrder;
|
||||
|
||||
export class GetOutOfOfficeEntryFiltersDTO extends SkipTakePagination {
|
||||
@IsOptional()
|
||||
@IsEnum(SortOrder, {
|
||||
message: 'SortStart must be either "asc" or "desc".',
|
||||
})
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: "Sort results by their start time in ascending or descending order.",
|
||||
example: "?sortStart=asc OR ?sortStart=desc",
|
||||
enum: SortOrder,
|
||||
})
|
||||
sortStart?: SortOrderType;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(SortOrder, {
|
||||
message: 'SortEnd must be either "asc" or "desc".',
|
||||
})
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: "Sort results by their end time in ascending or descending order.",
|
||||
example: "?sortEnd=asc OR ?sortEnd=desc",
|
||||
enum: SortOrder,
|
||||
})
|
||||
sortEnd?: SortOrderType;
|
||||
}
|
||||
|
||||
export class GetOrgUsersOutOfOfficeEntryFiltersDTO extends GetOutOfOfficeEntryFiltersDTO {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: "Filter ooo entries by the user email address. user must be within your organization.",
|
||||
example: "example@domain.com",
|
||||
})
|
||||
email?: string;
|
||||
}
|
||||
|
||||
@@ -46,12 +46,19 @@ export class UserOOORepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(userId: number, skip: number, take: number) {
|
||||
async getUserOOOPaginated(
|
||||
userId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
|
||||
) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findMany({
|
||||
where: { userId },
|
||||
skip,
|
||||
take,
|
||||
include: { reason: true },
|
||||
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
|
||||
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
CreateOutOfOfficeEntryDto,
|
||||
UpdateOutOfOfficeEntryDto,
|
||||
OutOfOfficeReason,
|
||||
GetOutOfOfficeEntryFiltersDTO,
|
||||
SortOrder,
|
||||
} from "@/modules/ooo/inputs/ooo.input";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
@@ -128,8 +130,13 @@ export class UserOOOService {
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(userId: number, skip: number, take: number) {
|
||||
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take);
|
||||
async getUserOOOPaginated(
|
||||
userId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
|
||||
) {
|
||||
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take, sort);
|
||||
return ooos.map((ooo) => this.formatOooReason(ooo));
|
||||
}
|
||||
}
|
||||
|
||||
+27
-19
@@ -590,6 +590,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -623,7 +624,6 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
@@ -632,6 +632,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: true,
|
||||
label: "Phone number",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
@@ -641,6 +643,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
@@ -657,14 +667,6 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -771,13 +773,19 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
|
||||
{
|
||||
disableOnPrefill: false,
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
@@ -788,6 +796,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
@@ -804,14 +820,6 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+38
-10
@@ -8,13 +8,19 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
import { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo";
|
||||
import { CreateOutOfOfficeEntryDto, UpdateOutOfOfficeEntryDto } from "@/modules/ooo/inputs/ooo.input";
|
||||
import {
|
||||
CreateOutOfOfficeEntryDto,
|
||||
UpdateOutOfOfficeEntryDto,
|
||||
GetOutOfOfficeEntryFiltersDTO,
|
||||
GetOrgUsersOutOfOfficeEntryFiltersDTO,
|
||||
} from "@/modules/ooo/inputs/ooo.input";
|
||||
import {
|
||||
UserOooOutputDto,
|
||||
UserOooOutputResponseDto,
|
||||
UserOoosOutputResponseDto,
|
||||
} from "@/modules/ooo/outputs/ooo.output";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
@@ -33,10 +39,9 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/users/:userId/ooo",
|
||||
path: "/v2/organizations/:orgId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@@ -44,18 +49,22 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
@UseGuards(IsOrgGuard)
|
||||
@DocsTags("Orgs / Users / OOO")
|
||||
export class OrganizationsUsersOOOController {
|
||||
constructor(private readonly userOOOService: UserOOOService) {}
|
||||
constructor(
|
||||
private readonly userOOOService: UserOOOService,
|
||||
private readonly orgUsersOOOService: OrgUsersOOOService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Get("/users/:userId/ooo")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@ApiOperation({ summary: "Get all ooo entries of a user" })
|
||||
async getOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Query() query: SkipTakePagination
|
||||
@Query() query: GetOutOfOfficeEntryFiltersDTO
|
||||
): Promise<UserOoosOutputResponseDto> {
|
||||
const ooos = await this.userOOOService.getUserOOOPaginated(userId, query.skip ?? 0, query.take ?? 250);
|
||||
const { skip, take, ...rest } = query ?? { skip: 0, take: 250 };
|
||||
const ooos = await this.userOOOService.getUserOOOPaginated(userId, skip ?? 0, take ?? 250, rest);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -63,7 +72,7 @@ export class OrganizationsUsersOOOController {
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Post("/users/:userId/ooo")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@@ -79,7 +88,7 @@ export class OrganizationsUsersOOOController {
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:oooId")
|
||||
@Patch("/users/:userId/ooo/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@@ -97,7 +106,7 @@ export class OrganizationsUsersOOOController {
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:oooId")
|
||||
@Delete("/users/:userId/ooo/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@@ -111,4 +120,23 @@ export class OrganizationsUsersOOOController {
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/ooo")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@ApiOperation({ summary: "Get all OOO entries of org users" })
|
||||
async getOrganizationUsersOOO(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Query() query: GetOrgUsersOutOfOfficeEntryFiltersDTO
|
||||
): Promise<UserOoosOutputResponseDto> {
|
||||
const { skip, take, email, ...rest } = query ?? { skip: 0, take: 250 };
|
||||
const ooos = await this.orgUsersOOOService.getOrgUsersOOOPaginated(orgId, skip ?? 0, take ?? 250, rest, {
|
||||
email,
|
||||
});
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -366,6 +366,30 @@ describe("Organizations User OOO Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 2 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/ooo?sortEnd=desc&email=${teammate1Email}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(2);
|
||||
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
|
||||
expect(oooUno).toBeDefined();
|
||||
if (oooUno) {
|
||||
expect(oooUno.reason).toEqual("vacation");
|
||||
expect(oooUno.toUserId).toEqual(teammate2.id);
|
||||
expect(oooUno.userId).toEqual(teammate1.id);
|
||||
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
}
|
||||
// test sort
|
||||
expect(data[1].id).toEqual(oooCreatedViaApiId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 2 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OrgUsersOOORepository {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
async getOrgUsersOOOPaginated(
|
||||
orgId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
|
||||
filters?: { email?: string }
|
||||
) {
|
||||
console.log({ sort, filters });
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findMany({
|
||||
where: {
|
||||
user: {
|
||||
...(filters?.email && { email: filters.email }),
|
||||
profiles: {
|
||||
some: {
|
||||
organizationId: orgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
include: { reason: true },
|
||||
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
|
||||
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
|
||||
});
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OrgUsersOOOService {
|
||||
constructor(
|
||||
private readonly oooRepository: UserOOORepository,
|
||||
private readonly oooUserService: UserOOOService,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly orgUsersOOORepository: OrgUsersOOORepository
|
||||
) {}
|
||||
|
||||
async getOrgUsersOOOPaginated(
|
||||
orgId: number,
|
||||
skip: number,
|
||||
take: number,
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
|
||||
filters?: { email?: string }
|
||||
) {
|
||||
const ooos = await this.orgUsersOOORepository.getOrgUsersOOOPaginated(orgId, skip, take, sort, filters);
|
||||
return ooos.map((ooo) => this.oooUserService.formatOooReason(ooo));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import { OrganizationsTeamsMembershipsController } from "@/modules/organizations
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller";
|
||||
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
|
||||
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
@@ -91,6 +93,8 @@ import { Module } from "@nestjs/common";
|
||||
OutputTeamEventTypesResponsePipe,
|
||||
UserOOOService,
|
||||
UserOOORepository,
|
||||
OrgUsersOOOService,
|
||||
OrgUsersOOORepository,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsOptional, Validate } from "class-validator";
|
||||
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
import { IsNumber, IsOptional, Max, Min, Validate } from "class-validator";
|
||||
|
||||
import { IsEmailStringOrArray } from "../validators/isEmailStringOrArray";
|
||||
|
||||
export class GetUsersInput extends SkipTakePagination {
|
||||
export class GetUsersInput {
|
||||
@ApiProperty({ required: false, description: "The number of items to return", example: 10 })
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
@IsOptional()
|
||||
take?: number;
|
||||
|
||||
@ApiProperty({ required: false, description: "The number of items to skip", example: 0 })
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
skip?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Validate(IsEmailStringOrArray)
|
||||
@Transform(({ value }: { value: string | string[] }) => {
|
||||
|
||||
@@ -2516,6 +2516,8 @@
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -2526,6 +2528,7 @@
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"minimum": 0,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -2716,6 +2719,34 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortStart",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their start time in ascending or descending order.",
|
||||
"example": "?sortStart=asc OR ?sortStart=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortEnd",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their end time in ascending or descending order.",
|
||||
"example": "?sortEnd=asc OR ?sortEnd=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -2824,6 +2855,88 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/ooo": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsUsersOOOController_getOrganizationUsersOOO",
|
||||
"summary": "Get all OOO entries of org users",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortStart",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their start time in ascending or descending order.",
|
||||
"example": "?sortStart=asc OR ?sortStart=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortEnd",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their end time in ascending or descending order.",
|
||||
"example": "?sortEnd=asc OR ?sortEnd=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter ooo entries by the user email address. user must be within your organization.",
|
||||
"example": "example@domain.com",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/webhooks": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks",
|
||||
@@ -7888,7 +8001,7 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "name",
|
||||
"description": "only allowed value for type is `name`"
|
||||
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
@@ -8682,7 +8795,7 @@
|
||||
"boolean"
|
||||
],
|
||||
"example": "name",
|
||||
"description": "only allowed value for type is `name`",
|
||||
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name.",
|
||||
"default": "name"
|
||||
},
|
||||
"label": {
|
||||
|
||||
@@ -29,4 +29,8 @@ export class SchedulesRepositoryFixture {
|
||||
async deleteAvailabilities(scheduleId: Schedule["id"]) {
|
||||
return this.prismaWriteClient.availability.deleteMany({ where: { scheduleId } });
|
||||
}
|
||||
|
||||
async getByUserId(userId: Schedule["userId"]) {
|
||||
return this.prismaReadClient.schedule.findMany({ where: { userId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ const ROUTES: [URLPattern, boolean][] = [
|
||||
["/auth/forgot-password/:path*", process.env.APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED === "1"] as const,
|
||||
["/auth/login", process.env.APP_ROUTER_AUTH_LOGIN_ENABLED === "1"] as const,
|
||||
["/auth/logout", process.env.APP_ROUTER_AUTH_LOGOUT_ENABLED === "1"] as const,
|
||||
["/auth/new", process.env.APP_ROUTER_AUTH_NEW_ENABLED === "1"] as const,
|
||||
["/auth/saml-idp", process.env.APP_ROUTER_AUTH_SAML_ENABLED === "1"] as const,
|
||||
["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const,
|
||||
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
|
||||
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
|
||||
["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const,
|
||||
|
||||
+21
-1
@@ -3,7 +3,8 @@ import i18next from "i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import type { MeetingImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
import { truncateOnWord } from "@calcom/lib/text";
|
||||
@@ -103,3 +104,22 @@ export const _generateMetadata = async (
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateMeetingMetadata = async (
|
||||
meeting: MeetingImageProps,
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string,
|
||||
hideBranding?: boolean,
|
||||
origin?: string
|
||||
) => {
|
||||
const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin);
|
||||
const image = SEO_IMG_OGIMG + constructMeetingImage(meeting);
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
openGraph: {
|
||||
...metadata.openGraph,
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,12 +10,18 @@ import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const inputSchema = z.object({
|
||||
customer: z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email(),
|
||||
username: z.string().optional(),
|
||||
timeZone: z.string().optional(),
|
||||
emailVerified: z.boolean().optional(),
|
||||
identityProvider: z.string().optional(),
|
||||
twoFactorEnabled: z.boolean().optional(),
|
||||
lastActiveAt: z.string().optional(),
|
||||
teamName: z.string().optional(),
|
||||
teamSlug: z.string().optional(),
|
||||
isOrganization: z.boolean().optional(),
|
||||
stripeCustomerId: z.string().optional(),
|
||||
}),
|
||||
cardKeys: z.array(z.string()),
|
||||
});
|
||||
@@ -53,168 +59,10 @@ async function handler(request: Request) {
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.email || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email Verified?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: customer.emailVerified ? "Yes" : "No",
|
||||
badgeColor: customer.emailVerified ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Username",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.username || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "User ID",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Time Zone",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.timeZone || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Two Factor Enabled?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: customer.twoFactorEnabled ? "Yes" : "No",
|
||||
badgeColor: customer.twoFactorEnabled ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Identity Provider",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: customer.identityProvider || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
componentText: {
|
||||
textSize: "L",
|
||||
textColor: "MUTED",
|
||||
text: "User does not exist!",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -223,18 +71,29 @@ async function handler(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch team details including userId and team name
|
||||
const teamMemberships = await UserRepository.findTeamsByUserId({ userId: user.id });
|
||||
const firstTeam = teamMemberships.teams[0] ?? null;
|
||||
|
||||
// Parse user metadata
|
||||
const parsedMetadata = userMetadata.parse(user.metadata);
|
||||
|
||||
const cards = await Promise.all(
|
||||
cardExamples.map(async (cardFn) => {
|
||||
return cardFn(
|
||||
user.name || "Unknown",
|
||||
user.email,
|
||||
user.id.toString(),
|
||||
user.username || "Unknown",
|
||||
user.timeZone,
|
||||
user.emailVerified,
|
||||
user.twoFactorEnabled
|
||||
user.twoFactorEnabled,
|
||||
user.identityProvider,
|
||||
user.lastActiveAt,
|
||||
firstTeam?.name || "Unknown",
|
||||
firstTeam?.slug || "Unknown",
|
||||
firstTeam?.isOrganization || false,
|
||||
(user.metadata as { stripeCustomerId?: string })?.stripeCustomerId || "Unknown"
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import Link from "next/link";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, Icon } from "@calcom/ui";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("error"),
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const querySchema = z.object({
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
const ServerPage = async ({ searchParams }: PageProps) => {
|
||||
const t = await getTranslate();
|
||||
const { error } = querySchema.parse({ error: searchParams?.error || undefined });
|
||||
const errorMsg = error || t("error_during_login");
|
||||
return (
|
||||
<AuthContainer title="" description="" isAppDir={true}>
|
||||
<div>
|
||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Icon name="x" className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
{errorMsg}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<Link href="/auth/login">
|
||||
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</AuthContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({ ServerPage })<"P">;
|
||||
+1
-1
@@ -5,7 +5,7 @@ import Page from "~/auth/oauth2/authorize-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => "Authorize",
|
||||
(t) => t("authorize"),
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -5,7 +5,7 @@ import Page from "~/auth/platform/authorize-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => "Authorize",
|
||||
(t) => t("authorize"),
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
@@ -4,9 +4,6 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
@@ -17,24 +14,15 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const pageProps = await getData(legacyCtx);
|
||||
|
||||
const { booking, user: username, slug: eventSlug, isTeamEvent } = pageProps;
|
||||
const { booking, eventData, isBrandingHidden } = pageProps;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
username,
|
||||
eventSlug,
|
||||
isTeamEvent,
|
||||
org,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
const profileName = eventData?.profile?.name ?? "";
|
||||
const title = eventData?.title ?? "";
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
|
||||
isBrandingHidden
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+45
-50
@@ -1,64 +1,59 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import type { NextPage } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import { captureException } from "@sentry/nextjs";
|
||||
import React from "react";
|
||||
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
import { ErrorPage } from "@components/error/error-page";
|
||||
|
||||
type NextError = Error & { digest?: string };
|
||||
|
||||
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
|
||||
export type DefaultErrorProps = {
|
||||
error: NextError;
|
||||
reset: () => void; // A function to reset the error boundary
|
||||
};
|
||||
|
||||
type AugmentedError = NextError | HttpError | null;
|
||||
|
||||
type CustomErrorProps = {
|
||||
err?: AugmentedError;
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
} & Omit<ErrorProps, "err" | "statusCode">;
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[error]"] });
|
||||
|
||||
const CustomError: NextPage<DefaultErrorProps> = (props) => {
|
||||
const { error } = props;
|
||||
let errorObject: CustomErrorProps = {
|
||||
message: error.message,
|
||||
err: error,
|
||||
};
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const redactedError = redactError(error);
|
||||
errorObject = {
|
||||
statusCode: error.statusCode,
|
||||
title: redactedError.name,
|
||||
message: redactedError.message,
|
||||
err: {
|
||||
...redactedError,
|
||||
...error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
|
||||
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
|
||||
log.info("errorObject: ", errorObject);
|
||||
|
||||
return (
|
||||
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
|
||||
);
|
||||
export type ErrorProps = {
|
||||
error: Error;
|
||||
reset?: () => void;
|
||||
};
|
||||
|
||||
export default CustomError;
|
||||
export default function Error({ error }: ErrorProps) {
|
||||
React.useEffect(() => {
|
||||
log.error(error);
|
||||
|
||||
// Log the error to Sentry
|
||||
captureException(error);
|
||||
}, [error]);
|
||||
|
||||
const processedError = React.useMemo(() => {
|
||||
const err = getErrorFromUnknown(error);
|
||||
|
||||
if (err instanceof HttpError) {
|
||||
const redactedError = redactError(err);
|
||||
return {
|
||||
statusCode: err.statusCode,
|
||||
title: redactedError.name,
|
||||
name: redactedError.name,
|
||||
message: redactedError.message,
|
||||
url: err.url,
|
||||
method: err.method,
|
||||
cause: err.cause,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 500,
|
||||
title: "Internal Server Error",
|
||||
name: "Internal Server Error",
|
||||
message: "An unexpected error occurred.",
|
||||
};
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<ErrorPage
|
||||
statusCode={processedError.statusCode}
|
||||
error={processedError}
|
||||
message={processedError.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSideProps } from "@lib/event-types/getServerSideProps";
|
||||
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
import EventTypes from "~/event-types/views/event-types-listing-view";
|
||||
|
||||
@@ -12,6 +18,16 @@ export const generateMetadata = async () =>
|
||||
(t) => t("event_types_page_subtitle")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const session = await getServerSessionForAppDir();
|
||||
if (!session?.user?.id) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: EventTypes })<"P">;
|
||||
await ssrInit(context);
|
||||
|
||||
return <EventTypes />;
|
||||
};
|
||||
|
||||
export default WithLayout({ ServerPage: Page })<"P">;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { withAppDirSsg } from "app/WithAppDirSsg";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getStaticProps } from "@server/lib/auth/error/getStaticProps";
|
||||
|
||||
import Page from "~/auth/error/error-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => "Error",
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsg(getStaticProps, "future/auth/error");
|
||||
|
||||
export default WithLayout({ getData, Page, getLayout: null })<"P">;
|
||||
export const dynamic = "force-static";
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { OrgTypePageProps } from "app/org/[orgSlug]/[user]/[type]/page";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
const getEmbedData = withEmbedSsrAppDir<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page });
|
||||
@@ -1,29 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/type-view";
|
||||
import TypePage from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { user: username, slug: eventSlug, booking } = props;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
username,
|
||||
eventSlug,
|
||||
isTeamEvent: true,
|
||||
org: isValidOrgDomain ? currentOrgDomain : null,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TypePage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import TeamPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,11 +0,0 @@
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/type-view";
|
||||
import LegacyPage from "~/team/type-view";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/lib/server/repository/event";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import LegacyPage, { type PageProps as LegacyPageProps } from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { user: username, slug: eventSlug, booking } = props;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
|
||||
|
||||
const event = await EventRepository.getPublicEvent({
|
||||
username,
|
||||
eventSlug,
|
||||
isTeamEvent: true,
|
||||
org: isValidOrgDomain ? currentOrgDomain : null,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -1,11 +0,0 @@
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import LegacyPage from "~/team/team-view";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import LegacyPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => props.team.name || t("nameless_team"),
|
||||
(t) => props.team.name || t("nameless_team")
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
})<"P">;
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { type NextPage } from "next";
|
||||
|
||||
import CustomError, { type DefaultErrorProps } from "./error";
|
||||
import CustomError from "./error";
|
||||
import type { ErrorProps } from "./error";
|
||||
|
||||
export const GlobalError: NextPage<DefaultErrorProps> = (props) => {
|
||||
export const GlobalError: NextPage<ErrorProps> = (props) => {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
|
||||
@@ -52,7 +52,6 @@ export function WithLayout<T extends Record<string, any>>({
|
||||
requiresLicense={requiresLicense || !!(Page && "requiresLicense" in Page && Page.requiresLicense)}
|
||||
nonce={nonce}
|
||||
themeBasis={null}
|
||||
isThemeSupported={Page && "isThemeSupported" in Page ? (Page.isThemeSupported as boolean) : undefined}
|
||||
isBookingPage={isBookingPage || !!(Page && "isBookingPage" in Page && Page.isBookingPage)}
|
||||
{...props}>
|
||||
{pageWithServerLayout}
|
||||
|
||||
+241
-6
@@ -1,12 +1,247 @@
|
||||
import React from "react";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { headers } from "next/headers";
|
||||
import Link from "next/link";
|
||||
|
||||
import {
|
||||
getOrgDomainConfigFromHostname,
|
||||
subdomainSuffix,
|
||||
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DOCS_URL, IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
enum PageType {
|
||||
ORG = "ORG",
|
||||
TEAM = "TEAM",
|
||||
USER = "USER",
|
||||
OTHER = "OTHER",
|
||||
}
|
||||
|
||||
function getPageInfo(pathname: string, host: string) {
|
||||
const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({ hostname: host });
|
||||
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
|
||||
if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) {
|
||||
const splitPath = routerUsername.split("/");
|
||||
if (splitPath[1] === "team" && splitPath.length === 3) {
|
||||
return {
|
||||
username: splitPath[2],
|
||||
pageType: PageType.TEAM,
|
||||
url: `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
username: routerUsername,
|
||||
pageType: PageType.USER,
|
||||
url: `${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
username: currentOrgDomain ?? "",
|
||||
pageType: PageType.ORG,
|
||||
url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${
|
||||
currentOrgDomain?.replace("/", "") ?? ""
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function NotFound() {
|
||||
const t = await getTranslate();
|
||||
const headersList = headers();
|
||||
const host = headersList.get("x-forwarded-host") ?? "";
|
||||
const pathname = headersList.get("x-pathname") ?? "";
|
||||
|
||||
// This makes more sense after full migration to App Router
|
||||
// if (!pathname) {
|
||||
// redirect("/500");
|
||||
// }
|
||||
|
||||
const { username, pageType, url } = getPageInfo(pathname, host);
|
||||
const isBookingSuccessPage = pathname?.startsWith("/booking");
|
||||
const isSubpage = pathname?.includes("/", 2) || isBookingSuccessPage;
|
||||
const isInsights = pathname?.startsWith("/insights");
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: t("enterprise"),
|
||||
description: "Learn more about organizations and subdomains in our enterprise plan.",
|
||||
icon: "shield" as const,
|
||||
href: `${WEBSITE_URL}/enterprise`,
|
||||
},
|
||||
{
|
||||
title: t("documentation"),
|
||||
description: t("documentation_description"),
|
||||
icon: "file-text" as const,
|
||||
href: DOCS_URL,
|
||||
},
|
||||
{
|
||||
title: t("blog"),
|
||||
description: t("blog_description"),
|
||||
icon: "book-open" as const,
|
||||
href: `${WEBSITE_URL}/blog`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* If we're on 404 and the route is insights it means it is disabled
|
||||
* TODO: Abstract this for all disabled features
|
||||
**/
|
||||
if (isInsights) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
|
||||
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||
{t("feature_currently_disabled") ?? "Feature is currently disabled"}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
<div className="mt-8">
|
||||
<Link href={WEBSITE_URL} className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<div data-testid="404-page">
|
||||
<h1>404 - Page Not Found</h1>
|
||||
<p>Sorry, the page you are looking for does not exist.</p>
|
||||
<div className="bg-default min-h-screen px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{t("error_404")}</p>
|
||||
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
|
||||
{isBookingSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
|
||||
</h1>
|
||||
{isSubpage && pageType !== PageType.TEAM ? (
|
||||
<span className="mt-2 inline-block text-lg">{t("check_spelling_mistakes_or_go_back")}</span>
|
||||
) : IS_CALCOM ? (
|
||||
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
|
||||
{t(`404_the_${pageType.toLowerCase()}`)}
|
||||
|
||||
{username ? (
|
||||
<>
|
||||
<strong className="text-blue-500">{username}</strong>
|
||||
{` ${t("is_still_available")} `}
|
||||
<span className="text-blue-500">{t("register_now")}</span>.
|
||||
</>
|
||||
) : null}
|
||||
</a>
|
||||
) : (
|
||||
<span className="mt-2 inline-block text-lg">
|
||||
{t(`404_the_${pageType.toLowerCase()}`)}{" "}
|
||||
{username ? (
|
||||
<>
|
||||
<strong className="text-lgtext-green-500 mt-2 inline-block">{username}</strong>{" "}
|
||||
{t("is_still_available")}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
{((!isSubpage && IS_CALCOM) || pageType === PageType.ORG || pageType === PageType.TEAM) && (
|
||||
<ul role="list" className="my-4">
|
||||
<li className="border-2 border-green-500 px-4 py-2">
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"
|
||||
rel="noreferrer">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
||||
<Icon name="check" className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="focus:outline-none">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{t("register")}{" "}
|
||||
<strong className="text-green-500">{`${
|
||||
pageType === PageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
|
||||
}${username}${pageType === PageType.ORG ? `.${subdomainSuffix()}` : ""}`}</strong>
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{t(`404_claim_entity_${pageType.toLowerCase()}`)}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon name="chevron-right" className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">{t("popular_pages")}</h2>
|
||||
<ul role="list" className="border-subtle divide-subtle divide-y">
|
||||
{links
|
||||
.filter((_, idx) => pageType === PageType.ORG || idx !== 0)
|
||||
.map((link, linkIdx) => (
|
||||
<li key={linkIdx} className="px-4 py-2">
|
||||
<a
|
||||
href={link.href}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Icon name={link.icon} className="text-default h-6 w-6" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{link.title}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{link.description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon name="chevron-right" className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-8">
|
||||
<Link href={WEBSITE_URL} className="hover:text-subtle text-emphasis text-base font-medium">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const headersList = headers();
|
||||
const pathname = headersList.get("x-pathname") ?? "";
|
||||
const isInsights = pathname?.startsWith("/insights");
|
||||
|
||||
const metadata = await _generateMetadata(
|
||||
(t) =>
|
||||
isInsights
|
||||
? t("feature_currently_disabled") ?? "Feature is currently disabled"
|
||||
: t("404_page_not_found"),
|
||||
(t) => t("404_page_not_found")
|
||||
);
|
||||
return {
|
||||
...metadata,
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
export default WithLayout({
|
||||
ServerPage: NotFound,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateMeetingMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps;
|
||||
const getData = withAppDirSsr<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
|
||||
const profileName = eventData?.profile?.name ?? "";
|
||||
const profileImage = eventData?.profile.image;
|
||||
const title = eventData?.title ?? "";
|
||||
const meeting = {
|
||||
title,
|
||||
profile: { name: profileName, image: profileImage },
|
||||
users: [
|
||||
...(eventData?.users || []).map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
],
|
||||
};
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
|
||||
isBrandingHidden,
|
||||
getOrgFullOrigin(eventData?.entity.orgSlug ?? null)
|
||||
);
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
robots: {
|
||||
follow: !(eventData?.hidden || !isSEOIndexable),
|
||||
index: !(eventData?.hidden || !isSEOIndexable),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const Page = async (props: OrgTypePageProps) => {
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
}
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
|
||||
@@ -0,0 +1,4 @@
|
||||
import Page from "app/team/[slug]/page";
|
||||
|
||||
export { generateMetadata } from "app/team/[slug]/page";
|
||||
export default Page;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Page from "app/team/[slug]/[type]/page";
|
||||
|
||||
export { generateMetadata } from "app/team/[slug]/[type]/page";
|
||||
export default Page;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Page from "app/team/[slug]/page";
|
||||
|
||||
export { generateMetadata } from "app/team/[slug]/page";
|
||||
export default Page;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { generateMeetingMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
import LegacyPage from "~/team/type-view";
|
||||
import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
|
||||
const profileName = eventData?.profile?.name ?? "";
|
||||
const profileImage = eventData?.profile.image;
|
||||
const title = eventData?.title ?? "";
|
||||
const meeting = {
|
||||
title,
|
||||
profile: { name: profileName, image: profileImage },
|
||||
users: [
|
||||
...(eventData?.users || []).map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`,
|
||||
isBrandingHidden,
|
||||
getOrgFullOrigin(eventData.entity.orgSlug ?? null)
|
||||
);
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
robots: {
|
||||
follow: !(eventData?.hidden || !isSEOIndexable),
|
||||
index: !(eventData?.hidden || !isSEOIndexable),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps as _PageProps } from "app/_types";
|
||||
import { generateMeetingMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/team/team-view";
|
||||
import LegacyPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams)
|
||||
);
|
||||
|
||||
const meeting = {
|
||||
title: markdownStrippedBio ?? "",
|
||||
profile: {
|
||||
name: `${team.name}`,
|
||||
image: getOrgOrTeamAvatar(team),
|
||||
},
|
||||
};
|
||||
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
(t) => team.name || t("nameless_team"),
|
||||
(t) => team.name || t("nameless_team"),
|
||||
false,
|
||||
getOrgFullOrigin(currentOrgDomain ?? null)
|
||||
);
|
||||
return {
|
||||
...metadata,
|
||||
robots: {
|
||||
follow: isSEOIndexable,
|
||||
index: isSEOIndexable,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: LegacyPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -18,7 +18,6 @@ export type PageWrapperProps = Readonly<{
|
||||
nonce: string | undefined;
|
||||
themeBasis: string | null;
|
||||
dehydratedState?: DehydratedState;
|
||||
isThemeSupported?: boolean;
|
||||
isBookingPage?: boolean;
|
||||
i18n?: SSRConfig;
|
||||
}>;
|
||||
|
||||
@@ -68,6 +68,7 @@ type BookingItemProps = BookingItem & {
|
||||
userTimeFormat: number | null | undefined;
|
||||
userEmail: string | undefined;
|
||||
};
|
||||
isToday: boolean;
|
||||
};
|
||||
|
||||
type ParsedBooking = ReturnType<typeof buildParsedBooking>;
|
||||
@@ -511,9 +512,12 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<tr data-testid="booking-item" className="hover:bg-muted group transition ">
|
||||
<div
|
||||
data-testid="booking-item"
|
||||
data-today={String(booking.isToday)}
|
||||
className="hover:bg-muted group w-full">
|
||||
<div className="flex flex-col sm:flex-row">
|
||||
<td className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
|
||||
<div className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
|
||||
<div className="flex h-full items-center">
|
||||
{eventTypeColor && (
|
||||
<div className="h-[70%] w-0.5" style={{ backgroundColor: eventTypeColor }} />
|
||||
@@ -562,8 +566,10 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
<td data-testid="title-and-attendees" className={`w-full px-4${isRejected ? " line-through" : ""}`}>
|
||||
</div>
|
||||
<div
|
||||
data-testid="title-and-attendees"
|
||||
className={`w-full px-4${isRejected ? " line-through" : ""}`}>
|
||||
<Link href={bookingLink}>
|
||||
{/* Time and Badges for mobile */}
|
||||
<div className="w-full pb-2 pt-4 sm:hidden">
|
||||
@@ -648,8 +654,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
|
||||
</div>
|
||||
<div className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
|
||||
{isUpcoming && !isCancelled ? (
|
||||
<>
|
||||
{isPending && <TableActions actions={pendingActions} />}
|
||||
@@ -674,7 +680,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<TableActions actions={chargeCardActions} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
<BookingItemBadges
|
||||
booking={booking}
|
||||
@@ -683,7 +689,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
userTimeFormat={userTimeFormat}
|
||||
userTimeZone={userTimeZone}
|
||||
/>
|
||||
</tr>
|
||||
</div>
|
||||
|
||||
{isBookingReroutable(parsedBooking) && (
|
||||
<RerouteDialog
|
||||
@@ -712,7 +718,7 @@ const BookingItemBadges = ({
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="hidden h-9 flex-row pb-4 pl-6 sm:flex">
|
||||
<div className="hidden h-9 flex-row items-center pb-4 pl-6 sm:flex">
|
||||
{isPending && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||
{t("unconfirmed")}
|
||||
@@ -1251,11 +1257,9 @@ const AssignmentReasonTooltip = ({ assignmentReason }: { assignmentReason: Assig
|
||||
const badgeTitle = assignmentReasonBadgeTitleMap(assignmentReason.reasonEnum);
|
||||
return (
|
||||
<Tooltip content={<p>{assignmentReason.reasonString}</p>}>
|
||||
<div className="-mt-1">
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{t(badgeTitle)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{t(badgeTitle)}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRefreshData } from "@calcom/lib/hooks/useRefreshData";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, Icon, TextArea } from "@calcom/ui";
|
||||
import { Button, Icon, Label, TextArea } from "@calcom/ui";
|
||||
|
||||
type Props = {
|
||||
booking: {
|
||||
@@ -33,6 +33,7 @@ type Props = {
|
||||
};
|
||||
eventType: unknown;
|
||||
};
|
||||
isHost: boolean;
|
||||
};
|
||||
|
||||
export default function CancelBooking(props: Props) {
|
||||
@@ -70,7 +71,7 @@ export default function CancelBooking(props: Props) {
|
||||
)}
|
||||
{!error && (
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
||||
<Label>{props.isHost ? t("cancellation_reason_host") : t("cancellation_reason")}</Label>
|
||||
<TextArea
|
||||
data-testid="cancel_reason"
|
||||
ref={cancelBookingRef}
|
||||
@@ -80,6 +81,14 @@ export default function CancelBooking(props: Props) {
|
||||
className="mb-4 mt-2 w-full "
|
||||
rows={3}
|
||||
/>
|
||||
{props.isHost ? (
|
||||
<div className="-mt-2 mb-4 flex items-center gap-2">
|
||||
<Icon name="info" className="text-subtle h-4 w-4" />
|
||||
<p className="text-default text-subtle text-sm leading-none">
|
||||
{t("notify_attendee_cancellation_reason_warning")}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||
<div className="ml-auto flex w-full space-x-4 ">
|
||||
<Button
|
||||
@@ -90,6 +99,7 @@ export default function CancelBooking(props: Props) {
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="confirm_cancel"
|
||||
disabled={props.isHost && !cancellationReason}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
type Props = {
|
||||
statusCode?: number | null;
|
||||
@@ -53,21 +56,37 @@ export const ErrorPage: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-default min-h-screen px-4">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
|
||||
<h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-emphasis text-6xl">{statusCode}</h1>
|
||||
<h2 className="text-emphasis mt-6 max-w-2xl text-2xl font-medium">
|
||||
It's not you, it's us.
|
||||
</h2>
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
Something went wrong on our end. Get in touch with our support team, and we'll get it fixed
|
||||
right away for you.
|
||||
</p>
|
||||
|
||||
<div className="mb-8 flex flex-col">
|
||||
<p className="text-default mb-4 max-w-2xl text-sm">
|
||||
Please provide the following text when contacting support to better help you:
|
||||
</p>
|
||||
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
|
||||
{message}
|
||||
</h1>
|
||||
</pre>
|
||||
</div>
|
||||
</main>
|
||||
{displayDebug && (
|
||||
<div className="flex-wrap">
|
||||
<ErrorDebugPanel error={error} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button href="mailto:support@cal.com">Contact Support</Button>
|
||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{displayDebug && (
|
||||
<div className="flex-wrap">
|
||||
<ErrorDebugPanel error={error} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,12 +11,13 @@ interface Props {
|
||||
showLogo?: boolean;
|
||||
heading?: string;
|
||||
loading?: boolean;
|
||||
isAppDir?: boolean;
|
||||
}
|
||||
|
||||
export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="bg-subtle dark:bg-darkgray-50 flex min-h-screen flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<HeadSeo title={props.title} description={props.description} />
|
||||
{!props.isAppDir ? <HeadSeo title={props.title} description={props.description} /> : null}
|
||||
{props.showLogo && <Logo small inline={false} className="mx-auto mb-auto" />}
|
||||
|
||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||
import { useFlags } from "@calcom/features/flags/hooks";
|
||||
|
||||
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
|
||||
import useIsThemeSupported from "@lib/hooks/useIsThemeSupported";
|
||||
import PlainChat from "@lib/plain/plainChat";
|
||||
import type { WithLocaleProps } from "@lib/withLocale";
|
||||
import type { WithNonceProps } from "@lib/withNonce";
|
||||
@@ -48,7 +49,6 @@ export type AppProps = Omit<
|
||||
> & {
|
||||
Component: NextAppProps["Component"] & {
|
||||
requiresLicense?: boolean;
|
||||
isThemeSupported?: boolean;
|
||||
isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean);
|
||||
getLayout?: (page: React.ReactElement) => ReactNode;
|
||||
PageWrapper?: (props: AppProps) => JSX.Element;
|
||||
@@ -129,7 +129,7 @@ type CalcomThemeProps = Readonly<{
|
||||
themeBasis: string | null;
|
||||
nonce: string | undefined;
|
||||
children: React.ReactNode;
|
||||
isThemeSupported?: boolean;
|
||||
isThemeSupported: boolean;
|
||||
}>;
|
||||
|
||||
const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||
@@ -196,8 +196,7 @@ function getThemeProviderProps({
|
||||
}) {
|
||||
const themeSupport = props.isBookingPage
|
||||
? ThemeSupport.Booking
|
||||
: // if isThemeSupported is explicitly false, we don't use theme there
|
||||
props.isThemeSupported === false
|
||||
: props.isThemeSupported === false
|
||||
? ThemeSupport.None
|
||||
: ThemeSupport.App;
|
||||
|
||||
@@ -263,6 +262,7 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
|
||||
const AppProviders = (props: PageWrapperProps) => {
|
||||
// No need to have intercom on public pages - Good for Page Performance
|
||||
const isBookingPage = useIsBookingPage();
|
||||
const isThemeSupported = useIsThemeSupported();
|
||||
|
||||
const RemainingProviders = (
|
||||
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
|
||||
@@ -273,7 +273,7 @@ const AppProviders = (props: PageWrapperProps) => {
|
||||
<CalcomThemeProvider
|
||||
themeBasis={props.themeBasis}
|
||||
nonce={props.nonce}
|
||||
isThemeSupported={/* undefined gets treated as true */ props.isThemeSupported}
|
||||
isThemeSupported={isThemeSupported}
|
||||
isBookingPage={props.isBookingPage || isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
|
||||
@@ -26,6 +26,25 @@ const buildLegacyCookies = (cookies: ReadonlyRequestCookies) => {
|
||||
return createProxifiedObject(cookiesObject);
|
||||
};
|
||||
|
||||
function decodeParams(params: Params): Params {
|
||||
return Object.entries(params).reduce((acc, [key, value]) => {
|
||||
// Handle array values
|
||||
if (Array.isArray(value)) {
|
||||
acc[key] = value.map((item) => decodeURIComponent(item));
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Handle single values
|
||||
if (value !== undefined) {
|
||||
acc[key] = decodeURIComponent(value);
|
||||
} else {
|
||||
acc[key] = value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Params);
|
||||
}
|
||||
|
||||
export const buildLegacyCtx = (
|
||||
headers: ReadonlyHeaders,
|
||||
cookies: ReadonlyRequestCookies,
|
||||
@@ -34,7 +53,10 @@ export const buildLegacyCtx = (
|
||||
) => {
|
||||
return {
|
||||
query: { ...searchParams, ...params },
|
||||
params,
|
||||
// decoding is required to be backward compatible with Pages Router
|
||||
// because Next.js App Router does not auto-decode query params while Pages Router does
|
||||
// e.g., params: { name: "John%20Doe" } => params: { name: "John Doe" }
|
||||
params: decodeParams(params),
|
||||
req: { headers: buildLegacyHeaders(headers), cookies: buildLegacyCookies(cookies) },
|
||||
res: new Proxy(Object.create(null), {
|
||||
// const { req, res } = ctx - valid
|
||||
|
||||
@@ -121,6 +121,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventData,
|
||||
entity: eventData.entity,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const session = await getServerSession({ req: context.req });
|
||||
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/auth/login",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { props: { trpcState: ssr.dehydrate() } };
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const THEME_UNSUPPORTED_ROUTES = ["/auth/setup"];
|
||||
|
||||
export default function useIsThemeSupported(): boolean {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Check if current pathname matches any unsupported route
|
||||
const isUnsupportedRoute = THEME_UNSUPPORTED_ROUTES.some((route) => pathname?.startsWith(route));
|
||||
|
||||
return !isUnsupportedRoute;
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
@@ -179,7 +179,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
eventData,
|
||||
});
|
||||
|
||||
const organizationSettings = OrganizationRepository.utils.getOrganizationSEOSettings(team);
|
||||
const organizationSettings = getOrganizationSEOSettings(team);
|
||||
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
|
||||
|
||||
if (!eventData) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import {
|
||||
getOrganizationSettings,
|
||||
getVerifiedDomain,
|
||||
} from "@calcom/features/ee/organizations/lib/orgSettings";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
import { IS_CALCOM } from "@calcom/lib/constants";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -30,8 +34,13 @@ function getOrgProfileRedirectToVerifiedDomain(
|
||||
if (!team.isOrganization) {
|
||||
return null;
|
||||
}
|
||||
// when this is not on a Cal.com page we don't auto redirect -
|
||||
// good for diagnosis purposes.
|
||||
if (!IS_CALCOM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const verifiedDomain = OrganizationRepository.utils.getVerifiedDomain(settings);
|
||||
const verifiedDomain = getVerifiedDomain(settings);
|
||||
|
||||
if (!settings.orgProfileRedirectsToVerifiedDomain || !verifiedDomain) {
|
||||
return null;
|
||||
@@ -145,7 +154,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
}
|
||||
|
||||
const organizationSettings = OrganizationRepository.utils.getOrganizationSettings(team);
|
||||
const organizationSettings = getOrganizationSettings(team);
|
||||
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
|
||||
|
||||
const redirectToVerifiedDomain = organizationSettings
|
||||
|
||||
+14
-5
@@ -22,7 +22,6 @@ const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
|
||||
const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
||||
const url = req.nextUrl;
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
|
||||
requestHeaders.set("x-url", req.url);
|
||||
|
||||
if (!url.pathname.startsWith("/api")) {
|
||||
@@ -155,6 +154,7 @@ export const config = {
|
||||
matcher: [
|
||||
"/403",
|
||||
"/500",
|
||||
"/icons",
|
||||
"/d/:path*",
|
||||
"/more/:path*",
|
||||
"/maintenance/:path*",
|
||||
@@ -167,7 +167,10 @@ export const config = {
|
||||
"/api/trpc/:path*",
|
||||
"/login",
|
||||
"/auth/login",
|
||||
"/future/auth/login",
|
||||
"/auth/error",
|
||||
"/auth/signin",
|
||||
"/auth/oauth2/authorize",
|
||||
"/auth/platform/authorize",
|
||||
/**
|
||||
* Paths required by routingForms.handle
|
||||
*/
|
||||
@@ -187,14 +190,20 @@ export const config = {
|
||||
"/workflows/:path*",
|
||||
"/getting-started/:path*",
|
||||
"/apps",
|
||||
"/bookings/:status/",
|
||||
"/bookings/:path*",
|
||||
"/video/:path*",
|
||||
"/teams",
|
||||
"/future/teams/",
|
||||
"/teams/:path*",
|
||||
"/signup/:path*",
|
||||
"/settings/:path*",
|
||||
"/reschedule/:path*",
|
||||
"/availability/:path*",
|
||||
"/booking/:path*",
|
||||
"/routing-forms/:path*",
|
||||
"/team/:path*",
|
||||
"/org/[orgSlug]/[user]/[type]",
|
||||
"/org/[orgSlug]/team/[slug]/[type]",
|
||||
"/org/[orgSlug]/team/[slug]",
|
||||
"/org/[orgSlug]",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -9,13 +9,11 @@ import { Button, Icon } from "@calcom/ui";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import type { PageProps } from "@server/lib/auth/error/getStaticProps";
|
||||
|
||||
const querySchema = z.object({
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function Error(props: PageProps) {
|
||||
export default function Error() {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const { error } = querySchema.parse({ error: searchParams?.get("error") || undefined });
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { Fragment, ReactElement, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
createColumnHelper,
|
||||
} from "@tanstack/react-table";
|
||||
import type { ReactElement } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -10,20 +17,20 @@ 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";
|
||||
import { DataTableProvider, DataTableWrapper } from "@calcom/features/data-table";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui";
|
||||
import { Alert, EmptyScreen, HorizontalTabs } from "@calcom/ui";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import BookingListItem from "@components/booking/BookingListItem";
|
||||
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
||||
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
import type { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
|
||||
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
|
||||
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
|
||||
@@ -66,7 +73,30 @@ const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
|
||||
unconfirmed: "unconfirmed_bookings",
|
||||
};
|
||||
|
||||
export default function Bookings({ status }: { status: (typeof validStatuses)[number] }) {
|
||||
type BookingsProps = {
|
||||
status: (typeof validStatuses)[number];
|
||||
};
|
||||
|
||||
export default function Bookings(props: BookingsProps) {
|
||||
return (
|
||||
<DataTableProvider>
|
||||
<BookingsContent {...props} />
|
||||
</DataTableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type RowData =
|
||||
| {
|
||||
type: "data";
|
||||
booking: BookingOutput;
|
||||
isToday: boolean;
|
||||
recurringInfo?: RecurringInfo;
|
||||
}
|
||||
| {
|
||||
type: "today" | "next";
|
||||
};
|
||||
|
||||
function BookingsContent({ status }: BookingsProps) {
|
||||
const { data: filterQuery } = useFilterQuery();
|
||||
|
||||
const { t } = useLocale();
|
||||
@@ -87,56 +117,130 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
}
|
||||
);
|
||||
|
||||
// Animate page (tab) transitions to look smoothing
|
||||
const columns = useMemo(() => {
|
||||
const columnHelper = createColumnHelper<RowData>();
|
||||
|
||||
const buttonInView = useInViewObserver(() => {
|
||||
if (!query.isFetching && query.hasNextPage && query.status === "success") {
|
||||
query.fetchNextPage();
|
||||
}
|
||||
});
|
||||
return [
|
||||
columnHelper.display({
|
||||
id: "custom-view",
|
||||
cell: (props) => {
|
||||
if (props.row.original.type === "data") {
|
||||
const { booking, recurringInfo, isToday } = props.row.original;
|
||||
return (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
isToday={isToday}
|
||||
loggedInUser={{
|
||||
userId: user?.id,
|
||||
userTimeZone: user?.timeZone,
|
||||
userTimeFormat: user?.timeFormat,
|
||||
userEmail: user?.email,
|
||||
}}
|
||||
listingStatus={status}
|
||||
recurringInfo={recurringInfo}
|
||||
{...booking}
|
||||
/>
|
||||
);
|
||||
} else if (props.row.original.type === "today") {
|
||||
return (
|
||||
<p className="text-subtle bg-subtle w-full py-4 pl-6 text-xs font-semibold uppercase leading-4">
|
||||
{t("today")}
|
||||
</p>
|
||||
);
|
||||
} else if (props.row.original.type === "next") {
|
||||
return (
|
||||
<p className="text-subtle bg-subtle w-full py-4 pl-6 text-xs font-semibold uppercase leading-4">
|
||||
{t("next")}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
}, [user, status]);
|
||||
|
||||
const isEmpty = !query.data?.pages[0]?.bookings.length;
|
||||
|
||||
const shownBookings: Record<string, BookingOutput[]> = {};
|
||||
const filterBookings = (booking: BookingOutput) => {
|
||||
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
|
||||
if (!booking.recurringEventId) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
shownBookings[booking.recurringEventId] !== undefined &&
|
||||
shownBookings[booking.recurringEventId].length > 0
|
||||
) {
|
||||
shownBookings[booking.recurringEventId].push(booking);
|
||||
return false;
|
||||
}
|
||||
shownBookings[booking.recurringEventId] = [booking];
|
||||
} else if (status === "upcoming") {
|
||||
return (
|
||||
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
|
||||
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
let recurringInfoToday: RecurringInfo | undefined;
|
||||
|
||||
const bookingsToday =
|
||||
query.data?.pages.map((page) =>
|
||||
page.bookings.filter((booking: BookingOutput) => {
|
||||
recurringInfoToday = page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
);
|
||||
const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]);
|
||||
|
||||
const flatData = useMemo<RowData[]>(() => {
|
||||
const shownBookings: Record<string, BookingOutput[]> = {};
|
||||
const filterBookings = (booking: BookingOutput) => {
|
||||
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
|
||||
if (!booking.recurringEventId) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
shownBookings[booking.recurringEventId] !== undefined &&
|
||||
shownBookings[booking.recurringEventId].length > 0
|
||||
) {
|
||||
shownBookings[booking.recurringEventId].push(booking);
|
||||
return false;
|
||||
}
|
||||
shownBookings[booking.recurringEventId] = [booking];
|
||||
} else if (status === "upcoming") {
|
||||
return (
|
||||
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
|
||||
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
|
||||
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
|
||||
);
|
||||
})
|
||||
)[0] || [];
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
return (
|
||||
query.data?.pages.flatMap((page) =>
|
||||
page.bookings.filter(filterBookings).map((booking) => ({
|
||||
type: "data",
|
||||
booking,
|
||||
recurringInfo: page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
),
|
||||
isToday: false,
|
||||
}))
|
||||
) || []
|
||||
);
|
||||
}, [query.data]);
|
||||
|
||||
const bookingsToday = useMemo<RowData[]>(() => {
|
||||
return (
|
||||
query.data?.pages.map((page) =>
|
||||
page.bookings
|
||||
.filter(
|
||||
(booking: BookingOutput) =>
|
||||
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
|
||||
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
|
||||
)
|
||||
.map((booking) => ({
|
||||
type: "data" as const,
|
||||
booking,
|
||||
recurringInfo: page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
),
|
||||
isToday: true,
|
||||
}))
|
||||
)[0] || []
|
||||
);
|
||||
}, [query.data]);
|
||||
|
||||
const finalData = useMemo<RowData[]>(() => {
|
||||
if (bookingsToday.length > 0 && status === "upcoming") {
|
||||
const merged: RowData[] = [
|
||||
{ type: "today" as const },
|
||||
...bookingsToday,
|
||||
{ type: "next" as const },
|
||||
...flatData,
|
||||
];
|
||||
return merged;
|
||||
} else {
|
||||
return flatData;
|
||||
}
|
||||
}, [bookingsToday, flatData, status]);
|
||||
|
||||
const table = useReactTable<RowData>({
|
||||
data: finalData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
@@ -146,7 +250,7 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
</div>
|
||||
<FiltersContainer isFiltersVisible={isFiltersVisible} />
|
||||
<main className="w-full">
|
||||
<div className="flex w-full flex-col" ref={animationParentRef}>
|
||||
<div className="flex w-full flex-col">
|
||||
{query.status === "error" && (
|
||||
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
|
||||
)}
|
||||
@@ -154,73 +258,19 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
{query.status === "success" && !isEmpty && (
|
||||
<>
|
||||
{!!bookingsToday.length && status === "upcoming" && (
|
||||
<div className="mb-6 pt-2 xl:pt-0">
|
||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
|
||||
<div className="border-subtle overflow-hidden rounded-md border">
|
||||
<table className="w-full max-w-full table-fixed">
|
||||
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
|
||||
<Fragment>
|
||||
{bookingsToday.map((booking: BookingOutput) => (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
loggedInUser={{
|
||||
userId: user?.id,
|
||||
userTimeZone: user?.timeZone,
|
||||
userTimeFormat: user?.timeFormat,
|
||||
userEmail: user?.email,
|
||||
}}
|
||||
listingStatus={status}
|
||||
recurringInfo={recurringInfoToday}
|
||||
{...booking}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||
)}
|
||||
<div className="pt-2 xl:pt-0">
|
||||
<div className="border-subtle overflow-hidden rounded-md border">
|
||||
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
|
||||
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
|
||||
{query.data.pages.map((page, index) => (
|
||||
<Fragment key={index}>
|
||||
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
||||
const recurringInfo = page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
);
|
||||
return (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
loggedInUser={{
|
||||
userId: user?.id,
|
||||
userTimeZone: user?.timeZone,
|
||||
userTimeFormat: user?.timeFormat,
|
||||
userEmail: user?.email,
|
||||
}}
|
||||
listingStatus={status}
|
||||
recurringInfo={recurringInfo}
|
||||
{...booking}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
|
||||
<Button
|
||||
color="minimal"
|
||||
loading={query.isFetchingNextPage}
|
||||
disabled={!query.hasNextPage}
|
||||
onClick={() => query.fetchNextPage()}>
|
||||
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DataTableWrapper
|
||||
table={table}
|
||||
testId={`${status}-bookings`}
|
||||
bodyTestId="bookings"
|
||||
hideHeader={true}
|
||||
isPending={query.isFetching && !flatData}
|
||||
hasNextPage={query.hasNextPage}
|
||||
fetchNextPage={query.fetchNextPage}
|
||||
isFetching={query.isFetching}
|
||||
variant="compact"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{query.status === "success" && isEmpty && (
|
||||
|
||||
@@ -163,10 +163,24 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
});
|
||||
|
||||
const userId = session?.user?.id;
|
||||
const isLoggedInUserHost =
|
||||
userId &&
|
||||
(eventType.users.some((user) => user.id === userId) ||
|
||||
eventType.hosts.some(({ user }) => user.id === userId));
|
||||
|
||||
const checkIfUserIsHost = (userId?: number | null) => {
|
||||
if (!userId) return false;
|
||||
|
||||
return (
|
||||
bookingInfo?.user?.id === userId ||
|
||||
eventType.users.some(
|
||||
(user) =>
|
||||
user.id === userId && bookingInfo.attendees.some((attendee) => attendee.email === user.email)
|
||||
) ||
|
||||
eventType.hosts.some(
|
||||
({ user }) =>
|
||||
user.id === userId && bookingInfo.attendees.some((attendee) => attendee.email === user.email)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const isLoggedInUserHost = checkIfUserIsHost(userId);
|
||||
|
||||
if (!isLoggedInUserHost) {
|
||||
// Removing hidden fields from responses
|
||||
@@ -195,6 +209,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
userTimeFormat,
|
||||
requiresLoginToUpdate,
|
||||
rescheduledToUid,
|
||||
isLoggedInUserHost,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import type { nameObjectSchema } from "@calcom/core/event";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import type { ConfigType } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
useIsBackgroundTransparent,
|
||||
@@ -61,7 +60,6 @@ import {
|
||||
showToast,
|
||||
EmptyScreen,
|
||||
Icon,
|
||||
HeadSeo,
|
||||
} from "@calcom/ui";
|
||||
import CancelBooking from "@calcom/web/components/booking/CancelBooking";
|
||||
import EventReservationSchema from "@calcom/web/components/schemas/EventReservationSchema";
|
||||
@@ -109,7 +107,7 @@ export default function Success(props: PageProps) {
|
||||
const routerQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { eventType, bookingInfo, requiresLoginToUpdate, orgSlug, rescheduledToUid } = props;
|
||||
const { eventType, bookingInfo, requiresLoginToUpdate, rescheduledToUid } = props;
|
||||
|
||||
const {
|
||||
allRemainingBookings,
|
||||
@@ -134,7 +132,6 @@ export default function Success(props: PageProps) {
|
||||
) {
|
||||
rescheduleLocation = bookingInfo.responses.location.optionValue;
|
||||
}
|
||||
|
||||
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
|
||||
bookingInfo?.metadata || {}
|
||||
)?.videoCallUrl;
|
||||
@@ -151,6 +148,7 @@ export default function Success(props: PageProps) {
|
||||
props?.userTimeFormat ? props.userTimeFormat === 24 : isBrowserLocale24h()
|
||||
);
|
||||
const { data: session } = useSession();
|
||||
const isHost = props.isLoggedInUserHost;
|
||||
|
||||
const [date, setDate] = useState(dayjs.utc(bookingInfo.startTime));
|
||||
|
||||
@@ -319,7 +317,6 @@ export default function Success(props: PageProps) {
|
||||
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
|
||||
}
|
||||
if (bookingInfo.user) {
|
||||
const isHost = bookingInfo.user.id === session?.user?.id;
|
||||
const isAttendee = bookingInfo.attendees.find((attendee) => attendee.email === session?.user?.email);
|
||||
const attendee = bookingInfo.attendees[0]?.name || bookingInfo.attendees[0]?.email || "Nameless";
|
||||
const host = bookingInfo.user.name || bookingInfo.user.email;
|
||||
@@ -348,10 +345,6 @@ export default function Success(props: PageProps) {
|
||||
brandColor: props.profile.brandColor,
|
||||
darkBrandColor: props.profile.darkBrandColor,
|
||||
});
|
||||
const title = t(
|
||||
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
|
||||
);
|
||||
|
||||
const locationToDisplay = getSuccessPageLocationMessage(
|
||||
locationVideoCallUrl ? locationVideoCallUrl : location,
|
||||
t,
|
||||
@@ -440,7 +433,6 @@ export default function Success(props: PageProps) {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<HeadSeo origin={getOrgFullOrigin(orgSlug)} title={title} description={title} />
|
||||
<BookingPageTagManager
|
||||
eventType={{ ...eventType, metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata) }}
|
||||
/>
|
||||
@@ -785,6 +777,7 @@ export default function Success(props: PageProps) {
|
||||
seatReferenceUid={seatReferenceUid}
|
||||
bookingCancelledEventProps={bookingCancelledEventProps}
|
||||
currentUserEmail={currentUserEmail}
|
||||
isHost={isHost}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
|
||||
|
||||
@@ -20,13 +19,6 @@ export default function Type({
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={entity}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -944,7 +944,6 @@ const InfiniteScrollMain = ({
|
||||
|
||||
const EventTypesPage: React.FC = () => {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const { data: user } = useMeQuery();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_showProfileBanner, setShowProfileBanner] = useState(false);
|
||||
|
||||
@@ -58,7 +58,7 @@ const signupSchema = apiSignupSchema.extend({
|
||||
cfToken: z.string().optional(),
|
||||
});
|
||||
|
||||
const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false });
|
||||
const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false });
|
||||
|
||||
type FormValues = z.infer<typeof signupSchema>;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -20,7 +19,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
|
||||
import { Avatar, Button, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
|
||||
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import type { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
@@ -29,14 +28,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import Team from "@components/team/screens/Team";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
function TeamPage({
|
||||
team,
|
||||
considerUnpublished,
|
||||
markdownStrippedBio,
|
||||
isValidOrgDomain,
|
||||
currentOrgDomain,
|
||||
isSEOIndexable,
|
||||
}: PageProps) {
|
||||
function TeamPage({ team, considerUnpublished, isValidOrgDomain }: PageProps) {
|
||||
useTheme(team.theme);
|
||||
const routerQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
@@ -165,22 +157,6 @@ function TeamPage({
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
origin={getOrgFullOrigin(currentOrgDomain)}
|
||||
title={teamName}
|
||||
description={teamName}
|
||||
meeting={{
|
||||
title: markdownStrippedBio,
|
||||
profile: {
|
||||
name: `${team.name}`,
|
||||
image: profileImageSrc,
|
||||
},
|
||||
}}
|
||||
nextSeoProps={{
|
||||
nofollow: !isSEOIndexable,
|
||||
noindex: !isSEOIndexable,
|
||||
}}
|
||||
/>
|
||||
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
|
||||
<div className="mx-auto mb-8 max-w-3xl text-center">
|
||||
<div className="relative">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { Booker } from "@calcom/atoms/monorepo";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
|
||||
import type { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -26,7 +25,6 @@ function Type({
|
||||
slug,
|
||||
user,
|
||||
booking,
|
||||
isEmbed,
|
||||
isBrandingHidden,
|
||||
eventData,
|
||||
isInstantMeeting,
|
||||
@@ -34,33 +32,12 @@ function Type({
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
isSEOIndexable,
|
||||
isEmbed,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const { profile, users, hidden, title } = eventData;
|
||||
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
eventData={
|
||||
profile && users && title && hidden !== undefined
|
||||
? {
|
||||
profile,
|
||||
users,
|
||||
title,
|
||||
hidden,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
entity={eventData.entity}
|
||||
bookingData={booking}
|
||||
isSEOIndexable={isSEOIndexable}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
@@ -87,6 +64,4 @@ function Type({
|
||||
);
|
||||
}
|
||||
|
||||
Type.isBookingPage = true;
|
||||
|
||||
export default Type;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.8.16",
|
||||
"version": "4.9.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -94,7 +94,7 @@
|
||||
"ics": "^2.37.0",
|
||||
"jose": "^4.13.1",
|
||||
"kbar": "^0.1.0-beta.36",
|
||||
"libphonenumber-js": "^1.10.51",
|
||||
"libphonenumber-js": "^1.11.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-react": "^2.3.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
|
||||
@@ -6,10 +6,18 @@ import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const userIp = getIP(req);
|
||||
|
||||
if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") {
|
||||
await checkCfTurnstileToken({
|
||||
token: req.body["cfToken"] as string,
|
||||
remoteIp: userIp,
|
||||
});
|
||||
}
|
||||
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: userIp,
|
||||
|
||||
@@ -6,12 +6,20 @@ import type { BookingResponse } from "@calcom/features/bookings/types";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
|
||||
|
||||
// @TODO: Didn't look at the contents of this function in order to not break old booking page.
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const userIp = getIP(req);
|
||||
|
||||
if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") {
|
||||
await checkCfTurnstileToken({
|
||||
token: req.body[0]["cfToken"] as string,
|
||||
remoteIp: userIp,
|
||||
});
|
||||
}
|
||||
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: userIp,
|
||||
|
||||
@@ -1,18 +1,63 @@
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
import type { Card } from "@pages/api/plain";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
export default (
|
||||
name: string,
|
||||
email: string,
|
||||
id: string,
|
||||
username: string,
|
||||
timeZone: string,
|
||||
emailVerified: Date | null,
|
||||
twoFactorEnabled: boolean | null
|
||||
twoFactorEnabled: boolean | null,
|
||||
identityProvider: string | null,
|
||||
lastActiveAt: Date | null,
|
||||
teamName: string | null,
|
||||
teamSlug: string | null,
|
||||
isOrganization: boolean | null,
|
||||
stripeCustomerId: string | null
|
||||
): Card => {
|
||||
return {
|
||||
key: "customer-card",
|
||||
timeToLiveSeconds: null,
|
||||
components: [
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: "Basic Information",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Name",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: name || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
@@ -42,31 +87,6 @@ export default (
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email Verified?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
|
||||
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
@@ -115,6 +135,24 @@ export default (
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
// Account Settings Section Title
|
||||
{
|
||||
componentText: {
|
||||
text: "Account Settings",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
@@ -139,6 +177,211 @@ export default (
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Identity Provider",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: identityProvider || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Last Active At",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: lastActiveAt ? dayjs(lastActiveAt).fromNow() : "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
// Team Information Section Title
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Information",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Name",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: teamName || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Slug",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: teamSlug || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Is Organization",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: isOrganization ? "Yes" : "No",
|
||||
badgeColor: isOrganization ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: "Security & Billing",
|
||||
textColor: "NORMAL",
|
||||
textSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Stripe Customer ID",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: stripeCustomerId || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email Verified?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
|
||||
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
|
||||
@@ -2,10 +2,17 @@ import type { Card } from "@pages/api/plain";
|
||||
import customerCardDisplay from "@pages/api/plain/customer-card-display";
|
||||
|
||||
export const cardExamples: ((
|
||||
name: string,
|
||||
email: string,
|
||||
id: string,
|
||||
username: string,
|
||||
timeZone: string,
|
||||
emailVerified: Date | null,
|
||||
twoFactorEnabled: boolean | null
|
||||
twoFactorEnabled: boolean | null,
|
||||
identityProvider: string | null,
|
||||
lastActiveAt: Date | null,
|
||||
teamName: string | null,
|
||||
teamSlug: string | null,
|
||||
isOrganization: boolean | null,
|
||||
stripeCustomerId: string | null
|
||||
) => Card)[] = [customerCardDisplay];
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps } from "@server/lib/auth/error/getStaticProps";
|
||||
import { getStaticProps } from "@server/lib/auth/error/getStaticProps";
|
||||
|
||||
import Error from "~/auth/error/error-view";
|
||||
|
||||
const Page = (props: PageProps) => <Error {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
export { getStaticProps };
|
||||
@@ -1,6 +0,0 @@
|
||||
export default function NewUserPage() {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.assign(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import Authorize from "~/auth/oauth2/authorize-view";
|
||||
|
||||
const Page = () => <Authorize />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import Authorize from "~/auth/platform/authorize-view";
|
||||
|
||||
const Page = () => <Authorize />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
@@ -1,14 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/auth/signin-view";
|
||||
import SignIn from "~/auth/signin-view";
|
||||
|
||||
const Page = (props: PageProps) => <SignIn {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
@@ -1,7 +1,20 @@
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from ".";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export { default, type PageProps } from ".";
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export type PageProps = UserTypePageProps | TeamTypePageProps;
|
||||
|
||||
export default function Page(props: PageProps) {
|
||||
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
}
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
|
||||
import TeamTypePage from "~/team/type-view";
|
||||
import UserTypePage from "~/users/views/users-type-public-view";
|
||||
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
|
||||
|
||||
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
export type PageProps = UserTypePageProps | TeamTypePageProps;
|
||||
|
||||
export default function Page(props: PageProps) {
|
||||
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
}
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
@@ -1,7 +1,14 @@
|
||||
import { getServerSideProps as _getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "./index";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export { default } from "./index";
|
||||
import TeamPage, { type PageProps } from "~/team/team-view";
|
||||
|
||||
const Page = (props: PageProps) => <TeamPage {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { getServerSideProps, default } from "@pages/team/[slug]";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user