Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea6e105e6 | ||
|
|
a082cf73a0 | ||
|
|
61268f36b9 | ||
|
|
99cd0c5c41 | ||
|
|
2266301469 | ||
|
|
30b0012e6a | ||
|
|
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,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": "*",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TestingModule } from "@nestjs/testing";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
@@ -33,6 +34,7 @@ describe("AppController", () => {
|
|||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
|
|
||||||
@@ -108,6 +110,14 @@ describe("AppController", () => {
|
|||||||
organization: { connect: { id: organization.id } },
|
organization: { connect: { id: organization.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|||||||
+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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.
|
|||||||
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
|
||||||
import { RedisModule } from "@/modules/redis/redis.module";
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -22,7 +21,6 @@ import { PassportModule } from "@nestjs/passport";
|
|||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
DeploymentsModule,
|
DeploymentsModule,
|
||||||
ProfilesModule,
|
|
||||||
],
|
],
|
||||||
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||||
exports: [NextAuthGuard, ApiAuthGuard],
|
exports: [NextAuthGuard, ApiAuthGuard],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
|||||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
@@ -20,6 +21,7 @@ import { Test, TestingModule } from "@nestjs/testing";
|
|||||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
import { createRequest } from "node-mocks-http";
|
import { createRequest } from "node-mocks-http";
|
||||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.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 { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
@@ -45,6 +47,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
|
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
|
||||||
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
|
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
|
||||||
@@ -65,6 +68,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
}),
|
}),
|
||||||
ProfilesModule,
|
ProfilesModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
|
MembershipsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
MockedRedisService,
|
MockedRedisService,
|
||||||
@@ -91,6 +95,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(module);
|
||||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
||||||
organizationTwo = await teamRepositoryFixture.create({
|
organizationTwo = await teamRepositoryFixture.create({
|
||||||
name: `api-auth-organization-2-${randomString()}`,
|
name: `api-auth-organization-2-${randomString()}`,
|
||||||
@@ -121,6 +126,20 @@ describe("ApiAuthStrategy", () => {
|
|||||||
organization: { connect: { id: organizationTwo.id } },
|
organization: { connect: { id: organizationTwo.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: validOAuthUser.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: validOAuthUser.id } },
|
||||||
|
team: { connect: { id: organizationTwo.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
logo: "logo-url",
|
logo: "logo-url",
|
||||||
name: "name",
|
name: "name",
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
|||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { TokensService } from "@/modules/tokens/tokens.service";
|
import { TokensService } from "@/modules/tokens/tokens.service";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
@@ -45,8 +45,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
private readonly userRepository: UsersRepository,
|
private readonly userRepository: UsersRepository,
|
||||||
private readonly apiKeyRepository: ApiKeysRepository,
|
private readonly apiKeyRepository: ApiKeysRepository,
|
||||||
private readonly oauthRepository: OAuthClientRepository,
|
private readonly oauthRepository: OAuthClientRepository,
|
||||||
private readonly profilesRepository: ProfilesRepository,
|
private readonly usersService: UsersService,
|
||||||
private readonly usersService: UsersService
|
private readonly membershipsRepository: MembershipsRepository
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,9 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
throw new UnauthorizedException("ApiAuthStrategy - oAuth client - Invalid client secret");
|
throw new UnauthorizedException("ApiAuthStrategy - oAuth client - Invalid client secret");
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformCreatorId = await this.profilesRepository.getPlatformOwnerUserId(client.organizationId);
|
const platformCreatorId =
|
||||||
|
(await this.membershipsRepository.findPlatformOwnerUserId(client.organizationId)) ||
|
||||||
|
(await this.membershipsRepository.findPlatformAdminUserId(client.organizationId));
|
||||||
|
|
||||||
if (!platformCreatorId) {
|
if (!platformCreatorId) {
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException(
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -20,6 +20,36 @@ export class MembershipsRepository {
|
|||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findPlatformOwnerUserId(organizationId: number): Promise<number | undefined> {
|
||||||
|
const owner = await this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return owner?.userId ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPlatformAdminUserId(organizationId: number): Promise<number | undefined> {
|
||||||
|
const admin = await this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
role: "ADMIN",
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return admin?.userId ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async findMembershipByTeamId(teamId: number, userId: number) {
|
async findMembershipByTeamId(teamId: number, userId: number) {
|
||||||
const membership = await this.dbRead.prisma.membership.findUnique({
|
const membership = await this.dbRead.prisma.membership.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
+10
@@ -15,6 +15,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
import { Test, TestingModule } 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 { 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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
@@ -63,6 +64,7 @@ describe("OAuthFlow Endpoints", () => {
|
|||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
let user: User;
|
let user: User;
|
||||||
let organization: Team;
|
let organization: Team;
|
||||||
@@ -89,6 +91,7 @@ describe("OAuthFlow Endpoints", () => {
|
|||||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
|
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
user = await usersRepositoryFixtures.create({
|
user = await usersRepositoryFixtures.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
@@ -104,6 +107,13 @@ describe("OAuthFlow Endpoints", () => {
|
|||||||
movedFromUser: { connect: { id: user.id } },
|
movedFromUser: { connect: { id: user.id } },
|
||||||
organization: { connect: { id: organization.id } },
|
organization: { connect: { id: organization.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+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) {
|
||||||
|
|||||||
+7
@@ -39,6 +39,7 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Query,
|
Query,
|
||||||
|
Logger,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
@@ -66,6 +67,8 @@ export type EventTypeHandlerResponse = {
|
|||||||
@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 OrganizationsEventTypesController {
|
export class OrganizationsEventTypesController {
|
||||||
|
private readonly logger = new Logger("OrganizationsEventTypesController");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
|
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
|
||||||
private readonly inputService: InputOrganizationsEventTypesService,
|
private readonly inputService: InputOrganizationsEventTypesService,
|
||||||
@@ -88,6 +91,10 @@ export class OrganizationsEventTypesController {
|
|||||||
teamId,
|
teamId,
|
||||||
bodyEventType
|
bodyEventType
|
||||||
);
|
);
|
||||||
|
this.logger.debug(
|
||||||
|
"nl debug - create org team event type - transformedBody",
|
||||||
|
JSON.stringify(transformedBody, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
||||||
user,
|
user,
|
||||||
|
|||||||
+4
@@ -47,6 +47,10 @@ export class OrganizationsEventTypesService {
|
|||||||
prisma: this.dbWrite.prisma,
|
prisma: this.dbWrite.prisma,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.logger.debug(
|
||||||
|
"nl debug - create org team event type - eventTypeCreated",
|
||||||
|
JSON.stringify(eventTypeCreated, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
|
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export class TeamsEventTypesService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId);
|
const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId);
|
||||||
|
this.logger.debug("nl debug - update team event type - eventType", JSON.stringify(eventType, null, 2));
|
||||||
|
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||||
@@ -156,17 +157,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,9 +1,14 @@
|
|||||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||||
import type { PageProps } from "app/_types";
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
import { _generateMetadata, getTranslate } 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 { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||||
import BookingsList from "~/bookings/views/bookings-listing-view";
|
import BookingsList from "~/bookings/views/bookings-listing-view";
|
||||||
|
|
||||||
@@ -26,10 +31,11 @@ const Page = async ({ params }: PageProps) => {
|
|||||||
redirect("/bookings/upcoming");
|
redirect("/bookings/upcoming");
|
||||||
}
|
}
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||||
<BookingsList status={parsed.data.status} />
|
<BookingsList status={parsed.data.status} userId={session?.user?.id} />
|
||||||
</ShellMainAppDir>
|
</ShellMainAppDir>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
appsCaller.integrations({
|
||||||
variant: "calendar",
|
variant: "calendar",
|
||||||
onlyInstalled: true,
|
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";
|
||||||
|
|||||||
@@ -910,7 +910,7 @@ export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: Rerout
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
<DialogContent enableOverflow preventCloseOnOutsideClick>
|
<DialogContent preventCloseOnOutsideClick>
|
||||||
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
|
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
|
||||||
<RerouteDialogContentAndFooter
|
<RerouteDialogContentAndFooter
|
||||||
booking={teamEventTypeBooking}
|
booking={teamEventTypeBooking}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
// 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") {
|
||||||
|
Sentry.init({
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
|
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,
|
||||||
@@ -39,6 +40,11 @@ Sentry.init({
|
|||||||
};
|
};
|
||||||
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.NODE_ENV === "production") {
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
await import("./sentry.server.config");
|
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")}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table";
|
import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams, usePathname } from "next/navigation";
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
ZMultiSelectFilterValue,
|
ZMultiSelectFilterValue,
|
||||||
ZDateRangeFilterValue,
|
ZDateRangeFilterValue,
|
||||||
ZTextFilterValue,
|
ZTextFilterValue,
|
||||||
|
type SystemFilterSegment,
|
||||||
} from "@calcom/features/data-table";
|
} from "@calcom/features/data-table";
|
||||||
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -55,11 +56,45 @@ const descriptionByStatus: Record<BookingListingStatus, string> = {
|
|||||||
|
|
||||||
type BookingsProps = {
|
type BookingsProps = {
|
||||||
status: (typeof validStatuses)[number];
|
status: (typeof validStatuses)[number];
|
||||||
|
userId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function useSystemSegments(userId?: number) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const systemSegments: SystemFilterSegment[] = useMemo(() => {
|
||||||
|
if (!userId) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "my_bookings",
|
||||||
|
name: t("my_bookings"),
|
||||||
|
type: "system",
|
||||||
|
activeFilters: [
|
||||||
|
{
|
||||||
|
f: "userId",
|
||||||
|
v: {
|
||||||
|
type: ColumnFilterType.MULTI_SELECT,
|
||||||
|
data: [userId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
perPage: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [userId, t]);
|
||||||
|
|
||||||
|
return systemSegments;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Bookings(props: BookingsProps) {
|
export default function Bookings(props: BookingsProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const systemSegments = useSystemSegments(props.userId);
|
||||||
return (
|
return (
|
||||||
<DataTableProvider useSegments={useSegments}>
|
<DataTableProvider
|
||||||
|
useSegments={useSegments}
|
||||||
|
systemSegments={systemSegments}
|
||||||
|
tableIdentifier={pathname || undefined}>
|
||||||
<BookingsContent {...props} />
|
<BookingsContent {...props} />
|
||||||
</DataTableProvider>
|
</DataTableProvider>
|
||||||
);
|
);
|
||||||
@@ -180,7 +215,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,11 +228,15 @@ 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
|
||||||
|
? true
|
||||||
|
: await shouldHideBrandingForEvent({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
team: eventType.team,
|
team: eventType.team,
|
||||||
owner: eventType.users[0] ?? null,
|
owner: eventType.users[0] ?? null,
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ColumnFilterType,
|
ColumnFilterType,
|
||||||
type FilterableColumn,
|
type FilterableColumn,
|
||||||
} from "@calcom/features/data-table";
|
} from "@calcom/features/data-table";
|
||||||
|
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
||||||
import {
|
import {
|
||||||
AverageEventDurationChart,
|
AverageEventDurationChart,
|
||||||
BookingKPICards,
|
BookingKPICards,
|
||||||
@@ -31,7 +32,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
|
|
||||||
export default function InsightsPage({ timeZone }: { timeZone: string }) {
|
export default function InsightsPage({ timeZone }: { timeZone: string }) {
|
||||||
return (
|
return (
|
||||||
<DataTableProvider timeZone={timeZone}>
|
<DataTableProvider useSegments={useSegments} timeZone={timeZone}>
|
||||||
<InsightsOrgTeamsProvider>
|
<InsightsOrgTeamsProvider>
|
||||||
<InsightsPageContent />
|
<InsightsPageContent />
|
||||||
</InsightsOrgTeamsProvider>
|
</InsightsOrgTeamsProvider>
|
||||||
|
|||||||
+14
-2
@@ -170,7 +170,18 @@ const orgDomainMatcherConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = (phase) => {
|
||||||
|
if (isOrganizationsEnabled) {
|
||||||
|
// We want to log the phase here because it is important that the rewrite is added during the build phase(phase=phase-production-build)
|
||||||
|
console.log(
|
||||||
|
`[Phase: ${phase}] Adding rewrite config for organizations - orgHostPath: ${nextJsOrgRewriteConfig.orgHostPath}, orgSlug: ${nextJsOrgRewriteConfig.orgSlug}, disableRootPathRewrite: ${nextJsOrgRewriteConfig.disableRootPathRewrite}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`[Phase: ${phase}] Skipping rewrite config for organizations because ORGANIZATIONS_ENABLED is not set`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined,
|
output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined,
|
||||||
serverExternalPackages: [
|
serverExternalPackages: [
|
||||||
"deasync",
|
"deasync",
|
||||||
@@ -677,6 +688,7 @@ const nextConfig = {
|
|||||||
|
|
||||||
return redirects;
|
return redirects;
|
||||||
},
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
module.exports = (phase) => plugins.reduce((acc, next) => next(acc), nextConfig(phase));
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export async function applySelectFilter(page: Page, columnId: string, value: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function selectOptionValue(page: Page, columnId: string, value: string) {
|
export async function selectOptionValue(page: Page, columnId: string, value: string) {
|
||||||
|
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).waitFor();
|
||||||
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
|
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,3 +177,71 @@ export async function listSegments(page: Page): Promise<string[]> {
|
|||||||
export function locateSelectedSegmentName(page: Page, expectedName: string) {
|
export function locateSelectedSegmentName(page: Page, expectedName: string) {
|
||||||
return page.locator('[data-testid="filter-segment-select"]').filter({ hasText: expectedName });
|
return page.locator('[data-testid="filter-segment-select"]').filter({ hasText: expectedName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a system segment is visible in the dropdown
|
||||||
|
*/
|
||||||
|
export async function expectSystemSegmentVisible(page: Page, segmentName: string) {
|
||||||
|
await page.getByTestId("filter-segment-select").click();
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="filter-segment-select-content"]').getByText("Default")
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
|
||||||
|
.filter({ hasText: segmentName })
|
||||||
|
).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that no segment is currently selected
|
||||||
|
*/
|
||||||
|
export async function expectSegmentCleared(page: Page) {
|
||||||
|
// Check that no segment is selected (button shows default text)
|
||||||
|
const segmentSelect = page.getByTestId("filter-segment-select");
|
||||||
|
const buttonText = await segmentSelect.textContent();
|
||||||
|
expect(buttonText?.trim()).toEqual("Segment");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a specific segment is currently selected
|
||||||
|
*/
|
||||||
|
export async function expectSegmentSelected(page: Page, segmentName: string) {
|
||||||
|
await expect(locateSelectedSegmentName(page, segmentName)).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate an existing segment
|
||||||
|
*/
|
||||||
|
export async function duplicateSegment(page: Page, originalName: string, newName: string) {
|
||||||
|
await openSegmentSubmenu(page, originalName);
|
||||||
|
await page.getByTestId("filter-segment-select-submenu-content").getByText("Duplicate").click();
|
||||||
|
await page.getByTestId("duplicate-segment-name").fill(newName);
|
||||||
|
await page.getByRole("button", { name: "Duplicate" }).click();
|
||||||
|
await expect(page.getByText("Filter segment duplicated")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename an existing segment
|
||||||
|
*/
|
||||||
|
export async function renameSegment(page: Page, originalName: string, newName: string) {
|
||||||
|
await openSegmentSubmenu(page, originalName);
|
||||||
|
await page.getByTestId("filter-segment-select-submenu-content").getByText("Rename").click();
|
||||||
|
await page.getByTestId("rename-segment-name").fill(newName);
|
||||||
|
await page.getByRole("button", { name: "Save" }).click();
|
||||||
|
await expect(page.getByText("Filter segment updated")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a segment group (like "Default" or "Personal") is visible
|
||||||
|
*/
|
||||||
|
export async function expectSegmentGroupVisible(page: Page, groupName: string) {
|
||||||
|
await page.getByTestId("filter-segment-select").click();
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="filter-segment-select-content"]').getByText(groupName)
|
||||||
|
).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { expect } from "@playwright/test";
|
|||||||
import { randomString } from "@calcom/lib/random";
|
import { randomString } from "@calcom/lib/random";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { addFilter, openFilter, clearFilters } from "./filter-helpers";
|
import { clearFilters, applySelectFilter } from "./filter-helpers";
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
@@ -202,25 +202,11 @@ test.describe("Insights", async () => {
|
|||||||
await page.locator('[data-testid="org-teams-filter-item"]').nth(1).click();
|
await page.locator('[data-testid="org-teams-filter-item"]').nth(1).click();
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
// Choose User filter item from dropdown
|
await applySelectFilter(page, "userId", member.username || "");
|
||||||
await addFilter(page, "userId");
|
|
||||||
|
|
||||||
// Wait for the URL to include userId
|
|
||||||
await page.waitForURL((url) => url.toString().includes("userId"));
|
|
||||||
|
|
||||||
// Click User filter to see a user list
|
|
||||||
await openFilter(page, "userId");
|
|
||||||
|
|
||||||
await page.locator('[data-testid="select-filter-options-userId"]').getByRole("option").nth(0).click();
|
|
||||||
|
|
||||||
await page.locator('[data-testid="select-filter-options-userId"]').getByRole("option").nth(1).click();
|
|
||||||
|
|
||||||
// press escape button to close the filter
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
|
|
||||||
await clearFilters(page);
|
await clearFilters(page);
|
||||||
|
|
||||||
await expect(page.url()).not.toContain("userId");
|
await expect(page).not.toHaveURL(/[?&]userId=/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should test download button", async ({ page, users }) => {
|
test("should test download button", async ({ page, users }) => {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { addFilter, selectSegment, expectSegmentCleared, expectSegmentSelected } from "./filter-helpers";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
|
test.afterEach(async ({ users }) => {
|
||||||
|
await users.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to bookings page with specific status
|
||||||
|
*/
|
||||||
|
async function navigateToBookings(page: Page, status = "upcoming") {
|
||||||
|
// Wait for the bookings API response like existing tests do
|
||||||
|
const bookingsGetResponse = page.waitForResponse((response) =>
|
||||||
|
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
||||||
|
);
|
||||||
|
await page.goto(`/bookings/${status}`, { waitUntil: "domcontentloaded" });
|
||||||
|
await bookingsGetResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("System Segments", () => {
|
||||||
|
test.describe("Core Functionality", () => {
|
||||||
|
test("My Bookings system segment filters to current user's bookings only", async ({ page, users }) => {
|
||||||
|
const user1 = await users.create({ username: "user1" });
|
||||||
|
|
||||||
|
// Create some bookings for user (this would need booking setup)
|
||||||
|
// For now, we'll test the filter application
|
||||||
|
await user1.apiLogin();
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Verify the userId filter is applied (check URL or filter indicators)
|
||||||
|
const url = page.url();
|
||||||
|
expect(url).toContain("activeFilters");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("System segments show only Duplicate option in submenu", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
await page.getByTestId("filter-segment-select").click();
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
|
||||||
|
.filter({ hasText: "My Bookings" })
|
||||||
|
.locator('[data-testid="filter-segment-select-submenu-button"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const submenu = page.getByTestId("filter-segment-select-submenu-content");
|
||||||
|
await expect(submenu.getByText("Duplicate")).toBeVisible();
|
||||||
|
await expect(submenu.getByText("Rename")).toBeHidden();
|
||||||
|
await expect(submenu.getByText("Delete")).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("State Persistence", () => {
|
||||||
|
test("System segment selection persists across page visits", async ({ page, users }) => {
|
||||||
|
const owner = await users.create(undefined, { hasTeam: true });
|
||||||
|
await owner.apiLogin();
|
||||||
|
|
||||||
|
// Visit bookings page and select system segment
|
||||||
|
await navigateToBookings(page);
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Revisit the page - should preserve selection
|
||||||
|
await navigateToBookings(page);
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Cleared system segment state persists (no default selection after manual clear)", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const owner = await users.create(undefined, { hasTeam: true });
|
||||||
|
await owner.apiLogin();
|
||||||
|
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
// Select system segment
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Unselect system segment
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
|
||||||
|
// Revisit the page - should NOT have any segment selected
|
||||||
|
await navigateToBookings(page);
|
||||||
|
await expectSegmentCleared(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Auto-clear Behavior", () => {
|
||||||
|
test("Manual filter clears system segment selection", async ({ page, users }) => {
|
||||||
|
const user = await users.create(undefined, { hasTeam: true });
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
// Select system segment
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Apply manual filter - this should clear system segment
|
||||||
|
await addFilter(page, "eventTypeId");
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expectSegmentCleared(page);
|
||||||
|
|
||||||
|
// Verify filter is still applied
|
||||||
|
await expect(page.getByTestId("filter-popover-trigger-eventTypeId")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user