Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77d1958c42 | ||
|
|
88f347f18c | ||
|
|
09cf8885cf | ||
|
|
f3d4eab951 | ||
|
|
866b6cc233 | ||
|
|
cafecad8ae | ||
|
|
0ea673e507 | ||
|
|
cf367c9340 | ||
|
|
d72e15649f | ||
|
|
89a3b77a1d | ||
|
|
38f6f71ae0 | ||
|
|
40caa82e55 | ||
|
|
b9aee3b18c | ||
|
|
3f8d2f2b38 | ||
|
|
c3731d9f17 | ||
|
|
6e4b2d8886 | ||
|
|
cf13c06c57 | ||
|
|
e309e72be3 | ||
|
|
b4d804de7c | ||
|
|
f501ef3269 | ||
|
|
79f1074f93 | ||
|
|
48aa6bf489 | ||
|
|
8ed2aacd9e | ||
|
|
dfc5d914fc | ||
|
|
0a11c45065 | ||
|
|
4ae5de5a3a | ||
|
|
e6959f3c7c | ||
|
|
67af8b4973 | ||
|
|
7794f2596e | ||
|
|
167cf6e352 | ||
|
|
3d778ba481 | ||
|
|
b8f0000b59 | ||
|
|
618ef63017 | ||
|
|
ae8e633716 | ||
|
|
4a23832388 | ||
|
|
02e486d757 | ||
|
|
af6e24f433 | ||
|
|
f9e24f51a7 | ||
|
|
9e9f815809 | ||
|
|
a91561f3c2 | ||
|
|
ff79064ca7 | ||
|
|
1a573e1619 | ||
|
|
511be7386c | ||
|
|
2a0991c259 | ||
|
|
2e454c9723 | ||
|
|
7e6d94c8e0 | ||
|
|
e18b790ccd | ||
|
|
0274417ad1 | ||
|
|
a8a3a93ace | ||
|
|
88bbab216e | ||
|
|
f74434a282 | ||
|
|
ed6f9d8252 | ||
|
|
a19ccab16a | ||
|
|
77369a13f1 | ||
|
|
8231356d2a | ||
|
|
469a942b5a | ||
|
|
0afe6cf5d1 | ||
|
|
75a161fe35 | ||
|
|
eb1ed107ba | ||
|
|
13e2e340ac | ||
|
|
0af62da16b | ||
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@calcom/atoms": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
This PR fixes `onBookerStateChange` prop not working
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@calcom/atoms": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add send & verify code flow to booker atom when email verification is turned on
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@calcom/atoms": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: added hideEventMetadata prop to the booker atom that controls the visibility of the event metadata sidebar. When `true`, hides the left sidebar containing event details like title, description, duration, and host information.
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
language: en-US
|
||||||
|
tone_instructions: ''
|
||||||
|
early_access: false
|
||||||
|
enable_free_tier: true
|
||||||
|
reviews:
|
||||||
|
profile: chill
|
||||||
|
request_changes_workflow: false
|
||||||
|
high_level_summary: false
|
||||||
|
high_level_summary_placeholder: '@coderabbitai summary'
|
||||||
|
high_level_summary_in_walkthrough: false
|
||||||
|
auto_title_placeholder: '@coderabbitai'
|
||||||
|
auto_title_instructions: ''
|
||||||
|
review_status: true
|
||||||
|
commit_status: true
|
||||||
|
fail_commit_status: false
|
||||||
|
collapse_walkthrough: false
|
||||||
|
changed_files_summary: false
|
||||||
|
sequence_diagrams: false
|
||||||
|
estimate_code_review_effort: false
|
||||||
|
assess_linked_issues: true
|
||||||
|
related_issues: false
|
||||||
|
related_prs: true
|
||||||
|
suggested_labels: false
|
||||||
|
auto_apply_labels: false
|
||||||
|
suggested_reviewers: false
|
||||||
|
auto_assign_reviewers: false
|
||||||
|
poem: false
|
||||||
|
labeling_instructions: []
|
||||||
|
path_filters:
|
||||||
|
- '!**/node_modules/**'
|
||||||
|
- '!**/dist/**'
|
||||||
|
- '!**/build/**'
|
||||||
|
- '!**/.next/**'
|
||||||
|
path_instructions:
|
||||||
|
- path: "**/*.{ts,tsx,js,jsx}"
|
||||||
|
instructions: "Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module."
|
||||||
|
abort_on_close: true
|
||||||
|
disable_cache: false
|
||||||
|
auto_review:
|
||||||
|
enabled: true
|
||||||
|
auto_incremental_review: true
|
||||||
|
ignore_title_keywords: []
|
||||||
|
labels: []
|
||||||
|
drafts: false
|
||||||
|
base_branches: []
|
||||||
|
finishing_touches:
|
||||||
|
docstrings:
|
||||||
|
enabled: true
|
||||||
|
unit_tests:
|
||||||
|
enabled: true
|
||||||
|
pre_merge_checks:
|
||||||
|
docstrings:
|
||||||
|
mode: warning
|
||||||
|
threshold: 80
|
||||||
|
title:
|
||||||
|
mode: warning
|
||||||
|
requirements: ''
|
||||||
|
description:
|
||||||
|
mode: warning
|
||||||
|
issue_assessment:
|
||||||
|
mode: warning
|
||||||
|
tools:
|
||||||
|
ast-grep:
|
||||||
|
rule_dirs: []
|
||||||
|
util_dirs: []
|
||||||
|
essential_rules: true
|
||||||
|
packages: []
|
||||||
|
shellcheck:
|
||||||
|
enabled: true
|
||||||
|
ruff:
|
||||||
|
enabled: true
|
||||||
|
markdownlint:
|
||||||
|
enabled: true
|
||||||
|
github-checks:
|
||||||
|
enabled: true
|
||||||
|
timeout_ms: 180000
|
||||||
|
languagetool:
|
||||||
|
enabled: true
|
||||||
|
enabled_rules: []
|
||||||
|
disabled_rules: []
|
||||||
|
enabled_categories: []
|
||||||
|
disabled_categories: []
|
||||||
|
enabled_only: false
|
||||||
|
level: default
|
||||||
|
biome:
|
||||||
|
enabled: true
|
||||||
|
hadolint:
|
||||||
|
enabled: true
|
||||||
|
swiftlint:
|
||||||
|
enabled: true
|
||||||
|
phpstan:
|
||||||
|
enabled: true
|
||||||
|
level: default
|
||||||
|
phpmd:
|
||||||
|
enabled: true
|
||||||
|
phpcs:
|
||||||
|
enabled: true
|
||||||
|
golangci-lint:
|
||||||
|
enabled: true
|
||||||
|
yamllint:
|
||||||
|
enabled: true
|
||||||
|
gitleaks:
|
||||||
|
enabled: true
|
||||||
|
checkov:
|
||||||
|
enabled: true
|
||||||
|
detekt:
|
||||||
|
enabled: true
|
||||||
|
eslint:
|
||||||
|
enabled: true
|
||||||
|
flake8:
|
||||||
|
enabled: true
|
||||||
|
rubocop:
|
||||||
|
enabled: true
|
||||||
|
buf:
|
||||||
|
enabled: true
|
||||||
|
regal:
|
||||||
|
enabled: true
|
||||||
|
actionlint:
|
||||||
|
enabled: true
|
||||||
|
pmd:
|
||||||
|
enabled: true
|
||||||
|
cppcheck:
|
||||||
|
enabled: true
|
||||||
|
semgrep:
|
||||||
|
enabled: true
|
||||||
|
circleci:
|
||||||
|
enabled: true
|
||||||
|
clippy:
|
||||||
|
enabled: true
|
||||||
|
sqlfluff:
|
||||||
|
enabled: true
|
||||||
|
prismaLint:
|
||||||
|
enabled: true
|
||||||
|
pylint:
|
||||||
|
enabled: true
|
||||||
|
oxc:
|
||||||
|
enabled: true
|
||||||
|
shopifyThemeCheck:
|
||||||
|
enabled: true
|
||||||
|
luacheck:
|
||||||
|
enabled: true
|
||||||
|
brakeman:
|
||||||
|
enabled: true
|
||||||
|
dotenvLint:
|
||||||
|
enabled: true
|
||||||
|
htmlhint:
|
||||||
|
enabled: true
|
||||||
|
checkmake:
|
||||||
|
enabled: true
|
||||||
|
osvScanner:
|
||||||
|
enabled: true
|
||||||
|
chat:
|
||||||
|
art: true
|
||||||
|
auto_reply: true
|
||||||
|
integrations:
|
||||||
|
jira:
|
||||||
|
usage: auto
|
||||||
|
linear:
|
||||||
|
usage: auto
|
||||||
|
knowledge_base:
|
||||||
|
opt_out: false
|
||||||
|
web_search:
|
||||||
|
enabled: true
|
||||||
|
code_guidelines:
|
||||||
|
enabled: true
|
||||||
|
filePatterns: []
|
||||||
|
learnings:
|
||||||
|
scope: auto
|
||||||
|
issues:
|
||||||
|
scope: auto
|
||||||
|
jira:
|
||||||
|
usage: auto
|
||||||
|
project_keys: []
|
||||||
|
linear:
|
||||||
|
usage: auto
|
||||||
|
team_keys: []
|
||||||
|
pull_requests:
|
||||||
|
scope: auto
|
||||||
|
mcp:
|
||||||
|
usage: auto
|
||||||
|
disabled_servers: []
|
||||||
|
code_generation:
|
||||||
|
docstrings:
|
||||||
|
language: en-US
|
||||||
|
path_instructions: []
|
||||||
|
unit_tests:
|
||||||
|
path_instructions: []
|
||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
|||||||
Under Vercel > Your API Project > Settings
|
Under Vercel > Your API Project > Settings
|
||||||
|
|
||||||
In General > Build & Development Settings
|
In General > Build & Development Settings
|
||||||
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
|
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
|
||||||
OUTPUT DIRECTORY: `apps/api/.next`
|
OUTPUT DIRECTORY: `apps/api/.next`
|
||||||
|
|
||||||
In Git > Ignored Build Step
|
In Git > Ignored Build Step
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"functions": {
|
|
||||||
"pages/api/slots/*.ts": {
|
|
||||||
"memory": 512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.320",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
+233
-3
@@ -15,11 +15,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
CreateBookingInput_2024_08_13,
|
CreateBookingInput_2024_08_13,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
@@ -50,9 +51,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
|
|
||||||
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
||||||
let platformAdmin: User;
|
let platformAdmin: User;
|
||||||
@@ -67,6 +68,10 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
let thirdManagedUser: CreateManagedUserData;
|
let thirdManagedUser: CreateManagedUserData;
|
||||||
|
|
||||||
let firstManagedUserEventTypeId: number;
|
let firstManagedUserEventTypeId: number;
|
||||||
|
let eventTypeRequiresConfirmationId: number;
|
||||||
|
|
||||||
|
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
|
||||||
|
let orgAdminManagedUser: CreateManagedUserData;
|
||||||
|
|
||||||
let firstManagedUserBookingsCount = 0;
|
let firstManagedUserBookingsCount = 0;
|
||||||
let secondManagedUserBookingsCount = 0;
|
let secondManagedUserBookingsCount = 0;
|
||||||
@@ -85,9 +90,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||||
|
|
||||||
@@ -95,6 +100,13 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
name: `oauth-client-users-organization-${randomString()}`,
|
name: `oauth-client-users-organization-${randomString()}`,
|
||||||
isPlatform: true,
|
isPlatform: true,
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
|
platformBilling: {
|
||||||
|
create: {
|
||||||
|
customerId: "cus_999",
|
||||||
|
plan: "SCALE",
|
||||||
|
subscriptionId: "sub_999",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
|
|
||||||
@@ -243,6 +255,59 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
thirdManagedUser = responseBody.data;
|
thirdManagedUser = responseBody.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should create org admin managed user`, async () => {
|
||||||
|
const requestBody: CreateManagedUserInput = {
|
||||||
|
email: orgAdminManagedUserEmail,
|
||||||
|
timeZone: managedUsersTimeZone,
|
||||||
|
weekStart: "Monday",
|
||||||
|
timeFormat: 24,
|
||||||
|
locale: Locales.FR,
|
||||||
|
name: "Org Admin Smith",
|
||||||
|
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: CreateManagedUserOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.user.email).toEqual(
|
||||||
|
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||||
|
);
|
||||||
|
expect(responseBody.data.accessToken).toBeDefined();
|
||||||
|
expect(responseBody.data.refreshToken).toBeDefined();
|
||||||
|
|
||||||
|
orgAdminManagedUser = responseBody.data;
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${organization.id}/memberships`)
|
||||||
|
.set("x-cal-client-id", oAuthClient.id)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send({
|
||||||
|
userId: orgAdminManagedUser.user.id,
|
||||||
|
role: "ADMIN",
|
||||||
|
accepted: true,
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create an event type requiring confirmation for first managed user", async () => {
|
||||||
|
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
firstManagedUser.user.id
|
||||||
|
);
|
||||||
|
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||||
|
});
|
||||||
|
|
||||||
describe("bookings using original emails", () => {
|
describe("bookings using original emails", () => {
|
||||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||||
const body: CreateBookingInput_2024_08_13 = {
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
@@ -543,10 +608,175 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("booking confirmation by org admin", () => {
|
||||||
|
it("should allow org admin managed user to confirm booking using access token", async () => {
|
||||||
|
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: firstManagedUser.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||||
|
title: "Booking requiring confirmation",
|
||||||
|
uid: `booking-requiring-confirmation-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeRequiresConfirmationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: secondManagedUser.user.name,
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
name: secondManagedUser.user.name!,
|
||||||
|
locale: secondManagedUser.user.locale,
|
||||||
|
timeZone: secondManagedUser.user.timeZone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||||
|
expect(bookingData.status).toEqual("accepted");
|
||||||
|
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||||
|
|
||||||
|
const confirmedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||||
|
expect(confirmedBooking?.status).toEqual("ACCEPTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow org admin managed user to reject booking using access token", async () => {
|
||||||
|
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: firstManagedUser.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 16, 0, 0)),
|
||||||
|
title: "Booking requiring rejection",
|
||||||
|
uid: `booking-requiring-rejection-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeRequiresConfirmationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: secondManagedUser.user.name,
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: secondManagedUserEmail,
|
||||||
|
name: secondManagedUser.user.name!,
|
||||||
|
locale: secondManagedUser.user.locale,
|
||||||
|
timeZone: secondManagedUser.user.timeZone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/decline`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||||
|
expect(bookingData.status).toEqual("rejected");
|
||||||
|
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||||
|
|
||||||
|
const rejectedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||||
|
expect(rejectedBooking?.status).toEqual("REJECTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unauthorized when org admin tries to confirm regular user's booking", async () => {
|
||||||
|
const regularUser = await userRepositoryFixture.create({
|
||||||
|
email: `managed-user-bookings-2024-08-13-regular-user-${randomString()}@api.com`,
|
||||||
|
name: "Regular User",
|
||||||
|
});
|
||||||
|
|
||||||
|
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
regularUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: regularUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 9, 17, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 9, 18, 0, 0)),
|
||||||
|
title: "Regular user booking requiring confirmation",
|
||||||
|
uid: `regular-user-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: regularUserEventType.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "PENDING",
|
||||||
|
responses: {
|
||||||
|
name: "External Attendee",
|
||||||
|
email: "external@example.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "external@example.com",
|
||||||
|
name: "External Attendee",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${regularUserBooking.uid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(401);
|
||||||
|
|
||||||
|
await userRepositoryFixture.delete(regularUser.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||||
|
await userRepositoryFixture.delete(orgAdminManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(platformAdmin.id);
|
await userRepositoryFixture.delete(platformAdmin.id);
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repo
|
|||||||
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
||||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
@@ -27,7 +26,6 @@ import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@cal
|
|||||||
import {
|
import {
|
||||||
CancelBookingInput_2024_08_13,
|
CancelBookingInput_2024_08_13,
|
||||||
CancelSeatedBookingInput_2024_08_13,
|
CancelSeatedBookingInput_2024_08_13,
|
||||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
|
||||||
CreateSeatedBookingOutput_2024_08_13,
|
CreateSeatedBookingOutput_2024_08_13,
|
||||||
GetBookingOutput_2024_08_13,
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
@@ -35,11 +33,9 @@ import {
|
|||||||
RescheduleSeatedBookingInput_2024_08_13,
|
RescheduleSeatedBookingInput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
|
||||||
|
|
||||||
describe("Seated bookings", () => {
|
describe("Seated bookings", () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let organization: Team;
|
let organization: Team;
|
||||||
@@ -48,7 +44,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
let schedulesService: SchedulesService_2024_04_15;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
||||||
@@ -58,7 +53,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
|
|
||||||
let seatedEventTypeId: number;
|
let seatedEventTypeId: number;
|
||||||
const maxRecurrenceCount = 3;
|
|
||||||
|
|
||||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||||
|
|
||||||
@@ -83,7 +77,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
@@ -190,6 +183,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -264,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid: createdSeatedBooking.seatUid,
|
seatUid: createdSeatedBooking.seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: createdSeatedBooking.attendees[0].name,
|
name: createdSeatedBooking.attendees[0].name,
|
||||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -278,6 +273,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -517,6 +513,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
|
|||||||
+76
-1
@@ -21,7 +21,12 @@ import { randomString } from "test/utils/randomString";
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||||
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
import {
|
||||||
|
CreateBookingInput_2024_08_13,
|
||||||
|
BookingOutput_2024_08_13,
|
||||||
|
CreateRecurringBookingInput_2024_08_13,
|
||||||
|
RecurringBookingOutput_2024_08_13,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
@@ -44,6 +49,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
let normalEventType: EventType;
|
let normalEventType: EventType;
|
||||||
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
|
let variableLengthRecurringEventType: EventType;
|
||||||
|
const VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG = `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
@@ -105,6 +112,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
variableLengthRecurringEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`,
|
||||||
|
slug: VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG,
|
||||||
|
length: 15,
|
||||||
|
metadata: { multipleDuration: [15, 30, 60] },
|
||||||
|
recurringEvent: { freq: 2, count: 3, interval: 1 }, // Weekly, 3 times, every 1 week
|
||||||
|
},
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
bootstrap(app as NestExpressApplication);
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
@@ -246,6 +264,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("create recurring bookings", () => {
|
||||||
|
it("should create recurring bookings with custom lengthInMinutes", async () => {
|
||||||
|
const lengthInMinutes = 30;
|
||||||
|
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 1, 1, 14, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: variableLengthRecurringEventType.id,
|
||||||
|
lengthInMinutes,
|
||||||
|
recurrenceCount: 2,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Recurring",
|
||||||
|
email: "mr_recurring@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: { status: string; data: RecurringBookingOutput_2024_08_13[] } = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||||
|
expect(responseBody.data).toHaveLength(2);
|
||||||
|
|
||||||
|
const firstBooking = responseBody.data[0];
|
||||||
|
const secondBooking = responseBody.data[1];
|
||||||
|
expect(firstBooking.duration).toEqual(lengthInMinutes);
|
||||||
|
expect(secondBooking.duration).toEqual(lengthInMinutes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject recurring booking with invalid lengthInMinutes", async () => {
|
||||||
|
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 3, 1, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: variableLengthRecurringEventType.id,
|
||||||
|
lengthInMinutes: 45, // Not in allowed [15, 30, 60]
|
||||||
|
recurrenceCount: 2,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Invalid",
|
||||||
|
email: "mr_invalid@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
@@ -420,6 +420,9 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
|
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.validateBookingLengthInMinutes(inputBooking, eventType);
|
||||||
|
const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length;
|
||||||
|
|
||||||
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
||||||
const repeatsEvery = occurrence.interval;
|
const repeatsEvery = occurrence.interval;
|
||||||
|
|
||||||
@@ -456,7 +459,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||||
|
|
||||||
for (let i = 0; i < repeatsTimes; i++) {
|
for (let i = 0; i < repeatsTimes; i++) {
|
||||||
const endTime = startTime.plus({ minutes: eventType.length });
|
const endTime = startTime.plus({ minutes: lengthInMinutes });
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
start: startTime.toISO(),
|
start: startTime.toISO(),
|
||||||
@@ -702,6 +705,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||||
afterCreatedDate: queryParams.afterCreatedAt,
|
afterCreatedDate: queryParams.afterCreatedAt,
|
||||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||||
|
bookingUid: queryParams.bookingUid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|||||||
+45
@@ -2147,6 +2147,51 @@ describe("Event types Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("EventType Hidden Property", () => {
|
||||||
|
let createdEventTypeId: number;
|
||||||
|
|
||||||
|
it("should create an event type with hidden=true", async () => {
|
||||||
|
const createPayload = {
|
||||||
|
title: "Hidden Event",
|
||||||
|
slug: "hidden-event",
|
||||||
|
lengthInMinutes: 30,
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(createPayload)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(createPayload.title);
|
||||||
|
expect(createdEventType.slug).toEqual(createPayload.slug);
|
||||||
|
expect(createdEventType.lengthInMinutes).toEqual(createPayload.lengthInMinutes);
|
||||||
|
expect(createdEventType.hidden).toBe(true);
|
||||||
|
|
||||||
|
createdEventTypeId = createdEventType.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update the hidden property to false", async () => {
|
||||||
|
const updatePayload = {
|
||||||
|
hidden: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${createdEventTypeId}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(updatePayload)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
+expect(updatedEventType.hidden).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
+3
@@ -89,6 +89,7 @@ type Input = Pick<
|
|||||||
| "hideCalendarEventDetails"
|
| "hideCalendarEventDetails"
|
||||||
| "hideOrganizerEmail"
|
| "hideOrganizerEmail"
|
||||||
| "calVideoSettings"
|
| "calVideoSettings"
|
||||||
|
| "hidden"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -127,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
|
hidden,
|
||||||
} = databaseEventType;
|
} = databaseEventType;
|
||||||
|
|
||||||
const locations = this.transformLocations(databaseEventType.locations);
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
@@ -203,6 +205,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
|
hidden,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,4 +229,19 @@ export class SchedulesRepository_2024_06_11 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSchedulesByUserIds(userIds: number[], skip: number, take: number) {
|
||||||
|
return this.dbRead.prisma.schedule.findMany({
|
||||||
|
where: {
|
||||||
|
userId: {
|
||||||
|
in: userIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
availability: true,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
@@ -12,6 +13,7 @@ import { BusyTimesService } from "@/lib/services/busy-times.service";
|
|||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -31,6 +33,7 @@ import { Module } from "@nestjs/common";
|
|||||||
PrismaTeamRepository,
|
PrismaTeamRepository,
|
||||||
RedisService,
|
RedisService,
|
||||||
PrismaFeaturesRepository,
|
PrismaFeaturesRepository,
|
||||||
|
PrismaMembershipRepository,
|
||||||
CheckBookingLimitsService,
|
CheckBookingLimitsService,
|
||||||
CacheService,
|
CacheService,
|
||||||
AvailableSlotsService,
|
AvailableSlotsService,
|
||||||
@@ -38,6 +41,7 @@ import { Module } from "@nestjs/common";
|
|||||||
BusyTimesService,
|
BusyTimesService,
|
||||||
FilterHostsService,
|
FilterHostsService,
|
||||||
QualifiedHostsService,
|
QualifiedHostsService,
|
||||||
|
NoSlotsNotificationService,
|
||||||
],
|
],
|
||||||
exports: [AvailableSlotsService],
|
exports: [AvailableSlotsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaMembershipRepository as PrismaMembershipRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaMembershipRepository extends PrismaMembershipRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
|
|||||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
@@ -35,7 +36,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
checkBookingLimitsService: CheckBookingLimitsService,
|
checkBookingLimitsService: CheckBookingLimitsService,
|
||||||
cacheService: CacheService,
|
cacheService: CacheService,
|
||||||
userAvailabilityService: UserAvailabilityService,
|
userAvailabilityService: UserAvailabilityService,
|
||||||
busyTimesService: BusyTimesService
|
busyTimesService: BusyTimesService,
|
||||||
|
noSlotsNotificationService: NoSlotsNotificationService
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
oooRepo: oooRepoDependency,
|
oooRepo: oooRepoDependency,
|
||||||
@@ -53,6 +55,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
busyTimesService,
|
busyTimesService,
|
||||||
qualifiedHostsService,
|
qualifiedHostsService,
|
||||||
featuresRepo: featuresRepository,
|
featuresRepo: featuresRepository,
|
||||||
|
noSlotsNotificationService,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@calcom/platform-libraries/slots";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NoSlotsNotificationService extends BaseNoSlotsNotificationService {
|
||||||
|
constructor(
|
||||||
|
teamRepository: PrismaTeamRepository,
|
||||||
|
membershipRepository: PrismaMembershipRepository,
|
||||||
|
redisService: RedisService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
teamRepo: teamRepository,
|
||||||
|
membershipRepo: membershipRepository,
|
||||||
|
redisClient: redisService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/ato
|
|||||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||||
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
||||||
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
||||||
|
import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller";
|
||||||
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
||||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||||
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
||||||
|
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
@@ -32,6 +34,7 @@ import { Module } from "@nestjs/common";
|
|||||||
AtomsRepository,
|
AtomsRepository,
|
||||||
UsersService,
|
UsersService,
|
||||||
SchedulesAtomsService,
|
SchedulesAtomsService,
|
||||||
|
VerificationAtomsService,
|
||||||
RedisService,
|
RedisService,
|
||||||
],
|
],
|
||||||
exports: [EventTypesAtomService],
|
exports: [EventTypesAtomService],
|
||||||
@@ -40,6 +43,7 @@ import { Module } from "@nestjs/common";
|
|||||||
AtomsEventTypesController,
|
AtomsEventTypesController,
|
||||||
AtomsConferencingAppsController,
|
AtomsConferencingAppsController,
|
||||||
AtomsSchedulesController,
|
AtomsSchedulesController,
|
||||||
|
AtomsVerificationController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AtomsModule {}
|
export class AtomsModule {}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||||
|
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||||
|
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||||
|
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
|
||||||
|
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
|
||||||
|
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
UseGuards,
|
||||||
|
Version,
|
||||||
|
VERSION_NEUTRAL,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/atoms",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Atoms - verification endpoints for atoms")
|
||||||
|
@DocsExcludeController(true)
|
||||||
|
export class AtomsVerificationController {
|
||||||
|
constructor(private readonly verificationService: VerificationAtomsService) {}
|
||||||
|
|
||||||
|
@Post("/verification/email/send-code")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "atoms_verification_email_send_code" })
|
||||||
|
async sendEmailVerificationCode(
|
||||||
|
@Body() body: SendVerificationEmailInput
|
||||||
|
): Promise<SendVerificationEmailOutput> {
|
||||||
|
const result = await this.verificationService.sendEmailVerificationCode({
|
||||||
|
email: body.email,
|
||||||
|
username: body.username,
|
||||||
|
language: body.language,
|
||||||
|
isVerifyingEmail: body.isVerifyingEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { sent: result.ok && !result.skipped },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/verification/email/check")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async checkEmailVerificationRequired(
|
||||||
|
@Query() query: CheckEmailVerificationRequiredParams
|
||||||
|
): Promise<ApiResponse<boolean>> {
|
||||||
|
const required = await this.verificationService.checkEmailVerificationRequired({
|
||||||
|
email: query.email,
|
||||||
|
userSessionEmail: query.userSessionEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: required,
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/verification/email/verify-code")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
|
||||||
|
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
|
||||||
|
email: body.email,
|
||||||
|
code: body.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { verified },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/verification/email/verify-code-authenticated")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmailCodeAuthenticated(
|
||||||
|
@Body() body: VerifyEmailCodeInput,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<VerifyEmailCodeOutput> {
|
||||||
|
const verified = await this.verificationService.verifyEmailCodeAuthenticated(user, {
|
||||||
|
email: body.email,
|
||||||
|
code: body.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { verified },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CheckEmailVerificationRequiredParams {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "user@example.com" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userSessionEmail?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsOptional, IsString, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
export class SendVerificationEmailInput {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "johndoe" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "en" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isVerifyingEmail?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class VerifyEmailCodeInput {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "123456" })
|
||||||
|
@IsString()
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class SendVerificationEmailData {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@Expose()
|
||||||
|
readonly sent!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendVerificationEmailOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => SendVerificationEmailData)
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ type: SendVerificationEmailData })
|
||||||
|
readonly data!: SendVerificationEmailData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class VerifyEmailCodeData {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@Expose()
|
||||||
|
readonly verified!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VerifyEmailCodeOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => VerifyEmailCodeData)
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ type: VerifyEmailCodeData })
|
||||||
|
readonly data!: VerifyEmailCodeData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||||
|
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||||
|
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
verifyCodeUnAuthenticated,
|
||||||
|
verifyCodeAuthenticated,
|
||||||
|
sendEmailVerificationByCode,
|
||||||
|
checkEmailVerificationRequired,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VerificationAtomsService {
|
||||||
|
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
||||||
|
return await checkEmailVerificationRequired(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmailCodeUnAuthenticated(input: VerifyEmailCodeInput) {
|
||||||
|
try {
|
||||||
|
return await verifyCodeUnAuthenticated(input);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "invalid_code") {
|
||||||
|
throw new BadRequestException("Invalid verification code");
|
||||||
|
}
|
||||||
|
if (error.message === "BAD_REQUEST") {
|
||||||
|
throw new BadRequestException("Email and code are required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BadRequestException("Verification failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmailCodeAuthenticated(user: UserWithProfile, input: VerifyEmailCodeInput) {
|
||||||
|
try {
|
||||||
|
return await verifyCodeAuthenticated({
|
||||||
|
user,
|
||||||
|
email: input.email,
|
||||||
|
code: input.code,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "invalid_code") {
|
||||||
|
throw new BadRequestException("Invalid verification code");
|
||||||
|
}
|
||||||
|
if (error.message === "BAD_REQUEST") {
|
||||||
|
throw new BadRequestException("Email, code, and user ID are required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UnauthorizedException("Verification failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEmailVerificationCode(input: SendVerificationEmailInput) {
|
||||||
|
return await sendEmailVerificationByCode({
|
||||||
|
email: input.email,
|
||||||
|
username: input.username,
|
||||||
|
language: input.language,
|
||||||
|
isVerifyingEmail: input.isVerifyingEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/
|
|||||||
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
||||||
import { RouterModule } from "@/modules/router/router.module";
|
import { RouterModule } from "@/modules/router/router.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
|
import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module";
|
||||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||||
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||||
@@ -38,6 +39,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
|||||||
OrganizationsRoutingFormsModule,
|
OrganizationsRoutingFormsModule,
|
||||||
VerifiedResourcesModule,
|
VerifiedResourcesModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
TeamsSchedulesModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class EndpointsModule implements NestModule {
|
export class EndpointsModule implements NestModule {
|
||||||
|
|||||||
+128
-1
@@ -66,6 +66,8 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
let orgEventTypeId2: number;
|
let orgEventTypeId2: number;
|
||||||
let nonOrgEventTypeId: number;
|
let nonOrgEventTypeId: number;
|
||||||
|
|
||||||
|
let collectiveOrgBookingUid: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
orgUserEmail2,
|
orgUserEmail2,
|
||||||
@@ -91,12 +93,17 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
|
||||||
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
|
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
|
||||||
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
team1 = await teamRepositoryFixture.create({
|
team1 = await teamRepositoryFixture.create({
|
||||||
name: "team orgs booking 1",
|
name: "team orgs booking 1",
|
||||||
isOrganization: false,
|
isOrganization: false,
|
||||||
parent: { connect: { id: organization.id } },
|
parent: { connect: { id: organization.id } },
|
||||||
|
createdByOAuthClient: {
|
||||||
|
connect: {
|
||||||
|
id: oAuthClient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
|
||||||
|
|
||||||
nonOrgUser1 = await userRepositoryFixture.create({
|
nonOrgUser1 = await userRepositoryFixture.create({
|
||||||
email: nonOrgUserEmail1,
|
email: nonOrgUserEmail1,
|
||||||
@@ -315,6 +322,7 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
|
|
||||||
if (responseDataIsBooking(responseBody.data)) {
|
if (responseDataIsBooking(responseBody.data)) {
|
||||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
collectiveOrgBookingUid = data.uid;
|
||||||
expect(data.id).toBeDefined();
|
expect(data.id).toBeDefined();
|
||||||
expect(data.uid).toBeDefined();
|
expect(data.uid).toBeDefined();
|
||||||
expect(data.hosts.length).toEqual(1);
|
expect(data.hosts.length).toEqual(1);
|
||||||
@@ -669,6 +677,125 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("get by bookingUid param", () => {
|
||||||
|
it("should get a specific booking by bookingUid query param", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${collectiveOrgBookingUid}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
|
||||||
|
const data: (
|
||||||
|
| BookingOutput_2024_08_13
|
||||||
|
| RecurringBookingOutput_2024_08_13
|
||||||
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
|
)[] = responseBody.data;
|
||||||
|
expect(data.length).toEqual(1);
|
||||||
|
expect(responseDataIsBooking(data[0])).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsBooking(data[0])) {
|
||||||
|
const booking: BookingOutput_2024_08_13 = data[0];
|
||||||
|
expect(booking.uid).toEqual(collectiveOrgBookingUid);
|
||||||
|
expect(booking.eventTypeId).toEqual(orgEventTypeId);
|
||||||
|
expect(booking.status).toEqual("accepted");
|
||||||
|
expect(booking.attendees.length).toEqual(2);
|
||||||
|
expect(booking.attendees[0].name).toEqual("alice");
|
||||||
|
expect(booking.attendees[0].email).toEqual("alice@gmail.com");
|
||||||
|
} else {
|
||||||
|
throw new Error("Expected single booking but received different response type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for non-existent booking UID query param", async () => {
|
||||||
|
const nonExistentUid = "non-existent-booking-uid";
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${nonExistentUid}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for booking UID not belonging to the organization", async () => {
|
||||||
|
const regularUser = await userRepositoryFixture.create({
|
||||||
|
email: `org-bookings-regular-user-${Math.floor(Math.random() * 10000)}@api.com`,
|
||||||
|
name: "Regular User",
|
||||||
|
});
|
||||||
|
|
||||||
|
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
slug: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
length: 60,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
},
|
||||||
|
regularUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: regularUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2030, 0, 15, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2030, 0, 15, 14, 0, 0)),
|
||||||
|
title: "Regular user booking",
|
||||||
|
uid: `regular-user-booking-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: regularUserEventType.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/regular-user",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "ACCEPTED",
|
||||||
|
responses: {
|
||||||
|
name: "Regular Attendee",
|
||||||
|
email: "regular@example.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "regular@example.com",
|
||||||
|
name: "Regular Attendee",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${regularUserBooking.uid}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data.length).toEqual(0);
|
||||||
|
|
||||||
|
await userRepositoryFixture.delete(regularUser.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createOAuthClient(organizationId: number) {
|
async function createOAuthClient(organizationId: number) {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type Input = Pick<
|
|||||||
| "hideOrganizerEmail"
|
| "hideOrganizerEmail"
|
||||||
| "team"
|
| "team"
|
||||||
| "calVideoSettings"
|
| "calVideoSettings"
|
||||||
|
| "hidden"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
|||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||||
|
|
||||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
// Get the membership first to get the userId
|
||||||
|
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({
|
||||||
|
teamIds: [organizationId],
|
||||||
|
userIds: [membership.userId],
|
||||||
|
isOrg: true,
|
||||||
|
});
|
||||||
|
|
||||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||||
|
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||||
|
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||||
|
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||||
@@ -18,13 +22,10 @@ import { OrganizationAttributesService } from "@/modules/organizations/attribute
|
|||||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||||
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
||||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||||
import { OrganizationsConferencingController } from "@/modules/organizations/conferencing/organizations-conferencing.controller";
|
|
||||||
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
||||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||||
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
||||||
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
|
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
|
||||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
|
||||||
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
|
||||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
||||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
||||||
@@ -38,7 +39,6 @@ import { OrganizationsMembershipOutputService } from "@/modules/organizations/me
|
|||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
|
||||||
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
||||||
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
|
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
|
||||||
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
|
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
|
||||||
@@ -65,6 +65,7 @@ import { RedisModule } from "@/modules/redis/redis.module";
|
|||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||||
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -102,7 +103,6 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsTeamsService,
|
OrganizationsTeamsService,
|
||||||
MembershipsRepository,
|
MembershipsRepository,
|
||||||
OrganizationsSchedulesService,
|
OrganizationsSchedulesService,
|
||||||
OrganizationSchedulesRepository,
|
|
||||||
OrganizationsUsersRepository,
|
OrganizationsUsersRepository,
|
||||||
OrganizationsUsersService,
|
OrganizationsUsersService,
|
||||||
EmailService,
|
EmailService,
|
||||||
@@ -144,6 +144,9 @@ import { Module } from "@nestjs/common";
|
|||||||
TeamWorkflowsService,
|
TeamWorkflowsService,
|
||||||
WorkflowsInputService,
|
WorkflowsInputService,
|
||||||
WorkflowsOutputService,
|
WorkflowsOutputService,
|
||||||
|
TeamsSchedulesService,
|
||||||
|
SchedulesService_2024_06_11,
|
||||||
|
InputSchedulesService_2024_06_11,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
OrganizationsService,
|
OrganizationsService,
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsSchedulesService {
|
export class OrganizationsSchedulesService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly organizationSchedulesService: OrganizationSchedulesRepository,
|
|
||||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
||||||
|
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||||
private readonly usersRepository: UsersRepository
|
private readonly usersRepository: UsersRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export class OrganizationsSchedulesService {
|
|||||||
const users = await this.usersRepository.getOrganizationUsers(organizationId);
|
const users = await this.usersRepository.getOrganizationUsers(organizationId);
|
||||||
const usersIds = users.map((user) => user.id);
|
const usersIds = users.map((user) => user.id);
|
||||||
|
|
||||||
const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take);
|
const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take);
|
||||||
|
|
||||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||||
|
|
||||||
|
|||||||
-4
@@ -20,7 +20,6 @@ import {
|
|||||||
OrgTeamMembershipOutputResponseDto,
|
OrgTeamMembershipOutputResponseDto,
|
||||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||||
private teamsEventTypesService: TeamsEventTypesService,
|
|
||||||
private readonly organizationsRepository: OrganizationsRepository
|
private readonly organizationsRepository: OrganizationsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
|||||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsTeamsMembershipsService {
|
export class OrganizationsTeamsMembershipsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
|
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
teamId,
|
teamId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -14,11 +14,12 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
|||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
||||||
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
|
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||||
@@ -29,7 +30,31 @@ import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
|||||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
export class OrganizationsTeamsSchedulesController {
|
export class OrganizationsTeamsSchedulesController {
|
||||||
constructor(private schedulesService: SchedulesService_2024_06_11) {}
|
constructor(
|
||||||
|
private schedulesService: SchedulesService_2024_06_11,
|
||||||
|
|
||||||
|
private teamsSchedulesService: TeamsSchedulesService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(IsTeamInOrg)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/schedules")
|
||||||
|
@DocsTags("Orgs / Teams / Schedules")
|
||||||
|
@ApiOperation({ summary: "Get all team member schedules" })
|
||||||
|
async getTeamSchedules(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() queryParams: SkipTakePagination
|
||||||
|
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
|
const { skip, take } = queryParams;
|
||||||
|
|
||||||
|
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@PlatformPlan("ESSENTIALS")
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
|||||||
+39
@@ -18,6 +18,7 @@ import { randomString } from "test/utils/randomString";
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
import { Team, Schedule } from "@calcom/prisma/client";
|
import { Team, Schedule } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
|||||||
let org: Team;
|
let org: Team;
|
||||||
let orgTeam: Team;
|
let orgTeam: Team;
|
||||||
let nonOrgTeam: Team;
|
let nonOrgTeam: Team;
|
||||||
|
|
||||||
|
let userSchedule: Schedule;
|
||||||
let user2Schedule: Schedule;
|
let user2Schedule: Schedule;
|
||||||
|
|
||||||
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
|
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
|
||||||
@@ -73,6 +76,16 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
|||||||
username: userEmail2,
|
username: userEmail2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
userSchedule = await scheduleRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: `organizations-teams-schedules-user-schedule-${randomString()}`,
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
});
|
||||||
|
|
||||||
user2Schedule = await scheduleRepositoryFixture.create({
|
user2Schedule = await scheduleRepositoryFixture.create({
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
@@ -214,6 +227,32 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should get all the schedules of members in a team", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||||
|
expect(responseBody.data.length).toEqual(2);
|
||||||
|
|
||||||
|
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||||
|
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||||
|
|
||||||
|
expect(userOneSchedule).toBeDefined();
|
||||||
|
expect(userTwoSchedule).toBeDefined();
|
||||||
|
|
||||||
|
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||||
|
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||||
|
|
||||||
|
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||||
|
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||||
|
|||||||
+21
-1
@@ -12,7 +12,12 @@ import {
|
|||||||
EMAIL,
|
EMAIL,
|
||||||
WorkflowEmailAttendeeStepDto,
|
WorkflowEmailAttendeeStepDto,
|
||||||
} from "@/modules/workflows/inputs/workflow-step.input";
|
} from "@/modules/workflows/inputs/workflow-step.input";
|
||||||
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
|
import {
|
||||||
|
BEFORE_EVENT,
|
||||||
|
DAY,
|
||||||
|
OnAfterEventTriggerDto,
|
||||||
|
OnBeforeEventTriggerDto,
|
||||||
|
} from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||||
// Adjust path if needed
|
// Adjust path if needed
|
||||||
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
"+37255555555"
|
"+37255555555"
|
||||||
);
|
);
|
||||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
||||||
|
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
|
|
||||||
createdWorkflowId = responseBody.data.id;
|
createdWorkflowId = responseBody.data.id;
|
||||||
createdWorkflow = responseBody.data;
|
createdWorkflow = responseBody.data;
|
||||||
});
|
});
|
||||||
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
expect(step1).toBeDefined();
|
expect(step1).toBeDefined();
|
||||||
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
|
trigger: {
|
||||||
|
type: "afterEvent",
|
||||||
|
offset: {
|
||||||
|
unit: "minute",
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
||||||
};
|
};
|
||||||
expect(createdWorkflowId).toBeDefined();
|
expect(createdWorkflowId).toBeDefined();
|
||||||
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
||||||
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
||||||
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
||||||
|
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
|
|||||||
|
|
||||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
|
||||||
try {
|
|
||||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
|
||||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error("Could not remove user from all team event-types.", {
|
|
||||||
error: err,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
|||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
|||||||
export class TeamsMembershipsController {
|
export class TeamsMembershipsController {
|
||||||
private logger = new Logger("TeamsMembershipsController");
|
private logger = new Logger("TeamsMembershipsController");
|
||||||
|
|
||||||
constructor(
|
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||||
private teamsMembershipsService: TeamsMembershipsService,
|
|
||||||
private teamsEventTypesService: TeamsEventTypesService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
|||||||
membershipId,
|
membershipId,
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||||
try {
|
try {
|
||||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
|||||||
): Promise<DeleteTeamMembershipOutput> {
|
): Promise<DeleteTeamMembershipOutput> {
|
||||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
|||||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamsMembershipsService {
|
export class TeamsMembershipsService {
|
||||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
teamId,
|
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||||
membershipId
|
|
||||||
);
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
|
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/teams/:teamId/schedules",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||||
|
@DocsTags("Teams / Schedules")
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
export class TeamsSchedulesController {
|
||||||
|
constructor(private teamsSchedulesService: TeamsSchedulesService) {}
|
||||||
|
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get all team member schedules",
|
||||||
|
})
|
||||||
|
async getTeamSchedules(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() queryParams: SkipTakePagination
|
||||||
|
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
|
const { skip, take } = queryParams;
|
||||||
|
|
||||||
|
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { randomString } from "test/utils/randomString";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
import { Team, Schedule } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Teams Schedules Endpoints", () => {
|
||||||
|
describe("User Authentication - User is Team Admin", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
|
||||||
|
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||||
|
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
let nonOrgTeam: Team;
|
||||||
|
|
||||||
|
let userSchedule: Schedule;
|
||||||
|
let user2Schedule: Schedule;
|
||||||
|
|
||||||
|
const userEmail = `teams-schedules-admin-${randomString()}@api.com`;
|
||||||
|
const userEmail2 = `teams-schedules-member-${randomString()}@api.com`;
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
let user2: User;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
});
|
||||||
|
user2 = await userRepositoryFixture.create({
|
||||||
|
email: userEmail2,
|
||||||
|
username: userEmail2,
|
||||||
|
});
|
||||||
|
|
||||||
|
userSchedule = await scheduleRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: `teams-schedules-user-schedule-${randomString()}`,
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
});
|
||||||
|
|
||||||
|
user2Schedule = await scheduleRepositoryFixture.create({
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: `teams-schedules-user2-schedule-${randomString()}`,
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
});
|
||||||
|
|
||||||
|
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||||
|
name: `teams-schedules-non-org-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: nonOrgTeam.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "MEMBER",
|
||||||
|
user: { connect: { id: user2.id } },
|
||||||
|
team: { connect: { id: nonOrgTeam.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get all the schedules of members in a team", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${nonOrgTeam.id}/schedules`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||||
|
expect(responseBody.data.length).toEqual(2);
|
||||||
|
|
||||||
|
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||||
|
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||||
|
|
||||||
|
expect(userOneSchedule).toBeDefined();
|
||||||
|
expect(userTwoSchedule).toBeDefined();
|
||||||
|
|
||||||
|
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||||
|
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||||
|
|
||||||
|
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||||
|
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||||
|
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||||
|
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TeamsSchedulesService {
|
||||||
|
constructor(
|
||||||
|
private readonly teamsRepository: TeamsRepository,
|
||||||
|
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||||
|
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getTeamSchedules(teamId: number, skip = 0, take = 250) {
|
||||||
|
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
|
||||||
|
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
|
||||||
|
|
||||||
|
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||||
|
|
||||||
|
for (const schedule of schedules) {
|
||||||
|
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseSchedules;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
|
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
|
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||||
|
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
|
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||||
|
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
|
import { TeamsSchedulesController } from "@/modules/teams/schedules/controllers/teams-schedules.controller";
|
||||||
|
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||||
|
import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller";
|
||||||
|
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
|
||||||
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, MembershipsModule, RedisModule],
|
||||||
|
providers: [
|
||||||
|
TeamsRepository,
|
||||||
|
TeamsService,
|
||||||
|
TeamsMembershipsRepository,
|
||||||
|
TeamsMembershipsService,
|
||||||
|
OutputSchedulesService_2024_06_11,
|
||||||
|
OrganizationSchedulesRepository,
|
||||||
|
StripeService,
|
||||||
|
UsersRepository,
|
||||||
|
AppsRepository,
|
||||||
|
CredentialsRepository,
|
||||||
|
OrganizationsRepository,
|
||||||
|
TeamsSchedulesService,
|
||||||
|
OrganizationsTeamsRepository,
|
||||||
|
SchedulesRepository_2024_06_11,
|
||||||
|
],
|
||||||
|
controllers: [TeamsController, TeamsSchedulesController],
|
||||||
|
exports: [TeamsRepository],
|
||||||
|
})
|
||||||
|
export class TeamsSchedulesModule {}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsNumber, ValidateNested } from "class-validator";
|
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
export const BEFORE_EVENT = "beforeEvent";
|
export const BEFORE_EVENT = "beforeEvent";
|
||||||
export const EVENT_CANCELLED = "eventCancelled";
|
export const EVENT_CANCELLED = "eventCancelled";
|
||||||
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
|
|||||||
value!: number;
|
value!: number;
|
||||||
|
|
||||||
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
||||||
|
@IsString()
|
||||||
|
@IsIn(TIME_UNITS)
|
||||||
unit!: TimeUnitType;
|
unit!: TimeUnitType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([WORKFLOW_TRIGGER_TYPES])
|
||||||
type!: WorkflowTriggerType;
|
type!: WorkflowTriggerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([NEW_EVENT])
|
||||||
type: typeof NEW_EVENT = NEW_EVENT;
|
type: typeof NEW_EVENT = NEW_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([RESCHEDULE_EVENT])
|
||||||
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
||||||
}
|
}
|
||||||
export class OnCancelTriggerDto {
|
export class OnCancelTriggerDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([EVENT_CANCELLED])
|
||||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: BEFORE_EVENT,
|
example: BEFORE_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([BEFORE_EVENT])
|
||||||
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_EVENT,
|
example: AFTER_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_EVENT])
|
||||||
type: typeof AFTER_EVENT = AFTER_EVENT;
|
type: typeof AFTER_EVENT = AFTER_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
|||||||
async delete(eventTypeId: EventType["id"]) {
|
async delete(eventTypeId: EventType["id"]) {
|
||||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(eventTypeId: EventType["id"]) {
|
||||||
|
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
|||||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(membershipId: Membership["id"]) {
|
||||||
|
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CustomI18nProvider } from "app/CustomI18nProvider";
|
import { CustomI18nProvider } from "app/CustomI18nProvider";
|
||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||||
import type { PageProps, SearchParams } from "app/_types";
|
import type { PageProps, Params, SearchParams } from "app/_types";
|
||||||
import { generateMeetingMetadata } from "app/_utils";
|
import { generateMeetingMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
|
|
||||||
@@ -15,20 +15,34 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
|||||||
import LegacyPage from "~/team/type-view";
|
import LegacyPage from "~/team/type-view";
|
||||||
import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
||||||
|
|
||||||
import CachedTeamBooker, { generateMetadata as generateCachedMetadata } from "./pageWithCachedData";
|
import CachedTeamBooker, {
|
||||||
|
generateMetadata as generateCachedMetadata,
|
||||||
|
getOrgContext,
|
||||||
|
} from "./pageWithCachedData";
|
||||||
|
import { getTeamId } from "./queries";
|
||||||
|
|
||||||
|
async function isCachedTeamBookingEnabled(params: Params, searchParams: SearchParams): Promise<boolean> {
|
||||||
|
if (searchParams.experimentalTeamBookingPageCache !== "true") return false;
|
||||||
|
|
||||||
|
const { teamSlug, currentOrgDomain, isValidOrgDomain } = await getOrgContext(params);
|
||||||
|
const orgSlug = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
const teamId = await getTeamId(teamSlug, orgSlug);
|
||||||
|
|
||||||
|
if (!teamId) return false;
|
||||||
|
|
||||||
async function isCachedTeamBookingEnabled(searchParams: SearchParams): Promise<boolean> {
|
|
||||||
const featuresRepository = new FeaturesRepository(prisma);
|
const featuresRepository = new FeaturesRepository(prisma);
|
||||||
const isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
|
const isTeamFeatureEnabled = await featuresRepository.checkIfTeamHasFeature(
|
||||||
|
teamId,
|
||||||
"team-booking-page-cache"
|
"team-booking-page-cache"
|
||||||
);
|
);
|
||||||
return isGloballyEnabled && searchParams.experimentalTeamBookingPageCache === "true";
|
return isTeamFeatureEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
if (await isCachedTeamBookingEnabled(await params, await searchParams)) {
|
||||||
return await generateCachedMetadata({ params, searchParams });
|
return await generateCachedMetadata({ params, searchParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||||
const props = await getData(legacyCtx);
|
const props = await getData(legacyCtx);
|
||||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||||
@@ -68,7 +82,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
|||||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
if (await isCachedTeamBookingEnabled(await params, await searchParams)) {
|
||||||
return await CachedTeamBooker({ params, searchParams });
|
return await CachedTeamBooker({ params, searchParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const _getTeamMetadataForBooking = (teamData: NonNullable<TeamData>, eventTypeId
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function _getOrgContext(params: Params) {
|
export async function getOrgContext(params: Params) {
|
||||||
const result = paramsSchema.safeParse({
|
const result = paramsSchema.safeParse({
|
||||||
slug: params?.slug,
|
slug: params?.slug,
|
||||||
type: params?.type,
|
type: params?.type,
|
||||||
@@ -74,7 +74,7 @@ const _getMultipleDurationValue = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||||
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
|
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params);
|
||||||
|
|
||||||
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
|
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
|
||||||
if (!teamData) return {}; // should never happen
|
if (!teamData) return {}; // should never happen
|
||||||
@@ -123,7 +123,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
|
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
|
||||||
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
|
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params);
|
||||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||||
|
|
||||||
// Handle org redirects
|
// Handle org redirects
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventTy
|
|||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
|
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||||
@@ -158,3 +159,14 @@ export async function getCRMData(
|
|||||||
crmRecordId,
|
crmRecordId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTeamId(teamSlug: string, orgSlug: string | null): Promise<number | null> {
|
||||||
|
const teamRepo = new TeamRepository(prisma);
|
||||||
|
const team = await teamRepo.findFirstBySlugAndParentSlug({
|
||||||
|
slug: teamSlug,
|
||||||
|
parentSlug: orgSlug,
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return team?.id ?? null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { createRouterCaller } from "app/_trpc/context";
|
import { createRouterCaller } from "app/_trpc/context";
|
||||||
import type { PageProps } from "app/_types";
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||||
|
|
||||||
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
@@ -31,16 +35,23 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
|||||||
redirect("/apps/installed/calendar");
|
redirect("/apps/installed/calendar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return redirect("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||||
createRouterCaller(calendarsRouter),
|
createRouterCaller(calendarsRouter),
|
||||||
createRouterCaller(appsRouter),
|
createRouterCaller(appsRouter),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
const [connectedCalendars, installedCalendars] = await Promise.all([
|
||||||
const installedCalendars = await appsCaller.integrations({
|
calendarsCaller.connectedCalendars(),
|
||||||
variant: "calendar",
|
appsCaller.integrations({
|
||||||
onlyInstalled: true,
|
variant: "calendar",
|
||||||
});
|
onlyInstalled: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstalledApps
|
<InstalledApps
|
||||||
|
|||||||
+4
-1
@@ -5,6 +5,7 @@ import { unstable_cache } from "next/cache";
|
|||||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||||
|
|
||||||
import { TeamMembersView } from "~/teams/team-members-view";
|
import { TeamMembersView } from "~/teams/team-members-view";
|
||||||
@@ -36,8 +37,10 @@ const getCachedTeamRoles = unstable_cache(
|
|||||||
const getCachedTeamAttributes = unstable_cache(
|
const getCachedTeamAttributes = unstable_cache(
|
||||||
async (organizationId?: number) => {
|
async (organizationId?: number) => {
|
||||||
if (!organizationId) return [];
|
if (!organizationId) return [];
|
||||||
|
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId: organizationId });
|
return await attributeRepo.findAllByOrgIdWithOptions({ orgId: organizationId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,7 @@ import { unstable_cache } from "next/cache";
|
|||||||
|
|
||||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
||||||
|
|
||||||
import { MembersView } from "~/members/members-view";
|
import { MembersView } from "~/members/members-view";
|
||||||
@@ -19,7 +20,9 @@ export const generateMetadata = async () =>
|
|||||||
|
|
||||||
const getCachedAttributes = unstable_cache(
|
const getCachedAttributes = unstable_cache(
|
||||||
async (orgId: number) => {
|
async (orgId: number) => {
|
||||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId });
|
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||||
|
|
||||||
|
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/about"
|
"/settings/organizations/new/about"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/add-teams"
|
"/settings/organizations/new/add-teams"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||||
|
|
||||||
@@ -18,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/handover"
|
"/settings/organizations/new/handover"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
+6
-12
@@ -1,21 +1,15 @@
|
|||||||
import type { GetServerSidePropsResult } from "next";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
|
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const featuresRepository = new FeaturesRepository(prisma);
|
const featuresRepository = new FeaturesRepository(prisma);
|
||||||
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||||
// Check if organizations are enabled
|
|
||||||
if (!organizations) {
|
if (!organizations) {
|
||||||
return {
|
return notFound();
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return children;
|
||||||
props: {
|
}
|
||||||
isOrg: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+1
-10
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/onboard-members"
|
"/settings/organizations/new/onboard-members"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
||||||
|
|
||||||
export const generateMetadata = async () =>
|
export const generateMetadata = async () =>
|
||||||
@@ -19,14 +13,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new"
|
"/settings/organizations/new"
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
const ServerPage = async () => {
|
||||||
isOrg: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/status"
|
"/settings/organizations/new/status"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
|||||||
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
||||||
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { truncateOnWord } from "@calcom/lib/text";
|
import { truncateOnWord } from "@calcom/lib/text";
|
||||||
@@ -32,7 +33,7 @@ const _generateMetadataWithoutImage = async (
|
|||||||
const description = getDescription(t);
|
const description = getDescription(t);
|
||||||
const titleSuffix = `| ${APP_NAME}`;
|
const titleSuffix = `| ${APP_NAME}`;
|
||||||
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: title.length === 0 ? APP_NAME : displayedTitle,
|
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ async function handler(req: NextRequest) {
|
|||||||
|
|
||||||
return new Response(img.body, {
|
return new Response(img.body, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "image/png", "cache-control": "max-age=0" },
|
headers: {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
@@ -116,7 +119,13 @@ async function handler(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||||
|
|
||||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
return new Response(img.body, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -144,7 +153,13 @@ async function handler(req: NextRequest) {
|
|||||||
|
|
||||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||||
|
|
||||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
return new Response(img.body, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
return new Response(
|
return new Response(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Script from "next/script";
|
|||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { IconSprites } from "@calcom/ui/components/icon";
|
import { IconSprites } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ function PageWrapper(props: AppProps) {
|
|||||||
// Set canonical to https://cal.com or self-hosted URL
|
// Set canonical to https://cal.com or self-hosted URL
|
||||||
canonical={
|
canonical={
|
||||||
IS_CALCOM
|
IS_CALCOM
|
||||||
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
? buildCanonical({ path, origin: getCalcomUrl() }) // cal.com & .dev
|
||||||
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||||
}
|
}
|
||||||
{...seoConfig.defaultNextSeo}
|
{...seoConfig.defaultNextSeo}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { useMemo } from "react";
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||||
|
|
||||||
import type { TLocationOptions } from "@calcom/features/eventtypes/components/Locations";
|
import type {
|
||||||
import type { TEventTypeLocation } from "@calcom/features/eventtypes/components/Locations";
|
TLocationOptions,
|
||||||
import Locations from "@calcom/features/eventtypes/components/Locations";
|
TEventTypeLocation,
|
||||||
|
} from "@calcom/features/eventtypes/components/locations/Locations";
|
||||||
|
import Locations from "@calcom/features/eventtypes/components/locations/Locations";
|
||||||
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||||
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
|
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -3,42 +3,48 @@
|
|||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
Sentry.init({
|
if (process.env.NODE_ENV === "production") {
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
|
||||||
|
|
||||||
sampleRate: parseFloat(process.env.SENTRY_SAMPLE_RATE ?? "1.0") || 1.0,
|
sampleRate: parseFloat(process.env.SENTRY_SAMPLE_RATE ?? "1.0") || 1.0,
|
||||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.0") || 0.0,
|
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||||
|
|
||||||
// Define how likely Replay events are sampled.
|
// Define how likely Replay events are sampled.
|
||||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
// in development and sample at a lower rate in production
|
// in development and sample at a lower rate in production
|
||||||
// replaysSessionSampleRate: parseFloat(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? "0.0") || 0.0,
|
// replaysSessionSampleRate: parseFloat(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||||
|
|
||||||
// Define how likely Replay events are sampled when an error occurs.
|
// Define how likely Replay events are sampled when an error occurs.
|
||||||
// replaysOnErrorSampleRate: parseFloat(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? "0.0") || 0.0,
|
// replaysOnErrorSampleRate: parseFloat(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||||
|
|
||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: !!process.env.SENTRY_DEBUG,
|
debug: !!process.env.SENTRY_DEBUG,
|
||||||
beforeSend(event) {
|
beforeSend(event) {
|
||||||
if (
|
if (
|
||||||
event.exception?.values?.some(
|
event.exception?.values?.some(
|
||||||
(e) =>
|
(e) =>
|
||||||
// Ignore fake error "UnhandledRejection: Non-Error promise rejection captured with value: Object Not Found Matching Id:3, MethodName:update, ParamCount:4"
|
// Ignore fake error "UnhandledRejection: Non-Error promise rejection captured with value: Object Not Found Matching Id:3, MethodName:update, ParamCount:4"
|
||||||
// Raised GH issue: https://github.com/getsentry/sentry-javascript/issues/3440
|
// Raised GH issue: https://github.com/getsentry/sentry-javascript/issues/3440
|
||||||
e.value?.includes("Non-Error promise rejection captured with") ||
|
e.value?.includes("Non-Error promise rejection captured with") ||
|
||||||
e.value?.includes("Object Not Found Matching Id")
|
e.value?.includes("Object Not Found Matching Id")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.tags = {
|
event.tags = {
|
||||||
...event.tags,
|
...event.tags,
|
||||||
errorSource: "client",
|
errorSource: "client",
|
||||||
};
|
};
|
||||||
return event;
|
return event;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
export function onRouterTransitionStart(url: string, navigationType: "push" | "replace" | "traverse") {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
Sentry.captureRouterTransitionStart(url, navigationType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
import { type Instrumentation } from 'next'
|
||||||
|
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
export async function register() {
|
export async function register() {
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
await import("./sentry.server.config");
|
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
}
|
await import("./sentry.server.config");
|
||||||
|
}
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
|
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
|
||||||
await import("./sentry.edge.config");
|
await import("./sentry.edge.config");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onRequestError = Sentry.captureRequestError;
|
export const onRequestError: Instrumentation.onRequestError = ( err,
|
||||||
|
request,
|
||||||
|
context) => {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
Sentry.captureRequestError(err, request, context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
|||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
email: true,
|
email: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
|
isPlatformManaged: true,
|
||||||
};
|
};
|
||||||
const eventType = await prisma.eventType.findUnique({
|
const eventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -75,6 +76,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
|||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdByOAuthClientId: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workflows: {
|
workflows: {
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
error: null,
|
|
||||||
migrated: null,
|
|
||||||
userId: session.user.id,
|
|
||||||
username: session.user.username,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -60,22 +60,6 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
|||||||
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
|
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
|
||||||
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
|
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
|
||||||
|
|
||||||
if (!url.pathname.startsWith("/api")) {
|
|
||||||
//
|
|
||||||
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
|
||||||
// by the application the user is automatically redirected to /auth/login.
|
|
||||||
//
|
|
||||||
// - For this reason our matchers are sufficient for an app-wide maintenance page.
|
|
||||||
//
|
|
||||||
// Check whether the maintenance page should be shown
|
|
||||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
|
||||||
// If is in maintenance mode, point the url pathname to the maintenance page
|
|
||||||
if (isInMaintenanceMode) {
|
|
||||||
reqWithEnrichedHeaders.nextUrl.pathname = `/maintenance`;
|
|
||||||
return NextResponse.rewrite(reqWithEnrichedHeaders.nextUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const routingFormRewriteResponse = routingForms.handleRewrite(url);
|
const routingFormRewriteResponse = routingForms.handleRewrite(url);
|
||||||
if (routingFormRewriteResponse) {
|
if (routingFormRewriteResponse) {
|
||||||
return responseWithHeaders({ url, res: routingFormRewriteResponse, req: reqWithEnrichedHeaders });
|
return responseWithHeaders({ url, res: routingFormRewriteResponse, req: reqWithEnrichedHeaders });
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block">
|
<div className="text-default mb-16 mt-4 block text-center text-sm">
|
||||||
{t("temporarily_out_of_office")}{" "}
|
{t("temporarily_out_of_office")}{" "}
|
||||||
<Link href="settings/my-account/out-of-office" className="underline">
|
<Link href="settings/my-account/out-of-office" className="underline">
|
||||||
{t("add_a_redirect")}
|
{t("add_a_redirect")}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ function BookingsContent({ status }: BookingsProps) {
|
|||||||
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
|
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
|
||||||
id: "userId",
|
id: "userId",
|
||||||
header: t("member"),
|
header: t("member"),
|
||||||
enableColumnFilter: true,
|
enableColumnFilter: user?.isTeamAdminOrOwner ?? false,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: () => null,
|
cell: () => null,
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -228,16 +228,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
? { ...previousBooking, rescheduledBy: bookingInfo.user?.name }
|
? { ...previousBooking, rescheduledBy: bookingInfo.user?.name }
|
||||||
: previousBooking;
|
: previousBooking;
|
||||||
|
|
||||||
|
const isPlatformBooking = eventType.users[0]?.isPlatformManaged || eventType.team?.createdByOAuthClientId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
orgSlug: currentOrgDomain,
|
orgSlug: currentOrgDomain,
|
||||||
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||||
hideBranding: await shouldHideBrandingForEvent({
|
hideBranding: isPlatformBooking
|
||||||
eventTypeId: eventType.id,
|
? true
|
||||||
team: eventType.team,
|
: await shouldHideBrandingForEvent({
|
||||||
owner: eventType.users[0] ?? null,
|
eventTypeId: eventType.id,
|
||||||
organizationId: session?.user?.profile?.organizationId ?? session?.user?.org?.id ?? null,
|
team: eventType.team,
|
||||||
}),
|
owner: eventType.users[0] ?? null,
|
||||||
|
organizationId: session?.user?.profile?.organizationId ?? session?.user?.org?.id ?? null,
|
||||||
|
}),
|
||||||
profile,
|
profile,
|
||||||
eventType,
|
eventType,
|
||||||
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ const OnboardingPage = (props: PageProps) => {
|
|||||||
"dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
"dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1]",
|
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1]",
|
||||||
"[--cal-brand-subtle:#9CA3AF]",
|
"[--cal-brand-subtle:#9CA3AF]",
|
||||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]"
|
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
|
||||||
|
"[--cal-brand-accent:#FFFFFF] dark:[--cal-brand-accent:#000000]"
|
||||||
)}
|
)}
|
||||||
data-testid="onboarding"
|
data-testid="onboarding"
|
||||||
key={pathname}>
|
key={pathname}>
|
||||||
|
|||||||
+490
-478
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.5.17",
|
"version": "5.6.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"yarn": "3.4.1"
|
"yarn": "3.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@boxyhq/saml-jackson": "1.42.0",
|
"@boxyhq/saml-jackson": "1.52.2",
|
||||||
"@calcom/app-store": "*",
|
"@calcom/app-store": "*",
|
||||||
"@calcom/app-store-cli": "*",
|
"@calcom/app-store-cli": "*",
|
||||||
"@calcom/dayjs": "*",
|
"@calcom/dayjs": "*",
|
||||||
@@ -77,7 +77,6 @@
|
|||||||
"@tanstack/react-query": "^5.17.15",
|
"@tanstack/react-query": "^5.17.15",
|
||||||
"@team-plain/typescript-sdk": "^5.9.0",
|
"@team-plain/typescript-sdk": "^5.9.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@unkey/ratelimit": "^0.1.1",
|
|
||||||
"@upstash/redis": "^1.35.2",
|
"@upstash/redis": "^1.35.2",
|
||||||
"@vercel/edge-config": "^0.1.1",
|
"@vercel/edge-config": "^0.1.1",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { wrapApiHandlerWithSentry } from "@sentry/nextjs";
|
|
||||||
|
|
||||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
|
||||||
import { highPerfRouter } from "@calcom/trpc/server/routers/viewer/highPerf/_router";
|
|
||||||
|
|
||||||
export default wrapApiHandlerWithSentry(createNextApiHandler(highPerfRouter), "/api/trpc/highPerf/[trpc]");
|
|
||||||
@@ -479,7 +479,6 @@ test.describe("Bookings", () => {
|
|||||||
const filterItemsConfig = [
|
const filterItemsConfig = [
|
||||||
{ key: "eventTypeId", name: "Event Type", testId: "add-filter-item-eventTypeId" },
|
{ key: "eventTypeId", name: "Event Type", testId: "add-filter-item-eventTypeId" },
|
||||||
{ key: "teamId", name: "Team", testId: "add-filter-item-teamId" },
|
{ key: "teamId", name: "Team", testId: "add-filter-item-teamId" },
|
||||||
{ key: "userId", name: "Member", testId: "add-filter-item-userId" },
|
|
||||||
{ key: "attendeeName", name: "Attendees Name", testId: "add-filter-item-attendeeName" },
|
{ key: "attendeeName", name: "Attendees Name", testId: "add-filter-item-attendeeName" },
|
||||||
{ key: "attendeeEmail", name: "Attendee Email", testId: "add-filter-item-attendeeEmail" },
|
{ key: "attendeeEmail", name: "Attendee Email", testId: "add-filter-item-attendeeEmail" },
|
||||||
{ key: "dateRange", name: "Date Range", testId: "add-filter-item-dateRange" },
|
{ key: "dateRange", name: "Date Range", testId: "add-filter-item-dateRange" },
|
||||||
@@ -488,8 +487,16 @@ test.describe("Bookings", () => {
|
|||||||
|
|
||||||
const getFilterItemLocator = (page: Page, testId: string) => page.locator(`[data-testid="${testId}"]`);
|
const getFilterItemLocator = (page: Page, testId: string) => page.locator(`[data-testid="${testId}"]`);
|
||||||
|
|
||||||
test.beforeEach(async ({ page, users }) => {
|
const setup = async ({
|
||||||
const user = await users.create();
|
isAdmin,
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}: {
|
||||||
|
isAdmin: boolean;
|
||||||
|
page: Page;
|
||||||
|
users: Fixtures["users"];
|
||||||
|
}) => {
|
||||||
|
const user = isAdmin ? await users.create(undefined, { hasTeam: true }) : await users.create();
|
||||||
await user.apiLogin();
|
await user.apiLogin();
|
||||||
const bookingsGetResponse = page.waitForResponse((response) =>
|
const bookingsGetResponse = page.waitForResponse((response) =>
|
||||||
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
||||||
@@ -498,9 +505,10 @@ test.describe("Bookings", () => {
|
|||||||
await bookingsGetResponse;
|
await bookingsGetResponse;
|
||||||
await page.locator('[data-testid="add-filter-button"]').click();
|
await page.locator('[data-testid="add-filter-button"]').click();
|
||||||
await expect(page.locator(searchInputSelector)).toBeVisible();
|
await expect(page.locator(searchInputSelector)).toBeVisible();
|
||||||
});
|
};
|
||||||
|
|
||||||
test("should show all filter items initially and after clearing search", async ({ page }) => {
|
test("should show all filter items initially and after clearing search", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: false, page, users });
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
|
|
||||||
// Initial check: all defined filter items should be visible
|
// Initial check: all defined filter items should be visible
|
||||||
@@ -524,7 +532,18 @@ test.describe("Bookings", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("search should be case-insensitive", async ({ page }) => {
|
test("should show admin-only filter", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: true, page, users });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getFilterItemLocator(page, "add-filter-item-userId"),
|
||||||
|
`Item "Member" should be visible initially`
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("search should be case-insensitive", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: true, page, users });
|
||||||
|
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
|
|
||||||
// Search for "member" (lowercase)
|
// Search for "member" (lowercase)
|
||||||
@@ -534,7 +553,8 @@ test.describe("Bookings", () => {
|
|||||||
await expect(getFilterItemLocator(page, "add-filter-item-teamId")).toBeHidden();
|
await expect(getFilterItemLocator(page, "add-filter-item-teamId")).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should individually find each filter item by its full name", async ({ page }) => {
|
test("should individually find each filter item by its full name", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: false, page, users });
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
|
|
||||||
for (const targetItem of filterItemsConfig) {
|
for (const targetItem of filterItemsConfig) {
|
||||||
@@ -550,7 +570,8 @@ test.describe("Bookings", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show no items for a non-matching search term", async ({ page }) => {
|
test("should show no items for a non-matching search term", async ({ page, users }) => {
|
||||||
|
await setup({ isAdmin: false, page, users });
|
||||||
const searchInput = page.locator(searchInputSelector);
|
const searchInput = page.locator(searchInputSelector);
|
||||||
const nonExistentTerm = "NonExistentFilterXYZ123";
|
const nonExistentTerm = "NonExistentFilterXYZ123";
|
||||||
await searchInput.fill(nonExistentTerm);
|
await searchInput.fill(nonExistentTerm);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { test } from "./lib/fixtures";
|
|||||||
import {
|
import {
|
||||||
bookTimeSlot,
|
bookTimeSlot,
|
||||||
confirmReschedule,
|
confirmReschedule,
|
||||||
|
confirmBooking,
|
||||||
doOnOrgDomain,
|
doOnOrgDomain,
|
||||||
selectFirstAvailableTimeSlotNextMonth,
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
selectSecondAvailableTimeSlotNextMonth,
|
selectSecondAvailableTimeSlotNextMonth,
|
||||||
@@ -98,6 +99,58 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
|
|||||||
activeState = await listItemLocator.getAttribute("data-active");
|
activeState = await listItemLocator.getAttribute("data-active");
|
||||||
expect(activeState).toEqual("false");
|
expect(activeState).toEqual("false");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("multiple duration selection updates event length correctly", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await test.step("update event title to include duration placeholder", async () => {
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
|
await page.click(`text=Multiple duration`);
|
||||||
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
|
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
||||||
|
"aria-current",
|
||||||
|
"page"
|
||||||
|
);
|
||||||
|
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||||
|
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
|
||||||
|
await page.locator('[data-testid="update-eventtype"]').click();
|
||||||
|
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/multiple-duration`);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="multiple-choice-30mins"]').waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
await test.step("verify default 30min duration is selected", async () => {
|
||||||
|
const duration30 = page.getByTestId("multiple-choice-30mins");
|
||||||
|
const activeState = await duration30.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("book with 90min duration and verify title", async () => {
|
||||||
|
await page.getByTestId("multiple-choice-90mins").click();
|
||||||
|
|
||||||
|
const duration90 = page.getByTestId("multiple-choice-90mins");
|
||||||
|
const activeState = await duration90.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await page.fill('[name="name"]', "Test User");
|
||||||
|
await page.fill('[name="email"]', "test@example.com");
|
||||||
|
await confirmBooking(page);
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
const bookingTitle = page.locator('[data-testid="booking-title"]');
|
||||||
|
await expect(bookingTitle).toBeVisible();
|
||||||
|
const titleText = await bookingTitle.textContent();
|
||||||
|
expect(titleText).toContain("90");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
test.skip("it contains the right event details", async ({ page }) => {
|
test.skip("it contains the right event details", async ({ page }) => {
|
||||||
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
module.exports = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
config.plugins.autoprefixer = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|||||||
@@ -3472,5 +3472,6 @@
|
|||||||
"webhook_metadata": "Metadata",
|
"webhook_metadata": "Metadata",
|
||||||
"stats": "Stats",
|
"stats": "Stats",
|
||||||
"booking_status": "Booking status",
|
"booking_status": "Booking status",
|
||||||
|
"phone": "Phone",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,18 +41,6 @@
|
|||||||
},
|
},
|
||||||
"pages/api/trpc/workflows/[trpc].ts": {
|
"pages/api/trpc/workflows/[trpc].ts": {
|
||||||
"maxDuration": 800
|
"maxDuration": 800
|
||||||
},
|
|
||||||
"pages/api/trpc/highPerf/[trpc].ts": {
|
|
||||||
"memory": 3008
|
|
||||||
},
|
|
||||||
"pages/api/trpc/appRoutingForms/[trpc].ts": {
|
|
||||||
"memory": 2048
|
|
||||||
},
|
|
||||||
"pages/router/embed.tsx": {
|
|
||||||
"memory": 2048
|
|
||||||
},
|
|
||||||
"pages/api/book/event.ts": {
|
|
||||||
"memory": 2048
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4264,9 +4264,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
||||||
@@ -4328,9 +4326,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
||||||
@@ -4402,9 +4398,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
||||||
@@ -4474,9 +4468,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
||||||
@@ -22121,13 +22113,7 @@
|
|||||||
"example": "2025-12-31T23:59:59.000Z"
|
"example": "2025-12-31T23:59:59.000Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"expiresAt"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"UsageBasedPrivateLinkOutput": {
|
"UsageBasedPrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22164,14 +22150,7 @@
|
|||||||
"example": 3
|
"example": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"maxUsageCount",
|
|
||||||
"usageCount"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkOutput": {
|
"CreatePrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22193,10 +22172,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"GetPrivateLinksOutput": {
|
"GetPrivateLinksOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22221,10 +22197,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"UpdatePrivateLinkOutput": {
|
"UpdatePrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22246,10 +22219,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"DeletePrivateLinkOutput": {
|
"DeletePrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22274,10 +22244,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"StripConnectOutputDto": {
|
"StripConnectOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -26267,205 +26234,6 @@
|
|||||||
},
|
},
|
||||||
"required": ["status", "data"]
|
"required": ["status", "data"]
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for time-based links",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1,
|
|
||||||
"default": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TimeBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for this time-based link",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2025-12-31T23:59:59.000Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
|
||||||
},
|
|
||||||
"UsageBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times this link can be used",
|
|
||||||
"example": 10
|
|
||||||
},
|
|
||||||
"usageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Current usage count for this link",
|
|
||||||
"example": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
|
||||||
},
|
|
||||||
"CreatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Created private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"GetPrivateLinksOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkBody": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"description": "New expiration date for time-based links",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "New maximum number of times the link can be used",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Updated private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"DeletePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Deleted link information",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Private link deleted successfully"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UserWebhookOutputDto": {
|
"UserWebhookOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ Below is a list of props that can be passed to the booker atom.
|
|||||||
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
||||||
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
||||||
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
||||||
|
| silentlyHandleCalendarFailures | No | Boolean when true the booker still displays slots when the third party calendars credentials are invalid or expired, Booker may show stale availability when enabled |
|
||||||
|
| hideEventMetadata | No | Boolean that controls the visibility of the event metadata sidebar. When `true`, hides the left sidebar containing event details like title, description, duration, and host information. Defaults to `false`. |
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Sometimes, yarn install might fail during deployment on Vercel, in which case, y
|
|||||||
<Step title="Override the build command">
|
<Step title="Override the build command">
|
||||||
Override to:
|
Override to:
|
||||||
```
|
```
|
||||||
cd ../.. && yarn build --include-dependencies --no-deps
|
cd ../.. && yarn build --no-deps
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Hit Deploy">
|
<Step title="Hit Deploy">
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ RUN set -eux; \
|
|||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
||||||
yarn config set httpTimeout 1200000 && \
|
yarn config set httpTimeout 1200000 && \
|
||||||
npx turbo prune --scope=@calcom/web --docker && \
|
npx turbo prune --filter=@calcom/web --docker && \
|
||||||
npx turbo prune --scope=@calcom/api --docker && \
|
npx turbo prune --filter=@calcom/api --docker && \
|
||||||
yarn install && \
|
yarn install && \
|
||||||
yarn turbo run build --filter=@calcom/api
|
yarn turbo run build --filter=@calcom/api
|
||||||
|
|
||||||
|
|||||||
+13
-14
@@ -28,23 +28,23 @@
|
|||||||
"edit-app-template": "yarn app-store edit-template",
|
"edit-app-template": "yarn app-store edit-template",
|
||||||
"delete-app-template": "yarn app-store delete-template",
|
"delete-app-template": "yarn app-store delete-template",
|
||||||
"build": "turbo run build --filter=@calcom/web...",
|
"build": "turbo run build --filter=@calcom/web...",
|
||||||
"build:ai": "turbo run build --scope=\"@calcom/ai\"",
|
"build:ai": "turbo run build --filter=\"@calcom/ai\"",
|
||||||
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
||||||
"db-deploy": "turbo run db-deploy",
|
"db-deploy": "turbo run db-deploy",
|
||||||
"db-seed": "turbo run db-seed",
|
"db-seed": "turbo run db-seed",
|
||||||
"db-studio": "yarn prisma studio",
|
"db-studio": "yarn prisma studio",
|
||||||
"deploy": "turbo run deploy",
|
"deploy": "turbo run deploy",
|
||||||
"dev:all": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"",
|
"dev:all": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\" --filter=\"@calcom/console\"",
|
||||||
"dev:ai": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/ai\"",
|
"dev:ai": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/ai\"",
|
||||||
"dev:api": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\"",
|
"dev:api": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\"",
|
||||||
"dev:api:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/console\"",
|
"dev:api:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/console\"",
|
||||||
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"",
|
"dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
|
||||||
"dev:swagger": "turbo run dev --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"",
|
"dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
|
||||||
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"",
|
"dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
|
||||||
"dev": "turbo run dev --scope=\"@calcom/web\"",
|
"dev": "turbo run dev --filter=\"@calcom/web\"",
|
||||||
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
|
"build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
|
||||||
"dx": "turbo run dx",
|
"dx": "turbo run dx",
|
||||||
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
|
"i-dev": "infisical run -- turbo run dev --filter=\"@calcom/web\"",
|
||||||
"i-dx": "infisical run -- turbo run dx",
|
"i-dx": "infisical run -- turbo run dx",
|
||||||
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
||||||
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
"predev": "echo 'Checking env files'",
|
"predev": "echo 'Checking env files'",
|
||||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||||
"start": "turbo run start --scope=\"@calcom/web\"",
|
"start": "turbo run start --filter=\"@calcom/web\"",
|
||||||
"tdd": "vitest watch",
|
"tdd": "vitest watch",
|
||||||
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
||||||
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
||||||
@@ -118,13 +118,12 @@
|
|||||||
"@daily-co/daily-js": "^0.76.0",
|
"@daily-co/daily-js": "^0.76.0",
|
||||||
"@evyweb/ioctopus": "^1.2.0",
|
"@evyweb/ioctopus": "^1.2.0",
|
||||||
"@next/third-parties": "^14.2.5",
|
"@next/third-parties": "^14.2.5",
|
||||||
"@unkey/ratelimit": "^2.1.2",
|
|
||||||
"@vercel/functions": "^1.4.0",
|
"@vercel/functions": "^1.4.0",
|
||||||
"city-timezones": "^1.2.1",
|
"city-timezones": "^1.2.1",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"turbo": "^1.10.1"
|
"turbo": "^2.5.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"types-ramda": "0.29.4",
|
"types-ramda": "0.29.4",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cach
|
|||||||
import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache";
|
import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache";
|
||||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||||
import { uniqueBy } from "@calcom/lib/array";
|
import { uniqueBy } from "@calcom/lib/array";
|
||||||
|
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||||
@@ -85,10 +86,16 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
const selectedHostDestinationCalendar = event.destinationCalendar?.find(
|
const selectedHostDestinationCalendar = event.destinationCalendar?.find(
|
||||||
(cal) => cal.credentialId === this.credential.id
|
(cal) => cal.credentialId === this.credential.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isOrganizerExempt = ORGANIZER_EMAIL_EXEMPT_DOMAINS?.split(",")
|
||||||
|
.filter((domain) => domain.trim() !== "")
|
||||||
|
.some((domain) => event.organizer.email.toLowerCase().endsWith(domain.toLowerCase()));
|
||||||
|
|
||||||
const eventAttendees = event.attendees.map(({ id: _id, ...rest }) => ({
|
const eventAttendees = event.attendees.map(({ id: _id, ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const attendees: calendar_v3.Schema$EventAttendee[] = [
|
const attendees: calendar_v3.Schema$EventAttendee[] = [
|
||||||
{
|
{
|
||||||
...event.organizer,
|
...event.organizer,
|
||||||
@@ -100,7 +107,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
// We use || instead of ?? here to handle empty strings
|
// We use || instead of ?? here to handle empty strings
|
||||||
email: hostExternalCalendarId || selectedHostDestinationCalendar?.externalId || event.organizer.email,
|
email: hostExternalCalendarId || selectedHostDestinationCalendar?.externalId || event.organizer.email,
|
||||||
},
|
},
|
||||||
...eventAttendees,
|
...(event.hideOrganizerEmail && !isOrganizerExempt ? [] : eventAttendees),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (event.team?.members) {
|
if (event.team?.members) {
|
||||||
|
|||||||
@@ -661,22 +661,30 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleErrorJsonOffice365Calendar = <Type>(response: Response): Promise<Type | string> => {
|
private async handleErrorJsonOffice365Calendar<Type>(response: Response): Promise<Type | string> {
|
||||||
if (response.headers.get("content-encoding") === "gzip") {
|
if (response.headers.get("content-encoding") === "gzip") {
|
||||||
return response.text();
|
return response.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
return new Promise((resolve) => resolve({} as Type));
|
return {} as Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok && response.status < 200 && response.status >= 300) {
|
if (!response.ok) {
|
||||||
response.json().then(console.log);
|
let errorBody: string | object;
|
||||||
throw Error(response.statusText);
|
try {
|
||||||
|
errorBody = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
errorBody = await response.text();
|
||||||
|
}
|
||||||
|
this.log.error(
|
||||||
|
`handleErrorJsonOffice365Calendar: Office365 API request failed with status ${response.status}`,
|
||||||
|
errorBody
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
}
|
||||||
|
|
||||||
async getMainTimeZone(): Promise<string> {
|
async getMainTimeZone(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,363 +0,0 @@
|
|||||||
import { describe, expect, it, afterEach, vi } from "vitest";
|
|
||||||
|
|
||||||
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("jsonLogicToPrisma(Reporting)", () => {
|
|
||||||
describe("Text Operand", () => {
|
|
||||||
it("should support 'Equals' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: { and: [{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "A"] }] },
|
|
||||||
});
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: "A",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support 'Not Equals' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: { and: [{ "!=": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abc"] }] },
|
|
||||||
});
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
NOT: {
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: "abc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support 'Contains' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: { and: [{ in: ["A", { var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }] }] },
|
|
||||||
});
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
string_contains: "A",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support 'Not Contains' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: { and: [{ "!": { in: ["a", { var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }] } }] },
|
|
||||||
});
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
NOT: {
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
string_contains: "a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Number Type", () => {
|
|
||||||
it("should support 'greater than' operator", () => {
|
|
||||||
let prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
">": [
|
|
||||||
{
|
|
||||||
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
|
|
||||||
},
|
|
||||||
// Giving a string here to test that it is converted to a number
|
|
||||||
"100",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gt: 100 } }],
|
|
||||||
});
|
|
||||||
|
|
||||||
prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
">": [
|
|
||||||
{
|
|
||||||
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
|
|
||||||
},
|
|
||||||
// A number would also work
|
|
||||||
100,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gt: 100 } }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("should support 'greater than or equal to' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
">=": [
|
|
||||||
{
|
|
||||||
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
|
|
||||||
},
|
|
||||||
// Giving a string here to test that it is converted to a number
|
|
||||||
"100",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gte: 100 } }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("should support 'less than' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
"<": [
|
|
||||||
{
|
|
||||||
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
|
|
||||||
},
|
|
||||||
// Giving a string here to test that it is converted to a number
|
|
||||||
"100",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], lt: 100 } }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("should support 'less than or equal to' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
"<=": [
|
|
||||||
{
|
|
||||||
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
|
|
||||||
},
|
|
||||||
// Giving a string here to test that it is converted to a number
|
|
||||||
"100",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], lte: 100 } }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("'Equals' operator should query with string as well as number", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: { and: [{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "1"] }] },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("MultiSelect", () => {
|
|
||||||
it("should support 'Equals' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
and: [
|
|
||||||
{ all: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, { in: [{ var: "" }, ["C", "D"]] }] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
response: { path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"], array_contains: ["C", "D"] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support 'Any In' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: { and: [{ in: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, ["C", "D"]] }] },
|
|
||||||
});
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
|
|
||||||
equals: "C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
|
|
||||||
equals: "D",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support 'Not In' operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: { and: [{ "!": { in: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, ["C", "D"]] } }] },
|
|
||||||
});
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
NOT: {
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
|
|
||||||
equals: "C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
|
|
||||||
equals: "D",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support where All Match ['Equals', 'Equals'] operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
and: [
|
|
||||||
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "a"] },
|
|
||||||
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "b"] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: "a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: "b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support where Any Match ['Equals', 'Equals'] operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
or: [
|
|
||||||
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "a"] },
|
|
||||||
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "b"] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: "a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
|
|
||||||
equals: "b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support where None Match ['Equals', 'Equals'] operator", () => {
|
|
||||||
const prismaWhere = jsonLogicToPrisma({
|
|
||||||
logic: {
|
|
||||||
"!": {
|
|
||||||
or: [
|
|
||||||
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abc"] },
|
|
||||||
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abcd"] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(prismaWhere).toEqual({
|
|
||||||
NOT: {
|
|
||||||
OR: [
|
|
||||||
{ response: { path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], equals: "abc" } },
|
|
||||||
{ response: { path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], equals: "abcd" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user